-
-
Save shricodev/583d31c69823cb772c7a7188c4b89f73 to your computer and use it in GitHub Desktop.
Admin Dashboard - Kombai 2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| From 30ce9fd0eca9e7c8c86ed0ffefae666b402892c2 Mon Sep 17 00:00:00 2001 | |
| From: Shrijal Acharya <shrijal.acharya@gmail.com> | |
| Date: Sun, 29 Mar 2026 12:56:46 +0545 | |
| Subject: [PATCH 2/2] kombai 2 | |
| --- | |
| .../kanban/components/board-column.tsx | 73 +++++++- | |
| .../kanban/components/new-task-dialog.tsx | 136 ++++++++++++--- | |
| src/features/kanban/components/task-card.tsx | 159 +++++++++++++++--- | |
| src/features/kanban/utils/store.ts | 66 ++++++-- | |
| 4 files changed, 362 insertions(+), 72 deletions(-) | |
| diff --git a/src/features/kanban/components/board-column.tsx b/src/features/kanban/components/board-column.tsx | |
| index 4a695f9..7b8b21b 100644 | |
| --- a/src/features/kanban/components/board-column.tsx | |
| +++ b/src/features/kanban/components/board-column.tsx | |
| @@ -1,43 +1,98 @@ | |
| 'use client'; | |
| import { Icons } from '@/components/icons'; | |
| -import { Badge } from '@/components/ui/badge'; | |
| import { Button } from '@/components/ui/button'; | |
| import { KanbanColumn, KanbanColumnHandle } from '@/components/ui/kanban'; | |
| +import { cn } from '@/lib/utils'; | |
| import type { Task } from '../utils/store'; | |
| import { TaskCard } from './task-card'; | |
| +// ─── Column metadata ────────────────────────────────────────────────────────── | |
| + | |
| const COLUMN_TITLES: Record<string, string> = { | |
| backlog: 'Backlog', | |
| inProgress: 'In Progress', | |
| - review: 'Review', | |
| + review: 'In Review', | |
| done: 'Done' | |
| }; | |
| +const COLUMN_DOT: Record<string, string> = { | |
| + backlog: 'bg-slate-400', | |
| + inProgress: 'bg-blue-400', | |
| + review: 'bg-amber-400', | |
| + done: 'bg-emerald-400' | |
| +}; | |
| + | |
| +// ─── Component ──────────────────────────────────────────────────────────────── | |
| + | |
| interface TaskColumnProps extends Omit<React.ComponentProps<typeof KanbanColumn>, 'children'> { | |
| tasks: Task[]; | |
| } | |
| export function TaskColumn({ value, tasks, ...props }: TaskColumnProps) { | |
| + const title = COLUMN_TITLES[value] ?? value; | |
| + const dotColor = COLUMN_DOT[value] ?? 'bg-muted-foreground'; | |
| + | |
| return ( | |
| - <KanbanColumn value={value} className='w-[320px] shrink-0' {...props}> | |
| - <div className='flex items-center justify-between'> | |
| + <KanbanColumn | |
| + value={value} | |
| + className={cn( | |
| + 'group w-[300px] shrink-0', | |
| + 'rounded border border-border', | |
| + 'bg-muted/50 dark:bg-zinc-900/60', | |
| + // Override KanbanColumn defaults | |
| + 'gap-0 p-0', | |
| + // Keep size-full width override respected | |
| + 'h-auto' | |
| + )} | |
| + {...props} | |
| + > | |
| + {/* ── Column header ─────────────────────────────────────────────────── */} | |
| + <div className='flex items-center justify-between px-4 py-3.5'> | |
| <div className='flex items-center gap-2'> | |
| - <span className='text-sm font-semibold'>{COLUMN_TITLES[value] ?? value}</span> | |
| - <Badge variant='secondary' className='pointer-events-none rounded-sm'> | |
| + {/* Status dot */} | |
| + <span className={cn('h-2 w-2 shrink-0 rounded-full', dotColor)} /> | |
| + | |
| + {/* Column title */} | |
| + <span className='text-foreground text-[15px] font-semibold tracking-tight'>{title}</span> | |
| + | |
| + {/* Task count */} | |
| + <span className='bg-muted text-muted-foreground ml-0.5 flex h-5 min-w-5 items-center justify-center rounded px-1.5 text-[11px] font-medium'> | |
| {tasks.length} | |
| - </Badge> | |
| + </span> | |
| </div> | |
| + | |
| + {/* Drag handle — visible on column hover */} | |
| <KanbanColumnHandle asChild> | |
| - <Button variant='ghost' size='icon'> | |
| + <Button | |
| + variant='ghost' | |
| + size='icon' | |
| + className={cn( | |
| + 'text-muted-foreground h-7 w-7', | |
| + 'opacity-0 transition-opacity duration-150 group-hover:opacity-100' | |
| + )} | |
| + aria-label={`Drag ${title} column`} | |
| + > | |
| <Icons.gripVertical className='h-4 w-4' /> | |
| </Button> | |
| </KanbanColumnHandle> | |
| </div> | |
| - <div className='flex flex-col gap-2 p-0.5'> | |
| + | |
| + {/* ── Divider ───────────────────────────────────────────────────────── */} | |
| + <div className='bg-border mx-4 h-px' /> | |
| + | |
| + {/* ── Cards ─────────────────────────────────────────────────────────── */} | |
| + <div className='flex flex-col gap-3 p-4'> | |
| {tasks.map((task) => ( | |
| <TaskCard key={task.id} task={task} asHandle /> | |
| ))} | |
| + | |
| + {/* Empty state */} | |
| + {tasks.length === 0 && ( | |
| + <div className='border-border text-muted-foreground flex flex-col items-center justify-center rounded border border-dashed py-10 text-sm'> | |
| + <p>No tasks yet</p> | |
| + </div> | |
| + )} | |
| </div> | |
| </KanbanColumn> | |
| ); | |
| diff --git a/src/features/kanban/components/new-task-dialog.tsx b/src/features/kanban/components/new-task-dialog.tsx | |
| index 3e51425..aeb0107 100644 | |
| --- a/src/features/kanban/components/new-task-dialog.tsx | |
| +++ b/src/features/kanban/components/new-task-dialog.tsx | |
| @@ -1,5 +1,7 @@ | |
| 'use client'; | |
| +import { useState } from 'react'; | |
| +import { Icons } from '@/components/icons'; | |
| import { Button } from '@/components/ui/button'; | |
| import { | |
| Dialog, | |
| @@ -11,54 +13,146 @@ import { | |
| DialogTrigger | |
| } from '@/components/ui/dialog'; | |
| import { Input } from '@/components/ui/input'; | |
| +import { Label } from '@/components/ui/label'; | |
| import { Textarea } from '@/components/ui/textarea'; | |
| +import { cn } from '@/lib/utils'; | |
| import { useTaskStore } from '../utils/store'; | |
| +// ─── Tag config (mirrors task-card for consistency) ─────────────────────────── | |
| + | |
| +const PREDEFINED_TAGS = [ | |
| + 'mobile', | |
| + 'web', | |
| + 'frontend', | |
| + 'backend', | |
| + 'design', | |
| + 'api', | |
| + 'testing' | |
| +] as const; | |
| + | |
| +const TAG_ACTIVE_COLORS: Record<string, string> = { | |
| + mobile: 'bg-pink-500 text-white border-pink-500', | |
| + web: 'bg-violet-600 text-white border-violet-600', | |
| + design: 'bg-blue-500 text-white border-blue-500', | |
| + api: 'bg-amber-500 text-white border-amber-500', | |
| + backend: 'bg-orange-600 text-white border-orange-600', | |
| + frontend: 'bg-cyan-600 text-white border-cyan-600', | |
| + testing: 'bg-slate-500 text-white border-slate-500' | |
| +}; | |
| + | |
| +// ─── Component ──────────────────────────────────────────────────────────────── | |
| + | |
| export default function NewTaskDialog() { | |
| const addTask = useTaskStore((state) => state.addTask); | |
| + const [open, setOpen] = useState(false); | |
| + const [selectedTags, setSelectedTags] = useState<string[]>([]); | |
| - const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { | |
| + function toggleTag(tag: string) { | |
| + setSelectedTags((prev) => | |
| + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] | |
| + ); | |
| + } | |
| + | |
| + function handleSubmit(e: React.FormEvent<HTMLFormElement>) { | |
| e.preventDefault(); | |
| const form = e.currentTarget; | |
| const formData = new FormData(form); | |
| const { title, description } = Object.fromEntries(formData); | |
| - if (typeof title !== 'string' || typeof description !== 'string') return; | |
| - addTask(title, description); | |
| - }; | |
| + if (typeof title !== 'string' || !title.trim()) return; | |
| + | |
| + addTask( | |
| + title.trim(), | |
| + typeof description === 'string' && description.trim() ? description.trim() : undefined, | |
| + selectedTags.length > 0 ? [...selectedTags] : undefined | |
| + ); | |
| + | |
| + form.reset(); | |
| + setSelectedTags([]); | |
| + setOpen(false); | |
| + } | |
| + | |
| + function handleOpenChange(next: boolean) { | |
| + setOpen(next); | |
| + if (!next) setSelectedTags([]); | |
| + } | |
| return ( | |
| - <Dialog> | |
| + <Dialog open={open} onOpenChange={handleOpenChange}> | |
| <DialogTrigger asChild> | |
| - <Button variant='secondary' size='sm'> | |
| - + Add New Task | |
| + <Button size='sm'> | |
| + <Icons.add className='mr-1.5 h-4 w-4' /> | |
| + Add Task | |
| </Button> | |
| </DialogTrigger> | |
| - <DialogContent className='sm:max-w-[425px]'> | |
| + | |
| + <DialogContent className='sm:max-w-[440px]'> | |
| <DialogHeader> | |
| - <DialogTitle>Add New Task</DialogTitle> | |
| - <DialogDescription>What do you want to get done today?</DialogDescription> | |
| + <DialogTitle>New Task</DialogTitle> | |
| + <DialogDescription>Add a new task to the backlog.</DialogDescription> | |
| </DialogHeader> | |
| - <form id='task-form' className='grid gap-4 py-4' onSubmit={handleSubmit}> | |
| - <div className='grid grid-cols-4 items-center gap-4'> | |
| - <Input id='title' name='title' placeholder='Task title...' className='col-span-4' /> | |
| + | |
| + <form id='task-form' className='flex flex-col gap-4 py-2' onSubmit={handleSubmit}> | |
| + {/* Title */} | |
| + <div className='flex flex-col gap-1.5'> | |
| + <Label htmlFor='title'>Title</Label> | |
| + <Input | |
| + id='title' | |
| + name='title' | |
| + placeholder='What needs to be done?' | |
| + required | |
| + autoComplete='off' | |
| + /> | |
| </div> | |
| - <div className='grid grid-cols-4 items-center gap-4'> | |
| + | |
| + {/* Description */} | |
| + <div className='flex flex-col gap-1.5'> | |
| + <Label htmlFor='description'>Description</Label> | |
| <Textarea | |
| id='description' | |
| name='description' | |
| - placeholder='Description...' | |
| - className='col-span-4' | |
| + placeholder='Add details (optional)...' | |
| + rows={3} | |
| + className='resize-none' | |
| /> | |
| </div> | |
| + | |
| + {/* Tags */} | |
| + <div className='flex flex-col gap-2'> | |
| + <Label>Tags</Label> | |
| + <div className='flex flex-wrap gap-2'> | |
| + {PREDEFINED_TAGS.map((tag) => { | |
| + const isSelected = selectedTags.includes(tag); | |
| + return ( | |
| + <button | |
| + type='button' | |
| + key={tag} | |
| + onClick={() => toggleTag(tag)} | |
| + aria-pressed={isSelected} | |
| + className={cn( | |
| + 'rounded-full border px-3 py-1 text-xs font-medium capitalize', | |
| + 'transition-all duration-150', | |
| + isSelected | |
| + ? TAG_ACTIVE_COLORS[tag] | |
| + : 'border-border text-muted-foreground bg-background hover:border-foreground/30 hover:text-foreground' | |
| + )} | |
| + > | |
| + {tag} | |
| + </button> | |
| + ); | |
| + })} | |
| + </div> | |
| + </div> | |
| </form> | |
| + | |
| <DialogFooter> | |
| - <DialogTrigger asChild> | |
| - <Button type='submit' size='sm' form='task-form'> | |
| - Add Task | |
| - </Button> | |
| - </DialogTrigger> | |
| + <Button variant='outline' onClick={() => setOpen(false)}> | |
| + Cancel | |
| + </Button> | |
| + <Button type='submit' form='task-form'> | |
| + Create Task | |
| + </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| diff --git a/src/features/kanban/components/task-card.tsx b/src/features/kanban/components/task-card.tsx | |
| index 13df61e..ce11f16 100644 | |
| --- a/src/features/kanban/components/task-card.tsx | |
| +++ b/src/features/kanban/components/task-card.tsx | |
| @@ -1,43 +1,152 @@ | |
| 'use client'; | |
| -import { Badge } from '@/components/ui/badge'; | |
| +import { cn } from '@/lib/utils'; | |
| import { KanbanItem } from '@/components/ui/kanban'; | |
| import type { Task } from '../utils/store'; | |
| +// ─── Priority badge config ──────────────────────────────────────────────────── | |
| + | |
| +const PRIORITY_CONFIG: Record<string, { label: string; className: string }> = { | |
| + low: { | |
| + label: 'Low', | |
| + className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' | |
| + }, | |
| + medium: { | |
| + label: 'Medium', | |
| + className: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400' | |
| + }, | |
| + high: { | |
| + label: 'High', | |
| + className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' | |
| + } | |
| +}; | |
| + | |
| +// ─── Tag color config ───────────────────────────────────────────────────────── | |
| + | |
| +const TAG_COLORS: Record<string, string> = { | |
| + mobile: 'bg-pink-500 text-white', | |
| + web: 'bg-violet-600 text-white', | |
| + design: 'bg-blue-500 text-white', | |
| + api: 'bg-amber-500 text-white', | |
| + backend: 'bg-orange-600 text-white', | |
| + frontend: 'bg-cyan-600 text-white', | |
| + database: 'bg-indigo-500 text-white', | |
| + testing: 'bg-slate-500 text-white' | |
| +}; | |
| + | |
| +function getTagColor(tag: string): string { | |
| + return TAG_COLORS[tag.toLowerCase()] ?? 'bg-slate-400 text-white'; | |
| +} | |
| + | |
| +function getAvatarUrl(name: string): string { | |
| + return `https://i.pravatar.cc/32?u=${encodeURIComponent(name)}`; | |
| +} | |
| + | |
| +function formatDueDate(dateStr: string): string { | |
| + try { | |
| + const date = new Date(dateStr); | |
| + return date.toLocaleDateString('en-US', { | |
| + month: 'short', | |
| + day: 'numeric', | |
| + year: 'numeric' | |
| + }); | |
| + } catch { | |
| + return dateStr; | |
| + } | |
| +} | |
| + | |
| +// ─── Component ──────────────────────────────────────────────────────────────── | |
| + | |
| interface TaskCardProps extends Omit<React.ComponentProps<typeof KanbanItem>, 'value'> { | |
| task: Task; | |
| } | |
| export function TaskCard({ task, ...props }: TaskCardProps) { | |
| + const priorityCfg = PRIORITY_CONFIG[task.priority] ?? PRIORITY_CONFIG.medium; | |
| + | |
| + const allAssignees = [...(task.assignee ? [task.assignee] : []), ...(task.coAssignees ?? [])]; | |
| + | |
| + const hasTags = task.tags && task.tags.length > 0; | |
| + const hasFooter = !!task.dueDate || allAssignees.length > 0; | |
| + | |
| return ( | |
| - <KanbanItem key={task.id} value={task.id} asChild {...props}> | |
| - <div className='bg-card rounded-md border p-3 shadow-xs'> | |
| - <div className='flex flex-col gap-2'> | |
| - <div className='flex items-center justify-between gap-2'> | |
| - <span className='line-clamp-1 text-sm font-medium'>{task.title}</span> | |
| - <Badge | |
| - variant={ | |
| - task.priority === 'high' | |
| - ? 'destructive' | |
| - : task.priority === 'medium' | |
| - ? 'default' | |
| - : 'secondary' | |
| - } | |
| - className='pointer-events-none h-5 rounded-sm px-1.5 text-[11px] capitalize' | |
| - > | |
| - {task.priority} | |
| - </Badge> | |
| + <KanbanItem value={task.id} asChild {...props}> | |
| + <div | |
| + className={cn( | |
| + 'flex flex-col gap-2.5 rounded border p-3', | |
| + 'bg-card border-zinc-300/70 dark:border-zinc-600/70', | |
| + 'shadow-xs transition-shadow duration-150 hover:shadow-sm', | |
| + 'cursor-grab active:cursor-grabbing', | |
| + 'focus-visible:ring-ring focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-offset-1' | |
| + )} | |
| + > | |
| + {/* Priority badge */} | |
| + <div> | |
| + <span | |
| + className={cn( | |
| + 'inline-flex items-center rounded-full px-2.5 py-0.5', | |
| + 'text-[10px] font-medium leading-none', | |
| + priorityCfg.className | |
| + )} | |
| + > | |
| + {priorityCfg.label} | |
| + </span> | |
| + </div> | |
| + | |
| + {/* Title */} | |
| + <p className={cn('text-card-foreground line-clamp-3 text-[13px] font-medium leading-snug')}> | |
| + {task.title} | |
| + </p> | |
| + | |
| + {/* Tags */} | |
| + {hasTags && ( | |
| + <div className='flex flex-wrap gap-1.5'> | |
| + {task.tags!.map((tag) => ( | |
| + <span | |
| + key={tag} | |
| + className={cn( | |
| + 'inline-flex items-center rounded-full px-2.5 py-0.5', | |
| + 'text-[10px] font-medium capitalize leading-none', | |
| + getTagColor(tag) | |
| + )} | |
| + > | |
| + {tag} | |
| + </span> | |
| + ))} | |
| </div> | |
| - <div className='text-muted-foreground flex items-center justify-between text-xs'> | |
| - {task.assignee && ( | |
| - <div className='flex items-center gap-1'> | |
| - <div className='bg-primary/20 size-2 rounded-full' /> | |
| - <span className='line-clamp-1'>{task.assignee}</span> | |
| + )} | |
| + | |
| + {/* Footer: date + avatars */} | |
| + {hasFooter && ( | |
| + <div className='flex items-center justify-between pt-0.5'> | |
| + <div> | |
| + {task.dueDate && ( | |
| + <time className='text-muted-foreground text-[11px]'> | |
| + {formatDueDate(task.dueDate)} | |
| + </time> | |
| + )} | |
| + </div> | |
| + | |
| + {allAssignees.length > 0 && ( | |
| + <div className='flex items-center'> | |
| + {allAssignees.slice(0, 3).map((name, i) => ( | |
| + <img | |
| + key={name} | |
| + src={getAvatarUrl(name)} | |
| + alt={name} | |
| + title={name} | |
| + width={28} | |
| + height={28} | |
| + className={cn( | |
| + 'h-7 w-7 rounded-full border-2 border-card object-cover', | |
| + i > 0 && '-ml-2' | |
| + )} | |
| + /> | |
| + ))} | |
| </div> | |
| )} | |
| - {task.dueDate && <time className='text-[10px] tabular-nums'>{task.dueDate}</time>} | |
| </div> | |
| - </div> | |
| + )} | |
| </div> | |
| </KanbanItem> | |
| ); | |
| diff --git a/src/features/kanban/utils/store.ts b/src/features/kanban/utils/store.ts | |
| index 01d1f16..d64ecba 100644 | |
| --- a/src/features/kanban/utils/store.ts | |
| +++ b/src/features/kanban/utils/store.ts | |
| @@ -1,22 +1,23 @@ | |
| import { create } from 'zustand'; | |
| import { v4 as uuid } from 'uuid'; | |
| -// import { persist } from 'zustand/middleware'; | |
| export type Priority = 'low' | 'medium' | 'high'; | |
| -export type Task = { | |
| +export interface Task { | |
| id: string; | |
| title: string; | |
| priority: Priority; | |
| description?: string; | |
| assignee?: string; | |
| + coAssignees?: string[]; | |
| dueDate?: string; | |
| -}; | |
| + tags?: string[]; | |
| +} | |
| type KanbanState = { | |
| columns: Record<string, Task[]>; | |
| setColumns: (columns: Record<string, Task[]>) => void; | |
| - addTask: (title: string, description?: string) => void; | |
| + addTask: (title: string, description?: string, tags?: string[]) => void; | |
| }; | |
| const initialColumns: Record<string, Task[]> = { | |
| @@ -26,28 +27,33 @@ const initialColumns: Record<string, Task[]> = { | |
| title: 'Migrate to Stripe billing API', | |
| priority: 'high', | |
| assignee: 'Sarah Chen', | |
| - dueDate: '2026-04-08' | |
| + coAssignees: ['Marcus Rivera'], | |
| + dueDate: '2026-04-08', | |
| + tags: ['backend', 'api'] | |
| }, | |
| { | |
| id: '2', | |
| title: 'Add CSV export to reports', | |
| priority: 'medium', | |
| assignee: 'Marcus Rivera', | |
| - dueDate: '2026-04-12' | |
| + dueDate: '2026-04-12', | |
| + tags: ['frontend', 'design'] | |
| }, | |
| { | |
| id: '3', | |
| title: 'Update onboarding flow copy', | |
| priority: 'low', | |
| assignee: 'Priya Sharma', | |
| - dueDate: '2026-04-15' | |
| + dueDate: '2026-04-15', | |
| + tags: ['web', 'design'] | |
| }, | |
| { | |
| id: '9', | |
| title: 'Audit RBAC permissions', | |
| priority: 'medium', | |
| assignee: 'Jordan Kim', | |
| - dueDate: '2026-04-10' | |
| + dueDate: '2026-04-10', | |
| + tags: ['backend', 'testing'] | |
| } | |
| ], | |
| inProgress: [ | |
| @@ -56,21 +62,44 @@ const initialColumns: Record<string, Task[]> = { | |
| title: 'Refactor notification service', | |
| priority: 'high', | |
| assignee: 'Alex Turner', | |
| - dueDate: '2026-04-03' | |
| + coAssignees: ['Sarah Chen'], | |
| + dueDate: '2026-04-03', | |
| + tags: ['backend', 'api'] | |
| }, | |
| { | |
| id: '5', | |
| title: 'Build team invitation flow', | |
| priority: 'medium', | |
| assignee: 'Emily Nakamura', | |
| - dueDate: '2026-04-06' | |
| + dueDate: '2026-04-06', | |
| + tags: ['mobile', 'web'] | |
| }, | |
| { | |
| id: '10', | |
| title: 'Fix timezone handling in scheduler', | |
| priority: 'high', | |
| assignee: 'Sarah Chen', | |
| - dueDate: '2026-04-04' | |
| + dueDate: '2026-04-04', | |
| + tags: ['backend'] | |
| + } | |
| + ], | |
| + review: [ | |
| + { | |
| + id: '11', | |
| + title: 'Dashboard layout responsiveness', | |
| + priority: 'medium', | |
| + assignee: 'Marcus Rivera', | |
| + coAssignees: ['Jordan Kim'], | |
| + dueDate: '2026-03-30', | |
| + tags: ['frontend', 'mobile'] | |
| + }, | |
| + { | |
| + id: '12', | |
| + title: 'API rate limiting middleware', | |
| + priority: 'high', | |
| + assignee: 'Jordan Kim', | |
| + dueDate: '2026-03-31', | |
| + tags: ['backend', 'api'] | |
| } | |
| ], | |
| done: [ | |
| @@ -79,21 +108,25 @@ const initialColumns: Record<string, Task[]> = { | |
| title: 'SSO integration with Okta', | |
| priority: 'high', | |
| assignee: 'Jordan Kim', | |
| - dueDate: '2026-03-22' | |
| + coAssignees: ['Sarah Chen'], | |
| + dueDate: '2026-03-22', | |
| + tags: ['backend', 'api'] | |
| }, | |
| { | |
| id: '7', | |
| title: 'Dashboard analytics charts', | |
| priority: 'medium', | |
| assignee: 'Marcus Rivera', | |
| - dueDate: '2026-03-20' | |
| + dueDate: '2026-03-20', | |
| + tags: ['frontend', 'design'] | |
| }, | |
| { | |
| id: '8', | |
| title: 'Webhook retry mechanism', | |
| priority: 'low', | |
| assignee: 'Alex Turner', | |
| - dueDate: '2026-03-18' | |
| + dueDate: '2026-03-18', | |
| + tags: ['backend'] | |
| } | |
| ] | |
| }; | |
| @@ -106,7 +139,7 @@ export const useTaskStore = create<KanbanState>()( | |
| setColumns: (columns) => set({ columns }), | |
| - addTask: (title, description) => | |
| + addTask: (title, description, tags) => | |
| set((state) => ({ | |
| columns: { | |
| ...state.columns, | |
| @@ -116,8 +149,7 @@ export const useTaskStore = create<KanbanState>()( | |
| title, | |
| description, | |
| priority: 'medium' as Priority, | |
| - assignee: undefined, | |
| - dueDate: undefined | |
| + tags: tags && tags.length > 0 ? tags : undefined | |
| }, | |
| ...(state.columns.backlog ?? []) | |
| ] | |
| -- | |
| 2.53.0 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment