Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: individual gallery page #650

Merged
merged 11 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions app/[locale]/gallery/[gallery]/[asset]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FunctionComponent, PropsWithChildren } from "react";
import { getAssetBreadcrumb, getRecentAssets } from "@/lib/api/galleries/asset";
import { addLocaleUriSegment, useTranslation } from "@/lib/i18n";
import Container from "@rubin-epo/epo-react-lib/Container";
import Stack from "@rubin-epo/epo-react-lib/Stack";
import Buttonish from "@rubin-epo/epo-react-lib/Buttonish";
import Breadcrumbs from "@/components/page/Breadcrumbs";

export async function generateStaticParams({
params: { locale, gallery },
}: GalleryProps) {
return getRecentAssets(locale, gallery);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generates for all assets right now, can be modified if this adds too much to build time

}

const AssetLayout: FunctionComponent<
PropsWithChildren<GalleryAssetProps>
> = async ({ children, params: { locale, gallery, asset } }) => {
const { t } = await useTranslation(locale);
const breadcrumbs = await getAssetBreadcrumb({ locale, gallery, asset });

return (
<>
<Breadcrumbs breadcrumbs={breadcrumbs} />
<Container>
<Stack>
{children}
<Buttonish
data-cy="back-to-gallery"
url={`${addLocaleUriSegment(locale)}/gallery/${gallery}`}
text={t("gallery.back-to-gallery")}
/>
</Stack>
</Container>
</>
);
};

export default AssetLayout;
92 changes: 92 additions & 0 deletions app/[locale]/gallery/[gallery]/[asset]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ComponentType, FunctionComponent } from "react";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAssetFromGallery } from "@/lib/api/galleries/asset";
import { assetTitle, assetToPageMetadata } from "@/lib/api/canto/metadata";
import { SupportedCantoAssetScheme } from "@/lib/api/galleries/schema";
import { addLocaleUriSegment } from "@/lib/i18n";
import CantoFigure from "@/components/organisms/gallery/CantoFigure";
import SingleMediaAsset from "@/components/templates/SingleMediaAsset";
import ImageSizes from "@/components/organisms/gallery/metadata/Sizes";
import AssetTags from "@/components/organisms/gallery/metadata/Tags";
import CantoImage from "@/components/organisms/gallery/CantoImage";
import CantoVideo from "@/components/organisms/gallery/CantoVideo";
import AssetMetadata from "@/components/organisms/gallery/metadata/Asset";

export async function generateMetadata({
params: { locale, gallery, asset: id },
}: GalleryAssetProps): Promise<Metadata> {
const asset = await getAssetFromGallery(gallery, id, locale);

if (!asset) {
notFound();
}

return assetToPageMetadata(asset, locale);
}

const assetComponent: Record<SupportedCantoAssetScheme, ComponentType> = {
image: CantoImage,
video: CantoVideo,
};

const GalleryAsset: FunctionComponent<GalleryAssetProps> = async ({
params: { locale, gallery, asset: id },
}) => {
const asset = await getAssetFromGallery(gallery, id, locale);

if (!asset) {
notFound();
}

const { name, width, height, url, scheme, tag, additional, size } = asset;

const { directUrlPreview, directUrlOriginal } = url;

const validTagsOnly =
tag?.filter((maybeTag): maybeTag is string => {
if (typeof maybeTag === "string") {
return maybeTag.toLowerCase() !== "untagged";
}
return false;
}) || [];

const links: Record<SupportedCantoAssetScheme, JSX.Element> = {
image: (
<>
<ImageSizes
width={parseInt(width)}
height={parseInt(height)}
{...{ directUrlPreview, directUrlOriginal }}
/>
<AssetTags tags={validTagsOnly} parentUri={`/gallery/${gallery}`} />
</>
),
video: <AssetTags tags={validTagsOnly} parentUri={`/gallery/${gallery}`} />,
};

const location = `${process.env.NEXT_PUBLIC_BASE_URL}${addLocaleUriSegment(
locale
)}/gallery/${gallery}/${id}`;

const Asset = assetComponent[scheme];

return (
<SingleMediaAsset
title={assetTitle(additional, locale) || name}
asset={
<CantoFigure
downloadUrl={directUrlOriginal}
asset={<Asset {...{ asset, locale }} />}
{...{ locale, additional, width, height, location, name }}
/>
}
metadataBlocks={
<AssetMetadata size={parseInt(size)} {...{ scheme, width, height }} />
}
metadataLinks={links[scheme]}
/>
);
};

