-
-
Save shricodev/19c1e6f9f640975f22363f4ad9aa7346 to your computer and use it in GitHub Desktop.
Admin Dashboard - Opus 4.6 (Second)
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 6b6493765a640afe54ba262512b7787901662ddc Mon Sep 17 00:00:00 2001 | |
| From: Shrijal Acharya <shrijal.acharya@gmail.com> | |
| Date: Sun, 29 Mar 2026 12:37:26 +0545 | |
| Subject: [PATCH 2/2] opus 2 kanban | |
| --- | |
| src/components/ui/kanban.tsx | 2 +- | |
| .../kanban/components/board-column.tsx | 34 +++-- | |
| .../kanban/components/kanban-board.tsx | 4 +- | |
| src/features/kanban/components/task-card.tsx | 138 ++++++++++++++---- | |
| src/features/kanban/utils/store.ts | 40 +++-- | |
| 5 files changed, 162 insertions(+), 56 deletions(-) | |
| diff --git a/src/components/ui/kanban.tsx b/src/components/ui/kanban.tsx | |
| index 5c3064a..9e00b33 100644 | |
| --- a/src/components/ui/kanban.tsx | |
| +++ b/src/components/ui/kanban.tsx | |
| @@ -749,7 +749,7 @@ function KanbanColumn(props: KanbanColumnProps) { | |
| ref={composedRef} | |
| style={composedStyle} | |
| className={cn( | |
| - 'flex size-full flex-col gap-2 rounded-lg border bg-zinc-100 p-2.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:bg-zinc-900', | |
| + 'flex size-full flex-col gap-3 rounded-lg border border-border/60 bg-muted/50 p-3 aria-disabled:pointer-events-none aria-disabled:opacity-50', | |
| { | |
| 'touch-none select-none': asHandle, | |
| 'cursor-default': context.flatCursor, | |
| diff --git a/src/features/kanban/components/board-column.tsx b/src/features/kanban/components/board-column.tsx | |
| index 4a695f9..1b0be9a 100644 | |
| --- a/src/features/kanban/components/board-column.tsx | |
| +++ b/src/features/kanban/components/board-column.tsx | |
| @@ -1,40 +1,42 @@ | |
| '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 type { Task } from '../utils/store'; | |
| +import { COLUMN_META } from '../utils/store'; | |
| import { TaskCard } from './task-card'; | |
| -const COLUMN_TITLES: Record<string, string> = { | |
| - backlog: 'Backlog', | |
| - inProgress: 'In Progress', | |
| - review: 'Review', | |
| - done: 'Done' | |
| -}; | |
| - | |
| interface TaskColumnProps extends Omit<React.ComponentProps<typeof KanbanColumn>, 'children'> { | |
| tasks: Task[]; | |
| } | |
| export function TaskColumn({ value, tasks, ...props }: TaskColumnProps) { | |
| + const meta = COLUMN_META[value]; | |
| + const accent = meta?.accent ?? 'hsl(var(--border))'; | |
| + | |
| return ( | |
| - <KanbanColumn value={value} className='w-[320px] shrink-0' {...props}> | |
| - <div className='flex items-center justify-between'> | |
| + <KanbanColumn value={value} className='w-[300px] shrink-0' {...props}> | |
| + {/* Colored accent strip */} | |
| + <div className='h-1 w-full rounded-full' style={{ backgroundColor: accent }} /> | |
| + | |
| + {/* Column header */} | |
| + <div className='flex items-center justify-between px-0.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'> | |
| + <span className='text-sm font-semibold tracking-tight'>{meta?.title ?? value}</span> | |
| + <span className='bg-muted text-muted-foreground flex size-5 items-center justify-center rounded-md text-[11px] font-medium'> | |
| {tasks.length} | |
| - </Badge> | |
| + </span> | |
| </div> | |
| <KanbanColumnHandle asChild> | |
| - <Button variant='ghost' size='icon'> | |
| - <Icons.gripVertical className='h-4 w-4' /> | |
| + <Button variant='ghost' size='icon' className='size-7'> | |
| + <Icons.gripVertical className='size-3.5' /> | |
| </Button> | |
| </KanbanColumnHandle> | |
| </div> | |
| - <div className='flex flex-col gap-2 p-0.5'> | |
| + | |
| + {/* Cards */} | |
| + <div className='flex flex-col gap-2.5 p-0.5'> | |
| {tasks.map((task) => ( | |
| <TaskCard key={task.id} task={task} asHandle /> | |
| ))} | |
| diff --git a/src/features/kanban/components/kanban-board.tsx b/src/features/kanban/components/kanban-board.tsx | |
| index b19d98c..10d213b 100644 | |
| --- a/src/features/kanban/components/kanban-board.tsx | |
| +++ b/src/features/kanban/components/kanban-board.tsx | |
| @@ -27,8 +27,8 @@ export function KanbanBoard() { | |
| modifiers={[restrictToBoard]} | |
| autoScroll={false} | |
| > | |
| - <ScrollArea className='w-full rounded-md pb-4'> | |
| - <KanbanBoardPrimitive className='flex items-start'> | |
| + <ScrollArea className='w-full rounded-lg pb-4'> | |
| + <KanbanBoardPrimitive className='flex items-start gap-5'> | |
| {Object.entries(columns).map(([columnValue, tasks]) => ( | |
| <TaskColumn key={columnValue} value={columnValue} tasks={tasks} /> | |
| ))} | |
| diff --git a/src/features/kanban/components/task-card.tsx b/src/features/kanban/components/task-card.tsx | |
| index 13df61e..9f79a0a 100644 | |
| --- a/src/features/kanban/components/task-card.tsx | |
| +++ b/src/features/kanban/components/task-card.tsx | |
| @@ -1,42 +1,126 @@ | |
| 'use client'; | |
| -import { Badge } from '@/components/ui/badge'; | |
| +import { Icons } from '@/components/icons'; | |
| +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; | |
| import { KanbanItem } from '@/components/ui/kanban'; | |
| -import type { Task } from '../utils/store'; | |
| +import { cn } from '@/lib/utils'; | |
| +import type { Tag, Task } from '../utils/store'; | |
| interface TaskCardProps extends Omit<React.ComponentProps<typeof KanbanItem>, 'value'> { | |
| task: Task; | |
| } | |
| -export function TaskCard({ task, ...props }: TaskCardProps) { | |
| +const TAG_STYLES: Record<Tag, string> = { | |
| + web: 'bg-violet-600 text-white', | |
| + mobile: 'bg-pink-600 text-white', | |
| + api: 'bg-sky-600 text-white', | |
| + design: 'bg-amber-600 text-white' | |
| +}; | |
| + | |
| +const TAG_LABELS: Record<Tag, string> = { | |
| + web: 'Web', | |
| + mobile: 'Mobile', | |
| + api: 'API', | |
| + design: 'Design' | |
| +}; | |
| + | |
| +function getInitials(name: string) { | |
| + return name | |
| + .split(' ') | |
| + .map((n) => n[0]) | |
| + .join('') | |
| + .toUpperCase() | |
| + .slice(0, 2); | |
| +} | |
| + | |
| +function getAvatarColor(name: string) { | |
| + const colors = [ | |
| + 'bg-rose-100 text-rose-700', | |
| + 'bg-sky-100 text-sky-700', | |
| + 'bg-amber-100 text-amber-700', | |
| + 'bg-emerald-100 text-emerald-700', | |
| + 'bg-violet-100 text-violet-700', | |
| + 'bg-pink-100 text-pink-700' | |
| + ]; | |
| + let hash = 0; | |
| + for (let i = 0; i < name.length; i++) { | |
| + hash = name.charCodeAt(i) + ((hash << 5) - hash); | |
| + } | |
| + return colors[Math.abs(hash) % colors.length]; | |
| +} | |
| + | |
| +export function TaskCard({ task, className, ...props }: TaskCardProps) { | |
| 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> | |
| - </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> | |
| - </div> | |
| + <div | |
| + className={cn( | |
| + 'bg-card flex flex-col gap-2.5 rounded-md border p-3 transition-shadow hover:shadow-md', | |
| + className | |
| + )} | |
| + > | |
| + {/* Priority pill */} | |
| + <div> | |
| + <span | |
| + className={cn( | |
| + 'inline-flex items-center rounded-full px-2.5 py-0.5 text-[10px] font-medium', | |
| + task.priority === 'high' && | |
| + 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400', | |
| + task.priority === 'medium' && | |
| + 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400', | |
| + task.priority === 'low' && | |
| + 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400' | |
| )} | |
| - {task.dueDate && <time className='text-[10px] tabular-nums'>{task.dueDate}</time>} | |
| + > | |
| + {task.priority.charAt(0).toUpperCase() + task.priority.slice(1)} | |
| + </span> | |
| + </div> | |
| + | |
| + {/* Title */} | |
| + <p className='text-foreground text-xs leading-snug font-medium'>{task.title}</p> | |
| + | |
| + {/* Tags */} | |
| + {task.tags && task.tags.length > 0 && ( | |
| + <div className='flex flex-wrap gap-1.5'> | |
| + {task.tags.map((tag) => ( | |
| + <span | |
| + key={tag} | |
| + className={cn( | |
| + 'inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium', | |
| + TAG_STYLES[tag] | |
| + )} | |
| + > | |
| + {TAG_LABELS[tag]} | |
| + </span> | |
| + ))} | |
| </div> | |
| + )} | |
| + | |
| + {/* Date and avatar row */} | |
| + <div className='flex items-center justify-between'> | |
| + {task.dueDate ? ( | |
| + <div className='text-muted-foreground flex items-center gap-1'> | |
| + <Icons.calendar className='size-3' /> | |
| + <time className='text-[11px]'> | |
| + {new Date(task.dueDate).toLocaleDateString('en-US', { | |
| + month: 'short', | |
| + day: 'numeric', | |
| + year: 'numeric' | |
| + })} | |
| + </time> | |
| + </div> | |
| + ) : ( | |
| + <div /> | |
| + )} | |
| + | |
| + {task.assignee && ( | |
| + <Avatar className='size-7'> | |
| + <AvatarFallback | |
| + className={cn('text-[10px] font-medium', getAvatarColor(task.assignee))} | |
| + > | |
| + {getInitials(task.assignee)} | |
| + </AvatarFallback> | |
| + </Avatar> | |
| + )} | |
| </div> | |
| </div> | |
| </KanbanItem> | |
| diff --git a/src/features/kanban/utils/store.ts b/src/features/kanban/utils/store.ts | |
| index 01d1f16..93ea61c 100644 | |
| --- a/src/features/kanban/utils/store.ts | |
| +++ b/src/features/kanban/utils/store.ts | |
| @@ -4,6 +4,8 @@ import { v4 as uuid } from 'uuid'; | |
| export type Priority = 'low' | 'medium' | 'high'; | |
| +export type Tag = 'web' | 'mobile' | 'api' | 'design'; | |
| + | |
| export type Task = { | |
| id: string; | |
| title: string; | |
| @@ -11,6 +13,14 @@ export type Task = { | |
| description?: string; | |
| assignee?: string; | |
| dueDate?: string; | |
| + tags?: Tag[]; | |
| +}; | |
| + | |
| +export const COLUMN_META: Record<string, { title: string; accent: string }> = { | |
| + backlog: { title: 'Backlog', accent: 'hsl(var(--chart-3))' }, | |
| + inProgress: { title: 'In Progress', accent: 'hsl(var(--chart-1))' }, | |
| + review: { title: 'Review', accent: 'hsl(var(--chart-2))' }, | |
| + done: { title: 'Done', accent: 'hsl(var(--chart-4))' } | |
| }; | |
| type KanbanState = { | |
| @@ -26,28 +36,32 @@ const initialColumns: Record<string, Task[]> = { | |
| title: 'Migrate to Stripe billing API', | |
| priority: 'high', | |
| assignee: 'Sarah Chen', | |
| - dueDate: '2026-04-08' | |
| + dueDate: '2026-04-08', | |
| + tags: ['api'] | |
| }, | |
| { | |
| id: '2', | |
| title: 'Add CSV export to reports', | |
| priority: 'medium', | |
| assignee: 'Marcus Rivera', | |
| - dueDate: '2026-04-12' | |
| + dueDate: '2026-04-12', | |
| + tags: ['web'] | |
| }, | |
| { | |
| id: '3', | |
| title: 'Update onboarding flow copy', | |
| priority: 'low', | |
| assignee: 'Priya Sharma', | |
| - dueDate: '2026-04-15' | |
| + dueDate: '2026-04-15', | |
| + tags: ['design', 'web'] | |
| }, | |
| { | |
| id: '9', | |
| title: 'Audit RBAC permissions', | |
| priority: 'medium', | |
| assignee: 'Jordan Kim', | |
| - dueDate: '2026-04-10' | |
| + dueDate: '2026-04-10', | |
| + tags: ['api'] | |
| } | |
| ], | |
| inProgress: [ | |
| @@ -56,21 +70,24 @@ const initialColumns: Record<string, Task[]> = { | |
| title: 'Refactor notification service', | |
| priority: 'high', | |
| assignee: 'Alex Turner', | |
| - dueDate: '2026-04-03' | |
| + dueDate: '2026-04-03', | |
| + tags: ['mobile', 'web'] | |
| }, | |
| { | |
| id: '5', | |
| title: 'Build team invitation flow', | |
| priority: 'medium', | |
| assignee: 'Emily Nakamura', | |
| - dueDate: '2026-04-06' | |
| + dueDate: '2026-04-06', | |
| + tags: ['web'] | |
| }, | |
| { | |
| id: '10', | |
| title: 'Fix timezone handling in scheduler', | |
| priority: 'high', | |
| assignee: 'Sarah Chen', | |
| - dueDate: '2026-04-04' | |
| + dueDate: '2026-04-04', | |
| + tags: ['api', 'mobile'] | |
| } | |
| ], | |
| done: [ | |
| @@ -79,21 +96,24 @@ const initialColumns: Record<string, Task[]> = { | |
| title: 'SSO integration with Okta', | |
| priority: 'high', | |
| assignee: 'Jordan Kim', | |
| - dueDate: '2026-03-22' | |
| + dueDate: '2026-03-22', | |
| + tags: ['api', 'web'] | |
| }, | |
| { | |
| id: '7', | |
| title: 'Dashboard analytics charts', | |
| priority: 'medium', | |
| assignee: 'Marcus Rivera', | |
| - dueDate: '2026-03-20' | |
| + dueDate: '2026-03-20', | |
| + tags: ['web'] | |
| }, | |
| { | |
| id: '8', | |
| title: 'Webhook retry mechanism', | |
| priority: 'low', | |
| assignee: 'Alex Turner', | |
| - dueDate: '2026-03-18' | |
| + dueDate: '2026-03-18', | |
| + tags: ['api'] | |
| } | |
| ] | |
| }; | |
| -- | |
| 2.53.0 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment