-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 9 commits
b5d42bb
f87c30e
4ca4cbc
3f2f335
5d2dc3d
36ad4ce
4b04bdb
6283e40
6005d3d
a4aa190
01ef368
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
|
||
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; |
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; |
This file was deleted.
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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. proxy downloader that uses the |
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 }); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -49,6 +50,15 @@ const indexNow = async (uri: string) => { | |
console.info(`${status}: ${indexNowStatusText[status]}`); | ||
}; | ||
|
||
const revalidateChildren = (parts: Array<string>): "layout" | "page" => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: In this exception, we want to use |
||
// 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"); | ||
|
@@ -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); | ||
|
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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. linter got mad at this, no clue why |
||
text-align: left; | ||
} | ||
`; | ||
|
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; |
There was a problem hiding this comment.
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