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 ( +
+