From 5a4f43783ae2db54426f176fd6133ef5f71ffbd4 Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 20 Jul 2026 12:54:44 +0530 Subject: [PATCH 1/2] feat(publications): add the Resource frontend (detail, listing, authoring, linking) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read side: - (user)/publications/[id]: detail page with inline PDF + YouTube and download cards for other file types; Explore listing at /publications; 'Resources' entry in the Explore nav. - Unified search: publication type + detail redirect. Authoring (dashboard): - publications list + Create; create/edit shell (Metadata / Content / Publish tabs) with a shared typed metadata form (title, abstract, authors, date, license, Resource Type, sectors, geographies, external link — a normal form, not the dynamic-metadata renderer); block editor (YouTube + file dropzone, reorder, remove); publish toggle + 'linked to N' flag. - Resources sidebar entry on the dashboard. Linking: - PublicationLinkPicker embedded in the Use Case & Collaborative assign pages (published resources only). Infra: single RESOURCE_LABEL constant (UI says 'Resource', code says publication); codegen documents glob widened to include components/. Verified: codegen + tsc (zero new errors) + eslint clean. --- .../PublicationDetailsPage.tsx | 139 ++++++++++ .../[publicationId]/components/Blocks.tsx | 113 ++++++++ .../[publicationId]/components/Metadata.tsx | 46 ++++ .../components/PrimaryData.tsx | 51 ++++ .../publications/[publicationId]/page.tsx | 41 +++ app/[locale]/(user)/publications/page.tsx | 56 ++++ .../components/UnifiedListingComponent.tsx | 6 +- .../collaboratives/edit/[id]/assign/page.tsx | 2 + .../[entityType]/[entitySlug]/layout.tsx | 7 +- .../[entitySlug]/publications/create/page.tsx | 76 ++++++ .../publications/edit/[id]/blocks/page.tsx | 231 ++++++++++++++++ .../publications/edit/[id]/metadata/page.tsx | 117 +++++++++ .../publications/edit/[id]/page.tsx | 22 ++ .../publications/edit/[id]/publish/page.tsx | 144 ++++++++++ .../publications/edit/context.tsx | 34 +++ .../[entitySlug]/publications/edit/layout.tsx | 62 +++++ .../[entitySlug]/publications/page.tsx | 71 +++++ .../usecases/edit/[id]/assign/page.tsx | 2 + .../dashboard/components/main-nav.tsx | 4 + components/publications/PublicationForm.tsx | 246 ++++++++++++++++++ .../publications/PublicationLinkPicker.tsx | 177 +++++++++++++ config/codegen.ts | 2 +- lib/constants/resourceLabel.ts | 11 + 23 files changed, 1655 insertions(+), 5 deletions(-) create mode 100644 app/[locale]/(user)/publications/[publicationId]/PublicationDetailsPage.tsx create mode 100644 app/[locale]/(user)/publications/[publicationId]/components/Blocks.tsx create mode 100644 app/[locale]/(user)/publications/[publicationId]/components/Metadata.tsx create mode 100644 app/[locale]/(user)/publications/[publicationId]/components/PrimaryData.tsx create mode 100644 app/[locale]/(user)/publications/[publicationId]/page.tsx create mode 100644 app/[locale]/(user)/publications/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/create/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/edit/[id]/blocks/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/edit/[id]/metadata/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/edit/[id]/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/edit/[id]/publish/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/edit/context.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/edit/layout.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/publications/page.tsx create mode 100644 components/publications/PublicationForm.tsx create mode 100644 components/publications/PublicationLinkPicker.tsx create mode 100644 lib/constants/resourceLabel.ts diff --git a/app/[locale]/(user)/publications/[publicationId]/PublicationDetailsPage.tsx b/app/[locale]/(user)/publications/[publicationId]/PublicationDetailsPage.tsx new file mode 100644 index 00000000..368a2655 --- /dev/null +++ b/app/[locale]/(user)/publications/[publicationId]/PublicationDetailsPage.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { graphql } from '@/gql'; +import { useQuery } from '@tanstack/react-query'; +import { Spinner, Text } from 'opub-ui'; + +import BreadCrumbs from '@/components/BreadCrumbs'; +import JsonLd from '@/components/JsonLd'; +import { RESOURCE_LABEL, RESOURCE_PATH } from '@/lib/constants/resourceLabel'; +import { GraphQL } from '@/lib/api'; +import { generateJsonLd } from '@/lib/utils'; +import { Blocks } from './components/Blocks'; +import { Metadata } from './components/Metadata'; +import { PrimaryData } from './components/PrimaryData'; + +const publicationQuery = graphql(` + query getPublication($publicationId: UUID!) { + getPublication(publicationId: $publicationId) { + id + title + description + slug + status + authors + publicationDate + license + externalSourceLink + downloadCount + created + modified + resourceType { + id + name + } + organization { + id + name + logo { + url + } + } + user { + id + fullName + profilePicture { + url + } + } + sectors { + id + name + } + geographies { + id + name + } + blocks { + id + position + blockType + fileName + fileFormat + fileSize + youtubeUrl + youtubeVideoId + } + } + } +`); + +export default function PublicationDetailsPage({ + publicationId, +}: { + publicationId: string; +}) { + const { data, isLoading, error } = useQuery( + ['publication_details', publicationId], + () => GraphQL(publicationQuery, {}, { publicationId }), + { retry: false } + ); + + const publication = data?.getPublication; + + const jsonLd = generateJsonLd({ + '@context': 'https://schema.org', + '@type': 'CreativeWork', + name: publication?.title, + url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}${RESOURCE_PATH}/${publicationId}`, + description: publication?.description, + publisher: { + '@type': 'Organization', + name: 'CivicDataSpace', + url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}`, + }, + }); + + return ( + <> + +
+ +
+
+ {isLoading ? ( +
+ +
+ ) : error ? ( +
+ Error loading {RESOURCE_LABEL} + + {error instanceof Error ? error.message : 'Failed to fetch'} + +
+ ) : publication ? ( + <> + + + + ) : ( +
+ {RESOURCE_LABEL} not found +
+ )} +
+
+ {!isLoading && publication && } +
+
+
+ + ); +} diff --git a/app/[locale]/(user)/publications/[publicationId]/components/Blocks.tsx b/app/[locale]/(user)/publications/[publicationId]/components/Blocks.tsx new file mode 100644 index 00000000..257a0176 --- /dev/null +++ b/app/[locale]/(user)/publications/[publicationId]/components/Blocks.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { Button, Text } from 'opub-ui'; + +import { RESOURCE_LABEL } from '@/lib/constants/resourceLabel'; + +type Block = { + id: string; + position: number; + blockType: string; + fileName: string; + fileFormat: string; + fileSize?: number | null; + youtubeUrl?: string | null; + youtubeVideoId: string; +}; + +/** The gated download URL for a block's file (draft files are access-checked). */ +function blockDownloadUrl(blockId: string): string { + return `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/publications/blocks/${blockId}/download/`; +} + +/** Human-readable file size. */ +function formatSize(bytes?: number | null): string { + if (!bytes) return ''; + const mb = bytes / (1024 * 1024); + if (mb >= 1) return `${mb.toFixed(1)} MB`; + return `${Math.max(1, Math.round(bytes / 1024))} KB`; +} + +function YoutubeBlock({ videoId }: { videoId: string }) { + return ( +
+