@burdenoff/microfe-theculturepost 2026.726.1 → 2026.801.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/theculturepost/TheCulturePostRoutes.js +1 -1
- package/dist/theculturepost/TheCulturePostRoutes.js.map +1 -1
- package/dist/theculturepost/context/TheCulturePostContext.js +1 -1
- package/dist/theculturepost/context/TheCulturePostContext.js.map +1 -1
- package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js +1 -1
- package/dist/theculturepost/pages/archive/ArchiveItemDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/archive/CollectionDetailPage.js +1 -1
- package/dist/theculturepost/pages/archive/CollectionDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js +1 -1
- package/dist/theculturepost/pages/discussions/DiscussionDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/stories/StoryDetailPage.js +1 -1
- package/dist/theculturepost/pages/stories/StoryDetailPage.js.map +1 -1
- package/dist/theculturepost/pages/topics/TopicDetailPage.js +1 -1
- package/dist/theculturepost/pages/topics/TopicDetailPage.js.map +1 -1
- package/package.json +6 -6
|
@@ -3,7 +3,7 @@ import { usePageAnalytics as t } from "./hooks/usePageAnalytics.js";
|
|
|
3
3
|
import { HomePage as n } from "./pages/HomePage.js";
|
|
4
4
|
import { Suspense as r, lazy as i } from "react";
|
|
5
5
|
import { FeatureGate as a } from "@burdenoff/fe-libs/shared/feature-flags";
|
|
6
|
-
import { Navigate as o, Route as s, Routes as c, useLocation as l } from "react-router
|
|
6
|
+
import { Navigate as o, Route as s, Routes as c, useLocation as l } from "react-router";
|
|
7
7
|
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
8
8
|
import { Spinner as f } from "@burdenoff/fe-libs/ui";
|
|
9
9
|
//#region src/theculturepost/TheCulturePostRoutes.tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TheCulturePostRoutes.js","names":[],"sources":["../../src/theculturepost/TheCulturePostRoutes.tsx"],"sourcesContent":["/**\n * TheCulturePost Routes\n * @module theculturepost\n *\n * Domain routes. The editorial home page is eager-loaded (primary landing); all\n * other pages are lazy-loaded. Every route maps to a real `tcp*` backend\n * capability.\n */\n\nimport { type FC, lazy, Suspense, type ReactElement } from 'react';\nimport { Routes, Route, Navigate, useLocation } from 'react-router
|
|
1
|
+
{"version":3,"file":"TheCulturePostRoutes.js","names":[],"sources":["../../src/theculturepost/TheCulturePostRoutes.tsx"],"sourcesContent":["/**\n * TheCulturePost Routes\n * @module theculturepost\n *\n * Domain routes. The editorial home page is eager-loaded (primary landing); all\n * other pages are lazy-loaded. Every route maps to a real `tcp*` backend\n * capability.\n */\n\nimport { type FC, lazy, Suspense, type ReactElement } from 'react';\nimport { Routes, Route, Navigate, useLocation } from 'react-router';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useTheCulturePost } from './context/TheCulturePostContext';\nimport { usePageAnalytics } from './hooks/usePageAnalytics';\n\nconst FEATURE = {\n home: 'theculturepost.home',\n overview: 'theculturepost.overview',\n stories: 'theculturepost.stories',\n topics: 'theculturepost.topics',\n timeline: 'theculturepost.timeline',\n discussions: 'theculturepost.discussions',\n archive: 'theculturepost.archive',\n saved: 'theculturepost.saved',\n} as const;\n\nfunction FeatureDisabledFallback(): ReactElement {\n return (\n <div className=\"flex min-h-[400px] flex-col items-center justify-center gap-3 text-text-secondary\">\n <span className=\"text-lg font-semibold text-text-primary\">Feature unavailable</span>\n <span className=\"text-sm\">This page is disabled in your workspace.</span>\n </div>\n );\n}\n\nfunction gated(element: ReactElement, flag: string): ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\n// Eager — primary landing\nimport { HomePage } from './pages/HomePage';\n\n// Stories\nconst StoriesPage = lazy(() =>\n import('./pages/stories/StoriesPage').then((m) => ({ default: m.StoriesPage }))\n);\nconst StoryDetailPage = lazy(() =>\n import('./pages/stories/StoryDetailPage').then((m) => ({ default: m.StoryDetailPage }))\n);\nconst SavedStoriesPage = lazy(() =>\n import('./pages/stories/SavedStoriesPage').then((m) => ({ default: m.SavedStoriesPage }))\n);\n\n// Topics\nconst TopicsPage = lazy(() =>\n import('./pages/topics/TopicsPage').then((m) => ({ default: m.TopicsPage }))\n);\nconst TopicDetailPage = lazy(() =>\n import('./pages/topics/TopicDetailPage').then((m) => ({ default: m.TopicDetailPage }))\n);\n\n// Discussions\nconst DiscussionsPage = lazy(() =>\n import('./pages/discussions/DiscussionsPage').then((m) => ({ default: m.DiscussionsPage }))\n);\nconst DiscussionDetailPage = lazy(() =>\n import('./pages/discussions/DiscussionDetailPage').then((m) => ({\n default: m.DiscussionDetailPage,\n }))\n);\n\n// Timeline\nconst TimelinePage = lazy(() =>\n import('./pages/timeline/TimelinePage').then((m) => ({ default: m.TimelinePage }))\n);\n\n// Governance / Overview\nconst GovernancePage = lazy(() =>\n import('./pages/governance/GovernancePage').then((m) => ({ default: m.GovernancePage }))\n);\n\n// Archive\nconst ArchivePage = lazy(() =>\n import('./pages/archive/ArchivePage').then((m) => ({ default: m.ArchivePage }))\n);\nconst CollectionDetailPage = lazy(() =>\n import('./pages/archive/CollectionDetailPage').then((m) => ({ default: m.CollectionDetailPage }))\n);\nconst ArchiveItemDetailPage = lazy(() =>\n import('./pages/archive/ArchiveItemDetailPage').then((m) => ({\n default: m.ArchiveItemDetailPage,\n }))\n);\n\nconst PageLoader: FC = () => (\n <div className=\"flex min-h-[400px] items-center justify-center\">\n <Spinner size=\"lg\" />\n </div>\n);\n\nexport const TheCulturePostRoutes: FC = () => {\n const { basePath, isStandalone } = useTheCulturePost();\n const location = useLocation();\n\n usePageAnalytics();\n\n const normalizedBasePath = basePath === '/' ? '' : (basePath ?? '').replace(/\\/$/, '');\n\n // Guard: only render if the current path matches basePath (integrated mode).\n if (normalizedBasePath && !isStandalone) {\n const pathMatches =\n location.pathname === normalizedBasePath ||\n location.pathname.startsWith(normalizedBasePath + '/');\n if (!pathMatches) {\n return null;\n }\n }\n\n return (\n <div className=\"flex min-h-full flex-col\">\n <Suspense fallback={<PageLoader />}>\n <Routes>\n <Route index element={gated(<HomePage />, FEATURE.home)} />\n\n <Route path=\"overview\" element={gated(<GovernancePage />, FEATURE.overview)} />\n\n <Route path=\"stories\" element={gated(<StoriesPage />, FEATURE.stories)} />\n <Route path=\"stories/:id\" element={gated(<StoryDetailPage />, FEATURE.stories)} />\n\n <Route path=\"saved\" element={gated(<SavedStoriesPage />, FEATURE.saved)} />\n\n <Route path=\"topics\" element={gated(<TopicsPage />, FEATURE.topics)} />\n <Route path=\"topics/:id\" element={gated(<TopicDetailPage />, FEATURE.topics)} />\n\n <Route path=\"timeline\" element={gated(<TimelinePage />, FEATURE.timeline)} />\n\n <Route path=\"discussions\" element={gated(<DiscussionsPage />, FEATURE.discussions)} />\n <Route\n path=\"discussions/:id\"\n element={gated(<DiscussionDetailPage />, FEATURE.discussions)}\n />\n\n <Route path=\"archive\" element={gated(<ArchivePage />, FEATURE.archive)} />\n <Route\n path=\"archive/collections/:id\"\n element={gated(<CollectionDetailPage />, FEATURE.archive)}\n />\n <Route\n path=\"archive/items/:id\"\n element={gated(<ArchiveItemDetailPage />, FEATURE.archive)}\n />\n\n <Route path=\"*\" element={<Navigate to=\"\" replace />} />\n </Routes>\n </Suspense>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AAgBA,IAAM,IAAU;CACd,MAAM;CACN,UAAU;CACV,SAAS;CACT,QAAQ;CACR,UAAU;CACV,aAAa;CACb,SAAS;CACT,OAAO;CACR;AAED,SAAS,IAAwC;AAC/C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,QAAD;GAAM,WAAU;aAA0C;GAA0B,CAAA,EACpF,kBAAC,QAAD;GAAM,WAAU;aAAU;GAA+C,CAAA,CACrE;;;AAIV,SAAS,EAAM,GAAuB,GAA4B;AAChE,QACE,kBAAC,GAAD;EAAmB;EAAM,cAAA;EAAa,UAAU,kBAAC,GAAD,EAA2B,CAAA;YACxE;EACW,CAAA;;AAQlB,IAAM,IAAc,QAClB,OAAO,kCAA+B,MAAM,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,CAChF,EACK,IAAkB,QACtB,OAAO,sCAAmC,MAAM,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CACxF,EACK,IAAmB,QACvB,OAAO,uCAAoC,MAAM,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAC1F,EAGK,IAAa,QACjB,OAAO,gCAA6B,MAAM,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,CAC7E,EACK,IAAkB,QACtB,OAAO,qCAAkC,MAAM,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CACvF,EAGK,IAAkB,QACtB,OAAO,0CAAuC,MAAM,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAC5F,EACK,IAAuB,QAC3B,OAAO,+CAA4C,MAAM,OAAO,EAC9D,SAAS,EAAE,sBACZ,EAAE,CACJ,EAGK,IAAe,QACnB,OAAO,oCAAiC,MAAM,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,CACnF,EAGK,IAAiB,QACrB,OAAO,wCAAqC,MAAM,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,CACzF,EAGK,IAAc,QAClB,OAAO,kCAA+B,MAAM,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,CAChF,EACK,IAAuB,QAC3B,OAAO,2CAAwC,MAAM,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAClG,EACK,IAAwB,QAC5B,OAAO,4CAAyC,MAAM,OAAO,EAC3D,SAAS,EAAE,uBACZ,EAAE,CACJ,EAEK,UACJ,kBAAC,OAAD;CAAK,WAAU;WACb,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;CACjB,CAAA,EAGK,UAAiC;CAC5C,IAAM,EAAE,aAAU,oBAAiB,GAAmB,EAChD,IAAW,GAAa;AAE9B,IAAkB;CAElB,IAAM,IAAqB,MAAa,MAAM,MAAM,KAAY,IAAI,QAAQ,OAAO,GAAG;AAYtF,QATI,KAAsB,CAAC,KAIrB,EAFF,EAAS,aAAa,KACtB,EAAS,SAAS,WAAW,IAAqB,IAAI,IAE/C,OAKT,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GAAU,UAAU,kBAAC,GAAD,EAAc,CAAA;aAChC,kBAAC,GAAD,EAAA,UAAA;IACE,kBAAC,GAAD;KAAO,OAAA;KAAM,SAAS,EAAM,kBAAC,GAAD,EAAY,CAAA,EAAE,EAAQ,KAAK;KAAI,CAAA;IAE3D,kBAAC,GAAD;KAAO,MAAK;KAAW,SAAS,EAAM,kBAAC,GAAD,EAAkB,CAAA,EAAE,EAAQ,SAAS;KAAI,CAAA;IAE/E,kBAAC,GAAD;KAAO,MAAK;KAAU,SAAS,EAAM,kBAAC,GAAD,EAAe,CAAA,EAAE,EAAQ,QAAQ;KAAI,CAAA;IAC1E,kBAAC,GAAD;KAAO,MAAK;KAAc,SAAS,EAAM,kBAAC,GAAD,EAAmB,CAAA,EAAE,EAAQ,QAAQ;KAAI,CAAA;IAElF,kBAAC,GAAD;KAAO,MAAK;KAAQ,SAAS,EAAM,kBAAC,GAAD,EAAoB,CAAA,EAAE,EAAQ,MAAM;KAAI,CAAA;IAE3E,kBAAC,GAAD;KAAO,MAAK;KAAS,SAAS,EAAM,kBAAC,GAAD,EAAc,CAAA,EAAE,EAAQ,OAAO;KAAI,CAAA;IACvE,kBAAC,GAAD;KAAO,MAAK;KAAa,SAAS,EAAM,kBAAC,GAAD,EAAmB,CAAA,EAAE,EAAQ,OAAO;KAAI,CAAA;IAEhF,kBAAC,GAAD;KAAO,MAAK;KAAW,SAAS,EAAM,kBAAC,GAAD,EAAgB,CAAA,EAAE,EAAQ,SAAS;KAAI,CAAA;IAE7E,kBAAC,GAAD;KAAO,MAAK;KAAc,SAAS,EAAM,kBAAC,GAAD,EAAmB,CAAA,EAAE,EAAQ,YAAY;KAAI,CAAA;IACtF,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,kBAAC,GAAD,EAAwB,CAAA,EAAE,EAAQ,YAAY;KAC7D,CAAA;IAEF,kBAAC,GAAD;KAAO,MAAK;KAAU,SAAS,EAAM,kBAAC,GAAD,EAAe,CAAA,EAAE,EAAQ,QAAQ;KAAI,CAAA;IAC1E,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,kBAAC,GAAD,EAAwB,CAAA,EAAE,EAAQ,QAAQ;KACzD,CAAA;IACF,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,kBAAC,GAAD,EAAyB,CAAA,EAAE,EAAQ,QAAQ;KAC1D,CAAA;IAEF,kBAAC,GAAD;KAAO,MAAK;KAAI,SAAS,kBAAC,GAAD;MAAU,IAAG;MAAG,SAAA;MAAU,CAAA;KAAI,CAAA;IAChD,EAAA,CAAA;GACA,CAAA;EACP,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { joinPath as e } from "../utils/navigation.js";
|
|
2
2
|
import { createContext as t, useCallback as n, useContext as r, useMemo as i } from "react";
|
|
3
|
-
import { MemoryRouter as a, useNavigate as o } from "react-router
|
|
3
|
+
import { MemoryRouter as a, useNavigate as o } from "react-router";
|
|
4
4
|
import { jsx as s } from "react/jsx-runtime";
|
|
5
5
|
//#region src/theculturepost/context/TheCulturePostContext.tsx
|
|
6
6
|
var c = t(null);
|
|
@@ -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
|
|
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';\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"}
|
|
@@ -12,7 +12,7 @@ import { DecorativeDivider as p } from "../../components/DecorativeDivider.js";
|
|
|
12
12
|
import { IMAGE_KINDS as m, KIND_ICON as h } from "../../components/kindIcon.js";
|
|
13
13
|
import { createElement as g, useEffect as _, useState as v } from "react";
|
|
14
14
|
import { ArrowLeft as y, CalendarClock as b, Clock as x, Download as S, Globe as C, Landmark as w, Languages as T, Library as E, Loader2 as D, Lock as O, Maximize2 as k, Minus as A, Plus as j, Quote as M, Tag as N, Users as P, X as F } from "lucide-react";
|
|
15
|
-
import { useParams as I } from "react-router
|
|
15
|
+
import { useParams as I } from "react-router";
|
|
16
16
|
import { Fragment as L, jsx as R, jsxs as z } from "react/jsx-runtime";
|
|
17
17
|
import { Button as B, GlassCard as V, Input as H, ResponsiveDialog as U, ResponsiveDialogContent as W, ResponsiveDialogDescription as G, ResponsiveDialogFooter as K, ResponsiveDialogHeader as q, ResponsiveDialogTitle as J } from "@burdenoff/fe-libs/ui";
|
|
18
18
|
//#region src/theculturepost/pages/archive/ArchiveItemDetailPage.tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArchiveItemDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/archive/ArchiveItemDetailPage.tsx"],"sourcesContent":["/**\n * ArchiveItemDetailPage — a single archived artifact.\n * @module theculturepost/pages/archive\n *\n * Reads `tcpArchiveItem` (with its parent collection). Two columns: a media\n * viewer with CSS zoom + a full-screen lightbox (the schema exposes a SINGLE\n * `file` — never a thumbnail-strip gallery) and a details panel. \"Cite in a\n * story\" opens a story picker and writes an `ARCHIVAL_RECORD` citation via\n * `tcpAddCitation`. There is no bookmark/share (the schema has neither).\n */\n\nimport { createElement, useEffect, useState } from 'react';\nimport {\n ArrowLeft,\n Landmark,\n Globe,\n CalendarClock,\n Languages,\n Download,\n Library,\n Tag,\n Quote,\n Maximize2,\n Plus,\n Minus,\n X,\n Loader2,\n Users,\n Lock,\n Clock,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useParams } from 'react-router-dom';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DecorativeDivider } from '../../components/DecorativeDivider';\nimport { KIND_ICON, IMAGE_KINDS } from '../../components/kindIcon';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useTcpArchiveItem, useFileUrl, useTcpStories, useTcpAddCitation } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { cn } from '../../utils/cn';\nimport { formatDate, formatEnumLabel } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpArchiveItem, TcpStory, TcpAccessLevel } from '../../types';\n\nconst ACCESS_ICON: Record<TcpAccessLevel, LucideIcon> = {\n PUBLIC: Globe,\n COMMUNITY: Users,\n RESTRICTED: Lock,\n EMBARGOED: Clock,\n};\n\nfunction Shell({ onBack, children }: { onBack: () => void; children: React.ReactNode }) {\n return (\n <div className=\"flex h-full flex-col\">\n <div className=\"flex-1 overflow-auto\">\n <div className=\"mx-auto w-full max-w-6xl p-4 sm:p-6\">\n <button\n type=\"button\"\n onClick={onBack}\n className=\"inline-flex items-center gap-1.5 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" /> Archive\n </button>\n <div className=\"mt-4\">{children}</div>\n </div>\n </div>\n </div>\n );\n}\n\nexport function ArchiveItemDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: item, isLoading, isError, refetch } = useTcpArchiveItem(id);\n const [citeOpen, setCiteOpen] = useState(false);\n\n const goBack = () => navigateTo('archive');\n\n if (isLoading) {\n return (\n <Shell onBack={goBack}>\n <LoadingView label=\"Loading artifact…\" />\n </Shell>\n );\n }\n\n if (isError || !item) {\n return (\n <Shell onBack={goBack}>\n <ErrorView\n title=\"Artifact not found\"\n message=\"This artifact may have been archived, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </Shell>\n );\n }\n\n const collection = item.collection;\n const AccessIcon = ACCESS_ICON[item.accessLevel];\n const KindIcon = KIND_ICON[item.kind];\n\n return (\n <Shell onBack={goBack}>\n <div className=\"grid gap-6 lg:grid-cols-[1.3fr_1fr]\">\n <MediaViewer item={item} />\n\n <div className=\"flex min-w-0 flex-col gap-5\">\n <h1 className=\"font-display text-3xl font-semibold leading-tight text-text-primary\">\n {item.title}\n </h1>\n\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n status={item.kind}\n icon={createElement(KindIcon, { className: 'h-3 w-3' })}\n />\n <StatusBadge\n status={item.accessLevel}\n icon={createElement(AccessIcon, { className: 'h-3 w-3' })}\n />\n </div>\n\n {item.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {item.description}\n </p>\n )}\n\n <DecorativeDivider className=\"py-2\" />\n\n <dl className=\"flex flex-col gap-4\">\n {item.provenance && (\n <DetailRow icon={Landmark} label=\"Provenance\">\n {item.provenance}\n </DetailRow>\n )}\n {item.origin && (\n <DetailRow icon={Globe} label=\"Origin\">\n {item.origin}\n </DetailRow>\n )}\n {item.datedAt && (\n <DetailRow icon={CalendarClock} label=\"Date\">\n {formatDate(item.datedAt)}\n </DetailRow>\n )}\n {item.languages.length > 0 && (\n <DetailRow icon={Languages} label=\"Languages\">\n {item.languages.map(formatEnumLabel).join(', ')}\n </DetailRow>\n )}\n {item.tags.length > 0 && (\n <DetailRow icon={Tag} label=\"Tags\">\n <div className=\"flex flex-wrap gap-1.5\">\n {item.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 </DetailRow>\n )}\n </dl>\n\n {collection && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`archive/collections/${collection.id}`)}\n className=\"inline-flex items-center gap-2 self-start rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Library className=\"h-4 w-4 text-text-tertiary\" aria-hidden=\"true\" />{' '}\n {collection.name}\n </button>\n )}\n\n <Button className=\"w-full sm:w-auto\" onClick={() => setCiteOpen(true)}>\n <Quote className=\"mr-1.5 h-4 w-4\" /> Cite in a story\n </Button>\n </div>\n </div>\n\n <CiteInStoryDialog open={citeOpen} onOpenChange={setCiteOpen} item={item} />\n </Shell>\n );\n}\n\n// ---------------------------------------------------------------------------\n// DetailRow\n// ---------------------------------------------------------------------------\n\nfunction DetailRow({\n icon: Icon,\n label,\n children,\n}: {\n icon: LucideIcon;\n label: string;\n children: React.ReactNode;\n}) {\n return (\n <div className=\"flex items-start gap-3\">\n <Icon className=\"mt-0.5 h-4 w-4 shrink-0 text-accent\" aria-hidden=\"true\" />\n <dt className=\"w-24 shrink-0 text-sm text-text-tertiary\">{label}</dt>\n <dd className=\"min-w-0 flex-1 text-sm text-text-primary\">{children}</dd>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// MediaViewer — single file only (no gallery), CSS zoom + a full-screen lightbox.\n// ---------------------------------------------------------------------------\n\nfunction MediaViewer({ item }: { item: TcpArchiveItem }) {\n const { data: url } = useFileUrl(item.file?.id);\n const Icon = KIND_ICON[item.kind];\n const [zoom, setZoom] = useState(1);\n const [lightbox, setLightbox] = useState(false);\n const [imgFailed, setImgFailed] = useState(false);\n const showImage = IMAGE_KINDS.has(item.kind) && Boolean(url) && !imgFailed;\n\n useEffect(() => {\n if (!lightbox) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setLightbox(false);\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [lightbox]);\n\n const zoomIn = () => setZoom((z) => Math.min(4, Math.round((z + 0.5) * 10) / 10));\n const zoomOut = () => setZoom((z) => Math.max(1, Math.round((z - 0.5) * 10) / 10));\n\n return (\n <div className=\"flex flex-col gap-4\">\n <GlassCard treatment=\"plain\" className=\"p-3 sm:p-4\">\n <div\n className={cn(\n 'relative aspect-[4/3] w-full rounded-xl bg-bg-sunken',\n zoom > 1 ? 'overflow-auto' : 'overflow-hidden'\n )}\n >\n {showImage && url ? (\n <img\n src={url}\n alt={item.title}\n onError={() => setImgFailed(true)}\n style={{ transform: `scale(${zoom})`, transformOrigin: 'center center' }}\n className=\"h-full w-full object-contain transition-transform duration-200\"\n />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-3 text-text-tertiary\">\n <Icon className=\"h-16 w-16\" aria-hidden=\"true\" />\n <span className=\"text-sm font-medium\">{formatEnumLabel(item.kind)}</span>\n </div>\n )}\n\n {showImage && url && (\n <>\n <button\n type=\"button\"\n onClick={() => setLightbox(true)}\n aria-label=\"View full screen\"\n className=\"absolute right-3 top-3 z-10 flex h-9 w-9 items-center justify-center rounded-lg bg-bg-canvas/85 text-text-primary shadow-sm backdrop-blur-sm hover:bg-bg-canvas\"\n >\n <Maximize2 className=\"h-4 w-4\" />\n </button>\n <div className=\"absolute bottom-3 left-3 z-10 flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-canvas/85 text-text-primary shadow-sm backdrop-blur-sm\">\n <button\n type=\"button\"\n onClick={zoomIn}\n disabled={zoom >= 4}\n aria-label=\"Zoom in\"\n className=\"flex h-8 w-9 items-center justify-center hover:bg-bg-sunken disabled:opacity-40\"\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setZoom(1)}\n aria-label=\"Reset zoom\"\n className=\"border-y border-border-seam px-1 py-1 text-[10px] font-medium tabular-nums hover:bg-bg-sunken\"\n >\n {Math.round(zoom * 100)}%\n </button>\n <button\n type=\"button\"\n onClick={zoomOut}\n disabled={zoom <= 1}\n aria-label=\"Zoom out\"\n className=\"flex h-8 w-9 items-center justify-center hover:bg-bg-sunken disabled:opacity-40\"\n >\n <Minus className=\"h-4 w-4\" />\n </button>\n </div>\n </>\n )}\n </div>\n </GlassCard>\n\n {url && (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n download\n className=\"inline-flex items-center justify-center gap-2 self-start rounded-lg border border-border-seam bg-bg-surface px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Download className=\"h-4 w-4\" /> Download\n </a>\n )}\n\n {lightbox && url && (\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${item.title} — full view`}\n onClick={() => setLightbox(false)}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-canvas/95 p-4 backdrop-blur-sm\"\n >\n <button\n type=\"button\"\n onClick={() => setLightbox(false)}\n aria-label=\"Close full view\"\n className=\"absolute right-4 top-4 flex h-10 w-10 items-center justify-center rounded-full bg-bg-surface text-text-primary shadow-sm hover:bg-bg-sunken\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n <img\n src={url}\n alt={item.title}\n onClick={(e) => e.stopPropagation()}\n className=\"max-h-full max-w-full rounded-lg object-contain\"\n />\n </div>\n )}\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// CiteInStoryDialog — pick a story and attach this artifact as a citation.\n// ---------------------------------------------------------------------------\n\nfunction CiteInStoryDialog({\n open,\n onOpenChange,\n item,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n item: TcpArchiveItem;\n}) {\n const storiesQuery = useTcpStories({ pagination: { limit: 50 } });\n const addCitation = useTcpAddCitation();\n const [query, setQuery] = useState('');\n const [pendingId, setPendingId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const stories = storiesQuery.data?.items ?? [];\n const q = query.trim().toLowerCase();\n const filtered = q ? stories.filter((s) => s.title.toLowerCase().includes(q)) : stories;\n\n const cite = async (story: TcpStory) => {\n setPendingId(story.id);\n setError(null);\n try {\n await addCitation.mutateAsync({\n storyId: story.id,\n title: item.title,\n sourceType: 'ARCHIVAL_RECORD',\n provenance: item.provenance ?? null,\n excerpt: item.description ?? null,\n sourceFileId: item.file?.id ?? null,\n });\n notify.success('Cited in a story', story.title);\n onOpenChange(false);\n } catch (err) {\n setError(describeError(err));\n } finally {\n setPendingId(null);\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Cite in a story</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Attach “{item.title}” to a story as an archival-record citation.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex flex-col gap-3 py-2\">\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search stories…\"\n aria-label=\"Search stories\"\n />\n <div className=\"flex max-h-[50vh] flex-col gap-1.5 overflow-y-auto\">\n {storiesQuery.isLoading ? (\n <LoadingView label=\"Loading stories…\" className=\"min-h-[8rem]\" />\n ) : filtered.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-text-tertiary\">\n {stories.length === 0\n ? 'No stories yet — draft a story first.'\n : 'No stories match your search.'}\n </p>\n ) : (\n filtered.map((story) => (\n <button\n key={story.id}\n type=\"button\"\n onClick={() => cite(story)}\n disabled={addCitation.isPending}\n className=\"flex items-center justify-between gap-3 rounded-xl border border-border-seam bg-bg-surface px-3 py-2.5 text-left transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {story.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-tertiary\">\n {story.topic?.name ?? formatEnumLabel(story.status)}\n </span>\n </span>\n {pendingId === story.id ? (\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin text-text-tertiary\" />\n ) : (\n <Quote className=\"h-4 w-4 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n )}\n </button>\n ))\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 Close\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ArchiveItemDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/archive/ArchiveItemDetailPage.tsx"],"sourcesContent":["/**\n * ArchiveItemDetailPage — a single archived artifact.\n * @module theculturepost/pages/archive\n *\n * Reads `tcpArchiveItem` (with its parent collection). Two columns: a media\n * viewer with CSS zoom + a full-screen lightbox (the schema exposes a SINGLE\n * `file` — never a thumbnail-strip gallery) and a details panel. \"Cite in a\n * story\" opens a story picker and writes an `ARCHIVAL_RECORD` citation via\n * `tcpAddCitation`. There is no bookmark/share (the schema has neither).\n */\n\nimport { createElement, useEffect, useState } from 'react';\nimport {\n ArrowLeft,\n Landmark,\n Globe,\n CalendarClock,\n Languages,\n Download,\n Library,\n Tag,\n Quote,\n Maximize2,\n Plus,\n Minus,\n X,\n Loader2,\n Users,\n Lock,\n Clock,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useParams } from 'react-router';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DecorativeDivider } from '../../components/DecorativeDivider';\nimport { KIND_ICON, IMAGE_KINDS } from '../../components/kindIcon';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useTcpArchiveItem, useFileUrl, useTcpStories, useTcpAddCitation } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { cn } from '../../utils/cn';\nimport { formatDate, formatEnumLabel } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpArchiveItem, TcpStory, TcpAccessLevel } from '../../types';\n\nconst ACCESS_ICON: Record<TcpAccessLevel, LucideIcon> = {\n PUBLIC: Globe,\n COMMUNITY: Users,\n RESTRICTED: Lock,\n EMBARGOED: Clock,\n};\n\nfunction Shell({ onBack, children }: { onBack: () => void; children: React.ReactNode }) {\n return (\n <div className=\"flex h-full flex-col\">\n <div className=\"flex-1 overflow-auto\">\n <div className=\"mx-auto w-full max-w-6xl p-4 sm:p-6\">\n <button\n type=\"button\"\n onClick={onBack}\n className=\"inline-flex items-center gap-1.5 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" /> Archive\n </button>\n <div className=\"mt-4\">{children}</div>\n </div>\n </div>\n </div>\n );\n}\n\nexport function ArchiveItemDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const { data: item, isLoading, isError, refetch } = useTcpArchiveItem(id);\n const [citeOpen, setCiteOpen] = useState(false);\n\n const goBack = () => navigateTo('archive');\n\n if (isLoading) {\n return (\n <Shell onBack={goBack}>\n <LoadingView label=\"Loading artifact…\" />\n </Shell>\n );\n }\n\n if (isError || !item) {\n return (\n <Shell onBack={goBack}>\n <ErrorView\n title=\"Artifact not found\"\n message=\"This artifact may have been archived, or you may not have access.\"\n onRetry={() => refetch()}\n />\n </Shell>\n );\n }\n\n const collection = item.collection;\n const AccessIcon = ACCESS_ICON[item.accessLevel];\n const KindIcon = KIND_ICON[item.kind];\n\n return (\n <Shell onBack={goBack}>\n <div className=\"grid gap-6 lg:grid-cols-[1.3fr_1fr]\">\n <MediaViewer item={item} />\n\n <div className=\"flex min-w-0 flex-col gap-5\">\n <h1 className=\"font-display text-3xl font-semibold leading-tight text-text-primary\">\n {item.title}\n </h1>\n\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n status={item.kind}\n icon={createElement(KindIcon, { className: 'h-3 w-3' })}\n />\n <StatusBadge\n status={item.accessLevel}\n icon={createElement(AccessIcon, { className: 'h-3 w-3' })}\n />\n </div>\n\n {item.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {item.description}\n </p>\n )}\n\n <DecorativeDivider className=\"py-2\" />\n\n <dl className=\"flex flex-col gap-4\">\n {item.provenance && (\n <DetailRow icon={Landmark} label=\"Provenance\">\n {item.provenance}\n </DetailRow>\n )}\n {item.origin && (\n <DetailRow icon={Globe} label=\"Origin\">\n {item.origin}\n </DetailRow>\n )}\n {item.datedAt && (\n <DetailRow icon={CalendarClock} label=\"Date\">\n {formatDate(item.datedAt)}\n </DetailRow>\n )}\n {item.languages.length > 0 && (\n <DetailRow icon={Languages} label=\"Languages\">\n {item.languages.map(formatEnumLabel).join(', ')}\n </DetailRow>\n )}\n {item.tags.length > 0 && (\n <DetailRow icon={Tag} label=\"Tags\">\n <div className=\"flex flex-wrap gap-1.5\">\n {item.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 </DetailRow>\n )}\n </dl>\n\n {collection && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`archive/collections/${collection.id}`)}\n className=\"inline-flex items-center gap-2 self-start rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Library className=\"h-4 w-4 text-text-tertiary\" aria-hidden=\"true\" />{' '}\n {collection.name}\n </button>\n )}\n\n <Button className=\"w-full sm:w-auto\" onClick={() => setCiteOpen(true)}>\n <Quote className=\"mr-1.5 h-4 w-4\" /> Cite in a story\n </Button>\n </div>\n </div>\n\n <CiteInStoryDialog open={citeOpen} onOpenChange={setCiteOpen} item={item} />\n </Shell>\n );\n}\n\n// ---------------------------------------------------------------------------\n// DetailRow\n// ---------------------------------------------------------------------------\n\nfunction DetailRow({\n icon: Icon,\n label,\n children,\n}: {\n icon: LucideIcon;\n label: string;\n children: React.ReactNode;\n}) {\n return (\n <div className=\"flex items-start gap-3\">\n <Icon className=\"mt-0.5 h-4 w-4 shrink-0 text-accent\" aria-hidden=\"true\" />\n <dt className=\"w-24 shrink-0 text-sm text-text-tertiary\">{label}</dt>\n <dd className=\"min-w-0 flex-1 text-sm text-text-primary\">{children}</dd>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// MediaViewer — single file only (no gallery), CSS zoom + a full-screen lightbox.\n// ---------------------------------------------------------------------------\n\nfunction MediaViewer({ item }: { item: TcpArchiveItem }) {\n const { data: url } = useFileUrl(item.file?.id);\n const Icon = KIND_ICON[item.kind];\n const [zoom, setZoom] = useState(1);\n const [lightbox, setLightbox] = useState(false);\n const [imgFailed, setImgFailed] = useState(false);\n const showImage = IMAGE_KINDS.has(item.kind) && Boolean(url) && !imgFailed;\n\n useEffect(() => {\n if (!lightbox) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setLightbox(false);\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [lightbox]);\n\n const zoomIn = () => setZoom((z) => Math.min(4, Math.round((z + 0.5) * 10) / 10));\n const zoomOut = () => setZoom((z) => Math.max(1, Math.round((z - 0.5) * 10) / 10));\n\n return (\n <div className=\"flex flex-col gap-4\">\n <GlassCard treatment=\"plain\" className=\"p-3 sm:p-4\">\n <div\n className={cn(\n 'relative aspect-[4/3] w-full rounded-xl bg-bg-sunken',\n zoom > 1 ? 'overflow-auto' : 'overflow-hidden'\n )}\n >\n {showImage && url ? (\n <img\n src={url}\n alt={item.title}\n onError={() => setImgFailed(true)}\n style={{ transform: `scale(${zoom})`, transformOrigin: 'center center' }}\n className=\"h-full w-full object-contain transition-transform duration-200\"\n />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-3 text-text-tertiary\">\n <Icon className=\"h-16 w-16\" aria-hidden=\"true\" />\n <span className=\"text-sm font-medium\">{formatEnumLabel(item.kind)}</span>\n </div>\n )}\n\n {showImage && url && (\n <>\n <button\n type=\"button\"\n onClick={() => setLightbox(true)}\n aria-label=\"View full screen\"\n className=\"absolute right-3 top-3 z-10 flex h-9 w-9 items-center justify-center rounded-lg bg-bg-canvas/85 text-text-primary shadow-sm backdrop-blur-sm hover:bg-bg-canvas\"\n >\n <Maximize2 className=\"h-4 w-4\" />\n </button>\n <div className=\"absolute bottom-3 left-3 z-10 flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-canvas/85 text-text-primary shadow-sm backdrop-blur-sm\">\n <button\n type=\"button\"\n onClick={zoomIn}\n disabled={zoom >= 4}\n aria-label=\"Zoom in\"\n className=\"flex h-8 w-9 items-center justify-center hover:bg-bg-sunken disabled:opacity-40\"\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setZoom(1)}\n aria-label=\"Reset zoom\"\n className=\"border-y border-border-seam px-1 py-1 text-[10px] font-medium tabular-nums hover:bg-bg-sunken\"\n >\n {Math.round(zoom * 100)}%\n </button>\n <button\n type=\"button\"\n onClick={zoomOut}\n disabled={zoom <= 1}\n aria-label=\"Zoom out\"\n className=\"flex h-8 w-9 items-center justify-center hover:bg-bg-sunken disabled:opacity-40\"\n >\n <Minus className=\"h-4 w-4\" />\n </button>\n </div>\n </>\n )}\n </div>\n </GlassCard>\n\n {url && (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n download\n className=\"inline-flex items-center justify-center gap-2 self-start rounded-lg border border-border-seam bg-bg-surface px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Download className=\"h-4 w-4\" /> Download\n </a>\n )}\n\n {lightbox && url && (\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${item.title} — full view`}\n onClick={() => setLightbox(false)}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-canvas/95 p-4 backdrop-blur-sm\"\n >\n <button\n type=\"button\"\n onClick={() => setLightbox(false)}\n aria-label=\"Close full view\"\n className=\"absolute right-4 top-4 flex h-10 w-10 items-center justify-center rounded-full bg-bg-surface text-text-primary shadow-sm hover:bg-bg-sunken\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n <img\n src={url}\n alt={item.title}\n onClick={(e) => e.stopPropagation()}\n className=\"max-h-full max-w-full rounded-lg object-contain\"\n />\n </div>\n )}\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// CiteInStoryDialog — pick a story and attach this artifact as a citation.\n// ---------------------------------------------------------------------------\n\nfunction CiteInStoryDialog({\n open,\n onOpenChange,\n item,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n item: TcpArchiveItem;\n}) {\n const storiesQuery = useTcpStories({ pagination: { limit: 50 } });\n const addCitation = useTcpAddCitation();\n const [query, setQuery] = useState('');\n const [pendingId, setPendingId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const stories = storiesQuery.data?.items ?? [];\n const q = query.trim().toLowerCase();\n const filtered = q ? stories.filter((s) => s.title.toLowerCase().includes(q)) : stories;\n\n const cite = async (story: TcpStory) => {\n setPendingId(story.id);\n setError(null);\n try {\n await addCitation.mutateAsync({\n storyId: story.id,\n title: item.title,\n sourceType: 'ARCHIVAL_RECORD',\n provenance: item.provenance ?? null,\n excerpt: item.description ?? null,\n sourceFileId: item.file?.id ?? null,\n });\n notify.success('Cited in a story', story.title);\n onOpenChange(false);\n } catch (err) {\n setError(describeError(err));\n } finally {\n setPendingId(null);\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Cite in a story</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Attach “{item.title}” to a story as an archival-record citation.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex flex-col gap-3 py-2\">\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search stories…\"\n aria-label=\"Search stories\"\n />\n <div className=\"flex max-h-[50vh] flex-col gap-1.5 overflow-y-auto\">\n {storiesQuery.isLoading ? (\n <LoadingView label=\"Loading stories…\" className=\"min-h-[8rem]\" />\n ) : filtered.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-text-tertiary\">\n {stories.length === 0\n ? 'No stories yet — draft a story first.'\n : 'No stories match your search.'}\n </p>\n ) : (\n filtered.map((story) => (\n <button\n key={story.id}\n type=\"button\"\n onClick={() => cite(story)}\n disabled={addCitation.isPending}\n className=\"flex items-center justify-between gap-3 rounded-xl border border-border-seam bg-bg-surface px-3 py-2.5 text-left transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {story.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-tertiary\">\n {story.topic?.name ?? formatEnumLabel(story.status)}\n </span>\n </span>\n {pendingId === story.id ? (\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin text-text-tertiary\" />\n ) : (\n <Quote className=\"h-4 w-4 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n )}\n </button>\n ))\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 Close\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuDA,IAAM,IAAkD;CACtD,QAAQ;CACR,WAAW;CACX,YAAY;CACZ,WAAW;CACZ;AAED,SAAS,EAAM,EAAE,WAAQ,eAA+D;AACtF,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAHZ,CAKE,kBAAC,GAAD;MAAW,WAAU;MAAU,eAAY;MAAS,CAAA,EAAA,WAC7C;QACT,kBAAC,OAAD;KAAK,WAAU;KAAQ;KAAe,CAAA,CAClC;;GACF,CAAA;EACF,CAAA;;AAIV,SAAgB,IAAwB;CACtC,IAAM,EAAE,UAAO,GAA2B,EACpC,EAAE,kBAAe,GAAmB,EACpC,EAAE,MAAM,GAAM,cAAW,YAAS,eAAY,EAAkB,EAAG,EACnE,CAAC,GAAU,KAAe,EAAS,GAAM,EAEzC,UAAe,EAAW,UAAU;AAE1C,KAAI,EACF,QACE,kBAAC,GAAD;EAAO,QAAQ;YACb,kBAAC,GAAD,EAAa,OAAM,qBAAsB,CAAA;EACnC,CAAA;AAIZ,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,GAAD;EAAO,QAAQ;YACb,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,GAAS;GACxB,CAAA;EACI,CAAA;CAIZ,IAAM,IAAa,EAAK,YAClB,IAAa,EAAY,EAAK,cAC9B,IAAW,EAAU,EAAK;AAEhC,QACE,kBAAC,GAAD;EAAO,QAAQ;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAmB,SAAQ,CAAA,EAE3B,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAK;MACH,CAAA;KAEL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,QAAQ,EAAK;OACb,MAAM,EAAc,GAAU,EAAE,WAAW,WAAW,CAAC;OACvD,CAAA,EACF,kBAAC,GAAD;OACE,QAAQ,EAAK;OACb,MAAM,EAAc,GAAY,EAAE,WAAW,WAAW,CAAC;OACzD,CAAA,CACE;;KAEL,EAAK,eACJ,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAK;MACJ,CAAA;KAGN,kBAAC,GAAD,EAAmB,WAAU,QAAS,CAAA;KAEtC,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACG,EAAK,cACJ,kBAAC,GAAD;QAAW,MAAM;QAAU,OAAM;kBAC9B,EAAK;QACI,CAAA;OAEb,EAAK,UACJ,kBAAC,GAAD;QAAW,MAAM;QAAO,OAAM;kBAC3B,EAAK;QACI,CAAA;OAEb,EAAK,WACJ,kBAAC,GAAD;QAAW,MAAM;QAAe,OAAM;kBACnC,EAAW,EAAK,QAAQ;QACf,CAAA;OAEb,EAAK,UAAU,SAAS,KACvB,kBAAC,GAAD;QAAW,MAAM;QAAW,OAAM;kBAC/B,EAAK,UAAU,IAAI,EAAgB,CAAC,KAAK,KAAK;QACrC,CAAA;OAEb,EAAK,KAAK,SAAS,KAClB,kBAAC,GAAD;QAAW,MAAM;QAAK,OAAM;kBAC1B,kBAAC,OAAD;SAAK,WAAU;mBACZ,EAAK,KAAK,KAAK,MACd,kBAAC,QAAD;UAEE,WAAU;oBAET;UACI,EAJA,EAIA,CACP;SACE,CAAA;QACI,CAAA;OAEX;;KAEJ,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAW,uBAAuB,EAAW,KAAK;MACjE,WAAU;gBAHZ;OAKE,kBAAC,GAAD;QAAS,WAAU;QAA6B,eAAY;QAAS,CAAA;OAAC;OACrE,EAAW;OACL;;KAGX,kBAAC,GAAD;MAAQ,WAAU;MAAmB,eAAe,EAAY,GAAK;gBAArE,CACE,kBAAC,GAAD,EAAO,WAAU,kBAAmB,CAAA,EAAA,mBAC7B;;KACL;MACF;MAEN,kBAAC,IAAD;GAAmB,MAAM;GAAU,cAAc;GAAmB;GAAQ,CAAA,CACtE;;;AAQZ,SAAS,EAAU,EACjB,MAAM,GACN,UACA,eAKC;AACD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAM,WAAU;IAAsC,eAAY;IAAS,CAAA;GAC3E,kBAAC,MAAD;IAAI,WAAU;cAA4C;IAAW,CAAA;GACrE,kBAAC,MAAD;IAAI,WAAU;IAA4C;IAAc,CAAA;GACpE;;;AAQV,SAAS,EAAY,EAAE,WAAkC;CACvD,IAAM,EAAE,MAAM,MAAQ,EAAW,EAAK,MAAM,GAAG,EACzC,IAAO,EAAU,EAAK,OACtB,CAAC,GAAM,KAAW,EAAS,EAAE,EAC7B,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,IAAY,EAAY,IAAI,EAAK,KAAK,IAAI,EAAQ,KAAQ,CAAC;AAcjE,QAZA,QAAgB;AACd,MAAI,CAAC,EAAU;EACf,IAAM,KAAS,MAAqB;AAClC,GAAI,EAAE,QAAQ,YAAU,EAAY,GAAM;;AAG5C,SADA,OAAO,iBAAiB,WAAW,EAAM,QAC5B,OAAO,oBAAoB,WAAW,EAAM;IACxD,CAAC,EAAS,CAAC,EAMZ,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cACrC,kBAAC,OAAD;KACE,WAAW,EACT,wDACA,IAAO,IAAI,kBAAkB,kBAC9B;eAJH,CAMG,KAAa,IACZ,kBAAC,OAAD;MACE,KAAK;MACL,KAAK,EAAK;MACV,eAAe,EAAa,GAAK;MACjC,OAAO;OAAE,WAAW,SAAS,EAAK;OAAI,iBAAiB;OAAiB;MACxE,WAAU;MACV,CAAA,GAEF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OAAM,WAAU;OAAY,eAAY;OAAS,CAAA,EACjD,kBAAC,QAAD;OAAM,WAAU;iBAAuB,EAAgB,EAAK,KAAK;OAAQ,CAAA,CACrE;SAGP,KAAa,KACZ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAY,GAAK;MAChC,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;MAC1B,CAAA,EACT,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,UAAD;QACE,MAAK;QACL,eAxCK,GAAS,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,IAAI,MAAO,GAAG,GAAG,GAAG,CAAC;QAyCjE,UAAU,KAAQ;QAClB,cAAW;QACX,WAAU;kBAEV,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;QACrB,CAAA;OACT,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAQ,EAAE;QACzB,cAAW;QACX,WAAU;kBAJZ,CAMG,KAAK,MAAM,IAAO,IAAI,EAAC,IACjB;;OACT,kBAAC,UAAD;QACE,MAAK;QACL,eAxDM,GAAS,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,IAAI,MAAO,GAAG,GAAG,GAAG,CAAC;QAyDlE,UAAU,KAAQ;QAClB,cAAW;QACX,WAAU;kBAEV,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;QACtB,CAAA;OACL;QACL,EAAA,CAAA,CAED;;IACI,CAAA;GAEX,KACC,kBAAC,KAAD;IACE,MAAM;IACN,QAAO;IACP,KAAI;IACJ,UAAA;IACA,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAAA,YAC9B;;GAGL,KAAY,KACX,kBAAC,OAAD;IACE,MAAK;IACL,cAAW;IACX,cAAY,GAAG,EAAK,MAAM;IAC1B,eAAe,EAAY,GAAM;IACjC,WAAU;cALZ,CAOE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAY,GAAM;KACjC,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;KAClB,CAAA,EACT,kBAAC,OAAD;KACE,KAAK;KACL,KAAK,EAAK;KACV,UAAU,MAAM,EAAE,iBAAiB;KACnC,WAAU;KACV,CAAA,CACE;;GAEJ;;;AAQV,SAAS,GAAkB,EACzB,SACA,iBACA,WAKC;CACD,IAAM,IAAe,EAAc,EAAE,YAAY,EAAE,OAAO,IAAI,EAAE,CAAC,EAC3D,IAAc,GAAmB,EACjC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAW,KAAgB,EAAwB,KAAK,EACzD,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAU,EAAa,MAAM,SAAS,EAAE,EACxC,IAAI,EAAM,MAAM,CAAC,aAAa,EAC9B,IAAW,IAAI,EAAQ,QAAQ,MAAM,EAAE,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,GAAG,GAE1E,IAAO,OAAO,MAAoB;AAEtC,EADA,EAAa,EAAM,GAAG,EACtB,EAAS,KAAK;AACd,MAAI;AAUF,GATA,MAAM,EAAY,YAAY;IAC5B,SAAS,EAAM;IACf,OAAO,EAAK;IACZ,YAAY;IACZ,YAAY,EAAK,cAAc;IAC/B,SAAS,EAAK,eAAe;IAC7B,cAAc,EAAK,MAAM,MAAM;IAChC,CAAC,EACF,EAAO,QAAQ,oBAAoB,EAAM,MAAM,EAC/C,EAAa,GAAM;WACZ,GAAK;AACZ,KAAS,EAAc,EAAI,CAAC;YACpB;AACR,KAAa,KAAK;;;AAItB,QACE,kBAAC,GAAD;EAAwB;EAAoB;YAC1C,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAuB,mBAAuC,CAAA,EAC9D,kBAAC,GAAD,EAAA,UAAA;KAA6B;KAClB,EAAK;KAAM;KACQ,EAAA,CAAA,CACP,EAAA,CAAA;IAEzB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OACE,OAAO;OACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;OACzC,aAAY;OACZ,cAAW;OACX,CAAA;MACF,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAa,YACZ,kBAAC,GAAD;QAAa,OAAM;QAAmB,WAAU;QAAiB,CAAA,GAC/D,EAAS,WAAW,IACtB,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAQ,WAAW,IAChB,0CACA;QACF,CAAA,GAEJ,EAAS,KAAK,MACZ,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAK,EAAM;QAC1B,UAAU,EAAY;QACtB,WAAU;kBALZ,CAOE,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACE,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAM;UACF,CAAA,EACP,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAM,OAAO,QAAQ,EAAgB,EAAM,OAAO;UAC9C,CAAA,CACF;YACN,MAAc,EAAM,KACnB,kBAAC,GAAD,EAAS,WAAU,oDAAqD,CAAA,GAExE,kBAAC,GAAD;SAAO,WAAU;SAAsC,eAAY;SAAS,CAAA,CAEvE;UAnBF,EAAM,GAmBJ,CACT;OAEA,CAAA;MACL,KAAS,kBAAC,KAAD;OAAG,WAAU;iBAAkC;OAAU,CAAA;MAC/D;;IAEN,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,eAAe,EAAa,GAAM;eAAE;KAErD,CAAA,EACc,CAAA;IACD;;EACT,CAAA"}
|
|
@@ -13,7 +13,7 @@ import { CreateArchiveItemDialog as u } from "../../components/CreateArchiveItem
|
|
|
13
13
|
import { OralHistoryPlayerRow as d } from "./ArchivePage.js";
|
|
14
14
|
import { useState as f } from "react";
|
|
15
15
|
import { AudioLines as p, Boxes as m, Mic as h, Plus as g } from "lucide-react";
|
|
16
|
-
import { useParams as _ } from "react-router
|
|
16
|
+
import { useParams as _ } from "react-router";
|
|
17
17
|
import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
18
18
|
import { Button as x, GlassCard as S } from "@burdenoff/fe-libs/ui";
|
|
19
19
|
//#region src/theculturepost/pages/archive/CollectionDetailPage.tsx
|
|
@@ -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). An editorial\n * {@link PageHero} shows the real cover (collections DO carry a cover file), with\n * real counts (itemCount, oralHistories.length). Two panels below list the\n * artifacts and the listenable oral histories. Adding an artifact or recording an\n * oral history is pre-scoped to this collection.\n */\n\nimport { useState } from 'react';\nimport { Boxes, AudioLines, Plus, Mic } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useParams } from 'react-router-dom';\nimport { PageHero } from '../../components/PageHero';\nimport { DecorativeDivider } from '../../components/DecorativeDivider';\nimport { ArchiveItemCard } from '../../components/ArchiveItemCard';\nimport { CreateArchiveItemDialog } from '../../components/CreateArchiveItemDialog';\nimport { RecordOralHistoryDialog } from '../../components/RecordOralHistoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { OralHistoryPlayerRow } from './ArchivePage';\nimport { useTcpCollection } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber } from '../../utils/formatters';\n\nfunction Shell({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex h-full flex-col\">\n <div className=\"flex-1 overflow-auto\">\n <div className=\"mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6\">{children}</div>\n </div>\n </div>\n );\n}\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 goBack = () => navigateTo('archive');\n\n if (isLoading) {\n return (\n <Shell>\n <LoadingView label=\"Loading collection…\" />\n </Shell>\n );\n }\n\n if (isError || !collection) {\n return (\n <Shell>\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 </Shell>\n );\n }\n\n const items = collection.items ?? [];\n const oralHistories = collection.oralHistories ?? [];\n\n return (\n <Shell>\n <PageHero\n eyebrow=\"Collection\"\n title={collection.name}\n description={collection.description ?? undefined}\n coverFileId={collection.cover?.id}\n backAction={{ label: 'Archive', onClick: goBack }}\n meta={\n <>\n {collection.tradition && (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {collection.tradition}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Boxes className=\"h-4 w-4\" aria-hidden=\"true\" /> {formatNumber(collection.itemCount)}{' '}\n {collection.itemCount === 1 ? 'item' : 'items'}\n </span>\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <AudioLines className=\"h-4 w-4\" aria-hidden=\"true\" />{' '}\n {formatNumber(oralHistories.length)}{' '}\n {oralHistories.length === 1 ? 'oral history' : 'oral histories'}\n </span>\n </>\n }\n actions={\n <>\n <Button onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n <Button variant=\"outline\" onClick={() => setRecordOpen(true)}>\n <Mic className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n </>\n }\n />\n\n <DecorativeDivider className=\"py-2\" />\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n {/* Items */}\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 font-display text-lg font-semibold text-text-primary\">\n <Boxes className=\"h-5 w-5 text-accent\" aria-hidden=\"true\" /> Items\n </h2>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add\n </Button>\n </div>\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\">\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 </GlassCard>\n\n {/* Oral histories */}\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 font-display text-lg font-semibold text-text-primary\">\n <AudioLines className=\"h-5 w-5 text-accent\" aria-hidden=\"true\" /> Oral histories\n </h2>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRecordOpen(true)}>\n <Mic className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n </div>\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 <Mic className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {oralHistories.map((oralHistory) => (\n <OralHistoryPlayerRow key={oralHistory.id} oralHistory={oralHistory} />\n ))}\n </div>\n )}\n </GlassCard>\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 </Shell>\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). An editorial\n * {@link PageHero} shows the real cover (collections DO carry a cover file), with\n * real counts (itemCount, oralHistories.length). Two panels below list the\n * artifacts and the listenable oral histories. Adding an artifact or recording an\n * oral history is pre-scoped to this collection.\n */\n\nimport { useState } from 'react';\nimport { Boxes, AudioLines, Plus, Mic } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useParams } from 'react-router';\nimport { PageHero } from '../../components/PageHero';\nimport { DecorativeDivider } from '../../components/DecorativeDivider';\nimport { ArchiveItemCard } from '../../components/ArchiveItemCard';\nimport { CreateArchiveItemDialog } from '../../components/CreateArchiveItemDialog';\nimport { RecordOralHistoryDialog } from '../../components/RecordOralHistoryDialog';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { OralHistoryPlayerRow } from './ArchivePage';\nimport { useTcpCollection } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber } from '../../utils/formatters';\n\nfunction Shell({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex h-full flex-col\">\n <div className=\"flex-1 overflow-auto\">\n <div className=\"mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6\">{children}</div>\n </div>\n </div>\n );\n}\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 goBack = () => navigateTo('archive');\n\n if (isLoading) {\n return (\n <Shell>\n <LoadingView label=\"Loading collection…\" />\n </Shell>\n );\n }\n\n if (isError || !collection) {\n return (\n <Shell>\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 </Shell>\n );\n }\n\n const items = collection.items ?? [];\n const oralHistories = collection.oralHistories ?? [];\n\n return (\n <Shell>\n <PageHero\n eyebrow=\"Collection\"\n title={collection.name}\n description={collection.description ?? undefined}\n coverFileId={collection.cover?.id}\n backAction={{ label: 'Archive', onClick: goBack }}\n meta={\n <>\n {collection.tradition && (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {collection.tradition}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Boxes className=\"h-4 w-4\" aria-hidden=\"true\" /> {formatNumber(collection.itemCount)}{' '}\n {collection.itemCount === 1 ? 'item' : 'items'}\n </span>\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <AudioLines className=\"h-4 w-4\" aria-hidden=\"true\" />{' '}\n {formatNumber(oralHistories.length)}{' '}\n {oralHistories.length === 1 ? 'oral history' : 'oral histories'}\n </span>\n </>\n }\n actions={\n <>\n <Button onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add artifact\n </Button>\n <Button variant=\"outline\" onClick={() => setRecordOpen(true)}>\n <Mic className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n </>\n }\n />\n\n <DecorativeDivider className=\"py-2\" />\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n {/* Items */}\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 font-display text-lg font-semibold text-text-primary\">\n <Boxes className=\"h-5 w-5 text-accent\" aria-hidden=\"true\" /> Items\n </h2>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddItemOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add\n </Button>\n </div>\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\">\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 </GlassCard>\n\n {/* Oral histories */}\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 font-display text-lg font-semibold text-text-primary\">\n <AudioLines className=\"h-5 w-5 text-accent\" aria-hidden=\"true\" /> Oral histories\n </h2>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRecordOpen(true)}>\n <Mic className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n </div>\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 <Mic className=\"mr-1.5 h-4 w-4\" /> Record\n </Button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {oralHistories.map((oralHistory) => (\n <OralHistoryPlayerRow key={oralHistory.id} oralHistory={oralHistory} />\n ))}\n </div>\n )}\n </GlassCard>\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 </Shell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA2BA,SAAS,EAAM,EAAE,eAA2C;AAC1D,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,OAAD;IAAK,WAAU;IAA2D;IAAe,CAAA;GACrF,CAAA;EACF,CAAA;;AAIV,SAAgB,IAAuB;CACrC,IAAM,EAAE,UAAO,GAA2B,EACpC,EAAE,kBAAe,GAAmB,EACpC,EAAE,MAAM,GAAY,cAAW,YAAS,eAAY,EAAiB,EAAG,EACxE,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAY,KAAiB,EAAS,GAAM,EAE7C,UAAe,EAAW,UAAU;AAE1C,KAAI,EACF,QACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAa,OAAM,uBAAwB,CAAA,EACrC,CAAA;AAIZ,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EACE,OAAM;EACN,SAAQ;EACR,eAAe,GAAS;EACxB,CAAA,EACI,CAAA;CAIZ,IAAM,IAAQ,EAAW,SAAS,EAAE,EAC9B,IAAgB,EAAW,iBAAiB,EAAE;AAEpD,QACE,kBAAC,GAAD,EAAA,UAAA;EACE,kBAAC,GAAD;GACE,SAAQ;GACR,OAAO,EAAW;GAClB,aAAa,EAAW,eAAe,KAAA;GACvC,aAAa,EAAW,OAAO;GAC/B,YAAY;IAAE,OAAO;IAAW,SAAS;IAAQ;GACjD,MACE,kBAAA,GAAA,EAAA,UAAA;IACG,EAAW,aACV,kBAAC,QAAD;KAAM,WAAU;eACb,EAAW;KACP,CAAA;IAET,kBAAC,QAAD;KAAM,WAAU;eAAhB;MACE,kBAAC,GAAD;OAAO,WAAU;OAAU,eAAY;OAAS,CAAA;;MAAE,EAAa,EAAW,UAAU;MAAE;MACrF,EAAW,cAAc,IAAI,SAAS;MAClC;;IACP,kBAAC,QAAD;KAAM,WAAU;eAAhB;MACE,kBAAC,GAAD;OAAY,WAAU;OAAU,eAAY;OAAS,CAAA;MAAC;MACrD,EAAa,EAAc,OAAO;MAAE;MACpC,EAAc,WAAW,IAAI,iBAAiB;MAC1C;;IACN,EAAA,CAAA;GAEL,SACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;IAAQ,eAAe,EAAe,GAAK;cAA3C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,gBAC5B;OACT,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAc,GAAK;cAA5D,CACE,kBAAC,GAAD,EAAK,WAAU,kBAAmB,CAAA,EAAA,UAC3B;MACR,EAAA,CAAA;GAEL,CAAA;EAEF,kBAAC,GAAD,EAAmB,WAAU,QAAS,CAAA;EAEtC,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAd,CACE,kBAAC,GAAD;OAAO,WAAU;OAAsB,eAAY;OAAS,CAAA,EAAA,SACzD;SACL,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAU,eAAe,EAAe,GAAK;gBAAvE,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,OAC5B;QACL;QACL,EAAM,WAAW,IAChB,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;KACnC,OAAM;KACN,aAAY;KACZ,QACE,kBAAC,GAAD;MAAQ,eAAe,EAAe,GAAK;gBAA3C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,gBAC5B;;KAEX,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAM,KAAK,MACV,kBAAC,GAAD;MAEQ;MACN,cAAc,EAAW,iBAAiB,EAAK,KAAK;MACpD,EAHK,EAAK,GAGV,CACF;KACE,CAAA,CAEE;OAGZ,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAd,CACE,kBAAC,GAAD;OAAY,WAAU;OAAsB,eAAY;OAAS,CAAA,EAAA,kBAC9D;SACL,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAU,eAAe,EAAc,GAAK;gBAAtE,CACE,kBAAC,GAAD,EAAK,WAAU,kBAAmB,CAAA,EAAA,UAC3B;QACL;QACL,EAAc,WAAW,IACxB,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;KACxC,OAAM;KACN,aAAY;KACZ,QACE,kBAAC,GAAD;MAAQ,eAAe,EAAc,GAAK;gBAA1C,CACE,kBAAC,GAAD,EAAK,WAAU,kBAAmB,CAAA,EAAA,UAC3B;;KAEX,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAc,KAAK,MAClB,kBAAC,GAAD,EAAwD,gBAAe,EAA5C,EAAY,GAAgC,CACvE;KACE,CAAA,CAEE;MACR;;EAEN,kBAAC,GAAD;GACE,MAAM;GACN,cAAc;GACd,cAAc,EAAW;GACzB,CAAA;EACF,kBAAC,GAAD;GACE,MAAM;GACN,cAAc;GACd,cAAc,EAAW;GACzB,CAAA;EACI,EAAA,CAAA"}
|
|
@@ -11,7 +11,7 @@ import { impact as d, nativeConfirm as f } from "../../utils/nativeBridge.js";
|
|
|
11
11
|
import { PageLayout as p } from "../../components/PageLayout.js";
|
|
12
12
|
import { ReplyThread as m } from "../../components/ReplyThread.js";
|
|
13
13
|
import { ArrowLeft as h, BookOpen as g, CheckCircle2 as _, Clock as v, ExternalLink as y, Flag as b, HeartHandshake as x, Lock as S, MessagesSquare as C, Quote as w, Trash2 as T, Unlock as E } from "lucide-react";
|
|
14
|
-
import { useParams as D } from "react-router
|
|
14
|
+
import { useParams as D } from "react-router";
|
|
15
15
|
import { Fragment as O, jsx as k, jsxs as A } from "react/jsx-runtime";
|
|
16
16
|
import { Button as j, GlassCard as M } from "@burdenoff/fe-libs/ui";
|
|
17
17
|
//#region src/theculturepost/pages/discussions/DiscussionDetailPage.tsx
|
|
@@ -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. The header is an editorial card: display-serif title, a\n * moderation-status + interfaith badge row, tag chips, an \"opened by\" byline and\n * a framed prompt callout.\n */\n\nimport type { ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Lock,\n Unlock,\n HeartHandshake,\n BookOpen,\n MessagesSquare,\n Quote,\n CheckCircle2,\n Clock,\n Flag,\n Trash2,\n ExternalLink,\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 } from '../../components/StateViews';\nimport { useTcpDiscussion, useTcpLockDiscussion } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport { impact, nativeConfirm } from '../../utils/nativeBridge';\nimport type { TcpModerationStatus } from '../../types';\n\n/** Leading icon for a moderation-status badge (token colours come from StatusBadge). */\nfunction statusIcon(status: TcpModerationStatus): ReactNode {\n const className = 'h-3 w-3';\n switch (status) {\n case 'APPROVED':\n return <CheckCircle2 className={className} aria-hidden=\"true\" />;\n case 'PENDING':\n return <Clock className={className} aria-hidden=\"true\" />;\n case 'FLAGGED':\n return <Flag className={className} aria-hidden=\"true\" />;\n case 'REMOVED':\n return <Trash2 className={className} aria-hidden=\"true\" />;\n }\n}\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 await impact('light');\n const action = discussion.isLocked ? 'unlock' : 'lock';\n const confirmed = await nativeConfirm({\n title: `${action === 'lock' ? 'Lock' : 'Unlock'} discussion`,\n message: `Are you sure you want to ${action} this discussion?`,\n okButtonTitle: action === 'lock' ? 'Lock' : 'Unlock',\n cancelButtonTitle: 'Cancel',\n });\n if (!confirmed) return;\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 title=\"Discussion\" actions={backButton}>\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 {/* Title + lock/unlock */}\n <div className=\"flex items-start justify-between gap-3\">\n <h1 className=\"font-display text-2xl font-semibold leading-tight text-text-primary sm:text-3xl\">\n {discussion.title}\n </h1>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={doLock}\n disabled={lock.isPending}\n className=\"shrink-0\"\n >\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 {/* Moderation status + interfaith / locked signals, with tags to the right */}\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={discussion.status} icon={statusIcon(discussion.status)} />\n {discussion.isInterfaith && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n <HeartHandshake className=\"h-3 w-3\" aria-hidden=\"true\" /> Interfaith\n </span>\n )}\n {discussion.isLocked && (\n <span 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\">\n <Lock className=\"h-3 w-3\" aria-hidden=\"true\" /> Locked\n </span>\n )}\n </div>\n {discussion.tags.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5 sm:ml-auto\">\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 </div>\n\n {/* Byline */}\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-tertiary\">\n <AuthorLabel actorId={discussion.openedById} prefix=\"Opened by\" />\n <span aria-hidden=\"true\">·</span>\n <span className=\"inline-flex items-center gap-1\">\n <MessagesSquare className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {formatNumber(discussion.replyCount)}{' '}\n {discussion.replyCount === 1 ? 'reply' : 'replies'}\n </span>\n <span aria-hidden=\"true\">·</span>\n <span>Started {formatRelativeTime(discussion.createdAt)}</span>\n </div>\n\n {/* Prompt callout + linked story */}\n {(discussion.prompt || discussion.storyId) && (\n <div className=\"flex flex-col gap-3 rounded-xl border border-border-seam bg-[var(--color-accent-soft)] p-4 sm:flex-row sm:items-start sm:justify-between\">\n {discussion.prompt ? (\n <div className=\"flex min-w-0 items-start gap-3\">\n <span\n className=\"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-canvas/70 text-accent\"\n aria-hidden=\"true\"\n >\n <Quote className=\"h-4 w-4\" />\n </span>\n <p className=\"min-w-0 whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {discussion.prompt}\n </p>\n </div>\n ) : (\n <span className=\"text-sm text-text-secondary\">Anchored to a story.</span>\n )}\n {discussion.storyId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`stories/${discussion.storyId}`)}\n className=\"inline-flex shrink-0 items-center gap-1.5 self-start rounded-lg px-1 text-sm font-medium text-accent hover:underline\"\n >\n <BookOpen className=\"h-4 w-4\" aria-hidden=\"true\" /> View linked story\n <ExternalLink className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n )}\n </GlassCard>\n\n <ReplyThread discussionId={discussion.id} isLocked={discussion.isLocked} />\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAyCA,SAAS,EAAW,GAAwC;CAC1D,IAAM,IAAY;AAClB,SAAQ,GAAR;EACE,KAAK,WACH,QAAO,kBAAC,GAAD;GAAyB;GAAW,eAAY;GAAS,CAAA;EAClE,KAAK,UACH,QAAO,kBAAC,GAAD;GAAkB;GAAW,eAAY;GAAS,CAAA;EAC3D,KAAK,UACH,QAAO,kBAAC,GAAD;GAAiB;GAAW,eAAY;GAAS,CAAA;EAC1D,KAAK,UACH,QAAO,kBAAC,GAAD;GAAmB;GAAW,eAAY;GAAS,CAAA;;;AAIhE,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;;AAyCX,QAtCI,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,GAuBf,kBAAC,GAAD;EAAY,OAAM;EAAa,SAAS;YACtC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC;KAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAW;OACT,CAAA,EACL,kBAAC,GAAD;OACE,SAAQ;OACR,MAAK;OACL,SA9BG,YAAY;AACzB,cAAM,EAAO,QAAQ;QACrB,IAAM,IAAS,EAAW,WAAW,WAAW;AAC9B,kBAAM,EAAc;SACpC,OAAO,GAAG,MAAW,SAAS,SAAS,SAAS;SAChD,SAAS,4BAA4B,EAAO;SAC5C,eAAe,MAAW,SAAS,SAAS;SAC5C,mBAAmB;SACpB,CAAC,CAEF,KAAI;AAEF,SADA,MAAM,EAAK,YAAY,EAAW,GAAG,EACrC,EAAO,QAAQ,EAAW,WAAW,wBAAwB,oBAAoB;iBAC1E,GAAK;AACZ,WAAO,MAAM,mCAAmC,EAAc,EAAI,CAAC;;;OAiB3D,UAAU,EAAK;OACf,WAAU;iBAET,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;OAEE,CAAA,CACL;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAa,QAAQ,EAAW;SAAQ,MAAM,EAAW,EAAW,OAAO;SAAI,CAAA;QAC9E,EAAW,gBACV,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACE,kBAAC,GAAD;UAAgB,WAAU;UAAU,eAAY;UAAS,CAAA,EAAA,cACpD;;QAER,EAAW,YACV,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACE,kBAAC,GAAD;UAAM,WAAU;UAAU,eAAY;UAAS,CAAA,EAAA,UAC1C;;QAEL;UACL,EAAW,KAAK,SAAS,KACxB,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAW,KAAK,KAAK,MACpB,kBAAC,QAAD;QAEE,WAAU;kBAFZ,CAGC,KACG,EACG;UAJA,EAIA,CACP;OACE,CAAA,CAEJ;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD;QAAa,SAAS,EAAW;QAAY,QAAO;QAAc,CAAA;OAClE,kBAAC,QAAD;QAAM,eAAY;kBAAO;QAAQ,CAAA;OACjC,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD;UAAgB,WAAU;UAAc,eAAY;UAAS,CAAA;SAC5D,EAAa,EAAW,WAAW;SAAE;SACrC,EAAW,eAAe,IAAI,UAAU;SACpC;;OACP,kBAAC,QAAD;QAAM,eAAY;kBAAO;QAAQ,CAAA;OACjC,kBAAC,QAAD,EAAA,UAAA,CAAM,YAAS,EAAmB,EAAW,UAAU,CAAQ,EAAA,CAAA;OAC3D;;MAGJ,EAAW,UAAU,EAAW,YAChC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,EAAW,SACV,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QACE,WAAU;QACV,eAAY;kBAEZ,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;QACxB,CAAA,EACP,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAW;QACV,CAAA,CACA;WAEN,kBAAC,QAAD;OAAM,WAAU;iBAA8B;OAA2B,CAAA,EAE1E,EAAW,WACV,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,WAAW,EAAW,UAAU;OAC1D,WAAU;iBAHZ;QAKE,kBAAC,GAAD;SAAU,WAAU;SAAU,eAAY;SAAS,CAAA;;QACnD,kBAAC,GAAD;SAAc,WAAU;SAAc,eAAY;SAAS,CAAA;QACpD;SAEP;;KAEE;OAEZ,kBAAC,GAAD;IAAa,cAAc,EAAW;IAAI,UAAU,EAAW;IAAY,CAAA,CACvE;;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. The header is an editorial card: display-serif title, a\n * moderation-status + interfaith badge row, tag chips, an \"opened by\" byline and\n * a framed prompt callout.\n */\n\nimport type { ReactNode } from 'react';\nimport { useParams } from 'react-router';\nimport {\n ArrowLeft,\n Lock,\n Unlock,\n HeartHandshake,\n BookOpen,\n MessagesSquare,\n Quote,\n CheckCircle2,\n Clock,\n Flag,\n Trash2,\n ExternalLink,\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 } from '../../components/StateViews';\nimport { useTcpDiscussion, useTcpLockDiscussion } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport { impact, nativeConfirm } from '../../utils/nativeBridge';\nimport type { TcpModerationStatus } from '../../types';\n\n/** Leading icon for a moderation-status badge (token colours come from StatusBadge). */\nfunction statusIcon(status: TcpModerationStatus): ReactNode {\n const className = 'h-3 w-3';\n switch (status) {\n case 'APPROVED':\n return <CheckCircle2 className={className} aria-hidden=\"true\" />;\n case 'PENDING':\n return <Clock className={className} aria-hidden=\"true\" />;\n case 'FLAGGED':\n return <Flag className={className} aria-hidden=\"true\" />;\n case 'REMOVED':\n return <Trash2 className={className} aria-hidden=\"true\" />;\n }\n}\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 await impact('light');\n const action = discussion.isLocked ? 'unlock' : 'lock';\n const confirmed = await nativeConfirm({\n title: `${action === 'lock' ? 'Lock' : 'Unlock'} discussion`,\n message: `Are you sure you want to ${action} this discussion?`,\n okButtonTitle: action === 'lock' ? 'Lock' : 'Unlock',\n cancelButtonTitle: 'Cancel',\n });\n if (!confirmed) return;\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 title=\"Discussion\" actions={backButton}>\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 {/* Title + lock/unlock */}\n <div className=\"flex items-start justify-between gap-3\">\n <h1 className=\"font-display text-2xl font-semibold leading-tight text-text-primary sm:text-3xl\">\n {discussion.title}\n </h1>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={doLock}\n disabled={lock.isPending}\n className=\"shrink-0\"\n >\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 {/* Moderation status + interfaith / locked signals, with tags to the right */}\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={discussion.status} icon={statusIcon(discussion.status)} />\n {discussion.isInterfaith && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n <HeartHandshake className=\"h-3 w-3\" aria-hidden=\"true\" /> Interfaith\n </span>\n )}\n {discussion.isLocked && (\n <span 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\">\n <Lock className=\"h-3 w-3\" aria-hidden=\"true\" /> Locked\n </span>\n )}\n </div>\n {discussion.tags.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5 sm:ml-auto\">\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 </div>\n\n {/* Byline */}\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-tertiary\">\n <AuthorLabel actorId={discussion.openedById} prefix=\"Opened by\" />\n <span aria-hidden=\"true\">·</span>\n <span className=\"inline-flex items-center gap-1\">\n <MessagesSquare className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {formatNumber(discussion.replyCount)}{' '}\n {discussion.replyCount === 1 ? 'reply' : 'replies'}\n </span>\n <span aria-hidden=\"true\">·</span>\n <span>Started {formatRelativeTime(discussion.createdAt)}</span>\n </div>\n\n {/* Prompt callout + linked story */}\n {(discussion.prompt || discussion.storyId) && (\n <div className=\"flex flex-col gap-3 rounded-xl border border-border-seam bg-[var(--color-accent-soft)] p-4 sm:flex-row sm:items-start sm:justify-between\">\n {discussion.prompt ? (\n <div className=\"flex min-w-0 items-start gap-3\">\n <span\n className=\"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-canvas/70 text-accent\"\n aria-hidden=\"true\"\n >\n <Quote className=\"h-4 w-4\" />\n </span>\n <p className=\"min-w-0 whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {discussion.prompt}\n </p>\n </div>\n ) : (\n <span className=\"text-sm text-text-secondary\">Anchored to a story.</span>\n )}\n {discussion.storyId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`stories/${discussion.storyId}`)}\n className=\"inline-flex shrink-0 items-center gap-1.5 self-start rounded-lg px-1 text-sm font-medium text-accent hover:underline\"\n >\n <BookOpen className=\"h-4 w-4\" aria-hidden=\"true\" /> View linked story\n <ExternalLink className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n )}\n </GlassCard>\n\n <ReplyThread discussionId={discussion.id} isLocked={discussion.isLocked} />\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -22,7 +22,7 @@ import { CitationList as M, VERIFICATIONS as N, VERIFICATION_META as P, sourceTy
|
|
|
22
22
|
import { AddCitationDialog as ee } from "../../components/AddCitationDialog.js";
|
|
23
23
|
import { Fragment as I, useMemo as L, useState as R } from "react";
|
|
24
24
|
import { Archive as z, ArrowLeft as B, BookOpen as te, Bookmark as ne, ChevronDown as re, Clock as ie, GitBranch as V, Languages as ae, Lightbulb as oe, Lock as se, MessagesSquare as H, Pencil as ce, Plus as U, RotateCcw as le, Scroll as W, Send as G, Share2 as ue, ShieldCheck as K, Sparkles as de, Tag as fe } from "lucide-react";
|
|
25
|
-
import { useParams as pe, useSearchParams as me } from "react-router
|
|
25
|
+
import { useParams as pe, useSearchParams as me } from "react-router";
|
|
26
26
|
import { Fragment as he, jsx as q, jsxs as J } from "react/jsx-runtime";
|
|
27
27
|
import { Button as Y, DropdownMenu as ge, DropdownMenuContent as _e, DropdownMenuItem as ve, DropdownMenuTrigger as ye, GlassCard as X, Input as be, Label as Z, ResponsiveDialog as xe, ResponsiveDialogContent as Se, ResponsiveDialogDescription as Ce, ResponsiveDialogFooter as we, ResponsiveDialogHeader as Te, ResponsiveDialogTitle as Ee, ResponsiveTable as De, Select as Oe, SelectContent as ke, SelectItem as Ae, SelectTrigger as je, SelectValue as Me, Textarea as Ne } from "@burdenoff/fe-libs/ui";
|
|
28
28
|
//#region src/theculturepost/pages/stories/StoryDetailPage.tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoryDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/stories/StoryDetailPage.tsx"],"sourcesContent":["/**\n * StoryDetailPage — a single story in two complementary surfaces.\n * @module theculturepost/pages/stories\n *\n * A `?view=` toggle (with a sub-tab control) splits the page into:\n * - READING (tcp-03): a cover-banner hero, serif display title, a drop-cap\n * article body with ornamental section breaks, and a right rail of citations\n * (compact) + linked discussions.\n * - EDITORIAL (tcp-04): a visual lifecycle stepper with validated transitions\n * (Publish / Send back), plus citations as a verifiable-source table with an\n * inline verify control.\n *\n * Reads `tcpStory` (+ citations), `tcpListDiscussions(storyId)`. Writes\n * `tcpTransitionStory`, `tcpUpdateStory`, `tcpVerifyCitation`. Person references\n * are id-only, so we render \"You\" / \"Contributor\".\n */\n\nimport { Fragment, useMemo, useState, type ReactNode } from 'react';\nimport { useParams, useSearchParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Bookmark,\n BookOpen,\n ChevronDown,\n Clock,\n GitBranch,\n Languages,\n Lightbulb,\n Lock,\n MessagesSquare,\n Pencil,\n Plus,\n RotateCcw,\n Scroll,\n Send,\n Share2,\n ShieldCheck,\n Sparkles,\n Tag,\n Archive as ArchiveIcon,\n type LucideIcon,\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 ResponsiveTable,\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageHero } from '../../components/PageHero';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { LifecycleStepper } from '../../components/LifecycleStepper';\nimport { DecorativeDivider } from '../../components/DecorativeDivider';\nimport { AuthorLabel } from '../../components/AuthorLabel';\nimport {\n CitationList,\n sourceTypeIcon,\n VERIFICATION_META,\n VERIFICATIONS,\n} from '../../components/CitationList';\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 } from '../../components/StateViews';\nimport {\n useTcpStory,\n useTcpTransitionStory,\n useTcpUpdateStory,\n useTcpToggleStoryBookmark,\n useTcpTopics,\n useTcpDiscussions,\n useTcpCitations,\n useTcpVerifyCitation,\n FILE_FOLDERS,\n} from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { cn } from '../../utils/cn';\nimport { share, impact, nativeToast } from '../../utils/nativeBridge';\nimport {\n formatDate,\n formatEnumLabel,\n formatNumber,\n formatRelativeTime,\n} from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type {\n TcpCitation,\n TcpDiscussion,\n TcpStory,\n TcpStoryFormat,\n TcpStoryStatus,\n TcpVerificationStatus,\n} from '../../types';\n\ntype StoryView = 'reading' | 'editorial';\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\ninterface TransitionMetaEntry {\n label: string;\n icon: LucideIcon;\n primary?: boolean;\n}\n\nconst TRANSITION_META: Record<TcpStoryStatus, TransitionMetaEntry> = {\n IDEA: { label: 'Move to Idea', icon: Lightbulb },\n DRAFT: { label: 'Send back to Draft', icon: RotateCcw },\n IN_REVIEW: { label: 'Send to Review', icon: Send },\n PUBLISHED: { label: 'Publish', icon: Send, primary: true },\n ARCHIVED: { label: 'Archive', icon: ArchiveIcon },\n};\n\n/** Semantic-token colour for a verification icon (matches CitationList legend). */\nfunction verificationIconClass(status: TcpVerificationStatus): string {\n switch (VERIFICATION_META[status].accent) {\n case 'success':\n return 'text-status-success-text';\n case 'warning':\n return 'text-status-warning-text';\n case 'destructive':\n return 'text-status-error-text';\n default:\n return 'text-text-tertiary';\n }\n}\n\n// ===========================================================================\n// Page\n// ===========================================================================\n\nexport function StoryDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const [searchParams, setSearchParams] = useSearchParams();\n\n const view: StoryView = searchParams.get('view') === 'editorial' ? 'editorial' : 'reading';\n const setView = (next: StoryView) => {\n setSearchParams(\n (prev) => {\n const params = new URLSearchParams(prev);\n if (next === 'editorial') params.set('view', 'editorial');\n else params.delete('view');\n return params;\n },\n { replace: true }\n );\n };\n\n const { data: story, isLoading, isError, refetch } = useTcpStory(id);\n const toggleBookmark = useTcpToggleStoryBookmark();\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 if (isLoading) {\n return (\n <div className=\"mx-auto max-w-6xl p-4 sm:p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('stories')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Stories\n </Button>\n <LoadingView label=\"Loading story…\" />\n </div>\n );\n }\n\n if (isError || !story) {\n return (\n <div className=\"mx-auto max-w-6xl p-4 sm:p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('stories')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Stories\n </Button>\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 </div>\n );\n }\n\n const heroMeta = (\n <>\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 transition-colors hover:text-text-primary\"\n >\n <Tag className=\"h-3 w-3\" aria-hidden=\"true\" /> {story.topic.name}\n </button>\n )}\n <span className=\"inline-flex items-center gap-1.5 text-xs text-text-tertiary\">\n <AuthorLabel actorId={story.authorId} prefix=\"By\" withIcon />\n </span>\n {story.readingMins > 0 && (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-tertiary\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatNumber(story.readingMins)} min\n read\n </span>\n )}\n {story.languages.length > 0 && (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-tertiary\">\n <Languages className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {story.languages.map(formatEnumLabel).join(', ')}\n </span>\n )}\n </>\n );\n\n const tabs: TabItem<StoryView>[] = [\n { value: 'reading', label: 'Reading', icon: <BookOpen className=\"h-4 w-4\" /> },\n {\n value: 'editorial',\n label: 'Editorial',\n icon: <GitBranch className=\"h-4 w-4\" />,\n count: story.citations.length,\n },\n ];\n\n return (\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6 p-4 sm:p-6\">\n <PageHero\n eyebrow=\"Story\"\n title={story.title}\n description={story.summary ?? undefined}\n coverFileId={story.cover?.id ?? undefined}\n backAction={{ label: 'Stories', onClick: () => navigateTo('stories') }}\n meta={heroMeta}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button\n variant={story.isBookmarked ? 'default' : 'outline'}\n size=\"sm\"\n onClick={async () => {\n await impact('light');\n try {\n const bookmarked = await toggleBookmark.mutateAsync(story.id);\n await nativeToast(\n bookmarked ? 'Saved to your reading list' : 'Removed from your reading list'\n );\n } catch (error) {\n notify.error(describeError(error));\n }\n }}\n disabled={toggleBookmark.isPending}\n aria-pressed={story.isBookmarked}\n aria-label={story.isBookmarked ? 'Remove from reading list' : 'Save to reading list'}\n data-tour=\"theculturepost-story-bookmark\"\n >\n <Bookmark className={cn('mr-1.5 h-4 w-4', story.isBookmarked && 'fill-current')} />{' '}\n {story.isBookmarked ? 'Saved' : 'Save'}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={async () => {\n await impact('light');\n const ok = await share({\n title: story.title,\n text: story.summary ?? undefined,\n url: window.location.href,\n });\n if (ok) await nativeToast('Link shared');\n }}\n aria-label=\"Share story\"\n >\n <Share2 className=\"mr-1.5 h-4 w-4\" /> Share\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => setEditOpen(true)}\n data-tour=\"theculturepost-story-edit\"\n >\n <Pencil className=\"mr-1.5 h-4 w-4\" /> Edit\n </Button>\n </div>\n }\n />\n\n <div data-tour=\"theculturepost-story-tabs\">\n <Tabs items={tabs} value={view} onChange={setView} />\n </div>\n\n {view === 'reading' ? (\n <ReadingView\n story={story}\n discussions={linkedDiscussions}\n discussionsLoading={discussionsQuery.isLoading}\n onViewCitations={() => setView('editorial')}\n onOpenDiscussion={() => setDiscussionOpen(true)}\n onViewAllDiscussions={() => navigateTo('discussions')}\n onOpenDiscussionItem={(discussion) => navigateTo(`discussions/${discussion.id}`)}\n />\n ) : (\n <EditorialView story={story} onAddCitation={() => setCitationOpen(true)} />\n )}\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 </div>\n );\n}\n\n// ===========================================================================\n// Reading view (tcp-03)\n// ===========================================================================\n\ninterface ReadingViewProps {\n story: TcpStory;\n discussions: TcpDiscussion[];\n discussionsLoading: boolean;\n onViewCitations: () => void;\n onOpenDiscussion: () => void;\n onViewAllDiscussions: () => void;\n onOpenDiscussionItem: (discussion: TcpDiscussion) => void;\n}\n\nfunction ReadingView({\n story,\n discussions,\n discussionsLoading,\n onViewCitations,\n onOpenDiscussion,\n onViewAllDiscussions,\n onOpenDiscussionItem,\n}: ReadingViewProps) {\n return (\n <div className=\"grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_360px]\">\n {/* Article */}\n <article className=\"min-w-0\">\n <StoryBody body={story.body} />\n\n {story.tags.length > 0 && (\n <div className=\"mt-8 flex flex-wrap gap-1.5 border-t border-border-seam pt-6\">\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 </article>\n\n {/* Right rail */}\n <aside className=\"flex flex-col gap-6 lg:sticky lg:top-4 lg:self-start\">\n <RailPanel\n title=\"Citations\"\n action={\n <button\n type=\"button\"\n onClick={onViewCitations}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n View all\n </button>\n }\n >\n <CitationList storyId={story.id} compact />\n </RailPanel>\n\n <RailPanel\n title=\"Linked discussions\"\n action={\n <button\n type=\"button\"\n onClick={onViewAllDiscussions}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n View all\n </button>\n }\n >\n {discussionsLoading ? (\n <LoadingView label=\"Loading discussions…\" className=\"min-h-[8rem]\" />\n ) : discussions.length === 0 ? (\n <EmptyState\n icon={<MessagesSquare className=\"h-8 w-8\" />}\n title=\"No discussions yet\"\n description=\"Open a discussion to invite perspectives on this story.\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={onOpenDiscussion}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Open discussion\n </Button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {discussions.slice(0, 5).map((discussion) => (\n <LinkedDiscussion\n key={discussion.id}\n discussion={discussion}\n onOpen={() => onOpenDiscussionItem(discussion)}\n />\n ))}\n </div>\n )}\n </RailPanel>\n </aside>\n </div>\n );\n}\n\nfunction RailPanel({\n title,\n action,\n children,\n}: {\n title: string;\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <section>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"font-display text-lg font-semibold text-text-primary\">{title}</h2>\n {action}\n </div>\n {children}\n </section>\n );\n}\n\nfunction StoryBody({ body }: { body?: string | null }) {\n const paragraphs = useMemo(\n () =>\n (body ?? '')\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean),\n [body]\n );\n\n if (paragraphs.length === 0) {\n return (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-10 text-center text-sm text-text-tertiary\">\n This story has no body yet. Use Edit to write it.\n </p>\n );\n }\n\n return (\n <div className=\"max-w-prose\">\n {paragraphs.map((paragraph, index) => (\n <Fragment key={index}>\n {index === 1 && <DecorativeDivider />}\n <p\n className={cn(\n 'text-[1.05rem] leading-8 text-text-primary',\n index > 0 && index !== 1 && 'mt-5',\n index === 0 &&\n 'first-letter:float-left first-letter:mr-3 first-letter:mt-1 first-letter:font-display first-letter:text-6xl first-letter:font-semibold first-letter:leading-[0.72] first-letter:text-accent'\n )}\n >\n {paragraph}\n </p>\n </Fragment>\n ))}\n </div>\n );\n}\n\nfunction LinkedDiscussion({\n discussion,\n onOpen,\n}: {\n discussion: TcpDiscussion;\n onOpen: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"w-full rounded-xl border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:border-border-strong\"\n >\n <div className=\"flex items-start gap-2.5\">\n <span\n className=\"grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <MessagesSquare className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{discussion.title}</p>\n {discussion.prompt && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-tertiary\">{discussion.prompt}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-tertiary\">\n <span>\n {formatNumber(discussion.replyCount)}{' '}\n {discussion.replyCount === 1 ? 'reply' : 'replies'}\n </span>\n <span aria-hidden=\"true\">·</span>\n <span>{formatRelativeTime(discussion.createdAt)}</span>\n {discussion.isInterfaith && (\n <StatusBadge\n status=\"Interfaith\"\n icon={<Sparkles className=\"h-3 w-3\" />}\n className=\"ml-0.5\"\n />\n )}\n {discussion.isLocked && (\n <span className=\"inline-flex items-center gap-1 text-text-tertiary\">\n <Lock className=\"h-3 w-3\" aria-hidden=\"true\" /> Locked\n </span>\n )}\n </div>\n </div>\n </div>\n </button>\n );\n}\n\n// ===========================================================================\n// Editorial view (tcp-04)\n// ===========================================================================\n\nfunction EditorialView({ story, onAddCitation }: { story: TcpStory; onAddCitation: () => void }) {\n const transition = useTcpTransitionStory();\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 <div className=\"flex flex-col gap-6\">\n {/* Lifecycle */}\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2.5\">\n <span\n className=\"grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <GitBranch className=\"h-5 w-5\" />\n </span>\n <h2 className=\"font-display text-xl font-semibold text-text-primary\">\n Editorial lifecycle\n </h2>\n </div>\n\n <LifecycleStepper status={story.status} className=\"mt-6\" />\n\n <div className=\"mt-6 flex flex-col gap-4 border-t border-border-seam pt-5 md:flex-row md:items-center md:justify-between\">\n {nextStatuses.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {nextStatuses.map((status) => {\n const meta = TRANSITION_META[status];\n const Icon = meta.icon;\n return (\n <Button\n key={status}\n variant={meta.primary ? 'default' : 'outline'}\n onClick={() => doTransition(status)}\n disabled={transition.isPending}\n >\n <Icon className=\"mr-1.5 h-4 w-4\" aria-hidden=\"true\" />\n {meta.label}\n </Button>\n );\n })}\n </div>\n ) : (\n <p className=\"text-sm text-text-tertiary\">This story is at the end of its lifecycle.</p>\n )}\n\n <p className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <ShieldCheck className=\"h-4 w-4 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n Reviewed with fact and sensitivity care before publishing.\n </p>\n </div>\n </GlassCard>\n\n {/* Citations */}\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2.5\">\n <span\n className=\"grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <Scroll className=\"h-5 w-5\" />\n </span>\n <div>\n <h2 className=\"font-display text-xl font-semibold text-text-primary\">Citations</h2>\n <p className=\"text-sm text-text-secondary\">\n Verifiable sources that ground this story.\n </p>\n </div>\n </div>\n <Button variant=\"outline\" onClick={onAddCitation}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add citation\n </Button>\n </div>\n\n <EditorialCitationsTable storyId={story.id} className=\"mt-5\" />\n\n <p className=\"mt-4 flex items-center gap-2 border-t border-border-seam pt-4 text-xs text-text-tertiary\">\n <ShieldCheck className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" />\n All sources are evaluated for accuracy, relevance, and cultural sensitivity.\n </p>\n </GlassCard>\n </div>\n );\n}\n\nfunction EditorialCitationsTable({ storyId, className }: { storyId: string; className?: string }) {\n const { data: citations, isLoading, isError, refetch } = useTcpCitations(storyId);\n\n const columns: ResponsiveTableColumn<TcpCitation>[] = [\n {\n key: 'source',\n header: 'Source',\n priority: 'primary',\n cell: (citation) => {\n const Icon = sourceTypeIcon(citation.sourceType);\n const meta = [citation.author, citation.publisher].filter((v) => v && v.trim()).join(' · ');\n return (\n <div className=\"flex min-w-0 items-start gap-3\">\n <span\n className=\"grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary\">{citation.title}</p>\n {citation.excerpt && (\n <p className=\"mt-0.5 line-clamp-1 text-xs italic text-text-tertiary\">\n {citation.excerpt}\n </p>\n )}\n {meta && <p className=\"mt-0.5 text-xs text-text-tertiary\">{meta}</p>}\n </div>\n </div>\n );\n },\n },\n {\n key: 'type',\n header: 'Type',\n cell: (citation) => (\n <StatusBadge\n status={citation.sourceType}\n icon={(() => {\n const Icon = sourceTypeIcon(citation.sourceType);\n return <Icon className=\"h-3 w-3\" />;\n })()}\n />\n ),\n },\n {\n key: 'added',\n header: 'Added on',\n priority: 'hidden-on-mobile',\n cell: (citation) => (\n <span className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDate(citation.createdAt)}\n </span>\n ),\n },\n {\n key: 'verification',\n header: 'Status',\n cell: (citation) => {\n const meta = VERIFICATION_META[citation.verification];\n const Icon = meta.icon;\n return (\n <StatusBadge\n status={citation.verification}\n icon={<Icon className={cn('h-3 w-3', verificationIconClass(citation.verification))} />}\n />\n );\n },\n },\n {\n key: 'actions',\n header: 'Actions',\n headerClassName: 'text-right',\n cellClassName: 'text-right',\n cell: (citation) => <EditorialCitationActions citation={citation} />,\n },\n ];\n\n if (isLoading) return <LoadingView label=\"Loading citations…\" className={className} />;\n if (isError)\n return (\n <div className={className}>\n <ErrorView message=\"We couldn't load citations.\" onRetry={() => refetch()} />\n </div>\n );\n\n return (\n <ResponsiveTable<TcpCitation>\n className={className}\n columns={columns}\n data={citations ?? []}\n rowKey={(citation) => citation.id}\n emptyState={\n <EmptyState\n icon={<Scroll 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}\n\nfunction EditorialCitationActions({ citation }: { citation: TcpCitation }) {\n const verify = useTcpVerifyCitation();\n\n const change = async (verification: TcpVerificationStatus) => {\n if (verification === citation.verification) return;\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 return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={verify.isPending}\n aria-label={`Change verification for ${citation.title}`}\n >\n Verify <ChevronDown className=\"ml-1 h-4 w-4\" aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {VERIFICATIONS.map((status) => {\n const meta = VERIFICATION_META[status];\n const Icon = meta.icon;\n const current = status === citation.verification;\n return (\n <DropdownMenuItem\n key={status}\n onSelect={() => change(status)}\n className={cn('gap-2', current && 'font-semibold')}\n >\n <Icon className={cn('h-4 w-4', verificationIconClass(status))} aria-hidden=\"true\" />\n {meta.label}\n </DropdownMenuItem>\n );\n })}\n </DropdownMenuContent>\n </DropdownMenu>\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
|
+
{"version":3,"file":"StoryDetailPage.js","names":[],"sources":["../../../../src/theculturepost/pages/stories/StoryDetailPage.tsx"],"sourcesContent":["/**\n * StoryDetailPage — a single story in two complementary surfaces.\n * @module theculturepost/pages/stories\n *\n * A `?view=` toggle (with a sub-tab control) splits the page into:\n * - READING (tcp-03): a cover-banner hero, serif display title, a drop-cap\n * article body with ornamental section breaks, and a right rail of citations\n * (compact) + linked discussions.\n * - EDITORIAL (tcp-04): a visual lifecycle stepper with validated transitions\n * (Publish / Send back), plus citations as a verifiable-source table with an\n * inline verify control.\n *\n * Reads `tcpStory` (+ citations), `tcpListDiscussions(storyId)`. Writes\n * `tcpTransitionStory`, `tcpUpdateStory`, `tcpVerifyCitation`. Person references\n * are id-only, so we render \"You\" / \"Contributor\".\n */\n\nimport { Fragment, useMemo, useState, type ReactNode } from 'react';\nimport { useParams, useSearchParams } from 'react-router';\nimport {\n ArrowLeft,\n Bookmark,\n BookOpen,\n ChevronDown,\n Clock,\n GitBranch,\n Languages,\n Lightbulb,\n Lock,\n MessagesSquare,\n Pencil,\n Plus,\n RotateCcw,\n Scroll,\n Send,\n Share2,\n ShieldCheck,\n Sparkles,\n Tag,\n Archive as ArchiveIcon,\n type LucideIcon,\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 ResponsiveTable,\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageHero } from '../../components/PageHero';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { LifecycleStepper } from '../../components/LifecycleStepper';\nimport { DecorativeDivider } from '../../components/DecorativeDivider';\nimport { AuthorLabel } from '../../components/AuthorLabel';\nimport {\n CitationList,\n sourceTypeIcon,\n VERIFICATION_META,\n VERIFICATIONS,\n} from '../../components/CitationList';\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 } from '../../components/StateViews';\nimport {\n useTcpStory,\n useTcpTransitionStory,\n useTcpUpdateStory,\n useTcpToggleStoryBookmark,\n useTcpTopics,\n useTcpDiscussions,\n useTcpCitations,\n useTcpVerifyCitation,\n FILE_FOLDERS,\n} from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { cn } from '../../utils/cn';\nimport { share, impact, nativeToast } from '../../utils/nativeBridge';\nimport {\n formatDate,\n formatEnumLabel,\n formatNumber,\n formatRelativeTime,\n} from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type {\n TcpCitation,\n TcpDiscussion,\n TcpStory,\n TcpStoryFormat,\n TcpStoryStatus,\n TcpVerificationStatus,\n} from '../../types';\n\ntype StoryView = 'reading' | 'editorial';\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\ninterface TransitionMetaEntry {\n label: string;\n icon: LucideIcon;\n primary?: boolean;\n}\n\nconst TRANSITION_META: Record<TcpStoryStatus, TransitionMetaEntry> = {\n IDEA: { label: 'Move to Idea', icon: Lightbulb },\n DRAFT: { label: 'Send back to Draft', icon: RotateCcw },\n IN_REVIEW: { label: 'Send to Review', icon: Send },\n PUBLISHED: { label: 'Publish', icon: Send, primary: true },\n ARCHIVED: { label: 'Archive', icon: ArchiveIcon },\n};\n\n/** Semantic-token colour for a verification icon (matches CitationList legend). */\nfunction verificationIconClass(status: TcpVerificationStatus): string {\n switch (VERIFICATION_META[status].accent) {\n case 'success':\n return 'text-status-success-text';\n case 'warning':\n return 'text-status-warning-text';\n case 'destructive':\n return 'text-status-error-text';\n default:\n return 'text-text-tertiary';\n }\n}\n\n// ===========================================================================\n// Page\n// ===========================================================================\n\nexport function StoryDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useTheCulturePost();\n const [searchParams, setSearchParams] = useSearchParams();\n\n const view: StoryView = searchParams.get('view') === 'editorial' ? 'editorial' : 'reading';\n const setView = (next: StoryView) => {\n setSearchParams(\n (prev) => {\n const params = new URLSearchParams(prev);\n if (next === 'editorial') params.set('view', 'editorial');\n else params.delete('view');\n return params;\n },\n { replace: true }\n );\n };\n\n const { data: story, isLoading, isError, refetch } = useTcpStory(id);\n const toggleBookmark = useTcpToggleStoryBookmark();\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 if (isLoading) {\n return (\n <div className=\"mx-auto max-w-6xl p-4 sm:p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('stories')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Stories\n </Button>\n <LoadingView label=\"Loading story…\" />\n </div>\n );\n }\n\n if (isError || !story) {\n return (\n <div className=\"mx-auto max-w-6xl p-4 sm:p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('stories')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Stories\n </Button>\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 </div>\n );\n }\n\n const heroMeta = (\n <>\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 transition-colors hover:text-text-primary\"\n >\n <Tag className=\"h-3 w-3\" aria-hidden=\"true\" /> {story.topic.name}\n </button>\n )}\n <span className=\"inline-flex items-center gap-1.5 text-xs text-text-tertiary\">\n <AuthorLabel actorId={story.authorId} prefix=\"By\" withIcon />\n </span>\n {story.readingMins > 0 && (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-tertiary\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatNumber(story.readingMins)} min\n read\n </span>\n )}\n {story.languages.length > 0 && (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-tertiary\">\n <Languages className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {story.languages.map(formatEnumLabel).join(', ')}\n </span>\n )}\n </>\n );\n\n const tabs: TabItem<StoryView>[] = [\n { value: 'reading', label: 'Reading', icon: <BookOpen className=\"h-4 w-4\" /> },\n {\n value: 'editorial',\n label: 'Editorial',\n icon: <GitBranch className=\"h-4 w-4\" />,\n count: story.citations.length,\n },\n ];\n\n return (\n <div className=\"mx-auto flex max-w-6xl flex-col gap-6 p-4 sm:p-6\">\n <PageHero\n eyebrow=\"Story\"\n title={story.title}\n description={story.summary ?? undefined}\n coverFileId={story.cover?.id ?? undefined}\n backAction={{ label: 'Stories', onClick: () => navigateTo('stories') }}\n meta={heroMeta}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button\n variant={story.isBookmarked ? 'default' : 'outline'}\n size=\"sm\"\n onClick={async () => {\n await impact('light');\n try {\n const bookmarked = await toggleBookmark.mutateAsync(story.id);\n await nativeToast(\n bookmarked ? 'Saved to your reading list' : 'Removed from your reading list'\n );\n } catch (error) {\n notify.error(describeError(error));\n }\n }}\n disabled={toggleBookmark.isPending}\n aria-pressed={story.isBookmarked}\n aria-label={story.isBookmarked ? 'Remove from reading list' : 'Save to reading list'}\n data-tour=\"theculturepost-story-bookmark\"\n >\n <Bookmark className={cn('mr-1.5 h-4 w-4', story.isBookmarked && 'fill-current')} />{' '}\n {story.isBookmarked ? 'Saved' : 'Save'}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={async () => {\n await impact('light');\n const ok = await share({\n title: story.title,\n text: story.summary ?? undefined,\n url: window.location.href,\n });\n if (ok) await nativeToast('Link shared');\n }}\n aria-label=\"Share story\"\n >\n <Share2 className=\"mr-1.5 h-4 w-4\" /> Share\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => setEditOpen(true)}\n data-tour=\"theculturepost-story-edit\"\n >\n <Pencil className=\"mr-1.5 h-4 w-4\" /> Edit\n </Button>\n </div>\n }\n />\n\n <div data-tour=\"theculturepost-story-tabs\">\n <Tabs items={tabs} value={view} onChange={setView} />\n </div>\n\n {view === 'reading' ? (\n <ReadingView\n story={story}\n discussions={linkedDiscussions}\n discussionsLoading={discussionsQuery.isLoading}\n onViewCitations={() => setView('editorial')}\n onOpenDiscussion={() => setDiscussionOpen(true)}\n onViewAllDiscussions={() => navigateTo('discussions')}\n onOpenDiscussionItem={(discussion) => navigateTo(`discussions/${discussion.id}`)}\n />\n ) : (\n <EditorialView story={story} onAddCitation={() => setCitationOpen(true)} />\n )}\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 </div>\n );\n}\n\n// ===========================================================================\n// Reading view (tcp-03)\n// ===========================================================================\n\ninterface ReadingViewProps {\n story: TcpStory;\n discussions: TcpDiscussion[];\n discussionsLoading: boolean;\n onViewCitations: () => void;\n onOpenDiscussion: () => void;\n onViewAllDiscussions: () => void;\n onOpenDiscussionItem: (discussion: TcpDiscussion) => void;\n}\n\nfunction ReadingView({\n story,\n discussions,\n discussionsLoading,\n onViewCitations,\n onOpenDiscussion,\n onViewAllDiscussions,\n onOpenDiscussionItem,\n}: ReadingViewProps) {\n return (\n <div className=\"grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_360px]\">\n {/* Article */}\n <article className=\"min-w-0\">\n <StoryBody body={story.body} />\n\n {story.tags.length > 0 && (\n <div className=\"mt-8 flex flex-wrap gap-1.5 border-t border-border-seam pt-6\">\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 </article>\n\n {/* Right rail */}\n <aside className=\"flex flex-col gap-6 lg:sticky lg:top-4 lg:self-start\">\n <RailPanel\n title=\"Citations\"\n action={\n <button\n type=\"button\"\n onClick={onViewCitations}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n View all\n </button>\n }\n >\n <CitationList storyId={story.id} compact />\n </RailPanel>\n\n <RailPanel\n title=\"Linked discussions\"\n action={\n <button\n type=\"button\"\n onClick={onViewAllDiscussions}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n View all\n </button>\n }\n >\n {discussionsLoading ? (\n <LoadingView label=\"Loading discussions…\" className=\"min-h-[8rem]\" />\n ) : discussions.length === 0 ? (\n <EmptyState\n icon={<MessagesSquare className=\"h-8 w-8\" />}\n title=\"No discussions yet\"\n description=\"Open a discussion to invite perspectives on this story.\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={onOpenDiscussion}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Open discussion\n </Button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {discussions.slice(0, 5).map((discussion) => (\n <LinkedDiscussion\n key={discussion.id}\n discussion={discussion}\n onOpen={() => onOpenDiscussionItem(discussion)}\n />\n ))}\n </div>\n )}\n </RailPanel>\n </aside>\n </div>\n );\n}\n\nfunction RailPanel({\n title,\n action,\n children,\n}: {\n title: string;\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <section>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"font-display text-lg font-semibold text-text-primary\">{title}</h2>\n {action}\n </div>\n {children}\n </section>\n );\n}\n\nfunction StoryBody({ body }: { body?: string | null }) {\n const paragraphs = useMemo(\n () =>\n (body ?? '')\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean),\n [body]\n );\n\n if (paragraphs.length === 0) {\n return (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-10 text-center text-sm text-text-tertiary\">\n This story has no body yet. Use Edit to write it.\n </p>\n );\n }\n\n return (\n <div className=\"max-w-prose\">\n {paragraphs.map((paragraph, index) => (\n <Fragment key={index}>\n {index === 1 && <DecorativeDivider />}\n <p\n className={cn(\n 'text-[1.05rem] leading-8 text-text-primary',\n index > 0 && index !== 1 && 'mt-5',\n index === 0 &&\n 'first-letter:float-left first-letter:mr-3 first-letter:mt-1 first-letter:font-display first-letter:text-6xl first-letter:font-semibold first-letter:leading-[0.72] first-letter:text-accent'\n )}\n >\n {paragraph}\n </p>\n </Fragment>\n ))}\n </div>\n );\n}\n\nfunction LinkedDiscussion({\n discussion,\n onOpen,\n}: {\n discussion: TcpDiscussion;\n onOpen: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"w-full rounded-xl border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:border-border-strong\"\n >\n <div className=\"flex items-start gap-2.5\">\n <span\n className=\"grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <MessagesSquare className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{discussion.title}</p>\n {discussion.prompt && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-tertiary\">{discussion.prompt}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-tertiary\">\n <span>\n {formatNumber(discussion.replyCount)}{' '}\n {discussion.replyCount === 1 ? 'reply' : 'replies'}\n </span>\n <span aria-hidden=\"true\">·</span>\n <span>{formatRelativeTime(discussion.createdAt)}</span>\n {discussion.isInterfaith && (\n <StatusBadge\n status=\"Interfaith\"\n icon={<Sparkles className=\"h-3 w-3\" />}\n className=\"ml-0.5\"\n />\n )}\n {discussion.isLocked && (\n <span className=\"inline-flex items-center gap-1 text-text-tertiary\">\n <Lock className=\"h-3 w-3\" aria-hidden=\"true\" /> Locked\n </span>\n )}\n </div>\n </div>\n </div>\n </button>\n );\n}\n\n// ===========================================================================\n// Editorial view (tcp-04)\n// ===========================================================================\n\nfunction EditorialView({ story, onAddCitation }: { story: TcpStory; onAddCitation: () => void }) {\n const transition = useTcpTransitionStory();\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 <div className=\"flex flex-col gap-6\">\n {/* Lifecycle */}\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2.5\">\n <span\n className=\"grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <GitBranch className=\"h-5 w-5\" />\n </span>\n <h2 className=\"font-display text-xl font-semibold text-text-primary\">\n Editorial lifecycle\n </h2>\n </div>\n\n <LifecycleStepper status={story.status} className=\"mt-6\" />\n\n <div className=\"mt-6 flex flex-col gap-4 border-t border-border-seam pt-5 md:flex-row md:items-center md:justify-between\">\n {nextStatuses.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {nextStatuses.map((status) => {\n const meta = TRANSITION_META[status];\n const Icon = meta.icon;\n return (\n <Button\n key={status}\n variant={meta.primary ? 'default' : 'outline'}\n onClick={() => doTransition(status)}\n disabled={transition.isPending}\n >\n <Icon className=\"mr-1.5 h-4 w-4\" aria-hidden=\"true\" />\n {meta.label}\n </Button>\n );\n })}\n </div>\n ) : (\n <p className=\"text-sm text-text-tertiary\">This story is at the end of its lifecycle.</p>\n )}\n\n <p className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <ShieldCheck className=\"h-4 w-4 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n Reviewed with fact and sensitivity care before publishing.\n </p>\n </div>\n </GlassCard>\n\n {/* Citations */}\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2.5\">\n <span\n className=\"grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <Scroll className=\"h-5 w-5\" />\n </span>\n <div>\n <h2 className=\"font-display text-xl font-semibold text-text-primary\">Citations</h2>\n <p className=\"text-sm text-text-secondary\">\n Verifiable sources that ground this story.\n </p>\n </div>\n </div>\n <Button variant=\"outline\" onClick={onAddCitation}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add citation\n </Button>\n </div>\n\n <EditorialCitationsTable storyId={story.id} className=\"mt-5\" />\n\n <p className=\"mt-4 flex items-center gap-2 border-t border-border-seam pt-4 text-xs text-text-tertiary\">\n <ShieldCheck className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" />\n All sources are evaluated for accuracy, relevance, and cultural sensitivity.\n </p>\n </GlassCard>\n </div>\n );\n}\n\nfunction EditorialCitationsTable({ storyId, className }: { storyId: string; className?: string }) {\n const { data: citations, isLoading, isError, refetch } = useTcpCitations(storyId);\n\n const columns: ResponsiveTableColumn<TcpCitation>[] = [\n {\n key: 'source',\n header: 'Source',\n priority: 'primary',\n cell: (citation) => {\n const Icon = sourceTypeIcon(citation.sourceType);\n const meta = [citation.author, citation.publisher].filter((v) => v && v.trim()).join(' · ');\n return (\n <div className=\"flex min-w-0 items-start gap-3\">\n <span\n className=\"grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary\">{citation.title}</p>\n {citation.excerpt && (\n <p className=\"mt-0.5 line-clamp-1 text-xs italic text-text-tertiary\">\n {citation.excerpt}\n </p>\n )}\n {meta && <p className=\"mt-0.5 text-xs text-text-tertiary\">{meta}</p>}\n </div>\n </div>\n );\n },\n },\n {\n key: 'type',\n header: 'Type',\n cell: (citation) => (\n <StatusBadge\n status={citation.sourceType}\n icon={(() => {\n const Icon = sourceTypeIcon(citation.sourceType);\n return <Icon className=\"h-3 w-3\" />;\n })()}\n />\n ),\n },\n {\n key: 'added',\n header: 'Added on',\n priority: 'hidden-on-mobile',\n cell: (citation) => (\n <span className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDate(citation.createdAt)}\n </span>\n ),\n },\n {\n key: 'verification',\n header: 'Status',\n cell: (citation) => {\n const meta = VERIFICATION_META[citation.verification];\n const Icon = meta.icon;\n return (\n <StatusBadge\n status={citation.verification}\n icon={<Icon className={cn('h-3 w-3', verificationIconClass(citation.verification))} />}\n />\n );\n },\n },\n {\n key: 'actions',\n header: 'Actions',\n headerClassName: 'text-right',\n cellClassName: 'text-right',\n cell: (citation) => <EditorialCitationActions citation={citation} />,\n },\n ];\n\n if (isLoading) return <LoadingView label=\"Loading citations…\" className={className} />;\n if (isError)\n return (\n <div className={className}>\n <ErrorView message=\"We couldn't load citations.\" onRetry={() => refetch()} />\n </div>\n );\n\n return (\n <ResponsiveTable<TcpCitation>\n className={className}\n columns={columns}\n data={citations ?? []}\n rowKey={(citation) => citation.id}\n emptyState={\n <EmptyState\n icon={<Scroll 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}\n\nfunction EditorialCitationActions({ citation }: { citation: TcpCitation }) {\n const verify = useTcpVerifyCitation();\n\n const change = async (verification: TcpVerificationStatus) => {\n if (verification === citation.verification) return;\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 return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={verify.isPending}\n aria-label={`Change verification for ${citation.title}`}\n >\n Verify <ChevronDown className=\"ml-1 h-4 w-4\" aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {VERIFICATIONS.map((status) => {\n const meta = VERIFICATION_META[status];\n const Icon = meta.icon;\n const current = status === citation.verification;\n return (\n <DropdownMenuItem\n key={status}\n onSelect={() => change(status)}\n className={cn('gap-2', current && 'font-semibold')}\n >\n <Icon className={cn('h-4 w-4', verificationIconClass(status))} aria-hidden=\"true\" />\n {meta.label}\n </DropdownMenuItem>\n );\n })}\n </DropdownMenuContent>\n </DropdownMenu>\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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"}
|
|
@@ -14,7 +14,7 @@ import { CreateStoryDialog as m } from "../../components/CreateStoryDialog.js";
|
|
|
14
14
|
import "../../components/index.js";
|
|
15
15
|
import { createElement as h, useMemo as g, useState as _ } from "react";
|
|
16
16
|
import { Archive as v, ArrowLeft as y, BookOpen as b, Globe as x, Pencil as S, Plus as C, Sparkles as w } from "lucide-react";
|
|
17
|
-
import { useParams as T } from "react-router
|
|
17
|
+
import { useParams as T } from "react-router";
|
|
18
18
|
import { Fragment as E, jsx as D, jsxs as O } from "react/jsx-runtime";
|
|
19
19
|
import { Button as k, Input as A, Label as j, ResponsiveDialog as M, ResponsiveDialogContent as N, ResponsiveDialogDescription as P, ResponsiveDialogFooter as F, ResponsiveDialogHeader as I, ResponsiveDialogTitle as L, Select as R, SelectContent as z, SelectItem as B, SelectTrigger as V, SelectValue as H, Textarea as U } from "@burdenoff/fe-libs/ui";
|
|
20
20
|
//#region src/theculturepost/pages/topics/TopicDetailPage.tsx
|
|
@@ -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 * An editorial {@link PageHero} banner carries the title, description and the\n * Featured / category / tradition badges, plus the feature-toggle, edit and\n * archive actions (`tcpUpdateTopic` / `tcpArchiveTopic`). Below, the topic's own\n * `stories` are shown in a richer {@link StoryCard} grid with a real count and a\n * client-side sort. Reads `tcpTopic` (with its stories) — no fabricated fields.\n */\n\nimport { createElement, useMemo, useState, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Globe, Sparkles, Pencil, Archive, Plus, BookOpen } from 'lucide-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 {\n PageHero,\n StatusBadge,\n StoryCard,\n CreateStoryDialog,\n EmptyState,\n LoadingView,\n ErrorView,\n categoryIcon,\n} from '../../components';\nimport { useTcpTopic, useTcpUpdateTopic, useTcpArchiveTopic } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpStory, 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\ntype SortKey = 'newest' | 'oldest' | 'updated' | 'title';\n\nconst SORT_OPTIONS: { value: SortKey; label: string }[] = [\n { value: 'newest', label: 'Newest' },\n { value: 'oldest', label: 'Oldest' },\n { value: 'updated', label: 'Recently updated' },\n { value: 'title', label: 'Title (A–Z)' },\n];\n\n/** ms timestamp of a story's effective publish date (published, else created). */\nconst publishedTs = (story: TcpStory): number => {\n const t = new Date(story.publishedAt ?? story.createdAt).getTime();\n return Number.isNaN(t) ? 0 : t;\n};\n\nconst updatedTs = (story: TcpStory): number => {\n const t = new Date(story.updatedAt).getTime();\n return Number.isNaN(t) ? 0 : t;\n};\n\nfunction sortStories(stories: TcpStory[], key: SortKey): TcpStory[] {\n const copy = [...stories];\n switch (key) {\n case 'newest':\n return copy.sort((a, b) => publishedTs(b) - publishedTs(a));\n case 'oldest':\n return copy.sort((a, b) => publishedTs(a) - publishedTs(b));\n case 'updated':\n return copy.sort((a, b) => updatedTs(b) - updatedTs(a));\n case 'title':\n return copy.sort((a, b) => a.title.localeCompare(b.title));\n default:\n return copy;\n }\n}\n\n/** Minimal scroll shell so hero/loading/error surfaces share one container. */\nfunction DetailShell({ children }: { children: ReactNode }) {\n return (\n <div className=\"h-full overflow-y-auto\">\n <div className=\"mx-auto flex max-w-5xl flex-col gap-6 p-4 sm:p-6\">{children}</div>\n </div>\n );\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 const [sort, setSort] = useState<SortKey>('newest');\n\n const stories = useMemo(() => topic?.stories ?? [], [topic]);\n const sortedStories = useMemo(() => sortStories(stories, sort), [stories, sort]);\n\n const backToTopics = () => navigateTo('topics');\n\n if (isLoading) {\n return (\n <DetailShell>\n <button\n type=\"button\"\n onClick={backToTopics}\n className=\"inline-flex w-fit items-center gap-1.5 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" /> Topics\n </button>\n <LoadingView label=\"Loading topic…\" />\n </DetailShell>\n );\n }\n\n if (isError || !topic) {\n return (\n <DetailShell>\n <button\n type=\"button\"\n onClick={backToTopics}\n className=\"inline-flex w-fit items-center gap-1.5 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" /> Topics\n </button>\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 </DetailShell>\n );\n }\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 const heroMeta = (\n <>\n {topic.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-warning-text\">\n <Sparkles className=\"h-3 w-3\" aria-hidden=\"true\" /> Featured\n </span>\n )}\n <StatusBadge\n status={topic.category}\n icon={createElement(categoryIcon(topic.category), { className: 'h-3 w-3' })}\n />\n {topic.tradition && (\n <span 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\">\n <Globe className=\"h-3 w-3\" aria-hidden=\"true\" /> {topic.tradition}\n </span>\n )}\n </>\n );\n\n const heroActions = (\n <>\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=\"outline\"\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 <Button size=\"sm\" onClick={() => setCreateStoryOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n </>\n );\n\n return (\n <DetailShell>\n <PageHero\n eyebrow=\"Topic\"\n title={topic.name}\n description={topic.description ?? undefined}\n backAction={{ label: 'Topics', onClick: backToTopics }}\n meta={heroMeta}\n actions={heroActions}\n />\n\n <section aria-labelledby=\"topic-stories-heading\">\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <h2\n id=\"topic-stories-heading\"\n className=\"font-display text-xl font-semibold text-text-primary\"\n >\n Stories in this topic\n </h2>\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-sm font-medium tabular-nums text-text-secondary\">\n {stories.length}\n </span>\n </div>\n {stories.length > 1 && (\n <div className=\"flex items-center gap-2\">\n <Label htmlFor=\"topic-story-sort\" className=\"text-sm text-text-secondary\">\n Sort by\n </Label>\n <Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>\n <SelectTrigger\n id=\"topic-story-sort\"\n className=\"w-[12rem]\"\n aria-label=\"Sort stories\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SORT_OPTIONS.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\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 {sortedStories.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\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 </DetailShell>\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 * An editorial {@link PageHero} banner carries the title, description and the\n * Featured / category / tradition badges, plus the feature-toggle, edit and\n * archive actions (`tcpUpdateTopic` / `tcpArchiveTopic`). Below, the topic's own\n * `stories` are shown in a richer {@link StoryCard} grid with a real count and a\n * client-side sort. Reads `tcpTopic` (with its stories) — no fabricated fields.\n */\n\nimport { createElement, useMemo, useState, type ReactNode } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, Globe, Sparkles, Pencil, Archive, Plus, BookOpen } from 'lucide-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 {\n PageHero,\n StatusBadge,\n StoryCard,\n CreateStoryDialog,\n EmptyState,\n LoadingView,\n ErrorView,\n categoryIcon,\n} from '../../components';\nimport { useTcpTopic, useTcpUpdateTopic, useTcpArchiveTopic } from '../../hooks';\nimport { useTheCulturePost } from '../../context';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type { TcpStory, 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\ntype SortKey = 'newest' | 'oldest' | 'updated' | 'title';\n\nconst SORT_OPTIONS: { value: SortKey; label: string }[] = [\n { value: 'newest', label: 'Newest' },\n { value: 'oldest', label: 'Oldest' },\n { value: 'updated', label: 'Recently updated' },\n { value: 'title', label: 'Title (A–Z)' },\n];\n\n/** ms timestamp of a story's effective publish date (published, else created). */\nconst publishedTs = (story: TcpStory): number => {\n const t = new Date(story.publishedAt ?? story.createdAt).getTime();\n return Number.isNaN(t) ? 0 : t;\n};\n\nconst updatedTs = (story: TcpStory): number => {\n const t = new Date(story.updatedAt).getTime();\n return Number.isNaN(t) ? 0 : t;\n};\n\nfunction sortStories(stories: TcpStory[], key: SortKey): TcpStory[] {\n const copy = [...stories];\n switch (key) {\n case 'newest':\n return copy.sort((a, b) => publishedTs(b) - publishedTs(a));\n case 'oldest':\n return copy.sort((a, b) => publishedTs(a) - publishedTs(b));\n case 'updated':\n return copy.sort((a, b) => updatedTs(b) - updatedTs(a));\n case 'title':\n return copy.sort((a, b) => a.title.localeCompare(b.title));\n default:\n return copy;\n }\n}\n\n/** Minimal scroll shell so hero/loading/error surfaces share one container. */\nfunction DetailShell({ children }: { children: ReactNode }) {\n return (\n <div className=\"h-full overflow-y-auto\">\n <div className=\"mx-auto flex max-w-5xl flex-col gap-6 p-4 sm:p-6\">{children}</div>\n </div>\n );\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 const [sort, setSort] = useState<SortKey>('newest');\n\n const stories = useMemo(() => topic?.stories ?? [], [topic]);\n const sortedStories = useMemo(() => sortStories(stories, sort), [stories, sort]);\n\n const backToTopics = () => navigateTo('topics');\n\n if (isLoading) {\n return (\n <DetailShell>\n <button\n type=\"button\"\n onClick={backToTopics}\n className=\"inline-flex w-fit items-center gap-1.5 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" /> Topics\n </button>\n <LoadingView label=\"Loading topic…\" />\n </DetailShell>\n );\n }\n\n if (isError || !topic) {\n return (\n <DetailShell>\n <button\n type=\"button\"\n onClick={backToTopics}\n className=\"inline-flex w-fit items-center gap-1.5 text-sm font-medium text-text-secondary hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden=\"true\" /> Topics\n </button>\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 </DetailShell>\n );\n }\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 const heroMeta = (\n <>\n {topic.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-warning-text\">\n <Sparkles className=\"h-3 w-3\" aria-hidden=\"true\" /> Featured\n </span>\n )}\n <StatusBadge\n status={topic.category}\n icon={createElement(categoryIcon(topic.category), { className: 'h-3 w-3' })}\n />\n {topic.tradition && (\n <span 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\">\n <Globe className=\"h-3 w-3\" aria-hidden=\"true\" /> {topic.tradition}\n </span>\n )}\n </>\n );\n\n const heroActions = (\n <>\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=\"outline\"\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 <Button size=\"sm\" onClick={() => setCreateStoryOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New story\n </Button>\n </>\n );\n\n return (\n <DetailShell>\n <PageHero\n eyebrow=\"Topic\"\n title={topic.name}\n description={topic.description ?? undefined}\n backAction={{ label: 'Topics', onClick: backToTopics }}\n meta={heroMeta}\n actions={heroActions}\n />\n\n <section aria-labelledby=\"topic-stories-heading\">\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <h2\n id=\"topic-stories-heading\"\n className=\"font-display text-xl font-semibold text-text-primary\"\n >\n Stories in this topic\n </h2>\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-sm font-medium tabular-nums text-text-secondary\">\n {stories.length}\n </span>\n </div>\n {stories.length > 1 && (\n <div className=\"flex items-center gap-2\">\n <Label htmlFor=\"topic-story-sort\" className=\"text-sm text-text-secondary\">\n Sort by\n </Label>\n <Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>\n <SelectTrigger\n id=\"topic-story-sort\"\n className=\"w-[12rem]\"\n aria-label=\"Sort stories\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SORT_OPTIONS.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\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 {sortedStories.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\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 </DetailShell>\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.
|
|
3
|
+
"version": "2026.801.1",
|
|
4
4
|
"description": "Burdenoff TheCulturePost Microfrontend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"types": "./dist/index.d.ts",
|
|
51
51
|
"scripts": {
|
|
52
52
|
"dev": "vite",
|
|
53
|
-
"build": "tsc && vite build",
|
|
53
|
+
"build": "tsc && bash scripts/build-limits.sh --gate vite build",
|
|
54
54
|
"lint": "eslint .",
|
|
55
55
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
56
|
-
"type:check": "tsc --noEmit",
|
|
57
|
-
"sanity": "bun run codegen && bun run lint && bun run format && bun run type:check && bun run build && bun run build-storybook",
|
|
56
|
+
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
57
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint && bun run format && bun run type:check && bun run build && bun run build-storybook'",
|
|
58
58
|
"storybook": "storybook dev -p 6006",
|
|
59
59
|
"build-storybook": "storybook build",
|
|
60
60
|
"test:storybook": "test-storybook",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@apollo/client": "^4.0.11",
|
|
73
|
-
"@burdenoff/fe-libs": "2026.
|
|
73
|
+
"@burdenoff/fe-libs": "2026.801.1",
|
|
74
74
|
"@tanstack/react-query": "^5.90.16",
|
|
75
75
|
"clsx": "^2.1.1",
|
|
76
76
|
"graphql": "^16.10.0",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"sonner": "^2.0.7",
|
|
79
79
|
"react": "^19.0.0",
|
|
80
80
|
"react-dom": "^19.0.0",
|
|
81
|
-
"react-router
|
|
81
|
+
"react-router": "^8.3.0",
|
|
82
82
|
"tailwind-merge": "^3.4.0",
|
|
83
83
|
"zustand": "^5.0.3",
|
|
84
84
|
"recharts": "^2.15.0",
|