@knotpad/app 0.1.5 → 0.1.6
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/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/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 +49 -3
- 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/brief.js +0 -311
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useRef } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import type { NoteRef, TaskRef } from "./note-types";
|
|
8
|
+
import { CALLOUTS, formatDatesForDisplay } from "./note-types";
|
|
9
|
+
|
|
10
|
+
export function PreviewPane({
|
|
11
|
+
title,
|
|
12
|
+
content,
|
|
13
|
+
notesList,
|
|
14
|
+
tasksList,
|
|
15
|
+
onToggleTask,
|
|
16
|
+
}: {
|
|
17
|
+
title: string;
|
|
18
|
+
content: string;
|
|
19
|
+
notesList: NoteRef[];
|
|
20
|
+
tasksList: TaskRef[];
|
|
21
|
+
onToggleTask: (index: number) => void;
|
|
22
|
+
}) {
|
|
23
|
+
const checkboxIndex = useRef(0);
|
|
24
|
+
checkboxIndex.current = 0;
|
|
25
|
+
|
|
26
|
+
function linkifyRefs(md: string): string {
|
|
27
|
+
return md
|
|
28
|
+
.replace(/\[\[([^\]]+)\]\]/g, (full, t) => {
|
|
29
|
+
const n = notesList.find((x) => x.title === String(t).trim());
|
|
30
|
+
return n ? `[${t}](/notes/${n.id})` : full;
|
|
31
|
+
})
|
|
32
|
+
.replace(/\(\(([^)]+)\)\)/g, (full, t) => {
|
|
33
|
+
const task = tasksList.find((x) => x.title === String(t).trim());
|
|
34
|
+
return task ? `[${t}](/tasks/${task.id})` : full;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="h-full min-w-0 overflow-y-auto px-6 py-6">
|
|
40
|
+
<h1 className="mb-6 text-2xl font-semibold text-zinc-100">{title || "Untitled"}</h1>
|
|
41
|
+
<div className="prose-brief">
|
|
42
|
+
<ReactMarkdown
|
|
43
|
+
remarkPlugins={[remarkGfm]}
|
|
44
|
+
components={{
|
|
45
|
+
h1: ({ children }) => <h1 className="mb-4 mt-6 text-xl font-bold text-zinc-100">{children}</h1>,
|
|
46
|
+
h2: ({ children }) => <h2 className="mb-3 mt-5 text-lg font-bold text-zinc-100">{children}</h2>,
|
|
47
|
+
h3: ({ children }) => <h3 className="mb-2 mt-4 text-base font-semibold text-zinc-200">{children}</h3>,
|
|
48
|
+
p: ({ children }) => <p className="mb-3 leading-7 text-zinc-300">{children}</p>,
|
|
49
|
+
ul: ({ children }) => <ul className="mb-3 ml-4 list-disc space-y-1 text-zinc-300">{children}</ul>,
|
|
50
|
+
ol: ({ children }) => <ol className="mb-3 ml-4 list-decimal space-y-1 text-zinc-300">{children}</ol>,
|
|
51
|
+
li: ({ children }) => <li className="leading-6 text-zinc-300">{children}</li>,
|
|
52
|
+
input: ({ type, checked, disabled }) => {
|
|
53
|
+
if (type !== "checkbox") return null;
|
|
54
|
+
const idx = checkboxIndex.current++;
|
|
55
|
+
return (
|
|
56
|
+
<input
|
|
57
|
+
type="checkbox"
|
|
58
|
+
checked={checked}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
onChange={() => onToggleTask(idx)}
|
|
61
|
+
className="mr-1.5 accent-blue-500 align-middle cursor-pointer"
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
code: ({ children, className }) => {
|
|
66
|
+
const isBlock = className?.startsWith("language-");
|
|
67
|
+
return isBlock ? (
|
|
68
|
+
<code className="block">{children}</code>
|
|
69
|
+
) : (
|
|
70
|
+
<code className="rounded bg-zinc-800 px-1 py-0.5 font-mono text-sm text-emerald-400">{children}</code>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
pre: ({ children }) => (
|
|
74
|
+
<pre className="mb-3 overflow-x-auto rounded-md border border-zinc-800 bg-zinc-900 p-4 font-mono text-sm text-zinc-300">{children}</pre>
|
|
75
|
+
),
|
|
76
|
+
blockquote: ({ children }) => {
|
|
77
|
+
let matchedType: keyof typeof CALLOUTS | null = null;
|
|
78
|
+
let bodyChildren = children;
|
|
79
|
+
const firstP = React.Children.toArray(children)[0];
|
|
80
|
+
if (
|
|
81
|
+
firstP &&
|
|
82
|
+
typeof firstP === "object" &&
|
|
83
|
+
"type" in firstP &&
|
|
84
|
+
(firstP as React.ReactElement<{ children?: React.ReactNode }>).type === "p"
|
|
85
|
+
) {
|
|
86
|
+
const pChildren = React.Children.toArray(
|
|
87
|
+
(firstP as React.ReactElement<{ children?: React.ReactNode }>).props.children
|
|
88
|
+
);
|
|
89
|
+
const firstText = pChildren[0];
|
|
90
|
+
if (typeof firstText === "string") {
|
|
91
|
+
const marker = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/.exec(firstText);
|
|
92
|
+
if (marker) {
|
|
93
|
+
matchedType = marker[1] as keyof typeof CALLOUTS;
|
|
94
|
+
const rest = firstText.slice(marker[0].length);
|
|
95
|
+
const restOfP = [...pChildren.slice(1)];
|
|
96
|
+
const newFirstP = rest || restOfP.length > 0
|
|
97
|
+
? <p key="callout-body" className="mb-1 leading-7">{rest}{restOfP}</p>
|
|
98
|
+
: null;
|
|
99
|
+
bodyChildren = newFirstP
|
|
100
|
+
? [newFirstP, ...React.Children.toArray(children).slice(1)]
|
|
101
|
+
: React.Children.toArray(children).slice(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (matchedType) {
|
|
106
|
+
const c = CALLOUTS[matchedType];
|
|
107
|
+
const Icon = c.icon;
|
|
108
|
+
return (
|
|
109
|
+
<div className={`mb-3 rounded-md border p-3 ${c.border} ${c.bg}`}>
|
|
110
|
+
<div className={`mb-1 flex items-center gap-1.5 text-sm font-medium ${c.heading}`}>
|
|
111
|
+
<Icon size={14} /> {c.label}
|
|
112
|
+
</div>
|
|
113
|
+
<div className={`text-sm ${c.body}`}>{bodyChildren}</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return (
|
|
118
|
+
<blockquote className="mb-3 border-l-2 border-zinc-600 pl-4 italic text-zinc-500">{children}</blockquote>
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
strong: ({ children }) => <strong className="font-semibold text-zinc-100">{children}</strong>,
|
|
122
|
+
em: ({ children }) => <em className="italic text-zinc-300">{children}</em>,
|
|
123
|
+
hr: () => <hr className="my-4 border-zinc-800" />,
|
|
124
|
+
a: ({ href, children }) => {
|
|
125
|
+
const internal = typeof href === "string" && href.startsWith("/");
|
|
126
|
+
if (internal) {
|
|
127
|
+
return (
|
|
128
|
+
<Link href={href} className="text-violet-400 underline decoration-dotted hover:text-violet-300">
|
|
129
|
+
{children}
|
|
130
|
+
</Link>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return (
|
|
134
|
+
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-400 underline hover:text-blue-300">
|
|
135
|
+
{children}
|
|
136
|
+
</a>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
table: ({ children }) => (
|
|
140
|
+
<div className="mb-3 overflow-x-auto rounded-md border border-zinc-800">
|
|
141
|
+
<table className="w-full border-collapse text-sm text-zinc-300">{children}</table>
|
|
142
|
+
</div>
|
|
143
|
+
),
|
|
144
|
+
thead: ({ children }) => <thead className="border-b border-zinc-700 bg-zinc-900 text-zinc-200">{children}</thead>,
|
|
145
|
+
tbody: ({ children }) => <tbody className="divide-y divide-zinc-800/40">{children}</tbody>,
|
|
146
|
+
tr: ({ children }) => <tr className="hover:bg-zinc-800/30">{children}</tr>,
|
|
147
|
+
th: ({ children }) => <th className="px-3 py-1.5 text-left font-medium">{children}</th>,
|
|
148
|
+
td: ({ children }) => <td className="px-3 py-1.5">{children}</td>,
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
{linkifyRefs(formatDatesForDisplay(content))}
|
|
152
|
+
</ReactMarkdown>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { X, Plus } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
type NoteRef = { id: string; title: string };
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = "brief_open_tabs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Browser-style tabs for open notes. The open set is persisted per device in
|
|
14
|
+
* localStorage; tabs for notes that no longer exist in the active workspace
|
|
15
|
+
* (deleted, or belonging to another workspace) are pruned automatically.
|
|
16
|
+
*/
|
|
17
|
+
export function NoteTabs({ notes }: { notes: NoteRef[] }) {
|
|
18
|
+
const pathname = usePathname();
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
const activeId = pathname.startsWith("/notes/") ? pathname.split("/")[2] : null;
|
|
21
|
+
|
|
22
|
+
const titleById = new Map(notes.map((n) => [n.id, n.title]));
|
|
23
|
+
const [openIds, setOpenIds] = useState<string[]>([]);
|
|
24
|
+
const [loaded, setLoaded] = useState(false);
|
|
25
|
+
|
|
26
|
+
// Load persisted tabs on mount.
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
try {
|
|
29
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
30
|
+
if (raw) setOpenIds(JSON.parse(raw));
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
setLoaded(true);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// Ensure the active note is an open tab; prune tabs that no longer exist.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!loaded) return;
|
|
40
|
+
setOpenIds((prev) => {
|
|
41
|
+
let next = prev.filter((id) => titleById.has(id));
|
|
42
|
+
if (activeId && titleById.has(activeId) && !next.includes(activeId)) {
|
|
43
|
+
next = [...next, activeId];
|
|
44
|
+
}
|
|
45
|
+
if (next.length !== prev.length || next.some((id, i) => id !== prev[i])) {
|
|
46
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch { /* ignore */ }
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
return prev;
|
|
50
|
+
});
|
|
51
|
+
}, [activeId, loaded, notes]);
|
|
52
|
+
|
|
53
|
+
function persist(next: string[]) {
|
|
54
|
+
setOpenIds(next);
|
|
55
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function closeTab(e: React.MouseEvent, id: string) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
e.stopPropagation();
|
|
61
|
+
const idx = openIds.indexOf(id);
|
|
62
|
+
const next = openIds.filter((x) => x !== id);
|
|
63
|
+
persist(next);
|
|
64
|
+
if (id === activeId) {
|
|
65
|
+
const fallback = next[idx] ?? next[idx - 1] ?? next[next.length - 1];
|
|
66
|
+
router.push(fallback ? `/notes/${fallback}` : "/notes");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!loaded || openIds.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex items-stretch gap-px overflow-x-auto border-b border-zinc-800 bg-zinc-950 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
|
74
|
+
{openIds
|
|
75
|
+
.filter((id) => titleById.has(id))
|
|
76
|
+
.map((id) => {
|
|
77
|
+
const active = id === activeId;
|
|
78
|
+
return (
|
|
79
|
+
<Link
|
|
80
|
+
key={id}
|
|
81
|
+
href={`/notes/${id}`}
|
|
82
|
+
className={`group/tab flex max-w-[180px] shrink-0 items-center gap-1.5 border-r border-zinc-800 px-3 py-1.5 text-xs transition-colors ${
|
|
83
|
+
active
|
|
84
|
+
? "bg-zinc-900 text-zinc-100"
|
|
85
|
+
: "text-zinc-500 hover:bg-zinc-900/60 hover:text-zinc-300"
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
<span className="truncate">{titleById.get(id) || "Untitled"}</span>
|
|
89
|
+
<button
|
|
90
|
+
onClick={(e) => closeTab(e, id)}
|
|
91
|
+
className="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded text-zinc-600 opacity-0 hover:bg-zinc-700 hover:text-zinc-200 group-hover/tab:opacity-100"
|
|
92
|
+
title="Close tab"
|
|
93
|
+
>
|
|
94
|
+
<X size={11} />
|
|
95
|
+
</button>
|
|
96
|
+
</Link>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
<button
|
|
100
|
+
onClick={async () => {
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch("/api/notes", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ title: "Untitled", content: "" }),
|
|
106
|
+
});
|
|
107
|
+
if (res.ok) {
|
|
108
|
+
const note = await res.json();
|
|
109
|
+
router.push(`/notes/${note.id}`);
|
|
110
|
+
}
|
|
111
|
+
} catch { /* ignore */ }
|
|
112
|
+
}}
|
|
113
|
+
title="New note"
|
|
114
|
+
className="flex shrink-0 items-center gap-1 border-r border-zinc-800 px-3 py-1.5 text-xs text-zinc-500 hover:bg-zinc-900/60 hover:text-zinc-300 transition-colors"
|
|
115
|
+
>
|
|
116
|
+
<Plus size={12} />
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
MessageSquare, TriangleAlert, LayoutTemplate, Info,
|
|
4
|
+
Lightbulb, MessageSquareWarning, OctagonAlert,
|
|
5
|
+
} from "lucide-react";
|
|
6
|
+
|
|
7
|
+
export type Task = {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
status: string;
|
|
11
|
+
assigneeType: "HUMAN" | "AGENT";
|
|
12
|
+
assignee: { id: string; name: string | null; email: string | null } | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type Member = { handle: string; label: string; isAgent: boolean };
|
|
16
|
+
export type NoteRef = { id: string; title: string };
|
|
17
|
+
export type TaskRef = { id: string; title: string; noteTitle: string };
|
|
18
|
+
|
|
19
|
+
export type Note = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
content: string;
|
|
23
|
+
isLocked: boolean;
|
|
24
|
+
version: number;
|
|
25
|
+
tasks: Task[];
|
|
26
|
+
folder?: { id: string; name: string } | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type Props = {
|
|
30
|
+
note: Note;
|
|
31
|
+
members?: Member[];
|
|
32
|
+
notesList?: NoteRef[];
|
|
33
|
+
tasksList?: TaskRef[];
|
|
34
|
+
highlight?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SlashCommandId = "todo" | "bullet" | "numbered" | "h1" | "h2" | "h3" | "bold" | "italic" | "strike" | "quote" | "code" | "table" | "callout" | "warning" | "hr" | "template";
|
|
38
|
+
|
|
39
|
+
export const SAVE_DEBOUNCE_MS = 1000;
|
|
40
|
+
export const TASK_LINE_REGEX = /^(\s*-\s+\[[ x]\]\s+)(.+)$/;
|
|
41
|
+
export const MENTION_REGEX = /@[\w-]+/g;
|
|
42
|
+
export const FILEREF_REGEX = /<[^>]+>/g;
|
|
43
|
+
export const BADGE_COMMENT_REGEX = /\s*<!--task::[A-Z_]+-->/g;
|
|
44
|
+
export const PRIORITY_TAG_REGEX = /!(low|medium|high|critical)\b/gi;
|
|
45
|
+
|
|
46
|
+
export type PriorityLevel = "critical" | "high" | "medium" | "low";
|
|
47
|
+
|
|
48
|
+
export const PRIORITY_LEVELS: { id: PriorityLevel; label: string; color: string }[] = [
|
|
49
|
+
{ id: "critical", label: "Critical", color: "text-red-500" },
|
|
50
|
+
{ id: "high", label: "High", color: "text-red-400" },
|
|
51
|
+
{ id: "medium", label: "Medium", color: "text-yellow-400" },
|
|
52
|
+
{ id: "low", label: "Low", color: "text-green-400" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export const CALLOUTS = {
|
|
56
|
+
NOTE: { label: "Note", icon: Info, border: "border-blue-500/30", bg: "bg-blue-950/30", heading: "text-blue-400", body: "text-blue-100/80" },
|
|
57
|
+
TIP: { label: "Tip", icon: Lightbulb, border: "border-emerald-500/30", bg: "bg-emerald-950/30", heading: "text-emerald-400", body: "text-emerald-100/80" },
|
|
58
|
+
IMPORTANT: { label: "Important", icon: MessageSquareWarning, border: "border-violet-500/30", bg: "bg-violet-950/30", heading: "text-violet-400", body: "text-violet-100/80" },
|
|
59
|
+
WARNING: { label: "Warning", icon: TriangleAlert, border: "border-amber-500/30", bg: "bg-amber-950/30", heading: "text-amber-400", body: "text-amber-100/80" },
|
|
60
|
+
CAUTION: { label: "Caution", icon: OctagonAlert, border: "border-rose-500/30", bg: "bg-rose-950/30", heading: "text-rose-400", body: "text-rose-100/80" },
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
export const AGENT_HANDLES: Member[] = [
|
|
64
|
+
{ handle: "@agent", label: "AI agent — any connected tool", isAgent: true },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export const SLASH_COMMANDS: { id: SlashCommandId; icon: React.ReactNode; label: string; hint: string }[] = [
|
|
68
|
+
{ id: "todo", icon: "☐", label: "Task", hint: "- [ ] task line" },
|
|
69
|
+
{ id: "bullet", icon: "•", label: "Bullet", hint: "- list item" },
|
|
70
|
+
{ id: "numbered", icon: "1.", label: "Numbered list", hint: "1. ordered item" },
|
|
71
|
+
{ id: "h1", icon: "H1", label: "Heading 1", hint: "# Large heading" },
|
|
72
|
+
{ id: "h2", icon: "H2", label: "Heading 2", hint: "## Medium heading" },
|
|
73
|
+
{ id: "h3", icon: "H3", label: "Heading 3", hint: "### Small heading" },
|
|
74
|
+
{ id: "bold", icon: "B", label: "Bold", hint: "**bold text**" },
|
|
75
|
+
{ id: "italic", icon: "I", label: "Italic", hint: "_italic text_" },
|
|
76
|
+
{ id: "strike", icon: "S", label: "Strikethrough", hint: "~~struck text~~" },
|
|
77
|
+
{ id: "quote", icon: "❝", label: "Quote", hint: "> blockquote" },
|
|
78
|
+
{ id: "code", icon: "<>", label: "Code block", hint: "Fenced ``` block" },
|
|
79
|
+
{ id: "table", icon: "⊞", label: "Table", hint: "| Col | Col |" },
|
|
80
|
+
{ id: "callout", icon: <MessageSquare size={14} />, label: "Callout", hint: "> [!NOTE]" },
|
|
81
|
+
{ id: "warning", icon: <TriangleAlert size={14} />, label: "Warning", hint: "> [!WARNING]" },
|
|
82
|
+
{ id: "hr", icon: "—", label: "Divider", hint: "Horizontal rule" },
|
|
83
|
+
{ id: "template", icon: <LayoutTemplate size={14} />, label: "Template", hint: "Insert a template" },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
export const TEMPLATES = [
|
|
87
|
+
{ id: "standup", label: "Standup", content: "**Yesterday**\n- \n\n**Today**\n- \n\n**Blockers**\n- " },
|
|
88
|
+
{ id: "sprint", label: "Sprint plan", content: "## Sprint Goal\n\n## Deliverables\n- [ ] \n\n## Out of scope\n- " },
|
|
89
|
+
{ id: "meeting", label: "Meeting notes",content: "## Attendees\n- \n\n## Agenda\n- \n\n## Decisions\n- \n\n## Actions\n- [ ] " },
|
|
90
|
+
{ id: "prd", label: "PRD", content: "## Problem\n\n## Solution\n\n## Success metrics\n\n## Out of scope\n" },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const EDITOR_DATE_RANGE_REGEX = /\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}/g;
|
|
94
|
+
const EDITOR_DATE_REGEX = /\d{4}-\d{2}-\d{2}/g;
|
|
95
|
+
|
|
96
|
+
const MONTH_NAMES = [
|
|
97
|
+
"January", "February", "March", "April", "May", "June",
|
|
98
|
+
"July", "August", "September", "October", "November", "December",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const MONTH_SHORT = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
102
|
+
|
|
103
|
+
export function formatDateChip(iso: string): string {
|
|
104
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
105
|
+
return `${d} ${MONTH_SHORT[m - 1]} ${y}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatDateRangeChip(s: string, e: string): string {
|
|
109
|
+
const [sy, sm, sd] = s.split("-").map(Number);
|
|
110
|
+
const [ey, em, ed] = e.split("-").map(Number);
|
|
111
|
+
const sm_ = MONTH_SHORT[sm - 1], em_ = MONTH_SHORT[em - 1];
|
|
112
|
+
if (sy === ey && sm === em) return `${sd}–${ed} ${sm_} ${sy}`;
|
|
113
|
+
if (sy === ey) return `${sd} ${sm_} – ${ed} ${em_} ${sy}`;
|
|
114
|
+
return `${sd} ${sm_} '${String(sy).slice(2)} – ${ed} ${em_} '${String(ey).slice(2)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function formatReadableDate(iso: string): string {
|
|
118
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
119
|
+
return `${d} ${MONTH_NAMES[m - 1]} ${y}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatDatesForDisplay(md: string): string {
|
|
123
|
+
return md
|
|
124
|
+
.replace(EDITOR_DATE_RANGE_REGEX, (range) => {
|
|
125
|
+
const [start, end] = range.split("..");
|
|
126
|
+
return `${formatReadableDate(start)} → ${formatReadableDate(end)}`;
|
|
127
|
+
})
|
|
128
|
+
.replace(EDITOR_DATE_REGEX, (date) => formatReadableDate(date));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatTodayHeader(): string {
|
|
132
|
+
return new Date().toLocaleDateString("en-GB", {
|
|
133
|
+
weekday: "long", day: "numeric", month: "long", year: "numeric",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function extractTaskTitleFromLine(line: string): string | null {
|
|
138
|
+
const m = TASK_LINE_REGEX.exec(line);
|
|
139
|
+
if (!m) return null;
|
|
140
|
+
return m[2]
|
|
141
|
+
.replace(/\[\[[^\]]*\]\]/g, "")
|
|
142
|
+
.replace(/\(\([^)]*\)\)/g, "")
|
|
143
|
+
.replace(MENTION_REGEX, "")
|
|
144
|
+
.replace(FILEREF_REGEX, "")
|
|
145
|
+
.replace(EDITOR_DATE_RANGE_REGEX, "")
|
|
146
|
+
.replace(EDITOR_DATE_REGEX, "")
|
|
147
|
+
.replace(BADGE_COMMENT_REGEX, "")
|
|
148
|
+
.replace(PRIORITY_TAG_REGEX, "")
|
|
149
|
+
.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function stripBadgeComments(content: string): string {
|
|
153
|
+
return content
|
|
154
|
+
.split("\n")
|
|
155
|
+
.map((line) => line.replace(BADGE_COMMENT_REGEX, ""))
|
|
156
|
+
.join("\n");
|
|
157
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
|
|
7
|
+
type Role = "OWNER" | "ADMIN" | "MEMBER";
|
|
8
|
+
const ROLE_LABELS: Record<Role, string> = { OWNER: "Owner", ADMIN: "Admin", MEMBER: "Member" };
|
|
9
|
+
|
|
10
|
+
export function AcceptInvite({
|
|
11
|
+
token,
|
|
12
|
+
email,
|
|
13
|
+
workspaceName,
|
|
14
|
+
invitedBy,
|
|
15
|
+
role,
|
|
16
|
+
isLoggedIn,
|
|
17
|
+
isLoggedInAsInvitee,
|
|
18
|
+
}: {
|
|
19
|
+
token: string;
|
|
20
|
+
email: string;
|
|
21
|
+
workspaceName: string;
|
|
22
|
+
invitedBy: string;
|
|
23
|
+
role: Role;
|
|
24
|
+
isLoggedIn: boolean;
|
|
25
|
+
isLoggedInAsInvitee: boolean;
|
|
26
|
+
}) {
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
const [accepting, setAccepting] = useState(false);
|
|
29
|
+
const [error, setError] = useState("");
|
|
30
|
+
|
|
31
|
+
async function accept() {
|
|
32
|
+
setAccepting(true);
|
|
33
|
+
setError("");
|
|
34
|
+
const res = await fetch(`/api/invites/${token}`, { method: "POST" });
|
|
35
|
+
if (res.ok) {
|
|
36
|
+
router.push("/notes");
|
|
37
|
+
router.refresh();
|
|
38
|
+
} else {
|
|
39
|
+
const d = await res.json();
|
|
40
|
+
setError(d.error ?? "Failed to accept invite");
|
|
41
|
+
setAccepting(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="w-full max-w-sm space-y-6">
|
|
47
|
+
<div className="space-y-1">
|
|
48
|
+
<h1 className="text-2xl font-semibold tracking-tight">You're invited</h1>
|
|
49
|
+
<p className="text-sm text-zinc-400">
|
|
50
|
+
<span className="text-zinc-300">{invitedBy}</span> invited you to join{" "}
|
|
51
|
+
<span className="text-zinc-300">{workspaceName}</span> as a{" "}
|
|
52
|
+
<span className="text-zinc-300">{ROLE_LABELS[role]}</span>.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="rounded-md border border-zinc-800 bg-zinc-900 px-4 py-3 text-sm text-zinc-500">
|
|
57
|
+
Invite sent to: <span className="text-zinc-300">{email}</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
61
|
+
|
|
62
|
+
{!isLoggedIn && (
|
|
63
|
+
<div className="space-y-3">
|
|
64
|
+
<p className="text-sm text-zinc-500">Sign in or register to accept this invite.</p>
|
|
65
|
+
<div className="flex gap-3">
|
|
66
|
+
<Link
|
|
67
|
+
href={`/login?next=/invite/${token}`}
|
|
68
|
+
className="flex-1 rounded-md bg-zinc-100 px-4 py-2 text-center text-sm font-medium text-zinc-900 hover:bg-zinc-200"
|
|
69
|
+
>
|
|
70
|
+
Sign in
|
|
71
|
+
</Link>
|
|
72
|
+
<Link
|
|
73
|
+
href={`/register?next=/invite/${token}`}
|
|
74
|
+
className="flex-1 rounded-md border border-zinc-700 px-4 py-2 text-center text-sm text-zinc-300 hover:bg-zinc-800"
|
|
75
|
+
>
|
|
76
|
+
Register
|
|
77
|
+
</Link>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{isLoggedIn && !isLoggedInAsInvitee && (
|
|
83
|
+
<div className="space-y-3">
|
|
84
|
+
<p className="text-sm text-amber-400">
|
|
85
|
+
You're signed in with a different account. This invite was sent to{" "}
|
|
86
|
+
<span className="font-medium">{email}</span>.
|
|
87
|
+
</p>
|
|
88
|
+
<Link
|
|
89
|
+
href={`/login?next=/invite/${token}`}
|
|
90
|
+
className="block rounded-md border border-zinc-700 px-4 py-2 text-center text-sm text-zinc-300 hover:bg-zinc-800"
|
|
91
|
+
>
|
|
92
|
+
Sign in as {email}
|
|
93
|
+
</Link>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{isLoggedIn && isLoggedInAsInvitee && (
|
|
98
|
+
<button
|
|
99
|
+
onClick={accept}
|
|
100
|
+
disabled={accepting}
|
|
101
|
+
className="w-full rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-200 disabled:opacity-50"
|
|
102
|
+
>
|
|
103
|
+
{accepting ? "Joining…" : `Join ${workspaceName}`}
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|