From 9139d5a93ab73595e4de677e5b5fa119d0bd6209 Mon Sep 17 00:00:00 2001 From: jlightner Date: Sat, 4 Apr 2026 09:13:48 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Built=20Tiptap=20rich=20text=20post=20e?= =?UTF-8?q?ditor=20with=20file=20attachments,=20multipa=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/pages/PostEditor.tsx" - "frontend/src/pages/PostEditor.module.css" - "frontend/src/api/posts.ts" - "frontend/src/api/client.ts" - "frontend/src/App.tsx" - "frontend/src/pages/CreatorDashboard.tsx" GSD-Task: S01/T03 --- .gsd/milestones/M023/slices/S01/S01-PLAN.md | 2 +- .../M023/slices/S01/tasks/T02-VERIFY.json | 9 + .../M023/slices/S01/tasks/T03-SUMMARY.md | 85 ++ frontend/package-lock.json | 838 +++++++++++++++++- frontend/package.json | 5 + frontend/src/App.tsx | 3 + frontend/src/api/client.ts | 46 + frontend/src/api/posts.ts | 96 ++ frontend/src/pages/CreatorDashboard.tsx | 7 + frontend/src/pages/PostEditor.module.css | 392 ++++++++ frontend/src/pages/PostEditor.tsx | 376 ++++++++ frontend/tsconfig.app.tsbuildinfo | 2 +- 12 files changed, 1855 insertions(+), 6 deletions(-) create mode 100644 .gsd/milestones/M023/slices/S01/tasks/T02-VERIFY.json create mode 100644 .gsd/milestones/M023/slices/S01/tasks/T03-SUMMARY.md create mode 100644 frontend/src/api/posts.ts create mode 100644 frontend/src/pages/PostEditor.module.css create mode 100644 frontend/src/pages/PostEditor.tsx diff --git a/.gsd/milestones/M023/slices/S01/S01-PLAN.md b/.gsd/milestones/M023/slices/S01/S01-PLAN.md index 59ec20a..a8ae806 100644 --- a/.gsd/milestones/M023/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M023/slices/S01/S01-PLAN.md @@ -149,7 +149,7 @@ Build both API routers: posts CRUD (create, list, get, update, delete) and file - Estimate: 1h - Files: backend/routers/posts.py, backend/routers/files.py, backend/main.py - Verify: docker exec chrysopedia-api python -c 'from routers.posts import router; from routers.files import router; print("routers ok")' && curl -sf http://ub01:8096/api/v1/posts?creator_id=1 | python3 -m json.tool -- [ ] **T03: Tiptap post editor page with file attachment upload** — ## Description +- [x] **T03: Built Tiptap rich text post editor with file attachments, multipart upload API client, and creator sidebar integration** — ## Description Build the creator-facing post editor with Tiptap rich text editing and file attachment upload. This is the write path — creators use this to compose and publish posts with downloadable files. diff --git a/.gsd/milestones/M023/slices/S01/tasks/T02-VERIFY.json b/.gsd/milestones/M023/slices/S01/tasks/T02-VERIFY.json new file mode 100644 index 0000000..57fc299 --- /dev/null +++ b/.gsd/milestones/M023/slices/S01/tasks/T02-VERIFY.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M023/S01/T02", + "timestamp": 1775293655825, + "passed": true, + "discoverySource": "none", + "checks": [] +} diff --git a/.gsd/milestones/M023/slices/S01/tasks/T03-SUMMARY.md b/.gsd/milestones/M023/slices/S01/tasks/T03-SUMMARY.md new file mode 100644 index 0000000..c1b0d18 --- /dev/null +++ b/.gsd/milestones/M023/slices/S01/tasks/T03-SUMMARY.md @@ -0,0 +1,85 @@ +--- +id: T03 +parent: S01 +milestone: M023 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/pages/PostEditor.tsx", "frontend/src/pages/PostEditor.module.css", "frontend/src/api/posts.ts", "frontend/src/api/client.ts", "frontend/src/App.tsx", "frontend/src/pages/CreatorDashboard.tsx"] +key_decisions: ["Tiptap v3 useEditor requires immediatelyRender: false for React 18", "requestMultipart helper in client.ts for FormData uploads without Content-Type header"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "cd frontend && npm run build passes with zero errors. All 179 modules transform successfully. PostEditor lazy-loads as separate chunk." +completed_at: 2026-04-04T09:13:17.608Z +blocker_discovered: false +--- + +# T03: Built Tiptap rich text post editor with file attachments, multipart upload API client, and creator sidebar integration + +> Built Tiptap rich text post editor with file attachments, multipart upload API client, and creator sidebar integration + +## What Happened +--- +id: T03 +parent: S01 +milestone: M023 +key_files: + - frontend/src/pages/PostEditor.tsx + - frontend/src/pages/PostEditor.module.css + - frontend/src/api/posts.ts + - frontend/src/api/client.ts + - frontend/src/App.tsx + - frontend/src/pages/CreatorDashboard.tsx +key_decisions: + - Tiptap v3 useEditor requires immediatelyRender: false for React 18 + - requestMultipart helper in client.ts for FormData uploads without Content-Type header +duration: "" +verification_result: passed +completed_at: 2026-04-04T09:13:17.608Z +blocker_discovered: false +--- + +# T03: Built Tiptap rich text post editor with file attachments, multipart upload API client, and creator sidebar integration + +**Built Tiptap rich text post editor with file attachments, multipart upload API client, and creator sidebar integration** + +## What Happened + +Installed Tiptap v3 dependencies. Added requestMultipart helper to client.ts for FormData uploads. Created api/posts.ts with full TypeScript types and CRUD + file upload/download functions. Built PostEditor.tsx with Tiptap editor (StarterKit + Link + Placeholder), formatting toolbar, title input, drag-and-drop file attachment zone, publish toggle, and save flow. Added lazy-loaded routes and SidebarNav link. + +## Verification + +cd frontend && npm run build passes with zero errors. All 179 modules transform successfully. PostEditor lazy-loads as separate chunk. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npm run build 2>&1 | tail -5` | 0 | ✅ pass | 6200ms | + + +## Deviations + +Added immediatelyRender: false to useEditor config (required by Tiptap v3 for React 18). Removed unused useAuth import. Used explicit Editor | null type for toolbar props. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/pages/PostEditor.tsx` +- `frontend/src/pages/PostEditor.module.css` +- `frontend/src/api/posts.ts` +- `frontend/src/api/client.ts` +- `frontend/src/App.tsx` +- `frontend/src/pages/CreatorDashboard.tsx` + + +## Deviations +Added immediatelyRender: false to useEditor config (required by Tiptap v3 for React 18). Removed unused useAuth import. Used explicit Editor | null type for toolbar props. + +## Known Issues +None. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f0b6d2e..50be9af 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,11 @@ "name": "chrysopedia-web", "version": "0.8.0", "dependencies": { + "@tiptap/extension-link": "^3.22.1", + "@tiptap/extension-placeholder": "^3.22.1", + "@tiptap/pm": "^3.22.1", + "@tiptap/react": "^3.22.1", + "@tiptap/starter-kit": "^3.22.1", "hls.js": "^1.6.15", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -746,6 +751,34 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT", + "optional": true + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -796,6 +829,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1162,6 +1201,453 @@ "win32" ] }, + "node_modules/@tiptap/core": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.1.tgz", + "integrity": "sha512-6wPNhkdLIGYiKAGqepDCRtR0TYGJxV40SwOEN2vlPhsXqAgzmyG37UyREj5pGH5xTekugqMCgCnyRg7m5nYoYQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.22.1.tgz", + "integrity": "sha512-omPsJ/IMAZYhXqOaEenYE+HA9U2zju5rQbAn6Xktynvr4A5P95jqkgAwncXB82pCkNYU/uYxi51vyTweTeEUHA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.1.tgz", + "integrity": "sha512-0+q6Apu1Vx2+ReB2ktTpBrQ5/dCvGzTkJCy+MZ/t8WBcybqFXOKYRCr/i/VGPDpXZttxpk0EPl0+ao+NVcUTAA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.1.tgz", + "integrity": "sha512-JJI63N55hLPjfqHgBnbG1ORZTXJiswnfBkfNd8YKytCC8D++g5qX3UMObxmJKLMBRGyqjEi6krzOyYtOix5ALA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.22.1.tgz", + "integrity": "sha512-83L+4N2JziWORbWtlsM0xBm3LOKIw4YtIm+Kh4amV5kGvIgIL5I1KYzoxv20qjgFX2k08LtLMwPdvPSPSh4e7g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.1.tgz", + "integrity": "sha512-Ze+hjSLLCn+5gVpuE/Uv7mQ83AlG5A9OPsuDoyzTpJ2XNvZP2iZdwQMGqwXKC8eH7fIOJN6XQ3IDv/EhltQx/Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.1.tgz", + "integrity": "sha512-fr3b1seFsAeYHtPAb9fbATkGcgyfStD05GHsZXFLh7yCpf2ejWLNxdWJT/g+FggSEHYFKCXT06aixk0WbtRcWw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.1.tgz", + "integrity": "sha512-fBI/+PGtK6pzitqjSSSYL2+uZglX6T53zb5nLEmN/q8q7FzUuUpglp8toHVhBG05WDk4vx6Z7bC95uyxkYdoAA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.1.tgz", + "integrity": "sha512-PuSNoTROZB564KpTG9ExVB3CsfRa0ridHx+1sWZajOBVZJiXSn4QlS/ShS509SOx8z17DyxEw06IH//OHY9XyQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.1.tgz", + "integrity": "sha512-TaZqmaoKv36FzbKTrBkkv74o0t8dYTftNZ7NotBqfSki0BB2PupTCJHafdu1YI0zmJ3xEzjB/XKcKPz2+10sDA==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.1.tgz", + "integrity": "sha512-qqsyy7unWM3elv+7ru+6paKAnw1PZTvjNVQu3UzB6d556Gx2uE4isXJNdBaslBZdp2EoaYdIkhhEccW9B/Nwqg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.22.1.tgz", + "integrity": "sha512-hzLwLEZVbZODa9q5UiCQpOUmDnyxN19FA4LhlqLP0/JSHewP/aol5igFZwuw0XVFp425BuzPjrB7tmr0GRTDWw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.22.1.tgz", + "integrity": "sha512-EaIihzrOfXUHQlL6fFyJCkDrjgg0e/eD4jpkjhKpeuJDcqf7eJ1c0E2zcNRAiZkeXdN/hTQFaXKsSyNUE7T7Sg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.1.tgz", + "integrity": "sha512-Q18A8IN+gnfptIksPeVAI6oOBGYKAGf+QN0FEJ5OXO4BEAmA3hflflA1rWNfPC4aQNry/N7sAl8Gpd6HuIbz2w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.1.tgz", + "integrity": "sha512-EXPZWEsWJK9tUMypddOBvayaBeu8wFV2uH5PNrtDKrfRZ1Bf8GQ3lfcO0blHssaQ9nWqa9HwBC1mdfWcmfpxig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.1.tgz", + "integrity": "sha512-RHch/Bqv+QDvW3J1CXmiTB54pyrQYNQq8Vfa7is/O209dNPA8tdbkRP44rDjqn8NeDCriC/oJ4avWeXL4qNDVw==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.1.tgz", + "integrity": "sha512-6bVI5A12sFeyb0EngABV8/qCtC2IgiDbWC8mtNNLh5dAVGaUKo1KucL6vRYDhzXhyO/eHuGYepXZDLOOdS9LIQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.22.1.tgz", + "integrity": "sha512-v0FgSX3cqLY3L1hIe2PFRTR3/+wlFOdFjv0p3fSJ5Tl7cgU7DR1OcljFqpw0exePcmt6dXqXVQua3PxSVV15eA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.22.1.tgz", + "integrity": "sha512-00Nz4jJygYGJg6N1mdbQUslFG9QaGZq5P9MFwqoduWku7gYHWkZoZvrkxZrYtxGTHVIlLnF8LIfblAlOwNd76g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.1.tgz", + "integrity": "sha512-sbd99ZUa1lIemH7N6dLB+9aYxUgduwW2216VM3dLJBS9hmTA4iDRxWx0a1ApnAVv+sZasRSbb/wpYLtXviA1XQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.1.tgz", + "integrity": "sha512-mnvGEZfZFysHGvmEqrSLjeddaNPB3UmomTInv9gsImw8hlB4/gQedvB6Qf2tFfIjl4ISKC5AbFxraSnJfjaL5g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.22.1.tgz", + "integrity": "sha512-f8NJNEJTDuT9UIZdVIAPoySgzQ/nKxR/gWRqCnwtR4O26zo/JdKI2XvrTE/iNrV3Khme8rjCtO7/8CQgTeMMxA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.1.tgz", + "integrity": "sha512-LTdnGmglK1f/AW//36k+Km8URA1wrTLENi3R5N+/ipv+yP2rZ2Ki1R1m6yJx3KSFzR55c91xE6659/vz1uZ6iA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.1.tgz", + "integrity": "sha512-wFCNCATSTTFhvA9wOPkAgzPVyG3RM6+jOlDeRhHUCHsFWFWj0w9ZPwA/nP+Qi5hEW7kGG9V8o62RjBdHNvK2PQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.1.tgz", + "integrity": "sha512-p8/ErqQInWJbpncBycIggmtCjdrMwHmA3GNhOugo6F4fYfeVxgy7pVb7ZF+ss62d0mpQvEd81pyrzhkBtb0nBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.1.tgz", + "integrity": "sha512-BKpp371Pl1CVcLRLrWH7PC1I+IsXOhet80+pILqCMlwkJnsVtOOVRr5uCF6rbPP4xK5H/ehkQWmxA8rqpv42aA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.1.tgz", + "integrity": "sha512-OSqSg2974eLJT5PNKFLM7156lBXCUf/dsKTQXWSzsLTf6HOP4dYP6c0YbAk6lgbNI+BdszsHNClmLVLA8H/L9A==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.22.1.tgz", + "integrity": "sha512-1pIRfgK9wape4nDXVJRfgUcYVZdPPkuECbGtz8bo0rgtdsVN7B8PBVCDyuitZ7acdLbMuuX5+TxeUOvME8np7Q==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.22.1", + "@tiptap/extension-floating-menu": "^3.22.1" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/pm": "^3.22.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.22.1.tgz", + "integrity": "sha512-1fFmURkgofxgP9GW993bSpxf2rIJzQbWZ9rPw17qbAVuGouIArG+Fd/A1WUD95Vdbx6JIrc1QxbNlLs7bhcoPA==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.22.1", + "@tiptap/extension-blockquote": "^3.22.1", + "@tiptap/extension-bold": "^3.22.1", + "@tiptap/extension-bullet-list": "^3.22.1", + "@tiptap/extension-code": "^3.22.1", + "@tiptap/extension-code-block": "^3.22.1", + "@tiptap/extension-document": "^3.22.1", + "@tiptap/extension-dropcursor": "^3.22.1", + "@tiptap/extension-gapcursor": "^3.22.1", + "@tiptap/extension-hard-break": "^3.22.1", + "@tiptap/extension-heading": "^3.22.1", + "@tiptap/extension-horizontal-rule": "^3.22.1", + "@tiptap/extension-italic": "^3.22.1", + "@tiptap/extension-link": "^3.22.1", + "@tiptap/extension-list": "^3.22.1", + "@tiptap/extension-list-item": "^3.22.1", + "@tiptap/extension-list-keymap": "^3.22.1", + "@tiptap/extension-ordered-list": "^3.22.1", + "@tiptap/extension-paragraph": "^3.22.1", + "@tiptap/extension-strike": "^3.22.1", + "@tiptap/extension-text": "^3.22.1", + "@tiptap/extension-underline": "^3.22.1", + "@tiptap/extensions": "^3.22.1", + "@tiptap/pm": "^3.22.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1214,18 +1700,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1236,12 +1742,17 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1263,6 +1774,12 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.12", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", @@ -1338,11 +1855,16 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -1370,6 +1892,18 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1422,6 +1956,27 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1503,6 +2058,21 @@ "node": ">=6" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1525,6 +2095,29 @@ "yallist": "^3.0.2" } }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1558,6 +2151,12 @@ "dev": true, "license": "MIT" }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1607,6 +2206,210 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", + "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz", + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.8", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", + "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1719,6 +2522,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1779,6 +2588,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -1810,6 +2625,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -1885,6 +2709,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/wavesurfer.js": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2d20444..17c248b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,11 @@ "preview": "vite preview" }, "dependencies": { + "@tiptap/extension-link": "^3.22.1", + "@tiptap/extension-placeholder": "^3.22.1", + "@tiptap/pm": "^3.22.1", + "@tiptap/react": "^3.22.1", + "@tiptap/starter-kit": "^3.22.1", "hls.js": "^1.6.15", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bade708..bb840bf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,6 +25,7 @@ const ChatPage = React.lazy(() => import("./pages/ChatPage")); const ChapterReview = React.lazy(() => import("./pages/ChapterReview")); const HighlightQueue = React.lazy(() => import("./pages/HighlightQueue")); const CreatorTiers = React.lazy(() => import("./pages/CreatorTiers")); +const PostEditor = React.lazy(() => import("./pages/PostEditor")); import AdminDropdown from "./components/AdminDropdown"; import ImpersonationBanner from "./components/ImpersonationBanner"; import AppFooter from "./components/AppFooter"; @@ -207,6 +208,8 @@ function AppShell() { }>} /> }>} /> }>} /> + }>} /> + }>} /> {/* Fallback */} } /> diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 41389ad..47f1158 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -54,3 +54,49 @@ export async function request(url: string, init?: RequestInit): Promise { return res.json() as Promise; } + +/** + * Multipart form-data request — does NOT set Content-Type (browser adds + * multipart boundary automatically). Still attaches auth token. + */ +export async function requestMultipart( + url: string, + formData: FormData, + init?: RequestInit, +): Promise { + const token = getStoredToken(); + const headers: Record = { + ...(init?.headers as Record), + }; + if (token && !headers["Authorization"]) { + headers["Authorization"] = `Bearer ${token}`; + } + + const res = await fetch(url, { + method: "POST", + ...init, + headers, + body: formData, + }); + + if (!res.ok) { + let detail = res.statusText; + try { + const body: unknown = await res.json(); + if (typeof body === "object" && body !== null && "detail" in body) { + const d = (body as { detail: unknown }).detail; + detail = + typeof d === "string" + ? d + : Array.isArray(d) + ? d.map((e: any) => e.msg || JSON.stringify(e)).join("; ") + : JSON.stringify(d); + } + } catch { + // body not JSON — keep statusText + } + throw new ApiError(res.status, detail); + } + + return res.json() as Promise; +} diff --git a/frontend/src/api/posts.ts b/frontend/src/api/posts.ts new file mode 100644 index 0000000..f618191 --- /dev/null +++ b/frontend/src/api/posts.ts @@ -0,0 +1,96 @@ +/** + * Post API — CRUD for creator posts + file upload/download. + */ + +import { BASE, request, requestMultipart } from "./client"; + +// ── Types ─────────────────────────────────────────────────────────────────── + +export interface PostAttachmentRead { + id: string; + filename: string; + content_type: string; + size_bytes: number; + download_url: string | null; + created_at: string; +} + +export interface PostRead { + id: string; + creator_id: string; + title: string; + body_json: Record; + is_published: boolean; + created_at: string; + updated_at: string; + attachments: PostAttachmentRead[]; +} + +export interface PostCreate { + title: string; + body_json: Record; + is_published?: boolean; +} + +export interface PostUpdate { + title?: string; + body_json?: Record; + is_published?: boolean; +} + +export interface PostListResponse { + items: PostRead[]; + total: number; +} + +// ── API Functions ─────────────────────────────────────────────────────────── + +export function createPost(data: PostCreate): Promise { + return request(`${BASE}/posts`, { + method: "POST", + body: JSON.stringify(data), + }); +} + +export function updatePost(id: string, data: PostUpdate): Promise { + return request(`${BASE}/posts/${id}`, { + method: "PUT", + body: JSON.stringify(data), + }); +} + +export function getPost(id: string): Promise { + return request(`${BASE}/posts/${id}`); +} + +export function listPosts( + creatorId: string, + page = 1, + limit = 20, +): Promise { + return request( + `${BASE}/posts?creator_id=${creatorId}&page=${page}&limit=${limit}`, + ); +} + +export function uploadFile( + postId: string, + file: File, +): Promise { + const form = new FormData(); + form.append("file", file); + form.append("post_id", postId); + return requestMultipart(`${BASE}/files/upload`, form); +} + +export function getDownloadUrl( + attachmentId: string, +): Promise<{ url: string; filename: string }> { + return request<{ url: string; filename: string }>( + `${BASE}/files/${attachmentId}/download`, + ); +} + +export function deletePost(id: string): Promise { + return request(`${BASE}/posts/${id}`, { method: "DELETE" }); +} diff --git a/frontend/src/pages/CreatorDashboard.tsx b/frontend/src/pages/CreatorDashboard.tsx index e2038d5..f04af87 100644 --- a/frontend/src/pages/CreatorDashboard.tsx +++ b/frontend/src/pages/CreatorDashboard.tsx @@ -59,6 +59,13 @@ function SidebarNav() { Tiers + + + + + + New Post + ); } diff --git a/frontend/src/pages/PostEditor.module.css b/frontend/src/pages/PostEditor.module.css new file mode 100644 index 0000000..77e1156 --- /dev/null +++ b/frontend/src/pages/PostEditor.module.css @@ -0,0 +1,392 @@ +/* PostEditor.module.css — dark theme, consistent with CreatorDashboard */ + +.layout { + display: flex; + gap: 0; + min-height: 60vh; +} + +.content { + flex: 1; + min-width: 0; + padding: 2rem 2.5rem; +} + +.header { + margin-bottom: 1.5rem; +} + +.pageTitle { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-text-primary); + margin: 0; +} + +.loadingText { + color: var(--color-text-secondary); + font-size: 0.95rem; + padding: 2rem; +} + +/* ── Error Banner ──────────────────────────────────────────────────────────── */ + +.errorBanner { + background: var(--color-error-bg); + color: var(--color-error); + padding: 0.75rem 1rem; + border-radius: 8px; + font-size: 0.875rem; + margin-bottom: 1rem; +} + +/* ── Title Input ───────────────────────────────────────────────────────────── */ + +.titleInput { + display: block; + width: 100%; + padding: 0.75rem 1rem; + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text-primary); + background: var(--color-bg-input); + border: 1px solid var(--color-border); + border-radius: 8px; + outline: none; + transition: border-color 0.15s; + margin-bottom: 1rem; +} + +.titleInput:focus { + border-color: var(--color-border-active); +} + +.titleInput::placeholder { + color: var(--color-text-muted); +} + +/* ── Editor Wrapper ────────────────────────────────────────────────────────── */ + +.editorWrapper { + border: 1px solid var(--color-border); + border-radius: 8px; + overflow: hidden; + background: var(--color-bg-input); + margin-bottom: 1.5rem; +} + +/* ── Toolbar ───────────────────────────────────────────────────────────────── */ + +.toolbar { + display: flex; + align-items: center; + gap: 2px; + padding: 0.5rem 0.75rem; + background: var(--color-bg-surface); + border-bottom: 1px solid var(--color-border); + flex-wrap: wrap; +} + +.toolbarBtn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + padding: 0 0.5rem; + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-text-secondary); + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + cursor: pointer; + transition: color 0.15s, background 0.15s, border-color 0.15s; + font-family: inherit; +} + +.toolbarBtn:hover { + color: var(--color-text-primary); + background: var(--color-bg-surface-hover); +} + +.toolbarBtnActive { + color: var(--color-accent); + background: var(--color-accent-subtle); + border-color: var(--color-accent); +} + +.toolbarSep { + width: 1px; + height: 20px; + background: var(--color-border); + margin: 0 0.375rem; +} + +/* ── Editor Content ────────────────────────────────────────────────────────── */ + +.editorContent { + min-height: 300px; + padding: 1rem; +} + +/* ProseMirror styles inside the editor */ +.proseMirror { + outline: none; + min-height: 280px; + color: var(--color-text-primary); + font-size: 0.9375rem; + line-height: 1.7; +} + +.proseMirror h2 { + font-size: 1.25rem; + font-weight: 700; + margin: 1.5rem 0 0.5rem; + color: var(--color-text-primary); +} + +.proseMirror h3 { + font-size: 1.0625rem; + font-weight: 600; + margin: 1.25rem 0 0.375rem; + color: var(--color-text-primary); +} + +.proseMirror p { + margin: 0 0 0.75rem; +} + +.proseMirror ul, +.proseMirror ol { + padding-left: 1.5rem; + margin: 0 0 0.75rem; +} + +.proseMirror code { + background: var(--color-bg-surface); + padding: 0.15em 0.35em; + border-radius: 4px; + font-size: 0.875em; +} + +.proseMirror pre { + background: var(--color-bg-surface); + padding: 0.875rem 1rem; + border-radius: 8px; + overflow-x: auto; + font-size: 0.8125rem; + margin: 0 0 0.75rem; +} + +.proseMirror pre code { + background: none; + padding: 0; + border-radius: 0; +} + +.proseMirror a, +.editorLink { + color: var(--color-accent); + text-decoration: underline; + text-underline-offset: 2px; +} + +/* Tiptap placeholder */ +.proseMirror p.is-editor-empty:first-child::before { + color: var(--color-text-muted); + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; +} + +/* ── Attachments Section ───────────────────────────────────────────────────── */ + +.attachmentsSection { + margin-bottom: 1.5rem; +} + +.attachmentsTitle { + font-size: 0.9375rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0 0 0.75rem; +} + +.fileList { + list-style: none; + margin: 0 0 0.75rem; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.fileItem { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background: var(--color-bg-surface); + border: 1px solid var(--color-border); + border-radius: 6px; + font-size: 0.8125rem; +} + +.fileIcon { + flex-shrink: 0; +} + +.fileName { + color: var(--color-text-primary); + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.fileSize { + color: var(--color-text-secondary); + flex-shrink: 0; +} + +.fileRemoveBtn { + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + font-size: 0.875rem; + padding: 0.125rem 0.375rem; + border-radius: 4px; + transition: color 0.15s, background 0.15s; +} + +.fileRemoveBtn:hover { + color: var(--color-error); + background: var(--color-error-bg); +} + +/* ── Drop Zone ─────────────────────────────────────────────────────────────── */ + +.dropZone { + border: 2px dashed var(--color-border); + border-radius: 8px; + padding: 1.5rem; + text-align: center; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} + +.dropZone:hover, +.dropZoneActive { + border-color: var(--color-accent); + background: var(--color-accent-subtle); +} + +.dropZoneText { + color: var(--color-text-secondary); + font-size: 0.875rem; + margin: 0 0 0.25rem; +} + +.dropZoneHint { + color: var(--color-text-muted); + font-size: 0.75rem; + margin: 0; +} + +.hiddenFileInput { + display: none; +} + +/* ── Footer Controls ───────────────────────────────────────────────────────── */ + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 1rem; + border-top: 1px solid var(--color-border); +} + +.publishToggle { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--color-text-secondary); + font-size: 0.875rem; + cursor: pointer; +} + +.publishToggle input[type="checkbox"] { + accent-color: var(--color-accent); + width: 16px; + height: 16px; +} + +.footerActions { + display: flex; + gap: 0.75rem; +} + +.cancelBtn { + padding: 0.5rem 1.25rem; + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-text-secondary); + background: transparent; + border: 1px solid var(--color-border); + border-radius: 8px; + cursor: pointer; + transition: color 0.15s, border-color 0.15s; +} + +.cancelBtn:hover { + color: var(--color-text-primary); + border-color: var(--color-text-secondary); +} + +.saveBtn { + padding: 0.5rem 1.5rem; + font-size: 0.8125rem; + font-weight: 600; + color: #000; + background: var(--color-accent); + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s, opacity 0.15s; +} + +.saveBtn:hover { + background: var(--color-accent-hover); +} + +.saveBtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ── Responsive ────────────────────────────────────────────────────────────── */ + +@media (max-width: 768px) { + .layout { + flex-direction: column; + } + + .content { + padding: 1.25rem 1rem; + } + + .footer { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .footerActions { + justify-content: flex-end; + } +} diff --git a/frontend/src/pages/PostEditor.tsx b/frontend/src/pages/PostEditor.tsx new file mode 100644 index 0000000..d71eeb7 --- /dev/null +++ b/frontend/src/pages/PostEditor.tsx @@ -0,0 +1,376 @@ +/** + * PostEditor — Tiptap rich text editor for creator posts with file attachments. + * + * /creator/posts/new → create mode + * /creator/posts/:postId/edit → edit mode (loads existing post) + */ + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useEditor, EditorContent, type Editor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Link from "@tiptap/extension-link"; +import Placeholder from "@tiptap/extension-placeholder"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; +import { + createPost, + getPost, + updatePost, + uploadFile, + type PostAttachmentRead, + type PostRead, +} from "../api/posts"; +import { ApiError } from "../api/client"; +import { SidebarNav } from "./CreatorDashboard"; +import styles from "./PostEditor.module.css"; + +// ── Toolbar Button ────────────────────────────────────────────────────────── + +function ToolbarBtn({ + label, + active, + onClick, + children, +}: { + label: string; + active?: boolean; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +// ── Editor Toolbar ────────────────────────────────────────────────────────── + +function EditorToolbar({ editor }: { editor: Editor | null }) { + if (!editor) return null; + + const setLink = () => { + const prev = editor.getAttributes("link").href ?? ""; + const url = window.prompt("URL", prev); + if (url === null) return; // cancelled + if (url === "") { + editor.chain().focus().extendMarkRange("link").unsetLink().run(); + } else { + editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run(); + } + }; + + return ( +
+ editor.chain().focus().toggleBold().run()}> + B + + editor.chain().focus().toggleItalic().run()}> + I + + + editor.chain().focus().toggleHeading({ level: 2 }).run()}> + H2 + + editor.chain().focus().toggleHeading({ level: 3 }).run()}> + H3 + + + editor.chain().focus().toggleBulletList().run()}> + • + + editor.chain().focus().toggleOrderedList().run()}> + 1. + + + editor.chain().focus().toggleCodeBlock().run()}> + {""} + + + 🔗 + +
+ ); +} + +// ── File size formatter ───────────────────────────────────────────────────── + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +// ── Main Component ────────────────────────────────────────────────────────── + +export default function PostEditor() { + const { postId } = useParams<{ postId: string }>(); + const isEdit = Boolean(postId); + useDocumentTitle(isEdit ? "Edit Post" : "New Post"); + + const navigate = useNavigate(); + + // Form state + const [title, setTitle] = useState(""); + const [isPublished, setIsPublished] = useState(false); + const [existingAttachments, setExistingAttachments] = useState([]); + const [pendingFiles, setPendingFiles] = useState([]); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(isEdit); + const fileInputRef = useRef(null); + + // Tiptap editor + const editor = useEditor({ + immediatelyRender: false, + extensions: [ + StarterKit.configure({ + heading: { levels: [2, 3] }, + }), + Link.configure({ + openOnClick: false, + HTMLAttributes: { class: styles.editorLink }, + }), + Placeholder.configure({ + placeholder: "Write your post…", + }), + ], + content: "", + editorProps: { + attributes: { + class: styles.proseMirror ?? "", + }, + }, + }); + + // Load existing post in edit mode + useEffect(() => { + if (!isEdit || !postId) return; + let cancelled = false; + + (async () => { + try { + const post: PostRead = await getPost(postId); + if (cancelled) return; + setTitle(post.title); + setIsPublished(post.is_published); + setExistingAttachments(post.attachments); + editor?.commands.setContent(post.body_json); + } catch (err) { + if (!cancelled) { + setError(err instanceof ApiError ? err.detail : "Failed to load post"); + } + } finally { + if (!cancelled) setLoading(false); + } + })(); + + return () => { cancelled = true; }; + }, [postId, isEdit, editor]); + + // File handlers + const addFiles = useCallback((files: FileList | File[]) => { + setPendingFiles((prev) => [...prev, ...Array.from(files)]); + }, []); + + const removePendingFile = useCallback((idx: number) => { + setPendingFiles((prev) => prev.filter((_, i) => i !== idx)); + }, []); + + // Drag-and-drop + const [dragOver, setDragOver] = useState(false); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setDragOver(false); + if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files); + }, + [addFiles], + ); + + // Save handler + const handleSave = async () => { + if (!editor) return; + if (!title.trim()) { + setError("Title is required"); + return; + } + + setSaving(true); + setError(null); + + try { + const bodyJson = editor.getJSON(); + + let post: PostRead; + if (isEdit && postId) { + post = await updatePost(postId, { + title: title.trim(), + body_json: bodyJson, + is_published: isPublished, + }); + } else { + post = await createPost({ + title: title.trim(), + body_json: bodyJson, + is_published: isPublished, + }); + } + + // Upload any pending files + for (const file of pendingFiles) { + await uploadFile(post.id, file); + } + + navigate("/creator/dashboard"); + } catch (err) { + setError(err instanceof ApiError ? err.detail : "Failed to save post"); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+ +
+

