@knotpad/app 0.1.5 → 0.1.7

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.
Files changed (337) hide show
  1. package/app/(app)/calendar/page.tsx +57 -0
  2. package/app/(app)/error.tsx +35 -0
  3. package/app/(app)/graph/page.tsx +32 -0
  4. package/app/(app)/guide/page.tsx +21 -0
  5. package/app/(app)/kanban/loading.tsx +24 -0
  6. package/app/(app)/kanban/page.tsx +59 -0
  7. package/app/(app)/layout.tsx +122 -0
  8. package/app/(app)/list/loading.tsx +21 -0
  9. package/app/(app)/list/page.tsx +137 -0
  10. package/app/(app)/loading.tsx +18 -0
  11. package/app/(app)/notes/[noteId]/page.tsx +84 -0
  12. package/app/(app)/notes/layout.tsx +30 -0
  13. package/app/(app)/notes/page.tsx +39 -0
  14. package/app/(app)/page.tsx +5 -0
  15. package/app/(app)/settings/agent-token/page.tsx +59 -0
  16. package/app/(app)/settings/backup/page.tsx +49 -0
  17. package/app/(app)/settings/billing/page.tsx +53 -0
  18. package/app/(app)/settings/calendar/page.tsx +41 -0
  19. package/app/(app)/settings/layout.test.tsx +39 -0
  20. package/app/(app)/settings/layout.tsx +71 -0
  21. package/app/(app)/settings/page.tsx +4 -0
  22. package/app/(app)/settings/security/page.tsx +43 -0
  23. package/app/(app)/settings/team/page.tsx +74 -0
  24. package/app/(app)/settings/workspace/page.tsx +27 -0
  25. package/app/(app)/tasks/[taskId]/page.tsx +79 -0
  26. package/app/(auth)/forgot-password/page.tsx +106 -0
  27. package/app/(auth)/guest/page.tsx +56 -0
  28. package/app/(auth)/layout.tsx +13 -0
  29. package/app/(auth)/login/page.tsx +14 -0
  30. package/app/(auth)/register/page.tsx +193 -0
  31. package/app/(auth)/reset-password/page.tsx +138 -0
  32. package/app/api/account/claim/route.tsx +135 -0
  33. package/app/api/admin/backfill-encryption/route.tsx +43 -0
  34. package/app/api/admin/license/route.tsx +42 -0
  35. package/app/api/auth/2fa/route.tsx +148 -0
  36. package/app/api/auth/[...nextauth]/route.tsx +3 -0
  37. package/app/api/auth/change-password/route.tsx +61 -0
  38. package/app/api/auth/check-2fa/route.tsx +19 -0
  39. package/app/api/auth/forgot-password/route.tsx +65 -0
  40. package/app/api/auth/reset-password/route.tsx +52 -0
  41. package/app/api/auth/verify-2fa/route.tsx +88 -0
  42. package/app/api/backup/download/db/route.ts +29 -0
  43. package/app/api/backup/download/notes/route.ts +25 -0
  44. package/app/api/backup/settings/route.ts +92 -0
  45. package/app/api/billing/checkout/route.tsx +81 -0
  46. package/app/api/billing/migrate/route.tsx +163 -0
  47. package/app/api/billing/portal/route.tsx +24 -0
  48. package/app/api/billing/setup-intent/route.tsx +55 -0
  49. package/app/api/billing/status/route.tsx +36 -0
  50. package/app/api/billing/subscribe/route.tsx +85 -0
  51. package/app/api/billing/webhook/route.tsx +199 -0
  52. package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
  53. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
  54. package/app/api/calendar-feeds/events/route.tsx +82 -0
  55. package/app/api/calendar-feeds/route.tsx +52 -0
  56. package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
  57. package/app/api/cron/calendar-feeds/route.tsx +31 -0
  58. package/app/api/cron/stale-tasks/route.tsx +51 -0
  59. package/app/api/cron/sync/route.tsx +34 -0
  60. package/app/api/devices/[deviceId]/route.tsx +25 -0
  61. package/app/api/devices/route.tsx +41 -0
  62. package/app/api/export/route.tsx +40 -0
  63. package/app/api/feedback/route.tsx +54 -0
  64. package/app/api/folders/[folderId]/route.tsx +51 -0
  65. package/app/api/folders/route.tsx +37 -0
  66. package/app/api/graph/route.tsx +242 -0
  67. package/app/api/guest/route.tsx +58 -0
  68. package/app/api/health/route.tsx +10 -0
  69. package/app/api/holidays/countries/route.tsx +14 -0
  70. package/app/api/holidays/route.tsx +49 -0
  71. package/app/api/holidays/states/route.tsx +21 -0
  72. package/app/api/invites/[token]/route.tsx +131 -0
  73. package/app/api/invites/route.tsx +74 -0
  74. package/app/api/mcp/generate-token/route.tsx +55 -0
  75. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
  76. package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
  77. package/app/api/notes/[noteId]/export/route.tsx +45 -0
  78. package/app/api/notes/[noteId]/route.tsx +360 -0
  79. package/app/api/notes/route.tsx +112 -0
  80. package/app/api/notifications/route.tsx +44 -0
  81. package/app/api/register/route.tsx +67 -0
  82. package/app/api/restore/route.tsx +148 -0
  83. package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
  84. package/app/api/sync/conflicts/route.tsx +48 -0
  85. package/app/api/sync/status/route.tsx +49 -0
  86. package/app/api/sync/trigger/route.tsx +15 -0
  87. package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
  88. package/app/api/tasks/[taskId]/route.tsx +259 -0
  89. package/app/api/tasks/bulk/route.tsx +133 -0
  90. package/app/api/tasks/route.tsx +36 -0
  91. package/app/api/workspace/active/route.tsx +39 -0
  92. package/app/api/workspace/create-team/route.tsx +42 -0
  93. package/app/api/workspace/kanban-statuses/route.tsx +71 -0
  94. package/app/api/workspace/members/[memberId]/route.tsx +69 -0
  95. package/app/api/workspace/route.tsx +24 -0
  96. package/app/download/page.tsx +170 -0
  97. package/app/favicon.ico +0 -0
  98. package/app/generated/prisma/client.d.ts +1 -0
  99. package/app/generated/prisma/client.js +5 -0
  100. package/app/generated/prisma/default.d.ts +1 -0
  101. package/app/generated/prisma/default.js +5 -0
  102. package/app/generated/prisma/edge.d.ts +1 -0
  103. package/app/generated/prisma/edge.js +497 -0
  104. package/app/generated/prisma/index-browser.js +523 -0
  105. package/app/generated/prisma/index.d.ts +46376 -0
  106. package/app/generated/prisma/index.js +497 -0
  107. package/app/generated/prisma/package.json +144 -0
  108. package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
  109. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  110. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
  111. package/app/generated/prisma/runtime/client.d.ts +3386 -0
  112. package/app/generated/prisma/runtime/client.js +86 -0
  113. package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
  114. package/app/generated/prisma/runtime/index-browser.js +6 -0
  115. package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
  116. package/app/generated/prisma/schema.prisma +456 -0
  117. package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
  118. package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
  119. package/app/globals.css +54 -0
  120. package/app/invite/[token]/page.tsx +52 -0
  121. package/app/layout.tsx +90 -0
  122. package/app/mcp/route.tsx +430 -0
  123. package/app/opengraph-image.tsx +120 -0
  124. package/app/page.tsx +398 -0
  125. package/app/privacy/page.tsx +69 -0
  126. package/app/robots.tsx +25 -0
  127. package/app/sitemap.tsx +36 -0
  128. package/app/terms/page.tsx +69 -0
  129. package/app/upgrade/page.tsx +75 -0
  130. package/auth.config.ts +33 -0
  131. package/auth.ts +79 -0
  132. package/bin/brief.js +229 -0
  133. package/components/auth/login-form.tsx +302 -0
  134. package/components/auth/password-checklist.tsx +31 -0
  135. package/components/auth/password-input.tsx +36 -0
  136. package/components/auth/switch-account-button.test.tsx +22 -0
  137. package/components/auth/switch-account-button.tsx +19 -0
  138. package/components/auth/two-factor-input.tsx +116 -0
  139. package/components/billing/billing-dashboard.tsx +265 -0
  140. package/components/billing/card-form.tsx +210 -0
  141. package/components/billing/claim-account-form.tsx +99 -0
  142. package/components/branding/app-logo.test.tsx +20 -0
  143. package/components/branding/app-logo.tsx +25 -0
  144. package/components/calendar/calendar-agenda.tsx +150 -0
  145. package/components/calendar/calendar-drag.test.tsx +177 -0
  146. package/components/calendar/calendar-grid.tsx +357 -0
  147. package/components/calendar/calendar-hooks.test.tsx +27 -0
  148. package/components/calendar/calendar-hooks.ts +351 -0
  149. package/components/calendar/calendar-toolbar.test.tsx +68 -0
  150. package/components/calendar/calendar-toolbar.tsx +291 -0
  151. package/components/calendar/calendar-types.ts +148 -0
  152. package/components/calendar/calendar-view.test.tsx +295 -0
  153. package/components/calendar/calendar-view.tsx +307 -0
  154. package/components/calendar/day-detail-popover.tsx +174 -0
  155. package/components/calendar/task-chip.tsx +86 -0
  156. package/components/command/command-palette.test.tsx +33 -0
  157. package/components/command/command-palette.tsx +310 -0
  158. package/components/download-cta.tsx +87 -0
  159. package/components/feedback/feedback-popup.tsx +207 -0
  160. package/components/graph/graph-draw.ts +337 -0
  161. package/components/graph/graph-overlays.tsx +160 -0
  162. package/components/graph/graph-page.test.tsx +131 -0
  163. package/components/graph/graph-page.tsx +263 -0
  164. package/components/graph/graph-types.ts +47 -0
  165. package/components/graph/graph-view.tsx +322 -0
  166. package/components/guide/guide-view.tsx +522 -0
  167. package/components/kanban/kanban-board.test.tsx +128 -0
  168. package/components/kanban/kanban-board.tsx +361 -0
  169. package/components/kanban/kanban-card-menu.tsx +102 -0
  170. package/components/kanban/kanban-card.tsx +227 -0
  171. package/components/kanban/kanban-column.tsx +49 -0
  172. package/components/kanban/kanban-status-context.tsx +28 -0
  173. package/components/landing/calendar-sandbox.test.tsx +15 -0
  174. package/components/landing/calendar-sandbox.tsx +107 -0
  175. package/components/landing/graph-sandbox.test.tsx +27 -0
  176. package/components/landing/graph-sandbox.tsx +80 -0
  177. package/components/landing/kanban-sandbox.test.tsx +24 -0
  178. package/components/landing/kanban-sandbox.tsx +101 -0
  179. package/components/landing/landing-showcase.test.tsx +21 -0
  180. package/components/landing/landing-showcase.tsx +54 -0
  181. package/components/landing/list-sandbox.tsx +86 -0
  182. package/components/landing/mock-workspace.ts +168 -0
  183. package/components/landing/notes-sandbox.test.tsx +14 -0
  184. package/components/landing/notes-sandbox.tsx +88 -0
  185. package/components/layout/app-shell.tsx +83 -0
  186. package/components/layout/backup-scheduler.tsx +122 -0
  187. package/components/layout/bottom-nav.tsx +43 -0
  188. package/components/layout/icon-bar.test.tsx +29 -0
  189. package/components/layout/icon-bar.tsx +118 -0
  190. package/components/layout/mobile-top-bar.tsx +68 -0
  191. package/components/layout/notes-panel-folder.tsx +127 -0
  192. package/components/layout/notes-panel-note-item.tsx +140 -0
  193. package/components/layout/notes-panel-task-tab.tsx +63 -0
  194. package/components/layout/notes-panel-types.ts +44 -0
  195. package/components/layout/notes-panel.tsx +476 -0
  196. package/components/layout/notification-bell.tsx +251 -0
  197. package/components/layout/paywall-screen.tsx +41 -0
  198. package/components/layout/pro-banner.tsx +76 -0
  199. package/components/layout/sw-register.tsx +27 -0
  200. package/components/layout/workspace-switcher.tsx +90 -0
  201. package/components/notes/mobile-bottom-sheet.tsx +99 -0
  202. package/components/notes/note-editor-context-menu.tsx +47 -0
  203. package/components/notes/note-editor-dom.ts +33 -0
  204. package/components/notes/note-editor-dropdowns.tsx +484 -0
  205. package/components/notes/note-editor-hooks.ts +692 -0
  206. package/components/notes/note-editor-keyboard.ts +305 -0
  207. package/components/notes/note-editor-overlay.tsx +90 -0
  208. package/components/notes/note-editor.test.tsx +372 -0
  209. package/components/notes/note-editor.tsx +662 -0
  210. package/components/notes/note-preview-pane.tsx +156 -0
  211. package/components/notes/note-tabs.tsx +120 -0
  212. package/components/notes/note-types.tsx +157 -0
  213. package/components/settings/accept-invite.tsx +108 -0
  214. package/components/settings/agent-token-settings.tsx +369 -0
  215. package/components/settings/backup-restore-settings.test.tsx +25 -0
  216. package/components/settings/backup-restore-settings.tsx +327 -0
  217. package/components/settings/calendar-feeds-settings.tsx +489 -0
  218. package/components/settings/calendar-general-settings.tsx +174 -0
  219. package/components/settings/confirm-danger-action.test.tsx +215 -0
  220. package/components/settings/confirm-danger-action.tsx +65 -0
  221. package/components/settings/security-settings.tsx +252 -0
  222. package/components/settings/settings-guidance.test.tsx +98 -0
  223. package/components/settings/team-settings.tsx +319 -0
  224. package/components/settings/two-factor-auth.tsx +296 -0
  225. package/components/settings/workspace-settings-client.tsx +363 -0
  226. package/components/settings/workspace-settings-form.tsx +73 -0
  227. package/components/sync/conflict-viewer.tsx +247 -0
  228. package/components/sync/sync-indicator.tsx +171 -0
  229. package/components/tasks/snippet-thread.tsx +119 -0
  230. package/components/tasks/status-dot.tsx +47 -0
  231. package/components/tasks/task-badge.tsx +43 -0
  232. package/components/tasks/task-detail.test.tsx +187 -0
  233. package/components/tasks/task-detail.tsx +458 -0
  234. package/components/tasks/task-list-filters.test.tsx +75 -0
  235. package/components/tasks/task-list-filters.tsx +163 -0
  236. package/components/tasks/task-list-types.ts +20 -0
  237. package/components/tasks/task-list.test.tsx +175 -0
  238. package/components/tasks/task-list.tsx +481 -0
  239. package/components/tasks/task-row.tsx +85 -0
  240. package/components/tasks/task-table-row.tsx +259 -0
  241. package/components/ui/skeleton.tsx +3 -0
  242. package/components/ui/toast.test.tsx +42 -0
  243. package/components/ui/toast.tsx +70 -0
  244. package/instrumentation.tsx +23 -0
  245. package/lib/api-error.ts +50 -0
  246. package/lib/backup/backup-runner.test.ts +32 -0
  247. package/lib/backup/backup-runner.ts +19 -0
  248. package/lib/backup/backup-schedule.test.ts +23 -0
  249. package/lib/backup/backup-schedule.ts +55 -0
  250. package/lib/backup/backup-settings.test.ts +30 -0
  251. package/lib/backup/backup-settings.ts +27 -0
  252. package/lib/backup/export-notes-zip.test.ts +26 -0
  253. package/lib/backup/export-notes-zip.ts +82 -0
  254. package/lib/backup/export-workspace-backup.test.ts +17 -0
  255. package/lib/backup/export-workspace-backup.ts +77 -0
  256. package/lib/backup/restore-workspace-from-export.test.ts +18 -0
  257. package/lib/backup/restore-workspace-from-export.ts +183 -0
  258. package/lib/backup/types.ts +14 -0
  259. package/lib/brand-icons.ts +1 -0
  260. package/lib/calendar-feed-crypto.ts +38 -0
  261. package/lib/calendar-feed.ts +239 -0
  262. package/lib/client/online-status.ts +47 -0
  263. package/lib/conflict-resolver.test.ts +57 -0
  264. package/lib/conflict-resolver.ts +240 -0
  265. package/lib/db-init.ts +79 -0
  266. package/lib/email.ts +159 -0
  267. package/lib/encryption.test.ts +41 -0
  268. package/lib/encryption.ts +98 -0
  269. package/lib/extract-snippet.test.ts +123 -0
  270. package/lib/extract-snippet.ts +69 -0
  271. package/lib/kanban-status.ts +55 -0
  272. package/lib/license.ts +21 -0
  273. package/lib/limits.ts +31 -0
  274. package/lib/mcp-auth.test.ts +58 -0
  275. package/lib/mcp-auth.ts +65 -0
  276. package/lib/mcp-contract.test.ts +25 -0
  277. package/lib/mcp-contract.ts +210 -0
  278. package/lib/mcp-handler.ts +31 -0
  279. package/lib/mcp-url.test.ts +12 -0
  280. package/lib/mcp-url.ts +7 -0
  281. package/lib/mentions.test.ts +45 -0
  282. package/lib/mentions.ts +73 -0
  283. package/lib/note-crypto.ts +108 -0
  284. package/lib/note-sync.ts +201 -0
  285. package/lib/note-title.ts +93 -0
  286. package/lib/prisma.ts +193 -0
  287. package/lib/pro-flush.ts +292 -0
  288. package/lib/rate-limit.ts +57 -0
  289. package/lib/stripe.ts +38 -0
  290. package/lib/sync-worker.ts +388 -0
  291. package/lib/task-parser.test.ts +91 -0
  292. package/lib/task-parser.ts +81 -0
  293. package/lib/task-utils.ts +52 -0
  294. package/lib/use-is-electron.ts +19 -0
  295. package/lib/use-is-mobile.ts +22 -0
  296. package/lib/validation/calendar-feed.ts +31 -0
  297. package/lib/validation/note.ts +27 -0
  298. package/lib/validation/task.ts +26 -0
  299. package/lib/view-preferences.test.ts +54 -0
  300. package/lib/view-preferences.ts +28 -0
  301. package/lib/workspace.ts +66 -0
  302. package/next.config.ts +21 -0
  303. package/package.json +54 -3
  304. package/postcss.config.mjs +7 -0
  305. package/prisma/migrations/20260519021916_init/migration.sql +388 -0
  306. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
  307. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
  308. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
  309. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
  310. package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
  311. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
  312. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
  313. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
  314. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
  315. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
  316. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
  317. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
  318. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
  319. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
  320. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
  321. package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
  322. package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
  323. package/prisma/migrations/migration_lock.toml +3 -0
  324. package/prisma/schema.prisma +457 -0
  325. package/public/Logo_icon.svg +1 -0
  326. package/public/file.svg +1 -0
  327. package/public/globe.svg +1 -0
  328. package/public/icon_dark.svg +1 -0
  329. package/public/knotpad_icon.svg +1 -0
  330. package/public/knotpad_logo_full.svg +1 -0
  331. package/public/manifest.json +14 -0
  332. package/public/next.svg +1 -0
  333. package/public/sw.js +137 -0
  334. package/public/vercel.svg +1 -0
  335. package/public/window.svg +1 -0
  336. package/tsconfig.json +35 -0
  337. package/brief.js +0 -311
