@knotpad/app 0.1.0 → 0.1.1

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 (341) hide show
  1. package/bin/brief.js +165 -78
  2. package/package.json +3 -17
  3. package/app/(app)/calendar/page.tsx +0 -57
  4. package/app/(app)/error.tsx +0 -35
  5. package/app/(app)/graph/page.tsx +0 -32
  6. package/app/(app)/guide/page.tsx +0 -21
  7. package/app/(app)/kanban/loading.tsx +0 -24
  8. package/app/(app)/kanban/page.tsx +0 -59
  9. package/app/(app)/layout.tsx +0 -122
  10. package/app/(app)/list/loading.tsx +0 -21
  11. package/app/(app)/list/page.tsx +0 -137
  12. package/app/(app)/loading.tsx +0 -18
  13. package/app/(app)/notes/[noteId]/page.tsx +0 -84
  14. package/app/(app)/notes/layout.tsx +0 -30
  15. package/app/(app)/notes/page.tsx +0 -39
  16. package/app/(app)/page.tsx +0 -5
  17. package/app/(app)/settings/agent-token/page.tsx +0 -59
  18. package/app/(app)/settings/backup/page.tsx +0 -49
  19. package/app/(app)/settings/billing/page.tsx +0 -53
  20. package/app/(app)/settings/calendar/page.tsx +0 -41
  21. package/app/(app)/settings/layout.test.tsx +0 -39
  22. package/app/(app)/settings/layout.tsx +0 -71
  23. package/app/(app)/settings/page.tsx +0 -4
  24. package/app/(app)/settings/security/page.tsx +0 -43
  25. package/app/(app)/settings/team/page.tsx +0 -74
  26. package/app/(app)/settings/workspace/page.tsx +0 -27
  27. package/app/(app)/tasks/[taskId]/page.tsx +0 -79
  28. package/app/(auth)/forgot-password/page.tsx +0 -106
  29. package/app/(auth)/guest/page.tsx +0 -56
  30. package/app/(auth)/layout.tsx +0 -13
  31. package/app/(auth)/login/page.tsx +0 -14
  32. package/app/(auth)/register/page.tsx +0 -193
  33. package/app/(auth)/reset-password/page.tsx +0 -138
  34. package/app/api/account/claim/route.tsx +0 -135
  35. package/app/api/admin/backfill-encryption/route.tsx +0 -43
  36. package/app/api/admin/license/route.tsx +0 -42
  37. package/app/api/auth/2fa/route.tsx +0 -148
  38. package/app/api/auth/[...nextauth]/route.tsx +0 -3
  39. package/app/api/auth/change-password/route.tsx +0 -61
  40. package/app/api/auth/check-2fa/route.tsx +0 -19
  41. package/app/api/auth/forgot-password/route.tsx +0 -65
  42. package/app/api/auth/reset-password/route.tsx +0 -52
  43. package/app/api/auth/verify-2fa/route.tsx +0 -88
  44. package/app/api/backup/download/db/route.ts +0 -29
  45. package/app/api/backup/download/notes/route.ts +0 -25
  46. package/app/api/backup/settings/route.ts +0 -92
  47. package/app/api/billing/checkout/route.tsx +0 -81
  48. package/app/api/billing/migrate/route.tsx +0 -163
  49. package/app/api/billing/portal/route.tsx +0 -24
  50. package/app/api/billing/setup-intent/route.tsx +0 -55
  51. package/app/api/billing/status/route.tsx +0 -36
  52. package/app/api/billing/subscribe/route.tsx +0 -85
  53. package/app/api/billing/webhook/route.tsx +0 -199
  54. package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
  55. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
  56. package/app/api/calendar-feeds/events/route.tsx +0 -82
  57. package/app/api/calendar-feeds/route.tsx +0 -52
  58. package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
  59. package/app/api/cron/calendar-feeds/route.tsx +0 -31
  60. package/app/api/cron/stale-tasks/route.tsx +0 -51
  61. package/app/api/cron/sync/route.tsx +0 -34
  62. package/app/api/devices/[deviceId]/route.tsx +0 -25
  63. package/app/api/devices/route.tsx +0 -41
  64. package/app/api/export/route.tsx +0 -40
  65. package/app/api/feedback/route.tsx +0 -54
  66. package/app/api/folders/[folderId]/route.tsx +0 -51
  67. package/app/api/folders/route.tsx +0 -37
  68. package/app/api/graph/route.tsx +0 -242
  69. package/app/api/guest/route.tsx +0 -58
  70. package/app/api/health/route.tsx +0 -10
  71. package/app/api/holidays/countries/route.tsx +0 -14
  72. package/app/api/holidays/route.tsx +0 -49
  73. package/app/api/holidays/states/route.tsx +0 -21
  74. package/app/api/invites/[token]/route.tsx +0 -131
  75. package/app/api/invites/route.tsx +0 -74
  76. package/app/api/mcp/generate-token/route.tsx +0 -55
  77. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
  78. package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
  79. package/app/api/notes/[noteId]/export/route.tsx +0 -45
  80. package/app/api/notes/[noteId]/route.tsx +0 -360
  81. package/app/api/notes/route.tsx +0 -112
  82. package/app/api/notifications/route.tsx +0 -44
  83. package/app/api/register/route.tsx +0 -67
  84. package/app/api/restore/route.tsx +0 -148
  85. package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
  86. package/app/api/sync/conflicts/route.tsx +0 -48
  87. package/app/api/sync/status/route.tsx +0 -49
  88. package/app/api/sync/trigger/route.tsx +0 -15
  89. package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
  90. package/app/api/tasks/[taskId]/route.tsx +0 -259
  91. package/app/api/tasks/bulk/route.tsx +0 -133
  92. package/app/api/tasks/route.tsx +0 -36
  93. package/app/api/workspace/active/route.tsx +0 -39
  94. package/app/api/workspace/create-team/route.tsx +0 -42
  95. package/app/api/workspace/kanban-statuses/route.tsx +0 -71
  96. package/app/api/workspace/members/[memberId]/route.tsx +0 -69
  97. package/app/api/workspace/route.tsx +0 -24
  98. package/app/download/page.tsx +0 -170
  99. package/app/favicon.ico +0 -0
  100. package/app/generated/prisma/client.d.ts +0 -1
  101. package/app/generated/prisma/client.js +0 -5
  102. package/app/generated/prisma/default.d.ts +0 -1
  103. package/app/generated/prisma/default.js +0 -5
  104. package/app/generated/prisma/edge.d.ts +0 -1
  105. package/app/generated/prisma/edge.js +0 -497
  106. package/app/generated/prisma/index-browser.js +0 -523
  107. package/app/generated/prisma/index.d.ts +0 -46376
  108. package/app/generated/prisma/index.js +0 -497
  109. package/app/generated/prisma/package.json +0 -144
  110. package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
  111. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  112. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
  113. package/app/generated/prisma/runtime/client.d.ts +0 -3386
  114. package/app/generated/prisma/runtime/client.js +0 -86
  115. package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
  116. package/app/generated/prisma/runtime/index-browser.js +0 -6
  117. package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
  118. package/app/generated/prisma/schema.prisma +0 -456
  119. package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
  120. package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
  121. package/app/globals.css +0 -54
  122. package/app/invite/[token]/page.tsx +0 -52
  123. package/app/layout.tsx +0 -90
  124. package/app/mcp/route.tsx +0 -430
  125. package/app/opengraph-image.tsx +0 -120
  126. package/app/page.tsx +0 -398
  127. package/app/privacy/page.tsx +0 -69
  128. package/app/robots.tsx +0 -25
  129. package/app/sitemap.tsx +0 -36
  130. package/app/terms/page.tsx +0 -69
  131. package/app/upgrade/page.tsx +0 -75
  132. package/auth.config.ts +0 -33
  133. package/auth.ts +0 -79
  134. package/components/auth/login-form.tsx +0 -302
  135. package/components/auth/password-checklist.tsx +0 -31
  136. package/components/auth/password-input.tsx +0 -36
  137. package/components/auth/switch-account-button.test.tsx +0 -22
  138. package/components/auth/switch-account-button.tsx +0 -19
  139. package/components/auth/two-factor-input.tsx +0 -116
  140. package/components/billing/billing-dashboard.tsx +0 -265
  141. package/components/billing/card-form.tsx +0 -210
  142. package/components/billing/claim-account-form.tsx +0 -99
  143. package/components/branding/app-logo.test.tsx +0 -20
  144. package/components/branding/app-logo.tsx +0 -25
  145. package/components/calendar/calendar-agenda.tsx +0 -150
  146. package/components/calendar/calendar-drag.test.tsx +0 -177
  147. package/components/calendar/calendar-grid.tsx +0 -357
  148. package/components/calendar/calendar-hooks.test.tsx +0 -27
  149. package/components/calendar/calendar-hooks.ts +0 -351
  150. package/components/calendar/calendar-toolbar.test.tsx +0 -68
  151. package/components/calendar/calendar-toolbar.tsx +0 -291
  152. package/components/calendar/calendar-types.ts +0 -148
  153. package/components/calendar/calendar-view.test.tsx +0 -295
  154. package/components/calendar/calendar-view.tsx +0 -307
  155. package/components/calendar/day-detail-popover.tsx +0 -174
  156. package/components/calendar/task-chip.tsx +0 -86
  157. package/components/command/command-palette.test.tsx +0 -33
  158. package/components/command/command-palette.tsx +0 -310
  159. package/components/download-cta.tsx +0 -87
  160. package/components/feedback/feedback-popup.tsx +0 -207
  161. package/components/graph/graph-draw.ts +0 -337
  162. package/components/graph/graph-overlays.tsx +0 -160
  163. package/components/graph/graph-page.test.tsx +0 -131
  164. package/components/graph/graph-page.tsx +0 -263
  165. package/components/graph/graph-types.ts +0 -47
  166. package/components/graph/graph-view.tsx +0 -322
  167. package/components/guide/guide-view.tsx +0 -522
  168. package/components/kanban/kanban-board.test.tsx +0 -128
  169. package/components/kanban/kanban-board.tsx +0 -361
  170. package/components/kanban/kanban-card-menu.tsx +0 -102
  171. package/components/kanban/kanban-card.tsx +0 -227
  172. package/components/kanban/kanban-column.tsx +0 -49
  173. package/components/kanban/kanban-status-context.tsx +0 -28
  174. package/components/landing/calendar-sandbox.test.tsx +0 -15
  175. package/components/landing/calendar-sandbox.tsx +0 -107
  176. package/components/landing/graph-sandbox.test.tsx +0 -27
  177. package/components/landing/graph-sandbox.tsx +0 -80
  178. package/components/landing/kanban-sandbox.test.tsx +0 -24
  179. package/components/landing/kanban-sandbox.tsx +0 -101
  180. package/components/landing/landing-showcase.test.tsx +0 -21
  181. package/components/landing/landing-showcase.tsx +0 -54
  182. package/components/landing/list-sandbox.tsx +0 -86
  183. package/components/landing/mock-workspace.ts +0 -168
  184. package/components/landing/notes-sandbox.test.tsx +0 -14
  185. package/components/landing/notes-sandbox.tsx +0 -88
  186. package/components/layout/app-shell.tsx +0 -83
  187. package/components/layout/backup-scheduler.tsx +0 -122
  188. package/components/layout/bottom-nav.tsx +0 -43
  189. package/components/layout/icon-bar.test.tsx +0 -29
  190. package/components/layout/icon-bar.tsx +0 -118
  191. package/components/layout/mobile-top-bar.tsx +0 -68
  192. package/components/layout/notes-panel-folder.tsx +0 -127
  193. package/components/layout/notes-panel-note-item.tsx +0 -140
  194. package/components/layout/notes-panel-task-tab.tsx +0 -63
  195. package/components/layout/notes-panel-types.ts +0 -44
  196. package/components/layout/notes-panel.tsx +0 -476
  197. package/components/layout/notification-bell.tsx +0 -251
  198. package/components/layout/paywall-screen.tsx +0 -41
  199. package/components/layout/pro-banner.tsx +0 -76
  200. package/components/layout/sw-register.tsx +0 -27
  201. package/components/layout/workspace-switcher.tsx +0 -90
  202. package/components/notes/mobile-bottom-sheet.tsx +0 -99
  203. package/components/notes/note-editor-context-menu.tsx +0 -47
  204. package/components/notes/note-editor-dom.ts +0 -33
  205. package/components/notes/note-editor-dropdowns.tsx +0 -484
  206. package/components/notes/note-editor-hooks.ts +0 -692
  207. package/components/notes/note-editor-keyboard.ts +0 -305
  208. package/components/notes/note-editor-overlay.tsx +0 -90
  209. package/components/notes/note-editor.test.tsx +0 -372
  210. package/components/notes/note-editor.tsx +0 -662
  211. package/components/notes/note-preview-pane.tsx +0 -156
  212. package/components/notes/note-tabs.tsx +0 -120
  213. package/components/notes/note-types.tsx +0 -157
  214. package/components/settings/accept-invite.tsx +0 -108
  215. package/components/settings/agent-token-settings.tsx +0 -369
  216. package/components/settings/backup-restore-settings.test.tsx +0 -25
  217. package/components/settings/backup-restore-settings.tsx +0 -327
  218. package/components/settings/calendar-feeds-settings.tsx +0 -489
  219. package/components/settings/calendar-general-settings.tsx +0 -174
  220. package/components/settings/confirm-danger-action.test.tsx +0 -215
  221. package/components/settings/confirm-danger-action.tsx +0 -65
  222. package/components/settings/security-settings.tsx +0 -252
  223. package/components/settings/settings-guidance.test.tsx +0 -98
  224. package/components/settings/team-settings.tsx +0 -319
  225. package/components/settings/two-factor-auth.tsx +0 -296
  226. package/components/settings/workspace-settings-client.tsx +0 -363
  227. package/components/settings/workspace-settings-form.tsx +0 -73
  228. package/components/sync/conflict-viewer.tsx +0 -247
  229. package/components/sync/sync-indicator.tsx +0 -171
  230. package/components/tasks/snippet-thread.tsx +0 -119
  231. package/components/tasks/status-dot.tsx +0 -47
  232. package/components/tasks/task-badge.tsx +0 -43
  233. package/components/tasks/task-detail.test.tsx +0 -187
  234. package/components/tasks/task-detail.tsx +0 -458
  235. package/components/tasks/task-list-filters.test.tsx +0 -75
  236. package/components/tasks/task-list-filters.tsx +0 -163
  237. package/components/tasks/task-list-types.ts +0 -20
  238. package/components/tasks/task-list.test.tsx +0 -175
  239. package/components/tasks/task-list.tsx +0 -481
  240. package/components/tasks/task-row.tsx +0 -85
  241. package/components/tasks/task-table-row.tsx +0 -259
  242. package/components/ui/skeleton.tsx +0 -3
  243. package/components/ui/toast.test.tsx +0 -42
  244. package/components/ui/toast.tsx +0 -70
  245. package/electron/main.ts +0 -251
  246. package/electron/preload.ts +0 -56
  247. package/instrumentation.tsx +0 -23
  248. package/lib/api-error.ts +0 -50
  249. package/lib/backup/backup-runner.test.ts +0 -32
  250. package/lib/backup/backup-runner.ts +0 -19
  251. package/lib/backup/backup-schedule.test.ts +0 -23
  252. package/lib/backup/backup-schedule.ts +0 -55
  253. package/lib/backup/backup-settings.test.ts +0 -30
  254. package/lib/backup/backup-settings.ts +0 -27
  255. package/lib/backup/export-notes-zip.test.ts +0 -26
  256. package/lib/backup/export-notes-zip.ts +0 -82
  257. package/lib/backup/export-workspace-backup.test.ts +0 -17
  258. package/lib/backup/export-workspace-backup.ts +0 -77
  259. package/lib/backup/restore-workspace-from-export.test.ts +0 -18
  260. package/lib/backup/restore-workspace-from-export.ts +0 -183
  261. package/lib/backup/types.ts +0 -14
  262. package/lib/brand-icons.ts +0 -1
  263. package/lib/calendar-feed-crypto.ts +0 -38
  264. package/lib/calendar-feed.ts +0 -239
  265. package/lib/client/online-status.ts +0 -47
  266. package/lib/conflict-resolver.test.ts +0 -57
  267. package/lib/conflict-resolver.ts +0 -240
  268. package/lib/db-init.ts +0 -79
  269. package/lib/email.ts +0 -159
  270. package/lib/encryption.test.ts +0 -41
  271. package/lib/encryption.ts +0 -98
  272. package/lib/extract-snippet.test.ts +0 -123
  273. package/lib/extract-snippet.ts +0 -69
  274. package/lib/kanban-status.ts +0 -55
  275. package/lib/license.ts +0 -21
  276. package/lib/limits.ts +0 -31
  277. package/lib/mcp-auth.test.ts +0 -58
  278. package/lib/mcp-auth.ts +0 -65
  279. package/lib/mcp-contract.test.ts +0 -25
  280. package/lib/mcp-contract.ts +0 -210
  281. package/lib/mcp-handler.ts +0 -31
  282. package/lib/mcp-url.test.ts +0 -12
  283. package/lib/mcp-url.ts +0 -7
  284. package/lib/mentions.test.ts +0 -45
  285. package/lib/mentions.ts +0 -73
  286. package/lib/note-crypto.ts +0 -108
  287. package/lib/note-sync.ts +0 -201
  288. package/lib/note-title.ts +0 -93
  289. package/lib/prisma.ts +0 -193
  290. package/lib/pro-flush.ts +0 -292
  291. package/lib/rate-limit.ts +0 -57
  292. package/lib/stripe.ts +0 -38
  293. package/lib/sync-worker.ts +0 -388
  294. package/lib/task-parser.test.ts +0 -91
  295. package/lib/task-parser.ts +0 -81
  296. package/lib/task-utils.ts +0 -52
  297. package/lib/use-is-electron.ts +0 -19
  298. package/lib/use-is-mobile.ts +0 -22
  299. package/lib/validation/calendar-feed.ts +0 -31
  300. package/lib/validation/note.ts +0 -27
  301. package/lib/validation/task.ts +0 -26
  302. package/lib/view-preferences.test.ts +0 -54
  303. package/lib/view-preferences.ts +0 -28
  304. package/lib/workspace.ts +0 -66
  305. package/next.config.ts +0 -21
  306. package/postcss.config.mjs +0 -7
  307. package/prisma/migrations/20260519021916_init/migration.sql +0 -388
  308. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
  309. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
  310. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
  311. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
  312. package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
  313. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
  314. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
  315. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
  316. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
  317. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
  318. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
  319. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
  320. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
  321. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
  322. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
  323. package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
  324. package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
  325. package/prisma/migrations/migration_lock.toml +0 -3
  326. package/prisma/schema.prisma +0 -457
  327. package/public/Logo_icon.svg +0 -1
  328. package/public/file.svg +0 -1
  329. package/public/globe.svg +0 -1
  330. package/public/icon-192.png +0 -0
  331. package/public/icon-512.png +0 -0
  332. package/public/icon.svg +0 -4
  333. package/public/icon_dark.svg +0 -1
  334. package/public/knotpad_icon.svg +0 -1
  335. package/public/knotpad_logo_full.svg +0 -1
  336. package/public/manifest.json +0 -14
  337. package/public/next.svg +0 -1
  338. package/public/sw.js +0 -137
  339. package/public/vercel.svg +0 -1
  340. package/public/window.svg +0 -1
  341. package/tsconfig.json +0 -35
