mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
## Summary Replaces the Cloudinary-backed image gallery example with a Vercel Blob–backed equivalent. The migration is driven by the broader move of [`vercel/nextconf-image-gallery`](https://github.com/vercel/nextconf-image-gallery) (the deployed companion to this example) off Cloudinary. - Renamed `examples/with-cloudinary` → `examples/with-vercel-blob` via `git mv` (history preserved). - Images are discovered at build time via the `@vercel/blob` SDK's `list()` API, then probed with [`sharp`](https://github.com/lovell/sharp) for dimensions and to pre-generate base64 blur placeholders. `next/image` (Vercel's image optimizer) handles resizing — no Cloudinary `c_scale,w_*` transform URLs. - `ImageProps` is now `{ id, url, width, height, blurDataUrl }` (was `{ public_id, format, ... }`). - `utils/cachedImages.ts` does the listing + dimension probe + blur placeholder generation. - Removed `utils/cloudinary.ts` and `utils/generateBlurPlaceholder.ts`. - `next.config.js` `remotePatterns` now allows `*.public.blob.vercel-storage.com`. - Deps: dropped `cloudinary`, `imagemin`, `imagemin-jpegtran`; added `@vercel/blob` and `sharp`. - `.env.local.example` reduced to a single `BLOB_READ_WRITE_TOKEN`. - README rewritten: blob description, deploy-button env, setup steps, references. ## Test plan - [x] `npx create-next-app --example with-vercel-blob ./blob-app` clones cleanly - [x] After populating a blob store and `.env.local`, `npm run dev` renders the gallery with blur placeholders - [x] `npm run build` succeeds (sharp probe + placeholder generation runs at build time) - [x] Vercel deploy button flow prompts only for `BLOB_READ_WRITE_TOKEN` --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> Co-authored-by: Joseph <[email protected]>
146 lines
5.4 KiB
TypeScript
146 lines
5.4 KiB
TypeScript
import type { NextPage } from "next";
|
|
import Head from "next/head";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/router";
|
|
import { useEffect, useRef } from "react";
|
|
import Bridge from "../components/Icons/Bridge";
|
|
import Logo from "../components/Icons/Logo";
|
|
import Modal from "../components/Modal";
|
|
import getResults from "../utils/cachedImages";
|
|
import type { ImageProps } from "../utils/types";
|
|
import { useLastViewedPhoto } from "../utils/useLastViewedPhoto";
|
|
|
|
const Home: NextPage = ({ images }: { images: ImageProps[] }) => {
|
|
const router = useRouter();
|
|
const { photoId } = router.query;
|
|
const [lastViewedPhoto, setLastViewedPhoto] = useLastViewedPhoto();
|
|
|
|
const lastViewedPhotoRef = useRef<HTMLAnchorElement>(null);
|
|
|
|
useEffect(() => {
|
|
// This effect keeps track of the last viewed photo in the modal to keep the index page in sync when the user navigates back
|
|
if (lastViewedPhoto && !photoId) {
|
|
lastViewedPhotoRef.current.scrollIntoView({ block: "center" });
|
|
setLastViewedPhoto(null);
|
|
}
|
|
}, [photoId, lastViewedPhoto, setLastViewedPhoto]);
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>Next.js Conf 2022 Photos</title>
|
|
<meta
|
|
property="og:image"
|
|
content="https://nextjsconf-pics.vercel.app/og-image.png"
|
|
/>
|
|
<meta
|
|
name="twitter:image"
|
|
content="https://nextjsconf-pics.vercel.app/og-image.png"
|
|
/>
|
|
</Head>
|
|
<main className="mx-auto max-w-[1960px] p-4">
|
|
{photoId && (
|
|
<Modal
|
|
images={images}
|
|
onClose={() => {
|
|
setLastViewedPhoto(photoId);
|
|
}}
|
|
/>
|
|
)}
|
|
<div className="columns-1 gap-4 sm:columns-2 xl:columns-3 2xl:columns-4">
|
|
<div className="after:content relative mb-5 flex h-[629px] flex-col items-center justify-end gap-4 overflow-hidden rounded-lg bg-white/10 px-6 pb-16 pt-64 text-center text-white shadow-highlight after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:shadow-highlight lg:pt-0">
|
|
<div className="absolute inset-0 flex items-center justify-center opacity-20">
|
|
<span className="flex max-h-full max-w-full items-center justify-center">
|
|
<Bridge />
|
|
</span>
|
|
<span className="absolute left-0 right-0 bottom-0 h-[400px] bg-gradient-to-b from-black/0 via-black to-black"></span>
|
|
</div>
|
|
<Logo />
|
|
<h1 className="mt-8 mb-4 text-base font-bold uppercase tracking-widest">
|
|
2022 Event Photos
|
|
</h1>
|
|
<p className="max-w-[40ch] text-white/75 sm:max-w-[32ch]">
|
|
Our incredible Next.js community got together in San Francisco for
|
|
our first ever in-person conference!
|
|
</p>
|
|
<a
|
|
className="pointer z-10 mt-6 rounded-lg border border-white bg-white px-3 py-2 text-sm font-semibold text-black transition hover:bg-white/10 hover:text-white md:mt-4"
|
|
href="https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-vercel-blob&project-name=nextjs-image-gallery&repository-name=with-vercel-blob&env=BLOB_READ_WRITE_TOKEN&envDescription=Read%2Fwrite%20token%20for%20your%20Vercel%20Blob%20store"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Clone and Deploy
|
|
</a>
|
|
</div>
|
|
{images.map(({ id, url, blurDataUrl }) => (
|
|
<Link
|
|
key={id}
|
|
href={`/?photoId=${id}`}
|
|
as={`/p/${id}`}
|
|
ref={id === Number(lastViewedPhoto) ? lastViewedPhotoRef : null}
|
|
shallow
|
|
className="after:content group relative mb-5 block w-full cursor-zoom-in after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:shadow-highlight"
|
|
>
|
|
<Image
|
|
alt="Next.js Conf photo"
|
|
className="transform rounded-lg brightness-90 transition will-change-auto group-hover:brightness-110"
|
|
style={{ transform: "translate3d(0, 0, 0)" }}
|
|
placeholder="blur"
|
|
blurDataURL={blurDataUrl}
|
|
src={url}
|
|
width={720}
|
|
height={480}
|
|
sizes="(max-width: 640px) 100vw,
|
|
(max-width: 1280px) 50vw,
|
|
(max-width: 1536px) 33vw,
|
|
25vw"
|
|
/>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</main>
|
|
<footer className="p-6 text-center text-white/80 sm:p-12">
|
|
Thank you to{" "}
|
|
<a
|
|
href="https://edelsonphotography.com/"
|
|
target="_blank"
|
|
className="font-semibold hover:text-white"
|
|
rel="noreferrer"
|
|
>
|
|
Josh Edelson
|
|
</a>
|
|
,{" "}
|
|
<a
|
|
href="https://www.newrevmedia.com/"
|
|
target="_blank"
|
|
className="font-semibold hover:text-white"
|
|
rel="noreferrer"
|
|
>
|
|
Jenny Morgan
|
|
</a>
|
|
, and{" "}
|
|
<a
|
|
href="https://www.garysextonphotography.com/"
|
|
target="_blank"
|
|
className="font-semibold hover:text-white"
|
|
rel="noreferrer"
|
|
>
|
|
Gary Sexton
|
|
</a>{" "}
|
|
for the pictures.
|
|
</footer>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Home;
|
|
|
|
export async function getStaticProps() {
|
|
return {
|
|
props: {
|
|
images: await getResults(),
|
|
},
|
|
};
|
|
}
|