@@ -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
+ }
@@ -0,0 +1,18 @@
1
+ import Image from "next/image";
2
+
3
+ export default function Loading() {
4
+ return (
5
+ <div className="flex flex-1 flex-col items-center justify-center gap-4 px-6">
6
+ <Image
7
+ src="/knotpad_logo_full.svg"
8
+ alt="Knotpad"
9
+ width={200}
10
+ height={60}
11
+ className="h-16 w-auto"
12
+ priority
13
+ />
14
+ <p className="text-sm text-zinc-500">Welcome to Knotpad</p>
15
+ <div className="h-6 w-6 animate-spin rounded-full border-2 border-zinc-700 border-t-zinc-400" />
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,84 @@
1
+ import { auth } from "@/auth";
2
+ import { redirect, notFound } from "next/navigation";
3
+ import { prisma } from "@/lib/prisma";
4
+ import { decryptContent } from "@/lib/note-crypto";
5
+ import { getActiveWorkspaceId } from "@/lib/workspace";
6
+ import { NoteEditor } from "@/components/notes/note-editor";
7
+
8
+ export default async function NotePage({
9
+ params,
10
+ searchParams,
11
+ }: {
12
+ params: Promise<{ noteId: string }>;
13
+ searchParams: Promise<{ q?: string }>;
14
+ }) {
15
+ const session = await auth();
16
+ if (!session) redirect("/login");
17
+
18
+ const { noteId } = await params;
19
+ const { q: highlight } = await searchParams;
20
+
21
+ const activeWs = await getActiveWorkspaceId(session.user.id);
22
+ const member = await prisma.workspaceMember.findFirst({
23
+ where: { userId: session.user.id, workspaceId: activeWs ?? undefined },
24
+ include: {
25
+ workspace: {
26
+ include: {
27
+ members: {
28
+ include: { user: { select: { id: true, name: true, email: true } } },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ });
34
+ if (!member) redirect("/login");
35
+
36
+ const [note, allNotes, allTasks] = await Promise.all([
37
+ prisma.note.findFirst({
38
+ where: { id: noteId, workspaceId: member.workspaceId },
39
+ include: {
40
+ folder: { select: { id: true, name: true } },
41
+ tasks: {
42
+ orderBy: { createdAt: "asc" },
43
+ include: { assignee: { select: { id: true, name: true, email: true } } },
44
+ },
45
+ },
46
+ }),
47
+ prisma.note.findMany({
48
+ where: { workspaceId: member.workspaceId },
49
+ orderBy: { updatedAt: "desc" },
50
+ select: { id: true, title: true },
51
+ }),
52
+ prisma.task.findMany({
53
+ where: { workspaceId: member.workspaceId, status: { not: "DONE" } },
54
+ orderBy: { createdAt: "desc" },
55
+ take: 200,
56
+ select: { id: true, title: true, note: { select: { title: true } } },
57
+ }),
58
+ ]);
59
+
60
+ if (!note) notFound();
61
+
62
+ const decryptedContent = await decryptContent(note.content, member.workspaceId);
63
+
64
+ const members = member.workspace.members
65
+ .map((m) => ({
66
+ handle: `@${(m.user.name ?? m.user.email ?? "user").toLowerCase().replace(/\s+/g, "")}`,
67
+ label: (m.user.name ?? m.user.email ?? "Unknown") + (m.userId === session.user.id ? " (you)" : ""),
68
+ isAgent: false,
69
+ }));
70
+
71
+ const notesList = allNotes
72
+ .filter((n) => n.id !== noteId)
73
+ .map((n) => ({ id: n.id, title: n.title }));
74
+
75
+ const tasksList = allTasks.map((t) => ({
76
+ id: t.id,
77
+ title: t.title,
78
+ noteTitle: t.note.title,
79
+ }));
80
+
81
+ // key by note id so the editor fully remounts (fresh state + flush of the
82
+ // previous note's pending save) when navigating between notes via tabs/links.
83
+ return <NoteEditor key={note.id} note={{ ...note, content: decryptedContent }} members={members} notesList={notesList} tasksList={tasksList} highlight={highlight} />;
84
+ }
@@ -0,0 +1,30 @@
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 { NoteTabs } from "@/components/notes/note-tabs";
6
+
7
+ /**
8
+ * Wraps the notes routes with a browser-style tab strip. Only mounts on
9
+ * /notes/* so the board/list/calendar views stay tab-free.
10
+ */
11
+ export default async function NotesLayout({ children }: { children: React.ReactNode }) {
12
+ const session = await auth();
13
+ if (!session) redirect("/login");
14
+
15
+ const workspaceId = await getActiveWorkspaceId(session.user.id);
16
+ const notes = workspaceId
17
+ ? await prisma.note.findMany({
18
+ where: { workspaceId },
19
+ orderBy: { updatedAt: "desc" },
20
+ select: { id: true, title: true },
21
+ })
22
+ : [];
23
+
24
+ return (
25
+ <div className="flex flex-1 flex-col overflow-hidden">
26
+ <NoteTabs notes={notes} />
27
+ <div className="flex flex-1 flex-col overflow-hidden">{children}</div>
28
+ </div>
29
+ );
30
+ }