@burdenoff/microfe-theculturepost 2026.704.1 → 2026.704.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/dist/theculturepost/components/AddCitationDialog.js.map +1 -1
  2. package/dist/theculturepost/components/CitationList.js.map +1 -1
  3. package/dist/theculturepost/components/CreateArchiveItemDialog.js.map +1 -1
  4. package/dist/theculturepost/components/CreateStoryDialog.js.map +1 -1
  5. package/dist/theculturepost/components/CreateTopicDialog.js.map +1 -1
  6. package/dist/theculturepost/components/FileUploadField.js.map +1 -1
  7. package/dist/theculturepost/components/RecordOralHistoryDialog.js.map +1 -1
  8. package/dist/theculturepost/components/ReplyThread.js +5 -1
  9. package/dist/theculturepost/components/ReplyThread.js.map +1 -1
  10. package/dist/theculturepost/components/StoryCard.js.map +1 -1
  11. package/dist/theculturepost/components/TopicCard.js.map +1 -1
  12. package/dist/theculturepost/components/kindIcon.js.map +1 -1
  13. package/dist/theculturepost/context/TheCulturePostContext.js.map +1 -1
  14. package/dist/theculturepost/pages/HomePage.js.map +1 -1
  15. package/dist/theculturepost/pages/archive/ArchivePage.js.map +1 -1
  16. package/dist/theculturepost/pages/archive/CollectionDetailPage.js.map +1 -1
  17. package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js +2 -1
  18. package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js.map +1 -1
  19. package/dist/theculturepost/pages/stories/StoriesPage.js.map +1 -1
  20. package/dist/theculturepost/pages/stories/StoryDetailPage.js +8 -4
  21. package/dist/theculturepost/pages/stories/StoryDetailPage.js.map +1 -1
  22. package/dist/theculturepost/pages/topics/TopicDetailPage.js.map +1 -1
  23. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AddCitationDialog.js","names":[],"sources":["../../../src/theculturepost/components/AddCitationDialog.tsx"],"sourcesContent":["/**\n * AddCitationDialog — attach a verifiable source to a story.\n * @module theculturepost/components\n *\n * Writes `tcpAddCitation`. An optional source scan is uploaded to\n * wspace-files-svc (only the id is stored).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpAddCitation, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpCitation, TcpSourceType } from '../types';\n\nconst SOURCE_TYPES: TcpSourceType[] = [\n 'WEB',\n 'BOOK',\n 'ACADEMIC',\n 'PERIODICAL',\n 'INTERVIEW',\n 'PRIMARY_DOCUMENT',\n 'ARCHIVAL_RECORD',\n 'ARTIFACT',\n];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface AddCitationDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n storyId: string;\n onCreated?: (citation: TcpCitation) => void;\n}\n\nexport function AddCitationDialog({ open, onOpenChange, storyId, onCreated }: AddCitationDialogProps) {\n const addCitation = useTcpAddCitation();\n const [title, setTitle] = useState('');\n const [author, setAuthor] = useState('');\n const [publisher, setPublisher] = useState('');\n const [publishedOn, setPublishedOn] = useState('');\n const [url, setUrl] = useState('');\n const [excerpt, setExcerpt] = useState('');\n const [provenance, setProvenance] = useState('');\n const [sourceType, setSourceType] = useState<TcpSourceType>('WEB');\n const [sourceFileId, setSourceFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setAuthor('');\n setPublisher('');\n setPublishedOn('');\n setUrl('');\n setExcerpt('');\n setProvenance('');\n setSourceType('WEB');\n setSourceFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A citation title is required.');\n return;\n }\n try {\n const created = await addCitation.mutateAsync({\n storyId,\n title: title.trim(),\n author: author.trim() || null,\n publisher: publisher.trim() || null,\n publishedOn: dateToIso(publishedOn),\n url: url.trim() || null,\n excerpt: excerpt.trim() || null,\n provenance: provenance.trim() || null,\n sourceType,\n sourceFileId,\n });\n notify.success('Citation added', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add a citation</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Ground this story in the record with a verifiable source.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-title\">Title</Label>\n <Input\n id=\"cite-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"Source title\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-author\">Author</Label>\n <Input\n id=\"cite-author\"\n value={author}\n onChange={(e) => setAuthor(e.target.value)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-publisher\">Publisher</Label>\n <Input\n id=\"cite-publisher\"\n value={publisher}\n onChange={(e) => setPublisher(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Source type</Label>\n <Select value={sourceType} onValueChange={(v) => setSourceType(v as TcpSourceType)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SOURCE_TYPES.map((s) => (\n <SelectItem key={s} value={s}>\n {formatEnumLabel(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-published\">Published on</Label>\n <Input\n id=\"cite-published\"\n type=\"date\"\n value={publishedOn}\n onChange={(e) => setPublishedOn(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-url\">URL</Label>\n <Input\n id=\"cite-url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n placeholder=\"https://…\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-excerpt\">Excerpt</Label>\n <Textarea\n id=\"cite-excerpt\"\n value={excerpt}\n onChange={(e) => setExcerpt(e.target.value)}\n placeholder=\"A short quoted passage\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-provenance\">Provenance</Label>\n <Textarea\n id=\"cite-provenance\"\n value={provenance}\n onChange={(e) => setProvenance(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Source scan</Label>\n <FileUploadField\n folder={FILE_FOLDERS.artifacts}\n accept=\"image/*,application/pdf\"\n label=\"Upload source scan\"\n hint=\"A scan of the primary source (optional).\"\n fileId={sourceFileId}\n onUploaded={setSourceFileId}\n onCleared={() => setSourceFileId(null)}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={addCitation.isPending}>\n Add citation\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AddCitationDialog.js","names":[],"sources":["../../../src/theculturepost/components/AddCitationDialog.tsx"],"sourcesContent":["/**\n * AddCitationDialog — attach a verifiable source to a story.\n * @module theculturepost/components\n *\n * Writes `tcpAddCitation`. An optional source scan is uploaded to\n * wspace-files-svc (only the id is stored).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpAddCitation, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpCitation, TcpSourceType } from '../types';\n\nconst SOURCE_TYPES: TcpSourceType[] = [\n 'WEB',\n 'BOOK',\n 'ACADEMIC',\n 'PERIODICAL',\n 'INTERVIEW',\n 'PRIMARY_DOCUMENT',\n 'ARCHIVAL_RECORD',\n 'ARTIFACT',\n];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface AddCitationDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n storyId: string;\n onCreated?: (citation: TcpCitation) => void;\n}\n\nexport function AddCitationDialog({\n open,\n onOpenChange,\n storyId,\n onCreated,\n}: AddCitationDialogProps) {\n const addCitation = useTcpAddCitation();\n const [title, setTitle] = useState('');\n const [author, setAuthor] = useState('');\n const [publisher, setPublisher] = useState('');\n const [publishedOn, setPublishedOn] = useState('');\n const [url, setUrl] = useState('');\n const [excerpt, setExcerpt] = useState('');\n const [provenance, setProvenance] = useState('');\n const [sourceType, setSourceType] = useState<TcpSourceType>('WEB');\n const [sourceFileId, setSourceFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setAuthor('');\n setPublisher('');\n setPublishedOn('');\n setUrl('');\n setExcerpt('');\n setProvenance('');\n setSourceType('WEB');\n setSourceFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A citation title is required.');\n return;\n }\n try {\n const created = await addCitation.mutateAsync({\n storyId,\n title: title.trim(),\n author: author.trim() || null,\n publisher: publisher.trim() || null,\n publishedOn: dateToIso(publishedOn),\n url: url.trim() || null,\n excerpt: excerpt.trim() || null,\n provenance: provenance.trim() || null,\n sourceType,\n sourceFileId,\n });\n notify.success('Citation added', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add a citation</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Ground this story in the record with a verifiable source.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-title\">Title</Label>\n <Input\n id=\"cite-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"Source title\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-author\">Author</Label>\n <Input id=\"cite-author\" value={author} onChange={(e) => setAuthor(e.target.value)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-publisher\">Publisher</Label>\n <Input\n id=\"cite-publisher\"\n value={publisher}\n onChange={(e) => setPublisher(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Source type</Label>\n <Select value={sourceType} onValueChange={(v) => setSourceType(v as TcpSourceType)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SOURCE_TYPES.map((s) => (\n <SelectItem key={s} value={s}>\n {formatEnumLabel(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-published\">Published on</Label>\n <Input\n id=\"cite-published\"\n type=\"date\"\n value={publishedOn}\n onChange={(e) => setPublishedOn(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-url\">URL</Label>\n <Input\n id=\"cite-url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n placeholder=\"https://…\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-excerpt\">Excerpt</Label>\n <Textarea\n id=\"cite-excerpt\"\n value={excerpt}\n onChange={(e) => setExcerpt(e.target.value)}\n placeholder=\"A short quoted passage\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cite-provenance\">Provenance</Label>\n <Textarea\n id=\"cite-provenance\"\n value={provenance}\n onChange={(e) => setProvenance(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Source scan</Label>\n <FileUploadField\n folder={FILE_FOLDERS.artifacts}\n accept=\"image/*,application/pdf\"\n label=\"Upload source scan\"\n hint=\"A scan of the primary source (optional).\"\n fileId={sourceFileId}\n onUploaded={setSourceFileId}\n onCleared={() => setSourceFileId(null)}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={addCitation.isPending}>\n Add citation\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":";;;;;;;;;;AAgCA,IAAM,IAAgC;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,KAAa,MAAiC;AAClD,KAAI,CAAC,EAAO,QAAO;CACnB,IAAM,IAAI,IAAI,KAAK,EAAM;AACzB,QAAO,OAAO,MAAM,EAAE,SAAS,CAAC,GAAG,OAAO,EAAE,aAAa;;AAU3D,SAAgB,EAAkB,EAChC,SACA,iBACA,YACA,gBACyB;CACzB,IAAM,IAAc,GAAmB,EACjC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAK,KAAU,EAAS,GAAG,EAC5B,CAAC,GAAS,KAAc,EAAS,GAAG,EACpC,CAAC,GAAY,KAAiB,EAAS,GAAG,EAC1C,CAAC,GAAY,KAAiB,EAAwB,MAAM,EAC5D,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAC/D,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,UAAc;AAUlB,EATA,EAAS,GAAG,EACZ,EAAU,GAAG,EACb,EAAa,GAAG,EAChB,EAAe,GAAG,EAClB,EAAO,GAAG,EACV,EAAW,GAAG,EACd,EAAc,GAAG,EACjB,EAAc,MAAM,EACpB,EAAgB,KAAK,EACrB,EAAS,KAAK;;AA8BhB,QACE,kBAAC,GAAD;EAAwB;EAAoB;YAC1C,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAuB,kBAAsC,CAAA,EAC7D,kBAAC,GAAD,EAAA,UAA6B,6DAEC,CAAA,CACP,EAAA,CAAA;IAEzB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAa;QAAa,CAAA,EACzC,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;QACzC,aAAY;QACZ,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAc;SAAc,CAAA,EAC3C,kBAAC,GAAD;SAAO,IAAG;SAAc,OAAO;SAAQ,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;SAAI,CAAA,CACjF;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAiB;SAAiB,CAAA,EACjD,kBAAC,GAAD;SACE,IAAG;SACH,OAAO;SACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;SAC7C,CAAA,CACE;UACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,eAAmB,CAAA,EAC1B,kBAAC,GAAD;SAAQ,OAAO;SAAY,gBAAgB,MAAM,EAAc,EAAmB;mBAAlF,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAe,CAAA,EACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAa,KAAK,MACjB,kBAAC,GAAD;UAAoB,OAAO;oBACxB,EAAgB,EAAE;UACR,EAFI,EAEJ,CACb,EACY,CAAA,CACT;WACL;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAiB;SAAoB,CAAA,EACpD,kBAAC,GAAD;SACE,IAAG;SACH,MAAK;SACL,OAAO;SACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;SAC/C,CAAA,CACE;UACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAW;QAAW,CAAA,EACrC,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAO,EAAE,OAAO,MAAM;QACvC,aAAY;QACZ,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAe;QAAe,CAAA,EAC7C,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAW,EAAE,OAAO,MAAM;QAC3C,aAAY;QACZ,MAAM;QACN,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAkB;QAAkB,CAAA,EACnD,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;QAC9C,MAAM;QACN,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,eAAmB,CAAA,EAC1B,kBAAC,GAAD;QACE,QAAQ,EAAa;QACrB,QAAO;QACP,OAAM;QACN,MAAK;QACL,QAAQ;QACR,YAAY;QACZ,iBAAiB,EAAgB,KAAK;QACtC,CAAA,CACE;;MACL,KAAS,kBAAC,KAAD;OAAG,WAAU;iBAAkC;OAAU,CAAA;MAC/D;;IAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,eAAe,EAAa,GAAM;eAAE;KAErD,CAAA,EACT,kBAAC,GAAD;KAAQ,SAtID,YAAY;AACzB,UAAI,CAAC,EAAM,MAAM,EAAE;AACjB,SAAS,gCAAgC;AACzC;;AAEF,UAAI;OACF,IAAM,IAAU,MAAM,EAAY,YAAY;QAC5C;QACA,OAAO,EAAM,MAAM;QACnB,QAAQ,EAAO,MAAM,IAAI;QACzB,WAAW,EAAU,MAAM,IAAI;QAC/B,aAAa,EAAU,EAAY;QACnC,KAAK,EAAI,MAAM,IAAI;QACnB,SAAS,EAAQ,MAAM,IAAI;QAC3B,YAAY,EAAW,MAAM,IAAI;QACjC;QACA;QACD,CAAC;AAIF,OAHA,EAAO,QAAQ,kBAAkB,EAAQ,MAAM,EAC/C,EAAa,GAAM,EACnB,GAAO,EACP,IAAY,EAAQ;eACb,GAAK;AACZ,SAAS,EAAc,EAAI,CAAC;;;KA+GC,UAAU,EAAY;eAAW;KAEjD,CAAA,CACc,EAAA,CAAA;IACD;;EACT,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CitationList.js","names":[],"sources":["../../../src/theculturepost/components/CitationList.tsx"],"sourcesContent":["/**\n * CitationList — the sources attached to a story, with verification controls.\n * @module theculturepost/components\n *\n * Reads `tcpListCitations(storyId)`; each entry can be moved through its\n * verification state (`tcpVerifyCitation`) — the archival integrity control at\n * the heart of a serious cultural publication.\n */\n\nimport {\n BookMarked,\n ExternalLink,\n Quote,\n ShieldCheck,\n} from 'lucide-react';\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { LoadingView, ErrorView } from './StateViews';\nimport { EmptyState } from './EmptyState';\nimport { useTcpCitations, useTcpVerifyCitation } from '../hooks';\nimport { formatDate, formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpCitation, TcpVerificationStatus } from '../types';\n\nconst VERIFICATIONS: TcpVerificationStatus[] = [\n 'UNVERIFIED',\n 'CORROBORATED',\n 'DISPUTED',\n 'RETRACTED',\n];\n\ninterface CitationListProps {\n storyId: string;\n}\n\nfunction CitationRow({ citation }: { citation: TcpCitation }) {\n const verify = useTcpVerifyCitation();\n\n const changeVerification = async (verification: TcpVerificationStatus) => {\n try {\n await verify.mutateAsync({ id: citation.id, verification });\n notify.success('Verification updated', formatEnumLabel(verification));\n } catch (err) {\n notify.error('Could not update verification', describeError(err));\n }\n };\n\n const meta = [citation.author, citation.publisher, formatDate(citation.publishedOn)]\n .filter((v) => v && v !== '—')\n .join(' · ');\n\n return (\n <li className=\"rounded-xl border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"mb-1.5 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={citation.sourceType} />\n <StatusBadge status={citation.verification} />\n </div>\n <p className=\"text-sm font-semibold text-text-primary\">{citation.title}</p>\n {meta && <p className=\"mt-0.5 text-xs text-text-tertiary\">{meta}</p>}\n </div>\n {citation.url && (\n <a\n href={citation.url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent hover:underline\"\n >\n Source <ExternalLink className=\"h-3 w-3\" />\n </a>\n )}\n </div>\n\n {citation.excerpt && (\n <blockquote className=\"mt-3 flex gap-2 rounded-lg bg-bg-sunken/60 px-3 py-2 text-sm italic text-text-secondary\">\n <Quote className=\"h-3.5 w-3.5 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n <span className=\"not-italic\">{citation.excerpt}</span>\n </blockquote>\n )}\n\n {citation.provenance && (\n <p className=\"mt-2 text-xs text-text-tertiary\">\n <span className=\"font-medium text-text-secondary\">Provenance:</span> {citation.provenance}\n </p>\n )}\n\n <div className=\"mt-3 flex items-center gap-2 border-t border-border-seam pt-3\">\n <ShieldCheck className=\"h-4 w-4 text-text-tertiary\" aria-hidden=\"true\" />\n <span className=\"text-xs text-text-secondary\">Verification</span>\n <Select\n value={citation.verification}\n onValueChange={(v) => changeVerification(v as TcpVerificationStatus)}\n >\n <SelectTrigger className=\"h-8 w-40\" aria-label=\"Change verification status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {VERIFICATIONS.map((v) => (\n <SelectItem key={v} value={v}>\n {formatEnumLabel(v)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {verify.isPending && <Spinner size=\"sm\" />}\n </div>\n </li>\n );\n}\n\nexport function CitationList({ storyId }: CitationListProps) {\n const { data: citations, isLoading, isError, refetch } = useTcpCitations(storyId);\n\n if (isLoading) return <LoadingView label=\"Loading citations…\" />;\n if (isError) return <ErrorView message=\"We couldn't load citations.\" onRetry={() => refetch()} />;\n\n if (!citations || citations.length === 0) {\n return (\n <EmptyState\n icon={<BookMarked className=\"h-9 w-9\" />}\n title=\"No citations yet\"\n description=\"Add verifiable sources to ground this story in the record.\"\n />\n );\n }\n\n return (\n <ul className=\"flex flex-col gap-3\">\n {citations.map((citation) => (\n <CitationRow key={citation.id} citation={citation} />\n ))}\n </ul>\n );\n}\n"],"mappings":";;;;;;;;;;;AA+BA,IAAM,IAAyC;CAC7C;CACA;CACA;CACA;CACD;AAMD,SAAS,EAAY,EAAE,eAAuC;CAC5D,IAAM,IAAS,GAAsB,EAE/B,IAAqB,OAAO,MAAwC;AACxE,MAAI;AAEF,GADA,MAAM,EAAO,YAAY;IAAE,IAAI,EAAS;IAAI;IAAc,CAAC,EAC3D,EAAO,QAAQ,wBAAwB,EAAgB,EAAa,CAAC;WAC9D,GAAK;AACZ,KAAO,MAAM,iCAAiC,EAAc,EAAI,CAAC;;IAI/D,IAAO;EAAC,EAAS;EAAQ,EAAS;EAAW,EAAW,EAAS,YAAY;EAAC,CACjF,QAAQ,MAAM,KAAK,MAAM,IAAI,CAC7B,KAAK,MAAM;AAEd,QACE,kBAAC,MAAD;EAAI,WAAU;YAAd;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAS,YAAc,CAAA,EAC5C,kBAAC,GAAD,EAAa,QAAQ,EAAS,cAAgB,CAAA,CAC1C;;MACN,kBAAC,KAAD;OAAG,WAAU;iBAA2C,EAAS;OAAU,CAAA;MAC1E,KAAQ,kBAAC,KAAD;OAAG,WAAU;iBAAqC;OAAS,CAAA;MAChE;QACL,EAAS,OACR,kBAAC,KAAD;KACE,MAAM,EAAS;KACf,QAAO;KACP,KAAI;KACJ,WAAU;eAJZ,CAKC,WACQ,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA,CACzC;OAEF;;GAEL,EAAS,WACR,kBAAC,cAAD;IAAY,WAAU;cAAtB,CACE,kBAAC,GAAD;KAAO,WAAU;KAA0C,eAAY;KAAS,CAAA,EAChF,kBAAC,QAAD;KAAM,WAAU;eAAc,EAAS;KAAe,CAAA,CAC3C;;GAGd,EAAS,cACR,kBAAC,KAAD;IAAG,WAAU;cAAb;KACE,kBAAC,QAAD;MAAM,WAAU;gBAAkC;MAAkB,CAAA;;KAAE,EAAS;KAC7E;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAa,WAAU;MAA6B,eAAY;MAAS,CAAA;KACzE,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAAmB,CAAA;KACjE,kBAAC,GAAD;MACE,OAAO,EAAS;MAChB,gBAAgB,MAAM,EAAmB,EAA2B;gBAFtE,CAIE,kBAAC,GAAD;OAAe,WAAU;OAAW,cAAW;iBAC7C,kBAAC,GAAD,EAAe,CAAA;OACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAc,KAAK,MAClB,kBAAC,GAAD;OAAoB,OAAO;iBACxB,EAAgB,EAAE;OACR,EAFI,EAEJ,CACb,EACY,CAAA,CACT;;KACR,EAAO,aAAa,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;KACtC;;GACH;;;AAIT,SAAgB,EAAa,EAAE,cAA8B;CAC3D,IAAM,EAAE,MAAM,GAAW,cAAW,YAAS,eAAY,EAAgB,EAAQ;AAejF,QAbI,IAAkB,kBAAC,GAAD,EAAa,OAAM,sBAAuB,CAAA,GAC5D,IAAgB,kBAAC,GAAD;EAAW,SAAQ;EAA8B,eAAe,GAAS;EAAI,CAAA,GAE7F,CAAC,KAAa,EAAU,WAAW,IAEnC,kBAAC,GAAD;EACE,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EACxC,OAAM;EACN,aAAY;EACZ,CAAA,GAKJ,kBAAC,MAAD;EAAI,WAAU;YACX,EAAU,KAAK,MACd,kBAAC,GAAD,EAAyC,aAAY,EAAnC,EAAS,GAA0B,CACrD;EACC,CAAA"}
