Skip to content

Instantly share code, notes, and snippets.

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

  • Save shricodev/583d31c69823cb772c7a7188c4b89f73 to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/583d31c69823cb772c7a7188c4b89f73 to your computer and use it in GitHub Desktop.
Admin Dashboard - Kombai 2
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