create-lego-one 2.0.10 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +179 -0
- package/dist/index.cjs.map +1 -1
- package/package.json +5 -3
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/e2e/auth.spec.ts +38 -0
- package/template/host/e2e/layout.spec.ts +38 -0
- package/template/host/modern.config.ts +19 -0
- package/template/host/package.json +71 -0
- package/template/host/playwright.config.ts +34 -0
- package/template/host/postcss.config.mjs +6 -0
- package/template/host/src/App.tsx +6 -0
- package/template/host/src/bootstrap.tsx +74 -0
- package/template/host/src/global.css +59 -0
- package/template/host/src/index.ts +2 -0
- package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
- package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
- package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
- package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
- package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
- package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
- package/template/host/src/kernel/auth/hooks.ts +174 -0
- package/template/host/src/kernel/auth/index.ts +5 -0
- package/template/host/src/kernel/auth/schemas.ts +27 -0
- package/template/host/src/kernel/auth/service.ts +197 -0
- package/template/host/src/kernel/auth/types.ts +36 -0
- package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
- package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
- package/template/host/src/kernel/channels/events.ts +27 -0
- package/template/host/src/kernel/channels/hooks.ts +168 -0
- package/template/host/src/kernel/channels/index.ts +6 -0
- package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
- package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
- package/template/host/src/kernel/channels/types.ts +112 -0
- package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
- package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
- package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
- package/template/host/src/kernel/components/index.ts +32 -0
- package/template/host/src/kernel/components/ui/alert.tsx +58 -0
- package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
- package/template/host/src/kernel/components/ui/badge.tsx +35 -0
- package/template/host/src/kernel/components/ui/button.tsx +50 -0
- package/template/host/src/kernel/components/ui/card.tsx +78 -0
- package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
- package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
- package/template/host/src/kernel/components/ui/index.ts +7 -0
- package/template/host/src/kernel/components/ui/input.tsx +24 -0
- package/template/host/src/kernel/components/ui/label.tsx +21 -0
- package/template/host/src/kernel/components/ui/popover.tsx +28 -0
- package/template/host/src/kernel/components/ui/progress.tsx +25 -0
- package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
- package/template/host/src/kernel/components/ui/select.tsx +155 -0
- package/template/host/src/kernel/components/ui/separator.tsx +28 -0
- package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
- package/template/host/src/kernel/components/ui/switch.tsx +26 -0
- package/template/host/src/kernel/components/ui/table.tsx +116 -0
- package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
- package/template/host/src/kernel/components/ui/toast.tsx +126 -0
- package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
- package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
- package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
- package/template/host/src/kernel/index.ts +48 -0
- package/template/host/src/kernel/lib/cn.ts +1 -0
- package/template/host/src/kernel/lib/utils.ts +36 -0
- package/template/host/src/kernel/plugins/Slot.tsx +41 -0
- package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
- package/template/host/src/kernel/plugins/index.ts +23 -0
- package/template/host/src/kernel/plugins/loader.ts +122 -0
- package/template/host/src/kernel/plugins/schemas.ts +54 -0
- package/template/host/src/kernel/plugins/store.ts +185 -0
- package/template/host/src/kernel/plugins/types.ts +103 -0
- package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
- package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
- package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
- package/template/host/src/kernel/providers/index.ts +3 -0
- package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
- package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
- package/template/host/src/kernel/rbac/hooks.ts +379 -0
- package/template/host/src/kernel/rbac/index.ts +6 -0
- package/template/host/src/kernel/rbac/service.ts +504 -0
- package/template/host/src/kernel/rbac/types.ts +164 -0
- package/template/host/src/kernel/rbac/utils.ts +34 -0
- package/template/host/src/kernel/shared-state/bridge.ts +31 -0
- package/template/host/src/kernel/shared-state/index.ts +3 -0
- package/template/host/src/kernel/shared-state/store.ts +62 -0
- package/template/host/src/kernel/shared-state/types.ts +60 -0
- package/template/host/src/kernel/use-migrations.ts +72 -0
- package/template/host/src/layout/MobileMenu.tsx +61 -0
- package/template/host/src/layout/Shell.tsx +42 -0
- package/template/host/src/layout/Sidebar.tsx +178 -0
- package/template/host/src/layout/Topbar.tsx +50 -0
- package/template/host/src/layout/index.ts +4 -0
- package/template/host/src/lib/pocketbase/client.ts +38 -0
- package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
- package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
- package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
- package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
- package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
- package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
- package/template/host/src/lib/pocketbase/index.ts +5 -0
- package/template/host/src/lib/pocketbase/migrations.ts +44 -0
- package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
- package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
- package/template/host/src/lib/pocketbase/seed.ts +113 -0
- package/template/host/src/lib/pocketbase/types.ts +102 -0
- package/template/host/src/modern.runtime.ts +26 -0
- package/template/host/src/plugins.d.ts +9 -0
- package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
- package/template/host/src/routes/_.tsx +6 -0
- package/template/host/src/routes/dashboard._.tsx +41 -0
- package/template/host/src/routes/index.tsx +93 -0
- package/template/host/src/routes/login.tsx +36 -0
- package/template/host/src/saas.config.ts +52 -0
- package/template/host/src/test/setup.ts +65 -0
- package/template/host/src/test/utils.tsx +69 -0
- package/template/host/src/test/vitest-globals.d.ts +19 -0
- package/template/host/src/vite-env.d.ts +16 -0
- package/template/host/tailwind.config.ts +77 -0
- package/template/host/tsconfig.json +19 -0
- package/template/host/vitest.config.ts +30 -0
- package/template/nginx.conf +72 -0
- package/template/package.json +44 -0
- package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
- package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
- package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
- package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
- package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
- package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
- package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
- package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
- package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
- package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
- package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
- package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
- package/template/tsconfig.json +8 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useForm } from 'react-hook-form';
|
|
2
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
3
|
+
import { todoFormSchema, type TodoFormInput } from '../schemas';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
|
|
6
|
+
interface TodoFormProps {
|
|
7
|
+
onSubmit: (data: TodoFormInput) => void;
|
|
8
|
+
isLoading?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TodoForm({ onSubmit, isLoading }: TodoFormProps) {
|
|
12
|
+
const {
|
|
13
|
+
register,
|
|
14
|
+
handleSubmit,
|
|
15
|
+
setValue,
|
|
16
|
+
formState: { errors },
|
|
17
|
+
} = useForm<TodoFormInput>({
|
|
18
|
+
resolver: zodResolver(todoFormSchema),
|
|
19
|
+
defaultValues: {
|
|
20
|
+
priority: 'medium',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
26
|
+
<div className="space-y-2">
|
|
27
|
+
<label htmlFor="title" className="text-sm font-medium">
|
|
28
|
+
Title *
|
|
29
|
+
</label>
|
|
30
|
+
<input
|
|
31
|
+
id="title"
|
|
32
|
+
placeholder="What needs to be done?"
|
|
33
|
+
{...register('title')}
|
|
34
|
+
className="w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
35
|
+
/>
|
|
36
|
+
{errors.title && (
|
|
37
|
+
<p className="text-sm text-destructive">{errors.title.message}</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="space-y-2">
|
|
42
|
+
<label htmlFor="description" className="text-sm font-medium">
|
|
43
|
+
Description
|
|
44
|
+
</label>
|
|
45
|
+
<input
|
|
46
|
+
id="description"
|
|
47
|
+
placeholder="Add details (optional)"
|
|
48
|
+
{...register('description')}
|
|
49
|
+
className="w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
50
|
+
/>
|
|
51
|
+
{errors.description && (
|
|
52
|
+
<p className="text-sm text-destructive">{errors.description.message}</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<label htmlFor="priority" className="text-sm font-medium">
|
|
58
|
+
Priority *
|
|
59
|
+
</label>
|
|
60
|
+
<select
|
|
61
|
+
{...register('priority')}
|
|
62
|
+
className="w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
63
|
+
>
|
|
64
|
+
<option value="low">Low</option>
|
|
65
|
+
<option value="medium">Medium</option>
|
|
66
|
+
<option value="high">High</option>
|
|
67
|
+
</select>
|
|
68
|
+
{errors.priority && (
|
|
69
|
+
<p className="text-sm text-destructive">{errors.priority.message}</p>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="space-y-2">
|
|
74
|
+
<label htmlFor="dueDate" className="text-sm font-medium">
|
|
75
|
+
Due Date
|
|
76
|
+
</label>
|
|
77
|
+
<input
|
|
78
|
+
id="dueDate"
|
|
79
|
+
type="date"
|
|
80
|
+
{...register('dueDate')}
|
|
81
|
+
className="w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<button
|
|
86
|
+
type="submit"
|
|
87
|
+
disabled={isLoading}
|
|
88
|
+
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
89
|
+
>
|
|
90
|
+
{isLoading ? 'Creating...' : 'Create Todo'}
|
|
91
|
+
</button>
|
|
92
|
+
</form>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { CheckSquare, Square, Trash2, Edit2, Calendar } from 'lucide-react';
|
|
3
|
+
import { TodoWithOwner } from '../types';
|
|
4
|
+
import { useUpdateTodo } from '../hooks/useUpdateTodo';
|
|
5
|
+
import { useDeleteTodo } from '../hooks/useDeleteTodo';
|
|
6
|
+
import { cn } from '../lib/utils';
|
|
7
|
+
import { formatRelativeTime } from '../lib/utils';
|
|
8
|
+
|
|
9
|
+
interface TodoItemProps {
|
|
10
|
+
todo: TodoWithOwner;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const priorityColors = {
|
|
14
|
+
low: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
|
15
|
+
medium: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
|
|
16
|
+
high: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function TodoItem({ todo }: TodoItemProps) {
|
|
20
|
+
const updateTodo = useUpdateTodo();
|
|
21
|
+
const deleteTodo = useDeleteTodo();
|
|
22
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
23
|
+
|
|
24
|
+
const handleToggleComplete = () => {
|
|
25
|
+
updateTodo.mutate({
|
|
26
|
+
id: todo.id,
|
|
27
|
+
data: { completed: !todo.completed },
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleDelete = () => {
|
|
32
|
+
if (confirm('Are you sure you want to delete this todo?')) {
|
|
33
|
+
deleteTodo.mutate(todo.id);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={cn(
|
|
40
|
+
'group rounded-lg border p-4 transition-colors hover:bg-muted/50',
|
|
41
|
+
todo.completed && 'opacity-60'
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<div className="flex items-start gap-3">
|
|
45
|
+
{/* Checkbox */}
|
|
46
|
+
<button
|
|
47
|
+
onClick={handleToggleComplete}
|
|
48
|
+
disabled={updateTodo.isPending}
|
|
49
|
+
className="mt-0.5 shrink-0"
|
|
50
|
+
>
|
|
51
|
+
{todo.completed ? (
|
|
52
|
+
<CheckSquare className="h-5 w-5 text-primary" />
|
|
53
|
+
) : (
|
|
54
|
+
<Square className="h-5 w-5 text-muted-foreground" />
|
|
55
|
+
)}
|
|
56
|
+
</button>
|
|
57
|
+
|
|
58
|
+
{/* Content */}
|
|
59
|
+
<div className="flex-1 space-y-1">
|
|
60
|
+
<div className="flex items-start justify-between gap-2">
|
|
61
|
+
<h4
|
|
62
|
+
className={cn(
|
|
63
|
+
'font-medium',
|
|
64
|
+
todo.completed && 'line-through text-muted-foreground'
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
{todo.title}
|
|
68
|
+
</h4>
|
|
69
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
|
|
70
|
+
<button
|
|
71
|
+
onClick={() => setIsEditing(true)}
|
|
72
|
+
className="rounded p-1 hover:bg-muted"
|
|
73
|
+
>
|
|
74
|
+
<Edit2 className="h-4 w-4" />
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
onClick={handleDelete}
|
|
78
|
+
disabled={deleteTodo.isPending}
|
|
79
|
+
className="rounded p-1 hover:bg-destructive hover:text-destructive-foreground"
|
|
80
|
+
>
|
|
81
|
+
<Trash2 className="h-4 w-4" />
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{todo.description && (
|
|
87
|
+
<p className="text-sm text-muted-foreground">{todo.description}</p>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
91
|
+
<span className={cn('rounded-full px-2 py-0.5', priorityColors[todo.priority])}>
|
|
92
|
+
{todo.priority}
|
|
93
|
+
</span>
|
|
94
|
+
{todo.dueDate && (
|
|
95
|
+
<span className="flex items-center gap-1">
|
|
96
|
+
<Calendar className="h-3 w-3" />
|
|
97
|
+
{formatRelativeTime(todo.dueDate)}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
{todo.ownerName && (
|
|
101
|
+
<span>Created by {todo.ownerName}</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Edit dialog would go here - for now using inline state */}
|
|
108
|
+
{isEditing && (
|
|
109
|
+
<div className="mt-4 pt-4 border-t">
|
|
110
|
+
<p className="text-sm text-muted-foreground">Edit functionality coming soon</p>
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => setIsEditing(false)}
|
|
113
|
+
className="mt-2 text-sm text-primary hover:underline"
|
|
114
|
+
>
|
|
115
|
+
Close
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TodoWithOwner } from '../types';
|
|
2
|
+
import { TodoItem } from './TodoItem';
|
|
3
|
+
|
|
4
|
+
interface TodoListProps {
|
|
5
|
+
todos: TodoWithOwner[];
|
|
6
|
+
isLoading?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function TodoList({ todos, isLoading }: TodoListProps) {
|
|
10
|
+
if (isLoading) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-3">
|
|
13
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
14
|
+
<div key={i} className="h-20 animate-pulse rounded-lg bg-muted" />
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (todos.length === 0) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
23
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
|
24
|
+
<span className="text-2xl">📝</span>
|
|
25
|
+
</div>
|
|
26
|
+
<h3 className="mt-4 text-lg font-semibold">No todos found</h3>
|
|
27
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
28
|
+
Create your first todo to get started
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-3">
|
|
36
|
+
{todos.map((todo) => (
|
|
37
|
+
<TodoItem key={todo.id} todo={todo} />
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 222.2 84% 4.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
13
|
+
--primary: 221.2 83.2% 53.3%;
|
|
14
|
+
--primary-foreground: 210 40% 98%;
|
|
15
|
+
--secondary: 210 40% 96.1%;
|
|
16
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
17
|
+
--muted: 210 40% 96.1%;
|
|
18
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
19
|
+
--accent: 210 40% 96.1%;
|
|
20
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
21
|
+
--destructive: 0 84.2% 60.2%;
|
|
22
|
+
--destructive-foreground: 210 40% 98%;
|
|
23
|
+
--border: 214.3 31.8% 91.4%;
|
|
24
|
+
--input: 214.3 31.8% 91.4%;
|
|
25
|
+
--ring: 221.2 83.2% 53.3%;
|
|
26
|
+
--radius: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark {
|
|
30
|
+
--background: 222.2 84% 4.9%;
|
|
31
|
+
--foreground: 210 40% 98%;
|
|
32
|
+
--card: 222.2 84% 4.9%;
|
|
33
|
+
--card-foreground: 210 40% 98%;
|
|
34
|
+
--popover: 222.2 84% 4.9%;
|
|
35
|
+
--popover-foreground: 210 40% 98%;
|
|
36
|
+
--primary: 217.2 91.2% 59.8%;
|
|
37
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
38
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
39
|
+
--secondary-foreground: 210 40% 98%;
|
|
40
|
+
--muted: 217.2 32.6% 17.5%;
|
|
41
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
42
|
+
--accent: 217.2 32.6% 17.5%;
|
|
43
|
+
--accent-foreground: 210 40% 98%;
|
|
44
|
+
--destructive: 0 62.8% 30.6%;
|
|
45
|
+
--destructive-foreground: 210 40% 98%;
|
|
46
|
+
--border: 217.2 32.6% 17.5%;
|
|
47
|
+
--input: 217.2 32.6% 17.5%;
|
|
48
|
+
--ring: 224.3 76.3% 48%;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@layer base {
|
|
53
|
+
* {
|
|
54
|
+
@apply border-border;
|
|
55
|
+
}
|
|
56
|
+
body {
|
|
57
|
+
@apply bg-background text-foreground;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { usePocketBase } from './usePocketBase';
|
|
3
|
+
import type { TodoFormData } from '../types';
|
|
4
|
+
|
|
5
|
+
export function useCreateTodo() {
|
|
6
|
+
const pb = usePocketBase();
|
|
7
|
+
const queryClient = useQueryClient();
|
|
8
|
+
|
|
9
|
+
return useMutation({
|
|
10
|
+
mutationFn: async (data: TodoFormData) => {
|
|
11
|
+
if (!pb) {
|
|
12
|
+
throw new Error('PocketBase not initialized');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
16
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
17
|
+
|
|
18
|
+
if (!state?.organization?.id) {
|
|
19
|
+
throw new Error('No organization selected');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = await pb.collection('todos').create({
|
|
23
|
+
title: data.title,
|
|
24
|
+
description: data.description || '',
|
|
25
|
+
completed: false,
|
|
26
|
+
priority: data.priority,
|
|
27
|
+
dueDate: data.dueDate || null,
|
|
28
|
+
organizationId: state.organization.id,
|
|
29
|
+
ownerId: state.user?.id,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
},
|
|
34
|
+
onSuccess: () => {
|
|
35
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
36
|
+
const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
|
|
37
|
+
if (channelBus) {
|
|
38
|
+
channelBus.publish('lego:toast', {
|
|
39
|
+
channel: 'lego:toast',
|
|
40
|
+
data: {
|
|
41
|
+
type: 'success',
|
|
42
|
+
title: 'Todo created',
|
|
43
|
+
description: 'Your todo has been created successfully',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
onError: (error: any) => {
|
|
49
|
+
const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
|
|
50
|
+
if (channelBus) {
|
|
51
|
+
channelBus.publish('lego:toast', {
|
|
52
|
+
channel: 'lego:toast',
|
|
53
|
+
data: {
|
|
54
|
+
type: 'error',
|
|
55
|
+
title: 'Failed to create todo',
|
|
56
|
+
description: error.message || 'An error occurred',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { usePocketBase } from './usePocketBase';
|
|
3
|
+
|
|
4
|
+
export function useDeleteTodo() {
|
|
5
|
+
const pb = usePocketBase();
|
|
6
|
+
const queryClient = useQueryClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
mutationFn: async (id: string) => {
|
|
10
|
+
if (!pb) {
|
|
11
|
+
throw new Error('PocketBase not initialized');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await pb.collection('todos').delete(id);
|
|
15
|
+
|
|
16
|
+
return id;
|
|
17
|
+
},
|
|
18
|
+
onSuccess: () => {
|
|
19
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
20
|
+
const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
|
|
21
|
+
if (channelBus) {
|
|
22
|
+
channelBus.publish('lego:toast', {
|
|
23
|
+
channel: 'lego:toast',
|
|
24
|
+
data: {
|
|
25
|
+
type: 'success',
|
|
26
|
+
title: 'Todo deleted',
|
|
27
|
+
description: 'Your todo has been deleted',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
onError: (error: any) => {
|
|
33
|
+
const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
|
|
34
|
+
if (channelBus) {
|
|
35
|
+
channelBus.publish('lego:toast', {
|
|
36
|
+
channel: 'lego:toast',
|
|
37
|
+
data: {
|
|
38
|
+
type: 'error',
|
|
39
|
+
title: 'Failed to delete todo',
|
|
40
|
+
description: error.message || 'An error occurred',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import PocketBase from 'pocketbase';
|
|
3
|
+
|
|
4
|
+
export function usePocketBase() {
|
|
5
|
+
const [pb, setPb] = useState<PocketBase | null>(null);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
9
|
+
|
|
10
|
+
if (!kernelState) {
|
|
11
|
+
console.error('[Todo] Kernel state bridge not found');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pbUrl = import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
|
|
16
|
+
const client = new PocketBase(pbUrl);
|
|
17
|
+
|
|
18
|
+
// Sync auth token
|
|
19
|
+
const state = kernelState.useGlobalKernelState.getState();
|
|
20
|
+
if (state.token) {
|
|
21
|
+
client.authStore.save(state.token, null as any);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const unsubscribe = kernelState.useGlobalKernelState.subscribe((newState: any) => {
|
|
25
|
+
if (newState.token && newState.token !== client.authStore.token) {
|
|
26
|
+
client.authStore.save(newState.token, null as any);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
setPb(client);
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
unsubscribe();
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return pb;
|
|
38
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { usePocketBase } from './usePocketBase';
|
|
3
|
+
import type { TodoWithOwner, TodoFilters } from '../types';
|
|
4
|
+
|
|
5
|
+
export function useTodos(filters: TodoFilters = {}) {
|
|
6
|
+
const pb = usePocketBase();
|
|
7
|
+
|
|
8
|
+
return useQuery({
|
|
9
|
+
queryKey: ['todos', filters],
|
|
10
|
+
queryFn: async (): Promise<TodoWithOwner[]> => {
|
|
11
|
+
if (!pb) {
|
|
12
|
+
throw new Error('PocketBase not initialized');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
16
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
17
|
+
|
|
18
|
+
if (!state?.organization?.id) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const orgId = state.organization.id;
|
|
23
|
+
|
|
24
|
+
// Build filter
|
|
25
|
+
let filterParts: string[] = [`organizationId = "${orgId}"`];
|
|
26
|
+
|
|
27
|
+
if (filters.status === 'active') {
|
|
28
|
+
filterParts.push('completed = false');
|
|
29
|
+
} else if (filters.status === 'completed') {
|
|
30
|
+
filterParts.push('completed = true');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (filters.priority && filters.priority !== 'all') {
|
|
34
|
+
filterParts.push(`priority = "${filters.priority}"`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (filters.search) {
|
|
38
|
+
filterParts.push(`title ~ "${filters.search}"`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await pb.collection('todos').getList(1, 50, {
|
|
42
|
+
filter: filterParts.join(' && '),
|
|
43
|
+
sort: filters.status === 'completed' ? '-updated,-created' : '-created',
|
|
44
|
+
expand: 'ownerId',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return result.items.map((item: any) => ({
|
|
48
|
+
id: item.id,
|
|
49
|
+
title: item.title,
|
|
50
|
+
description: item.description,
|
|
51
|
+
completed: item.completed,
|
|
52
|
+
priority: item.priority,
|
|
53
|
+
dueDate: item.dueDate,
|
|
54
|
+
organizationId: item.organizationId,
|
|
55
|
+
ownerId: item.ownerId,
|
|
56
|
+
created: item.created,
|
|
57
|
+
updated: item.updated,
|
|
58
|
+
ownerName: item.expand?.ownerId?.name || item.expand?.ownerId?.email,
|
|
59
|
+
ownerEmail: item.expand?.ownerId?.email,
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
enabled: !!pb,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { usePocketBase } from './usePocketBase';
|
|
3
|
+
|
|
4
|
+
export function useUpdateTodo() {
|
|
5
|
+
const pb = usePocketBase();
|
|
6
|
+
const queryClient = useQueryClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
mutationFn: async ({ id, data }: { id: string; data: Partial<any> }) => {
|
|
10
|
+
if (!pb) {
|
|
11
|
+
throw new Error('PocketBase not initialized');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result = await pb.collection('todos').update(id, data);
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
},
|
|
18
|
+
onSuccess: () => {
|
|
19
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
20
|
+
},
|
|
21
|
+
onError: (error: any) => {
|
|
22
|
+
const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
|
|
23
|
+
if (channelBus) {
|
|
24
|
+
channelBus.publish('lego:toast', {
|
|
25
|
+
channel: 'lego:toast',
|
|
26
|
+
data: {
|
|
27
|
+
type: 'error',
|
|
28
|
+
title: 'Failed to update todo',
|
|
29
|
+
description: error.message || 'An error occurred',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatRelativeTime(dateString: string): string {
|
|
9
|
+
const date = new Date(dateString);
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const diffMs = now.getTime() - date.getTime();
|
|
12
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
13
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
14
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
15
|
+
|
|
16
|
+
if (diffMins < 1) return 'just now';
|
|
17
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
18
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
19
|
+
return `${diffDays}d ago`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Plus } from 'lucide-react';
|
|
3
|
+
import { TodoList } from '../components/TodoList';
|
|
4
|
+
import { TodoDialog } from '../components/TodoDialog';
|
|
5
|
+
import { TodoFilters } from '../components/TodoFilters';
|
|
6
|
+
import { useTodos } from '../hooks/useTodos';
|
|
7
|
+
import { useCreateTodo } from '../hooks/useCreateTodo';
|
|
8
|
+
import { TodoFilters as TodoFiltersType } from '../types';
|
|
9
|
+
import { todoFormSchema, type TodoFormInput } from '../schemas';
|
|
10
|
+
|
|
11
|
+
export function TodoPage() {
|
|
12
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
13
|
+
const [filters, setFilters] = useState<TodoFiltersType>({
|
|
14
|
+
status: 'all',
|
|
15
|
+
priority: 'all',
|
|
16
|
+
search: '',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const { data: todos = [], isLoading } = useTodos(filters);
|
|
20
|
+
const createTodo = useCreateTodo();
|
|
21
|
+
|
|
22
|
+
const handleCreateTodo = (data: TodoFormInput) => {
|
|
23
|
+
createTodo.mutate(data, {
|
|
24
|
+
onSuccess: () => {
|
|
25
|
+
setIsDialogOpen(false);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const filteredTodos = todos.filter((todo) => {
|
|
31
|
+
// Status filter
|
|
32
|
+
if (filters.status === 'active' && todo.completed) return false;
|
|
33
|
+
if (filters.status === 'completed' && !todo.completed) return false;
|
|
34
|
+
|
|
35
|
+
// Priority filter
|
|
36
|
+
if (filters.priority !== 'all' && todo.priority !== filters.priority) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Search filter
|
|
41
|
+
if (filters.search) {
|
|
42
|
+
const searchLower = filters.search.toLowerCase();
|
|
43
|
+
const matchesTitle = todo.title.toLowerCase().includes(searchLower);
|
|
44
|
+
const matchesDescription = todo.description?.toLowerCase().includes(searchLower);
|
|
45
|
+
if (!matchesTitle && !matchesDescription) return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="container mx-auto max-w-4xl py-8 px-4">
|
|
53
|
+
{/* Header */}
|
|
54
|
+
<div className="mb-8">
|
|
55
|
+
<h1 className="text-3xl font-bold tracking-tight">Todos</h1>
|
|
56
|
+
<p className="mt-2 text-muted-foreground">
|
|
57
|
+
Manage your tasks and stay organized
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Actions */}
|
|
62
|
+
<div className="mb-6 flex items-center justify-between">
|
|
63
|
+
<TodoFilters
|
|
64
|
+
filters={filters}
|
|
65
|
+
onFiltersChange={setFilters}
|
|
66
|
+
todoCount={filteredTodos.length}
|
|
67
|
+
/>
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => setIsDialogOpen(true)}
|
|
70
|
+
className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
71
|
+
>
|
|
72
|
+
<Plus className="h-4 w-4" />
|
|
73
|
+
New Todo
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Todo List */}
|
|
78
|
+
<TodoList todos={filteredTodos} isLoading={isLoading} />
|
|
79
|
+
|
|
80
|
+
{/* Create Dialog */}
|
|
81
|
+
<TodoDialog
|
|
82
|
+
open={isDialogOpen}
|
|
83
|
+
onOpenChange={setIsDialogOpen}
|
|
84
|
+
onSubmit={handleCreateTodo}
|
|
85
|
+
isLoading={createTodo.isPending}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|