1
+ {"version":3,"file":"CitationList.js","names":[],"sources":["../../../src/theculturepost/components/CitationList.tsx"],"sourcesContent":["/**\n * CitationList — the sources attached to a story, with verification controls.\n * @module theculturepost/components\n *\n * Reads `tcpListCitations(storyId)`; each entry can be moved through its\n * verification state (`tcpVerifyCitation`) — the archival integrity control at\n * the heart of a serious cultural publication.\n */\n\nimport { BookMarked, ExternalLink, Quote, ShieldCheck } from 'lucide-react';\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { LoadingView, ErrorView } from './StateViews';\nimport { EmptyState } from './EmptyState';\nimport { useTcpCitations, useTcpVerifyCitation } from '../hooks';\nimport { formatDate, formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpCitation, TcpVerificationStatus } from '../types';\n\nconst VERIFICATIONS: TcpVerificationStatus[] = [\n 'UNVERIFIED',\n 'CORROBORATED',\n 'DISPUTED',\n 'RETRACTED',\n];\n\ninterface CitationListProps {\n storyId: string;\n}\n\nfunction CitationRow({ citation }: { citation: TcpCitation }) {\n const verify = useTcpVerifyCitation();\n\n const changeVerification = async (verification: TcpVerificationStatus) => {\n try {\n await verify.mutateAsync({ id: citation.id, verification });\n notify.success('Verification updated', formatEnumLabel(verification));\n } catch (err) {\n notify.error('Could not update verification', describeError(err));\n }\n };\n\n const meta = [citation.author, citation.publisher, formatDate(citation.publishedOn)]\n .filter((v) => v && v !== '—')\n .join(' · ');\n\n return (\n <li className=\"rounded-xl border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"mb-1.5 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={citation.sourceType} />\n <StatusBadge status={citation.verification} />\n </div>\n <p className=\"text-sm font-semibold text-text-primary\">{citation.title}</p>\n {meta && <p className=\"mt-0.5 text-xs text-text-tertiary\">{meta}</p>}\n </div>\n {citation.url && (\n <a\n href={citation.url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent hover:underline\"\n >\n Source <ExternalLink className=\"h-3 w-3\" />\n </a>\n )}\n </div>\n\n {citation.excerpt && (\n <blockquote className=\"mt-3 flex gap-2 rounded-lg bg-bg-sunken/60 px-3 py-2 text-sm italic text-text-secondary\">\n <Quote className=\"h-3.5 w-3.5 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n <span className=\"not-italic\">{citation.excerpt}</span>\n </blockquote>\n )}\n\n {citation.provenance && (\n <p className=\"mt-2 text-xs text-text-tertiary\">\n <span className=\"font-medium text-text-secondary\">Provenance:</span> {citation.provenance}\n </p>\n )}\n\n <div className=\"mt-3 flex items-center gap-2 border-t border-border-seam pt-3\">\n <ShieldCheck className=\"h-4 w-4 text-text-tertiary\" aria-hidden=\"true\" />\n <span className=\"text-xs text-text-secondary\">Verification</span>\n <Select\n value={citation.verification}\n onValueChange={(v) => changeVerification(v as TcpVerificationStatus)}\n >\n <SelectTrigger className=\"h-8 w-40\" aria-label=\"Change verification status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {VERIFICATIONS.map((v) => (\n <SelectItem key={v} value={v}>\n {formatEnumLabel(v)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {verify.isPending && <Spinner size=\"sm\" />}\n </div>\n </li>\n );\n}\n\nexport function CitationList({ storyId }: CitationListProps) {\n const { data: citations, isLoading, isError, refetch } = useTcpCitations(storyId);\n\n if (isLoading) return <LoadingView label=\"Loading citations…\" />;\n if (isError) return <ErrorView message=\"We couldn't load citations.\" onRetry={() => refetch()} />;\n\n if (!citations || citations.length === 0) {\n return (\n <EmptyState\n icon={<BookMarked className=\"h-9 w-9\" />}\n title=\"No citations yet\"\n description=\"Add verifiable sources to ground this story in the record.\"\n />\n );\n }\n\n return (\n <ul className=\"flex flex-col gap-3\">\n {citations.map((citation) => (\n <CitationRow key={citation.id} citation={citation} />\n ))}\n </ul>\n );\n}\n"],"mappings":";;;;;;;;;;;AA0BA,IAAM,IAAyC;CAC7C;CACA;CACA;CACA;CACD;AAMD,SAAS,EAAY,EAAE,eAAuC;CAC5D,IAAM,IAAS,GAAsB,EAE/B,IAAqB,OAAO,MAAwC;AACxE,MAAI;AAEF,GADA,MAAM,EAAO,YAAY;IAAE,IAAI,EAAS;IAAI;IAAc,CAAC,EAC3D,EAAO,QAAQ,wBAAwB,EAAgB,EAAa,CAAC;WAC9D,GAAK;AACZ,KAAO,MAAM,iCAAiC,EAAc,EAAI,CAAC;;IAI/D,IAAO;EAAC,EAAS;EAAQ,EAAS;EAAW,EAAW,EAAS,YAAY;EAAC,CACjF,QAAQ,MAAM,KAAK,MAAM,IAAI,CAC7B,KAAK,MAAM;AAEd,QACE,kBAAC,MAAD;EAAI,WAAU;YAAd;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAS,YAAc,CAAA,EAC5C,kBAAC,GAAD,EAAa,QAAQ,EAAS,cAAgB,CAAA,CAC1C;;MACN,kBAAC,KAAD;OAAG,WAAU;iBAA2C,EAAS;OAAU,CAAA;MAC1E,KAAQ,kBAAC,KAAD;OAAG,WAAU;iBAAqC;OAAS,CAAA;MAChE;QACL,EAAS,OACR,kBAAC,KAAD;KACE,MAAM,EAAS;KACf,QAAO;KACP,KAAI;KACJ,WAAU;eAJZ,CAKC,WACQ,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA,CACzC;OAEF;;GAEL,EAAS,WACR,kBAAC,cAAD;IAAY,WAAU;cAAtB,CACE,kBAAC,GAAD;KAAO,WAAU;KAA0C,eAAY;KAAS,CAAA,EAChF,kBAAC,QAAD;KAAM,WAAU;eAAc,EAAS;KAAe,CAAA,CAC3C;;GAGd,EAAS,cACR,kBAAC,KAAD;IAAG,WAAU;cAAb;KACE,kBAAC,QAAD;MAAM,WAAU;gBAAkC;MAAkB,CAAA;;KAAE,EAAS;KAC7E;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAa,WAAU;MAA6B,eAAY;MAAS,CAAA;KACzE,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAAmB,CAAA;KACjE,kBAAC,GAAD;MACE,OAAO,EAAS;MAChB,gBAAgB,MAAM,EAAmB,EAA2B;gBAFtE,CAIE,kBAAC,GAAD;OAAe,WAAU;OAAW,cAAW;iBAC7C,kBAAC,GAAD,EAAe,CAAA;OACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAc,KAAK,MAClB,kBAAC,GAAD;OAAoB,OAAO;iBACxB,EAAgB,EAAE;OACR,EAFI,EAEJ,CACb,EACY,CAAA,CACT;;KACR,EAAO,aAAa,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;KACtC;;GACH;;;AAIT,SAAgB,EAAa,EAAE,cAA8B;CAC3D,IAAM,EAAE,MAAM,GAAW,cAAW,YAAS,eAAY,EAAgB,EAAQ;AAejF,QAbI,IAAkB,kBAAC,GAAD,EAAa,OAAM,sBAAuB,CAAA,GAC5D,IAAgB,kBAAC,GAAD;EAAW,SAAQ;EAA8B,eAAe,GAAS;EAAI,CAAA,GAE7F,CAAC,KAAa,EAAU,WAAW,IAEnC,kBAAC,GAAD;EACE,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EACxC,OAAM;EACN,aAAY;EACZ,CAAA,GAKJ,kBAAC,MAAD;EAAI,WAAU;YACX,EAAU,KAAK,MACd,kBAAC,GAAD,EAAyC,aAAY,EAAnC,EAAS,GAA0B,CACrD;EACC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreateArchiveItemDialog.js","names":[],"sources":["../../../src/theculturepost/components/CreateArchiveItemDialog.tsx"],"sourcesContent":["/**\n * CreateArchiveItemDialog — add an artifact to the archive.\n * @module theculturepost/components\n *\n * Writes `tcpCreateArchiveItem`. The binary is uploaded to wspace-files-svc and\n * only its id is stored here. Kind + access level are chosen up front.\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpCreateArchiveItem, useTcpCollections, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpArchiveItem, TcpArchiveItemKind, TcpAccessLevel } from '../types';\n\nconst KINDS: TcpArchiveItemKind[] = [\n 'DOCUMENT',\n 'MANUSCRIPT',\n 'PHOTOGRAPH',\n 'AUDIO',\n 'VIDEO',\n 'MAP',\n 'OBJECT',\n];\n\nconst ACCESS_LEVELS: TcpAccessLevel[] = ['PUBLIC', 'COMMUNITY', 'RESTRICTED', 'EMBARGOED'];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface CreateArchiveItemDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Pre-select a collection (e.g. from a collection detail page). */\n collectionId?: string;\n onCreated?: (item: TcpArchiveItem) => void;\n}\n\nexport function CreateArchiveItemDialog({\n open,\n onOpenChange,\n collectionId,\n onCreated,\n}: CreateArchiveItemDialogProps) {\n const createItem = useTcpCreateArchiveItem();\n const collectionsQuery = useTcpCollections({ limit: 100 });\n\n const [title, setTitle] = useState('');\n const [description, setDescription] = useState('');\n const [kind, setKind] = useState<TcpArchiveItemKind>('DOCUMENT');\n const [accessLevel, setAccessLevel] = useState<TcpAccessLevel>('PUBLIC');\n const [pickedCollection, setPickedCollection] = useState(collectionId ?? '');\n const [fileId, setFileId] = useState<string | null>(null);\n const [provenance, setProvenance] = useState('');\n const [origin, setOrigin] = useState('');\n const [languages, setLanguages] = useState<string[]>([]);\n const [tags, setTags] = useState<string[]>([]);\n const [datedAt, setDatedAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setDescription('');\n setKind('DOCUMENT');\n setAccessLevel('PUBLIC');\n setPickedCollection(collectionId ?? '');\n setFileId(null);\n setProvenance('');\n setOrigin('');\n setLanguages([]);\n setTags([]);\n setDatedAt('');\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n try {\n const created = await createItem.mutateAsync({\n title: title.trim(),\n description: description.trim() || null,\n kind,\n accessLevel,\n collectionId: pickedCollection || null,\n fileId,\n provenance: provenance.trim() || null,\n origin: origin.trim() || null,\n languages: languages.length ? languages : null,\n tags: tags.length ? tags : null,\n datedAt: dateToIso(datedAt),\n });\n notify.success('Artifact added', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add an artifact</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Catalogue a manuscript, photograph, recording, map, object or document with its\n provenance.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>File</Label>\n <FileUploadField\n folder={FILE_FOLDERS.artifacts}\n accept=\"image/*,audio/*,video/*,application/pdf\"\n label=\"Upload artifact file\"\n hint=\"Image, audio, video or PDF. Up to 25 MB.\"\n fileId={fileId}\n onUploaded={setFileId}\n onCleared={() => setFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-title\">Title</Label>\n <Input\n id=\"item-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Palm-leaf manuscript, 18th c.\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Kind</Label>\n <Select value={kind} onValueChange={(v) => setKind(v as TcpArchiveItemKind)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {KINDS.map((k) => (\n <SelectItem key={k} value={k}>\n {formatEnumLabel(k)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Access level</Label>\n <Select value={accessLevel} onValueChange={(v) => setAccessLevel(v as TcpAccessLevel)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ACCESS_LEVELS.map((a) => (\n <SelectItem key={a} value={a}>\n {formatEnumLabel(a)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n {!collectionId && (\n <div className=\"grid gap-1.5\">\n <Label>Collection</Label>\n <Select value={pickedCollection} onValueChange={setPickedCollection}>\n <SelectTrigger>\n <SelectValue placeholder=\"Add to a collection (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(collectionsQuery.data ?? []).map((collection) => (\n <SelectItem key={collection.id} value={collection.id}>\n {collection.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-desc\">Description</Label>\n <Textarea\n id=\"item-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-origin\">Origin</Label>\n <Input\n id=\"item-origin\"\n value={origin}\n onChange={(e) => setOrigin(e.target.value)}\n placeholder=\"Place of origin\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-dated\">Dated</Label>\n <Input\n id=\"item-dated\"\n type=\"date\"\n value={datedAt}\n onChange={(e) => setDatedAt(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-provenance\">Provenance</Label>\n <Textarea\n id=\"item-provenance\"\n value={provenance}\n onChange={(e) => setProvenance(e.target.value)}\n placeholder=\"Chain of custody / source history\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-langs\">Languages</Label>\n <TagInput\n id=\"item-langs\"\n value={languages}\n onChange={setLanguages}\n placeholder=\"e.g. Tamil, Sanskrit…\"\n preserveCase\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-tags\">Tags</Label>\n <TagInput id=\"item-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createItem.isPending}>\n Add artifact\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateArchiveItemDialog.js","names":[],"sources":["../../../src/theculturepost/components/CreateArchiveItemDialog.tsx"],"sourcesContent":["/**\n * CreateArchiveItemDialog — add an artifact to the archive.\n * @module theculturepost/components\n *\n * Writes `tcpCreateArchiveItem`. The binary is uploaded to wspace-files-svc and\n * only its id is stored here. Kind + access level are chosen up front.\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpCreateArchiveItem, useTcpCollections, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpArchiveItem, TcpArchiveItemKind, TcpAccessLevel } from '../types';\n\nconst KINDS: TcpArchiveItemKind[] = [\n 'DOCUMENT',\n 'MANUSCRIPT',\n 'PHOTOGRAPH',\n 'AUDIO',\n 'VIDEO',\n 'MAP',\n 'OBJECT',\n];\n\nconst ACCESS_LEVELS: TcpAccessLevel[] = ['PUBLIC', 'COMMUNITY', 'RESTRICTED', 'EMBARGOED'];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface CreateArchiveItemDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Pre-select a collection (e.g. from a collection detail page). */\n collectionId?: string;\n onCreated?: (item: TcpArchiveItem) => void;\n}\n\nexport function CreateArchiveItemDialog({\n open,\n onOpenChange,\n collectionId,\n onCreated,\n}: CreateArchiveItemDialogProps) {\n const createItem = useTcpCreateArchiveItem();\n const collectionsQuery = useTcpCollections({ limit: 100 });\n\n const [title, setTitle] = useState('');\n const [description, setDescription] = useState('');\n const [kind, setKind] = useState<TcpArchiveItemKind>('DOCUMENT');\n const [accessLevel, setAccessLevel] = useState<TcpAccessLevel>('PUBLIC');\n const [pickedCollection, setPickedCollection] = useState(collectionId ?? '');\n const [fileId, setFileId] = useState<string | null>(null);\n const [provenance, setProvenance] = useState('');\n const [origin, setOrigin] = useState('');\n const [languages, setLanguages] = useState<string[]>([]);\n const [tags, setTags] = useState<string[]>([]);\n const [datedAt, setDatedAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setDescription('');\n setKind('DOCUMENT');\n setAccessLevel('PUBLIC');\n setPickedCollection(collectionId ?? '');\n setFileId(null);\n setProvenance('');\n setOrigin('');\n setLanguages([]);\n setTags([]);\n setDatedAt('');\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n try {\n const created = await createItem.mutateAsync({\n title: title.trim(),\n description: description.trim() || null,\n kind,\n accessLevel,\n collectionId: pickedCollection || null,\n fileId,\n provenance: provenance.trim() || null,\n origin: origin.trim() || null,\n languages: languages.length ? languages : null,\n tags: tags.length ? tags : null,\n datedAt: dateToIso(datedAt),\n });\n notify.success('Artifact added', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add an artifact</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Catalogue a manuscript, photograph, recording, map, object or document with its\n provenance.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>File</Label>\n <FileUploadField\n folder={FILE_FOLDERS.artifacts}\n accept=\"image/*,audio/*,video/*,application/pdf\"\n label=\"Upload artifact file\"\n hint=\"Image, audio, video or PDF. Up to 25 MB.\"\n fileId={fileId}\n onUploaded={setFileId}\n onCleared={() => setFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-title\">Title</Label>\n <Input\n id=\"item-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Palm-leaf manuscript, 18th c.\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Kind</Label>\n <Select value={kind} onValueChange={(v) => setKind(v as TcpArchiveItemKind)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {KINDS.map((k) => (\n <SelectItem key={k} value={k}>\n {formatEnumLabel(k)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Access level</Label>\n <Select\n value={accessLevel}\n onValueChange={(v) => setAccessLevel(v as TcpAccessLevel)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ACCESS_LEVELS.map((a) => (\n <SelectItem key={a} value={a}>\n {formatEnumLabel(a)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n {!collectionId && (\n <div className=\"grid gap-1.5\">\n <Label>Collection</Label>\n <Select value={pickedCollection} onValueChange={setPickedCollection}>\n <SelectTrigger>\n <SelectValue placeholder=\"Add to a collection (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(collectionsQuery.data ?? []).map((collection) => (\n <SelectItem key={collection.id} value={collection.id}>\n {collection.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-desc\">Description</Label>\n <Textarea\n id=\"item-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-origin\">Origin</Label>\n <Input\n id=\"item-origin\"\n value={origin}\n onChange={(e) => setOrigin(e.target.value)}\n placeholder=\"Place of origin\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-dated\">Dated</Label>\n <Input\n id=\"item-dated\"\n type=\"date\"\n value={datedAt}\n onChange={(e) => setDatedAt(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-provenance\">Provenance</Label>\n <Textarea\n id=\"item-provenance\"\n value={provenance}\n onChange={(e) => setProvenance(e.target.value)}\n placeholder=\"Chain of custody / source history\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-langs\">Languages</Label>\n <TagInput\n id=\"item-langs\"\n value={languages}\n onChange={setLanguages}\n placeholder=\"e.g. Tamil, Sanskrit…\"\n preserveCase\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"item-tags\">Tags</Label>\n <TagInput id=\"item-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createItem.isPending}>\n Add artifact\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreateStoryDialog.js","names":[],"sources":["../../../src/theculturepost/components/CreateStoryDialog.tsx"],"sourcesContent":["/**\n * CreateStoryDialog — draft a new story.\n * @module theculturepost/components\n *\n * Writes `tcpCreateStory`. Slug auto-derives from the title (editable). New\n * stories begin at the start of the editorial lifecycle (IDEA/DRAFT on the svc).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpCreateStory, useTcpTopics, FILE_FOLDERS } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpStory, TcpStoryFormat } from '../types';\n\nconst FORMATS: TcpStoryFormat[] = [\n 'ARTICLE',\n 'ESSAY',\n 'EXPLAINER',\n 'ORAL_HISTORY',\n 'PHOTO_STORY',\n 'TIMELINE',\n];\n\ninterface CreateStoryDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Pre-select a topic (e.g. from a topic detail page). */\n topicId?: string;\n onCreated?: (story: TcpStory) => void;\n}\n\nexport function CreateStoryDialog({ open, onOpenChange, topicId, onCreated }: CreateStoryDialogProps) {\n const createStory = useTcpCreateStory();\n const topicsQuery = useTcpTopics({ pagination: { limit: 100 } });\n\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [summary, setSummary] = useState('');\n const [body, setBody] = useState('');\n const [format, setFormat] = useState<TcpStoryFormat>('ARTICLE');\n const [pickedTopic, setPickedTopic] = useState(topicId ?? '');\n const [languages, setLanguages] = useState<string[]>([]);\n const [tags, setTags] = useState<string[]>([]);\n const [readingMins, setReadingMins] = useState('');\n const [coverFileId, setCoverFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(title);\n\n const reset = () => {\n setTitle('');\n setSlug('');\n setSlugTouched(false);\n setSummary('');\n setBody('');\n setFormat('ARTICLE');\n setPickedTopic(topicId ?? '');\n setLanguages([]);\n setTags([]);\n setReadingMins('');\n setCoverFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createStory.mutateAsync({\n title: title.trim(),\n slug: effectiveSlug,\n summary: summary.trim() || null,\n body: body.trim() || null,\n format,\n topicId: pickedTopic || null,\n languages: languages.length ? languages : null,\n tags: tags.length ? tags : null,\n readingMins: readingMins ? Number(readingMins) : null,\n coverFileId,\n });\n notify.success('Story created', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Draft a story</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Begin a new piece — it enters the editorial lifecycle as a draft you can refine and\n publish later.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Cover image</Label>\n <FileUploadField\n folder={FILE_FOLDERS.covers}\n accept=\"image/*\"\n label=\"Upload cover\"\n hint=\"A hero image for the story. PNG or JPG, up to 25 MB.\"\n fileId={coverFileId}\n onUploaded={setCoverFileId}\n onCleared={() => setCoverFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-title\">Title</Label>\n <Input\n id=\"story-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. The Weavers of the Coromandel Coast\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-slug\">URL slug</Label>\n <Input\n id=\"story-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"the-weavers-of-the-coromandel-coast\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Format</Label>\n <Select value={format} onValueChange={(v) => setFormat(v as TcpStoryFormat)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {FORMATS.map((f) => (\n <SelectItem key={f} value={f}>\n {formatEnumLabel(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-reading\">Reading time (min)</Label>\n <Input\n id=\"story-reading\"\n type=\"number\"\n min={0}\n value={readingMins}\n onChange={(e) => setReadingMins(e.target.value)}\n placeholder=\"Optional\"\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Topic</Label>\n <Select value={pickedTopic} onValueChange={setPickedTopic}>\n <SelectTrigger>\n <SelectValue placeholder=\"Choose a topic (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(topicsQuery.data?.items ?? []).map((topic) => (\n <SelectItem key={topic.id} value={topic.id}>\n {topic.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-summary\">Summary</Label>\n <Textarea\n id=\"story-summary\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short standfirst that introduces the story.\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-body\">Body</Label>\n <Textarea\n id=\"story-body\"\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write the story… (line breaks are preserved)\"\n rows={5}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-langs\">Languages</Label>\n <TagInput\n id=\"story-langs\"\n value={languages}\n onChange={setLanguages}\n placeholder=\"e.g. English, Tamil…\"\n preserveCase\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-tags\">Tags</Label>\n <TagInput id=\"story-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createStory.isPending}>\n Create story\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,IAAM,IAA4B;CAChC;CACA;CACA;CACA;CACA;CACA;CACD;AAUD,SAAgB,EAAkB,EAAE,SAAM,iBAAc,YAAS,iBAAqC;CACpG,IAAM,IAAc,GAAmB,EACjC,IAAc,EAAa,EAAE,YAAY,EAAE,OAAO,KAAK,EAAE,CAAC,EAE1D,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAS,KAAc,EAAS,GAAG,EACpC,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAQ,KAAa,EAAyB,UAAU,EACzD,CAAC,GAAa,KAAkB,EAAS,KAAW,GAAG,EACvD,CAAC,GAAW,KAAgB,EAAmB,EAAE,CAAC,EAClD,CAAC,GAAM,KAAW,EAAmB,EAAE,CAAC,EACxC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAgB,IAAc,IAAO,EAAQ,EAAM,EAEnD,WAAc;AAYlB,EAXA,EAAS,GAAG,EACZ,EAAQ,GAAG,EACX,EAAe,GAAM,EACrB,EAAW,GAAG,EACd,EAAQ,GAAG,EACX,EAAU,UAAU,EACpB,EAAe,KAAW,GAAG,EAC7B,EAAa,EAAE,CAAC,EAChB,EAAQ,EAAE,CAAC,EACX,EAAe,GAAG,EAClB,EAAe,KAAK,EACpB,EAAS,KAAK;;AAkChB,QACE,kBAAC,IAAD;EAAwB;EAAoB;YAC1C,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,IAAD,EAAA,UAAuB,iBAAqC,CAAA,EAC5D,kBAAC,GAAD,EAAA,UAA6B,sGAGC,CAAA,CACP,EAAA,CAAA;IAEzB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,eAAmB,CAAA,EAC1B,kBAAC,GAAD;QACE,QAAQ,EAAa;QACrB,QAAO;QACP,OAAM;QACN,MAAK;QACL,QAAQ;QACR,YAAY;QACZ,iBAAiB,EAAe,KAAK;QACrC,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAc;QAAa,CAAA,EAC1C,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;QACzC,aAAY;QACZ,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAa;QAAgB,CAAA,EAC5C,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM;AAEf,SADA,EAAe,GAAK,EACpB,EAAQ,EAAQ,EAAE,OAAO,MAAM,CAAC;;QAElC,aAAY;QACZ,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,UAAc,CAAA,EACrB,kBAAC,GAAD;SAAQ,OAAO;SAAQ,gBAAgB,MAAM,EAAU,EAAoB;mBAA3E,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAe,CAAA,EACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,GAAD;UAAoB,OAAO;oBACxB,EAAgB,EAAE;UACR,EAFI,EAEJ,CACb,EACY,CAAA,CACT;WACL;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAgB;SAA0B,CAAA,EACzD,kBAAC,GAAD;SACE,IAAG;SACH,MAAK;SACL,KAAK;SACL,OAAO;SACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;SAC/C,aAAY;SACZ,CAAA,CACE;UACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,SAAa,CAAA,EACpB,kBAAC,GAAD;QAAQ,OAAO;QAAa,eAAe;kBAA3C,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAa,aAAY,6BAA8B,CAAA,EACzC,CAAA,EAChB,kBAAC,GAAD,EAAA,WACI,EAAY,MAAM,SAAS,EAAE,EAAE,KAAK,MACpC,kBAAC,GAAD;SAA2B,OAAO,EAAM;mBACrC,EAAM;SACI,EAFI,EAAM,GAEV,CACb,EACY,CAAA,CACT;UACL;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAgB;QAAe,CAAA,EAC9C,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAW,EAAE,OAAO,MAAM;QAC3C,aAAY;QACZ,MAAM;QACN,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAa;QAAY,CAAA,EACxC,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;QACxC,aAAY;QACZ,MAAM;QACN,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAc;QAAiB,CAAA,EAC9C,kBAAC,GAAD;QACE,IAAG;QACH,OAAO;QACP,UAAU;QACV,aAAY;QACZ,cAAA;QACA,CAAA,CACE;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAa;QAAY,CAAA,EACxC,kBAAC,GAAD;QAAU,IAAG;QAAa,OAAO;QAAM,UAAU;QAAS,aAAY;QAAc,CAAA,CAChF;;MACL,KAAS,kBAAC,KAAD;OAAG,WAAU;iBAAkC;OAAU,CAAA;MAC/D;;IAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,eAAe,EAAa,GAAM;eAAE;KAErD,CAAA,EACT,kBAAC,GAAD;KAAQ,SAhKD,YAAY;AACzB,UAAI,CAAC,EAAM,MAAM,EAAE;AACjB,SAAS,uBAAuB;AAChC;;AAEF,UAAI,CAAC,GAAe;AAClB,SAAS,0BAA0B;AACnC;;AAEF,UAAI;OACF,IAAM,IAAU,MAAM,EAAY,YAAY;QAC5C,OAAO,EAAM,MAAM;QACnB,MAAM;QACN,SAAS,EAAQ,MAAM,IAAI;QAC3B,MAAM,EAAK,MAAM,IAAI;QACrB;QACA,SAAS,KAAe;QACxB,WAAW,EAAU,SAAS,IAAY;QAC1C,MAAM,EAAK,SAAS,IAAO;QAC3B,aAAa,IAAc,OAAO,EAAY,GAAG;QACjD;QACD,CAAC;AAIF,OAHA,GAAO,QAAQ,iBAAiB,EAAQ,MAAM,EAC9C,EAAa,GAAM,EACnB,IAAO,EACP,KAAY,EAAQ;eACb,GAAK;AACZ,SAAS,GAAc,EAAI,CAAC;;;KAqIC,UAAU,EAAY;eAAW;KAEjD,CAAA,CACc,EAAA,CAAA;IACD;;EACT,CAAA"}
1
+ {"version":3,"file":"CreateStoryDialog.js","names":[],"sources":["../../../src/theculturepost/components/CreateStoryDialog.tsx"],"sourcesContent":["/**\n * CreateStoryDialog — draft a new story.\n * @module theculturepost/components\n *\n * Writes `tcpCreateStory`. Slug auto-derives from the title (editable). New\n * stories begin at the start of the editorial lifecycle (IDEA/DRAFT on the svc).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpCreateStory, useTcpTopics, FILE_FOLDERS } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpStory, TcpStoryFormat } from '../types';\n\nconst FORMATS: TcpStoryFormat[] = [\n 'ARTICLE',\n 'ESSAY',\n 'EXPLAINER',\n 'ORAL_HISTORY',\n 'PHOTO_STORY',\n 'TIMELINE',\n];\n\ninterface CreateStoryDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Pre-select a topic (e.g. from a topic detail page). */\n topicId?: string;\n onCreated?: (story: TcpStory) => void;\n}\n\nexport function CreateStoryDialog({\n open,\n onOpenChange,\n topicId,\n onCreated,\n}: CreateStoryDialogProps) {\n const createStory = useTcpCreateStory();\n const topicsQuery = useTcpTopics({ pagination: { limit: 100 } });\n\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [summary, setSummary] = useState('');\n const [body, setBody] = useState('');\n const [format, setFormat] = useState<TcpStoryFormat>('ARTICLE');\n const [pickedTopic, setPickedTopic] = useState(topicId ?? '');\n const [languages, setLanguages] = useState<string[]>([]);\n const [tags, setTags] = useState<string[]>([]);\n const [readingMins, setReadingMins] = useState('');\n const [coverFileId, setCoverFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(title);\n\n const reset = () => {\n setTitle('');\n setSlug('');\n setSlugTouched(false);\n setSummary('');\n setBody('');\n setFormat('ARTICLE');\n setPickedTopic(topicId ?? '');\n setLanguages([]);\n setTags([]);\n setReadingMins('');\n setCoverFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createStory.mutateAsync({\n title: title.trim(),\n slug: effectiveSlug,\n summary: summary.trim() || null,\n body: body.trim() || null,\n format,\n topicId: pickedTopic || null,\n languages: languages.length ? languages : null,\n tags: tags.length ? tags : null,\n readingMins: readingMins ? Number(readingMins) : null,\n coverFileId,\n });\n notify.success('Story created', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Draft a story</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Begin a new piece — it enters the editorial lifecycle as a draft you can refine and\n publish later.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Cover image</Label>\n <FileUploadField\n folder={FILE_FOLDERS.covers}\n accept=\"image/*\"\n label=\"Upload cover\"\n hint=\"A hero image for the story. PNG or JPG, up to 25 MB.\"\n fileId={coverFileId}\n onUploaded={setCoverFileId}\n onCleared={() => setCoverFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-title\">Title</Label>\n <Input\n id=\"story-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. The Weavers of the Coromandel Coast\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-slug\">URL slug</Label>\n <Input\n id=\"story-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"the-weavers-of-the-coromandel-coast\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Format</Label>\n <Select value={format} onValueChange={(v) => setFormat(v as TcpStoryFormat)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {FORMATS.map((f) => (\n <SelectItem key={f} value={f}>\n {formatEnumLabel(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-reading\">Reading time (min)</Label>\n <Input\n id=\"story-reading\"\n type=\"number\"\n min={0}\n value={readingMins}\n onChange={(e) => setReadingMins(e.target.value)}\n placeholder=\"Optional\"\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Topic</Label>\n <Select value={pickedTopic} onValueChange={setPickedTopic}>\n <SelectTrigger>\n <SelectValue placeholder=\"Choose a topic (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(topicsQuery.data?.items ?? []).map((topic) => (\n <SelectItem key={topic.id} value={topic.id}>\n {topic.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-summary\">Summary</Label>\n <Textarea\n id=\"story-summary\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short standfirst that introduces the story.\"\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-body\">Body</Label>\n <Textarea\n id=\"story-body\"\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write the story… (line breaks are preserved)\"\n rows={5}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-langs\">Languages</Label>\n <TagInput\n id=\"story-langs\"\n value={languages}\n onChange={setLanguages}\n placeholder=\"e.g. English, Tamil…\"\n preserveCase\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"story-tags\">Tags</Label>\n <TagInput id=\"story-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createStory.isPending}>\n Create story\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreateTopicDialog.js","names":[],"sources":["../../../src/theculturepost/components/CreateTopicDialog.tsx"],"sourcesContent":["/**\n * CreateTopicDialog — curate a new topic.\n * @module theculturepost/components\n *\n * Writes `tcpCreateTopic`. Slug auto-derives from the name (editable).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Switch,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useTcpCreateTopic } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpTopic, TcpTopicCategory } from '../types';\n\nconst CATEGORIES: TcpTopicCategory[] = [\n 'CULTURE_AND_IDENTITY',\n 'RELIGION_AND_FAITH',\n 'HISTORY',\n 'HERITAGE_AND_ARCHIVES',\n 'FESTIVALS_AND_RITUALS',\n 'MIGRATION_AND_DIASPORA',\n 'ORAL_HISTORIES',\n 'PHILOSOPHY_AND_THOUGHT',\n 'VALUES_AND_ETHICS',\n];\n\ninterface CreateTopicDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (topic: TcpTopic) => void;\n}\n\nexport function CreateTopicDialog({ open, onOpenChange, onCreated }: CreateTopicDialogProps) {\n const createTopic = useTcpCreateTopic();\n const [name, setName] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [tradition, setTradition] = useState('');\n const [description, setDescription] = useState('');\n const [category, setCategory] = useState<TcpTopicCategory>('CULTURE_AND_IDENTITY');\n const [isFeatured, setIsFeatured] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(name);\n\n const reset = () => {\n setName('');\n setSlug('');\n setSlugTouched(false);\n setTradition('');\n setDescription('');\n setCategory('CULTURE_AND_IDENTITY');\n setIsFeatured(false);\n setError(null);\n };\n\n const submit = async () => {\n if (!name.trim()) {\n setError('A topic name is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createTopic.mutateAsync({\n name: name.trim(),\n slug: effectiveSlug,\n tradition: tradition.trim() || null,\n description: description.trim() || null,\n category,\n isFeatured,\n });\n notify.success('Topic created', created.name);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Curate a topic</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Topics gather stories around a shared subject across culture, faith, history and\n heritage.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[60vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-name\">Name</Label>\n <Input\n id=\"topic-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Diwali & Festivals of Light\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-slug\">URL slug</Label>\n <Input\n id=\"topic-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"diwali-festivals-of-light\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as TcpTopicCategory)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CATEGORIES.map((c) => (\n <SelectItem key={c} value={c}>\n {formatEnumLabel(c)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-tradition\">Tradition</Label>\n <Input\n id=\"topic-tradition\"\n value={tradition}\n onChange={(e) => setTradition(e.target.value)}\n placeholder=\"e.g. Hindu, Tamil, Abrahamic (optional)\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-desc\">Description</Label>\n <Textarea\n id=\"topic-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"What does this topic explore?\"\n rows={3}\n />\n </div>\n <label className=\"flex items-center justify-between gap-3 rounded-xl border border-border-seam bg-bg-surface px-4 py-3\">\n <span>\n <span className=\"block text-sm font-medium text-text-primary\">Feature this topic</span>\n <span className=\"block text-xs text-text-secondary\">\n Featured topics are surfaced on the home page.\n </span>\n </span>\n <Switch checked={isFeatured} onCheckedChange={setIsFeatured} aria-label=\"Feature topic\" />\n </label>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createTopic.isPending}>\n Create topic\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateTopicDialog.js","names":[],"sources":["../../../src/theculturepost/components/CreateTopicDialog.tsx"],"sourcesContent":["/**\n * CreateTopicDialog — curate a new topic.\n * @module theculturepost/components\n *\n * Writes `tcpCreateTopic`. Slug auto-derives from the name (editable).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Switch,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useTcpCreateTopic } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpTopic, TcpTopicCategory } from '../types';\n\nconst CATEGORIES: TcpTopicCategory[] = [\n 'CULTURE_AND_IDENTITY',\n 'RELIGION_AND_FAITH',\n 'HISTORY',\n 'HERITAGE_AND_ARCHIVES',\n 'FESTIVALS_AND_RITUALS',\n 'MIGRATION_AND_DIASPORA',\n 'ORAL_HISTORIES',\n 'PHILOSOPHY_AND_THOUGHT',\n 'VALUES_AND_ETHICS',\n];\n\ninterface CreateTopicDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (topic: TcpTopic) => void;\n}\n\nexport function CreateTopicDialog({ open, onOpenChange, onCreated }: CreateTopicDialogProps) {\n const createTopic = useTcpCreateTopic();\n const [name, setName] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [tradition, setTradition] = useState('');\n const [description, setDescription] = useState('');\n const [category, setCategory] = useState<TcpTopicCategory>('CULTURE_AND_IDENTITY');\n const [isFeatured, setIsFeatured] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(name);\n\n const reset = () => {\n setName('');\n setSlug('');\n setSlugTouched(false);\n setTradition('');\n setDescription('');\n setCategory('CULTURE_AND_IDENTITY');\n setIsFeatured(false);\n setError(null);\n };\n\n const submit = async () => {\n if (!name.trim()) {\n setError('A topic name is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createTopic.mutateAsync({\n name: name.trim(),\n slug: effectiveSlug,\n tradition: tradition.trim() || null,\n description: description.trim() || null,\n category,\n isFeatured,\n });\n notify.success('Topic created', created.name);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Curate a topic</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Topics gather stories around a shared subject across culture, faith, history and\n heritage.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[60vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-name\">Name</Label>\n <Input\n id=\"topic-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Diwali & Festivals of Light\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-slug\">URL slug</Label>\n <Input\n id=\"topic-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"diwali-festivals-of-light\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as TcpTopicCategory)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CATEGORIES.map((c) => (\n <SelectItem key={c} value={c}>\n {formatEnumLabel(c)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-tradition\">Tradition</Label>\n <Input\n id=\"topic-tradition\"\n value={tradition}\n onChange={(e) => setTradition(e.target.value)}\n placeholder=\"e.g. Hindu, Tamil, Abrahamic (optional)\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"topic-desc\">Description</Label>\n <Textarea\n id=\"topic-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"What does this topic explore?\"\n rows={3}\n />\n </div>\n <label className=\"flex items-center justify-between gap-3 rounded-xl border border-border-seam bg-bg-surface px-4 py-3\">\n <span>\n <span className=\"block text-sm font-medium text-text-primary\">\n Feature this topic\n </span>\n <span className=\"block text-xs text-text-secondary\">\n Featured topics are surfaced on the home page.\n </span>\n </span>\n <Switch\n checked={isFeatured}\n onCheckedChange={setIsFeatured}\n aria-label=\"Feature topic\"\n />\n </label>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createTopic.isPending}>\n Create topic\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileUploadField.js","names":[],"sources":["../../../src/theculturepost/components/FileUploadField.tsx"],"sourcesContent":["/**\n * FileUploadField — upload a cover / artifact / recording / source scan to\n * wspace-files-svc and hand the resulting file id back to the caller.\n * @module theculturepost/components\n *\n * On select, uploads via {@link uploadImageToFiles} (a generic blob upload) and\n * returns the file id (persisted as coverFileId / fileId / recordingFileId /\n * sourceFileId). Image uploads show a thumbnail preview; other kinds show a\n * filename chip. `File` blobs live in wspace-files-svc — we only store ids here.\n */\n\nimport { useId, useRef, useState } from 'react';\nimport { UploadCloud, Loader2, FileCheck2, X } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { uploadImageToFiles } from '../hooks/useFiles';\nimport { notify, describeError } from '../lib/toast';\n\ninterface FileUploadFieldProps {\n /** Destination folder in wspace-files-svc (see FILE_FOLDERS). */\n folder: string;\n /** Called with the new file id after a successful upload. */\n onUploaded: (fileId: string) => void;\n /** Currently selected file id (for the cleared/preview state). */\n fileId?: string | null;\n /** HTML accept attribute (e.g. \"image/*\", \"audio/*,video/*\"). */\n accept?: string;\n label?: string;\n hint?: string;\n /** Max upload size in MB. */\n maxMb?: number;\n disabled?: boolean;\n onCleared?: () => void;\n}\n\nexport function FileUploadField({\n folder,\n onUploaded,\n fileId,\n accept = 'image/*',\n label = 'Upload file',\n hint,\n maxMb = 25,\n disabled = false,\n onCleared,\n}: FileUploadFieldProps) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [previewUrl, setPreviewUrl] = useState<string | null>(null);\n const [fileName, setFileName] = useState<string | null>(null);\n\n const maxBytes = maxMb * 1024 * 1024;\n\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = '';\n if (!file) return;\n if (file.size > maxBytes) {\n notify.error('File too large', `Please choose a file under ${maxMb} MB.`);\n return;\n }\n setUploading(true);\n try {\n const uploaded = await uploadImageToFiles(file, folder);\n setFileName(uploaded.name);\n if (file.type.startsWith('image/')) {\n setPreviewUrl(uploaded.url);\n } else {\n setPreviewUrl(null);\n }\n onUploaded(uploaded.id);\n notify.success('File uploaded', uploaded.name);\n } catch (err) {\n notify.error('Upload failed', describeError(err));\n } finally {\n setUploading(false);\n }\n };\n\n const clear = () => {\n setPreviewUrl(null);\n setFileName(null);\n onCleared?.();\n };\n\n const hasSelection = Boolean(fileId) || Boolean(fileName);\n\n return (\n <div className=\"flex flex-col gap-2\">\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept={accept}\n className=\"sr-only\"\n onChange={handleSelect}\n disabled={disabled || uploading}\n />\n {previewUrl ? (\n <div className=\"relative w-full overflow-hidden rounded-xl border border-border-seam\">\n <img src={previewUrl} alt={fileName ?? 'Preview'} className=\"max-h-48 w-full object-cover\" />\n {onCleared && (\n <button\n type=\"button\"\n onClick={clear}\n aria-label=\"Remove file\"\n className=\"absolute right-2 top-2 rounded-full bg-bg-canvas/80 p-1 text-text-primary hover:bg-bg-canvas\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n ) : hasSelection && fileName ? (\n <div className=\"flex items-center justify-between gap-2 rounded-xl border border-border-seam bg-bg-surface px-3 py-2 text-sm\">\n <span className=\"inline-flex min-w-0 items-center gap-2 text-text-primary\">\n <FileCheck2 className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n <span className=\"truncate\">{fileName}</span>\n </span>\n {onCleared && (\n <button\n type=\"button\"\n onClick={clear}\n aria-label=\"Remove file\"\n className=\"text-text-tertiary hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n ) : null}\n\n <button\n type=\"button\"\n onClick={() => inputRef.current?.click()}\n disabled={disabled || uploading}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-lg border border-dashed border-border-seam bg-bg-surface px-3 py-2 text-sm font-medium text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary',\n (disabled || uploading) && 'cursor-not-allowed opacity-60'\n )}\n >\n {uploading ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Uploading…\n </>\n ) : (\n <>\n <UploadCloud className=\"h-4 w-4\" /> {hasSelection ? 'Replace file' : label}\n </>\n )}\n </button>\n {hint && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAkCA,SAAgB,EAAgB,EAC9B,WACA,eACA,WACA,YAAS,WACT,WAAQ,eACR,SACA,WAAQ,IACR,cAAW,IACX,gBACuB;CACvB,IAAM,IAAW,EAAgC,KAAK,EAChD,IAAU,GAAO,EACjB,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAC3D,CAAC,GAAU,KAAe,EAAwB,KAAK,EAEvD,IAAW,IAAQ,OAAO,MAE1B,IAAe,OAAO,MAA+C;EACzE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAClC,QAAM,OAAO,QAAQ,IAChB,GACL;OAAI,EAAK,OAAO,GAAU;AACxB,MAAO,MAAM,kBAAkB,8BAA8B,EAAM,MAAM;AACzE;;AAEF,KAAa,GAAK;AAClB,OAAI;IACF,IAAM,IAAW,MAAM,EAAmB,GAAM,EAAO;AAQvD,IAPA,EAAY,EAAS,KAAK,EACtB,EAAK,KAAK,WAAW,SAAS,GAChC,EAAc,EAAS,IAAI,GAE3B,EAAc,KAAK,EAErB,EAAW,EAAS,GAAG,EACvB,EAAO,QAAQ,iBAAiB,EAAS,KAAK;YACvC,GAAK;AACZ,MAAO,MAAM,iBAAiB,EAAc,EAAI,CAAC;aACzC;AACR,MAAa,GAAM;;;IAIjB,UAAc;AAGlB,EAFA,EAAc,KAAK,EACnB,EAAY,KAAK,EACjB,KAAa;IAGT,IAAe,EAAQ,KAAW,EAAQ;AAEhD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,SAAD;IACE,KAAK;IACL,IAAI;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU;IACV,UAAU,KAAY;IACtB,CAAA;GACD,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,KAAK;KAAY,KAAK,KAAY;KAAW,WAAU;KAAiC,CAAA,EAC5F,KACC,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;KAClB,CAAA,CAEP;QACJ,KAAgB,IAClB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD,EAAY,WAAU,6CAA8C,CAAA,EACpE,kBAAC,QAAD;MAAM,WAAU;gBAAY;MAAgB,CAAA,CACvC;QACN,KACC,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;KAClB,CAAA,CAEP;QACJ;GAEJ,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,SAAS,OAAO;IACxC,UAAU,KAAY;IACtB,WAAW,EACT,kOACC,KAAY,MAAc,gCAC5B;cAEA,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAAA,cAC3C,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA;KACE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;;KAAE,IAAe,iBAAiB;KACpE,EAAA,CAAA;IAEE,CAAA;GACR,KAAQ,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAAS,CAAA;GACzD"}
1
+ {"version":3,"file":"FileUploadField.js","names":[],"sources":["../../../src/theculturepost/components/FileUploadField.tsx"],"sourcesContent":["/**\n * FileUploadField — upload a cover / artifact / recording / source scan to\n * wspace-files-svc and hand the resulting file id back to the caller.\n * @module theculturepost/components\n *\n * On select, uploads via {@link uploadImageToFiles} (a generic blob upload) and\n * returns the file id (persisted as coverFileId / fileId / recordingFileId /\n * sourceFileId). Image uploads show a thumbnail preview; other kinds show a\n * filename chip. `File` blobs live in wspace-files-svc — we only store ids here.\n */\n\nimport { useId, useRef, useState } from 'react';\nimport { UploadCloud, Loader2, FileCheck2, X } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { uploadImageToFiles } from '../hooks/useFiles';\nimport { notify, describeError } from '../lib/toast';\n\ninterface FileUploadFieldProps {\n /** Destination folder in wspace-files-svc (see FILE_FOLDERS). */\n folder: string;\n /** Called with the new file id after a successful upload. */\n onUploaded: (fileId: string) => void;\n /** Currently selected file id (for the cleared/preview state). */\n fileId?: string | null;\n /** HTML accept attribute (e.g. \"image/*\", \"audio/*,video/*\"). */\n accept?: string;\n label?: string;\n hint?: string;\n /** Max upload size in MB. */\n maxMb?: number;\n disabled?: boolean;\n onCleared?: () => void;\n}\n\nexport function FileUploadField({\n folder,\n onUploaded,\n fileId,\n accept = 'image/*',\n label = 'Upload file',\n hint,\n maxMb = 25,\n disabled = false,\n onCleared,\n}: FileUploadFieldProps) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [previewUrl, setPreviewUrl] = useState<string | null>(null);\n const [fileName, setFileName] = useState<string | null>(null);\n\n const maxBytes = maxMb * 1024 * 1024;\n\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = '';\n if (!file) return;\n if (file.size > maxBytes) {\n notify.error('File too large', `Please choose a file under ${maxMb} MB.`);\n return;\n }\n setUploading(true);\n try {\n const uploaded = await uploadImageToFiles(file, folder);\n setFileName(uploaded.name);\n if (file.type.startsWith('image/')) {\n setPreviewUrl(uploaded.url);\n } else {\n setPreviewUrl(null);\n }\n onUploaded(uploaded.id);\n notify.success('File uploaded', uploaded.name);\n } catch (err) {\n notify.error('Upload failed', describeError(err));\n } finally {\n setUploading(false);\n }\n };\n\n const clear = () => {\n setPreviewUrl(null);\n setFileName(null);\n onCleared?.();\n };\n\n const hasSelection = Boolean(fileId) || Boolean(fileName);\n\n return (\n <div className=\"flex flex-col gap-2\">\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept={accept}\n className=\"sr-only\"\n onChange={handleSelect}\n disabled={disabled || uploading}\n />\n {previewUrl ? (\n <div className=\"relative w-full overflow-hidden rounded-xl border border-border-seam\">\n <img\n src={previewUrl}\n alt={fileName ?? 'Preview'}\n className=\"max-h-48 w-full object-cover\"\n />\n {onCleared && (\n <button\n type=\"button\"\n onClick={clear}\n aria-label=\"Remove file\"\n className=\"absolute right-2 top-2 rounded-full bg-bg-canvas/80 p-1 text-text-primary hover:bg-bg-canvas\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n ) : hasSelection && fileName ? (\n <div className=\"flex items-center justify-between gap-2 rounded-xl border border-border-seam bg-bg-surface px-3 py-2 text-sm\">\n <span className=\"inline-flex min-w-0 items-center gap-2 text-text-primary\">\n <FileCheck2 className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n <span className=\"truncate\">{fileName}</span>\n </span>\n {onCleared && (\n <button\n type=\"button\"\n onClick={clear}\n aria-label=\"Remove file\"\n className=\"text-text-tertiary hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n ) : null}\n\n <button\n type=\"button\"\n onClick={() => inputRef.current?.click()}\n disabled={disabled || uploading}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-lg border border-dashed border-border-seam bg-bg-surface px-3 py-2 text-sm font-medium text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary',\n (disabled || uploading) && 'cursor-not-allowed opacity-60'\n )}\n >\n {uploading ? (\n <>\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Uploading…\n </>\n ) : (\n <>\n <UploadCloud className=\"h-4 w-4\" /> {hasSelection ? 'Replace file' : label}\n </>\n )}\n </button>\n {hint && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAkCA,SAAgB,EAAgB,EAC9B,WACA,eACA,WACA,YAAS,WACT,WAAQ,eACR,SACA,WAAQ,IACR,cAAW,IACX,gBACuB;CACvB,IAAM,IAAW,EAAgC,KAAK,EAChD,IAAU,GAAO,EACjB,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAC3D,CAAC,GAAU,KAAe,EAAwB,KAAK,EAEvD,IAAW,IAAQ,OAAO,MAE1B,IAAe,OAAO,MAA+C;EACzE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAClC,QAAM,OAAO,QAAQ,IAChB,GACL;OAAI,EAAK,OAAO,GAAU;AACxB,MAAO,MAAM,kBAAkB,8BAA8B,EAAM,MAAM;AACzE;;AAEF,KAAa,GAAK;AAClB,OAAI;IACF,IAAM,IAAW,MAAM,EAAmB,GAAM,EAAO;AAQvD,IAPA,EAAY,EAAS,KAAK,EACtB,EAAK,KAAK,WAAW,SAAS,GAChC,EAAc,EAAS,IAAI,GAE3B,EAAc,KAAK,EAErB,EAAW,EAAS,GAAG,EACvB,EAAO,QAAQ,iBAAiB,EAAS,KAAK;YACvC,GAAK;AACZ,MAAO,MAAM,iBAAiB,EAAc,EAAI,CAAC;aACzC;AACR,MAAa,GAAM;;;IAIjB,UAAc;AAGlB,EAFA,EAAc,KAAK,EACnB,EAAY,KAAK,EACjB,KAAa;IAGT,IAAe,EAAQ,KAAW,EAAQ;AAEhD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,SAAD;IACE,KAAK;IACL,IAAI;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU;IACV,UAAU,KAAY;IACtB,CAAA;GACD,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KACE,KAAK;KACL,KAAK,KAAY;KACjB,WAAU;KACV,CAAA,EACD,KACC,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;KAClB,CAAA,CAEP;QACJ,KAAgB,IAClB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD,EAAY,WAAU,6CAA8C,CAAA,EACpE,kBAAC,QAAD;MAAM,WAAU;gBAAY;MAAgB,CAAA,CACvC;QACN,KACC,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;KAClB,CAAA,CAEP;QACJ;GAEJ,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,SAAS,OAAO;IACxC,UAAU,KAAY;IACtB,WAAW,EACT,kOACC,KAAY,MAAc,gCAC5B;cAEA,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAAA,cAC3C,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA;KACE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;;KAAE,IAAe,iBAAiB;KACpE,EAAA,CAAA;IAEE,CAAA;GACR,KAAQ,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAAS,CAAA;GACzD"}
@@ -1 +1 @@
1
- {"version":3,"file":"RecordOralHistoryDialog.js","names":[],"sources":["../../../src/theculturepost/components/RecordOralHistoryDialog.tsx"],"sourcesContent":["/**\n * RecordOralHistoryDialog — capture an oral history recording.\n * @module theculturepost/components\n *\n * Writes `tcpRecordOralHistory`. The recording (audio/video) is uploaded to\n * wspace-files-svc; `narratorName` is a plain string stored directly.\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpRecordOralHistory, useTcpCollections, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpOralHistory, TcpAccessLevel } from '../types';\n\nconst ACCESS_LEVELS: TcpAccessLevel[] = ['COMMUNITY', 'PUBLIC', 'RESTRICTED', 'EMBARGOED'];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface RecordOralHistoryDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Pre-select a collection. */\n collectionId?: string;\n onCreated?: (oralHistory: TcpOralHistory) => void;\n}\n\nexport function RecordOralHistoryDialog({\n open,\n onOpenChange,\n collectionId,\n onCreated,\n}: RecordOralHistoryDialogProps) {\n const record = useTcpRecordOralHistory();\n const collectionsQuery = useTcpCollections({ limit: 100 });\n\n const [title, setTitle] = useState('');\n const [summary, setSummary] = useState('');\n const [transcript, setTranscript] = useState('');\n const [narratorName, setNarratorName] = useState('');\n const [language, setLanguage] = useState('');\n const [accessLevel, setAccessLevel] = useState<TcpAccessLevel>('COMMUNITY');\n const [pickedCollection, setPickedCollection] = useState(collectionId ?? '');\n const [recordingFileId, setRecordingFileId] = useState<string | null>(null);\n const [durationSecs, setDurationSecs] = useState('');\n const [recordedAt, setRecordedAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setSummary('');\n setTranscript('');\n setNarratorName('');\n setLanguage('');\n setAccessLevel('COMMUNITY');\n setPickedCollection(collectionId ?? '');\n setRecordingFileId(null);\n setDurationSecs('');\n setRecordedAt('');\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n try {\n const created = await record.mutateAsync({\n title: title.trim(),\n summary: summary.trim() || null,\n transcript: transcript.trim() || null,\n narratorName: narratorName.trim() || null,\n language: language.trim() || null,\n accessLevel,\n collectionId: pickedCollection || null,\n recordingFileId,\n durationSecs: durationSecs ? Number(durationSecs) : null,\n recordedAt: dateToIso(recordedAt),\n });\n notify.success('Oral history recorded', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Record an oral history</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Preserve a first-person account. Attach the recording and a transcript for the archive.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Recording</Label>\n <FileUploadField\n folder={FILE_FOLDERS.recordings}\n accept=\"audio/*,video/*\"\n label=\"Upload recording\"\n hint=\"Audio or video. Up to 25 MB.\"\n fileId={recordingFileId}\n onUploaded={setRecordingFileId}\n onCleared={() => setRecordingFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-title\">Title</Label>\n <Input\n id=\"oral-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. A weaver remembers the looms\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-narrator\">Narrator</Label>\n <Input\n id=\"oral-narrator\"\n value={narratorName}\n onChange={(e) => setNarratorName(e.target.value)}\n placeholder=\"Full name\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-language\">Language</Label>\n <Input\n id=\"oral-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"e.g. Tamil\"\n />\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Access level</Label>\n <Select value={accessLevel} onValueChange={(v) => setAccessLevel(v as TcpAccessLevel)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ACCESS_LEVELS.map((a) => (\n <SelectItem key={a} value={a}>\n {formatEnumLabel(a)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-duration\">Duration (seconds)</Label>\n <Input\n id=\"oral-duration\"\n type=\"number\"\n min={0}\n value={durationSecs}\n onChange={(e) => setDurationSecs(e.target.value)}\n placeholder=\"Optional\"\n />\n </div>\n </div>\n {!collectionId && (\n <div className=\"grid gap-1.5\">\n <Label>Collection</Label>\n <Select value={pickedCollection} onValueChange={setPickedCollection}>\n <SelectTrigger>\n <SelectValue placeholder=\"Add to a collection (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(collectionsQuery.data ?? []).map((collection) => (\n <SelectItem key={collection.id} value={collection.id}>\n {collection.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-recorded\">Recorded on</Label>\n <Input\n id=\"oral-recorded\"\n type=\"date\"\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-summary\">Summary</Label>\n <Textarea\n id=\"oral-summary\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-transcript\">Transcript</Label>\n <Textarea\n id=\"oral-transcript\"\n value={transcript}\n onChange={(e) => setTranscript(e.target.value)}\n placeholder=\"The transcribed account (line breaks are preserved).\"\n rows={4}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={record.isPending}>\n Record oral history\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RecordOralHistoryDialog.js","names":[],"sources":["../../../src/theculturepost/components/RecordOralHistoryDialog.tsx"],"sourcesContent":["/**\n * RecordOralHistoryDialog — capture an oral history recording.\n * @module theculturepost/components\n *\n * Writes `tcpRecordOralHistory`. The recording (audio/video) is uploaded to\n * wspace-files-svc; `narratorName` is a plain string stored directly.\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { FileUploadField } from './FileUploadField';\nimport { useTcpRecordOralHistory, useTcpCollections, FILE_FOLDERS } from '../hooks';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpOralHistory, TcpAccessLevel } from '../types';\n\nconst ACCESS_LEVELS: TcpAccessLevel[] = ['COMMUNITY', 'PUBLIC', 'RESTRICTED', 'EMBARGOED'];\n\nconst dateToIso = (value: string): string | null => {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n};\n\ninterface RecordOralHistoryDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Pre-select a collection. */\n collectionId?: string;\n onCreated?: (oralHistory: TcpOralHistory) => void;\n}\n\nexport function RecordOralHistoryDialog({\n open,\n onOpenChange,\n collectionId,\n onCreated,\n}: RecordOralHistoryDialogProps) {\n const record = useTcpRecordOralHistory();\n const collectionsQuery = useTcpCollections({ limit: 100 });\n\n const [title, setTitle] = useState('');\n const [summary, setSummary] = useState('');\n const [transcript, setTranscript] = useState('');\n const [narratorName, setNarratorName] = useState('');\n const [language, setLanguage] = useState('');\n const [accessLevel, setAccessLevel] = useState<TcpAccessLevel>('COMMUNITY');\n const [pickedCollection, setPickedCollection] = useState(collectionId ?? '');\n const [recordingFileId, setRecordingFileId] = useState<string | null>(null);\n const [durationSecs, setDurationSecs] = useState('');\n const [recordedAt, setRecordedAt] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setSummary('');\n setTranscript('');\n setNarratorName('');\n setLanguage('');\n setAccessLevel('COMMUNITY');\n setPickedCollection(collectionId ?? '');\n setRecordingFileId(null);\n setDurationSecs('');\n setRecordedAt('');\n setError(null);\n };\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n try {\n const created = await record.mutateAsync({\n title: title.trim(),\n summary: summary.trim() || null,\n transcript: transcript.trim() || null,\n narratorName: narratorName.trim() || null,\n language: language.trim() || null,\n accessLevel,\n collectionId: pickedCollection || null,\n recordingFileId,\n durationSecs: durationSecs ? Number(durationSecs) : null,\n recordedAt: dateToIso(recordedAt),\n });\n notify.success('Oral history recorded', created.title);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Record an oral history</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Preserve a first-person account. Attach the recording and a transcript for the archive.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Recording</Label>\n <FileUploadField\n folder={FILE_FOLDERS.recordings}\n accept=\"audio/*,video/*\"\n label=\"Upload recording\"\n hint=\"Audio or video. Up to 25 MB.\"\n fileId={recordingFileId}\n onUploaded={setRecordingFileId}\n onCleared={() => setRecordingFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-title\">Title</Label>\n <Input\n id=\"oral-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. A weaver remembers the looms\"\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-narrator\">Narrator</Label>\n <Input\n id=\"oral-narrator\"\n value={narratorName}\n onChange={(e) => setNarratorName(e.target.value)}\n placeholder=\"Full name\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-language\">Language</Label>\n <Input\n id=\"oral-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"e.g. Tamil\"\n />\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Access level</Label>\n <Select\n value={accessLevel}\n onValueChange={(v) => setAccessLevel(v as TcpAccessLevel)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ACCESS_LEVELS.map((a) => (\n <SelectItem key={a} value={a}>\n {formatEnumLabel(a)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-duration\">Duration (seconds)</Label>\n <Input\n id=\"oral-duration\"\n type=\"number\"\n min={0}\n value={durationSecs}\n onChange={(e) => setDurationSecs(e.target.value)}\n placeholder=\"Optional\"\n />\n </div>\n </div>\n {!collectionId && (\n <div className=\"grid gap-1.5\">\n <Label>Collection</Label>\n <Select value={pickedCollection} onValueChange={setPickedCollection}>\n <SelectTrigger>\n <SelectValue placeholder=\"Add to a collection (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(collectionsQuery.data ?? []).map((collection) => (\n <SelectItem key={collection.id} value={collection.id}>\n {collection.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-recorded\">Recorded on</Label>\n <Input\n id=\"oral-recorded\"\n type=\"date\"\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-summary\">Summary</Label>\n <Textarea\n id=\"oral-summary\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"oral-transcript\">Transcript</Label>\n <Textarea\n id=\"oral-transcript\"\n value={transcript}\n onChange={(e) => setTranscript(e.target.value)}\n placeholder=\"The transcribed account (line breaks are preserved).\"\n rows={4}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={record.isPending}>\n Record oral history\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -97,7 +97,11 @@ function k({ discussionId: n, isLocked: r }) {
97
97
  className: "flex flex-col items-center gap-2 rounded-xl border border-dashed border-border-seam py-8 text-center",
98
98
  children: [/* @__PURE__ */ g(f, { className: "h-8 w-8 text-text-tertiary" }), /* @__PURE__ */ _("p", {
99
99
  className: "text-sm text-text-secondary",
100
- children: ["No replies yet. ", r ? "This discussion is closed." : "Share the first perspective."]
100
+ children: [
101
+ "No replies yet.",
102
+ " ",
103
+ r ? "This discussion is closed." : "Share the first perspective."
104
+ ]
101
105
  })]
102
106
  }) : /* @__PURE__ */ g("ul", {
103
107
  className: "flex flex-col gap-3",
@@ -1 +1 @@
1
- {"version":3,"file":"ReplyThread.js","names":[],"sources":["../../../src/theculturepost/components/ReplyThread.tsx"],"sourcesContent":["/**\n * ReplyThread — moderated replies for a discussion.\n * @module theculturepost/components\n *\n * Reads `tcpListReplies(discussionId)`; writes `tcpPostReply` (with an optional\n * perspective label) and `tcpModerateReply` (approve / flag / remove) —\n * respectful-by-design controls. Author identity is id-only, so we render \"You\"\n * or \"Contributor\".\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, Send, ShieldAlert, Sparkle } from 'lucide-react';\nimport {\n Button,\n Input,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { AuthorLabel } from './AuthorLabel';\nimport { LoadingView, ErrorView } from './StateViews';\nimport { useTcpReplies, useTcpPostReply, useTcpModerateReply } from '../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpModerationStatus, TcpReply } from '../types';\n\nconst MODERATION_ACTIONS: TcpModerationStatus[] = ['APPROVED', 'FLAGGED', 'REMOVED'];\n\nfunction ReplyRow({ reply }: { reply: TcpReply }) {\n const moderate = useTcpModerateReply();\n\n const doModerate = async (status: TcpModerationStatus) => {\n try {\n await moderate.mutateAsync({ id: reply.id, status });\n notify.success('Reply moderated', formatEnumLabel(status));\n } catch (err) {\n notify.error('Could not moderate reply', describeError(err));\n }\n };\n\n const removed = reply.status === 'REMOVED';\n\n return (\n <li className=\"rounded-xl border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <AuthorLabel actorId={reply.authorId} className=\"text-sm font-medium text-text-primary\" />\n <span className=\"text-xs text-text-tertiary\">· {formatRelativeTime(reply.createdAt)}</span>\n {reply.perspective && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <Sparkle className=\"h-3 w-3\" /> {reply.perspective}\n </span>\n )}\n </div>\n <StatusBadge status={reply.status} />\n </div>\n\n <p\n className={\n removed\n ? 'mt-2 whitespace-pre-wrap text-sm italic text-text-tertiary'\n : 'mt-2 whitespace-pre-wrap break-words text-sm text-text-primary'\n }\n >\n {removed ? 'This reply has been removed by a moderator.' : reply.body}\n </p>\n\n <div className=\"mt-3 flex items-center gap-2 border-t border-border-seam pt-3\">\n <ShieldAlert className=\"h-4 w-4 text-text-tertiary\" aria-hidden=\"true\" />\n <span className=\"text-xs text-text-secondary\">Moderate</span>\n <Select value={reply.status} onValueChange={(v) => doModerate(v as TcpModerationStatus)}>\n <SelectTrigger className=\"h-8 w-36\" aria-label=\"Moderate reply\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {MODERATION_ACTIONS.map((s) => (\n <SelectItem key={s} value={s}>\n {formatEnumLabel(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {moderate.isPending && <Spinner size=\"sm\" />}\n </div>\n </li>\n );\n}\n\ninterface ReplyThreadProps {\n discussionId: string;\n isLocked: boolean;\n}\n\nexport function ReplyThread({ discussionId, isLocked }: ReplyThreadProps) {\n const { data: replies, isLoading, isError, refetch } = useTcpReplies(discussionId);\n const postReply = useTcpPostReply();\n const [body, setBody] = useState('');\n const [perspective, setPerspective] = useState('');\n\n const submit = async () => {\n const text = body.trim();\n if (!text) return;\n try {\n await postReply.mutateAsync({\n discussionId,\n body: text,\n perspective: perspective.trim() || null,\n });\n setBody('');\n setPerspective('');\n notify.success('Reply posted');\n } catch (err) {\n notify.error('Could not post reply', describeError(err));\n }\n };\n\n return (\n <div className=\"flex flex-col gap-4\">\n {isLoading ? (\n <LoadingView label=\"Loading replies…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load replies.\" onRetry={() => refetch()} />\n ) : !replies || replies.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 rounded-xl border border-dashed border-border-seam py-8 text-center\">\n <MessageSquare className=\"h-8 w-8 text-text-tertiary\" />\n <p className=\"text-sm text-text-secondary\">\n No replies yet. {isLocked ? 'This discussion is closed.' : 'Share the first perspective.'}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-3\">\n {replies.map((reply) => (\n <ReplyRow key={reply.id} reply={reply} />\n ))}\n </ul>\n )}\n\n {isLocked ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken/60 px-4 py-3 text-center text-sm text-text-secondary\">\n This discussion is locked. New replies are closed.\n </div>\n ) : (\n <div className=\"flex flex-col gap-2 rounded-xl border border-border-seam bg-bg-surface p-4\">\n <label htmlFor=\"reply-body\" className=\"text-sm font-medium text-text-primary\">\n Add your reply\n </label>\n <Textarea\n id=\"reply-body\"\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Contribute a thoughtful, respectful perspective…\"\n rows={3}\n />\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <Input\n value={perspective}\n onChange={(e) => setPerspective(e.target.value)}\n placeholder=\"Perspective (optional) — e.g. Historian, Elder\"\n className=\"sm:max-w-xs\"\n aria-label=\"Perspective\"\n />\n <Button\n onClick={submit}\n disabled={!body.trim() || postReply.isPending}\n className=\"sm:ml-auto\"\n >\n <Send className=\"mr-1.5 h-4 w-4\" /> Post reply\n </Button>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ReplyThread.js","names":[],"sources":["../../../src/theculturepost/components/ReplyThread.tsx"],"sourcesContent":["/**\n * ReplyThread — moderated replies for a discussion.\n * @module theculturepost/components\n *\n * Reads `tcpListReplies(discussionId)`; writes `tcpPostReply` (with an optional\n * perspective label) and `tcpModerateReply` (approve / flag / remove) —\n * respectful-by-design controls. Author identity is id-only, so we render \"You\"\n * or \"Contributor\".\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, Send, ShieldAlert, Sparkle } from 'lucide-react';\nimport {\n Button,\n Input,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { AuthorLabel } from './AuthorLabel';\nimport { LoadingView, ErrorView } from './StateViews';\nimport { useTcpReplies, useTcpPostReply, useTcpModerateReply } from '../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { TcpModerationStatus, TcpReply } from '../types';\n\nconst MODERATION_ACTIONS: TcpModerationStatus[] = ['APPROVED', 'FLAGGED', 'REMOVED'];\n\nfunction ReplyRow({ reply }: { reply: TcpReply }) {\n const moderate = useTcpModerateReply();\n\n const doModerate = async (status: TcpModerationStatus) => {\n try {\n await moderate.mutateAsync({ id: reply.id, status });\n notify.success('Reply moderated', formatEnumLabel(status));\n } catch (err) {\n notify.error('Could not moderate reply', describeError(err));\n }\n };\n\n const removed = reply.status === 'REMOVED';\n\n return (\n <li className=\"rounded-xl border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <AuthorLabel actorId={reply.authorId} className=\"text-sm font-medium text-text-primary\" />\n <span className=\"text-xs text-text-tertiary\">\n · {formatRelativeTime(reply.createdAt)}\n </span>\n {reply.perspective && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <Sparkle className=\"h-3 w-3\" /> {reply.perspective}\n </span>\n )}\n </div>\n <StatusBadge status={reply.status} />\n </div>\n\n <p\n className={\n removed\n ? 'mt-2 whitespace-pre-wrap text-sm italic text-text-tertiary'\n : 'mt-2 whitespace-pre-wrap break-words text-sm text-text-primary'\n }\n >\n {removed ? 'This reply has been removed by a moderator.' : reply.body}\n </p>\n\n <div className=\"mt-3 flex items-center gap-2 border-t border-border-seam pt-3\">\n <ShieldAlert className=\"h-4 w-4 text-text-tertiary\" aria-hidden=\"true\" />\n <span className=\"text-xs text-text-secondary\">Moderate</span>\n <Select value={reply.status} onValueChange={(v) => doModerate(v as TcpModerationStatus)}>\n <SelectTrigger className=\"h-8 w-36\" aria-label=\"Moderate reply\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {MODERATION_ACTIONS.map((s) => (\n <SelectItem key={s} value={s}>\n {formatEnumLabel(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {moderate.isPending && <Spinner size=\"sm\" />}\n </div>\n </li>\n );\n}\n\ninterface ReplyThreadProps {\n discussionId: string;\n isLocked: boolean;\n}\n\nexport function ReplyThread({ discussionId, isLocked }: ReplyThreadProps) {\n const { data: replies, isLoading, isError, refetch } = useTcpReplies(discussionId);\n const postReply = useTcpPostReply();\n const [body, setBody] = useState('');\n const [perspective, setPerspective] = useState('');\n\n const submit = async () => {\n const text = body.trim();\n if (!text) return;\n try {\n await postReply.mutateAsync({\n discussionId,\n body: text,\n perspective: perspective.trim() || null,\n });\n setBody('');\n setPerspective('');\n notify.success('Reply posted');\n } catch (err) {\n notify.error('Could not post reply', describeError(err));\n }\n };\n\n return (\n <div className=\"flex flex-col gap-4\">\n {isLoading ? (\n <LoadingView label=\"Loading replies…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load replies.\" onRetry={() => refetch()} />\n ) : !replies || replies.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 rounded-xl border border-dashed border-border-seam py-8 text-center\">\n <MessageSquare className=\"h-8 w-8 text-text-tertiary\" />\n <p className=\"text-sm text-text-secondary\">\n No replies yet.{' '}\n {isLocked ? 'This discussion is closed.' : 'Share the first perspective.'}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-3\">\n {replies.map((reply) => (\n <ReplyRow key={reply.id} reply={reply} />\n ))}\n </ul>\n )}\n\n {isLocked ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken/60 px-4 py-3 text-center text-sm text-text-secondary\">\n This discussion is locked. New replies are closed.\n </div>\n ) : (\n <div className=\"flex flex-col gap-2 rounded-xl border border-border-seam bg-bg-surface p-4\">\n <label htmlFor=\"reply-body\" className=\"text-sm font-medium text-text-primary\">\n Add your reply\n </label>\n <Textarea\n id=\"reply-body\"\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Contribute a thoughtful, respectful perspective…\"\n rows={3}\n />\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <Input\n value={perspective}\n onChange={(e) => setPerspective(e.target.value)}\n placeholder=\"Perspective (optional) — e.g. Historian, Elder\"\n className=\"sm:max-w-xs\"\n aria-label=\"Perspective\"\n />\n <Button\n onClick={submit}\n disabled={!body.trim() || postReply.isPending}\n className=\"sm:ml-auto\"\n >\n <Send className=\"mr-1.5 h-4 w-4\" /> Post reply\n </Button>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StoryCard.js","names":[],"sources":["../../../src/theculturepost/components/StoryCard.tsx"],"sourcesContent":["/**\n * StoryCard — an editorial tile for a story.\n * @module theculturepost/components\n *\n * Cover image (via file id), title, summary, format + status badges, topic,\n * reading time, languages. Links to the story detail page.\n */\n\nimport { BookOpen, Clock, Languages, Tag } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { CoverImage } from './CoverImage';\nimport { StatusBadge } from './StatusBadge';\nimport { AuthorLabel } from './AuthorLabel';\nimport { formatEnumLabel, formatNumber, formatRelativeTime } from '../utils/formatters';\nimport type { TcpStory } from '../types';\n\ninterface StoryCardProps {\n story: TcpStory;\n onOpen: () => void;\n /** Hide the status badge (e.g. on published-only surfaces). */\n hideStatus?: boolean;\n}\n\nexport function StoryCard({ story, onOpen, hideStatus = false }: StoryCardProps) {\n const dateLabel = story.publishedAt ?? story.updatedAt;\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col overflow-hidden\">\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"group relative block aspect-[16/9] w-full overflow-hidden text-left\"\n aria-label={`Open ${story.title}`}\n >\n <CoverImage\n fileId={story.cover?.id}\n alt={story.title}\n rounded=\"rounded-none\"\n className=\"h-full w-full transition-transform duration-500 group-hover:scale-[1.03]\"\n icon={<BookOpen className=\"h-8 w-8\" />}\n />\n </button>\n\n <div className=\"flex flex-1 flex-col p-5\">\n <div className=\"mb-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={story.format} />\n {!hideStatus && <StatusBadge status={story.status} />}\n {story.topic && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <Tag className=\"h-3 w-3\" /> {story.topic.name}\n </span>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"text-left text-base font-semibold leading-snug text-text-primary hover:underline\"\n >\n {story.title}\n </button>\n\n {story.summary && (\n <p className=\"mt-1.5 line-clamp-2 text-sm text-text-secondary\">{story.summary}</p>\n )}\n\n <div className=\"mt-auto flex flex-wrap items-center gap-x-3 gap-y-1 pt-4 text-xs text-text-tertiary\">\n <AuthorLabel actorId={story.authorId} prefix=\"By\" />\n {story.readingMins > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatNumber(story.readingMins)} min read\n </span>\n )}\n {story.languages.length > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Languages className=\"h-3.5 w-3.5\" /> {story.languages.map(formatEnumLabel).join(', ')}\n </span>\n )}\n <span>{formatRelativeTime(dateLabel)}</span>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;AAuBA,SAAgB,EAAU,EAAE,UAAO,WAAQ,gBAAa,MAAyB;CAC/E,IAAM,IAAY,EAAM,eAAe,EAAM;AAE7C,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC,CACE,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;GACV,cAAY,QAAQ,EAAM;aAE1B,kBAAC,GAAD;IACE,QAAQ,EAAM,OAAO;IACrB,KAAK,EAAM;IACX,SAAQ;IACR,WAAU;IACV,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;IACtC,CAAA;GACK,CAAA,EAET,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAa,QAAQ,EAAM,QAAU,CAAA;MACpC,CAAC,KAAc,kBAAC,GAAD,EAAa,QAAQ,EAAM,QAAU,CAAA;MACpD,EAAM,SACL,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;;QAAE,EAAM,MAAM;QACpC;;MAEL;;IAEN,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAET,EAAM;KACA,CAAA;IAER,EAAM,WACL,kBAAC,KAAD;KAAG,WAAU;eAAmD,EAAM;KAAY,CAAA;IAGpF,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OAAa,SAAS,EAAM;OAAU,QAAO;OAAO,CAAA;MACnD,EAAM,cAAc,KACnB,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;QAAE,EAAa,EAAM,YAAY;QAAC;QAC9D;;MAER,EAAM,UAAU,SAAS,KACxB,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA;;QAAE,EAAM,UAAU,IAAI,EAAgB,CAAC,KAAK,KAAK;QACjF;;MAET,kBAAC,QAAD,EAAA,UAAO,EAAmB,EAAU,EAAQ,CAAA;MACxC;;IACF;KACI"}
1
+ {"version":3,"file":"StoryCard.js","names":[],"sources":["../../../src/theculturepost/components/StoryCard.tsx"],"sourcesContent":["/**\n * StoryCard — an editorial tile for a story.\n * @module theculturepost/components\n *\n * Cover image (via file id), title, summary, format + status badges, topic,\n * reading time, languages. Links to the story detail page.\n */\n\nimport { BookOpen, Clock, Languages, Tag } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { CoverImage } from './CoverImage';\nimport { StatusBadge } from './StatusBadge';\nimport { AuthorLabel } from './AuthorLabel';\nimport { formatEnumLabel, formatNumber, formatRelativeTime } from '../utils/formatters';\nimport type { TcpStory } from '../types';\n\ninterface StoryCardProps {\n story: TcpStory;\n onOpen: () => void;\n /** Hide the status badge (e.g. on published-only surfaces). */\n hideStatus?: boolean;\n}\n\nexport function StoryCard({ story, onOpen, hideStatus = false }: StoryCardProps) {\n const dateLabel = story.publishedAt ?? story.updatedAt;\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col overflow-hidden\">\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"group relative block aspect-[16/9] w-full overflow-hidden text-left\"\n aria-label={`Open ${story.title}`}\n >\n <CoverImage\n fileId={story.cover?.id}\n alt={story.title}\n rounded=\"rounded-none\"\n className=\"h-full w-full transition-transform duration-500 group-hover:scale-[1.03]\"\n icon={<BookOpen className=\"h-8 w-8\" />}\n />\n </button>\n\n <div className=\"flex flex-1 flex-col p-5\">\n <div className=\"mb-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={story.format} />\n {!hideStatus && <StatusBadge status={story.status} />}\n {story.topic && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <Tag className=\"h-3 w-3\" /> {story.topic.name}\n </span>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"text-left text-base font-semibold leading-snug text-text-primary hover:underline\"\n >\n {story.title}\n </button>\n\n {story.summary && (\n <p className=\"mt-1.5 line-clamp-2 text-sm text-text-secondary\">{story.summary}</p>\n )}\n\n <div className=\"mt-auto flex flex-wrap items-center gap-x-3 gap-y-1 pt-4 text-xs text-text-tertiary\">\n <AuthorLabel actorId={story.authorId} prefix=\"By\" />\n {story.readingMins > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatNumber(story.readingMins)} min read\n </span>\n )}\n {story.languages.length > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Languages className=\"h-3.5 w-3.5\" />{' '}\n {story.languages.map(formatEnumLabel).join(', ')}\n </span>\n )}\n <span>{formatRelativeTime(dateLabel)}</span>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;AAuBA,SAAgB,EAAU,EAAE,UAAO,WAAQ,gBAAa,MAAyB;CAC/E,IAAM,IAAY,EAAM,eAAe,EAAM;AAE7C,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC,CACE,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;GACV,cAAY,QAAQ,EAAM;aAE1B,kBAAC,GAAD;IACE,QAAQ,EAAM,OAAO;IACrB,KAAK,EAAM;IACX,SAAQ;IACR,WAAU;IACV,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;IACtC,CAAA;GACK,CAAA,EAET,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAa,QAAQ,EAAM,QAAU,CAAA;MACpC,CAAC,KAAc,kBAAC,GAAD,EAAa,QAAQ,EAAM,QAAU,CAAA;MACpD,EAAM,SACL,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;;QAAE,EAAM,MAAM;QACpC;;MAEL;;IAEN,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAET,EAAM;KACA,CAAA;IAER,EAAM,WACL,kBAAC,KAAD;KAAG,WAAU;eAAmD,EAAM;KAAY,CAAA;IAGpF,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OAAa,SAAS,EAAM;OAAU,QAAO;OAAO,CAAA;MACnD,EAAM,cAAc,KACnB,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;QAAE,EAAa,EAAM,YAAY;QAAC;QAC9D;;MAER,EAAM,UAAU,SAAS,KACxB,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA;QAAC;QACrC,EAAM,UAAU,IAAI,EAAgB,CAAC,KAAK,KAAK;QAC3C;;MAET,kBAAC,QAAD,EAAA,UAAO,EAAmB,EAAU,EAAQ,CAAA;MACxC;;IACF;KACI"}
@@ -1 +1 @@
1
- {"version":3,"file":"TopicCard.js","names":[],"sources":["../../../src/theculturepost/components/TopicCard.tsx"],"sourcesContent":["/**\n * TopicCard — a discovery tile for a curated topic.\n * @module theculturepost/components\n *\n * Category badge, featured marker, tradition, description. Links to the topic\n * detail page.\n */\n\nimport { Compass, Sparkles } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport type { TcpTopic } from '../types';\n\ninterface TopicCardProps {\n topic: TcpTopic;\n onOpen: () => void;\n}\n\nexport function TopicCard({ topic, onOpen }: TopicCardProps) {\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Compass className=\"h-5 w-5\" />\n </div>\n {topic.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text\">\n <Sparkles className=\"h-3 w-3\" /> Featured\n </span>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"mt-3 text-left text-base font-semibold text-text-primary hover:underline\"\n >\n {topic.name}\n </button>\n\n {topic.tradition && (\n <p className=\"mt-0.5 text-sm text-text-secondary\">{topic.tradition}</p>\n )}\n\n {topic.description && (\n <p className=\"mt-2 line-clamp-3 text-sm text-text-secondary\">{topic.description}</p>\n )}\n\n <div className=\"mt-auto flex items-center justify-between gap-2 pt-4\">\n <StatusBadge status={topic.category} />\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n Explore →\n </button>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;AAkBA,SAAgB,EAAU,EAAE,UAAO,aAA0B;AAC3D,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;KAC3B,CAAA,EACL,EAAM,cACL,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAAA,YAC3B;OAEL;;GAEN,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAET,EAAM;IACA,CAAA;GAER,EAAM,aACL,kBAAC,KAAD;IAAG,WAAU;cAAsC,EAAM;IAAc,CAAA;GAGxE,EAAM,eACL,kBAAC,KAAD;IAAG,WAAU;cAAiD,EAAM;IAAgB,CAAA;GAGtF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAM,UAAY,CAAA,EACvC,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eACX;KAEQ,CAAA,CACL;;GACI"}
1
+ {"version":3,"file":"TopicCard.js","names":[],"sources":["../../../src/theculturepost/components/TopicCard.tsx"],"sourcesContent":["/**\n * TopicCard — a discovery tile for a curated topic.\n * @module theculturepost/components\n *\n * Category badge, featured marker, tradition, description. Links to the topic\n * detail page.\n */\n\nimport { Compass, Sparkles } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport type { TcpTopic } from '../types';\n\ninterface TopicCardProps {\n topic: TcpTopic;\n onOpen: () => void;\n}\n\nexport function TopicCard({ topic, onOpen }: TopicCardProps) {\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Compass className=\"h-5 w-5\" />\n </div>\n {topic.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text\">\n <Sparkles className=\"h-3 w-3\" /> Featured\n </span>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"mt-3 text-left text-base font-semibold text-text-primary hover:underline\"\n >\n {topic.name}\n </button>\n\n {topic.tradition && <p className=\"mt-0.5 text-sm text-text-secondary\">{topic.tradition}</p>}\n\n {topic.description && (\n <p className=\"mt-2 line-clamp-3 text-sm text-text-secondary\">{topic.description}</p>\n )}\n\n <div className=\"mt-auto flex items-center justify-between gap-2 pt-4\">\n <StatusBadge status={topic.category} />\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n Explore →\n </button>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;AAkBA,SAAgB,EAAU,EAAE,UAAO,aAA0B;AAC3D,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;KAC3B,CAAA,EACL,EAAM,cACL,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAAA,YAC3B;OAEL;;GAEN,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAET,EAAM;IACA,CAAA;GAER,EAAM,aAAa,kBAAC,KAAD;IAAG,WAAU;cAAsC,EAAM;IAAc,CAAA;GAE1F,EAAM,eACL,kBAAC,KAAD;IAAG,WAAU;cAAiD,EAAM;IAAgB,CAAA;GAGtF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAM,UAAY,CAAA,EACvC,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eACX;KAEQ,CAAA,CACL;;GACI"}
@@ -1 +1 @@
1
- {"version":3,"file":"kindIcon.js","names":[],"sources":["../../../src/theculturepost/components/kindIcon.tsx"],"sourcesContent":["/**\n * kindIcon — map an archive-item kind to a lucide icon component.\n * @module theculturepost/components\n */\n\nimport { FileText, Image, AudioLines, Video, Map, Box, ScrollText, type LucideIcon } from 'lucide-react';\nimport type { TcpArchiveItemKind } from '../types';\n\nexport const KIND_ICON: Record<TcpArchiveItemKind, LucideIcon> = {\n MANUSCRIPT: ScrollText,\n PHOTOGRAPH: Image,\n AUDIO: AudioLines,\n VIDEO: Video,\n MAP: Map,\n OBJECT: Box,\n DOCUMENT: FileText,\n};\n\n/** Kinds whose file is expected to be a viewable image. */\nexport const IMAGE_KINDS = new Set<TcpArchiveItemKind>(['PHOTOGRAPH', 'MAP', 'MANUSCRIPT', 'DOCUMENT']);\n"],"mappings":";;AAQA,IAAa,IAAoD;CAC/D,YAAY;CACZ,YAAY;CACZ,OAAO;CACP,OAAO;CACP,KAAK;CACL,QAAQ;CACR,UAAU;CACX,EAGY,IAAc,IAAI,IAAwB;CAAC;CAAc;CAAO;CAAc;CAAW,CAAC"}
1
+ {"version":3,"file":"kindIcon.js","names":[],"sources":["../../../src/theculturepost/components/kindIcon.tsx"],"sourcesContent":["/**\n * kindIcon — map an archive-item kind to a lucide icon component.\n * @module theculturepost/components\n */\n\nimport {\n FileText,\n Image,\n AudioLines,\n Video,\n Map,\n Box,\n ScrollText,\n type LucideIcon,\n} from 'lucide-react';\nimport type { TcpArchiveItemKind } from '../types';\n\nexport const KIND_ICON: Record<TcpArchiveItemKind, LucideIcon> = {\n MANUSCRIPT: ScrollText,\n PHOTOGRAPH: Image,\n AUDIO: AudioLines,\n VIDEO: Video,\n MAP: Map,\n OBJECT: Box,\n DOCUMENT: FileText,\n};\n\n/** Kinds whose file is expected to be a viewable image. */\nexport const IMAGE_KINDS = new Set<TcpArchiveItemKind>([\n 'PHOTOGRAPH',\n 'MAP',\n 'MANUSCRIPT',\n 'DOCUMENT',\n]);\n"],"mappings":";;AAiBA,IAAa,IAAoD;CAC/D,YAAY;CACZ,YAAY;CACZ,OAAO;CACP,OAAO;CACP,KAAK;CACL,QAAQ;CACR,UAAU;CACX,EAGY,IAAc,IAAI,IAAwB;CACrD;CACA;CACA;CACA;CACD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"TheCulturePostContext.js","names":[],"sources":["../../../src/theculturepost/context/TheCulturePostContext.tsx"],"sourcesContent":["/**\n * TheCulturePost Context Provider\n * @module theculturepost/context\n *\n * Splits standalone mode (wraps its own MemoryRouter + useNavigate) from\n * integrated mode (the host shell supplies `navigate`). Exposes gateway/auth\n * context + relative navigation helpers to every page.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { TheCulturePostRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\ninterface TheCulturePostContextValue\n extends Omit<TheCulturePostRootProps, 'registerNavItems' | 'registerFooterItems' | 'navigate'> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode). */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath. */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path. */\n getPath: (relativePath: string) => string;\n}\n\nconst TheCulturePostContext = createContext<TheCulturePostContextValue | null>(null);\n\nexport function useTheCulturePost(): TheCulturePostContextValue {\n const context = useContext(TheCulturePostContext);\n if (!context) {\n throw new Error('useTheCulturePost must be used within a TheCulturePostProvider');\n }\n return context;\n}\n\ntype ProviderProps = Omit<TheCulturePostRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n routerNavigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<TheCulturePostContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <TheCulturePostContext.Provider value={contextValue}>{children}</TheCulturePostContext.Provider>\n );\n};\n\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({ children, basePath, navigate, currentUser, tenantId, workspaceId, apiGatewayUrl, authToken }) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n navigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<TheCulturePostContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <TheCulturePostContext.Provider value={contextValue}>{children}</TheCulturePostContext.Provider>\n );\n};\n\nexport const TheCulturePostProvider: FC<PropsWithChildren<TheCulturePostRootProps>> = (props) => {\n const { children, navigate } = props;\n const isStandalone = !navigate;\n\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AA0BA,IAAM,IAAwB,EAAiD,KAAK;AAEpF,SAAgB,IAAgD;CAC9D,IAAM,IAAU,EAAW,EAAsB;AACjD,KAAI,CAAC,EACH,OAAU,MAAM,iEAAiE;AAEnF,QAAO;;AAOT,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAiB,GAAmB,EACpC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAe,EAAS,GAAmB,EAAa,CAAC;IAE3D,CAAC,GAAmB,EAAe,CACpC,EAEK,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAsB,UAAvB;EAAgC,OAAO;EAAe;EAA0C,CAAA;GAI9F,KAED,EAAE,aAAU,aAAU,aAAU,gBAAa,aAAU,gBAAa,kBAAe,mBAAgB;CACtG,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAS,EAAS,GAAmB,EAAa,CAAC;IAErD,CAAC,GAAmB,EAAS,CAC9B,EAEK,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAsB,UAAvB;EAAgC,OAAO;EAAe;EAA0C,CAAA;GAIvF,KAA0E,MAAU;CAC/F,IAAM,EAAE,aAAU,gBAAa;AAa/B,QAZsB,IAapB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;EACuB,CAAA,GAXxB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;EACuB,CAAA,EACb,CAAA"}
1
+ {"version":3,"file":"TheCulturePostContext.js","names":[],"sources":["../../../src/theculturepost/context/TheCulturePostContext.tsx"],"sourcesContent":["/**\n * TheCulturePost Context Provider\n * @module theculturepost/context\n *\n * Splits standalone mode (wraps its own MemoryRouter + useNavigate) from\n * integrated mode (the host shell supplies `navigate`). Exposes gateway/auth\n * context + relative navigation helpers to every page.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { TheCulturePostRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\ninterface TheCulturePostContextValue extends Omit<\n TheCulturePostRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode). */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath. */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path. */\n getPath: (relativePath: string) => string;\n}\n\nconst TheCulturePostContext = createContext<TheCulturePostContextValue | null>(null);\n\nexport function useTheCulturePost(): TheCulturePostContextValue {\n const context = useContext(TheCulturePostContext);\n if (!context) {\n throw new Error('useTheCulturePost must be used within a TheCulturePostProvider');\n }\n return context;\n}\n\ntype ProviderProps = Omit<TheCulturePostRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n routerNavigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<TheCulturePostContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <TheCulturePostContext.Provider value={contextValue}>{children}</TheCulturePostContext.Provider>\n );\n};\n\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n navigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<TheCulturePostContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <TheCulturePostContext.Provider value={contextValue}>{children}</TheCulturePostContext.Provider>\n );\n};\n\nexport const TheCulturePostProvider: FC<PropsWithChildren<TheCulturePostRootProps>> = (props) => {\n const { children, navigate } = props;\n const isStandalone = !navigate;\n\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AA4BA,IAAM,IAAwB,EAAiD,KAAK;AAEpF,SAAgB,IAAgD;CAC9D,IAAM,IAAU,EAAW,EAAsB;AACjD,KAAI,CAAC,EACH,OAAU,MAAM,iEAAiE;AAEnF,QAAO;;AAOT,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAiB,GAAmB,EACpC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAe,EAAS,GAAmB,EAAa,CAAC;IAE3D,CAAC,GAAmB,EAAe,CACpC,EAEK,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAsB,UAAvB;EAAgC,OAAO;EAAe;EAA0C,CAAA;GAI9F,KAED,EACH,aACA,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAS,EAAS,GAAmB,EAAa,CAAC;IAErD,CAAC,GAAmB,EAAS,CAC9B,EAEK,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAsB,UAAvB;EAAgC,OAAO;EAAe;EAA0C,CAAA;GAIvF,KAA0E,MAAU;CAC/F,IAAM,EAAE,aAAU,gBAAa;AAa/B,QAZsB,IAapB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;EACuB,CAAA,GAXxB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;EACuB,CAAA,EACb,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/theculturepost/pages/HomePage.tsx"],"sourcesContent":["/**\n * HomePage — the editorial landing for TheCulturePost.\n * @module theculturepost/pages\n *\n * Surfaces featured topics (`tcpListTopics`), the latest published stories\n * (`tcpListStories status:PUBLISHED`), active discussions\n * (`tcpListDiscussions status:APPROVED`) and featured collections\n * (`tcpListCollections`). Everything links into its section; honest empty states\n * appear when a surface has no data yet.\n */\n\nimport { BookOpen, Compass, MessagesSquare, Library, ArrowRight } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { Section, LoadingView } from '../components/StateViews';\nimport { StoryCard } from '../components/StoryCard';\nimport { TopicCard } from '../components/TopicCard';\nimport { DiscussionCard } from '../components/DiscussionCard';\nimport { CollectionCard } from '../components/CollectionCard';\nimport { EmptyState } from '../components/EmptyState';\nimport {\n useTcpTopics,\n useTcpStories,\n useTcpDiscussions,\n useTcpCollections,\n} from '../hooks';\nimport { useTheCulturePost } from '../context';\n\nfunction SectionLink({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <Button variant=\"ghost\" size=\"sm\" onClick={onClick}>\n {label} <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n );\n}\n\nexport function HomePage() {\n const { navigateTo } = useTheCulturePost();\n\n const topicsQuery = useTcpTopics({ pagination: { limit: 6 } });\n const storiesQuery = useTcpStories({ status: 'PUBLISHED', pagination: { limit: 6 } });\n const discussionsQuery = useTcpDiscussions({ status: 'APPROVED', pagination: { limit: 4 } });\n const collectionsQuery = useTcpCollections({ limit: 4 });\n\n const topics = topicsQuery.data?.items ?? [];\n const featuredTopics = topics.filter((t) => t.isFeatured);\n const shownTopics = (featuredTopics.length > 0 ? featuredTopics : topics).slice(0, 6);\n const stories = storiesQuery.data?.items ?? [];\n const discussions = discussionsQuery.data?.items ?? [];\n const collections = collectionsQuery.data ?? [];\n\n const anythingLoading =\n topicsQuery.isLoading &&\n storiesQuery.isLoading &&\n discussionsQuery.isLoading &&\n collectionsQuery.isLoading;\n\n const everythingEmpty =\n !topicsQuery.isLoading &&\n !storiesQuery.isLoading &&\n !discussionsQuery.isLoading &&\n !collectionsQuery.isLoading &&\n shownTopics.length === 0 &&\n stories.length === 0 &&\n discussions.length === 0 &&\n collections.length === 0;\n\n return (\n <PageLayout\n title=\"TheCulturePost\"\n titleGradient\n aurora\n description=\"Culture, religion, values and history — told with care and grounded in the record.\"\n actions={\n <Button onClick={() => navigateTo('stories')}>\n <BookOpen className=\"mr-1.5 h-4 w-4\" /> Read stories\n </Button>\n }\n >\n {anythingLoading ? (\n <LoadingView label=\"Loading the latest…\" />\n ) : everythingEmpty ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<BookOpen className=\"h-10 w-10\" />}\n title=\"Welcome to TheCulturePost\"\n description=\"This publication is just getting started. Curate a topic, draft a story, or begin building the archive.\"\n action={\n <div className=\"flex flex-wrap items-center justify-center gap-3\">\n <Button onClick={() => navigateTo('stories')}>\n <BookOpen className=\"mr-1.5 h-4 w-4\" /> Draft a story\n </Button>\n <Button variant=\"outline\" onClick={() => navigateTo('topics')}>\n <Compass className=\"mr-1.5 h-4 w-4\" /> Curate a topic\n </Button>\n </div>\n }\n />\n ) : (\n <div className=\"mx-auto flex max-w-6xl flex-col gap-2\">\n {/* Featured topics */}\n <Section\n title=\"Featured topics\"\n description=\"Curated subjects across culture, faith, history and heritage.\"\n action={<SectionLink label=\"All topics\" onClick={() => navigateTo('topics')} />}\n >\n {shownTopics.length === 0 ? (\n <EmptyState\n icon={<Compass className=\"h-9 w-9\" />}\n title=\"No topics yet\"\n description=\"Curate the first topic to organise the publication.\"\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {shownTopics.map((topic) => (\n <TopicCard\n key={topic.id}\n topic={topic}\n onOpen={() => navigateTo(`topics/${topic.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n {/* Latest stories */}\n <Section\n title=\"Latest stories\"\n description=\"Freshly published pieces from the newsroom.\"\n action={<SectionLink label=\"All stories\" onClick={() => navigateTo('stories')} />}\n >\n {stories.length === 0 ? (\n <EmptyState\n icon={<BookOpen className=\"h-9 w-9\" />}\n title=\"No published stories yet\"\n description=\"Draft a story and move it through the editorial lifecycle to publish.\"\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {stories.map((story) => (\n <StoryCard\n key={story.id}\n story={story}\n hideStatus\n onOpen={() => navigateTo(`stories/${story.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n {/* Active discussions */}\n <Section\n title=\"Active discussions\"\n description=\"Respectful, moderated conversations from the community.\"\n action={\n <SectionLink label=\"All discussions\" onClick={() => navigateTo('discussions')} />\n }\n >\n {discussions.length === 0 ? (\n <EmptyState\n icon={<MessagesSquare className=\"h-9 w-9\" />}\n title=\"No open discussions\"\n description=\"Open a discussion to invite thoughtful perspectives.\"\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {discussions.map((discussion) => (\n <DiscussionCard\n key={discussion.id}\n discussion={discussion}\n onOpen={() => navigateTo(`discussions/${discussion.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n {/* Featured collections */}\n <Section\n title=\"From the archive\"\n description=\"Collections of manuscripts, photographs, recordings and objects.\"\n action={<SectionLink label=\"Open archive\" onClick={() => navigateTo('archive')} />}\n >\n {collections.length === 0 ? (\n <EmptyState\n icon={<Library className=\"h-9 w-9\" />}\n title=\"The archive is empty\"\n description=\"Create a collection to begin preserving cultural heritage.\"\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {collections.map((collection) => (\n <CollectionCard\n key={collection.id}\n collection={collection}\n onOpen={() => navigateTo(`archive/collections/${collection.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/theculturepost/pages/HomePage.tsx"],"sourcesContent":["/**\n * HomePage — the editorial landing for TheCulturePost.\n * @module theculturepost/pages\n *\n * Surfaces featured topics (`tcpListTopics`), the latest published stories\n * (`tcpListStories status:PUBLISHED`), active discussions\n * (`tcpListDiscussions status:APPROVED`) and featured collections\n * (`tcpListCollections`). Everything links into its section; honest empty states\n * appear when a surface has no data yet.\n */\n\nimport { BookOpen, Compass, MessagesSquare, Library, ArrowRight } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { Section, LoadingView } from '../components/StateViews';\nimport { StoryCard } from '../components/StoryCard';\nimport { TopicCard } from '../components/TopicCard';\nimport { DiscussionCard } from '../components/DiscussionCard';\nimport { CollectionCard } from '../components/CollectionCard';\nimport { EmptyState } from '../components/EmptyState';\nimport { useTcpTopics, useTcpStories, useTcpDiscussions, useTcpCollections } from '../hooks';\nimport { useTheCulturePost } from '../context';\n\nfunction SectionLink({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <Button variant=\"ghost\" size=\"sm\" onClick={onClick}>\n {label} <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n );\n}\n\nexport function HomePage() {\n const { navigateTo } = useTheCulturePost();\n\n const topicsQuery = useTcpTopics({ pagination: { limit: 6 } });\n const storiesQuery = useTcpStories({ status: 'PUBLISHED', pagination: { limit: 6 } });\n const discussionsQuery = useTcpDiscussions({ status: 'APPROVED', pagination: { limit: 4 } });\n const collectionsQuery = useTcpCollections({ limit: 4 });\n\n const topics = topicsQuery.data?.items ?? [];\n const featuredTopics = topics.filter((t) => t.isFeatured);\n const shownTopics = (featuredTopics.length > 0 ? featuredTopics : topics).slice(0, 6);\n const stories = storiesQuery.data?.items ?? [];\n const discussions = discussionsQuery.data?.items ?? [];\n const collections = collectionsQuery.data ?? [];\n\n const anythingLoading =\n topicsQuery.isLoading &&\n storiesQuery.isLoading &&\n discussionsQuery.isLoading &&\n collectionsQuery.isLoading;\n\n const everythingEmpty =\n !topicsQuery.isLoading &&\n !storiesQuery.isLoading &&\n !discussionsQuery.isLoading &&\n !collectionsQuery.isLoading &&\n shownTopics.length === 0 &&\n stories.length === 0 &&\n discussions.length === 0 &&\n collections.length === 0;\n\n return (\n <PageLayout\n title=\"TheCulturePost\"\n titleGradient\n aurora\n description=\"Culture, religion, values and history — told with care and grounded in the record.\"\n actions={\n <Button onClick={() => navigateTo('stories')}>\n <BookOpen className=\"mr-1.5 h-4 w-4\" /> Read stories\n </Button>\n }\n >\n {anythingLoading ? (\n <LoadingView label=\"Loading the latest…\" />\n ) : everythingEmpty ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<BookOpen className=\"h-10 w-10\" />}\n title=\"Welcome to TheCulturePost\"\n description=\"This publication is just getting started. Curate a topic, draft a story, or begin building the archive.\"\n action={\n <div className=\"flex flex-wrap items-center justify-center gap-3\">\n <Button onClick={() => navigateTo('stories')}>\n <BookOpen className=\"mr-1.5 h-4 w-4\" /> Draft a story\n </Button>\n <Button variant=\"outline\" onClick={() => navigateTo('topics')}>\n <Compass className=\"mr-1.5 h-4 w-4\" /> Curate a topic\n </Button>\n </div>\n }\n />\n ) : (\n <div className=\"mx-auto flex max-w-6xl flex-col gap-2\">\n {/* Featured topics */}\n <Section\n title=\"Featured topics\"\n description=\"Curated subjects across culture, faith, history and heritage.\"\n action={<SectionLink label=\"All topics\" onClick={() => navigateTo('topics')} />}\n >\n {shownTopics.length === 0 ? (\n <EmptyState\n icon={<Compass className=\"h-9 w-9\" />}\n title=\"No topics yet\"\n description=\"Curate the first topic to organise the publication.\"\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {shownTopics.map((topic) => (\n <TopicCard\n key={topic.id}\n topic={topic}\n onOpen={() => navigateTo(`topics/${topic.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n {/* Latest stories */}\n <Section\n title=\"Latest stories\"\n description=\"Freshly published pieces from the newsroom.\"\n action={<SectionLink label=\"All stories\" onClick={() => navigateTo('stories')} />}\n >\n {stories.length === 0 ? (\n <EmptyState\n icon={<BookOpen className=\"h-9 w-9\" />}\n title=\"No published stories yet\"\n description=\"Draft a story and move it through the editorial lifecycle to publish.\"\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {stories.map((story) => (\n <StoryCard\n key={story.id}\n story={story}\n hideStatus\n onOpen={() => navigateTo(`stories/${story.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n {/* Active discussions */}\n <Section\n title=\"Active discussions\"\n description=\"Respectful, moderated conversations from the community.\"\n action={\n <SectionLink label=\"All discussions\" onClick={() => navigateTo('discussions')} />\n }\n >\n {discussions.length === 0 ? (\n <EmptyState\n icon={<MessagesSquare className=\"h-9 w-9\" />}\n title=\"No open discussions\"\n description=\"Open a discussion to invite thoughtful perspectives.\"\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {discussions.map((discussion) => (\n <DiscussionCard\n key={discussion.id}\n discussion={discussion}\n onOpen={() => navigateTo(`discussions/${discussion.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n {/* Featured collections */}\n <Section\n title=\"From the archive\"\n description=\"Collections of manuscripts, photographs, recordings and objects.\"\n action={<SectionLink label=\"Open archive\" onClick={() => navigateTo('archive')} />}\n >\n {collections.length === 0 ? (\n <EmptyState\n icon={<Library className=\"h-9 w-9\" />}\n title=\"The archive is empty\"\n description=\"Create a collection to begin preserving cultural heritage.\"\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {collections.map((collection) => (\n <CollectionCard\n key={collection.id}\n collection={collection}\n onOpen={() => navigateTo(`archive/collections/${collection.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ArchivePage.js","names":[],"sources":["../../../../src/theculturepost/pages/archive/ArchivePage.tsx"],"sourcesContent":["/**\n * ArchivePage — the cultural archive: collections, artifacts and oral histories.\n * @module theculturepost/pages/archive\n *\n * Three tabs, each backed by a real list field: `tcpListCollections`,\n * `tcpListArchiveItems` (filtered by kind + access level) and\n * `tcpListOralHistories`. Creation opens the matching dialog.\n */\n\nimport { useState } from 'react';\nimport { Library, Boxes, AudioLines, Plus } from 'lucide-react';\nimport {\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { CollectionCard } from '../../components/CollectionCard';\nimport { ArchiveItemCard } from '../../components/ArchiveItemCard';\nimport { OralHistoryCard } from '../../components/OralHistoryCard';\nimport { CreateCollectionDialog } from '../../components/CreateCollectionDialog';\nimport { CreateArchiveItemDialog } from '../../components/CreateArchiveItemDialog';\nimport { RecordOralHistoryDialog } from '../../components/RecordOralHistoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useTcpCollections,\n useTcpArchiveItems,\n useTcpOralHistories,\n useCursorPager,\n} from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { TcpArchiveItemKind, TcpAccessLevel } from '../../types';\n\ntype TabKey = 'collections' | 'artifacts' | 'oral-histories';\n\nconst TABS: TabItem<TabKey>[] = [\n { value: 'collections', label: 'Collections', icon: <Library className=\"h-4 w-4\" /> },\n { value: 'artifacts', label: 'Artifacts', icon: <Boxes className=\"h-4 w-4\" /> },\n { value: 'oral-histories', label: 'Oral Histories', icon: <AudioLines className=\"h-4 w-4\" /> },\n];\n\nexport function ArchivePage() {\n const [tab, setTab] = useState<TabKey>('collections');\n\n return (\n <PageLayout\n title=\"Archive\"\n description=\"A living archive of manuscripts, photographs, recordings, maps and objects — grouped into collections.\"\n >\n <Tabs items={TABS} value={tab} onChange={setTab} className=\"mb-6\" />\n {tab === 'collections' && <CollectionsTab />}\n {tab === 'artifacts' && <ArtifactsTab />}\n {tab === 'oral-histories' && <OralHistoriesTab />}\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Collections\n// ---------------------------------------------------------------------------\n\nfunction CollectionsTab() {\n const { navigateTo } = useTheCulturePost();\n const { data, isLoading, isError, refetch } = useTcpCollections({ limit: 60 });\n const [createOpen, setCreateOpen] = useState(false);\n const collections = data ?? [];\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New collection\n </Button>\n </div>\n {isLoading ? (\n <LoadingView label=\"Loading collections…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load collections.\" onRetry={() => refetch()} />\n ) : collections.length === 0 ? (\n <EmptyState\n illustration=\"empty-files\"\n icon={<Library className=\"h-10 w-10\" />}\n title=\"No collections yet\"\n description=\"Create a collection to begin grouping artifacts and oral histories.\"\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New collection\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {collections.map((collection) => (\n <CollectionCard\n key={collection.id}\n collection={collection}\n onOpen={() => navigateTo(`archive/collections/${collection.id}`)}\n />\n ))}\n </div>\n )}\n <CreateCollectionDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(collection) => navigateTo(`archive/collections/${collection.id}`)}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Artifacts\n// ---------------------------------------------------------------------------\n\nconst PAGE_SIZE = 16;\n\ntype KindFilter = 'ALL' | TcpArchiveItemKind;\ntype AccessFilter = 'ALL' | TcpAccessLevel;\n\nconst KIND_FILTERS: FilterOption<KindFilter>[] = [\n { value: 'ALL', label: 'All kinds' },\n { value: 'MANUSCRIPT', label: 'Manuscripts' },\n { value: 'PHOTOGRAPH', label: 'Photographs' },\n { value: 'AUDIO', label: 'Audio' },\n { value: 'VIDEO', label: 'Video' },\n { value: 'MAP', label: 'Maps' },\n { value: 'OBJECT', label: 'Objects' },\n { value: 'DOCUMENT', label: 'Documents' },\n];\n\nconst ACCESS_LEVELS: TcpAccessLevel[] = ['PUBLIC', 'COMMUNITY', 'RESTRICTED', 'EMBARGOED'];\n\nfunction ArtifactsTab() {\n const { navigateTo } = useTheCulturePost();\n const pager = useCursorPager();\n const [kind, setKind] = useState<KindFilter>('ALL');\n const [accessLevel, setAccessLevel] = useState<AccessFilter>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n\n const { data, isLoading, isError, refetch } = useTcpArchiveItems({\n kind: kind === 'ALL' ? undefined : kind,\n accessLevel: accessLevel === 'ALL' ? undefined : accessLevel,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const items = data?.items ?? [];\n\n const changeKind = (next: KindFilter) => {\n setKind(next);\n pager.reset();\n };\n\n const changeAccess = (next: string) => {\n setAccessLevel(next as AccessFilter);\n pager.reset();\n };\n\n return (\n <div>\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <FilterTabs options={KIND_FILTERS} value={kind} onChange={changeKind} />\n <Select value={accessLevel} onValueChange={changeAccess}>\n <SelectTrigger className=\"w-[12rem]\" aria-label=\"Filter by access level\">\n <SelectValue placeholder=\"All access\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All access</SelectItem>\n {ACCESS_LEVELS.map((a) => (\n <SelectItem key={a} value={a}>\n {formatEnumLabel(a)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Button className=\"ml-auto\" onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading artifacts…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load artifacts.\" onRetry={() => refetch()} />\n ) : items.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-files\"\n icon={<Boxes className=\"h-10 w-10\" />}\n title={kind === 'ALL' && accessLevel === 'ALL' ? 'No artifacts yet' : 'No artifacts match these filters'}\n description={\n kind === 'ALL' && accessLevel === 'ALL'\n ? 'Add the first artifact to the archive.'\n : 'Try widening the filters, or add a new artifact.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((item) => (\n <ArchiveItemCard\n key={item.id}\n item={item}\n onOpen={() => navigateTo(`archive/items/${item.id}`)}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={items.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </>\n )}\n\n <CreateArchiveItemDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(item) => navigateTo(`archive/items/${item.id}`)}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Oral histories\n// ---------------------------------------------------------------------------\n\nfunction OralHistoriesTab() {\n const { data, isLoading, isError, refetch } = useTcpOralHistories();\n const [createOpen, setCreateOpen] = useState(false);\n const oralHistories = data ?? [];\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record oral history\n </Button>\n </div>\n {isLoading ? (\n <LoadingView label=\"Loading oral histories…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load oral histories.\" onRetry={() => refetch()} />\n ) : oralHistories.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<AudioLines className=\"h-10 w-10\" />}\n title=\"No oral histories yet\"\n description=\"Record a first-person account to preserve it for the archive.\"\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record oral history\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n {oralHistories.map((oralHistory) => (\n <OralHistoryCard key={oralHistory.id} oralHistory={oralHistory} />\n ))}\n </div>\n )}\n <RecordOralHistoryDialog open={createOpen} onOpenChange={setCreateOpen} />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ArchivePage.js","names":[],"sources":["../../../../src/theculturepost/pages/archive/ArchivePage.tsx"],"sourcesContent":["/**\n * ArchivePage — the cultural archive: collections, artifacts and oral histories.\n * @module theculturepost/pages/archive\n *\n * Three tabs, each backed by a real list field: `tcpListCollections`,\n * `tcpListArchiveItems` (filtered by kind + access level) and\n * `tcpListOralHistories`. Creation opens the matching dialog.\n */\n\nimport { useState } from 'react';\nimport { Library, Boxes, AudioLines, Plus } from 'lucide-react';\nimport {\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { CollectionCard } from '../../components/CollectionCard';\nimport { ArchiveItemCard } from '../../components/ArchiveItemCard';\nimport { OralHistoryCard } from '../../components/OralHistoryCard';\nimport { CreateCollectionDialog } from '../../components/CreateCollectionDialog';\nimport { CreateArchiveItemDialog } from '../../components/CreateArchiveItemDialog';\nimport { RecordOralHistoryDialog } from '../../components/RecordOralHistoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useTcpCollections,\n useTcpArchiveItems,\n useTcpOralHistories,\n useCursorPager,\n} from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { TcpArchiveItemKind, TcpAccessLevel } from '../../types';\n\ntype TabKey = 'collections' | 'artifacts' | 'oral-histories';\n\nconst TABS: TabItem<TabKey>[] = [\n { value: 'collections', label: 'Collections', icon: <Library className=\"h-4 w-4\" /> },\n { value: 'artifacts', label: 'Artifacts', icon: <Boxes className=\"h-4 w-4\" /> },\n { value: 'oral-histories', label: 'Oral Histories', icon: <AudioLines className=\"h-4 w-4\" /> },\n];\n\nexport function ArchivePage() {\n const [tab, setTab] = useState<TabKey>('collections');\n\n return (\n <PageLayout\n title=\"Archive\"\n description=\"A living archive of manuscripts, photographs, recordings, maps and objects — grouped into collections.\"\n >\n <Tabs items={TABS} value={tab} onChange={setTab} className=\"mb-6\" />\n {tab === 'collections' && <CollectionsTab />}\n {tab === 'artifacts' && <ArtifactsTab />}\n {tab === 'oral-histories' && <OralHistoriesTab />}\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Collections\n// ---------------------------------------------------------------------------\n\nfunction CollectionsTab() {\n const { navigateTo } = useTheCulturePost();\n const { data, isLoading, isError, refetch } = useTcpCollections({ limit: 60 });\n const [createOpen, setCreateOpen] = useState(false);\n const collections = data ?? [];\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New collection\n </Button>\n </div>\n {isLoading ? (\n <LoadingView label=\"Loading collections…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load collections.\" onRetry={() => refetch()} />\n ) : collections.length === 0 ? (\n <EmptyState\n illustration=\"empty-files\"\n icon={<Library className=\"h-10 w-10\" />}\n title=\"No collections yet\"\n description=\"Create a collection to begin grouping artifacts and oral histories.\"\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New collection\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {collections.map((collection) => (\n <CollectionCard\n key={collection.id}\n collection={collection}\n onOpen={() => navigateTo(`archive/collections/${collection.id}`)}\n />\n ))}\n </div>\n )}\n <CreateCollectionDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(collection) => navigateTo(`archive/collections/${collection.id}`)}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Artifacts\n// ---------------------------------------------------------------------------\n\nconst PAGE_SIZE = 16;\n\ntype KindFilter = 'ALL' | TcpArchiveItemKind;\ntype AccessFilter = 'ALL' | TcpAccessLevel;\n\nconst KIND_FILTERS: FilterOption<KindFilter>[] = [\n { value: 'ALL', label: 'All kinds' },\n { value: 'MANUSCRIPT', label: 'Manuscripts' },\n { value: 'PHOTOGRAPH', label: 'Photographs' },\n { value: 'AUDIO', label: 'Audio' },\n { value: 'VIDEO', label: 'Video' },\n { value: 'MAP', label: 'Maps' },\n { value: 'OBJECT', label: 'Objects' },\n { value: 'DOCUMENT', label: 'Documents' },\n];\n\nconst ACCESS_LEVELS: TcpAccessLevel[] = ['PUBLIC', 'COMMUNITY', 'RESTRICTED', 'EMBARGOED'];\n\nfunction ArtifactsTab() {\n const { navigateTo } = useTheCulturePost();\n const pager = useCursorPager();\n const [kind, setKind] = useState<KindFilter>('ALL');\n const [accessLevel, setAccessLevel] = useState<AccessFilter>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n\n const { data, isLoading, isError, refetch } = useTcpArchiveItems({\n kind: kind === 'ALL' ? undefined : kind,\n accessLevel: accessLevel === 'ALL' ? undefined : accessLevel,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const items = data?.items ?? [];\n\n const changeKind = (next: KindFilter) => {\n setKind(next);\n pager.reset();\n };\n\n const changeAccess = (next: string) => {\n setAccessLevel(next as AccessFilter);\n pager.reset();\n };\n\n return (\n <div>\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <FilterTabs options={KIND_FILTERS} value={kind} onChange={changeKind} />\n <Select value={accessLevel} onValueChange={changeAccess}>\n <SelectTrigger className=\"w-[12rem]\" aria-label=\"Filter by access level\">\n <SelectValue placeholder=\"All access\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All access</SelectItem>\n {ACCESS_LEVELS.map((a) => (\n <SelectItem key={a} value={a}>\n {formatEnumLabel(a)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Button className=\"ml-auto\" onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading artifacts…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load artifacts.\" onRetry={() => refetch()} />\n ) : items.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-files\"\n icon={<Boxes className=\"h-10 w-10\" />}\n title={\n kind === 'ALL' && accessLevel === 'ALL'\n ? 'No artifacts yet'\n : 'No artifacts match these filters'\n }\n description={\n kind === 'ALL' && accessLevel === 'ALL'\n ? 'Add the first artifact to the archive.'\n : 'Try widening the filters, or add a new artifact.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((item) => (\n <ArchiveItemCard\n key={item.id}\n item={item}\n onOpen={() => navigateTo(`archive/items/${item.id}`)}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={items.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </>\n )}\n\n <CreateArchiveItemDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n onCreated={(item) => navigateTo(`archive/items/${item.id}`)}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Oral histories\n// ---------------------------------------------------------------------------\n\nfunction OralHistoriesTab() {\n const { data, isLoading, isError, refetch } = useTcpOralHistories();\n const [createOpen, setCreateOpen] = useState(false);\n const oralHistories = data ?? [];\n\n return (\n <div>\n <div className=\"mb-4 flex items-center justify-end\">\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record oral history\n </Button>\n </div>\n {isLoading ? (\n <LoadingView label=\"Loading oral histories…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load oral histories.\" onRetry={() => refetch()} />\n ) : oralHistories.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<AudioLines className=\"h-10 w-10\" />}\n title=\"No oral histories yet\"\n description=\"Record a first-person account to preserve it for the archive.\"\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record oral history\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n {oralHistories.map((oralHistory) => (\n <OralHistoryCard key={oralHistory.id} oralHistory={oralHistory} />\n ))}\n </div>\n )}\n <RecordOralHistoryDialog open={createOpen} onOpenChange={setCreateOpen} />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CollectionDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/archive/CollectionDetailPage.tsx"],"sourcesContent":["/**\n * CollectionDetailPage — a single archive collection.\n * @module theculturepost/pages/archive\n *\n * Reads `tcpCollection` (with its items + oral histories). Adding an artifact or\n * recording an oral history is pre-scoped to this collection.\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft, Library, Boxes, AudioLines, Plus } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { CoverImage } from '../../components/CoverImage';\nimport { ArchiveItemCard } from '../../components/ArchiveItemCard';\nimport { OralHistoryCard } from '../../components/OralHistoryCard';\nimport { CreateArchiveItemDialog } from '../../components/CreateArchiveItemDialog';\nimport { RecordOralHistoryDialog } from '../../components/RecordOralHistoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useTcpCollection } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber } from '../../utils/formatters';\nimport { useParams } from 'react-router-dom';\n\nexport function CollectionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: collection, isLoading, isError, refetch } = useTcpCollection(id);\n const [addItemOpen, setAddItemOpen] = useState(false);\n const [recordOpen, setRecordOpen] = useState(false);\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('archive')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Collection\" actions={backButton}>\n <LoadingView label=\"Loading collection…\" />\n </PageLayout>\n );\n }\n\n if (isError || !collection) {\n return (\n <PageLayout title=\"Collection\" actions={backButton}>\n <ErrorView\n title=\"Collection not found\"\n message=\"This collection may have been removed, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const items = collection.items ?? [];\n const oralHistories = collection.oralHistories ?? [];\n\n return (\n <PageLayout title={collection.name} actions={backButton}>\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 overflow-hidden p-0 sm:flex-row\">\n <div className=\"sm:w-64 sm:shrink-0\">\n <CoverImage\n fileId={collection.cover?.id}\n alt={collection.name}\n rounded=\"rounded-none\"\n className=\"aspect-[3/2] w-full sm:h-full\"\n icon={<Library className=\"h-10 w-10\" />}\n />\n </div>\n <div className=\"flex flex-1 flex-col gap-2 p-5 sm:p-6\">\n {collection.tradition && (\n <span className=\"text-sm font-medium text-text-secondary\">{collection.tradition}</span>\n )}\n {collection.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {collection.description}\n </p>\n )}\n <div className=\"mt-auto flex flex-wrap items-center gap-4 pt-2 text-sm text-text-tertiary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <Boxes className=\"h-4 w-4\" /> {formatNumber(collection.itemCount)} artifacts\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <AudioLines className=\"h-4 w-4\" /> {formatNumber(oralHistories.length)} oral histories\n </span>\n </div>\n </div>\n </GlassCard>\n\n <Section\n title=\"Artifacts\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n }\n >\n {items.length === 0 ? (\n <EmptyState\n icon={<Boxes className=\"h-9 w-9\" />}\n title=\"No artifacts in this collection\"\n description=\"Add the first artifact to this collection.\"\n action={\n <Button onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((item) => (\n <ArchiveItemCard\n key={item.id}\n item={item}\n onOpen={() => navigateTo(`archive/items/${item.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n <Section\n title=\"Oral histories\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRecordOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n }\n >\n {oralHistories.length === 0 ? (\n <EmptyState\n icon={<AudioLines className=\"h-9 w-9\" />}\n title=\"No oral histories yet\"\n description=\"Record a first-person account for this collection.\"\n action={\n <Button onClick={() => setRecordOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n {oralHistories.map((oralHistory) => (\n <OralHistoryCard key={oralHistory.id} oralHistory={oralHistory} />\n ))}\n </div>\n )}\n </Section>\n </div>\n\n <CreateArchiveItemDialog\n open={addItemOpen}\n onOpenChange={setAddItemOpen}\n collectionId={collection.id}\n />\n <RecordOralHistoryDialog\n open={recordOpen}\n onOpenChange={setRecordOpen}\n collectionId={collection.id}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CollectionDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/archive/CollectionDetailPage.tsx"],"sourcesContent":["/**\n * CollectionDetailPage — a single archive collection.\n * @module theculturepost/pages/archive\n *\n * Reads `tcpCollection` (with its items + oral histories). Adding an artifact or\n * recording an oral history is pre-scoped to this collection.\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft, Library, Boxes, AudioLines, Plus } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { CoverImage } from '../../components/CoverImage';\nimport { ArchiveItemCard } from '../../components/ArchiveItemCard';\nimport { OralHistoryCard } from '../../components/OralHistoryCard';\nimport { CreateArchiveItemDialog } from '../../components/CreateArchiveItemDialog';\nimport { RecordOralHistoryDialog } from '../../components/RecordOralHistoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useTcpCollection } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber } from '../../utils/formatters';\nimport { useParams } from 'react-router-dom';\n\nexport function CollectionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: collection, isLoading, isError, refetch } = useTcpCollection(id);\n const [addItemOpen, setAddItemOpen] = useState(false);\n const [recordOpen, setRecordOpen] = useState(false);\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('archive')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Collection\" actions={backButton}>\n <LoadingView label=\"Loading collection…\" />\n </PageLayout>\n );\n }\n\n if (isError || !collection) {\n return (\n <PageLayout title=\"Collection\" actions={backButton}>\n <ErrorView\n title=\"Collection not found\"\n message=\"This collection may have been removed, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const items = collection.items ?? [];\n const oralHistories = collection.oralHistories ?? [];\n\n return (\n <PageLayout title={collection.name} actions={backButton}>\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6\">\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-col gap-4 overflow-hidden p-0 sm:flex-row\"\n >\n <div className=\"sm:w-64 sm:shrink-0\">\n <CoverImage\n fileId={collection.cover?.id}\n alt={collection.name}\n rounded=\"rounded-none\"\n className=\"aspect-[3/2] w-full sm:h-full\"\n icon={<Library className=\"h-10 w-10\" />}\n />\n </div>\n <div className=\"flex flex-1 flex-col gap-2 p-5 sm:p-6\">\n {collection.tradition && (\n <span className=\"text-sm font-medium text-text-secondary\">\n {collection.tradition}\n </span>\n )}\n {collection.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {collection.description}\n </p>\n )}\n <div className=\"mt-auto flex flex-wrap items-center gap-4 pt-2 text-sm text-text-tertiary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <Boxes className=\"h-4 w-4\" /> {formatNumber(collection.itemCount)} artifacts\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <AudioLines className=\"h-4 w-4\" /> {formatNumber(oralHistories.length)} oral\n histories\n </span>\n </div>\n </div>\n </GlassCard>\n\n <Section\n title=\"Artifacts\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n }\n >\n {items.length === 0 ? (\n <EmptyState\n icon={<Boxes className=\"h-9 w-9\" />}\n title=\"No artifacts in this collection\"\n description=\"Add the first artifact to this collection.\"\n action={\n <Button onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((item) => (\n <ArchiveItemCard\n key={item.id}\n item={item}\n onOpen={() => navigateTo(`archive/items/${item.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n\n <Section\n title=\"Oral histories\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRecordOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n }\n >\n {oralHistories.length === 0 ? (\n <EmptyState\n icon={<AudioLines className=\"h-9 w-9\" />}\n title=\"No oral histories yet\"\n description=\"Record a first-person account for this collection.\"\n action={\n <Button onClick={() => setRecordOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n {oralHistories.map((oralHistory) => (\n <OralHistoryCard key={oralHistory.id} oralHistory={oralHistory} />\n ))}\n </div>\n )}\n </Section>\n </div>\n\n <CreateArchiveItemDialog\n open={addItemOpen}\n onOpenChange={setAddItemOpen}\n collectionId={collection.id}\n />\n <RecordOralHistoryDialog\n open={recordOpen}\n onOpenChange={setRecordOpen}\n collectionId={collection.id}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -98,7 +98,8 @@ function D() {
98
98
  /* @__PURE__ */ C(v, { className: "h-3.5 w-3.5" }),
99
99
  " ",
100
100
  a(k.replyCount),
101
- " replies"
101
+ " ",
102
+ "replies"
102
103
  ]
103
104
  }),
104
105
  /* @__PURE__ */ w("span", { children: ["Started ", o(k.createdAt)] }),
@@ -1 +1 @@
1
- {"version":3,"file":"DiscussionDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/discussions/DiscussionDetailPage.tsx"],"sourcesContent":["/**\n * DiscussionDetailPage — a single discussion and its moderated replies.\n * @module theculturepost/pages/discussions\n *\n * Reads `tcpDiscussion`. Writes `tcpLockDiscussion` and, via {@link ReplyThread},\n * `tcpPostReply` / `tcpModerateReply`. When anchored to a story, a link back to\n * the story is shown.\n */\n\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Lock,\n Unlock,\n HeartHandshake,\n Tag,\n BookOpen,\n MessagesSquare,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { AuthorLabel } from '../../components/AuthorLabel';\nimport { ReplyThread } from '../../components/ReplyThread';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useTcpDiscussion, useTcpLockDiscussion } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\n\nexport function DiscussionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: discussion, isLoading, isError, refetch } = useTcpDiscussion(id);\n const lock = useTcpLockDiscussion();\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('discussions')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Discussion\" actions={backButton}>\n <LoadingView label=\"Loading discussion…\" />\n </PageLayout>\n );\n }\n\n if (isError || !discussion) {\n return (\n <PageLayout title=\"Discussion\" actions={backButton}>\n <ErrorView\n title=\"Discussion not found\"\n message=\"This discussion may have been removed, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const doLock = async () => {\n try {\n await lock.mutateAsync(discussion.id);\n notify.success(discussion.isLocked ? 'Discussion unlocked' : 'Discussion locked');\n } catch (err) {\n notify.error('Could not update the discussion', describeError(err));\n }\n };\n\n return (\n <PageLayout\n title={discussion.title}\n actions={\n <div className=\"flex items-center gap-2\">\n {backButton}\n <Button variant=\"outline\" size=\"sm\" onClick={doLock} disabled={lock.isPending}>\n {discussion.isLocked ? (\n <>\n <Unlock className=\"mr-1.5 h-4 w-4\" /> Unlock\n </>\n ) : (\n <>\n <Lock className=\"mr-1.5 h-4 w-4\" /> Lock\n </>\n )}\n </Button>\n </div>\n }\n >\n <div className=\"mx-auto flex max-w-3xl flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={discussion.status} />\n {discussion.isInterfaith && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <HeartHandshake className=\"h-3 w-3\" /> Interfaith\n </span>\n )}\n {discussion.isLocked && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <Lock className=\"h-3 w-3\" /> Locked\n </span>\n )}\n </div>\n\n {discussion.prompt && (\n <p className=\"whitespace-pre-wrap text-base leading-relaxed text-text-secondary\">\n {discussion.prompt}\n </p>\n )}\n\n {discussion.tags.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <Tag className=\"h-3.5 w-3.5 text-text-tertiary\" aria-hidden=\"true\" />\n {discussion.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-border-seam pt-3 text-xs text-text-tertiary\">\n <AuthorLabel actorId={discussion.openedById} prefix=\"Opened by\" />\n <span className=\"inline-flex items-center gap-1\">\n <MessagesSquare className=\"h-3.5 w-3.5\" /> {formatNumber(discussion.replyCount)} replies\n </span>\n <span>Started {formatRelativeTime(discussion.createdAt)}</span>\n {discussion.storyId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`stories/${discussion.storyId}`)}\n className=\"inline-flex items-center gap-1 font-medium text-accent hover:underline\"\n >\n <BookOpen className=\"h-3.5 w-3.5\" /> View linked story\n </button>\n )}\n </div>\n </GlassCard>\n\n <Section title=\"Replies\" description=\"Perspectives from the community — moderated with care.\">\n <ReplyThread discussionId={discussion.id} isLocked={discussion.isLocked} />\n </Section>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA8BA,SAAgB,IAAuB;CACrC,IAAM,EAAE,UAAO,GAA2B,EACpC,EAAE,kBAAe,GAAmB,EACpC,EAAE,MAAM,GAAY,cAAW,YAAS,eAAY,EAAiB,EAAG,EACxE,IAAO,GAAsB,EAE7B,IACJ,kBAAC,GAAD;EAAQ,SAAQ;EAAQ,MAAK;EAAK,eAAe,EAAW,cAAc;YAA1E,CACE,kBAAC,GAAD,EAAW,WAAU,kBAAmB,CAAA,EAAA,QACjC;;AAgCX,QA7BI,IAEA,kBAAC,GAAD;EAAY,OAAM;EAAa,SAAS;YACtC,kBAAC,GAAD,EAAa,OAAM,uBAAwB,CAAA;EAChC,CAAA,GAIb,KAAW,CAAC,IAEZ,kBAAC,GAAD;EAAY,OAAM;EAAa,SAAS;YACtC,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,GAAS;GACxB,CAAA;EACS,CAAA,GAcf,kBAAC,GAAD;EACE,OAAO,EAAW;EAClB,SACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,GACD,kBAAC,GAAD;IAAQ,SAAQ;IAAU,MAAK;IAAK,SAf7B,YAAY;AACzB,SAAI;AAEF,MADA,MAAM,EAAK,YAAY,EAAW,GAAG,EACrC,EAAO,QAAQ,EAAW,WAAW,wBAAwB,oBAAoB;cAC1E,GAAK;AACZ,QAAO,MAAM,mCAAmC,EAAc,EAAI,CAAC;;;IAUV,UAAU,EAAK;cACjE,EAAW,WACV,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAQ,WAAU,kBAAmB,CAAA,EAAA,UACpC,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,QAClC,EAAA,CAAA;IAEE,CAAA,CACL;;YAGR,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD,EAAa,QAAQ,EAAW,QAAU,CAAA;OACzC,EAAW,gBACV,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD,EAAgB,WAAU,WAAY,CAAA,EAAA,cACjC;;OAER,EAAW,YACV,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,UACvB;;OAEL;;KAEL,EAAW,UACV,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAW;MACV,CAAA;KAGL,EAAW,KAAK,SAAS,KACxB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OAAK,WAAU;OAAiC,eAAY;OAAS,CAAA,EACpE,EAAW,KAAK,KAAK,MACpB,kBAAC,QAAD;OAEE,WAAU;iBAFZ,CAGC,KACG,EACG;SAJA,EAIA,CACP,CACE;;KAGR,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD;QAAa,SAAS,EAAW;QAAY,QAAO;QAAc,CAAA;OAClE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAgB,WAAU,eAAgB,CAAA;;SAAE,EAAa,EAAW,WAAW;SAAC;SAC3E;;OACP,kBAAC,QAAD,EAAA,UAAA,CAAM,YAAS,EAAmB,EAAW,UAAU,CAAQ,EAAA,CAAA;OAC9D,EAAW,WACV,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAW,WAAW,EAAW,UAAU;QAC1D,WAAU;kBAHZ,CAKE,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA,EAAA,qBAC7B;;OAEP;;KACI;OAEZ,kBAAC,GAAD;IAAS,OAAM;IAAU,aAAY;cACnC,kBAAC,GAAD;KAAa,cAAc,EAAW;KAAI,UAAU,EAAW;KAAY,CAAA;IACnE,CAAA,CACN;;EACK,CAAA"}
1
+ {"version":3,"file":"DiscussionDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/discussions/DiscussionDetailPage.tsx"],"sourcesContent":["/**\n * DiscussionDetailPage — a single discussion and its moderated replies.\n * @module theculturepost/pages/discussions\n *\n * Reads `tcpDiscussion`. Writes `tcpLockDiscussion` and, via {@link ReplyThread},\n * `tcpPostReply` / `tcpModerateReply`. When anchored to a story, a link back to\n * the story is shown.\n */\n\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Lock,\n Unlock,\n HeartHandshake,\n Tag,\n BookOpen,\n MessagesSquare,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { AuthorLabel } from '../../components/AuthorLabel';\nimport { ReplyThread } from '../../components/ReplyThread';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useTcpDiscussion, useTcpLockDiscussion } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\n\nexport function DiscussionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: discussion, isLoading, isError, refetch } = useTcpDiscussion(id);\n const lock = useTcpLockDiscussion();\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('discussions')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Discussion\" actions={backButton}>\n <LoadingView label=\"Loading discussion…\" />\n </PageLayout>\n );\n }\n\n if (isError || !discussion) {\n return (\n <PageLayout title=\"Discussion\" actions={backButton}>\n <ErrorView\n title=\"Discussion not found\"\n message=\"This discussion may have been removed, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const doLock = async () => {\n try {\n await lock.mutateAsync(discussion.id);\n notify.success(discussion.isLocked ? 'Discussion unlocked' : 'Discussion locked');\n } catch (err) {\n notify.error('Could not update the discussion', describeError(err));\n }\n };\n\n return (\n <PageLayout\n title={discussion.title}\n actions={\n <div className=\"flex items-center gap-2\">\n {backButton}\n <Button variant=\"outline\" size=\"sm\" onClick={doLock} disabled={lock.isPending}>\n {discussion.isLocked ? (\n <>\n <Unlock className=\"mr-1.5 h-4 w-4\" /> Unlock\n </>\n ) : (\n <>\n <Lock className=\"mr-1.5 h-4 w-4\" /> Lock\n </>\n )}\n </Button>\n </div>\n }\n >\n <div className=\"mx-auto flex max-w-3xl flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={discussion.status} />\n {discussion.isInterfaith && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <HeartHandshake className=\"h-3 w-3\" /> Interfaith\n </span>\n )}\n {discussion.isLocked && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <Lock className=\"h-3 w-3\" /> Locked\n </span>\n )}\n </div>\n\n {discussion.prompt && (\n <p className=\"whitespace-pre-wrap text-base leading-relaxed text-text-secondary\">\n {discussion.prompt}\n </p>\n )}\n\n {discussion.tags.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <Tag className=\"h-3.5 w-3.5 text-text-tertiary\" aria-hidden=\"true\" />\n {discussion.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-border-seam pt-3 text-xs text-text-tertiary\">\n <AuthorLabel actorId={discussion.openedById} prefix=\"Opened by\" />\n <span className=\"inline-flex items-center gap-1\">\n <MessagesSquare className=\"h-3.5 w-3.5\" /> {formatNumber(discussion.replyCount)}{' '}\n replies\n </span>\n <span>Started {formatRelativeTime(discussion.createdAt)}</span>\n {discussion.storyId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`stories/${discussion.storyId}`)}\n className=\"inline-flex items-center gap-1 font-medium text-accent hover:underline\"\n >\n <BookOpen className=\"h-3.5 w-3.5\" /> View linked story\n </button>\n )}\n </div>\n </GlassCard>\n\n <Section\n title=\"Replies\"\n description=\"Perspectives from the community — moderated with care.\"\n >\n <ReplyThread discussionId={discussion.id} isLocked={discussion.isLocked} />\n </Section>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StoriesPage.js","names":[],"sources":["../../../../src/theculturepost/pages/stories/StoriesPage.tsx"],"sourcesContent":["/**\n * StoriesPage — browse the newsroom.\n * @module theculturepost/pages/stories\n *\n * Reads `tcpListStories` (filtered by status + optional topic). \"New Story\"\n * opens {@link CreateStoryDialog}. Cursor-paginated.\n */\n\nimport { useState } from 'react';\nimport { BookOpen, Plus } from 'lucide-react';\nimport {\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { StoryCard } from '../../components/StoryCard';\nimport { CreateStoryDialog } from '../../components/CreateStoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport { useTcpStories, useTcpTopics, useCursorPager } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport type { TcpStoryStatus } from '../../types';\n\nconst PAGE_SIZE = 12;\n\ntype StatusFilter = 'ALL' | TcpStoryStatus;\n\nconst STATUS_FILTERS: FilterOption<StatusFilter>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'IN_REVIEW', label: 'In Review' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'IDEA', label: 'Idea' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nexport function StoriesPage() {\n const { navigateTo } = useTheCulturePost();\n const pager = useCursorPager();\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [topicId, setTopicId] = useState<string>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n\n const topicsQuery = useTcpTopics({ pagination: { limit: 100 } });\n const { data, isLoading, isError, refetch } = useTcpStories({\n status: status === 'ALL' ? undefined : status,\n topicId: topicId === 'ALL' ? undefined : topicId,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const stories = data?.items ?? [];\n\n const changeStatus = (next: StatusFilter) => {\n setStatus(next);\n pager.reset();\n };\n\n const changeTopic = (next: string) => {\n setTopicId(next);\n pager.reset();\n };\n\n return (\n <PageLayout\n title=\"Stories\"\n description=\"Articles, essays, explainers, photo stories and timelines — across the full editorial lifecycle.\"\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={STATUS_FILTERS} value={status} onChange={changeStatus} />\n <Select value={topicId} onValueChange={changeTopic}>\n <SelectTrigger className=\"w-[12rem]\" aria-label=\"Filter by topic\">\n <SelectValue placeholder=\"All topics\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All topics</SelectItem>\n {(topicsQuery.data?.items ?? []).map((topic) => (\n <SelectItem key={topic.id} value={topic.id}>\n {topic.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading stories…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load stories.\" onRetry={() => refetch()} />\n ) : stories.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BookOpen className=\"h-10 w-10\" />}\n title={status === 'ALL' && topicId === 'ALL' ? 'No stories yet' : 'No stories match these filters'}\n description={\n status === 'ALL' && topicId === 'ALL'\n ? 'Draft the first story — it enters the editorial lifecycle you can publish from.'\n : 'Try widening the filters, or draft a new story.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid gap-5 sm:grid-cols-2 xl:grid-cols-3\">\n {stories.map((story) => (\n <StoryCard\n key={story.id}\n story={story}\n onOpen={() => navigateTo(`stories/${story.id}`)}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={stories.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </>\n )}\n\n <CreateStoryDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n topicId={topicId === 'ALL' ? undefined : topicId}\n onCreated={(story) => navigateTo(`stories/${story.id}`)}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"StoriesPage.js","names":[],"sources":["../../../../src/theculturepost/pages/stories/StoriesPage.tsx"],"sourcesContent":["/**\n * StoriesPage — browse the newsroom.\n * @module theculturepost/pages/stories\n *\n * Reads `tcpListStories` (filtered by status + optional topic). \"New Story\"\n * opens {@link CreateStoryDialog}. Cursor-paginated.\n */\n\nimport { useState } from 'react';\nimport { BookOpen, Plus } from 'lucide-react';\nimport {\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { StoryCard } from '../../components/StoryCard';\nimport { CreateStoryDialog } from '../../components/CreateStoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport { useTcpStories, useTcpTopics, useCursorPager } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport type { TcpStoryStatus } from '../../types';\n\nconst PAGE_SIZE = 12;\n\ntype StatusFilter = 'ALL' | TcpStoryStatus;\n\nconst STATUS_FILTERS: FilterOption<StatusFilter>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'IN_REVIEW', label: 'In Review' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'IDEA', label: 'Idea' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nexport function StoriesPage() {\n const { navigateTo } = useTheCulturePost();\n const pager = useCursorPager();\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [topicId, setTopicId] = useState<string>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n\n const topicsQuery = useTcpTopics({ pagination: { limit: 100 } });\n const { data, isLoading, isError, refetch } = useTcpStories({\n status: status === 'ALL' ? undefined : status,\n topicId: topicId === 'ALL' ? undefined : topicId,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const stories = data?.items ?? [];\n\n const changeStatus = (next: StatusFilter) => {\n setStatus(next);\n pager.reset();\n };\n\n const changeTopic = (next: string) => {\n setTopicId(next);\n pager.reset();\n };\n\n return (\n <PageLayout\n title=\"Stories\"\n description=\"Articles, essays, explainers, photo stories and timelines — across the full editorial lifecycle.\"\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={STATUS_FILTERS} value={status} onChange={changeStatus} />\n <Select value={topicId} onValueChange={changeTopic}>\n <SelectTrigger className=\"w-[12rem]\" aria-label=\"Filter by topic\">\n <SelectValue placeholder=\"All topics\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All topics</SelectItem>\n {(topicsQuery.data?.items ?? []).map((topic) => (\n <SelectItem key={topic.id} value={topic.id}>\n {topic.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading stories…\" />\n ) : isError ? (\n <ErrorView message=\"We couldn't load stories.\" onRetry={() => refetch()} />\n ) : stories.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BookOpen className=\"h-10 w-10\" />}\n title={\n status === 'ALL' && topicId === 'ALL'\n ? 'No stories yet'\n : 'No stories match these filters'\n }\n description={\n status === 'ALL' && topicId === 'ALL'\n ? 'Draft the first story — it enters the editorial lifecycle you can publish from.'\n : 'Try widening the filters, or draft a new story.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid gap-5 sm:grid-cols-2 xl:grid-cols-3\">\n {stories.map((story) => (\n <StoryCard\n key={story.id}\n story={story}\n onOpen={() => navigateTo(`stories/${story.id}`)}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={stories.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </>\n )}\n\n <CreateStoryDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n topicId={topicId === 'ALL' ? undefined : topicId}\n onCreated={(story) => navigateTo(`stories/${story.id}`)}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -167,10 +167,14 @@ function re() {
167
167
  }),
168
168
  /* @__PURE__ */ R("p", {
169
169
  className: "mt-1 text-sm text-text-secondary",
170
- children: ["Current stage: ", /* @__PURE__ */ L("span", {
171
- className: "font-medium text-text-primary",
172
- children: s(_.status)
173
- })]
170
+ children: [
171
+ "Current stage:",
172
+ " ",
173
+ /* @__PURE__ */ L("span", {
174
+ className: "font-medium text-text-primary",
175
+ children: s(_.status)
176
+ })
177
+ ]
174
178
  }),
175
179
  Z.length > 0 ? /* @__PURE__ */ L("div", {
176
180
  className: "mt-3 flex flex-wrap gap-2",
@@ -1 +1 @@
1
- {"version":3,"file":"StoryDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/stories/StoryDetailPage.tsx"],"sourcesContent":["/**\n * StoryDetailPage — a single story with its editorial lifecycle, citations and\n * linked discussions.\n * @module theculturepost/pages/stories\n *\n * Reads `tcpStory` (+ its citations). Writes `tcpTransitionStory` (validated\n * lifecycle), `tcpUpdateStory` (edit), `tcpAddCitation` / `tcpVerifyCitation`\n * (via {@link CitationList}). Person references are id-only, so we render \"You\"\n * / \"Contributor\".\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n BookOpen,\n Clock,\n Languages,\n Tag,\n Pencil,\n Plus,\n GitBranch,\n MessagesSquare,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { CoverImage } from '../../components/CoverImage';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { AuthorLabel } from '../../components/AuthorLabel';\nimport { CitationList } from '../../components/CitationList';\nimport { DiscussionCard } from '../../components/DiscussionCard';\nimport { AddCitationDialog } from '../../components/AddCitationDialog';\nimport { OpenDiscussionDialog } from '../../components/OpenDiscussionDialog';\nimport { TagInput } from '../../components/TagInput';\nimport { FileUploadField } from '../../components/FileUploadField';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport {\n useTcpStory,\n useTcpTransitionStory,\n useTcpUpdateStory,\n useTcpTopics,\n useTcpDiscussions,\n FILE_FOLDERS,\n} from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpStory, TcpStoryFormat, TcpStoryStatus } from '../../types';\n\nconst NEXT_STATUSES: Record<TcpStoryStatus, TcpStoryStatus[]> = {\n IDEA: ['DRAFT'],\n DRAFT: ['IN_REVIEW'],\n IN_REVIEW: ['PUBLISHED', 'DRAFT'],\n PUBLISHED: ['ARCHIVED'],\n ARCHIVED: ['PUBLISHED'],\n};\n\nconst TRANSITION_LABEL: Record<TcpStoryStatus, string> = {\n IDEA: 'Move to Idea',\n DRAFT: 'Move to Draft',\n IN_REVIEW: 'Send to Review',\n PUBLISHED: 'Publish',\n ARCHIVED: 'Archive',\n};\n\nexport function StoryDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: story, isLoading, isError, refetch } = useTcpStory(id);\n const transition = useTcpTransitionStory();\n\n const [citationOpen, setCitationOpen] = useState(false);\n const [discussionOpen, setDiscussionOpen] = useState(false);\n const [editOpen, setEditOpen] = useState(false);\n\n const discussionsQuery = useTcpDiscussions({ storyId: id, pagination: { limit: 20 } });\n const linkedDiscussions = discussionsQuery.data?.items ?? [];\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('stories')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Story\" actions={backButton}>\n <LoadingView label=\"Loading story…\" />\n </PageLayout>\n );\n }\n\n if (isError || !story) {\n return (\n <PageLayout title=\"Story\" actions={backButton}>\n <ErrorView\n title=\"Story not found\"\n message=\"This story may have been archived, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const nextStatuses = NEXT_STATUSES[story.status] ?? [];\n\n const doTransition = async (status: TcpStoryStatus) => {\n try {\n await transition.mutateAsync({ id: story.id, status });\n notify.success('Story updated', `Moved to ${formatEnumLabel(status)}`);\n } catch (err) {\n notify.error('Could not update the story', describeError(err));\n }\n };\n\n return (\n <PageLayout\n title={story.title}\n actions={\n <div className=\"flex items-center gap-2\">\n {backButton}\n <Button variant=\"outline\" onClick={() => setEditOpen(true)}>\n <Pencil className=\"mr-1.5 h-4 w-4\" /> Edit\n </Button>\n </div>\n }\n >\n <article className=\"mx-auto flex max-w-3xl flex-col gap-6\">\n {/* Hero cover */}\n <CoverImage\n fileId={story.cover?.id}\n alt={story.title}\n className=\"aspect-[16/9] w-full\"\n icon={<BookOpen className=\"h-10 w-10\" />}\n />\n\n {/* Meta */}\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={story.format} />\n <StatusBadge status={story.status} />\n {story.topic && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`topics/${story.topic!.id}`)}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary hover:text-text-primary\"\n >\n <Tag className=\"h-3 w-3\" /> {story.topic.name}\n </button>\n )}\n </div>\n\n {story.summary && (\n <p className=\"text-lg leading-relaxed text-text-secondary\">{story.summary}</p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1.5 text-sm text-text-tertiary\">\n <AuthorLabel actorId={story.authorId} prefix=\"By\" withIcon />\n {story.editorId && <AuthorLabel actorId={story.editorId} prefix=\"Edited by\" />}\n {story.readingMins > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-4 w-4\" /> {formatNumber(story.readingMins)} min read\n </span>\n )}\n {story.languages.length > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Languages className=\"h-4 w-4\" /> {story.languages.map(formatEnumLabel).join(', ')}\n </span>\n )}\n </div>\n\n {story.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\">\n {story.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n </div>\n\n {/* Editorial lifecycle */}\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex items-center gap-2\">\n <GitBranch className=\"h-4 w-4 text-text-tertiary\" />\n <h2 className=\"text-sm font-semibold text-text-primary\">Editorial lifecycle</h2>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Current stage: <span className=\"font-medium text-text-primary\">{formatEnumLabel(story.status)}</span>\n </p>\n {nextStatuses.length > 0 ? (\n <div className=\"mt-3 flex flex-wrap gap-2\">\n {nextStatuses.map((status) => (\n <Button\n key={status}\n size=\"sm\"\n variant={status === 'PUBLISHED' ? 'default' : 'outline'}\n onClick={() => doTransition(status)}\n disabled={transition.isPending}\n >\n {TRANSITION_LABEL[status]}\n </Button>\n ))}\n </div>\n ) : (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n This story is at the end of its lifecycle.\n </p>\n )}\n </GlassCard>\n\n {/* Body */}\n {story.body ? (\n <div className=\"whitespace-pre-wrap text-base leading-relaxed text-text-primary\">\n {story.body}\n </div>\n ) : (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-6 text-center text-sm text-text-tertiary\">\n This story has no body yet. Use Edit to write it.\n </p>\n )}\n\n {/* Citations */}\n <Section\n title=\"Citations\"\n description=\"Verifiable sources that ground this story.\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setCitationOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add citation\n </Button>\n }\n >\n <CitationList storyId={story.id} />\n </Section>\n\n {/* Linked discussions */}\n <Section\n title=\"Discussions\"\n description=\"Conversations anchored to this story.\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setDiscussionOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Open discussion\n </Button>\n }\n >\n {discussionsQuery.isLoading ? (\n <LoadingView label=\"Loading discussions…\" />\n ) : linkedDiscussions.length === 0 ? (\n <EmptyState\n icon={<MessagesSquare className=\"h-9 w-9\" />}\n title=\"No discussions yet\"\n description=\"Open a discussion to invite perspectives on this story.\"\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {linkedDiscussions.map((discussion) => (\n <DiscussionCard\n key={discussion.id}\n discussion={discussion}\n onOpen={() => navigateTo(`discussions/${discussion.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </article>\n\n <AddCitationDialog open={citationOpen} onOpenChange={setCitationOpen} storyId={story.id} />\n <OpenDiscussionDialog\n open={discussionOpen}\n onOpenChange={setDiscussionOpen}\n storyId={story.id}\n onCreated={(discussion) => navigateTo(`discussions/${discussion.id}`)}\n />\n <EditStoryDialog open={editOpen} onOpenChange={setEditOpen} story={story} />\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Edit story dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nconst FORMATS: TcpStoryFormat[] = [\n 'ARTICLE',\n 'ESSAY',\n 'EXPLAINER',\n 'ORAL_HISTORY',\n 'PHOTO_STORY',\n 'TIMELINE',\n];\n\nfunction EditStoryDialog({\n open,\n onOpenChange,\n story,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n story: TcpStory;\n}) {\n const updateStory = useTcpUpdateStory();\n const topicsQuery = useTcpTopics({ pagination: { limit: 100 } });\n\n const [title, setTitle] = useState(story.title);\n const [summary, setSummary] = useState(story.summary ?? '');\n const [body, setBody] = useState(story.body ?? '');\n const [format, setFormat] = useState<TcpStoryFormat>(story.format);\n const [pickedTopic, setPickedTopic] = useState(story.topicId ?? '');\n const [languages, setLanguages] = useState<string[]>(story.languages);\n const [tags, setTags] = useState<string[]>(story.tags);\n const [readingMins, setReadingMins] = useState(story.readingMins ? String(story.readingMins) : '');\n const [coverFileId, setCoverFileId] = useState<string | null>(story.cover?.id ?? null);\n const [error, setError] = useState<string | null>(null);\n\n // Reset local state whenever a different story is opened for editing.\n const storyKey = useMemo(() => `${story.id}:${story.updatedAt}`, [story.id, story.updatedAt]);\n const [seededKey, setSeededKey] = useState(storyKey);\n if (seededKey !== storyKey) {\n setSeededKey(storyKey);\n setTitle(story.title);\n setSummary(story.summary ?? '');\n setBody(story.body ?? '');\n setFormat(story.format);\n setPickedTopic(story.topicId ?? '');\n setLanguages(story.languages);\n setTags(story.tags);\n setReadingMins(story.readingMins ? String(story.readingMins) : '');\n setCoverFileId(story.cover?.id ?? null);\n setError(null);\n }\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n try {\n await updateStory.mutateAsync({\n id: story.id,\n input: {\n title: title.trim(),\n summary: summary.trim() || null,\n body: body.trim() || null,\n format,\n topicId: pickedTopic || null,\n languages,\n tags,\n readingMins: readingMins ? Number(readingMins) : null,\n coverFileId,\n },\n });\n notify.success('Story updated', title.trim());\n onOpenChange(false);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit story</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Refine the story. The URL slug is fixed once created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Cover image</Label>\n <FileUploadField\n folder={FILE_FOLDERS.covers}\n accept=\"image/*\"\n label=\"Upload cover\"\n fileId={coverFileId}\n onUploaded={setCoverFileId}\n onCleared={() => setCoverFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-title\">Title</Label>\n <Input id=\"edit-title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Format</Label>\n <Select value={format} onValueChange={(v) => setFormat(v as TcpStoryFormat)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {FORMATS.map((f) => (\n <SelectItem key={f} value={f}>\n {formatEnumLabel(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-reading\">Reading time (min)</Label>\n <Input\n id=\"edit-reading\"\n type=\"number\"\n min={0}\n value={readingMins}\n onChange={(e) => setReadingMins(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Topic</Label>\n <Select value={pickedTopic} onValueChange={setPickedTopic}>\n <SelectTrigger>\n <SelectValue placeholder=\"Choose a topic (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(topicsQuery.data?.items ?? []).map((topic) => (\n <SelectItem key={topic.id} value={topic.id}>\n {topic.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-summary\">Summary</Label>\n <Textarea\n id=\"edit-summary\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-body\">Body</Label>\n <Textarea id=\"edit-body\" value={body} onChange={(e) => setBody(e.target.value)} rows={6} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-langs\">Languages</Label>\n <TagInput id=\"edit-langs\" value={languages} onChange={setLanguages} preserveCase />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-tags\">Tags</Label>\n <TagInput id=\"edit-tags\" value={tags} onChange={setTags} />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={updateStory.isPending}>\n Save changes\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"StoryDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/stories/StoryDetailPage.tsx"],"sourcesContent":["/**\n * StoryDetailPage — a single story with its editorial lifecycle, citations and\n * linked discussions.\n * @module theculturepost/pages/stories\n *\n * Reads `tcpStory` (+ its citations). Writes `tcpTransitionStory` (validated\n * lifecycle), `tcpUpdateStory` (edit), `tcpAddCitation` / `tcpVerifyCitation`\n * (via {@link CitationList}). Person references are id-only, so we render \"You\"\n * / \"Contributor\".\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n BookOpen,\n Clock,\n Languages,\n Tag,\n Pencil,\n Plus,\n GitBranch,\n MessagesSquare,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { CoverImage } from '../../components/CoverImage';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { AuthorLabel } from '../../components/AuthorLabel';\nimport { CitationList } from '../../components/CitationList';\nimport { DiscussionCard } from '../../components/DiscussionCard';\nimport { AddCitationDialog } from '../../components/AddCitationDialog';\nimport { OpenDiscussionDialog } from '../../components/OpenDiscussionDialog';\nimport { TagInput } from '../../components/TagInput';\nimport { FileUploadField } from '../../components/FileUploadField';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport {\n useTcpStory,\n useTcpTransitionStory,\n useTcpUpdateStory,\n useTcpTopics,\n useTcpDiscussions,\n FILE_FOLDERS,\n} from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpStory, TcpStoryFormat, TcpStoryStatus } from '../../types';\n\nconst NEXT_STATUSES: Record<TcpStoryStatus, TcpStoryStatus[]> = {\n IDEA: ['DRAFT'],\n DRAFT: ['IN_REVIEW'],\n IN_REVIEW: ['PUBLISHED', 'DRAFT'],\n PUBLISHED: ['ARCHIVED'],\n ARCHIVED: ['PUBLISHED'],\n};\n\nconst TRANSITION_LABEL: Record<TcpStoryStatus, string> = {\n IDEA: 'Move to Idea',\n DRAFT: 'Move to Draft',\n IN_REVIEW: 'Send to Review',\n PUBLISHED: 'Publish',\n ARCHIVED: 'Archive',\n};\n\nexport function StoryDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: story, isLoading, isError, refetch } = useTcpStory(id);\n const transition = useTcpTransitionStory();\n\n const [citationOpen, setCitationOpen] = useState(false);\n const [discussionOpen, setDiscussionOpen] = useState(false);\n const [editOpen, setEditOpen] = useState(false);\n\n const discussionsQuery = useTcpDiscussions({ storyId: id, pagination: { limit: 20 } });\n const linkedDiscussions = discussionsQuery.data?.items ?? [];\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('stories')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Story\" actions={backButton}>\n <LoadingView label=\"Loading story…\" />\n </PageLayout>\n );\n }\n\n if (isError || !story) {\n return (\n <PageLayout title=\"Story\" actions={backButton}>\n <ErrorView\n title=\"Story not found\"\n message=\"This story may have been archived, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const nextStatuses = NEXT_STATUSES[story.status] ?? [];\n\n const doTransition = async (status: TcpStoryStatus) => {\n try {\n await transition.mutateAsync({ id: story.id, status });\n notify.success('Story updated', `Moved to ${formatEnumLabel(status)}`);\n } catch (err) {\n notify.error('Could not update the story', describeError(err));\n }\n };\n\n return (\n <PageLayout\n title={story.title}\n actions={\n <div className=\"flex items-center gap-2\">\n {backButton}\n <Button variant=\"outline\" onClick={() => setEditOpen(true)}>\n <Pencil className=\"mr-1.5 h-4 w-4\" /> Edit\n </Button>\n </div>\n }\n >\n <article className=\"mx-auto flex max-w-3xl flex-col gap-6\">\n {/* Hero cover */}\n <CoverImage\n fileId={story.cover?.id}\n alt={story.title}\n className=\"aspect-[16/9] w-full\"\n icon={<BookOpen className=\"h-10 w-10\" />}\n />\n\n {/* Meta */}\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={story.format} />\n <StatusBadge status={story.status} />\n {story.topic && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`topics/${story.topic!.id}`)}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary hover:text-text-primary\"\n >\n <Tag className=\"h-3 w-3\" /> {story.topic.name}\n </button>\n )}\n </div>\n\n {story.summary && (\n <p className=\"text-lg leading-relaxed text-text-secondary\">{story.summary}</p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1.5 text-sm text-text-tertiary\">\n <AuthorLabel actorId={story.authorId} prefix=\"By\" withIcon />\n {story.editorId && <AuthorLabel actorId={story.editorId} prefix=\"Edited by\" />}\n {story.readingMins > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-4 w-4\" /> {formatNumber(story.readingMins)} min read\n </span>\n )}\n {story.languages.length > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Languages className=\"h-4 w-4\" /> {story.languages.map(formatEnumLabel).join(', ')}\n </span>\n )}\n </div>\n\n {story.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\">\n {story.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n </div>\n\n {/* Editorial lifecycle */}\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex items-center gap-2\">\n <GitBranch className=\"h-4 w-4 text-text-tertiary\" />\n <h2 className=\"text-sm font-semibold text-text-primary\">Editorial lifecycle</h2>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Current stage:{' '}\n <span className=\"font-medium text-text-primary\">{formatEnumLabel(story.status)}</span>\n </p>\n {nextStatuses.length > 0 ? (\n <div className=\"mt-3 flex flex-wrap gap-2\">\n {nextStatuses.map((status) => (\n <Button\n key={status}\n size=\"sm\"\n variant={status === 'PUBLISHED' ? 'default' : 'outline'}\n onClick={() => doTransition(status)}\n disabled={transition.isPending}\n >\n {TRANSITION_LABEL[status]}\n </Button>\n ))}\n </div>\n ) : (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n This story is at the end of its lifecycle.\n </p>\n )}\n </GlassCard>\n\n {/* Body */}\n {story.body ? (\n <div className=\"whitespace-pre-wrap text-base leading-relaxed text-text-primary\">\n {story.body}\n </div>\n ) : (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-6 text-center text-sm text-text-tertiary\">\n This story has no body yet. Use Edit to write it.\n </p>\n )}\n\n {/* Citations */}\n <Section\n title=\"Citations\"\n description=\"Verifiable sources that ground this story.\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setCitationOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add citation\n </Button>\n }\n >\n <CitationList storyId={story.id} />\n </Section>\n\n {/* Linked discussions */}\n <Section\n title=\"Discussions\"\n description=\"Conversations anchored to this story.\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setDiscussionOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Open discussion\n </Button>\n }\n >\n {discussionsQuery.isLoading ? (\n <LoadingView label=\"Loading discussions…\" />\n ) : linkedDiscussions.length === 0 ? (\n <EmptyState\n icon={<MessagesSquare className=\"h-9 w-9\" />}\n title=\"No discussions yet\"\n description=\"Open a discussion to invite perspectives on this story.\"\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {linkedDiscussions.map((discussion) => (\n <DiscussionCard\n key={discussion.id}\n discussion={discussion}\n onOpen={() => navigateTo(`discussions/${discussion.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </article>\n\n <AddCitationDialog open={citationOpen} onOpenChange={setCitationOpen} storyId={story.id} />\n <OpenDiscussionDialog\n open={discussionOpen}\n onOpenChange={setDiscussionOpen}\n storyId={story.id}\n onCreated={(discussion) => navigateTo(`discussions/${discussion.id}`)}\n />\n <EditStoryDialog open={editOpen} onOpenChange={setEditOpen} story={story} />\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Edit story dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nconst FORMATS: TcpStoryFormat[] = [\n 'ARTICLE',\n 'ESSAY',\n 'EXPLAINER',\n 'ORAL_HISTORY',\n 'PHOTO_STORY',\n 'TIMELINE',\n];\n\nfunction EditStoryDialog({\n open,\n onOpenChange,\n story,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n story: TcpStory;\n}) {\n const updateStory = useTcpUpdateStory();\n const topicsQuery = useTcpTopics({ pagination: { limit: 100 } });\n\n const [title, setTitle] = useState(story.title);\n const [summary, setSummary] = useState(story.summary ?? '');\n const [body, setBody] = useState(story.body ?? '');\n const [format, setFormat] = useState<TcpStoryFormat>(story.format);\n const [pickedTopic, setPickedTopic] = useState(story.topicId ?? '');\n const [languages, setLanguages] = useState<string[]>(story.languages);\n const [tags, setTags] = useState<string[]>(story.tags);\n const [readingMins, setReadingMins] = useState(\n story.readingMins ? String(story.readingMins) : ''\n );\n const [coverFileId, setCoverFileId] = useState<string | null>(story.cover?.id ?? null);\n const [error, setError] = useState<string | null>(null);\n\n // Reset local state whenever a different story is opened for editing.\n const storyKey = useMemo(() => `${story.id}:${story.updatedAt}`, [story.id, story.updatedAt]);\n const [seededKey, setSeededKey] = useState(storyKey);\n if (seededKey !== storyKey) {\n setSeededKey(storyKey);\n setTitle(story.title);\n setSummary(story.summary ?? '');\n setBody(story.body ?? '');\n setFormat(story.format);\n setPickedTopic(story.topicId ?? '');\n setLanguages(story.languages);\n setTags(story.tags);\n setReadingMins(story.readingMins ? String(story.readingMins) : '');\n setCoverFileId(story.cover?.id ?? null);\n setError(null);\n }\n\n const submit = async () => {\n if (!title.trim()) {\n setError('A title is required.');\n return;\n }\n try {\n await updateStory.mutateAsync({\n id: story.id,\n input: {\n title: title.trim(),\n summary: summary.trim() || null,\n body: body.trim() || null,\n format,\n topicId: pickedTopic || null,\n languages,\n tags,\n readingMins: readingMins ? Number(readingMins) : null,\n coverFileId,\n },\n });\n notify.success('Story updated', title.trim());\n onOpenChange(false);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit story</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Refine the story. The URL slug is fixed once created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex max-h-[62vh] flex-col gap-4 overflow-y-auto py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Cover image</Label>\n <FileUploadField\n folder={FILE_FOLDERS.covers}\n accept=\"image/*\"\n label=\"Upload cover\"\n fileId={coverFileId}\n onUploaded={setCoverFileId}\n onCleared={() => setCoverFileId(null)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-title\">Title</Label>\n <Input id=\"edit-title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"grid gap-1.5\">\n <Label>Format</Label>\n <Select value={format} onValueChange={(v) => setFormat(v as TcpStoryFormat)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {FORMATS.map((f) => (\n <SelectItem key={f} value={f}>\n {formatEnumLabel(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-reading\">Reading time (min)</Label>\n <Input\n id=\"edit-reading\"\n type=\"number\"\n min={0}\n value={readingMins}\n onChange={(e) => setReadingMins(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Topic</Label>\n <Select value={pickedTopic} onValueChange={setPickedTopic}>\n <SelectTrigger>\n <SelectValue placeholder=\"Choose a topic (optional)\" />\n </SelectTrigger>\n <SelectContent>\n {(topicsQuery.data?.items ?? []).map((topic) => (\n <SelectItem key={topic.id} value={topic.id}>\n {topic.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-summary\">Summary</Label>\n <Textarea\n id=\"edit-summary\"\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n rows={2}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-body\">Body</Label>\n <Textarea\n id=\"edit-body\"\n value={body}\n onChange={(e) => setBody(e.target.value)}\n rows={6}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-langs\">Languages</Label>\n <TagInput id=\"edit-langs\" value={languages} onChange={setLanguages} preserveCase />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-tags\">Tags</Label>\n <TagInput id=\"edit-tags\" value={tags} onChange={setTags} />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={updateStory.isPending}>\n Save changes\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TopicDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/topics/TopicDetailPage.tsx"],"sourcesContent":["/**\n * TopicDetailPage — a single topic and the stories curated under it.\n * @module theculturepost/pages/topics\n *\n * Reads `tcpTopic` (with its stories). Writes `tcpUpdateTopic` (edit + feature\n * toggle) and `tcpArchiveTopic`. Stories are drawn from the topic's own\n * `stories` field.\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Compass,\n Sparkles,\n Pencil,\n Archive,\n Plus,\n BookOpen,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { StoryCard } from '../../components/StoryCard';\nimport { CreateStoryDialog } from '../../components/CreateStoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useTcpTopic, useTcpUpdateTopic, useTcpArchiveTopic } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpTopic, TcpTopicCategory } from '../../types';\n\nconst CATEGORIES: TcpTopicCategory[] = [\n 'CULTURE_AND_IDENTITY',\n 'RELIGION_AND_FAITH',\n 'HISTORY',\n 'HERITAGE_AND_ARCHIVES',\n 'FESTIVALS_AND_RITUALS',\n 'MIGRATION_AND_DIASPORA',\n 'ORAL_HISTORIES',\n 'PHILOSOPHY_AND_THOUGHT',\n 'VALUES_AND_ETHICS',\n];\n\nexport function TopicDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: topic, isLoading, isError, refetch } = useTcpTopic(id);\n const updateTopic = useTcpUpdateTopic();\n const archiveTopic = useTcpArchiveTopic();\n\n const [editOpen, setEditOpen] = useState(false);\n const [createStoryOpen, setCreateStoryOpen] = useState(false);\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('topics')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Topic\" actions={backButton}>\n <LoadingView label=\"Loading topic…\" />\n </PageLayout>\n );\n }\n\n if (isError || !topic) {\n return (\n <PageLayout title=\"Topic\" actions={backButton}>\n <ErrorView\n title=\"Topic not found\"\n message=\"This topic may have been archived, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const stories = topic.stories ?? [];\n\n const toggleFeatured = async () => {\n try {\n await updateTopic.mutateAsync({ id: topic.id, input: { isFeatured: !topic.isFeatured } });\n notify.success(topic.isFeatured ? 'Removed from featured' : 'Topic featured');\n } catch (err) {\n notify.error('Could not update topic', describeError(err));\n }\n };\n\n const doArchive = async () => {\n try {\n await archiveTopic.mutateAsync(topic.id);\n notify.success('Topic archived');\n navigateTo('topics');\n } catch (err) {\n notify.error('Could not archive topic', describeError(err));\n }\n };\n\n return (\n <PageLayout\n title={topic.name}\n actions={\n <div className=\"flex items-center gap-2\">\n {backButton}\n <Button variant=\"outline\" size=\"sm\" onClick={toggleFeatured} disabled={updateTopic.isPending}>\n <Sparkles className=\"mr-1.5 h-4 w-4\" />\n {topic.isFeatured ? 'Unfeature' : 'Feature'}\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditOpen(true)}>\n <Pencil className=\"mr-1.5 h-4 w-4\" /> Edit\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={doArchive}\n disabled={archiveTopic.isPending}\n className=\"text-status-error-text\"\n >\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n </div>\n }\n >\n <div className=\"mx-auto flex max-w-5xl flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-6\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Compass className=\"h-6 w-6\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={topic.category} />\n {topic.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text\">\n <Sparkles className=\"h-3 w-3\" /> Featured\n </span>\n )}\n </div>\n {topic.tradition && (\n <p className=\"mt-1.5 text-sm text-text-secondary\">{topic.tradition}</p>\n )}\n </div>\n </div>\n {topic.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {topic.description}\n </p>\n )}\n </GlassCard>\n\n <Section\n title=\"Stories in this topic\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setCreateStoryOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n >\n {stories.length === 0 ? (\n <EmptyState\n icon={<BookOpen className=\"h-9 w-9\" />}\n title=\"No stories yet\"\n description=\"Draft the first story under this topic.\"\n action={\n <Button onClick={() => setCreateStoryOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-5 sm:grid-cols-2 xl:grid-cols-3\">\n {stories.map((story) => (\n <StoryCard\n key={story.id}\n story={story}\n onOpen={() => navigateTo(`stories/${story.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n\n <CreateStoryDialog\n open={createStoryOpen}\n onOpenChange={setCreateStoryOpen}\n topicId={topic.id}\n onCreated={(story) => navigateTo(`stories/${story.id}`)}\n />\n <EditTopicDialog open={editOpen} onOpenChange={setEditOpen} topic={topic} />\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Edit topic dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nfunction EditTopicDialog({\n open,\n onOpenChange,\n topic,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n topic: TcpTopic;\n}) {\n const updateTopic = useTcpUpdateTopic();\n const [name, setName] = useState(topic.name);\n const [tradition, setTradition] = useState(topic.tradition ?? '');\n const [description, setDescription] = useState(topic.description ?? '');\n const [category, setCategory] = useState<TcpTopicCategory>(topic.category);\n const [error, setError] = useState<string | null>(null);\n\n const topicKey = useMemo(() => `${topic.id}:${topic.updatedAt}`, [topic.id, topic.updatedAt]);\n const [seededKey, setSeededKey] = useState(topicKey);\n if (seededKey !== topicKey) {\n setSeededKey(topicKey);\n setName(topic.name);\n setTradition(topic.tradition ?? '');\n setDescription(topic.description ?? '');\n setCategory(topic.category);\n setError(null);\n }\n\n const submit = async () => {\n if (!name.trim()) {\n setError('A topic name is required.');\n return;\n }\n try {\n await updateTopic.mutateAsync({\n id: topic.id,\n input: {\n name: name.trim(),\n tradition: tradition.trim() || null,\n description: description.trim() || null,\n category,\n },\n });\n notify.success('Topic updated', name.trim());\n onOpenChange(false);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit topic</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n The URL slug is fixed once created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-topic-name\">Name</Label>\n <Input id=\"edit-topic-name\" value={name} onChange={(e) => setName(e.target.value)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as TcpTopicCategory)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CATEGORIES.map((c) => (\n <SelectItem key={c} value={c}>\n {formatEnumLabel(c)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-topic-tradition\">Tradition</Label>\n <Input\n id=\"edit-topic-tradition\"\n value={tradition}\n onChange={(e) => setTradition(e.target.value)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-topic-desc\">Description</Label>\n <Textarea\n id=\"edit-topic-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={3}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={updateTopic.isPending}>\n Save changes\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TopicDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/topics/TopicDetailPage.tsx"],"sourcesContent":["/**\n * TopicDetailPage — a single topic and the stories curated under it.\n * @module theculturepost/pages/topics\n *\n * Reads `tcpTopic` (with its stories). Writes `tcpUpdateTopic` (edit + feature\n * toggle) and `tcpArchiveTopic`. Stories are drawn from the topic's own\n * `stories` field.\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Compass, Sparkles, Pencil, Archive, Plus, BookOpen } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { StoryCard } from '../../components/StoryCard';\nimport { CreateStoryDialog } from '../../components/CreateStoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useTcpTopic, useTcpUpdateTopic, useTcpArchiveTopic } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpTopic, TcpTopicCategory } from '../../types';\n\nconst CATEGORIES: TcpTopicCategory[] = [\n 'CULTURE_AND_IDENTITY',\n 'RELIGION_AND_FAITH',\n 'HISTORY',\n 'HERITAGE_AND_ARCHIVES',\n 'FESTIVALS_AND_RITUALS',\n 'MIGRATION_AND_DIASPORA',\n 'ORAL_HISTORIES',\n 'PHILOSOPHY_AND_THOUGHT',\n 'VALUES_AND_ETHICS',\n];\n\nexport function TopicDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: topic, isLoading, isError, refetch } = useTcpTopic(id);\n const updateTopic = useTcpUpdateTopic();\n const archiveTopic = useTcpArchiveTopic();\n\n const [editOpen, setEditOpen] = useState(false);\n const [createStoryOpen, setCreateStoryOpen] = useState(false);\n\n const backButton = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('topics')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Topic\" actions={backButton}>\n <LoadingView label=\"Loading topic…\" />\n </PageLayout>\n );\n }\n\n if (isError || !topic) {\n return (\n <PageLayout title=\"Topic\" actions={backButton}>\n <ErrorView\n title=\"Topic not found\"\n message=\"This topic may have been archived, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n const stories = topic.stories ?? [];\n\n const toggleFeatured = async () => {\n try {\n await updateTopic.mutateAsync({ id: topic.id, input: { isFeatured: !topic.isFeatured } });\n notify.success(topic.isFeatured ? 'Removed from featured' : 'Topic featured');\n } catch (err) {\n notify.error('Could not update topic', describeError(err));\n }\n };\n\n const doArchive = async () => {\n try {\n await archiveTopic.mutateAsync(topic.id);\n notify.success('Topic archived');\n navigateTo('topics');\n } catch (err) {\n notify.error('Could not archive topic', describeError(err));\n }\n };\n\n return (\n <PageLayout\n title={topic.name}\n actions={\n <div className=\"flex items-center gap-2\">\n {backButton}\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={toggleFeatured}\n disabled={updateTopic.isPending}\n >\n <Sparkles className=\"mr-1.5 h-4 w-4\" />\n {topic.isFeatured ? 'Unfeature' : 'Feature'}\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditOpen(true)}>\n <Pencil className=\"mr-1.5 h-4 w-4\" /> Edit\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={doArchive}\n disabled={archiveTopic.isPending}\n className=\"text-status-error-text\"\n >\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n </div>\n }\n >\n <div className=\"mx-auto flex max-w-5xl flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-6\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Compass className=\"h-6 w-6\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={topic.category} />\n {topic.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text\">\n <Sparkles className=\"h-3 w-3\" /> Featured\n </span>\n )}\n </div>\n {topic.tradition && (\n <p className=\"mt-1.5 text-sm text-text-secondary\">{topic.tradition}</p>\n )}\n </div>\n </div>\n {topic.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {topic.description}\n </p>\n )}\n </GlassCard>\n\n <Section\n title=\"Stories in this topic\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setCreateStoryOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n >\n {stories.length === 0 ? (\n <EmptyState\n icon={<BookOpen className=\"h-9 w-9\" />}\n title=\"No stories yet\"\n description=\"Draft the first story under this topic.\"\n action={\n <Button onClick={() => setCreateStoryOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-5 sm:grid-cols-2 xl:grid-cols-3\">\n {stories.map((story) => (\n <StoryCard\n key={story.id}\n story={story}\n onOpen={() => navigateTo(`stories/${story.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n\n <CreateStoryDialog\n open={createStoryOpen}\n onOpenChange={setCreateStoryOpen}\n topicId={topic.id}\n onCreated={(story) => navigateTo(`stories/${story.id}`)}\n />\n <EditTopicDialog open={editOpen} onOpenChange={setEditOpen} topic={topic} />\n </PageLayout>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Edit topic dialog (local — only used here)\n// ---------------------------------------------------------------------------\n\nfunction EditTopicDialog({\n open,\n onOpenChange,\n topic,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n topic: TcpTopic;\n}) {\n const updateTopic = useTcpUpdateTopic();\n const [name, setName] = useState(topic.name);\n const [tradition, setTradition] = useState(topic.tradition ?? '');\n const [description, setDescription] = useState(topic.description ?? '');\n const [category, setCategory] = useState<TcpTopicCategory>(topic.category);\n const [error, setError] = useState<string | null>(null);\n\n const topicKey = useMemo(() => `${topic.id}:${topic.updatedAt}`, [topic.id, topic.updatedAt]);\n const [seededKey, setSeededKey] = useState(topicKey);\n if (seededKey !== topicKey) {\n setSeededKey(topicKey);\n setName(topic.name);\n setTradition(topic.tradition ?? '');\n setDescription(topic.description ?? '');\n setCategory(topic.category);\n setError(null);\n }\n\n const submit = async () => {\n if (!name.trim()) {\n setError('A topic name is required.');\n return;\n }\n try {\n await updateTopic.mutateAsync({\n id: topic.id,\n input: {\n name: name.trim(),\n tradition: tradition.trim() || null,\n description: description.trim() || null,\n category,\n },\n });\n notify.success('Topic updated', name.trim());\n onOpenChange(false);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit topic</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n The URL slug is fixed once created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-topic-name\">Name</Label>\n <Input id=\"edit-topic-name\" value={name} onChange={(e) => setName(e.target.value)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as TcpTopicCategory)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CATEGORIES.map((c) => (\n <SelectItem key={c} value={c}>\n {formatEnumLabel(c)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-topic-tradition\">Tradition</Label>\n <Input\n id=\"edit-topic-tradition\"\n value={tradition}\n onChange={(e) => setTradition(e.target.value)}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"edit-topic-desc\">Description</Label>\n <Textarea\n id=\"edit-topic-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={3}\n />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={updateTopic.isPending}>\n Save changes\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-theculturepost",
3
- "version": "2026.704.1",
3
+ "version": "2026.704.2",
4
4
  "description": "Burdenoff TheCulturePost Microfrontend",
5
5
  "type": "module",
6
6
  "files": [