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:
parent
cc60852ac9
commit
9139d5a93a
12 changed files with 1855 additions and 6 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
9
.gsd/milestones/M023/slices/S01/tasks/T02-VERIFY.json
Normal file
9
.gsd/milestones/M023/slices/S01/tasks/T02-VERIFY.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T02",
|
||||
"unitId": "M023/S01/T02",
|
||||
"timestamp": 1775293655825,
|
||||
"passed": true,
|
||||
"discoverySource": "none",
|
||||
"checks": []
|
||||
}
|
||||
85
.gsd/milestones/M023/slices/S01/tasks/T03-SUMMARY.md
Normal file
85
.gsd/milestones/M023/slices/S01/tasks/T03-SUMMARY.md
Normal 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.
|
||||
838
frontend/package-lock.json
generated
838
frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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
96
frontend/src/api/posts.ts
Normal 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" });
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
392
frontend/src/pages/PostEditor.module.css
Normal file
392
frontend/src/pages/PostEditor.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
376
frontend/src/pages/PostEditor.tsx
Normal file
376
frontend/src/pages/PostEditor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"}
|
||||
Loading…
Add table
Reference in a new issue