Skip to content

Commit 4aa6215

Browse files
committed
feat: premium makeover with dark glassmorphic UI
1 parent 689f51c commit 4aa6215

15 files changed

Lines changed: 1326 additions & 216 deletions

File tree

webapp/prisma/schema.prisma

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,22 @@ enum TodoItemStatus {
2525
COMPLETED
2626
}
2727

28+
enum TodoItemPriority {
29+
LOW
30+
MEDIUM
31+
HIGH
32+
URGENT
33+
}
34+
2835
model TodoItem {
29-
id String @id @default(uuid())
36+
id String @id @default(uuid())
3037
title String
31-
description String @db.Text()
38+
description String @db.Text()
3239
userId String
3340
status TodoItemStatus
41+
priority TodoItemPriority @default(MEDIUM)
42+
dueDate DateTime?
43+
category String?
3444
3545
createdAt DateTime @default(now())
3646
updatedAt DateTime @updatedAt

webapp/src/app/(root)/actions.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import {
1010
} from './schemas';
1111
import { prisma } from '@/lib/prisma';
1212
import { revalidatePath } from 'next/cache';
13-
import { TodoItemStatus } from '@prisma/client';
13+
import { TodoItemStatus, TodoItemPriority } from '@prisma/client';
1414
import { runJob } from '@/lib/jobs';
1515

1616
export const createTodo = authActionClient.schema(createTodoSchema).action(async ({ parsedInput, ctx }) => {
17-
const { title, description } = parsedInput;
17+
const { title, description, priority, dueDate, category } = parsedInput;
1818
const { userId } = ctx;
1919

2020
const todo = await prisma.todoItem.create({
@@ -23,6 +23,9 @@ export const createTodo = authActionClient.schema(createTodoSchema).action(async
2323
description,
2424
userId,
2525
status: TodoItemStatus.PENDING,
26+
priority: (priority as TodoItemPriority) || TodoItemPriority.MEDIUM,
27+
dueDate: dueDate ? new Date(dueDate) : null,
28+
category: category || null,
2629
},
2730
});
2831

@@ -31,7 +34,7 @@ export const createTodo = authActionClient.schema(createTodoSchema).action(async
3134
});
3235

3336
export const updateTodo = authActionClient.schema(updateTodoSchema).action(async ({ parsedInput, ctx }) => {
34-
const { id, title, description, status } = parsedInput;
37+
const { id, title, description, status, priority, dueDate, category } = parsedInput;
3538
const { userId } = ctx;
3639

3740
const todo = await prisma.todoItem.update({
@@ -43,6 +46,9 @@ export const updateTodo = authActionClient.schema(updateTodoSchema).action(async
4346
title,
4447
description,
4548
status,
49+
...(priority && { priority: priority as TodoItemPriority }),
50+
...(dueDate !== undefined && { dueDate: dueDate ? new Date(dueDate) : null }),
51+
...(category !== undefined && { category: category || null }),
4652
},
4753
});
4854

webapp/src/app/(root)/components/CreateTodoForm.tsx

Lines changed: 103 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createTodoSchema } from '../schemas';
88
import { toast } from 'sonner';
99
import { useEventBus } from '@/hooks/use-event-bus';
1010
import { useRouter } from 'next/navigation';
11+
import { Plus, X, Sparkles, ArrowDown, Minus, AlertTriangle, Flame } from 'lucide-react';
1112

1213
export default function CreateTodoForm(props: { userId: string }) {
1314
const [isFormOpen, setIsFormOpen] = useState(false);
@@ -22,88 +23,170 @@ export default function CreateTodoForm(props: { userId: string }) {
2223
});
2324

2425
const {
25-
form: { register, formState, reset },
26+
form: { register, formState, reset, setValue, watch },
2627
action,
2728
handleSubmitWithAction,
2829
} = useHookFormAction(createTodo, zodResolver(createTodoSchema), {
2930
actionProps: {
3031
onSuccess: () => {
31-
toast.success('Todo created successfully');
32+
toast.success('Task created successfully');
3233
reset();
3334
setIsFormOpen(false);
3435
},
3536
onError: ({ error }) => {
36-
toast.error(typeof error === 'string' ? error : 'Failed to create todo');
37+
toast.error(typeof error === 'string' ? error : 'Failed to create task');
3738
},
3839
},
3940
formProps: {
4041
defaultValues: {
4142
title: '',
4243
description: '',
44+
priority: 'MEDIUM',
45+
dueDate: '',
46+
category: '',
4347
},
4448
},
4549
});
4650

51+
const selectedPriority = watch('priority');
52+
53+
const priorities = [
54+
{ value: 'LOW', label: 'Low', icon: ArrowDown, color: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', activeBg: 'bg-emerald-500/20' },
55+
{ value: 'MEDIUM', label: 'Med', icon: Minus, color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/30', activeBg: 'bg-amber-500/20' },
56+
{ value: 'HIGH', label: 'High', icon: AlertTriangle, color: 'text-orange-400', bg: 'bg-orange-500/10', border: 'border-orange-500/30', activeBg: 'bg-orange-500/20' },
57+
{ value: 'URGENT', label: 'Urgent', icon: Flame, color: 'text-red-400', bg: 'bg-red-500/10', border: 'border-red-500/30', activeBg: 'bg-red-500/20' },
58+
];
59+
4760
if (!isFormOpen) {
4861
return (
4962
<div className="mb-6">
5063
<button
5164
onClick={() => setIsFormOpen(true)}
52-
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
65+
className="group inline-flex items-center gap-2.5 btn-primary py-2.5 px-5 text-sm"
5366
>
54-
+ Add New Todo
67+
<div className="w-5 h-5 rounded-md bg-white/20 flex items-center justify-center group-hover:bg-white/30 transition-colors">
68+
<Plus className="w-3.5 h-3.5 text-white" />
69+
</div>
70+
Add New Task
5571
</button>
5672
</div>
5773
);
5874
}
5975

6076
return (
61-
<div className="bg-white shadow-md rounded-lg p-6 mb-6">
62-
<h2 className="text-lg font-medium text-gray-900 mb-4">Create New Todo</h2>
77+
<div className="glass-card rounded-xl p-6 mb-6 animate-scale-in">
78+
<div className="flex items-center gap-2 mb-5">
79+
<Sparkles className="w-5 h-5 text-violet-400" />
80+
<h2 className="text-lg font-semibold text-zinc-200">Create New Task</h2>
81+
</div>
82+
6383
<form onSubmit={handleSubmitWithAction} className="space-y-4">
84+
{/* Title */}
6485
<div>
65-
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
86+
<label htmlFor="title" className="form-label">
6687
Title
6788
</label>
6889
<input
6990
id="title"
7091
{...register('title')}
71-
placeholder="Your TODO item title."
72-
className="mt-1 p-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
92+
placeholder="What needs to be done?"
93+
className="w-full px-3.5 py-2.5 rounded-xl glass-input text-sm text-zinc-200 placeholder-zinc-600"
7394
/>
74-
{formState.errors.title && <p className="mt-1 text-sm text-red-600">{formState.errors.title.message}</p>}
95+
{formState.errors.title && (
96+
<p className="mt-1 text-xs text-red-400">{formState.errors.title.message}</p>
97+
)}
7598
</div>
7699

100+
{/* Description */}
77101
<div>
78-
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
102+
<label htmlFor="description" className="form-label">
79103
Description
80104
</label>
81105
<textarea
82106
id="description"
83107
{...register('description')}
84108
rows={3}
85-
placeholder="Describe your TODO item."
86-
className="mt-1 p-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
109+
placeholder="Add some details..."
110+
className="w-full px-3.5 py-2.5 rounded-xl glass-input text-sm text-zinc-200 placeholder-zinc-600 resize-none"
87111
/>
88112
{formState.errors.description && (
89-
<p className="mt-1 text-sm text-red-600">{formState.errors.description.message}</p>
113+
<p className="mt-1 text-xs text-red-400">{formState.errors.description.message}</p>
90114
)}
91115
</div>
92116

93-
<div className="flex justify-end space-x-2">
117+
{/* Priority Selector */}
118+
<div>
119+
<label className="form-label">Priority</label>
120+
<div className="flex gap-2">
121+
{priorities.map((p) => {
122+
const Icon = p.icon;
123+
const isActive = selectedPriority === p.value;
124+
return (
125+
<button
126+
key={p.value}
127+
type="button"
128+
onClick={() => setValue('priority', p.value as 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT')}
129+
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-medium border transition-all duration-200 ${
130+
isActive
131+
? `${p.activeBg} ${p.color} ${p.border}`
132+
: 'bg-transparent text-zinc-500 border-white/[0.06] hover:border-white/[0.12]'
133+
}`}
134+
>
135+
<Icon className="w-3.5 h-3.5" />
136+
{p.label}
137+
</button>
138+
);
139+
})}
140+
</div>
141+
</div>
142+
143+
{/* Due Date & Category Row */}
144+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
145+
<div>
146+
<label htmlFor="dueDate" className="form-label">
147+
Due Date
148+
</label>
149+
<input
150+
id="dueDate"
151+
type="date"
152+
{...register('dueDate')}
153+
className="w-full px-3.5 py-2.5 rounded-xl glass-input text-sm text-zinc-200 [color-scheme:dark]"
154+
/>
155+
</div>
156+
<div>
157+
<label htmlFor="category" className="form-label">
158+
Category
159+
</label>
160+
<input
161+
id="category"
162+
type="text"
163+
{...register('category')}
164+
placeholder="e.g. Work, Personal"
165+
className="w-full px-3.5 py-2.5 rounded-xl glass-input text-sm text-zinc-200 placeholder-zinc-600"
166+
/>
167+
</div>
168+
</div>
169+
170+
{/* Actions */}
171+
<div className="flex justify-end gap-2 pt-2">
94172
<button
95173
type="button"
96-
onClick={() => setIsFormOpen(false)}
97-
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-gray-700 bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
174+
onClick={() => {
175+
setIsFormOpen(false);
176+
reset();
177+
}}
178+
className="btn-ghost inline-flex items-center gap-1.5"
98179
>
180+
<X className="w-3.5 h-3.5" />
99181
Cancel
100182
</button>
101183
<button
102184
type="submit"
103185
disabled={action.isExecuting}
104-
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
186+
className="btn-primary inline-flex items-center gap-1.5"
105187
>
106-
{action.isExecuting ? 'Creating...' : 'Create Todo'}
188+
<Plus className="w-3.5 h-3.5" />
189+
{action.isExecuting ? 'Creating...' : 'Create Task'}
107190
</button>
108191
</div>
109192
</form>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use client';
2+
3+
import { ListChecks, Clock, CheckCircle2, AlertCircle } from 'lucide-react';
4+
5+
interface DashboardStatsProps {
6+
total: number;
7+
pending: number;
8+
completed: number;
9+
overdue: number;
10+
}
11+
12+
const stats = [
13+
{
14+
key: 'total',
15+
label: 'Total Tasks',
16+
icon: ListChecks,
17+
colorClass: 'text-violet-400',
18+
bgClass: 'bg-violet-500/10',
19+
borderClass: 'border-violet-500/20',
20+
},
21+
{
22+
key: 'pending',
23+
label: 'Pending',
24+
icon: Clock,
25+
colorClass: 'text-amber-400',
26+
bgClass: 'bg-amber-500/10',
27+
borderClass: 'border-amber-500/20',
28+
},
29+
{
30+
key: 'completed',
31+
label: 'Completed',
32+
icon: CheckCircle2,
33+
colorClass: 'text-emerald-400',
34+
bgClass: 'bg-emerald-500/10',
35+
borderClass: 'border-emerald-500/20',
36+
},
37+
{
38+
key: 'overdue',
39+
label: 'Overdue',
40+
icon: AlertCircle,
41+
colorClass: 'text-rose-400',
42+
bgClass: 'bg-rose-500/10',
43+
borderClass: 'border-rose-500/20',
44+
},
45+
] as const;
46+
47+
export default function DashboardStats({ total, pending, completed, overdue }: DashboardStatsProps) {
48+
const values = { total, pending, completed, overdue };
49+
50+
return (
51+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-8">
52+
{stats.map((stat, index) => {
53+
const Icon = stat.icon;
54+
return (
55+
<div
56+
key={stat.key}
57+
className={`glass-card rounded-xl p-4 animate-fade-in-up stagger-${index + 1}`}
58+
style={{ opacity: 0 }}
59+
>
60+
<div className="flex items-center justify-between mb-3">
61+
<span className="text-xs font-medium text-zinc-500 uppercase tracking-wider">
62+
{stat.label}
63+
</span>
64+
<div className={`w-8 h-8 rounded-lg ${stat.bgClass} border ${stat.borderClass} flex items-center justify-center`}>
65+
<Icon className={`w-4 h-4 ${stat.colorClass}`} />
66+
</div>
67+
</div>
68+
<p className={`text-2xl font-bold ${stat.colorClass}`}>
69+
{values[stat.key]}
70+
</p>
71+
</div>
72+
);
73+
})}
74+
</div>
75+
);
76+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use client';
2+
3+
import { type LucideIcon } from 'lucide-react';
4+
5+
interface EmptyStateProps {
6+
icon: LucideIcon;
7+
title: string;
8+
subtitle?: string;
9+
action?: React.ReactNode;
10+
}
11+
12+
export default function EmptyState({ icon: Icon, title, subtitle, action }: EmptyStateProps) {
13+
return (
14+
<div className="flex flex-col items-center justify-center py-16 animate-fade-in-up">
15+
<div className="w-16 h-16 rounded-2xl bg-white/[0.03] border border-white/[0.06] flex items-center justify-center mb-5">
16+
<Icon className="w-7 h-7 text-zinc-600" />
17+
</div>
18+
<h3 className="text-lg font-semibold text-zinc-400 mb-1.5">{title}</h3>
19+
{subtitle && (
20+
<p className="text-sm text-zinc-600 text-center max-w-xs">{subtitle}</p>
21+
)}
22+
{action && <div className="mt-5">{action}</div>}
23+
</div>
24+
);
25+
}

0 commit comments

Comments
 (0)