Mirror of
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fill emptyness on project id page (merge)

deploy

+409 -584
+59 -72
src/pages/projects/[id].tsx
··· 4 4 Activity, 5 5 Archive, 6 6 ArrowUpRight, 7 + Calculator, 7 8 CalendarCog, 8 9 CalendarMinus, 9 10 CalendarPlus, ··· 15 16 Package2, 16 17 Plus, 17 18 Search, 19 + Timer, 18 20 Users, 19 21 Workflow, 20 22 } from "lucide-react"; ··· 24 26 25 27 import Task from "@/models/task"; 26 28 27 - import { calcPriorityComparison, calcStatusComparison } from "@/utils/projectUtils"; 29 + import { 30 + msToTime, 31 + msToTimeDaysOrSecondsLong, 32 + msToTimeFitting, 33 + msToTimeFittingLong, 34 + msToTimeHours, 35 + } from "@/utils/dateUtils"; 36 + import { calcPriorityComparison, calcStatusComparison, getProjectDuration } from "@/utils/projectUtils"; 28 37 import { saveData } from "@/utils/save"; 29 38 import { getMostRecentSessionDateOfTask } from "@/utils/taskUtils"; 30 39 ··· 237 246 </Card> 238 247 </div> 239 248 <div className="grid gap-4 md:gap-8 lg:grid-cols-2 xl:grid-cols-3"> 240 - <Card className="xl:col-span-2" x-chunk="dashboard-01-chunk-4"> 249 + <Card className="xl:col-span-2 row-span-3 max-lg:order-1" x-chunk="dashboard-01-chunk-4"> 241 250 <CardHeader className="flex flex-row items-center gap-4"> 242 251 <div className="grid gap-2"> 243 252 <CardTitle>Tasks</CardTitle> ··· 309 318 </Table> 310 319 </CardContent> 311 320 </Card> 312 - {/* <Card x-chunk="dashboard-01-chunk-5"> 313 - <CardHeader> 314 - <CardTitle>Recent Sales</CardTitle> 321 + <Card x-chunk="dashboard-01-chunk-0"> 322 + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> 323 + <CardTitle className="text-sm font-medium text-primary">Duration</CardTitle> 324 + <Timer className="h-4 w-4 text-muted-foreground" /> 315 325 </CardHeader> 316 - <CardContent className="grid gap-8"> 317 - <div className="flex items-center gap-4"> 318 - <Avatar className="hidden h-9 w-9 sm:flex"> 319 - <AvatarImage src="/avatars/01.png" alt="Avatar" /> 320 - <AvatarFallback>OM</AvatarFallback> 321 - </Avatar> 322 - <div className="grid gap-1"> 323 - <p className="text-sm font-medium leading-none"> 324 - Olivia Martin 325 - </p> 326 - <p className="text-sm text-muted-foreground"> 327 - olivia.martin@email.com 328 - </p> 329 - </div> 330 - <div className="ml-auto font-medium">+$1,999.00</div> 331 - </div> 332 - <div className="flex items-center gap-4"> 333 - <Avatar className="hidden h-9 w-9 sm:flex"> 334 - <AvatarImage src="/avatars/02.png" alt="Avatar" /> 335 - <AvatarFallback>JL</AvatarFallback> 336 - </Avatar> 337 - <div className="grid gap-1"> 338 - <p className="text-sm font-medium leading-none">Jackson Lee</p> 339 - <p className="text-sm text-muted-foreground"> 340 - jackson.lee@email.com 341 - </p> 342 - </div> 343 - <div className="ml-auto font-medium">+$39.00</div> 344 - </div> 345 - <div className="flex items-center gap-4"> 346 - <Avatar className="hidden h-9 w-9 sm:flex"> 347 - <AvatarImage src="/avatars/03.png" alt="Avatar" /> 348 - <AvatarFallback>IN</AvatarFallback> 349 - </Avatar> 350 - <div className="grid gap-1"> 351 - <p className="text-sm font-medium leading-none"> 352 - Isabella Nguyen 353 - </p> 354 - <p className="text-sm text-muted-foreground"> 355 - isabella.nguyen@email.com 356 - </p> 357 - </div> 358 - <div className="ml-auto font-medium">+$299.00</div> 359 - </div> 360 - <div className="flex items-center gap-4"> 361 - <Avatar className="hidden h-9 w-9 sm:flex"> 362 - <AvatarImage src="/avatars/04.png" alt="Avatar" /> 363 - <AvatarFallback>WK</AvatarFallback> 364 - </Avatar> 365 - <div className="grid gap-1"> 366 - <p className="text-sm font-medium leading-none">William Kim</p> 367 - <p className="text-sm text-muted-foreground">will@email.com</p> 368 - </div> 369 - <div className="ml-auto font-medium">+$99.00</div> 326 + <CardContent> 327 + <div className="text-2xl font-bold">{msToTime(getProjectDuration(project))}</div> 328 + <p className="text-xs text-muted-foreground"> 329 + You could also say around {msToTimeDaysOrSecondsLong(getProjectDuration(project))} 330 + </p> 331 + </CardContent> 332 + </Card> 333 + <Card x-chunk="dashboard-01-chunk-0"> 334 + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> 335 + <CardTitle className="text-sm font-medium text-primary">Task Count</CardTitle> 336 + <Calculator className="h-4 w-4 text-muted-foreground" /> 337 + </CardHeader> 338 + <CardContent> 339 + <div className="text-2xl font-bold"> 340 + {project.tasks.length} task{project.tasks.length !== 1 && "s"} 370 341 </div> 371 - <div className="flex items-center gap-4"> 372 - <Avatar className="hidden h-9 w-9 sm:flex"> 373 - <AvatarImage src="/avatars/05.png" alt="Avatar" /> 374 - <AvatarFallback>SD</AvatarFallback> 375 - </Avatar> 376 - <div className="grid gap-1"> 377 - <p className="text-sm font-medium leading-none">Sofia Davis</p> 378 - <p className="text-sm text-muted-foreground"> 379 - sofia.davis@email.com 380 - </p> 381 - </div> 382 - <div className="ml-auto font-medium">+$39.00</div> 342 + <p className="text-xs text-muted-foreground"> 343 + {project.tasks.filter((task) => task.priority === "high").length} task 344 + {project.tasks.filter((task) => task.priority === "high").length !== 1 && "s"} with high 345 + priority 346 + </p> 347 + </CardContent> 348 + </Card> 349 + <Card x-chunk="dashboard-01-chunk-0"> 350 + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> 351 + <CardTitle className="text-sm font-medium text-primary">Session Count</CardTitle> 352 + <Calculator className="h-4 w-4 text-muted-foreground" /> 353 + </CardHeader> 354 + <CardContent> 355 + <div className="text-2xl font-bold"> 356 + {project.tasks.flatMap((task) => task.sessions).length} session 357 + {project.tasks.flatMap((task) => task.sessions).length !== 1 && "s"} 383 358 </div> 359 + <p className="text-xs text-muted-foreground"> 360 + { 361 + project.tasks 362 + .flatMap((task) => task.sessions) 363 + .filter((session) => session.flow === "smooth").length 364 + }{" "} 365 + session 366 + {project.tasks 367 + .flatMap((task) => task.sessions) 368 + .filter((session) => session.flow === "smooth").length !== 1 && "s"}{" "} 369 + with smooth flow 370 + </p> 384 371 </CardContent> 385 - </Card> */} 372 + </Card> 386 373 </div> 387 374 </main> 388 375 </div>
+138 -140
src/pages/sessions/[id]/edit.tsx
··· 133 133 return ( 134 134 <div className="flex w-full flex-col"> 135 135 <main className="flex min-h-[calc(100vh-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10"> 136 - <div className="grid flex-1 auto-rows-max gap-4"> 137 - <div className="flex items-center gap-4 overflow-hidden min-h-9"> 136 + <div className="flex items-center gap-4 overflow-hidden min-h-9"> 137 + <Link href={`/sessions/${session.id}`} className="text-muted-foreground"> 138 + <Button variant="outline" size="icon" className="h-7 w-7"> 139 + <ChevronLeft className="h-4 w-4" /> 140 + <span className="sr-only">Back</span> 141 + </Button> 142 + </Link> 143 + <h1 className="text-xl font-semibold tracking-tight truncate"> 144 + {msToTimeFitting( 145 + session.end 146 + ? new Date(session.end!).getTime() - new Date(session.start).getTime() 147 + : Date.now() - new Date(session.start).getTime(), 148 + ) + " session"} 149 + </h1> 150 + {session.flow && ( 151 + <Badge variant="outline" className="ml-auto sm:ml-0 py-2 bg-background"> 152 + <FlowIconLabel flowValue={session.flow} className="text-muted-foreground" /> 153 + </Badge> 154 + )} 155 + <div className="flex items-center gap-2 md:ml-auto max-md:hidden"> 156 + <Dialog open={deleteDialogIsOpen} onOpenChange={setDeleteDialogIsOpen}> 157 + <DialogTrigger asChild> 158 + <Button variant={"destructive"} size={"sm"}> 159 + Delete 160 + </Button> 161 + </DialogTrigger> 162 + <DialogContent> 163 + <DialogHeader> 164 + <DialogTitle>Are you absolutely sure?</DialogTitle> 165 + <DialogDescription> 166 + This action cannot be undone. This will permanently delete this session and 167 + remove this data from your local storage. 168 + </DialogDescription> 169 + </DialogHeader> 170 + <DialogFooter> 171 + <DialogClose> 172 + <Button variant={"outline"}>Cancel</Button> 173 + </DialogClose> 174 + <Button onClick={handleDeleteSession}>Continue</Button> 175 + </DialogFooter> 176 + </DialogContent> 177 + </Dialog> 138 178 <Link href={`/sessions/${session.id}`} className="text-muted-foreground"> 139 - <Button variant="outline" size="icon" className="h-7 w-7"> 140 - <ChevronLeft className="h-4 w-4" /> 141 - <span className="sr-only">Back</span> 179 + <Button variant="outline" size="sm"> 180 + Discard 142 181 </Button> 143 182 </Link> 144 - <h1 className="text-xl font-semibold tracking-tight truncate"> 145 - {msToTimeFitting( 146 - session.end 147 - ? new Date(session.end!).getTime() - new Date(session.start).getTime() 148 - : Date.now() - new Date(session.start).getTime(), 149 - ) + " session"} 150 - </h1> 151 - {session.flow && ( 152 - <Badge variant="outline" className="ml-auto sm:ml-0 py-2 bg-background"> 153 - <FlowIconLabel flowValue={session.flow} className="text-muted-foreground" /> 154 - </Badge> 155 - )} 156 - <div className="flex items-center gap-2 md:ml-auto max-md:hidden"> 157 - <Dialog open={deleteDialogIsOpen} onOpenChange={setDeleteDialogIsOpen}> 158 - <DialogTrigger asChild> 159 - <Button variant={"destructive"} size={"sm"}> 160 - Delete 161 - </Button> 162 - </DialogTrigger> 163 - <DialogContent> 164 - <DialogHeader> 165 - <DialogTitle>Are you absolutely sure?</DialogTitle> 166 - <DialogDescription> 167 - This action cannot be undone. This will permanently delete this session and 168 - remove this data from your local storage. 169 - </DialogDescription> 170 - </DialogHeader> 171 - <DialogFooter> 172 - <DialogClose> 173 - <Button variant={"outline"}>Cancel</Button> 174 - </DialogClose> 175 - <Button onClick={handleDeleteSession}>Continue</Button> 176 - </DialogFooter> 177 - </DialogContent> 178 - </Dialog> 179 - <Link href={`/sessions/${session.id}`} className="text-muted-foreground"> 180 - <Button variant="outline" size="sm"> 181 - Discard 182 - </Button> 183 - </Link> 184 - <Button size="sm" onClick={handleSaveSession}> 185 - Save Session 186 - </Button> 187 - </div> 183 + <Button size="sm" onClick={handleSaveSession}> 184 + Save Session 185 + </Button> 188 186 </div> 189 - <div className="grid gap-4 md:grid-cols-[1fr_250px] lg:grid-cols-3 lg:gap-8"> 190 - <div className="grid auto-rows-max items-start gap-4 lg:col-span-2 lg:gap-8"> 191 - <Card x-chunk="dashboard-07-chunk-0"> 192 - <CardHeader> 193 - <CardTitle>Session Details</CardTitle> 194 - <CardDescription>Edit the description of the session.</CardDescription> 195 - </CardHeader> 196 - <CardContent> 197 - <div className="grid gap-6"> 198 - <div className="grid gap-3"> 199 - <Label htmlFor="name">Identifier</Label> 200 - <Input id="id" type="text" className="w-full" value={session.id} disabled /> 201 - </div> 202 - <div className="grid gap-3"> 203 - <Label htmlFor="description">Description</Label> 204 - <Textarea 205 - id="description" 206 - value={session.description} 207 - onChange={(e) => handleInputChange("description", e.target.value)} 208 - className="min-h-36" 209 - /> 210 - </div> 187 + </div> 188 + <div className="grid gap-4 md:grid-cols-[1fr_250px] lg:grid-cols-3 lg:gap-8"> 189 + <div className="grid auto-rows-max items-start gap-4 lg:col-span-2 lg:gap-8"> 190 + <Card x-chunk="dashboard-07-chunk-0"> 191 + <CardHeader> 192 + <CardTitle>Session Details</CardTitle> 193 + <CardDescription>Edit the description of the session.</CardDescription> 194 + </CardHeader> 195 + <CardContent> 196 + <div className="grid gap-6"> 197 + <div className="grid gap-3"> 198 + <Label htmlFor="name">Identifier</Label> 199 + <Input id="id" type="text" className="w-full" value={session.id} disabled /> 211 200 </div> 212 - </CardContent> 213 - </Card> 214 - </div> 215 - <div className="grid auto-rows-max items-start gap-4 lg:gap-8"> 216 - <Card x-chunk="dashboard-07-chunk-3"> 217 - <CardHeader> 218 - <CardTitle> 219 - <div className="flex items-center justify-between"> 220 - <div>Session Flow</div> 221 - {/* <HoverCard> 201 + <div className="grid gap-3"> 202 + <Label htmlFor="description">Description</Label> 203 + <Textarea 204 + id="description" 205 + value={session.description} 206 + onChange={(e) => handleInputChange("description", e.target.value)} 207 + className="min-h-36" 208 + /> 209 + </div> 210 + </div> 211 + </CardContent> 212 + </Card> 213 + </div> 214 + <div className="grid auto-rows-max items-start gap-4 lg:gap-8"> 215 + <Card x-chunk="dashboard-07-chunk-3"> 216 + <CardHeader> 217 + <CardTitle> 218 + <div className="flex items-center justify-between"> 219 + <div>Session Flow</div> 220 + {/* <HoverCard> 222 221 <HoverCardTrigger asChild> 223 222 <BadgeInfo className="h-5 w-5 text-primary" /> 224 223 </HoverCardTrigger> ··· 251 250 </div> 252 251 </HoverCardContent> 253 252 </HoverCard> */} 254 - </div> 255 - </CardTitle> 256 - </CardHeader> 257 - <CardContent> 258 - <div className="grid gap-6"> 259 - <div className="grid gap-3"> 260 - <Label htmlFor="flow">Flow</Label> 261 - <Select 262 - value={sessionFlow} 263 - onValueChange={(value) => { 264 - setSessionFlow(value); 265 - handleInputChange("flow", value); 266 - }} 267 - > 268 - <SelectTrigger id="flow" aria-label="Select flow"> 269 - <SelectValue placeholder="Select flow" /> 270 - </SelectTrigger> 271 - <SelectContent> 272 - {flows.map((flow) => ( 273 - <SelectItem key={flow.value} value={flow.value}> 274 - <FlowIconLabel flowValue={flow.value} /> 275 - </SelectItem> 276 - ))} 277 - </SelectContent> 278 - </Select> 279 - </div> 253 + </div> 254 + </CardTitle> 255 + </CardHeader> 256 + <CardContent> 257 + <div className="grid gap-6"> 258 + <div className="grid gap-3"> 259 + <Label htmlFor="flow">Flow</Label> 260 + <Select 261 + value={sessionFlow} 262 + onValueChange={(value) => { 263 + setSessionFlow(value); 264 + handleInputChange("flow", value); 265 + }} 266 + > 267 + <SelectTrigger id="flow" aria-label="Select flow"> 268 + <SelectValue placeholder="Select flow" /> 269 + </SelectTrigger> 270 + <SelectContent> 271 + {flows.map((flow) => ( 272 + <SelectItem key={flow.value} value={flow.value}> 273 + <FlowIconLabel flowValue={flow.value} /> 274 + </SelectItem> 275 + ))} 276 + </SelectContent> 277 + </Select> 280 278 </div> 281 - </CardContent> 282 - </Card> 283 - </div> 279 + </div> 280 + </CardContent> 281 + </Card> 284 282 </div> 285 - <div className="flex items-center justify-end gap-2 md:hidden"> 286 - <Dialog open={deleteDialogMobileIsOpen} onOpenChange={setDeleteDialogMobileIsOpen}> 287 - <DialogTrigger asChild> 288 - <Button variant={"destructive"} size={"sm"}> 289 - Delete 290 - </Button> 291 - </DialogTrigger> 292 - <DialogContent> 293 - <DialogHeader> 294 - <DialogTitle>Are you absolutely sure?</DialogTitle> 295 - <DialogDescription> 296 - This action cannot be undone. This will permanently delete this session and 297 - remove this data from your local storage. 298 - </DialogDescription> 299 - </DialogHeader> 300 - <DialogFooter> 301 - <DialogClose> 302 - <Button variant={"outline"}>Cancel</Button> 303 - </DialogClose> 304 - <Button onClick={handleDeleteSession}>Continue</Button> 305 - </DialogFooter> 306 - </DialogContent> 307 - </Dialog> 308 - <Link href={`/sessions/${session.id}`} className="text-muted-foreground"> 309 - <Button variant="outline" size="sm"> 310 - Discard 283 + </div> 284 + <div className="flex items-center justify-end gap-2 md:hidden"> 285 + <Dialog open={deleteDialogMobileIsOpen} onOpenChange={setDeleteDialogMobileIsOpen}> 286 + <DialogTrigger asChild> 287 + <Button variant={"destructive"} size={"sm"}> 288 + Delete 311 289 </Button> 312 - </Link> 313 - <Button size="sm" onClick={handleSaveSession}> 314 - Save Session 290 + </DialogTrigger> 291 + <DialogContent> 292 + <DialogHeader> 293 + <DialogTitle>Are you absolutely sure?</DialogTitle> 294 + <DialogDescription> 295 + This action cannot be undone. This will permanently delete this session and remove 296 + this data from your local storage. 297 + </DialogDescription> 298 + </DialogHeader> 299 + <DialogFooter> 300 + <DialogClose> 301 + <Button variant={"outline"}>Cancel</Button> 302 + </DialogClose> 303 + <Button onClick={handleDeleteSession}>Continue</Button> 304 + </DialogFooter> 305 + </DialogContent> 306 + </Dialog> 307 + <Link href={`/sessions/${session.id}`} className="text-muted-foreground"> 308 + <Button variant="outline" size="sm"> 309 + Discard 315 310 </Button> 316 - </div> 311 + </Link> 312 + <Button size="sm" onClick={handleSaveSession}> 313 + Save Session 314 + </Button> 317 315 </div> 318 316 </main> 319 317 </div>
+182 -184
src/pages/tasks/[id]/edit.tsx
··· 127 127 return ( 128 128 <div className="flex w-full flex-col"> 129 129 <main className="flex min-h-[calc(100vh-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10"> 130 - <div className="grid flex-1 auto-rows-max gap-4"> 131 - <div className="flex items-center gap-4 overflow-hidden min-h-9"> 132 - <Link href={`/tasks/${task.id}`} className="text-muted-foreground"> 133 - <Button variant="outline" size="icon" className="h-7 w-7"> 134 - <ChevronLeft className="h-4 w-4" /> 135 - <span className="sr-only">Back</span> 136 - </Button> 137 - </Link> 138 - <h1 className="text-xl font-semibold tracking-tight truncate">{task.name}</h1> 139 - {task.priority && ( 140 - <Badge variant="outline" className="ml-auto sm:ml-0 py-2 bg-background max-xs:hidden"> 141 - <PriorityIconLabel priorityValue={task.priority} className="text-muted-foreground" /> 142 - </Badge> 143 - )} 144 - {task.status && ( 145 - <Badge variant="outline" className="ml-auto sm:ml-0 py-2 bg-background max-sm:hidden"> 146 - <StatusIconLabel statusValue={task.status} className="text-muted-foreground" /> 147 - </Badge> 148 - )} 149 - <div className="flex items-center gap-2 md:ml-auto max-md:hidden"> 150 - <Dialog open={deleteDialogIsOpen} onOpenChange={setDeleteDialogIsOpen}> 151 - <DialogTrigger asChild> 152 - <Button variant={"destructive"} size={"sm"}> 153 - Delete 154 - </Button> 155 - </DialogTrigger> 156 - <DialogContent> 157 - <DialogHeader> 158 - <DialogTitle>Are you absolutely sure?</DialogTitle> 159 - <DialogDescription> 160 - This action cannot be undone. This will permanently delete your task{" "} 161 - {task.name} and remove this data from your local storage. 162 - </DialogDescription> 163 - </DialogHeader> 164 - <DialogFooter> 165 - <DialogClose> 166 - <Button variant={"outline"}>Cancel</Button> 167 - </DialogClose> 168 - <Button onClick={handleDeleteTask}>Continue</Button> 169 - </DialogFooter> 170 - </DialogContent> 171 - </Dialog> 172 - <Link href={`/tasks/${task.id}`} className="text-muted-foreground"> 173 - <Button variant="outline" size="sm"> 174 - Discard 175 - </Button> 176 - </Link> 177 - <Button size="sm" onClick={handleSaveTask}> 178 - Save Task 179 - </Button> 180 - </div> 181 - </div> 182 - <div className="grid gap-4 md:grid-cols-[1fr_250px] lg:grid-cols-3 lg:gap-8"> 183 - <div className="grid auto-rows-max items-start gap-4 lg:col-span-2 lg:gap-8"> 184 - <Card x-chunk="dashboard-07-chunk-0"> 185 - <CardHeader> 186 - <CardTitle>Task Details</CardTitle> 187 - <CardDescription>Edit the name and description of the task.</CardDescription> 188 - </CardHeader> 189 - <CardContent> 190 - <div className="grid gap-6"> 191 - <div className="grid gap-3"> 192 - <Label htmlFor="name">Identifier</Label> 193 - <Input id="id" type="text" className="w-full" value={task.id} disabled /> 194 - </div> 195 - <div className="grid gap-3"> 196 - <Label htmlFor="name">Name</Label> 197 - <Input 198 - id="name" 199 - type="text" 200 - className="w-full" 201 - value={task.name} 202 - onChange={(e) => handleInputChange("name", e.target.value)} 203 - /> 204 - </div> 205 - <div className="grid gap-3"> 206 - <Label htmlFor="description">Description</Label> 207 - <Textarea 208 - id="description" 209 - value={task.description} 210 - onChange={(e) => handleInputChange("description", e.target.value)} 211 - className="min-h-36" 212 - /> 213 - </div> 214 - </div> 215 - </CardContent> 216 - </Card> 217 - </div> 218 - <div className="grid auto-rows-max items-start gap-4 lg:gap-8"> 219 - <Card x-chunk="dashboard-07-chunk-3"> 220 - <CardHeader> 221 - <CardTitle> 222 - <div className="flex items-center justify-between"> 223 - <div>Task Status</div> 224 - <HoverCard> 225 - <HoverCardTrigger asChild> 226 - <BadgeInfo className="h-5 w-5 text-primary" /> 227 - </HoverCardTrigger> 228 - <HoverCardContent className="w-80" align="end"> 229 - <div className="flex justify-between space-x-4"> 230 - <Avatar> 231 - <AvatarImage src="https://github.com/trueberryless.png" /> 232 - <AvatarFallback>T</AvatarFallback> 233 - </Avatar> 234 - <div className="space-y-1"> 235 - <h4 className="text-sm font-semibold">@trueberryless</h4> 236 - <p className="text-sm"> 237 - We try to automate this status in order to help you 238 - focus on your projects, not this app. 239 - </p> 240 - <div className="flex items-center pt-2"> 241 - <span className="text-xs text-muted-foreground"> 242 - For example we will automatically move this task 243 - from “Backlog” to “In Progress”, when you start 244 - working on it - when the first session is started. 245 - </span> 246 - </div> 247 - </div> 248 - </div> 249 - </HoverCardContent> 250 - </HoverCard> 251 - </div> 252 - </CardTitle> 253 - </CardHeader> 254 - <CardContent> 255 - <div className="grid gap-6"> 256 - <div className="grid gap-3"> 257 - <Label htmlFor="status">Status</Label> 258 - <Select 259 - value={taskStatus} 260 - onValueChange={(value) => { 261 - setTaskStatus(value); 262 - handleInputChange("status", value); 263 - }} 264 - > 265 - <SelectTrigger id="status" aria-label="Select status"> 266 - <SelectValue placeholder="Select status" /> 267 - </SelectTrigger> 268 - <SelectContent> 269 - {statuses.map((status) => ( 270 - <SelectItem key={status.value} value={status.value}> 271 - <StatusIconLabel statusValue={status.value} /> 272 - </SelectItem> 273 - ))} 274 - </SelectContent> 275 - </Select> 276 - </div> 277 - </div> 278 - </CardContent> 279 - </Card> 280 - <Card x-chunk="dashboard-07-chunk-3"> 281 - <CardHeader> 282 - <CardTitle>Task Priority</CardTitle> 283 - </CardHeader> 284 - <CardContent> 285 - <div className="grid gap-6"> 286 - <div className="grid gap-3"> 287 - <Label htmlFor="priority">Priority</Label> 288 - <Select 289 - value={taskPriority} 290 - onValueChange={(value) => { 291 - setTaskPriority(value); 292 - handleInputChange("priority", value); 293 - }} 294 - > 295 - <SelectTrigger id="priority" aria-label="Select priority"> 296 - <SelectValue placeholder="Select priority" /> 297 - </SelectTrigger> 298 - <SelectContent> 299 - {priorities.map((priority) => ( 300 - <SelectItem key={priority.value} value={priority.value}> 301 - <PriorityIconLabel priorityValue={priority.value} /> 302 - </SelectItem> 303 - ))} 304 - </SelectContent> 305 - </Select> 306 - </div> 307 - </div> 308 - </CardContent> 309 - </Card> 310 - </div> 311 - </div> 312 - <div className="flex items-center justify-end gap-2 md:hidden"> 313 - <Dialog open={deleteDialogMobileIsOpen} onOpenChange={setDeleteDialogMobileIsOpen}> 130 + <div className="flex items-center gap-4 overflow-hidden min-h-9"> 131 + <Link href={`/tasks/${task.id}`} className="text-muted-foreground"> 132 + <Button variant="outline" size="icon" className="h-7 w-7"> 133 + <ChevronLeft className="h-4 w-4" /> 134 + <span className="sr-only">Back</span> 135 + </Button> 136 + </Link> 137 + <h1 className="text-xl font-semibold tracking-tight truncate">{task.name}</h1> 138 + {task.priority && ( 139 + <Badge variant="outline" className="ml-auto sm:ml-0 py-2 bg-background max-xs:hidden"> 140 + <PriorityIconLabel priorityValue={task.priority} className="text-muted-foreground" /> 141 + </Badge> 142 + )} 143 + {task.status && ( 144 + <Badge variant="outline" className="ml-auto sm:ml-0 py-2 bg-background max-sm:hidden"> 145 + <StatusIconLabel statusValue={task.status} className="text-muted-foreground" /> 146 + </Badge> 147 + )} 148 + <div className="flex items-center gap-2 md:ml-auto max-md:hidden"> 149 + <Dialog open={deleteDialogIsOpen} onOpenChange={setDeleteDialogIsOpen}> 314 150 <DialogTrigger asChild> 315 151 <Button variant={"destructive"} size={"sm"}> 316 152 Delete ··· 341 177 Save Task 342 178 </Button> 343 179 </div> 180 + </div> 181 + <div className="grid gap-4 md:grid-cols-[1fr_250px] lg:grid-cols-3 lg:gap-8"> 182 + <div className="grid auto-rows-max items-start gap-4 lg:col-span-2 lg:gap-8"> 183 + <Card x-chunk="dashboard-07-chunk-0"> 184 + <CardHeader> 185 + <CardTitle>Task Details</CardTitle> 186 + <CardDescription>Edit the name and description of the task.</CardDescription> 187 + </CardHeader> 188 + <CardContent> 189 + <div className="grid gap-6"> 190 + <div className="grid gap-3"> 191 + <Label htmlFor="name">Identifier</Label> 192 + <Input id="id" type="text" className="w-full" value={task.id} disabled /> 193 + </div> 194 + <div className="grid gap-3"> 195 + <Label htmlFor="name">Name</Label> 196 + <Input 197 + id="name" 198 + type="text" 199 + className="w-full" 200 + value={task.name} 201 + onChange={(e) => handleInputChange("name", e.target.value)} 202 + /> 203 + </div> 204 + <div className="grid gap-3"> 205 + <Label htmlFor="description">Description</Label> 206 + <Textarea 207 + id="description" 208 + value={task.description} 209 + onChange={(e) => handleInputChange("description", e.target.value)} 210 + className="min-h-36" 211 + /> 212 + </div> 213 + </div> 214 + </CardContent> 215 + </Card> 216 + </div> 217 + <div className="grid auto-rows-max items-start gap-4 lg:gap-8"> 218 + <Card x-chunk="dashboard-07-chunk-3"> 219 + <CardHeader> 220 + <CardTitle> 221 + <div className="flex items-center justify-between"> 222 + <div>Task Status</div> 223 + <HoverCard> 224 + <HoverCardTrigger asChild> 225 + <BadgeInfo className="h-5 w-5 text-primary" /> 226 + </HoverCardTrigger> 227 + <HoverCardContent className="w-80" align="end"> 228 + <div className="flex justify-between space-x-4"> 229 + <Avatar> 230 + <AvatarImage src="https://github.com/trueberryless.png" /> 231 + <AvatarFallback>T</AvatarFallback> 232 + </Avatar> 233 + <div className="space-y-1"> 234 + <h4 className="text-sm font-semibold">@trueberryless</h4> 235 + <p className="text-sm"> 236 + We try to automate this status in order to help you focus on 237 + your projects, not this app. 238 + </p> 239 + <div className="flex items-center pt-2"> 240 + <span className="text-xs text-muted-foreground"> 241 + For example we will automatically move this task from 242 + “Backlog” to “In Progress”, when you start working on it 243 + - when the first session is started. 244 + </span> 245 + </div> 246 + </div> 247 + </div> 248 + </HoverCardContent> 249 + </HoverCard> 250 + </div> 251 + </CardTitle> 252 + </CardHeader> 253 + <CardContent> 254 + <div className="grid gap-6"> 255 + <div className="grid gap-3"> 256 + <Label htmlFor="status">Status</Label> 257 + <Select 258 + value={taskStatus} 259 + onValueChange={(value) => { 260 + setTaskStatus(value); 261 + handleInputChange("status", value); 262 + }} 263 + > 264 + <SelectTrigger id="status" aria-label="Select status"> 265 + <SelectValue placeholder="Select status" /> 266 + </SelectTrigger> 267 + <SelectContent> 268 + {statuses.map((status) => ( 269 + <SelectItem key={status.value} value={status.value}> 270 + <StatusIconLabel statusValue={status.value} /> 271 + </SelectItem> 272 + ))} 273 + </SelectContent> 274 + </Select> 275 + </div> 276 + </div> 277 + </CardContent> 278 + </Card> 279 + <Card x-chunk="dashboard-07-chunk-3"> 280 + <CardHeader> 281 + <CardTitle>Task Priority</CardTitle> 282 + </CardHeader> 283 + <CardContent> 284 + <div className="grid gap-6"> 285 + <div className="grid gap-3"> 286 + <Label htmlFor="priority">Priority</Label> 287 + <Select 288 + value={taskPriority} 289 + onValueChange={(value) => { 290 + setTaskPriority(value); 291 + handleInputChange("priority", value); 292 + }} 293 + > 294 + <SelectTrigger id="priority" aria-label="Select priority"> 295 + <SelectValue placeholder="Select priority" /> 296 + </SelectTrigger> 297 + <SelectContent> 298 + {priorities.map((priority) => ( 299 + <SelectItem key={priority.value} value={priority.value}> 300 + <PriorityIconLabel priorityValue={priority.value} /> 301 + </SelectItem> 302 + ))} 303 + </SelectContent> 304 + </Select> 305 + </div> 306 + </div> 307 + </CardContent> 308 + </Card> 309 + </div> 310 + </div> 311 + <div className="flex items-center justify-end gap-2 md:hidden"> 312 + <Dialog open={deleteDialogMobileIsOpen} onOpenChange={setDeleteDialogMobileIsOpen}> 313 + <DialogTrigger asChild> 314 + <Button variant={"destructive"} size={"sm"}> 315 + Delete 316 + </Button> 317 + </DialogTrigger> 318 + <DialogContent> 319 + <DialogHeader> 320 + <DialogTitle>Are you absolutely sure?</DialogTitle> 321 + <DialogDescription> 322 + This action cannot be undone. This will permanently delete your task {task.name} and 323 + remove this data from your local storage. 324 + </DialogDescription> 325 + </DialogHeader> 326 + <DialogFooter> 327 + <DialogClose> 328 + <Button variant={"outline"}>Cancel</Button> 329 + </DialogClose> 330 + <Button onClick={handleDeleteTask}>Continue</Button> 331 + </DialogFooter> 332 + </DialogContent> 333 + </Dialog> 334 + <Link href={`/tasks/${task.id}`} className="text-muted-foreground"> 335 + <Button variant="outline" size="sm"> 336 + Discard 337 + </Button> 338 + </Link> 339 + <Button size="sm" onClick={handleSaveTask}> 340 + Save Task 341 + </Button> 344 342 </div> 345 343 </main> 346 344 </div>
-188
src/pages/tasks/[id]/sessions.tsx
··· 1 - import { Project, Session, Task } from "@/models"; 2 - import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; 3 - import { AlertCircle } from "lucide-react"; 4 - import Link from "next/link"; 5 - import router from "next/router"; 6 - import { Key, useEffect, useState } from "react"; 7 - 8 - import { formatDateTime, formatDateToDistanceFromNow } from "@/utils/dateUtils"; 9 - import { saveData } from "@/utils/save"; 10 - 11 - import { useUser } from "@/components/UserContext"; 12 - import { columnsLg, columnsMd, columnsMobile, columnsSm, columnsXl } from "@/components/tasks/columns"; 13 - import { DataTable } from "@/components/tasks/data-table"; 14 - import StartStopButton from "@/components/tasks/start-stop-button"; 15 - 16 - import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 17 - import { Button } from "@/components/ui/button"; 18 - import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 19 - import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; 20 - import { Input } from "@/components/ui/input"; 21 - import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 22 - 23 - export default function Tasks() { 24 - const { user, setUser } = useUser(); 25 - const [forceUpdate, setForceUpdate] = useState(0); 26 - 27 - useEffect(() => { 28 - const interval = setInterval(() => { 29 - setForceUpdate((prev) => prev + 1); 30 - }, 1000); 31 - 32 - return () => clearInterval(interval); 33 - }, []); 34 - 35 - if (!user) { 36 - return ( 37 - <div className="flex w-full flex-col"> 38 - <main className="flex min-h-[calc(100vh-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10"> 39 - <Alert variant="default"> 40 - <AlertCircle className="h-4 w-4" /> 41 - <AlertTitle>Loading...</AlertTitle> 42 - <AlertDescription> 43 - We are currently trying to fetch your data from your local storage. 44 - </AlertDescription> 45 - </Alert> 46 - </main> 47 - </div> 48 - ); 49 - } 50 - 51 - const foundTask = user?.projects.flatMap((project) => project.tasks).find((task) => task.id === router.query.id); 52 - 53 - const task = foundTask 54 - ? { 55 - ...foundTask, 56 - projectName: 57 - user?.projects.find((project) => project.tasks.some((t: { id: any }) => t.id === foundTask.id)) 58 - ?.name || "Project Not Found", 59 - projectId: 60 - user?.projects.find((project) => project.tasks.some((t: { id: any }) => t.id === foundTask.id))?.id || 61 - undefined, 62 - projectIsArchived: 63 - user?.projects.find((project) => project.tasks.some((t: { id: any }) => t.id === foundTask.id)) 64 - ?.archivedAt !== null, 65 - } 66 - : undefined; 67 - 68 - if (!task) { 69 - return ( 70 - <div className="flex w-full flex-col"> 71 - <main className="flex min-h-[calc(100vh-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10"> 72 - <Alert variant="destructive"> 73 - <AlertCircle className="h-4 w-4" /> 74 - <AlertTitle>Error</AlertTitle> 75 - <AlertDescription>Unfortunately, this task does not exist.</AlertDescription> 76 - </Alert> 77 - </main> 78 - </div> 79 - ); 80 - } 81 - 82 - const handleSessionChange = (taskId: string, newSession: Session) => { 83 - if (user) { 84 - const updatedProjects = user.projects.map((project) => { 85 - const updatedTasks = project.tasks.map((task: Task) => { 86 - if (task.id === taskId) { 87 - if (newSession.end === null) { 88 - // Start a new Session 89 - return { 90 - ...task, 91 - sessions: [...task.sessions, newSession], 92 - }; 93 - } else { 94 - // Stop the current Session 95 - const updatedSessions = task.sessions.map((session) => 96 - session.end ? session : { ...session, end: new Date() }, 97 - ); 98 - return { 99 - ...task, 100 - sessions: updatedSessions, 101 - }; 102 - } 103 - } 104 - return task; 105 - }); 106 - 107 - return { 108 - ...project, 109 - tasks: updatedTasks, 110 - }; 111 - }); 112 - 113 - const updatedUser = { 114 - ...user, 115 - projects: updatedProjects, 116 - }; 117 - 118 - setUser(updatedUser); 119 - saveData(updatedUser); 120 - } 121 - }; 122 - 123 - return ( 124 - <div className="flex w-full flex-col"> 125 - <main className="flex min-h-[calc(100vh-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10"> 126 - <Card> 127 - <CardHeader> 128 - <div className="flex items-center justify-between"> 129 - <div> 130 - Sessions of {task.name.toLowerCase().includes("task") ? task.name : "Task " + task.name} 131 - </div> 132 - {!task.projectIsArchived && ( 133 - <StartStopButton 134 - key={task.id} 135 - task={task} 136 - onSessionChange={handleSessionChange} 137 - ></StartStopButton> 138 - )} 139 - </div> 140 - </CardHeader> 141 - <Table> 142 - <TableHeader> 143 - <TableRow> 144 - <TableHead>Start</TableHead> 145 - <TableHead className="text-right">End</TableHead> 146 - </TableRow> 147 - </TableHeader> 148 - <TableBody> 149 - {task.sessions 150 - .sort( 151 - (a: Session, b: Session) => 152 - new Date(b.start).getTime() - new Date(a.start).getTime(), 153 - ) 154 - .map((session: Session, i: Key | null | undefined) => { 155 - return ( 156 - <TableRow 157 - key={i} 158 - // onClick={() => router.push(`/tasks/${task.id}`)} 159 - // className="cursor-pointer" 160 - > 161 - <TableCell> 162 - <div className="font-medium"> 163 - {formatDateTime(new Date(session.start))} 164 - </div> 165 - <div className="hidden text-sm text-muted-foreground md:inline"> 166 - {formatDateToDistanceFromNow(new Date(session.start))} 167 - </div> 168 - </TableCell> 169 - <TableCell className="text-right"> 170 - <div className="font-medium"> 171 - {session.end ? formatDateTime(new Date(session.end)) : "Active"} 172 - </div> 173 - <div className="hidden text-sm text-muted-foreground md:inline"> 174 - {session.end 175 - ? formatDateToDistanceFromNow(new Date(session.end)) 176 - : ""} 177 - </div> 178 - </TableCell> 179 - </TableRow> 180 - ); 181 - })} 182 - </TableBody> 183 - </Table> 184 - </Card> 185 - </main> 186 - </div> 187 - ); 188 - }
+30
src/utils/dateUtils.ts
··· 145 145 return `${seconds}s`; 146 146 }; 147 147 148 + export const msToTimeFittingLong = (duration: number) => { 149 + const seconds = Math.floor(duration / 1000), 150 + minutes = Math.floor(duration / (1000 * 60)), 151 + hours = Math.floor(duration / (1000 * 60 * 60)), 152 + days = Math.floor(duration / (1000 * 60 * 60 * 24)); 153 + 154 + if (days > 0) { 155 + return `${days} day${days !== 1 ? "s" : ""}`; 156 + } 157 + if (hours > 0) { 158 + return `${hours} hour${hours !== 1 ? "s" : ""}`; 159 + } 160 + if (minutes > 0) { 161 + return `${minutes} minute${minutes !== 1 ? "s" : ""}`; 162 + } 163 + return `${seconds} second${seconds !== 1 ? "s" : ""}`; 164 + }; 165 + 166 + export const msToTimeDaysOrSecondsLong = (duration: number) => { 167 + const seconds = Math.floor(duration / 1000), 168 + minutes = Math.floor(duration / (1000 * 60)), 169 + hours = Math.floor(duration / (1000 * 60 * 60)), 170 + days = Math.floor(duration / (1000 * 60 * 60 * 24)); 171 + 172 + if (days > 0) { 173 + return `${days} day${days !== 1 ? "s" : ""}`; 174 + } 175 + return `${seconds} second${seconds !== 1 ? "s" : ""}`; 176 + }; 177 + 148 178 /** 149 179 * regular expression to check for valid hour format (01-23) 150 180 */