Compare commits

...

1 Commits

Author SHA1 Message Date
christin 5ccc70ed1d feat(site): add shadcn todo list demo 2025-11-12 08:58:26 +01:00
2 changed files with 192 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
import { Button } from "components/Button/Button";
import { Checkbox } from "components/Checkbox/Checkbox";
import { Input } from "components/Input/Input";
import { Label } from "components/Label/Label";
import { cn } from "utils/cn";
import { type FC, type FormEvent, useMemo, useState } from "react";
import { Trash2 } from "lucide-react";
type TodoItem = {
id: string;
title: string;
completed: boolean;
createdAt: number;
};
const buildTodo = (title: string): TodoItem => {
return {
id:
typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
title,
completed: false,
createdAt: Date.now(),
};
};
const TodoPage: FC = () => {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [pendingTitle, setPendingTitle] = useState("");
const remainingCount = useMemo(
() => todos.filter((todo) => !todo.completed).length,
[todos],
);
const completedCount = todos.length - remainingCount;
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const trimmedTitle = pendingTitle.trim();
if (trimmedTitle.length === 0) {
return;
}
setTodos((current) => [buildTodo(trimmedTitle), ...current]);
setPendingTitle("");
};
const handleToggle = (id: string, checked: boolean) => {
setTodos((current) =>
current.map((todo) =>
todo.id === id
? {
...todo,
completed: checked,
}
: todo,
),
);
};
const handleRemove = (id: string) => {
setTodos((current) => current.filter((todo) => todo.id !== id));
};
const clearCompleted = () => {
setTodos((current) => current.filter((todo) => !todo.completed));
};
const hasTodos = todos.length > 0;
const hasCompleted = completedCount > 0;
return (
<main className="flex min-h-screen items-center justify-center bg-surface-primary px-4 py-12">
<title>Shadcn To-Do List</title>
<section className="w-full max-w-2xl space-y-8 rounded-2xl border border-border bg-surface-secondary/60 p-8 shadow-lg">
<header className="space-y-3 text-center">
<p className="text-xs font-semibold uppercase tracking-wide text-content-secondary">
Demo
</p>
<h1 className="text-3xl font-semibold text-content-primary">
To-do list
</h1>
<p className="text-sm text-content-secondary">
Organize your day with a lightweight task list built with shadcn/ui
components.
</p>
</header>
<form className="space-y-3" onSubmit={handleSubmit}>
<div className="flex flex-col gap-3 sm:flex-row">
<Input
aria-label="Task description"
placeholder="What do you want to get done?"
value={pendingTitle}
onChange={(event) => setPendingTitle(event.target.value)}
autoFocus
/>
<Button
className="sm:self-start"
type="submit"
disabled={pendingTitle.trim().length === 0}
>
Add task
</Button>
</div>
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-content-secondary">
<span>
{remainingCount === 0
? "You're all caught up!"
: `${remainingCount} task${remainingCount === 1 ? "" : "s"} remaining`}
</span>
{hasCompleted && (
<Button
type="button"
variant="subtle"
size="sm"
onClick={clearCompleted}
className="px-3"
>
Clear completed ({completedCount})
</Button>
)}
</div>
</form>
<div className="space-y-3">
{!hasTodos && (
<div className="rounded-xl border border-dashed border-border bg-surface-primary/60 px-6 py-12 text-center">
<p className="text-sm font-medium text-content-secondary">
No tasks yet
</p>
<p className="text-xs text-content-secondary">
Add your first task to see it appear in the list.
</p>
</div>
)}
{hasTodos && (
<ul className="space-y-3">
{todos.map((todo) => {
const checkboxId = `todo-${todo.id}`;
return (
<li
key={todo.id}
className="flex items-center justify-between gap-4 rounded-xl border border-border bg-surface-primary px-4 py-3 shadow-sm"
>
<div className="flex grow items-center gap-3">
<Checkbox
id={checkboxId}
checked={todo.completed}
onCheckedChange={(value) =>
handleToggle(todo.id, value === true)
}
aria-labelledby={`${checkboxId}-label`}
/>
<Label
id={`${checkboxId}-label`}
htmlFor={checkboxId}
className={cn(
"text-base text-content-primary",
todo.completed &&
"text-content-secondary line-through",
)}
>
{todo.title}
</Label>
</div>
<Button
variant="subtle"
size="icon"
type="button"
onClick={() => handleRemove(todo.id)}
aria-label={`Delete ${todo.title}`}
>
<Trash2 className="h-4 w-4" aria-hidden />
</Button>
</li>
);
})}
</ul>
)}
</div>
</section>
</main>
);
};
export default TodoPage;
+2
View File
@@ -29,6 +29,7 @@ import WorkspacesPage from "./pages/WorkspacesPage/WorkspacesPage";
// - Pages that are secondary, not in the main navigation or not usually accessed
// - Pages that use heavy dependencies like charts or time libraries
const NotFoundPage = lazy(() => import("./pages/404Page/404Page"));
const TodoPage = lazy(() => import("./pages/TodoPage/TodoPage"));
const DeploymentSettingsLayout = lazy(
() => import("./modules/management/DeploymentSettingsLayout"),
);
@@ -403,6 +404,7 @@ export const router = createBrowserRouter(
<Route index element={<RequestOTPPage />} />
<Route path="change" element={<ChangePasswordPage />} />
</Route>
<Route path="/todo" element={<TodoPage />} />
{/* Dashboard routes */}
<Route element={<RequireAuth />}>