feat: Built Tiptap rich text post editor with file attachments, multipa…

- "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
This commit is contained in:
jlightner 2026-04-04 09:13:48 +00:00
parent cc60852ac9
commit 9139d5a93a
12 changed files with 1855 additions and 6 deletions

View file

@ -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.

View file

@ -0,0 +1,9 @@
{
"schemaVersion": 1,
"taskId": "T02",
"unitId": "M023/S01/T02",
"timestamp": 1775293655825,
"passed": true,
"discoverySource": "none",
"checks": []
}

View file

@ -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.

View file

@ -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",

View file

@ -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",

View file

@ -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() {
<Route path="/creator/chapters/:videoId" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ChapterReview /></Suspense></ProtectedRoute>} />
<Route path="/creator/highlights" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><HighlightQueue /></Suspense></ProtectedRoute>} />
<Route path="/creator/tiers" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><CreatorTiers /></Suspense></ProtectedRoute>} />
<Route path="/creator/posts/new" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />
<Route path="/creator/posts/:postId/edit" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><PostEditor /></Suspense></ProtectedRoute>} />
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />

View file

@ -54,3 +54,49 @@ export async function request<T>(url: string, init?: RequestInit): Promise<T> {
return res.json() as Promise<T>;
}
/**
* Multipart form-data request does NOT set Content-Type (browser adds
* multipart boundary automatically). Still attaches auth token.
*/
export async function requestMultipart<T>(
url: string,
formData: FormData,
init?: RequestInit,
): Promise<T> {
const token = getStoredToken();
const headers: Record<string, string> = {
...(init?.headers as Record<string, string>),
};
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<T>;
}

96
frontend/src/api/posts.ts Normal file
View file

@ -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<string, unknown>;
is_published: boolean;
created_at: string;
updated_at: string;
attachments: PostAttachmentRead[];
}
export interface PostCreate {
title: string;
body_json: Record<string, unknown>;
is_published?: boolean;
}
export interface PostUpdate {
title?: string;
body_json?: Record<string, unknown>;
is_published?: boolean;
}
export interface PostListResponse {
items: PostRead[];
total: number;
}
// ── API Functions ───────────────────────────────────────────────────────────
export function createPost(data: PostCreate): Promise<PostRead> {
return request<PostRead>(`${BASE}/posts`, {
method: "POST",
body: JSON.stringify(data),
});
}
export function updatePost(id: string, data: PostUpdate): Promise<PostRead> {
return request<PostRead>(`${BASE}/posts/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
export function getPost(id: string): Promise<PostRead> {
return request<PostRead>(`${BASE}/posts/${id}`);
}
export function listPosts(
creatorId: string,
page = 1,
limit = 20,
): Promise<PostListResponse> {
return request<PostListResponse>(
`${BASE}/posts?creator_id=${creatorId}&page=${page}&limit=${limit}`,
);
}
export function uploadFile(
postId: string,
file: File,
): Promise<PostAttachmentRead> {
const form = new FormData();
form.append("file", file);
form.append("post_id", postId);
return requestMultipart<PostAttachmentRead>(`${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<void> {
return request<void>(`${BASE}/posts/${id}`, { method: "DELETE" });
}

View file

@ -59,6 +59,13 @@ function SidebarNav() {
</svg>
Tiers
</NavLink>
<NavLink to="/creator/posts/new" className={linkClass}>
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
</svg>
New Post
</NavLink>
</nav>
);
}

View file

@ -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;
}
}

View file