Loading post…

+
+
+ ); + } + + return ( +
+ +
+
+

{isEdit ? "Edit Post" : "New Post"}

+
+ + {error && ( +
+ {error} +
+ )} + + {/* Title */} + setTitle(e.target.value)} + placeholder="Post title" + maxLength={500} + autoFocus + /> + + {/* Editor */} +
+ + +
+ + {/* File Attachments */} +
+

File Attachments

+ + {/* Existing attachments (edit mode) */} + {existingAttachments.length > 0 && ( +
    + {existingAttachments.map((att) => ( +
  • + 📎 + {att.filename} + {formatBytes(att.size_bytes)} +
  • + ))} +
+ )} + + {/* Pending files to upload */} + {pendingFiles.length > 0 && ( +
    + {pendingFiles.map((file, idx) => ( +
  • + 📄 + {file.name} + {formatBytes(file.size)} + +
  • + ))} +
+ )} + + {/* Drop zone */} +
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + role="button" + tabIndex={0} + onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); }} + > +

+ {dragOver ? "Drop files here" : "Drag & drop files or click to browse"} +

+

Presets, sample packs, project files, etc.

+
+ { if (e.target.files?.length) addFiles(e.target.files); e.target.value = ""; }} + /> +
+ + {/* Footer controls */} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 852419b..f55eed7 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/follows.ts","./src/api/highlights.ts","./src/api/index.ts","./src/api/posts.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ChatWidget.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PersonalityProfile.tsx","./src/components/PlayerControls.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorTiers.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/HighlightQueue.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/PostEditor.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file