@knotpad/app 0.1.0
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/README.md +167 -0
- package/app/(app)/calendar/page.tsx +57 -0
- package/app/(app)/error.tsx +35 -0
- package/app/(app)/graph/page.tsx +32 -0
- package/app/(app)/guide/page.tsx +21 -0
- package/app/(app)/kanban/loading.tsx +24 -0
- package/app/(app)/kanban/page.tsx +59 -0
- package/app/(app)/layout.tsx +122 -0
- package/app/(app)/list/loading.tsx +21 -0
- package/app/(app)/list/page.tsx +137 -0
- package/app/(app)/loading.tsx +18 -0
- package/app/(app)/notes/[noteId]/page.tsx +84 -0
- package/app/(app)/notes/layout.tsx +30 -0
- package/app/(app)/notes/page.tsx +39 -0
- package/app/(app)/page.tsx +5 -0
- package/app/(app)/settings/agent-token/page.tsx +59 -0
- package/app/(app)/settings/backup/page.tsx +49 -0
- package/app/(app)/settings/billing/page.tsx +53 -0
- package/app/(app)/settings/calendar/page.tsx +41 -0
- package/app/(app)/settings/layout.test.tsx +39 -0
- package/app/(app)/settings/layout.tsx +71 -0
- package/app/(app)/settings/page.tsx +4 -0
- package/app/(app)/settings/security/page.tsx +43 -0
- package/app/(app)/settings/team/page.tsx +74 -0
- package/app/(app)/settings/workspace/page.tsx +27 -0
- package/app/(app)/tasks/[taskId]/page.tsx +79 -0
- package/app/(auth)/forgot-password/page.tsx +106 -0
- package/app/(auth)/guest/page.tsx +56 -0
- package/app/(auth)/layout.tsx +13 -0
- package/app/(auth)/login/page.tsx +14 -0
- package/app/(auth)/register/page.tsx +193 -0
- package/app/(auth)/reset-password/page.tsx +138 -0
- package/app/api/account/claim/route.tsx +135 -0
- package/app/api/admin/backfill-encryption/route.tsx +43 -0
- package/app/api/admin/license/route.tsx +42 -0
- package/app/api/auth/2fa/route.tsx +148 -0
- package/app/api/auth/[...nextauth]/route.tsx +3 -0
- package/app/api/auth/change-password/route.tsx +61 -0
- package/app/api/auth/check-2fa/route.tsx +19 -0
- package/app/api/auth/forgot-password/route.tsx +65 -0
- package/app/api/auth/reset-password/route.tsx +52 -0
- package/app/api/auth/verify-2fa/route.tsx +88 -0
- package/app/api/backup/download/db/route.ts +29 -0
- package/app/api/backup/download/notes/route.ts +25 -0
- package/app/api/backup/settings/route.ts +92 -0
- package/app/api/billing/checkout/route.tsx +81 -0
- package/app/api/billing/migrate/route.tsx +163 -0
- package/app/api/billing/portal/route.tsx +24 -0
- package/app/api/billing/setup-intent/route.tsx +55 -0
- package/app/api/billing/status/route.tsx +36 -0
- package/app/api/billing/subscribe/route.tsx +85 -0
- package/app/api/billing/webhook/route.tsx +199 -0
- package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
- package/app/api/calendar-feeds/events/route.tsx +82 -0
- package/app/api/calendar-feeds/route.tsx +52 -0
- package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
- package/app/api/cron/calendar-feeds/route.tsx +31 -0
- package/app/api/cron/stale-tasks/route.tsx +51 -0
- package/app/api/cron/sync/route.tsx +34 -0
- package/app/api/devices/[deviceId]/route.tsx +25 -0
- package/app/api/devices/route.tsx +41 -0
- package/app/api/export/route.tsx +40 -0
- package/app/api/feedback/route.tsx +54 -0
- package/app/api/folders/[folderId]/route.tsx +51 -0
- package/app/api/folders/route.tsx +37 -0
- package/app/api/graph/route.tsx +242 -0
- package/app/api/guest/route.tsx +58 -0
- package/app/api/health/route.tsx +10 -0
- package/app/api/holidays/countries/route.tsx +14 -0
- package/app/api/holidays/route.tsx +49 -0
- package/app/api/holidays/states/route.tsx +21 -0
- package/app/api/invites/[token]/route.tsx +131 -0
- package/app/api/invites/route.tsx +74 -0
- package/app/api/mcp/generate-token/route.tsx +55 -0
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
- package/app/api/notes/[noteId]/export/route.tsx +45 -0
- package/app/api/notes/[noteId]/route.tsx +360 -0
- package/app/api/notes/route.tsx +112 -0
- package/app/api/notifications/route.tsx +44 -0
- package/app/api/register/route.tsx +67 -0
- package/app/api/restore/route.tsx +148 -0
- package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
- package/app/api/sync/conflicts/route.tsx +48 -0
- package/app/api/sync/status/route.tsx +49 -0
- package/app/api/sync/trigger/route.tsx +15 -0
- package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
- package/app/api/tasks/[taskId]/route.tsx +259 -0
- package/app/api/tasks/bulk/route.tsx +133 -0
- package/app/api/tasks/route.tsx +36 -0
- package/app/api/workspace/active/route.tsx +39 -0
- package/app/api/workspace/create-team/route.tsx +42 -0
- package/app/api/workspace/kanban-statuses/route.tsx +71 -0
- package/app/api/workspace/members/[memberId]/route.tsx +69 -0
- package/app/api/workspace/route.tsx +24 -0
- package/app/download/page.tsx +170 -0
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +1 -0
- package/app/generated/prisma/client.js +5 -0
- package/app/generated/prisma/default.d.ts +1 -0
- package/app/generated/prisma/default.js +5 -0
- package/app/generated/prisma/edge.d.ts +1 -0
- package/app/generated/prisma/edge.js +497 -0
- package/app/generated/prisma/index-browser.js +523 -0
- package/app/generated/prisma/index.d.ts +46376 -0
- package/app/generated/prisma/index.js +497 -0
- package/app/generated/prisma/package.json +144 -0
- package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/app/generated/prisma/runtime/client.d.ts +3386 -0
- package/app/generated/prisma/runtime/client.js +86 -0
- package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
- package/app/generated/prisma/runtime/index-browser.js +6 -0
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
- package/app/generated/prisma/schema.prisma +456 -0
- package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
- package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
- package/app/globals.css +54 -0
- package/app/invite/[token]/page.tsx +52 -0
- package/app/layout.tsx +90 -0
- package/app/mcp/route.tsx +430 -0
- package/app/opengraph-image.tsx +120 -0
- package/app/page.tsx +398 -0
- package/app/privacy/page.tsx +69 -0
- package/app/robots.tsx +25 -0
- package/app/sitemap.tsx +36 -0
- package/app/terms/page.tsx +69 -0
- package/app/upgrade/page.tsx +75 -0
- package/auth.config.ts +33 -0
- package/auth.ts +79 -0
- package/bin/brief.js +224 -0
- package/components/auth/login-form.tsx +302 -0
- package/components/auth/password-checklist.tsx +31 -0
- package/components/auth/password-input.tsx +36 -0
- package/components/auth/switch-account-button.test.tsx +22 -0
- package/components/auth/switch-account-button.tsx +19 -0
- package/components/auth/two-factor-input.tsx +116 -0
- package/components/billing/billing-dashboard.tsx +265 -0
- package/components/billing/card-form.tsx +210 -0
- package/components/billing/claim-account-form.tsx +99 -0
- package/components/branding/app-logo.test.tsx +20 -0
- package/components/branding/app-logo.tsx +25 -0
- package/components/calendar/calendar-agenda.tsx +150 -0
- package/components/calendar/calendar-drag.test.tsx +177 -0
- package/components/calendar/calendar-grid.tsx +357 -0
- package/components/calendar/calendar-hooks.test.tsx +27 -0
- package/components/calendar/calendar-hooks.ts +351 -0
- package/components/calendar/calendar-toolbar.test.tsx +68 -0
- package/components/calendar/calendar-toolbar.tsx +291 -0
- package/components/calendar/calendar-types.ts +148 -0
- package/components/calendar/calendar-view.test.tsx +295 -0
- package/components/calendar/calendar-view.tsx +307 -0
- package/components/calendar/day-detail-popover.tsx +174 -0
- package/components/calendar/task-chip.tsx +86 -0
- package/components/command/command-palette.test.tsx +33 -0
- package/components/command/command-palette.tsx +310 -0
- package/components/download-cta.tsx +87 -0
- package/components/feedback/feedback-popup.tsx +207 -0
- package/components/graph/graph-draw.ts +337 -0
- package/components/graph/graph-overlays.tsx +160 -0
- package/components/graph/graph-page.test.tsx +131 -0
- package/components/graph/graph-page.tsx +263 -0
- package/components/graph/graph-types.ts +47 -0
- package/components/graph/graph-view.tsx +322 -0
- package/components/guide/guide-view.tsx +522 -0
- package/components/kanban/kanban-board.test.tsx +128 -0
- package/components/kanban/kanban-board.tsx +361 -0
- package/components/kanban/kanban-card-menu.tsx +102 -0
- package/components/kanban/kanban-card.tsx +227 -0
- package/components/kanban/kanban-column.tsx +49 -0
- package/components/kanban/kanban-status-context.tsx +28 -0
- package/components/landing/calendar-sandbox.test.tsx +15 -0
- package/components/landing/calendar-sandbox.tsx +107 -0
- package/components/landing/graph-sandbox.test.tsx +27 -0
- package/components/landing/graph-sandbox.tsx +80 -0
- package/components/landing/kanban-sandbox.test.tsx +24 -0
- package/components/landing/kanban-sandbox.tsx +101 -0
- package/components/landing/landing-showcase.test.tsx +21 -0
- package/components/landing/landing-showcase.tsx +54 -0
- package/components/landing/list-sandbox.tsx +86 -0
- package/components/landing/mock-workspace.ts +168 -0
- package/components/landing/notes-sandbox.test.tsx +14 -0
- package/components/landing/notes-sandbox.tsx +88 -0
- package/components/layout/app-shell.tsx +83 -0
- package/components/layout/backup-scheduler.tsx +122 -0
- package/components/layout/bottom-nav.tsx +43 -0
- package/components/layout/icon-bar.test.tsx +29 -0
- package/components/layout/icon-bar.tsx +118 -0
- package/components/layout/mobile-top-bar.tsx +68 -0
- package/components/layout/notes-panel-folder.tsx +127 -0
- package/components/layout/notes-panel-note-item.tsx +140 -0
- package/components/layout/notes-panel-task-tab.tsx +63 -0
- package/components/layout/notes-panel-types.ts +44 -0
- package/components/layout/notes-panel.tsx +476 -0
- package/components/layout/notification-bell.tsx +251 -0
- package/components/layout/paywall-screen.tsx +41 -0
- package/components/layout/pro-banner.tsx +76 -0
- package/components/layout/sw-register.tsx +27 -0
- package/components/layout/workspace-switcher.tsx +90 -0
- package/components/notes/mobile-bottom-sheet.tsx +99 -0
- package/components/notes/note-editor-context-menu.tsx +47 -0
- package/components/notes/note-editor-dom.ts +33 -0
- package/components/notes/note-editor-dropdowns.tsx +484 -0
- package/components/notes/note-editor-hooks.ts +692 -0
- package/components/notes/note-editor-keyboard.ts +305 -0
- package/components/notes/note-editor-overlay.tsx +90 -0
- package/components/notes/note-editor.test.tsx +372 -0
- package/components/notes/note-editor.tsx +662 -0
- package/components/notes/note-preview-pane.tsx +156 -0
- package/components/notes/note-tabs.tsx +120 -0
- package/components/notes/note-types.tsx +157 -0
- package/components/settings/accept-invite.tsx +108 -0
- package/components/settings/agent-token-settings.tsx +369 -0
- package/components/settings/backup-restore-settings.test.tsx +25 -0
- package/components/settings/backup-restore-settings.tsx +327 -0
- package/components/settings/calendar-feeds-settings.tsx +489 -0
- package/components/settings/calendar-general-settings.tsx +174 -0
- package/components/settings/confirm-danger-action.test.tsx +215 -0
- package/components/settings/confirm-danger-action.tsx +65 -0
- package/components/settings/security-settings.tsx +252 -0
- package/components/settings/settings-guidance.test.tsx +98 -0
- package/components/settings/team-settings.tsx +319 -0
- package/components/settings/two-factor-auth.tsx +296 -0
- package/components/settings/workspace-settings-client.tsx +363 -0
- package/components/settings/workspace-settings-form.tsx +73 -0
- package/components/sync/conflict-viewer.tsx +247 -0
- package/components/sync/sync-indicator.tsx +171 -0
- package/components/tasks/snippet-thread.tsx +119 -0
- package/components/tasks/status-dot.tsx +47 -0
- package/components/tasks/task-badge.tsx +43 -0
- package/components/tasks/task-detail.test.tsx +187 -0
- package/components/tasks/task-detail.tsx +458 -0
- package/components/tasks/task-list-filters.test.tsx +75 -0
- package/components/tasks/task-list-filters.tsx +163 -0
- package/components/tasks/task-list-types.ts +20 -0
- package/components/tasks/task-list.test.tsx +175 -0
- package/components/tasks/task-list.tsx +481 -0
- package/components/tasks/task-row.tsx +85 -0
- package/components/tasks/task-table-row.tsx +259 -0
- package/components/ui/skeleton.tsx +3 -0
- package/components/ui/toast.test.tsx +42 -0
- package/components/ui/toast.tsx +70 -0
- package/electron/main.ts +251 -0
- package/electron/preload.ts +56 -0
- package/instrumentation.tsx +23 -0
- package/lib/api-error.ts +50 -0
- package/lib/backup/backup-runner.test.ts +32 -0
- package/lib/backup/backup-runner.ts +19 -0
- package/lib/backup/backup-schedule.test.ts +23 -0
- package/lib/backup/backup-schedule.ts +55 -0
- package/lib/backup/backup-settings.test.ts +30 -0
- package/lib/backup/backup-settings.ts +27 -0
- package/lib/backup/export-notes-zip.test.ts +26 -0
- package/lib/backup/export-notes-zip.ts +82 -0
- package/lib/backup/export-workspace-backup.test.ts +17 -0
- package/lib/backup/export-workspace-backup.ts +77 -0
- package/lib/backup/restore-workspace-from-export.test.ts +18 -0
- package/lib/backup/restore-workspace-from-export.ts +183 -0
- package/lib/backup/types.ts +14 -0
- package/lib/brand-icons.ts +1 -0
- package/lib/calendar-feed-crypto.ts +38 -0
- package/lib/calendar-feed.ts +239 -0
- package/lib/client/online-status.ts +47 -0
- package/lib/conflict-resolver.test.ts +57 -0
- package/lib/conflict-resolver.ts +240 -0
- package/lib/db-init.ts +79 -0
- package/lib/email.ts +159 -0
- package/lib/encryption.test.ts +41 -0
- package/lib/encryption.ts +98 -0
- package/lib/extract-snippet.test.ts +123 -0
- package/lib/extract-snippet.ts +69 -0
- package/lib/kanban-status.ts +55 -0
- package/lib/license.ts +21 -0
- package/lib/limits.ts +31 -0
- package/lib/mcp-auth.test.ts +58 -0
- package/lib/mcp-auth.ts +65 -0
- package/lib/mcp-contract.test.ts +25 -0
- package/lib/mcp-contract.ts +210 -0
- package/lib/mcp-handler.ts +31 -0
- package/lib/mcp-url.test.ts +12 -0
- package/lib/mcp-url.ts +7 -0
- package/lib/mentions.test.ts +45 -0
- package/lib/mentions.ts +73 -0
- package/lib/note-crypto.ts +108 -0
- package/lib/note-sync.ts +201 -0
- package/lib/note-title.ts +93 -0
- package/lib/prisma.ts +193 -0
- package/lib/pro-flush.ts +292 -0
- package/lib/rate-limit.ts +57 -0
- package/lib/stripe.ts +38 -0
- package/lib/sync-worker.ts +388 -0
- package/lib/task-parser.test.ts +91 -0
- package/lib/task-parser.ts +81 -0
- package/lib/task-utils.ts +52 -0
- package/lib/use-is-electron.ts +19 -0
- package/lib/use-is-mobile.ts +22 -0
- package/lib/validation/calendar-feed.ts +31 -0
- package/lib/validation/note.ts +27 -0
- package/lib/validation/task.ts +26 -0
- package/lib/view-preferences.test.ts +54 -0
- package/lib/view-preferences.ts +28 -0
- package/lib/workspace.ts +66 -0
- package/next.config.ts +21 -0
- package/package.json +99 -0
- package/postcss.config.mjs +7 -0
- package/prisma/migrations/20260519021916_init/migration.sql +388 -0
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
- package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +457 -0
- package/public/Logo_icon.svg +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +4 -0
- package/public/icon_dark.svg +1 -0
- package/public/knotpad_icon.svg +1 -0
- package/public/knotpad_logo_full.svg +1 -0
- package/public/manifest.json +14 -0
- package/public/next.svg +1 -0
- package/public/sw.js +137 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Knotpad
|
|
2
|
+
|
|
3
|
+
> Note-first project management. Write a note — tasks appear automatically.
|
|
4
|
+
> Assign them to teammates or AI agents, right from your prose.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Quick start (recommended)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx @knotpad/app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That's it. Knotpad sets up a local database, syncs the schema, and opens your browser at `http://localhost:3000`.
|
|
15
|
+
|
|
16
|
+
**Requirements:** Node 18+ and PostgreSQL running locally.
|
|
17
|
+
|
|
18
|
+
Don't have PostgreSQL? Spin one up in 10 seconds:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
docker run -d -p 5432:5432 \
|
|
22
|
+
-e POSTGRES_USER=brief \
|
|
23
|
+
-e POSTGRES_PASSWORD=brief \
|
|
24
|
+
-e POSTGRES_DB=brief \
|
|
25
|
+
postgres:16-alpine
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then `npx @knotpad/app`.
|
|
29
|
+
|
|
30
|
+
**Update:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @knotpad/app@latest
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Docker (self-host)
|
|
39
|
+
|
|
40
|
+
Download [`docker-compose.yml`](./docker-compose.yml), then:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 1. Set your secrets
|
|
44
|
+
cp .env.example .env
|
|
45
|
+
# Edit .env: set NEXTAUTH_SECRET, MCP_SECRET (generate with: openssl rand -base64 32)
|
|
46
|
+
|
|
47
|
+
# 2. Start everything
|
|
48
|
+
docker compose up
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
App: `http://localhost:3000`
|
|
52
|
+
MCP: `http://localhost:3001/mcp`
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
Write notes in plain markdown. Use `[ ]` checkboxes to create tasks:
|
|
59
|
+
|
|
60
|
+
```markdown
|
|
61
|
+
## Build Login Page
|
|
62
|
+
|
|
63
|
+
The login screen should feel premium.
|
|
64
|
+
|
|
65
|
+
### Tasks
|
|
66
|
+
- [ ] Prepare typography @thillagen
|
|
67
|
+
- [ ] Prepare design @iqbal
|
|
68
|
+
- [ ] Build the page using <plan.md> @agent
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
On save, Knotpad:
|
|
72
|
+
- Parses every `[ ]` and creates a task record with a UUID
|
|
73
|
+
- Resolves `@mentions` to team members or the agent pool
|
|
74
|
+
- Tasks appear in Kanban, List, Calendar, and Graph views immediately
|
|
75
|
+
- Status changes propagate back to inline badges in the note
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Views
|
|
80
|
+
|
|
81
|
+
| View | What it shows |
|
|
82
|
+
|---|---|
|
|
83
|
+
| **Notes** | Markdown editor with live task badges |
|
|
84
|
+
| **Kanban** | Open → In Progress → Review → Done columns, drag to move |
|
|
85
|
+
| **List** | Tasks grouped by note, sortable |
|
|
86
|
+
| **Calendar** | Tasks by due date, drag to reschedule |
|
|
87
|
+
| **Graph** | Obsidian-style relationship graph (notes, tasks, people, agents) |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## AI agent integration
|
|
92
|
+
|
|
93
|
+
Knotpad is tool-agnostic. Generate your MCP token in **Settings → Agent Token**, then paste it into your preferred AI tool:
|
|
94
|
+
|
|
95
|
+
**Claude Code:**
|
|
96
|
+
```bash
|
|
97
|
+
claude mcp add knotpad \
|
|
98
|
+
--url http://localhost:3001/mcp \
|
|
99
|
+
--token <your-mcp-token>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Cursor / Windsurf / Trae:** The settings page auto-generates the correct config for each tool.
|
|
103
|
+
|
|
104
|
+
### Agent workflow
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# In your terminal (via Claude Code or any MCP client)
|
|
108
|
+
> get_tasks()
|
|
109
|
+
|
|
110
|
+
Available agent tasks (3):
|
|
111
|
+
[1] Build login page <plan.md> · login_brief.md
|
|
112
|
+
[2] Write auth API endpoint · api_spec.md
|
|
113
|
+
[3] Fix N+1 on user query · db_notes.md
|
|
114
|
+
|
|
115
|
+
Pick tasks (e.g. 1,3 or 'all'): 1
|
|
116
|
+
|
|
117
|
+
✓ Claimed: Build login page → @agent:thillagen
|
|
118
|
+
|
|
119
|
+
> update_task(1, "done")
|
|
120
|
+
✓ Task marked done · note updated to [x] · audit logged
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Pricing
|
|
126
|
+
|
|
127
|
+
| | Free (Local) | Pro (Cloud) |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| Price | $0 forever | $5/seat/month |
|
|
130
|
+
| Notes & tasks | Unlimited | Unlimited |
|
|
131
|
+
| AI agents | Unlimited | Unlimited |
|
|
132
|
+
| Views | All 5 | All 5 |
|
|
133
|
+
| Cloud sync | — | AES-256 encrypted at rest |
|
|
134
|
+
| Team seats | — | Invite unlimited humans |
|
|
135
|
+
| Cloud backup | — | Automatic |
|
|
136
|
+
| MCP | localhost | mcp.$NEXT_PUBLIC_APP_DOMAIN |
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Environment variables
|
|
141
|
+
|
|
142
|
+
See [`.env.example`](./.env.example) for the full list. The key ones:
|
|
143
|
+
|
|
144
|
+
```env
|
|
145
|
+
DATABASE_URL="postgresql://brief:brief@localhost:5432/brief"
|
|
146
|
+
NEXTAUTH_SECRET="generate: openssl rand -base64 32"
|
|
147
|
+
MCP_SECRET="generate: openssl rand -base64 32"
|
|
148
|
+
|
|
149
|
+
# Cloud/paid tier only
|
|
150
|
+
STRIPE_SECRET_KEY=""
|
|
151
|
+
STRIPE_WEBHOOK_SECRET=""
|
|
152
|
+
STRIPE_PRICE_ID_PERSONAL_PRO=""
|
|
153
|
+
STRIPE_PRICE_ID_TEAM_PRO=""
|
|
154
|
+
IS_CLOUD="false" # set to "true" on your VPS
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Stack
|
|
160
|
+
|
|
161
|
+
Next.js 16 · PostgreSQL · Prisma 7 · shadcn/ui · Tailwind CSS · NextAuth.js · Stripe · AES-256
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT — Built by [Nexstrive](https://nexstrive.com)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
5
|
+
import { CalendarView } from "@/components/calendar/calendar-view";
|
|
6
|
+
|
|
7
|
+
export default async function CalendarPage({
|
|
8
|
+
searchParams,
|
|
9
|
+
}: {
|
|
10
|
+
searchParams: Promise<{ note?: string; month?: string; folder?: string }>;
|
|
11
|
+
}) {
|
|
12
|
+
const session = await auth();
|
|
13
|
+
if (!session) redirect("/login");
|
|
14
|
+
|
|
15
|
+
const { note: filterNoteId, month, folder: filterFolderId } = await searchParams;
|
|
16
|
+
|
|
17
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
18
|
+
if (!workspaceId) redirect("/login");
|
|
19
|
+
|
|
20
|
+
const tasks = await prisma.task.findMany({
|
|
21
|
+
where: {
|
|
22
|
+
workspaceId,
|
|
23
|
+
...(filterNoteId ? { noteId: filterNoteId } : {}),
|
|
24
|
+
...(filterFolderId && { note: { folderId: filterFolderId } }),
|
|
25
|
+
status: { not: "DONE" },
|
|
26
|
+
},
|
|
27
|
+
orderBy: { dueDate: "asc" },
|
|
28
|
+
include: {
|
|
29
|
+
note: { select: { id: true, title: true } },
|
|
30
|
+
assignee: { select: { id: true, name: true } },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const serialized = tasks.map((t) => ({
|
|
35
|
+
id: t.id,
|
|
36
|
+
title: t.title,
|
|
37
|
+
status: t.status,
|
|
38
|
+
startDate: t.startDate?.toISOString() ?? null,
|
|
39
|
+
dueDate: t.dueDate?.toISOString() ?? null,
|
|
40
|
+
noteId: t.noteId,
|
|
41
|
+
noteTitle: t.note.title,
|
|
42
|
+
assigneeName: t.assignee?.name ?? null,
|
|
43
|
+
assigneeType: t.assigneeType,
|
|
44
|
+
claimedByAlias: t.claimedByAlias,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
49
|
+
<div className="border-b border-zinc-800 px-6 py-3">
|
|
50
|
+
<h1 className="text-sm font-semibold text-zinc-300">
|
|
51
|
+
{filterFolderId ? "Filtered tasks" : "Calendar"}
|
|
52
|
+
</h1>
|
|
53
|
+
</div>
|
|
54
|
+
<CalendarView tasks={serialized} initialMonth={month} filterNoteId={filterNoteId} filterFolderId={filterFolderId} />
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { AlertTriangle } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export default function Error({
|
|
7
|
+
error,
|
|
8
|
+
reset,
|
|
9
|
+
}: {
|
|
10
|
+
error: Error & { digest?: string };
|
|
11
|
+
reset: () => void;
|
|
12
|
+
}) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Surfaces in the server/console; hook a real error tracker here later.
|
|
15
|
+
console.error(error);
|
|
16
|
+
}, [error]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-8 text-center">
|
|
20
|
+
<AlertTriangle size={28} className="text-amber-500" />
|
|
21
|
+
<div className="space-y-1">
|
|
22
|
+
<p className="text-sm font-medium text-zinc-300">Something went wrong</p>
|
|
23
|
+
<p className="max-w-sm text-xs text-zinc-600">
|
|
24
|
+
This view failed to load. You can retry, or head back to your notes.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
<button
|
|
28
|
+
onClick={reset}
|
|
29
|
+
className="rounded-md border border-zinc-700 px-3 py-1.5 text-sm text-zinc-300 transition-colors ring-focus hover:bg-zinc-800 hover:text-zinc-100"
|
|
30
|
+
>
|
|
31
|
+
Try again
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { GraphPage } from "@/components/graph/graph-page";
|
|
5
|
+
|
|
6
|
+
export default async function GraphRoute({
|
|
7
|
+
searchParams,
|
|
8
|
+
}: {
|
|
9
|
+
searchParams: Promise<{ notes?: string }>;
|
|
10
|
+
}) {
|
|
11
|
+
const session = await auth();
|
|
12
|
+
if (!session) redirect("/login");
|
|
13
|
+
|
|
14
|
+
const { notes: notesParam } = await searchParams;
|
|
15
|
+
const selectedNoteIds = notesParam ? notesParam.split(",").filter(Boolean) : [];
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
19
|
+
<div className="border-b border-zinc-800 px-6 py-3">
|
|
20
|
+
<h1 className="text-sm font-semibold text-zinc-300">
|
|
21
|
+
Graph
|
|
22
|
+
{selectedNoteIds.length > 0 && (
|
|
23
|
+
<span className="ml-2 text-xs font-normal text-zinc-500">
|
|
24
|
+
— {selectedNoteIds.length} note{selectedNoteIds.length > 1 ? "s" : ""} selected
|
|
25
|
+
</span>
|
|
26
|
+
)}
|
|
27
|
+
</h1>
|
|
28
|
+
</div>
|
|
29
|
+
<GraphPage selectedNoteIds={selectedNoteIds} />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { GuideView } from "@/components/guide/guide-view";
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
title: "User Guide - Knotpad",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default async function GuidePage() {
|
|
10
|
+
const session = await auth();
|
|
11
|
+
if (!session) redirect("/login");
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
15
|
+
<div className="border-b border-zinc-800 px-6 py-3">
|
|
16
|
+
<h1 className="text-sm font-semibold text-zinc-300">User Guide</h1>
|
|
17
|
+
</div>
|
|
18
|
+
<GuideView />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function KanbanLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
6
|
+
<div className="border-b border-zinc-800 px-6 py-3">
|
|
7
|
+
<Skeleton className="h-4 w-20" />
|
|
8
|
+
</div>
|
|
9
|
+
<div className="flex flex-1 flex-col gap-3 overflow-hidden p-3 md:flex-row md:p-4">
|
|
10
|
+
{[0, 1, 2].map((c) => (
|
|
11
|
+
<div
|
|
12
|
+
key={c}
|
|
13
|
+
className="flex w-full shrink-0 flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900/40 p-2 md:w-64"
|
|
14
|
+
>
|
|
15
|
+
<Skeleton className="mb-1 h-4 w-24" />
|
|
16
|
+
{[0, 1, 2].map((card) => (
|
|
17
|
+
<Skeleton key={card} className="h-14 w-full" />
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
5
|
+
import { KanbanBoard } from "@/components/kanban/kanban-board";
|
|
6
|
+
|
|
7
|
+
export default async function KanbanPage({
|
|
8
|
+
searchParams,
|
|
9
|
+
}: {
|
|
10
|
+
searchParams: Promise<{ note?: string; folder?: string }>;
|
|
11
|
+
}) {
|
|
12
|
+
const session = await auth();
|
|
13
|
+
if (!session) redirect("/login");
|
|
14
|
+
|
|
15
|
+
const { note: filterNoteId, folder: filterFolderId } = await searchParams;
|
|
16
|
+
|
|
17
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
18
|
+
if (!workspaceId) redirect("/login");
|
|
19
|
+
|
|
20
|
+
const tasks = await prisma.task.findMany({
|
|
21
|
+
where: {
|
|
22
|
+
workspaceId,
|
|
23
|
+
...(filterNoteId && { noteId: filterNoteId }),
|
|
24
|
+
...(filterFolderId && { note: { folderId: filterFolderId } }),
|
|
25
|
+
},
|
|
26
|
+
orderBy: { createdAt: "asc" },
|
|
27
|
+
include: {
|
|
28
|
+
note: { select: { id: true, title: true, folder: { select: { id: true, name: true } } } },
|
|
29
|
+
assignee: { select: { id: true, name: true, email: true } },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Serialize dates for the client component
|
|
34
|
+
const serialized = tasks.map((t) => ({
|
|
35
|
+
id: t.id,
|
|
36
|
+
title: t.title,
|
|
37
|
+
status: t.status,
|
|
38
|
+
claimedBy: t.claimedBy,
|
|
39
|
+
claimedByAlias: t.claimedByAlias,
|
|
40
|
+
lastHeartbeat: t.lastHeartbeat?.toISOString() ?? null,
|
|
41
|
+
assigneeType: t.assigneeType,
|
|
42
|
+
assignee: t.assignee,
|
|
43
|
+
note: t.note,
|
|
44
|
+
dueDate: t.dueDate?.toISOString() ?? null,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
49
|
+
<div className="border-b border-zinc-800 px-6 py-3">
|
|
50
|
+
<h1 className="text-sm font-semibold text-zinc-300">
|
|
51
|
+
{filterNoteId || filterFolderId ? "Filtered tasks" : "Kanban"}
|
|
52
|
+
</h1>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex-1 overflow-hidden">
|
|
55
|
+
<KanbanBoard tasks={serialized} filterNoteId={filterNoteId} filterFolderId={filterFolderId} />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { IconBar } from "@/components/layout/icon-bar";
|
|
5
|
+
import { NotesPanel } from "@/components/layout/notes-panel";
|
|
6
|
+
import { ProBanner } from "@/components/layout/pro-banner";
|
|
7
|
+
import { AppShell } from "@/components/layout/app-shell";
|
|
8
|
+
import { PaywallScreen } from "@/components/layout/paywall-screen";
|
|
9
|
+
import { SessionProvider } from "next-auth/react";
|
|
10
|
+
import { getActiveWorkspaceId, listUserWorkspaces } from "@/lib/workspace";
|
|
11
|
+
import { getKanbanStatuses } from "@/lib/kanban-status";
|
|
12
|
+
import { isWorkspacePro } from "@/lib/license";
|
|
13
|
+
import { KanbanStatusProvider } from "@/components/kanban/kanban-status-context";
|
|
14
|
+
import { CommandPaletteProvider } from "@/components/command/command-palette";
|
|
15
|
+
import { BackupScheduler } from "@/components/layout/backup-scheduler";
|
|
16
|
+
|
|
17
|
+
// Cap the shell's eager note/task fetch so navigation stays fast at scale.
|
|
18
|
+
const LAYOUT_LIST_CAP = 500;
|
|
19
|
+
|
|
20
|
+
const IS_CLOUD = process.env.IS_CLOUD === "true";
|
|
21
|
+
|
|
22
|
+
export default async function AppLayout({ children }: { children: React.ReactNode }) {
|
|
23
|
+
const session = await auth();
|
|
24
|
+
if (!session) redirect("/login");
|
|
25
|
+
|
|
26
|
+
const [workspaceId, allWorkspaces] = await Promise.all([
|
|
27
|
+
getActiveWorkspaceId(session.user.id),
|
|
28
|
+
listUserWorkspaces(session.user.id),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
if (!workspaceId) {
|
|
32
|
+
// On cloud, a user with no Pro-accessible workspace isn't missing
|
|
33
|
+
// membership — they're FREE-only and must upgrade or be invited to a team.
|
|
34
|
+
if (IS_CLOUD) return <PaywallScreen />;
|
|
35
|
+
redirect("/login");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const workspaces = IS_CLOUD ? allWorkspaces.filter((w) => w.isPro) : allWorkspaces;
|
|
39
|
+
|
|
40
|
+
const member = await prisma.workspaceMember.findFirst({
|
|
41
|
+
where: { userId: session.user.id, workspaceId },
|
|
42
|
+
include: {
|
|
43
|
+
workspace: {
|
|
44
|
+
include: {
|
|
45
|
+
notes: {
|
|
46
|
+
orderBy: { updatedAt: "desc" },
|
|
47
|
+
// Bounded so the shell (and command-palette index) stay fast on
|
|
48
|
+
// large workspaces. Matches the cap on GET /api/notes.
|
|
49
|
+
take: LAYOUT_LIST_CAP,
|
|
50
|
+
include: {
|
|
51
|
+
folder: { select: { id: true, name: true } },
|
|
52
|
+
_count: { select: { tasks: true } },
|
|
53
|
+
tasks: {
|
|
54
|
+
where: { status: { in: ["CLAIMED", "IN_PROGRESS", "REVIEW"] } },
|
|
55
|
+
select: { id: true },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
folders: { orderBy: { name: "asc" }, select: { id: true, name: true } },
|
|
60
|
+
tasks: {
|
|
61
|
+
orderBy: { updatedAt: "desc" },
|
|
62
|
+
take: LAYOUT_LIST_CAP,
|
|
63
|
+
select: {
|
|
64
|
+
id: true,
|
|
65
|
+
title: true,
|
|
66
|
+
status: true,
|
|
67
|
+
note: { select: { id: true, title: true } },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!member) redirect("/login");
|
|
76
|
+
|
|
77
|
+
const notes = member.workspace.notes.map((n) => ({
|
|
78
|
+
id: n.id,
|
|
79
|
+
title: n.title,
|
|
80
|
+
taskCount: n._count.tasks,
|
|
81
|
+
activeTaskCount: n.tasks.length,
|
|
82
|
+
isLocked: n.isLocked,
|
|
83
|
+
folderId: n.folderId,
|
|
84
|
+
}));
|
|
85
|
+
const folders = member.workspace.folders;
|
|
86
|
+
const tasks = member.workspace.tasks.map((t) => ({
|
|
87
|
+
id: t.id,
|
|
88
|
+
title: t.title,
|
|
89
|
+
status: t.status,
|
|
90
|
+
note: t.note,
|
|
91
|
+
}));
|
|
92
|
+
const latestNoteId = notes[0]?.id;
|
|
93
|
+
const notesHref = latestNoteId ? `/notes/${latestNoteId}` : "/notes";
|
|
94
|
+
|
|
95
|
+
const commandData = {
|
|
96
|
+
notes: notes.map((n) => ({ id: n.id, title: n.title })),
|
|
97
|
+
tasks: tasks.map((t) => ({ id: t.id, title: t.title, note: t.note })),
|
|
98
|
+
folders,
|
|
99
|
+
notesHref,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const kanbanStatuses = await getKanbanStatuses(workspaceId);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<SessionProvider>
|
|
106
|
+
<BackupScheduler />
|
|
107
|
+
<KanbanStatusProvider statuses={kanbanStatuses}>
|
|
108
|
+
<CommandPaletteProvider data={commandData}>
|
|
109
|
+
<div className="flex h-full flex-col overflow-hidden">
|
|
110
|
+
<ProBanner initialIsPro={isWorkspacePro(member.workspace)} />
|
|
111
|
+
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
112
|
+
<IconBar latestNoteId={latestNoteId} />
|
|
113
|
+
<AppShell panel={<NotesPanel notes={notes} folders={folders} tasks={tasks} workspaces={workspaces} activeWorkspaceId={workspaceId} />}>
|
|
114
|
+
{children}
|
|
115
|
+
</AppShell>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</CommandPaletteProvider>
|
|
119
|
+
</KanbanStatusProvider>
|
|
120
|
+
</SessionProvider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function ListLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
6
|
+
<div className="border-b border-zinc-800 px-3 py-3 md:px-6">
|
|
7
|
+
<Skeleton className="h-4 w-24" />
|
|
8
|
+
</div>
|
|
9
|
+
<div className="flex flex-col gap-4 px-3 py-4 md:px-6">
|
|
10
|
+
{[0, 1, 2].map((g) => (
|
|
11
|
+
<div key={g} className="space-y-2">
|
|
12
|
+
<Skeleton className="h-3 w-20" />
|
|
13
|
+
{[0, 1, 2].map((r) => (
|
|
14
|
+
<Skeleton key={r} className="h-10 w-full" />
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
))}
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { getActiveWorkspaceId } from "@/lib/workspace";
|
|
5
|
+
import { TaskList } from "@/components/tasks/task-list";
|
|
6
|
+
import { TaskListFilters } from "@/components/tasks/task-list-filters";
|
|
7
|
+
import { TaskStatus } from "@/app/generated/prisma/client";
|
|
8
|
+
|
|
9
|
+
export default async function ListPage({
|
|
10
|
+
searchParams,
|
|
11
|
+
}: {
|
|
12
|
+
searchParams: Promise<{
|
|
13
|
+
note?: string;
|
|
14
|
+
sort?: string;
|
|
15
|
+
folder?: string;
|
|
16
|
+
status?: string;
|
|
17
|
+
assigneeType?: string;
|
|
18
|
+
overdue?: string;
|
|
19
|
+
hasDueDate?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}) {
|
|
22
|
+
const session = await auth();
|
|
23
|
+
if (!session) redirect("/login");
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
note: filterNoteId,
|
|
27
|
+
sort = "status",
|
|
28
|
+
folder: filterFolderId,
|
|
29
|
+
status: statusFilter,
|
|
30
|
+
assigneeType: assigneeFilter,
|
|
31
|
+
overdue: overdueOnly,
|
|
32
|
+
hasDueDate: hasDueDateFilter,
|
|
33
|
+
} = await searchParams;
|
|
34
|
+
|
|
35
|
+
const workspaceId = await getActiveWorkspaceId(session.user.id);
|
|
36
|
+
if (!workspaceId) redirect("/login");
|
|
37
|
+
|
|
38
|
+
// Build status filter condition
|
|
39
|
+
const statusFilterCondition = statusFilter
|
|
40
|
+
? { status: { in: statusFilter.split(",") as TaskStatus[] } }
|
|
41
|
+
: {};
|
|
42
|
+
|
|
43
|
+
// Build assignee filter condition
|
|
44
|
+
const assigneeFilterCondition = assigneeFilter
|
|
45
|
+
? { assigneeType: assigneeFilter as "HUMAN" | "AGENT" }
|
|
46
|
+
: {};
|
|
47
|
+
|
|
48
|
+
// Build overdue filter condition
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const overdueFilterCondition = overdueOnly === "true"
|
|
51
|
+
? { dueDate: { lt: now }, NOT: { status: "DONE" as TaskStatus } }
|
|
52
|
+
: {};
|
|
53
|
+
|
|
54
|
+
// Build hasDueDate filter condition
|
|
55
|
+
const hasDueDateCondition = hasDueDateFilter === "true"
|
|
56
|
+
? { dueDate: { not: null } }
|
|
57
|
+
: {};
|
|
58
|
+
|
|
59
|
+
// Get folders for filter dropdown
|
|
60
|
+
const foldersPromise = prisma.folder.findMany({
|
|
61
|
+
where: { workspaceId },
|
|
62
|
+
select: { id: true, name: true },
|
|
63
|
+
orderBy: { name: "asc" },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const [tasks, filterNote, folders] = await Promise.all([
|
|
67
|
+
prisma.task.findMany({
|
|
68
|
+
where: {
|
|
69
|
+
workspaceId,
|
|
70
|
+
...(filterNoteId && { noteId: filterNoteId }),
|
|
71
|
+
...(filterFolderId && { note: { folderId: filterFolderId } }),
|
|
72
|
+
...statusFilterCondition,
|
|
73
|
+
...assigneeFilterCondition,
|
|
74
|
+
...overdueFilterCondition,
|
|
75
|
+
...hasDueDateCondition,
|
|
76
|
+
},
|
|
77
|
+
orderBy: { createdAt: "desc" },
|
|
78
|
+
include: {
|
|
79
|
+
note: { select: { id: true, title: true, folder: { select: { id: true, name: true } } } },
|
|
80
|
+
assignee: { select: { id: true, name: true, email: true, image: true } },
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
filterNoteId
|
|
84
|
+
? prisma.note.findFirst({ where: { id: filterNoteId, workspaceId }, select: { title: true } })
|
|
85
|
+
: Promise.resolve(null),
|
|
86
|
+
foldersPromise,
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
91
|
+
<div className="flex items-center justify-between border-b border-zinc-800 px-3 md:px-6 py-3">
|
|
92
|
+
<div className="flex items-center gap-2">
|
|
93
|
+
<h1 className="text-sm font-semibold text-zinc-300">
|
|
94
|
+
{filterNoteId || filterFolderId ? "Filtered tasks" : "All Tasks"}
|
|
95
|
+
</h1>
|
|
96
|
+
{filterNote && (
|
|
97
|
+
<span className="rounded-full bg-zinc-800 px-2 py-0.5 text-xs text-zinc-400">
|
|
98
|
+
{filterNote.title}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
<div className="flex items-center gap-2 text-xs text-zinc-500">
|
|
103
|
+
<span>Sort:</span>
|
|
104
|
+
{(() => {
|
|
105
|
+
const params = new URLSearchParams();
|
|
106
|
+
if (filterNoteId) params.set("note", filterNoteId);
|
|
107
|
+
if (filterFolderId) params.set("folder", filterFolderId);
|
|
108
|
+
const baseQuery = params.toString();
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<a href={`?${baseQuery ? baseQuery + "&" : ""}sort=status`}
|
|
112
|
+
className={sort === "status" ? "text-zinc-300" : "hover:text-zinc-300"}>Status</a>
|
|
113
|
+
<span>/</span>
|
|
114
|
+
<a href={`?${baseQuery ? baseQuery + "&" : ""}sort=note`}
|
|
115
|
+
className={sort === "note" ? "text-zinc-300" : "hover:text-zinc-300"}>Note</a>
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
})()}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Filter Bar - Tasks 2.4.1-2.4.5 */}
|
|
123
|
+
<TaskListFilters
|
|
124
|
+
folders={folders}
|
|
125
|
+
filterFolderId={filterFolderId}
|
|
126
|
+
statusFilter={statusFilter}
|
|
127
|
+
assigneeFilter={assigneeFilter}
|
|
128
|
+
overdueOnly={overdueOnly}
|
|
129
|
+
hasDueDateFilter={hasDueDateFilter}
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
<div className="flex-1 overflow-y-auto">
|
|
133
|
+
<TaskList tasks={tasks} groupBy={sort === "note" ? "note" : "status"} />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|