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 playground23h ago| #1 | import { motion } from "framer-motion"; |
| #2 | import { |
| #3 | CheckCircle2, |
| #4 | Clock, |
| #5 | AlertTriangle, |
| #6 | TrendingUp, |
| #7 | ArrowUpRight, |
| #8 | Layers, |
| #9 | ListTodo, |
| #10 | CalendarDays, |
| #11 | } from "lucide-react"; |
| #12 | import { useStore } from "../store"; |
| #13 | import { Avatar, ProgressBar } from "./ui"; |
| #14 | import { formatDistanceToNow } from "date-fns"; |
| #15 | |
| #16 | export function Dashboard() { |
| #17 | const { |
| #18 | issues, |
| #19 | sprints, |
| #20 | projects, |
| #21 | currentUser, |
| #22 | setActiveView, |
| #23 | setCurrentProject, |
| #24 | activities, |
| #25 | users, |
| #26 | } = useStore(); |
| #27 | |
| #28 | // Stats |
| #29 | const totalIssues = issues.length; |
| #30 | const completedIssues = issues.filter((i) => i.status === "done").length; |
| #31 | const inProgressIssues = issues.filter( |
| #32 | (i) => i.status === "in-progress" |
| #33 | ).length; |
| #34 | const overdueIssues = issues.filter( |
| #35 | (i) => |
| #36 | i.dueDate && |
| #37 | new Date(i.dueDate) < new Date() && |
| #38 | i.status !== "done" && |
| #39 | i.status !== "cancelled" |
| #40 | ).length; |
| #41 | |
| #42 | const activeSprint = sprints.find((s) => s.status === "active"); |
| #43 | const sprintIssues = activeSprint |
| #44 | ? issues.filter((i) => i.sprintId === activeSprint.id) |
| #45 | : []; |
| #46 | const sprintCompleted = sprintIssues.filter( |
| #47 | (i) => i.status === "done" |
| #48 | ).length; |
| #49 | |
| #50 | const recentActivities = activities.slice(0, 5); |
| #51 | |
| #52 | const stats = [ |
| #53 | { |
| #54 | label: "Total Issues", |
| #55 | value: totalIssues, |
| #56 | icon: Layers, |
| #57 | color: "text-accent", |
| #58 | bg: "bg-accent/10", |
| #59 | }, |
| #60 | { |
| #61 | label: "Completed", |
| #62 | value: completedIssues, |
| #63 | icon: CheckCircle2, |
| #64 | color: "text-emerald-400", |
| #65 | bg: "bg-emerald-500/10", |
| #66 | }, |
| #67 | { |
| #68 | label: "In Progress", |
| #69 | value: inProgressIssues, |
| #70 | icon: Clock, |
| #71 | color: "text-amber-400", |
| #72 | bg: "bg-amber-500/10", |
| #73 | }, |
| #74 | { |
| #75 | label: "Overdue", |
| #76 | value: overdueIssues, |
| #77 | icon: AlertTriangle, |
| #78 | color: "text-red-400", |
| #79 | bg: "bg-red-500/10", |
| #80 | }, |
| #81 | ]; |
| #82 | |
| #83 | const container = { |
| #84 | hidden: { opacity: 0 }, |
| #85 | show: { |
| #86 | opacity: 1, |
| #87 | transition: { staggerChildren: 0.05 }, |
| #88 | }, |
| #89 | }; |
| #90 | |
| #91 | const item = { |
| #92 | hidden: { opacity: 0, y: 10 }, |
| #93 | show: { opacity: 1, y: 0 }, |
| #94 | }; |
| #95 | |
| #96 | return ( |
| #97 | <motion.div |
| #98 | variants={container} |
| #99 | initial="hidden" |
| #100 | animate="show" |
| #101 | className="p-6 max-w-6xl mx-auto space-y-6" |
| #102 | > |
| #103 | {/* Welcome */} |
| #104 | <motion.div variants={item}> |
| #105 | <h1 className="text-2xl font-semibold text-zinc-100"> |
| #106 | Good {new Date().getHours() < 12 ? "morning" : new Date().getHours() < 18 ? "afternoon" : "evening"},{" "} |
| #107 | {currentUser?.name?.split(" ")[0]} |
| #108 | </h1> |
| #109 | <p className="text-sm text-zinc-500 mt-1"> |
| #110 | Here's what's happening across your projects |
| #111 | </p> |
| #112 | </motion.div> |
| #113 | |
| #114 | {/* Stats */} |
| #115 | <motion.div |
| #116 | variants={item} |
| #117 | className="grid grid-cols-2 lg:grid-cols-4 gap-3" |
| #118 | > |
| #119 | {stats.map((stat) => ( |
| #120 | <div |
| #121 | key={stat.label} |
| #122 | className="card flex items-start justify-between" |
| #123 | > |
| #124 | <div> |
| #125 | <p className="text-2xs font-medium text-zinc-500 uppercase tracking-wider"> |
| #126 | {stat.label} |
| #127 | </p> |
| #128 | <p className="text-2xl font-semibold text-zinc-100 mt-1"> |
| #129 | {stat.value} |
| #130 | </p> |
| #131 | </div> |
| #132 | <div className={`p-2 rounded-lg ${stat.bg}`}> |
| #133 | <stat.icon size={18} className={stat.color} /> |
| #134 | </div> |
| #135 | </div> |
| #136 | ))} |
| #137 | </motion.div> |
| #138 | |
| #139 | <div className="grid lg:grid-cols-3 gap-6"> |
| #140 | {/* Active Sprint */} |
| #141 | <motion.div variants={item} className="lg:col-span-2 card"> |
| #142 | <div className="flex items-center justify-between mb-4"> |
| #143 | <h2 className="text-sm font-semibold text-zinc-200"> |
| #144 | Active Sprint |
| #145 | </h2> |
| #146 | {activeSprint && ( |
| #147 | <button |
| #148 | onClick={() => { |
| #149 | setCurrentProject( |
| #150 | projects.find((p) => p.id === activeSprint.projectId)?.id || |
| #151 | projects[0].id |
| #152 | ); |
| #153 | setActiveView("sprints"); |
| #154 | }} |
| #155 | className="text-2xs text-accent hover:text-accent-hover flex items-center gap-1" |
| #156 | > |
| #157 | View sprint <ArrowUpRight size={12} /> |
| #158 | </button> |
| #159 | )} |
| #160 | </div> |
| #161 | |
| #162 | {activeSprint ? ( |
| #163 | <div> |
| #164 | <div className="flex items-center justify-between mb-2"> |
| #165 | <span className="text-sm text-zinc-300"> |
| #166 | {activeSprint.name} |
| #167 | </span> |
| #168 | <span className="text-2xs text-zinc-500"> |
| #169 | {sprintCompleted}/{sprintIssues.length} issues |
| #170 | </span> |
| #171 | </div> |
| #172 | <ProgressBar value={sprintCompleted} max={sprintIssues.length} /> |
| #173 | <div className="mt-4 space-y-2"> |
| #174 | {sprintIssues.slice(0, 5).map((issue) => ( |
| #175 | <div |
| #176 | key={issue.id} |
| #177 | className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-surface-3 transition-colors" |
| #178 | > |
| #179 | <div className="flex items-center gap-3 min-w-0"> |
| #180 | <span className="text-2xs font-mono text-zinc-500 shrink-0"> |
| #181 | {issue.identifier} |
| #182 | </span> |
| #183 | <span className="text-sm text-zinc-300 truncate"> |
| #184 | {issue.title} |
| #185 | </span> |
| #186 | </div> |
| #187 | <div className="flex items-center gap-2 shrink-0 ml-3"> |
| #188 | {issue.assigneeId && ( |
| #189 | <Avatar |
| #190 | name={ |
| #191 | users.find((u) => u.id === issue.assigneeId) |
| #192 | ?.name || "" |
| #193 | } |
| #194 | size="xs" |
| #195 | /> |
| #196 | )} |
| #197 | </div> |
| #198 | </div> |
| #199 | ))} |
| #200 | </div> |
| #201 | </div> |
| #202 | ) : ( |
| #203 | <div className="text-center py-8"> |
| #204 | <CalendarDays |
| #205 | size={32} |
| #206 | className="mx-auto text-zinc-600 mb-3" |
| #207 | /> |
| #208 | <p className="text-sm text-zinc-500">No active sprint</p> |
| #209 | <button |
| #210 | onClick={() => setActiveView("sprints")} |
| #211 | className="btn-ghost mt-3 text-sm" |
| #212 | > |
| #213 | <ListTodo size={14} /> |
| #214 | Go to Sprints |
| #215 | </button> |
| #216 | </div> |
| #217 | )} |
| #218 | </motion.div> |
| #219 | |
| #220 | {/* Recent Activity */} |
| #221 | <motion.div variants={item} className="card"> |
| #222 | <h2 className="text-sm font-semibold text-zinc-200 mb-4"> |
| #223 | Recent Activity |
| #224 | </h2> |
| #225 | <div className="space-y-3"> |
| #226 | {recentActivities.map((activity) => { |
| #227 | const user = users.find((u) => u.id === activity.userId); |
| #228 | const issue = issues.find((i) => i.id === activity.issueId); |
| #229 | return ( |
| #230 | <div key={activity.id} className="flex gap-3"> |
| #231 | <Avatar name={user?.name || ""} size="xs" className="mt-0.5" /> |
| #232 | <div className="min-w-0"> |
| #233 | <p className="text-sm text-zinc-300 leading-snug"> |
| #234 | <span className="font-medium text-zinc-200"> |
| #235 | {user?.name} |
| #236 | </span>{" "} |
| #237 | {activity.type === "status_change" && ( |
| #238 | <> |
| #239 | changed{" "} |
| #240 | <span className="font-mono text-2xs text-zinc-400"> |
| #241 | {issue?.identifier} |
| #242 | </span>{" "} |
| #243 | to {activity.newValue?.replace("-", " ")} |
| #244 | </> |
| #245 | )} |
| #246 | {activity.type === "assignment" && ( |
| #247 | <> |
| #248 | assigned{" "} |
| #249 | <span className="font-mono text-2xs text-zinc-400"> |
| #250 | {issue?.identifier} |
| #251 | </span> |
| #252 | </> |
| #253 | )} |
| #254 | {activity.type === "created" && ( |
| #255 | <> |
| #256 | created{" "} |
| #257 | <span className="font-mono text-2xs text-zinc-400"> |
| #258 | {issue?.identifier} |
| #259 | </span> |
| #260 | </> |
| #261 | )} |
| #262 | {activity.type === "comment" && ( |
| #263 | <> |
| #264 | commented on{" "} |
| #265 | <span className="font-mono text-2xs text-zinc-400"> |
| #266 | {issue?.identifier} |
| #267 | </span> |
| #268 | </> |
| #269 | )} |
| #270 | </p> |
| #271 | <p className="text-2xs text-zinc-600 mt-0.5"> |
| #272 | {formatDistanceToNow(new Date(activity.createdAt), { |
| #273 | addSuffix: true, |
| #274 | })} |
| #275 | </p> |
| #276 | </div> |
| #277 | </div> |
| #278 | ); |
| #279 | })} |
| #280 | </div> |
| #281 | </motion.div> |
| #282 | </div> |
| #283 | |
| #284 | {/* Projects */} |
| #285 | <motion.div variants={item}> |
| #286 | <div className="flex items-center justify-between mb-4"> |
| #287 | <h2 className="text-sm font-semibold text-zinc-200">Projects</h2> |
| #288 | <button |
| #289 | onClick={() => setActiveView("projects")} |
| #290 | className="text-2xs text-accent hover:text-accent-hover flex items-center gap-1" |
| #291 | > |
| #292 | View all <ArrowUpRight size={12} /> |
| #293 | </button> |
| #294 | </div> |
| #295 | <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3"> |
| #296 | {projects.map((project) => { |
| #297 | const projectIssues = issues.filter( |
| #298 | (i) => i.projectId === project.id |
| #299 | ); |
| #300 | const completed = projectIssues.filter( |
| #301 | (i) => i.status === "done" |
| #302 | ).length; |
| #303 | return ( |
| #304 | <button |
| #305 | key={project.id} |
| #306 | onClick={() => { |
| #307 | setCurrentProject(project.id); |
| #308 | setActiveView("board"); |
| #309 | }} |
| #310 | className="card text-left hover:border-border-strong transition-colors group" |
| #311 | > |
| #312 | <div className="flex items-center gap-3 mb-3"> |
| #313 | <span className="text-2xl">{project.emoji}</span> |
| #314 | <div className="min-w-0"> |
| #315 | <h3 className="text-sm font-medium text-zinc-200 group-hover:text-zinc-100 truncate"> |
| #316 | {project.name} |
| #317 | </h3> |
| #318 | <p className="text-2xs text-zinc-500"> |
| #319 | {projectIssues.length} issues |
| #320 | </p> |
| #321 | </div> |
| #322 | </div> |
| #323 | <ProgressBar |
| #324 | value={completed} |
| #325 | max={projectIssues.length} |
| #326 | color={`bg-[${project.color}]`} |
| #327 | /> |
| #328 | </button> |
| #329 | ); |
| #330 | })} |
| #331 | </div> |
| #332 | </motion.div> |
| #333 | |
| #334 | {/* Trending */} |
| #335 | <motion.div variants={item} className="card"> |
| #336 | <div className="flex items-center gap-2 mb-4"> |
| #337 | <TrendingUp size={16} className="text-emerald-400" /> |
| #338 | <h2 className="text-sm font-semibold text-zinc-200">Velocity</h2> |
| #339 | </div> |
| #340 | <div className="flex items-end gap-1 h-24"> |
| #341 | {[3, 5, 4, 7, 6, 8, 5, 9, 7, 11, 8, 12].map((v, i) => ( |
| #342 | <motion.div |
| #343 | key={i} |
| #344 | initial={{ height: 0 }} |
| #345 | animate={{ height: `${(v / 12) * 100}%` }} |
| #346 | transition={{ delay: i * 0.05, duration: 0.4 }} |
| #347 | className="flex-1 bg-accent/20 rounded-t-sm hover:bg-accent/40 transition-colors" |
| #348 | /> |
| #349 | ))} |
| #350 | </div> |
| #351 | <div className="flex justify-between mt-2"> |
| #352 | <span className="text-2xs text-zinc-600">12 weeks ago</span> |
| #353 | <span className="text-2xs text-zinc-600">This week</span> |
| #354 | </div> |
| #355 | </motion.div> |
| #356 | </motion.div> |
| #357 | ); |
| #358 | } |
| #359 |