Files
old.cms.fellies.org/apps/web/src/app/[locale]/portfolio/page.tsx

292 lines
9.1 KiB
TypeScript

import { listPublishedArtworks, listPublishedPortfolioGroups } from "@cms/db"
import Image from "next/image"
import { getTranslations } from "next-intl/server"
import { Link } from "@/i18n/navigation"
export const dynamic = "force-dynamic"
type SearchParamsInput = Record<string, string | string[] | undefined>
type PortfolioPageProps = {
searchParams: Promise<SearchParamsInput>
}
function readFirstValue(value: string | string[] | undefined): string | null {
if (Array.isArray(value)) {
return value[0] ?? null
}
return value ?? null
}
function resolveGroupFilter(searchParams: SearchParamsInput) {
const gallery = readFirstValue(searchParams.gallery)
if (gallery) {
return { groupType: "gallery" as const, groupSlug: gallery }
}
const album = readFirstValue(searchParams.album)
if (album) {
return { groupType: "album" as const, groupSlug: album }
}
const category = readFirstValue(searchParams.category)
if (category) {
return { groupType: "category" as const, groupSlug: category }
}
const tag = readFirstValue(searchParams.tag)
if (tag) {
return { groupType: "tag" as const, groupSlug: tag }
}
return null
}
function resolveSort(searchParams: SearchParamsInput): "latest" | "title_asc" | "title_desc" {
const sort = readFirstValue(searchParams.sort)
if (sort === "title_asc" || sort === "title_desc") {
return sort
}
return "latest"
}
function buildPortfolioQuery(
filter: ReturnType<typeof resolveGroupFilter>,
sort: ReturnType<typeof resolveSort>,
) {
const query: Record<string, string> = {}
if (filter) {
query[filter.groupType] = filter.groupSlug
}
if (sort !== "latest") {
query.sort = sort
}
return query
}
function findPreviewAsset(
renditions: Array<{
slot: string
mediaAssetId: string
mediaAsset: {
id: string
altText: string | null
title: string
}
}>,
) {
const byPreference =
renditions.find((item) => item.slot === "card") ??
renditions.find((item) => item.slot === "thumbnail") ??
renditions.find((item) => item.slot === "full") ??
renditions[0]
return byPreference ?? null
}
export default async function PortfolioPage({ searchParams }: PortfolioPageProps) {
const [resolvedSearchParams, t] = await Promise.all([searchParams, getTranslations("Portfolio")])
const activeFilter = resolveGroupFilter(resolvedSearchParams)
const activeSort = resolveSort(resolvedSearchParams)
const [groups, artworks] = await Promise.all([
listPublishedPortfolioGroups(),
listPublishedArtworks(
activeFilter
? {
groupType: activeFilter.groupType,
groupSlug: activeFilter.groupSlug,
sort: activeSort,
}
: {
sort: activeSort,
},
),
])
return (
<section className="mx-auto w-full max-w-6xl space-y-6 px-6 py-16">
<header className="space-y-2">
<p className="text-sm uppercase tracking-[0.2em] text-neutral-500">{t("badge")}</p>
<h1 className="text-4xl font-semibold tracking-tight">{t("title")}</h1>
<p className="text-neutral-600">{t("description")}</p>
</header>
<section className="rounded-xl border border-neutral-200 p-4">
<div className="flex flex-wrap items-center gap-2">
<Link
href={{
pathname: "/portfolio",
query: activeSort === "latest" ? undefined : { sort: activeSort },
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("filters.clear")}
</Link>
{groups.galleries.map((group) => (
<Link
key={`gallery-${group.id}`}
href={{
pathname: "/portfolio",
query: {
...buildPortfolioQuery(activeFilter, activeSort),
gallery: group.slug,
album: undefined,
category: undefined,
tag: undefined,
},
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("filters.gallery")}: {group.name}
</Link>
))}
{groups.albums.map((group) => (
<Link
key={`album-${group.id}`}
href={{
pathname: "/portfolio",
query: {
...buildPortfolioQuery(activeFilter, activeSort),
gallery: undefined,
album: group.slug,
category: undefined,
tag: undefined,
},
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("filters.album")}: {group.name}
</Link>
))}
{groups.categories.map((group) => (
<Link
key={`category-${group.id}`}
href={{
pathname: "/portfolio",
query: {
...buildPortfolioQuery(activeFilter, activeSort),
gallery: undefined,
album: undefined,
category: group.slug,
tag: undefined,
},
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("filters.category")}: {group.name}
</Link>
))}
{groups.tags.map((group) => (
<Link
key={`tag-${group.id}`}
href={{
pathname: "/portfolio",
query: {
...buildPortfolioQuery(activeFilter, activeSort),
gallery: undefined,
album: undefined,
category: undefined,
tag: group.slug,
},
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("filters.tag")}: {group.name}
</Link>
))}
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
<span className="text-xs uppercase tracking-wide text-neutral-500">
{t("sort.label")}:
</span>
<Link
href={{
pathname: "/portfolio",
query: buildPortfolioQuery(activeFilter, "latest"),
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("sort.latest")}
</Link>
<Link
href={{
pathname: "/portfolio",
query: buildPortfolioQuery(activeFilter, "title_asc"),
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("sort.titleAsc")}
</Link>
<Link
href={{
pathname: "/portfolio",
query: buildPortfolioQuery(activeFilter, "title_desc"),
}}
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100"
>
{t("sort.titleDesc")}
</Link>
</div>
</section>
{artworks.length === 0 ? (
<section className="rounded-xl border border-neutral-200 p-6 text-sm text-neutral-600">
{t("empty")}
</section>
) : (
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{artworks.map((artwork) => {
const preview = findPreviewAsset(artwork.renditions)
return (
<article
key={artwork.id}
className="overflow-hidden rounded-xl border border-neutral-200"
>
{preview ? (
<Image
src={`/api/media/file/${preview.mediaAssetId}`}
alt={preview.mediaAsset.altText || artwork.title}
width={1200}
height={800}
className="h-56 w-full object-cover"
/>
) : (
<div className="flex h-56 items-center justify-center bg-neutral-100 text-sm text-neutral-500">
{t("noPreview")}
</div>
)}
<div className="space-y-2 p-4">
<h2 className="text-lg font-medium">{artwork.title}</h2>
<p className="line-clamp-3 text-sm text-neutral-600">
{artwork.description || t("noDescription")}
</p>
<Link
href={`/portfolio/${artwork.slug}`}
className="text-sm underline underline-offset-2"
>
{t("viewArtwork")}
</Link>
</div>
</article>
)
})}
</section>
)}
</section>
)
}