Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created March 30, 2026 16:13
Show Gist options
  • Select an option

  • Save shricodev/19c1e6f9f640975f22363f4ad9aa7346 to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/19c1e6f9f640975f22363f4ad9aa7346 to your computer and use it in GitHub Desktop.
Admin Dashboard - Opus 4.6 (Second)
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