@@ -1,489 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useCallback, useEffect } from "react";
4
- import { useRouter } from "next/navigation";
5
- import {
6
- Trash2,
7
- RefreshCw,
8
- AlertTriangle,
9
- CheckCircle2,
10
- ExternalLink,
11
- ClipboardPaste,
12
- ChevronDown,
13
- ChevronUp,
14
- } from "lucide-react";
15
- import { useToast } from "@/components/ui/toast";
16
- import { ConfirmDangerAction } from "@/components/settings/confirm-danger-action";
17
- import { MAX_CALENDAR_FEEDS, MAX_FEED_LABEL_LEN, FEED_COLORS, FEED_COLOR_KEYS, DEFAULT_FEED_COLOR, type FeedColorKey } from "@/lib/limits";
18
-
19
- /** Quick-connect providers: label, settings deep-link, and a hint for finding the iCal URL. */
20
- const PROVIDERS = [
21
- {
22
- id: "google",
23
- name: "Google Calendar",
24
- href: "https://calendar.google.com/calendar/u/0/r/settings",
25
- hint: "Settings → pick a calendar → scroll to \"Secret address in iCal format\" → copy the URL.",
26
- },
27
- {
28
- id: "apple",
29
- name: "Apple iCloud",
30
- href: "https://www.icloud.com/calendar",
31
- hint: "Click the share icon next to a calendar → enable \"Public Calendar\" → copy the link.",
32
- },
33
- {
34
- id: "outlook",
35
- name: "Outlook / Microsoft 365",
36
- href: "https://outlook.live.com/calendar/0/options/calendar/SharedCalendars",
37
- hint: "Settings → Calendar → Shared calendars → Publish a calendar → copy the ICS link.",
38
- },
39
- ] as const;
40
-
41
- /** Returns true if the string looks like an iCal subscription URL. */
42
- function looksLikeIcsUrl(s: string): boolean {
43
- const trimmed = s.trim();
44
- return /^https?:\/\/\S+\.ics(\b|$)/i.test(trimmed) || trimmed.includes("/ical/") || trimmed.includes("/caldav/");
45
- }
46
-
47
- type Feed = {
48
- id: string;
49
- label: string;
50
- color: string;
51
- enabled: boolean;
52
- lastFetchedAt: string | null;
53
- lastError: string | null;
54
- };
55
-
56
- type Props = { feeds: Feed[] };
57
-
58
- export function CalendarFeedsSettings({ feeds: initialFeeds }: Props) {
59
- const router = useRouter();
60
- const toast = useToast();
61
- const [feeds, setFeeds] = useState<Feed[]>(initialFeeds);
62
- const [label, setLabel] = useState("");
63
- const [url, setUrl] = useState("");
64
- const [color, setColor] = useState<FeedColorKey>(DEFAULT_FEED_COLOR);
65
- const [adding, setAdding] = useState(false);
66
- const [syncingId, setSyncingId] = useState<string | null>(null);
67
- const [deletingId, setDeletingId] = useState<string | null>(null);
68
- const [editingColorId, setEditingColorId] = useState<string | null>(null);
69
- const [providerHint, setProviderHint] = useState<string | null>(null);
70
- const [clipboardNotice, setClipboardNotice] = useState(false);
71
- const [showInstructions, setShowInstructions] = useState(false);
72
- const [deleteConfirm, setDeleteConfirm] = useState<{ open: boolean; feedId: string }>({ open: false, feedId: "" });
73
-
74
- const atLimit = feeds.length >= MAX_CALENDAR_FEEDS;
75
-
76
- // Auto-detect .ics URL on clipboard when the URL field is focused.
77
- const handleUrlFocus = useCallback(async () => {
78
- if (url) return; // field already has content
79
- try {
80
- const text = await navigator.clipboard.readText();
81
- if (looksLikeIcsUrl(text)) {
82
- setUrl(text.trim());
83
- setClipboardNotice(true);
84
- // Auto-fill label if empty — try to guess from URL
85
- if (!label) {
86
- if (text.includes("google")) setLabel("Google Calendar");
87
- else if (text.includes("outlook") || text.includes("office") || text.includes("live")) setLabel("Outlook Calendar");
88
- else if (text.includes("icloud") || text.includes("apple")) setLabel("Apple Calendar");
89
- else setLabel("My Calendar");
90
- }
91
- setTimeout(() => setClipboardNotice(false), 4000);
92
- }
93
- } catch {
94
- // Clipboard permission denied — that's fine, user can paste manually
95
- }
96
- }, [url, label]);
97
-
98
- // Dismiss clipboard notice when URL changes
99
- useEffect(() => {
100
- if (clipboardNotice && url) {
101
- const t = setTimeout(() => setClipboardNotice(false), 3000);
102
- return () => clearTimeout(t);
103
- }
104
- }, [clipboardNotice, url]);
105
-
106
- async function handleAdd(e: React.FormEvent) {
107
- e.preventDefault();
108
- if (!label.trim() || !url.trim()) return;
109
-
110
- setAdding(true);
111
- try {
112
- const res = await fetch("/api/calendar-feeds", {
113
- method: "POST",
114
- headers: { "Content-Type": "application/json" },
115
- body: JSON.stringify({ label: label.trim(), url: url.trim(), color }),
116
- });
117
- const data = await res.json();
118
- if (!res.ok) {
119
- toast(data.error ?? "Failed to add calendar", "error");
120
- return;
121
- }
122
- setFeeds((prev) => [...prev, data]);
123
- setLabel("");
124
- setUrl("");
125
- setColor(DEFAULT_FEED_COLOR);
126
- setProviderHint(null);
127
- if (data.lastError) {
128
- toast(`Calendar added, but the first sync failed: ${data.lastError}`, "error");
129
- } else {
130
- toast("Calendar connected and synced", "success");
131
- }
132
- router.refresh();
133
- } catch {
134
- toast("Failed to add calendar", "error");
135
- } finally {
136
- setAdding(false);
137
- }
138
- }
139
-
140
- async function handleToggle(feed: Feed) {
141
- const next = !feed.enabled;
142
- setFeeds((prev) => prev.map((f) => (f.id === feed.id ? { ...f, enabled: next } : f)));
143
- try {
144
- const res = await fetch(`/api/calendar-feeds/${feed.id}`, {
145
- method: "PATCH",
146
- headers: { "Content-Type": "application/json" },
147
- body: JSON.stringify({ enabled: next }),
148
- });
149
- if (!res.ok) throw new Error();
150
- router.refresh();
151
- } catch {
152
- setFeeds((prev) => prev.map((f) => (f.id === feed.id ? { ...f, enabled: !next } : f)));
153
- toast("Failed to update calendar", "error");
154
- }
155
- }
156
-
157
- async function handleSync(feedId: string) {
158
- setSyncingId(feedId);
159
- try {
160
- const res = await fetch(`/api/calendar-feeds/${feedId}/sync`, { method: "POST" });
161
- const data = await res.json();
162
- if (!res.ok) {
163
- toast(data.error ?? "Sync failed", "error");
164
- return;
165
- }
166
- setFeeds((prev) => prev.map((f) => (f.id === feedId ? { ...f, ...data } : f)));
167
- if (data.lastError) {
168
- toast(`Sync failed: ${data.lastError}`, "error");
169
- } else {
170
- toast("Calendar synced", "success");
171
- }
172
- router.refresh();
173
- } catch {
174
- toast("Sync failed", "error");
175
- } finally {
176
- setSyncingId(null);
177
- }
178
- }
179
-
180
- async function handleDelete(feedId: string) {
181
- setDeleteConfirm({ open: true, feedId });
182
- }
183
-
184
- async function confirmDeleteFeed() {
185
- const { feedId } = deleteConfirm;
186
- setDeletingId(feedId);
187
- try {
188
- const res = await fetch(`/api/calendar-feeds/${feedId}`, { method: "DELETE" });
189
- if (!res.ok) throw new Error();
190
- setFeeds((prev) => prev.filter((f) => f.id !== feedId));
191
- router.refresh();
192
- } catch {
193
- toast("Failed to remove calendar", "error");
194
- } finally {
195
- setDeletingId(null);
196
- setDeleteConfirm({ open: false, feedId: "" });
197
- }
198
- }
199
-
200
- async function handleColorChange(feedId: string, newColor: FeedColorKey) {
201
- setFeeds((prev) => prev.map((f) => (f.id === feedId ? { ...f, color: newColor } : f)));
202
- setEditingColorId(null);
203
- try {
204
- await fetch(`/api/calendar-feeds/${feedId}`, {
205
- method: "PATCH",
206
- headers: { "Content-Type": "application/json" },
207
- body: JSON.stringify({ color: newColor }),
208
- });
209
- router.refresh();
210
- } catch {
211
- toast("Failed to update colour", "error");
212
- }
213
- }
214
-
215
- return (
216
- <div className="space-y-6">
217
- {/* Connected feeds list */}
218
- {feeds.length > 0 && (
219
- <div className="space-y-2">
220
- {feeds.map((feed) => {
221
- const fc =
222
- (FEED_COLORS as Record<string, { bg: string; bgLight: string; border: string; text: string; label: string }>)[
223
- feed.color
224
- ] ?? FEED_COLORS.sky;
225
- return (
226
- <div
227
- key={feed.id}
228
- className="relative rounded-md border border-zinc-800 bg-zinc-900/40 px-3 py-2.5"
229
- >
230
- <div className="flex items-center gap-3">
231
- {/* Colour dot — click to open picker */}
232
- <button
233
- type="button"
234
- onClick={() => setEditingColorId(editingColorId === feed.id ? null : feed.id)}
235
- title="Change colour"
236
- className={`h-4 w-4 shrink-0 rounded-full ring-2 ring-transparent hover:ring-zinc-600 transition-all ${fc.bg}`}
237
- />
238
-
239
- <button
240
- type="button"
241
- role="switch"
242
- aria-checked={feed.enabled}
243
- onClick={() => handleToggle(feed)}
244
- title={feed.enabled ? "Disable" : "Enable"}
245
- className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 ${
246
- feed.enabled ? "bg-emerald-600" : "bg-zinc-700"
247
- }`}
248
- >
249
- <span
250
- className={`inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
251
- feed.enabled ? "translate-x-6" : "translate-x-1"
252
- }`}
253
- />
254
- </button>
255
-
256
- <div className="min-w-0 flex-1">
257
- <p className="truncate text-sm text-zinc-200">{feed.label}</p>
258
- {feed.lastError ? (
259
- <p className="flex items-center gap-1 text-xs text-red-400">
260
- <AlertTriangle size={11} className="shrink-0" />
261
- <span className="truncate">{feed.lastError}</span>
262
- </p>
263
- ) : feed.lastFetchedAt ? (
264
- <p className="flex items-center gap-1 text-xs text-zinc-600">
265
- <CheckCircle2 size={11} className="shrink-0 text-emerald-500" />
266
- Synced {new Date(feed.lastFetchedAt).toLocaleString()}
267
- </p>
268
- ) : (
269
- <p className="text-xs text-zinc-600">Not synced yet</p>
270
- )}
271
- </div>
272
-
273
- <button
274
- type="button"
275
- onClick={() => handleSync(feed.id)}
276
- disabled={syncingId === feed.id}
277
- title="Sync now"
278
- className="text-zinc-500 hover:text-zinc-200 transition-colors disabled:opacity-50"
279
- >
280
- <RefreshCw size={14} className={syncingId === feed.id ? "animate-spin" : ""} />
281
- </button>
282
-
283
- <button
284
- type="button"
285
- onClick={() => handleDelete(feed.id)}
286
- disabled={deletingId === feed.id}
287
- title="Remove calendar"
288
- className="text-zinc-600 hover:text-red-400 transition-colors disabled:opacity-50"
289
- >
290
- <Trash2 size={14} />
291
- </button>
292
- </div>
293
-
294
- {/* Inline colour picker popover */}
295
- {editingColorId === feed.id && (
296
- <div className="mt-2 flex flex-wrap gap-1.5 pl-7">
297
- {FEED_COLOR_KEYS.map((key) => {
298
- const c = FEED_COLORS[key];
299
- const selected = feed.color === key;
300
- return (
301
- <button
302
- key={key}
303
- type="button"
304
- onClick={() => handleColorChange(feed.id, key)}
305
- title={c.label}
306
- className={`h-5 w-5 rounded-full transition-all ${c.bg} ${
307
- selected
308
- ? "ring-2 ring-white ring-offset-2 ring-offset-zinc-900 scale-110"
309
- : "ring-1 ring-zinc-700 hover:ring-zinc-500 hover:scale-110"
310
- }`}
311
- />
312
- );
313
- })}
314
- </div>
315
- )}
316
- </div>
317
- );
318
- })}
319
- </div>
320
- )}
321
-
322
- {atLimit ? (
323
- <p className="text-sm text-zinc-500">
324
- You&apos;ve connected the maximum of {MAX_CALENDAR_FEEDS} calendars. Remove one to add another.
325
- </p>
326
- ) : (
327
- <form onSubmit={handleAdd} className="space-y-4 rounded-md border border-zinc-800 bg-zinc-900/40 p-4">
328
- {/* Quick-connect provider buttons */}
329
- <div className="space-y-2">
330
- <p className="text-xs font-medium text-zinc-500 uppercase tracking-wide">1. Get your calendar URL</p>
331
- <div className="flex flex-wrap gap-2">
332
- {PROVIDERS.map((p) => (
333
- <button
334
- key={p.id}
335
- type="button"
336
- onClick={() => {
337
- window.open(p.href, "_blank", "noopener,noreferrer");
338
- setProviderHint(p.hint);
339
- // Auto-fill label if empty
340
- if (!label) setLabel(p.name);
341
- }}
342
- className={`flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${
343
- providerHint === p.hint
344
- ? "border-zinc-500 bg-zinc-800 text-zinc-200"
345
- : "border-zinc-700 text-zinc-400 hover:border-zinc-600 hover:text-zinc-200 hover:bg-zinc-800/60"
346
- }`}
347
- >
348
- <ExternalLink size={11} />
349
- <span>{p.name}</span>
350
- </button>
351
- ))}
352
- </div>
353
- {providerHint && (
354
- <p className="text-xs text-zinc-400 bg-zinc-800/60 rounded-md px-3 py-2 border border-zinc-700/50">
355
- {providerHint}
356
- </p>
357
- )}
358
- </div>
359
-
360
- {/* Paste URL */}
361
- <div className="space-y-2">
362
- <p className="text-xs font-medium text-zinc-500 uppercase tracking-wide">2. Paste the URL here</p>
363
- <div className="space-y-1">
364
- <div className="relative">
365
- <input
366
- value={url}
367
- onChange={(e) => { setUrl(e.target.value); setClipboardNotice(false); }}
368
- onFocus={handleUrlFocus}
369
- placeholder="https://calendar.google.com/calendar/ical/.../basic.ics"
370
- className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 pr-9 text-sm text-zinc-100 focus:border-zinc-500 focus:outline-none ring-focus"
371
- />
372
- {!url && (
373
- <button
374
- type="button"
375
- onClick={async () => {
376
- try {
377
- const text = await navigator.clipboard.readText();
378
- if (looksLikeIcsUrl(text)) {
379
- setUrl(text.trim());
380
- if (!label) {
381
- if (text.includes("google")) setLabel("Google Calendar");
382
- else if (text.includes("outlook") || text.includes("office") || text.includes("live")) setLabel("Outlook Calendar");
383
- else if (text.includes("icloud") || text.includes("apple")) setLabel("Apple Calendar");
384
- else setLabel("My Calendar");
385
- }
386
- toast("Pasted iCal URL from clipboard", "success");
387
- } else if (text) {
388
- setUrl(text.trim());
389
- toast("Pasted from clipboard", "success");
390
- }
391
- } catch {
392
- toast("Could not read clipboard", "error");
393
- }
394
- }}
395
- title="Paste from clipboard"
396
- className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-300 transition-colors"
397
- >
398
- <ClipboardPaste size={14} />
399
- </button>
400
- )}
401
- </div>
402
- {clipboardNotice && (
403
- <p className="text-xs text-emerald-400 flex items-center gap-1">
404
- <CheckCircle2 size={11} />
405
- Auto-detected iCal URL from your clipboard
406
- </p>
407
- )}
408
- </div>
409
- </div>
410
-
411
- {/* Name */}
412
- <div className="space-y-1">
413
- <label className="text-xs font-medium text-zinc-500 uppercase tracking-wide">Calendar name</label>
414
- <input
415
- value={label}
416
- onChange={(e) => setLabel(e.target.value)}
417
- maxLength={MAX_FEED_LABEL_LEN}
418
- placeholder="e.g. Work Calendar"
419
- className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 focus:border-zinc-500 focus:outline-none ring-focus"
420
- />
421
- </div>
422
-
423
- {/* Colour */}
424
- <div className="space-y-2">
425
- <label className="text-xs font-medium text-zinc-500 uppercase tracking-wide">Colour</label>
426
- <div className="flex flex-wrap gap-2">
427
- {FEED_COLOR_KEYS.map((key) => {
428
- const c = FEED_COLORS[key];
429
- const selected = color === key;
430
- return (
431
- <button
432
- key={key}
433
- type="button"
434
- onClick={() => setColor(key)}
435
- title={c.label}
436
- className={`h-6 w-6 rounded-full transition-all ${c.bg} ${
437
- selected
438
- ? "ring-2 ring-white ring-offset-2 ring-offset-zinc-900 scale-110"
439
- : "ring-1 ring-zinc-700 hover:ring-zinc-500 hover:scale-110"
440
- }`}
441
- />
442
- );
443
- })}
444
- </div>
445
- </div>
446
-
447
- {/* Submit */}
448
- <button
449
- type="submit"
450
- disabled={adding || !label.trim() || !url.trim()}
451
- className="rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-200 disabled:opacity-50 transition-colors"
452
- >
453
- {adding ? "Connecting…" : "Connect calendar"}
454
- </button>
455
-
456
- {/* Collapsible: how it works */}
457
- <button
458
- type="button"
459
- onClick={() => setShowInstructions((v) => !v)}
460
- className="flex items-center gap-1 text-xs text-zinc-600 hover:text-zinc-400 transition-colors"
461
- >
462
- {showInstructions ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
463
- <span>How does this work?</span>
464
- </button>
465
- {showInstructions && (
466
- <div className="rounded-md border border-zinc-800 bg-zinc-900/60 p-3 text-xs text-zinc-500 space-y-1.5">
467
- <p>Knotpad connects to your calendar using a <span className="text-zinc-400">read-only subscription URL</span> (iCal/ICS format). This is a one-way sync — events from your calendar appear as &ldquo;busy blocks&rdquo; in Knotpad&apos;s calendar view.</p>
468
- <p>Your calendar provider never sees your Knotpad data. The URL is encrypted at rest.</p>
469
- <p className="text-zinc-600 pt-1 border-t border-zinc-800">
470
- Events sync automatically every few hours, or you can tap the refresh icon to sync on demand.
471
- </p>
472
- </div>
473
- )}
474
- </form>
475
- )}
476
-
477
- {/* Confirmation dialog */}
478
- <ConfirmDangerAction
479
- open={deleteConfirm.open}
480
- title="Remove Calendar"
481
- body="Remove this calendar? Cached events will be deleted."
482
- confirmLabel="Remove"
483
- cancelLabel="Cancel"
484
- onConfirm={confirmDeleteFeed}
485
- onClose={() => setDeleteConfirm({ open: false, feedId: "" })}
486
- />
487
- </div>
488
- );
489
- }
@@ -1,174 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect } from "react";
4
- import { useRouter } from "next/navigation";
5
-
6
- export function CalendarGeneralSettings() {
7
- const router = useRouter();
8
-
9
- // Public holidays state
10
- const [holidaysEnabled, setHolidaysEnabled] = useState(false);
11
- const [holidayCountry, setHolidayCountry] = useState("MY");
12
- const [holidayRegion, setHolidayRegion] = useState("");
13
- const [countries, setCountries] = useState<{ code: string; name: string }[]>([]);
14
- const [regions, setRegions] = useState<{ code: string; name: string }[]>([]);
15
-
16
- // First day of week state
17
- const [firstDayOfWeek, setFirstDayOfWeek] = useState("Sun"); // "Sun" | "Mon" | "Sat"
18
-
19
- // Initial load — persist defaults to localStorage so other pages (calendar)
20
- // always find an explicit value instead of falling back to browser locale.
21
- useEffect(() => {
22
- const storedCountry = localStorage.getItem("brief:holidays-country");
23
- if (!storedCountry) localStorage.setItem("brief:holidays-country", "MY");
24
- setHolidaysEnabled(localStorage.getItem("brief:holidays") === "true");
25
- setHolidayCountry(storedCountry ?? "MY");
26
- setHolidayRegion(localStorage.getItem("brief:holidays-region") ?? "");
27
- setFirstDayOfWeek(localStorage.getItem("brief:first-day-of-week") ?? "Sun");
28
-
29
- // Fetch countries
30
- fetch("/api/holidays/countries")
31
- .then((r) => (r.ok ? r.json() : { countries: [] }))
32
- .then((d: { countries: { code: string; name: string }[] }) => {
33
- setCountries(d.countries);
34
- })
35
- .catch(() => {});
36
- }, []);
37
-
38
- // Fetch regions when country changes
39
- useEffect(() => {
40
- if (!holidayCountry) {
41
- setRegions([]);
42
- return;
43
- }
44
- fetch(`/api/holidays/states?country=${holidayCountry}`)
45
- .then((r) => (r.ok ? r.json() : { states: [] }))
46
- .then((d: { states: { code: string; name: string }[] }) => {
47
- setRegions(d.states ?? []);
48
- })
49
- .catch(() => setRegions([]));
50
- }, [holidayCountry]);
51
-
52
- // Handle holiday toggle
53
- function handleToggleHolidays() {
54
- const next = !holidaysEnabled;
55
- setHolidaysEnabled(next);
56
- localStorage.setItem("brief:holidays", String(next));
57
- window.dispatchEvent(new Event("brief:holidays-settings-changed"));
58
- router.refresh();
59
- }
60
-
61
- // Handle country change
62
- function handleCountryChange(code: string) {
63
- setHolidayCountry(code);
64
- setHolidayRegion("");
65
- localStorage.setItem("brief:holidays-country", code);
66
- localStorage.removeItem("brief:holidays-region");
67
- window.dispatchEvent(new Event("brief:holidays-settings-changed"));
68
- router.refresh();
69
- }
70
-
71
- // Handle region change
72
- function handleRegionChange(code: string) {
73
- setHolidayRegion(code);
74
- localStorage.setItem("brief:holidays-region", code);
75
- window.dispatchEvent(new Event("brief:holidays-settings-changed"));
76
- router.refresh();
77
- }
78
-
79
- // Handle first day of week change
80
- function handleFirstDayOfWeekChange(val: string) {
81
- setFirstDayOfWeek(val);
82
- localStorage.setItem("brief:first-day-of-week", val);
83
- router.refresh();
84
- }
85
-
86
- return (
87
- <div className="space-y-6 rounded-lg border border-zinc-800 bg-zinc-900/30 p-4">
88
- {/* General Calendar Preferences */}
89
- <div className="space-y-4">
90
- <h3 className="text-sm font-semibold text-zinc-300">Preferences</h3>
91
-
92
- <div className="flex flex-col gap-1.5">
93
- <label className="text-xs font-medium text-zinc-400">First day of week</label>
94
- <select
95
- value={firstDayOfWeek}
96
- onChange={(e) => handleFirstDayOfWeekChange(e.target.value)}
97
- className="w-full max-w-xs rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-300 focus:outline-none focus:border-zinc-500"
98
- >
99
- <option value="Sun">Sunday</option>
100
- <option value="Mon">Monday</option>
101
- <option value="Sat">Saturday</option>
102
- </select>
103
- </div>
104
- </div>
105
-
106
- <hr className="border-zinc-800" />
107
-
108
- {/* Public Holidays Section */}
109
- <div className="space-y-4">
110
- <div className="flex items-center justify-between">
111
- <div className="space-y-0.5">
112
- <h3 className="text-sm font-semibold text-zinc-300">Public Holidays</h3>
113
- <p className="text-xs text-zinc-500">
114
- Display official public holidays directly on your calendar grid.
115
- </p>
116
- </div>
117
- <button
118
- type="button"
119
- role="switch"
120
- aria-checked={holidaysEnabled}
121
- onClick={handleToggleHolidays}
122
- title={holidaysEnabled ? "Disable" : "Enable"}
123
- className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 ${
124
- holidaysEnabled ? "bg-emerald-600" : "bg-zinc-700"
125
- }`}
126
- >
127
- <span
128
- className={`inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
129
- holidaysEnabled ? "translate-x-6" : "translate-x-1"
130
- }`}
131
- />
132
- </button>
133
- </div>
134
-
135
- {holidaysEnabled && (
136
- <div className="grid gap-4 sm:grid-cols-2">
137
- <div className="flex flex-col gap-1.5">
138
- <label className="text-xs font-medium text-zinc-400">Country</label>
139
- <select
140
- value={holidayCountry}
141
- onChange={(e) => handleCountryChange(e.target.value)}
142
- className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-300 focus:outline-none focus:border-zinc-500"
143
- >
144
- {countries.map((c) => (
145
- <option key={c.code} value={c.code}>
146
- {c.name}
147
- </option>
148
- ))}
149
- </select>
150
- </div>
151
-
152
- {regions.length > 0 && (
153
- <div className="flex flex-col gap-1.5">
154
- <label className="text-xs font-medium text-zinc-400">State / Region (Optional)</label>
155
- <select
156
- value={holidayRegion}
157
- onChange={(e) => handleRegionChange(e.target.value)}
158
- className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-300 focus:outline-none focus:border-zinc-500"
159
- >
160
- <option value="">All Regions</option>
161
- {regions.map((r) => (
162
- <option key={r.code} value={r.code}>
163
- {r.name}
164
- </option>
165
- ))}
166
- </select>
167
- </div>
168
- )}
169
- </div>
170
- )}
171
- </div>
172
- </div>
173
- );
174
- }