repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Projectflow
stars
latest
clone command
git clone gitlawb://did:key:z6Mkfh4Y...QBEi/projectflowgit clone gitlawb://did:key:z6Mkfh4Y.../projectflowb3cded1async from playground1d ago| #1 | import { useState, useEffect } from "react"; |
| #2 | import { motion, AnimatePresence } from "framer-motion"; |
| #3 | import { |
| #4 | Search, |
| #5 | Bell, |
| #6 | Plus, |
| #7 | Settings, |
| #8 | LogOut, |
| #9 | Command, |
| #10 | Sun, |
| #11 | Moon, |
| #12 | Monitor, |
| #13 | } from "lucide-react"; |
| #14 | import clsx from "clsx"; |
| #15 | import { useStore } from "../../application/stores"; |
| #16 | import { Avatar, Tooltip } from "../ui"; |
| #17 | |
| #18 | export function Topbar() { |
| #19 | const { |
| #20 | setSearchOpen, |
| #21 | currentUser, |
| #22 | logout, |
| #23 | notifications, |
| #24 | setActiveView, |
| #25 | currentProject, |
| #26 | activeView, |
| #27 | markAllNotificationsRead, |
| #28 | theme, |
| #29 | setTheme, |
| #30 | } = useStore(); |
| #31 | |
| #32 | const [showNotifications, setShowNotifications] = useState(false); |
| #33 | const [showProfile, setShowProfile] = useState(false); |
| #34 | const unreadCount = notifications.filter((n) => !n.read).length; |
| #35 | |
| #36 | useEffect(() => { |
| #37 | const handleClick = () => { |
| #38 | setShowNotifications(false); |
| #39 | setShowProfile(false); |
| #40 | }; |
| #41 | if (showNotifications || showProfile) { |
| #42 | window.addEventListener("click", handleClick); |
| #43 | return () => window.removeEventListener("click", handleClick); |
| #44 | } |
| #45 | }, [showNotifications, showProfile]); |
| #46 | |
| #47 | const getPageTitle = () => { |
| #48 | const titles: Record<string, string> = { |
| #49 | dashboard: "Dashboard", |
| #50 | projects: "Projects", |
| #51 | board: "Board", |
| #52 | backlog: "Backlog", |
| #53 | sprints: "Sprints", |
| #54 | analytics: "Analytics", |
| #55 | members: "Members", |
| #56 | settings: "Settings", |
| #57 | }; |
| #58 | return titles[activeView] || "Dashboard"; |
| #59 | }; |
| #60 | |
| #61 | return ( |
| #62 | <header className="h-14 flex items-center justify-between px-5 border-b border-border bg-surface-1/80 backdrop-blur-sm shrink-0"> |
| #63 | <div className="flex items-center gap-3"> |
| #64 | <h1 className="text-sm font-semibold text-zinc-200"> |
| #65 | {currentProject && activeView !== "dashboard" && activeView !== "projects" |
| #66 | ? `${currentProject.emoji} ${currentProject.name}` |
| #67 | : getPageTitle()} |
| #68 | </h1> |
| #69 | {currentProject && activeView !== "dashboard" && activeView !== "projects" && ( |
| #70 | <span className="text-zinc-600">/</span> |
| #71 | )} |
| #72 | {currentProject && activeView !== "dashboard" && activeView !== "projects" && ( |
| #73 | <span className="text-sm text-zinc-400">{getPageTitle()}</span> |
| #74 | )} |
| #75 | </div> |
| #76 | |
| #77 | <div className="flex items-center gap-2"> |
| #78 | <button |
| #79 | onClick={() => setSearchOpen(true)} |
| #80 | className="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-border bg-surface-2 text-zinc-400 hover:text-zinc-200 hover:border-border-strong transition-colors text-sm" |
| #81 | > |
| #82 | <Search size={14} /> |
| #83 | <span className="hidden sm:inline">Search...</span> |
| #84 | <div className="flex items-center gap-0.5 ml-2"> |
| #85 | <kbd className="kbd"> |
| #86 | <Command size={10} /> |
| #87 | </kbd> |
| #88 | <kbd className="kbd">K</kbd> |
| #89 | </div> |
| #90 | </button> |
| #91 | |
| #92 | <Tooltip |
| #93 | content={ |
| #94 | theme === "system" |
| #95 | ? "Theme: System" |
| #96 | : theme === "light" |
| #97 | ? "Theme: Light" |
| #98 | : "Theme: Dark" |
| #99 | } |
| #100 | > |
| #101 | <button |
| #102 | onClick={() => { |
| #103 | const order = ["system", "light", "dark"] as const; |
| #104 | const next = order[(order.indexOf(theme) + 1) % order.length]; |
| #105 | setTheme(next); |
| #106 | }} |
| #107 | className="p-2 rounded-lg text-zinc-400 hover:text-zinc-200 hover:bg-surface-3 transition-colors" |
| #108 | > |
| #109 | {theme === "system" ? ( |
| #110 | <Monitor size={18} /> |
| #111 | ) : theme === "light" ? ( |
| #112 | <Sun size={18} /> |
| #113 | ) : ( |
| #114 | <Moon size={18} /> |
| #115 | )} |
| #116 | </button> |
| #117 | </Tooltip> |
| #118 | |
| #119 | <Tooltip content="New issue"> |
| #120 | <button |
| #121 | onClick={() => setActiveView("board")} |
| #122 | className="p-2 rounded-lg text-zinc-400 hover:text-zinc-200 hover:bg-surface-3 transition-colors" |
| #123 | > |
| #124 | <Plus size={18} /> |
| #125 | </button> |
| #126 | </Tooltip> |
| #127 | |
| #128 | <div className="relative"> |
| #129 | <Tooltip content="Notifications"> |
| #130 | <button |
| #131 | onClick={(e) => { |
| #132 | e.stopPropagation(); |
| #133 | setShowNotifications(!showNotifications); |
| #134 | setShowProfile(false); |
| #135 | }} |
| #136 | className="p-2 rounded-lg text-zinc-400 hover:text-zinc-200 hover:bg-surface-3 transition-colors relative" |
| #137 | > |
| #138 | <Bell size={18} /> |
| #139 | {unreadCount > 0 && ( |
| #140 | <span className="absolute -top-0.5 -right-0.5 w-4 h-4 rounded-full bg-accent text-[10px] font-medium text-white flex items-center justify-center"> |
| #141 | {unreadCount} |
| #142 | </span> |
| #143 | )} |
| #144 | </button> |
| #145 | </Tooltip> |
| #146 | |
| #147 | <AnimatePresence> |
| #148 | {showNotifications && ( |
| #149 | <motion.div |
| #150 | initial={{ opacity: 0, y: -4, scale: 0.98 }} |
| #151 | animate={{ opacity: 1, y: 0, scale: 1 }} |
| #152 | exit={{ opacity: 0, y: -4, scale: 0.98 }} |
| #153 | transition={{ duration: 0.1 }} |
| #154 | onClick={(e) => e.stopPropagation()} |
| #155 | className="absolute right-0 top-full mt-2 w-80 rounded-xl border border-border bg-surface-2 shadow-xl shadow-black/30 z-50 overflow-hidden" |
| #156 | > |
| #157 | <div className="flex items-center justify-between px-4 py-3 border-b border-border"> |
| #158 | <span className="text-sm font-medium text-zinc-200"> |
| #159 | Notifications |
| #160 | </span> |
| #161 | {unreadCount > 0 && ( |
| #162 | <button |
| #163 | onClick={() => markAllNotificationsRead()} |
| #164 | className="text-2xs text-accent hover:text-accent-hover" |
| #165 | > |
| #166 | Mark all read |
| #167 | </button> |
| #168 | )} |
| #169 | </div> |
| #170 | <div className="max-h-80 overflow-y-auto"> |
| #171 | {notifications.length === 0 ? ( |
| #172 | <div className="p-4 text-center text-sm text-zinc-500"> |
| #173 | No notifications |
| #174 | </div> |
| #175 | ) : ( |
| #176 | notifications.slice(0, 10).map((n) => ( |
| #177 | <div |
| #178 | key={n.id} |
| #179 | className={clsx( |
| #180 | "px-4 py-3 border-b border-border-subtle hover:bg-surface-3 transition-colors cursor-pointer", |
| #181 | !n.read && "bg-accent/5" |
| #182 | )} |
| #183 | > |
| #184 | <p className="text-sm text-zinc-200 leading-snug"> |
| #185 | {n.message} |
| #186 | </p> |
| #187 | <p className="text-2xs text-zinc-500 mt-1"> |
| #188 | {new Date(n.createdAt).toLocaleDateString()} |
| #189 | </p> |
| #190 | </div> |
| #191 | )) |
| #192 | )} |
| #193 | </div> |
| #194 | </motion.div> |
| #195 | )} |
| #196 | </AnimatePresence> |
| #197 | </div> |
| #198 | |
| #199 | <div className="relative"> |
| #200 | <button |
| #201 | onClick={(e) => { |
| #202 | e.stopPropagation(); |
| #203 | setShowProfile(!showProfile); |
| #204 | setShowNotifications(false); |
| #205 | }} |
| #206 | className="flex items-center gap-2 p-1 rounded-lg hover:bg-surface-3 transition-colors" |
| #207 | > |
| #208 | <Avatar |
| #209 | name={currentUser?.name || "User"} |
| #210 | size="sm" |
| #211 | /> |
| #212 | </button> |
| #213 | |
| #214 | <AnimatePresence> |
| #215 | {showProfile && ( |
| #216 | <motion.div |
| #217 | initial={{ opacity: 0, y: -4, scale: 0.98 }} |
| #218 | animate={{ opacity: 1, y: 0, scale: 1 }} |
| #219 | exit={{ opacity: 0, y: -4, scale: 0.98 }} |
| #220 | transition={{ duration: 0.1 }} |
| #221 | onClick={(e) => e.stopPropagation()} |
| #222 | className="absolute right-0 top-full mt-2 w-56 rounded-xl border border-border bg-surface-2 shadow-xl shadow-black/30 z-50 overflow-hidden" |
| #223 | > |
| #224 | <div className="px-4 py-3 border-b border-border"> |
| #225 | <p className="text-sm font-medium text-zinc-200"> |
| #226 | {currentUser?.name} |
| #227 | </p> |
| #228 | <p className="text-2xs text-zinc-500">{currentUser?.email}</p> |
| #229 | </div> |
| #230 | <div className="py-1"> |
| #231 | <button |
| #232 | onClick={() => { |
| #233 | setActiveView("settings"); |
| #234 | setShowProfile(false); |
| #235 | }} |
| #236 | className="w-full flex items-center gap-2 px-4 py-2 text-sm text-zinc-300 hover:bg-surface-3 hover:text-zinc-100 transition-colors" |
| #237 | > |
| #238 | <Settings size={14} /> |
| #239 | Settings |
| #240 | </button> |
| #241 | <button |
| #242 | onClick={() => { |
| #243 | logout(); |
| #244 | setShowProfile(false); |
| #245 | }} |
| #246 | className="w-full flex items-center gap-2 px-4 py-2 text-sm text-red-400 hover:bg-red-500/10 transition-colors" |
| #247 | > |
| #248 | <LogOut size={14} /> |
| #249 | Log out |
| #250 | </button> |
| #251 | </div> |
| #252 | </motion.div> |
| #253 | )} |
| #254 | </AnimatePresence> |
| #255 | </div> |
| #256 | </div> |
| #257 | </header> |
| #258 | ); |
| #259 | } |
| #260 |