@ -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 (
<button
type="button"
className={`${styles.toolbarBtn}${active ? ` ${styles.toolbarBtnActive}` : ""}`}
onClick={onClick}
aria-label={label}
title={label}
>
{children}
</button>
);
}
// ── 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 (
<div className={styles.toolbar} role="toolbar" aria-label="Text formatting">
<ToolbarBtn label="Bold" active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()}>
<strong>B</strong>
</ToolbarBtn>
<ToolbarBtn label="Italic" active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()}>
<em>I</em>
</ToolbarBtn>
<span className={styles.toolbarSep} />
<ToolbarBtn label="Heading 2" active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
H2
</ToolbarBtn>
<ToolbarBtn label="Heading 3" active={editor.isActive("heading", { level: 3 })} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}>
H3
</ToolbarBtn>
<span className={styles.toolbarSep} />
<ToolbarBtn label="Bullet list" active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()}>
</ToolbarBtn>
<ToolbarBtn label="Ordered list" active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()}>
1.
</ToolbarBtn>
<span className={styles.toolbarSep} />
<ToolbarBtn label="Code block" active={editor.isActive("codeBlock")} onClick={() => editor.chain().focus().toggleCodeBlock().run()}>
{"</>"}
</ToolbarBtn>
<ToolbarBtn label="Link" active={editor.isActive("link")} onClick={setLink}>
🔗
</ToolbarBtn>
</div>
);
}
// ── 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<PostAttachmentRead[]>([]);
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(isEdit);
const fileInputRef = useRef<HTMLInputElement>(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 (
<div className={styles.layout}>
<SidebarNav />
<div className={styles.content}>
<p className={styles.loadingText}>Loading post</p>
</div>
</div>
);
}
return (
<div className={styles.layout}>
<SidebarNav />
<div className={styles.content}>
<div className={styles.header}>
<h1 className={styles.pageTitle}>{isEdit ? "Edit Post" : "New Post"}</h1>
</div>
{error && (
<div className={styles.errorBanner} role="alert">
{error}
</div>
)}
{/* Title */}
<input
type="text"
className={styles.titleInput}
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
maxLength={500}
autoFocus
/>
{/* Editor */}
<div className={styles.editorWrapper}>
<EditorToolbar editor={editor} />
<EditorContent editor={editor} className={styles.editorContent} />
</div>
{/* File Attachments */}
<div className={styles.attachmentsSection}>
<h3 className={styles.attachmentsTitle}>File Attachments</h3>
{/* Existing attachments (edit mode) */}
{existingAttachments.length > 0 && (
<ul className={styles.fileList}>
{existingAttachments.map((att) => (
<li key={att.id} className={styles.fileItem}>
<span className={styles.fileIcon}>📎</span>
<span className={styles.fileName}>{att.filename}</span>
<span className={styles.fileSize}>{formatBytes(att.size_bytes)}</span>
</li>
))}
</ul>
)}
{/* Pending files to upload */}
{pendingFiles.length > 0 && (
<ul className={styles.fileList}>
{pendingFiles.map((file, idx) => (
<li key={`pending-${idx}`} className={styles.fileItem}>
<span className={styles.fileIcon}>📄</span>
<span className={styles.fileName}>{file.name}</span>
<span className={styles.fileSize}>{formatBytes(file.size)}</span>
<button
type="button"
className={styles.fileRemoveBtn}
onClick={() => removePendingFile(idx)}
aria-label={`Remove ${file.name}`}
>
</button>
</li>
))}
</ul>
)}
{/* Drop zone */}
<div
className={`${styles.dropZone}${dragOver ? ` ${styles.dropZoneActive}` : ""}`}
onDragOver={(e) => { 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(); }}
>
<p className={styles.dropZoneText}>
{dragOver ? "Drop files here" : "Drag & drop files or click to browse"}
</p>
<p className={styles.dropZoneHint}>Presets, sample packs, project files, etc.</p>
</div>
<input
ref={fileInputRef}
type="file"
multiple
className={styles.hiddenFileInput}
onChange={(e) => { if (e.target.files?.length) addFiles(e.target.files); e.target.value = ""; }}
/>
</div>
{/* Footer controls */}
<div className={styles.footer}>
<label className={styles.publishToggle}>
<input
type="checkbox"
checked={isPublished}
onChange={(e) => setIsPublished(e.target.checked)}
/>
<span>Publish</span>
</label>
<div className={styles.footerActions}>
<button
type="button"
className={styles.cancelBtn}
onClick={() => navigate("/creator/dashboard")}
>
Cancel
</button>
<button
type="button"
className={styles.saveBtn}
onClick={handleSave}
disabled={saving}
>
{saving ? "Saving…" : isEdit ? "Update Post" : "Create Post"}
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -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"}
{"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"}