export default GalleryAsset;
14 changes: 0 additions & 14 deletions app/[locale]/gallery/[gallery]/[image]/page.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions app/[locale]/gallery/[gallery]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getAllGalleries } from "@/lib/api/galleries";
import { FunctionComponent, PropsWithChildren } from "react";

export const dynamicParams = false;

export async function generateStaticParams({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generates pages for all gallery entries

params: { locale },
}: LocaleProps) {
return getAllGalleries(locale);
}

export const GalleryLayout: FunctionComponent<PropsWithChildren> = ({
children,
}) => {
return <>{children}</>;
};

export default GalleryLayout;
11 changes: 3 additions & 8 deletions app/[locale]/gallery/[gallery]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { FunctionComponent } from "react";
import { getGalleryData } from "@/lib/api/galleries";

// should retrieve all galleries by entry, and
// return an array of their slugs to statically
// generate their content
// export async function generateStaticParams() {
// }

const Gallery: FunctionComponent<WithSearchParams<GalleryProps>> = async ({
params: { locale, gallery },
searchParams = {},
}) => {

const data = await getGalleryData(
gallery,
locale,
searchParams
// section,
// type,
Expand All @@ -24,7 +18,8 @@ const Gallery: FunctionComponent<WithSearchParams<GalleryProps>> = async ({
return (
<div>
<h1>Gallery {gallery}</h1>
<a href="?sort=asc">Sort Ascending</a><br></br>
<a href="?sort=asc">Sort Ascending</a>
<br></br>
<a href="?sort=desc">Sort Descending</a>
<p>searchParams: {JSON.stringify(searchParams)}</p>
<pre>Data : {JSON.stringify(data, null, 2)} </pre>
Expand Down
74 changes: 74 additions & 0 deletions app/api/canto/[scheme]/[id]/[directUrlOriginalHash]/route.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proxy downloader that uses the directUrlOriginal to fetch the file from Canto, cache to Next, and serve the file again with an attachment content type so that browsers download instead of navigating

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import pick from "lodash/pick";
import {
SupportedCantoAssetScheme,
SupportedCantoScheme,
} from "@/lib/api/galleries/schema";

type CantoAssetParams = {
scheme: SupportedCantoAssetScheme;
directUrlOriginalHash: string;
id: string;
};

interface CantoDownloadProps {
params: CantoAssetParams;
}

export async function GET(
request: NextRequest,
{ params: { scheme, id, directUrlOriginalHash } }: CantoDownloadProps
) {
const { error } = SupportedCantoScheme.safeParse(scheme);

if (error) {
return new NextResponse("Invalid scheme", { status: 400 });
}

const { searchParams } = request.nextUrl;
const fileName = searchParams.get("name");
const contentType = searchParams.get("content-type");

if (!fileName) {
return new NextResponse("No filename specified", { status: 400 });
}

if (!contentType) {
return new NextResponse("Invalid content type", { status: 400 });
}

const proxySearchParams = new URLSearchParams({ fileName, contentType });

const {
body,
ok,
status,
statusText,
headers: cantoHeaders,
} = await fetch(
`${
process.env.CANTO_BASE_URL
}/direct/${scheme}/${id}/${directUrlOriginalHash}/original?${proxySearchParams.toString()}`
);

if (ok) {
const headers = new Headers(
pick(Object.fromEntries(cantoHeaders.entries()), [
"content-length",
"content-type",
"date",
"etag",
"last-modified",
])
);

headers.set("content-disposition", `attachment; filename="${fileName}"`);

return new NextResponse(body, {
status: 200,
headers,
});
} else {
return new NextResponse(statusText, { status });
}
}
20 changes: 14 additions & 6 deletions app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
import tags from "@/lib/api/client/tags";
import { fallbackLng, languages } from "@/lib/i18n/settings";
import { addLocaleUriSegment } from "@/lib/i18n";

const HOST = process.env.NEXT_PUBLIC_BASE_URL;
const REVALIDATE_SECRET_TOKEN = process.env.CRAFT_REVALIDATE_SECRET_TOKEN;
Expand Down Expand Up @@ -49,6 +50,15 @@ const indexNow = async (uri: string) => {
console.info(`${status}: ${indexNowStatusText[status]}`);
};

const revalidateChildren = (parts: Array<string>): "layout" | "page" => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the assets in galleries are not represented in Craft, they need a new revalidation pipeline. Next has two types of revalidation: page which revalidates only the specified URL and layout which revalidates the URL + it's children.

In this exception, we want to use layout so that when the gallery entry is saved (either manually or by webhook), the assets are also revalidated to pick up any changes

// revalidate gallery children if the URI is a gallery, but not the root gallery
if (parts.indexOf("gallery") === 0 && parts.length > 1) {
return "layout";
}

return "page";
};

export async function GET(request: NextRequest): Promise<NextResponse> {
const uri = request.nextUrl.searchParams.get("uri");
const secret = request.nextUrl.searchParams.get("secret");
Expand All @@ -71,14 +81,12 @@ export async function GET(request: NextRequest): Promise<NextResponse> {

if (uri) {
languages.forEach((locale) => {
const parts: Array<string> = uri === CRAFT_HOMEPAGE_URI ? [] : [uri];
if (locale !== fallbackLng) {
parts.unshift(locale);
}
const parts: Array<string> =
uri === CRAFT_HOMEPAGE_URI ? [] : uri.split("/");

const path = `/${parts.join("/")}`;
const path = `${addLocaleUriSegment(locale)}/${parts.join("/")}`;

revalidatePath(path);
revalidatePath(path, revalidateChildren(parts));
});

revalidateTag(tags.globals);
Expand Down
25 changes: 25 additions & 0 deletions codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { loadEnvConfig } from "@next/env";
import type { CodegenConfig } from "@graphql-codegen/cli";

loadEnvConfig(process.cwd());

const config: CodegenConfig = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set up codegen so we can get typings for GQL responses

generates: {
"./gql/": {
documents: [
"app/[locale]/gallery/**/*.{ts,tsx}",
"lib/api/galleries/*.ts",
],
preset: "client",
},
},
config: {
useTypeImports: true,
avoidOptionals: true,
},
schema: process.env.NEXT_PUBLIC_API_URL,

ignoreNoDocuments: true, // for better experience with the watcher
};

export default config;
3 changes: 1 addition & 2 deletions components/atomic/Tile/patterns/InvestigationTile/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ export const Title = styled.div`
transition: backgroud-color 0.2s;

@media (max-width: ${BREAK_PHABLET}) {
align-self: center;
justify-self: left;
place-self: center left;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linter got mad at this, no clue why

text-align: left;
}
`;
Expand Down
40 changes: 40 additions & 0 deletions components/molecules/MetadataList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { FunctionComponent, ReactNode } from "react";
import classNames from "classnames";
import styles from "./styles.module.css";

export interface MetadataItem {
key: ReactNode;
value: ReactNode;
}

interface MetadataListProps {
items: Array<MetadataItem>;
separator?: string;
className?: string;
}

const MetadataList: FunctionComponent<MetadataListProps> = ({
items,
separator = ":",
className,
}) => {
return (
<dl className={classNames(styles.metadataList, className)}>
{items.map(({ key, value }, i) => {
return (
<div className={styles.metadataSet} key={i}>
<dt>
{key}
{separator}
</dt>
<dd>{value}</dd>
</div>
);
})}
</dl>
);
};

MetadataList.displayName = "Molecule.MetadataList";

export default MetadataList;
Loading