diff --git a/examples/with-cloudinary/.env.local.example b/examples/with-cloudinary/.env.local.example new file mode 100644 index 000000000000..c4b7620d9f8f --- /dev/null +++ b/examples/with-cloudinary/.env.local.example @@ -0,0 +1,4 @@ +NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME= +CLOUDINARY_API_KEY= +CLOUDINARY_API_SECRET= +CLOUDINARY_FOLDER= diff --git a/examples/with-cloudinary/.gitignore b/examples/with-cloudinary/.gitignore new file mode 100644 index 000000000000..c87c9b392c02 --- /dev/null +++ b/examples/with-cloudinary/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/with-cloudinary/README.md b/examples/with-cloudinary/README.md new file mode 100644 index 000000000000..27c8c2939df6 --- /dev/null +++ b/examples/with-cloudinary/README.md @@ -0,0 +1,31 @@ +# Next.js & Cloudinary example app + +This example shows how to create an image gallery site using Next.js, [Cloudinary](https://cloudinary.com), and [Tailwind](https://tailwindcss.com). + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or view the demo [here](https://nextconf-images.vercel.app/) + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-cloudinary&project-name=nextjs-image-gallery&repository-name=with-cloudinary&env=NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,CLOUDINARY_API_KEY,CLOUDINARY_API_SECRET,CLOUDINARY_FOLDER&envDescription=API%20Keys%20from%20Cloudinary%20needed%20to%20run%20this%20application.) + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:: + +```bash + npx create-next-app --example with-cloudinary nextjs-image-gallery +``` + +```bash +yarn create next-app --example with-cloudinary nextjs-image-gallery +``` + +```bash +pnpm create next-app --example with-cloudinary nextjs-image-gallery +``` + +## References + +- Cloudinary API: https://cloudinary.com/documentation/transformation_reference diff --git a/examples/with-cloudinary/components/Carousel.tsx b/examples/with-cloudinary/components/Carousel.tsx new file mode 100644 index 000000000000..3dc61b90db6a --- /dev/null +++ b/examples/with-cloudinary/components/Carousel.tsx @@ -0,0 +1,54 @@ +import Image from 'next/image' +import { useRouter } from 'next/router' +import useKeypress from 'react-use-keypress' +import type { ImageProps } from '../utils/types' +import { useLastViewedPhoto } from '../utils/useLastViewedPhoto' +import SharedModal from './SharedModal' + +export default function Carousel({ + index, + currentPhoto, +}: { + index: number + currentPhoto: ImageProps +}) { + const router = useRouter() + const [, setLastViewedPhoto] = useLastViewedPhoto() + + function closeModal() { + setLastViewedPhoto(currentPhoto.id) + router.push('/', undefined, { shallow: true }) + } + + function changePhotoId(newVal: number) { + return newVal + } + + useKeypress('Escape', () => { + closeModal() + }) + + return ( +
+ + +
+ ) +} diff --git a/examples/with-cloudinary/components/Icons/Bridge.tsx b/examples/with-cloudinary/components/Icons/Bridge.tsx new file mode 100644 index 000000000000..0e326cd8f0d8 --- /dev/null +++ b/examples/with-cloudinary/components/Icons/Bridge.tsx @@ -0,0 +1,126 @@ +export default function Bridge() { + return ( + + + Line drawing of the Golden Gate Bridge in San Francisco. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/examples/with-cloudinary/components/Icons/Logo.tsx b/examples/with-cloudinary/components/Icons/Logo.tsx new file mode 100644 index 000000000000..9c6a2d610848 --- /dev/null +++ b/examples/with-cloudinary/components/Icons/Logo.tsx @@ -0,0 +1,71 @@ +export default function Logo() { + return ( + + + Next.js Conf logo using a newly designed Next.js logo. + + + + + + + + + + + + + + ) +} diff --git a/examples/with-cloudinary/components/Icons/Twitter.tsx b/examples/with-cloudinary/components/Icons/Twitter.tsx new file mode 100644 index 000000000000..76f42ee7604f --- /dev/null +++ b/examples/with-cloudinary/components/Icons/Twitter.tsx @@ -0,0 +1,19 @@ +export default function Twitter(props: any) { + return ( + + + + ) +} diff --git a/examples/with-cloudinary/components/Modal.tsx b/examples/with-cloudinary/components/Modal.tsx new file mode 100644 index 000000000000..a4c24e54e531 --- /dev/null +++ b/examples/with-cloudinary/components/Modal.tsx @@ -0,0 +1,84 @@ +import { Dialog } from '@headlessui/react' +import { motion } from 'framer-motion' +import { useRouter } from 'next/router' +import { useRef, useState } from 'react' +import useKeypress from 'react-use-keypress' +import type { ImageProps } from '../utils/types' +import SharedModal from './SharedModal' + +export default function Modal({ + images, + onClose, +}: { + images: ImageProps[] + onClose?: () => void +}) { + let overlayRef = useRef() + const router = useRouter() + + const { photoId } = router.query + let index = Number(photoId) + + const [direction, setDirection] = useState(0) + const [curIndex, setCurIndex] = useState(index) + + function handleClose() { + router.push('/', undefined, { shallow: true }) + onClose() + } + + function changePhotoId(newVal: number) { + if (newVal > index) { + setDirection(1) + } else { + setDirection(-1) + } + setCurIndex(newVal) + router.push( + { + query: { photoId: newVal }, + }, + `/p/${newVal}`, + { shallow: true } + ) + } + + useKeypress('ArrowRight', () => { + if (index + 1 < images.length) { + changePhotoId(index + 1) + } + }) + + useKeypress('ArrowLeft', () => { + if (index > 0) { + changePhotoId(index - 1) + } + }) + + return ( + + + + + ) +} diff --git a/examples/with-cloudinary/components/SharedModal.tsx b/examples/with-cloudinary/components/SharedModal.tsx new file mode 100644 index 000000000000..162e7e42e227 --- /dev/null +++ b/examples/with-cloudinary/components/SharedModal.tsx @@ -0,0 +1,210 @@ +import { + ArrowDownTrayIcon, + ArrowTopRightOnSquareIcon, + ArrowUturnLeftIcon, + ChevronLeftIcon, + ChevronRightIcon, + XMarkIcon, +} from '@heroicons/react/24/outline' +import { AnimatePresence, motion, MotionConfig } from 'framer-motion' +import Image from 'next/image' +import { useState } from 'react' +import { useSwipeable } from 'react-swipeable' +import { variants } from '../utils/animationVariants' +import downloadPhoto from '../utils/downloadPhoto' +import { range } from '../utils/range' +import type { ImageProps, SharedModalProps } from '../utils/types' +import Twitter from './Icons/Twitter' + +export default function SharedModal({ + index, + images, + changePhotoId, + closeModal, + navigation, + currentPhoto, + direction, +}: SharedModalProps) { + const [loaded, setLoaded] = useState(false) + + let filteredImages = images?.filter((img: ImageProps) => + range(index - 15, index + 15).includes(img.id) + ) + + const handlers = useSwipeable({ + onSwipedLeft: () => changePhotoId(index + 1), + onSwipedRight: () => changePhotoId(index - 1), + trackMouse: true, + }) + + let currentImage = images ? images[index] : currentPhoto + + return ( + +
+ {/* Main image */} +
+
+ + + Next.js Conf image setLoaded(true)} + /> + + +
+
+ + {/* Buttons + bottom nav bar */} +
+ {/* Buttons */} + {loaded && ( +
+ {navigation && ( + <> + {index > 0 && ( + + )} + {index + 1 < images.length && ( + + )} + + )} +
+ {navigation ? ( + + + + ) : ( + + + + )} + +
+
+ +
+
+ )} + {/* Bottom Nav bar */} + {navigation && ( +
+ + + {filteredImages.map(({ public_id, format, id }) => ( + changePhotoId(id)} + key={id} + className={`${ + id === index + ? 'z-20 rounded-md shadow shadow-black/50' + : 'z-10' + } ${id === 0 ? 'rounded-l-md' : ''} ${ + id === images.length - 1 ? 'rounded-r-md' : '' + } relative inline-block w-full shrink-0 transform-gpu overflow-hidden focus:outline-none`} + > + small photos on the bottom + + ))} + + +
+ )} +
+
+
+ ) +} diff --git a/examples/with-cloudinary/next.config.js b/examples/with-cloudinary/next.config.js new file mode 100644 index 000000000000..8a5b9da7f892 --- /dev/null +++ b/examples/with-cloudinary/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + formats: ['image/avif', 'image/webp'], + domains: ['res.cloudinary.com'], + }, +} diff --git a/examples/with-cloudinary/package.json b/examples/with-cloudinary/package.json new file mode 100644 index 000000000000..f9c49ad231b4 --- /dev/null +++ b/examples/with-cloudinary/package.json @@ -0,0 +1,31 @@ +{ + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@headlessui/react": "^1.7.4", + "@heroicons/react": "^2.0.13", + "cloudinary": "^1.32.0", + "eslint-config-next": "^13.0.1", + "framer-motion": "^7.6.4", + "next": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hooks-global-state": "^2.0.0", + "react-swipeable": "^7.0.0", + "react-use-keypress": "^1.3.1" + }, + "devDependencies": { + "@types/node": "18.11.9", + "@types/react": "18.0.25", + "autoprefixer": "^10.4.13", + "postcss": "^8.4.18", + "prettier": "^2.7.1", + "prettier-plugin-tailwindcss": "^0.1.13", + "tailwindcss": "^3.2.1" + } +} diff --git a/examples/with-cloudinary/pages/_app.tsx b/examples/with-cloudinary/pages/_app.tsx new file mode 100644 index 000000000000..e9bc82183eea --- /dev/null +++ b/examples/with-cloudinary/pages/_app.tsx @@ -0,0 +1,6 @@ +import type { AppProps } from 'next/app' +import '../styles/index.css' + +export default function MyApp({ Component, pageProps }: AppProps) { + return +} diff --git a/examples/with-cloudinary/pages/_document.tsx b/examples/with-cloudinary/pages/_document.tsx new file mode 100644 index 000000000000..1c9aff5b2acb --- /dev/null +++ b/examples/with-cloudinary/pages/_document.tsx @@ -0,0 +1,35 @@ +import Document, { Head, Html, Main, NextScript } from 'next/document' + +class MyDocument extends Document { + render() { + return ( + + + + + + + + + + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/with-cloudinary/pages/index.tsx b/examples/with-cloudinary/pages/index.tsx new file mode 100644 index 000000000000..b214b2d9286a --- /dev/null +++ b/examples/with-cloudinary/pages/index.tsx @@ -0,0 +1,176 @@ +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 } from 'react' +import Bridge from '../components/Icons/Bridge' +import Logo from '../components/Icons/Logo' +import Modal from '../components/Modal' +import cloudinary from '../utils/cloudinary' +import getBase64ImageUrl from '../utils/generateBlurPlaceholder' +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() + + 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) { + document + .querySelector(`#photo-${lastViewedPhoto}`) + .scrollIntoView({ block: 'center' }) + + setLastViewedPhoto(null) + } + }, [photoId, lastViewedPhoto, setLastViewedPhoto]) + + return ( + <> + + Next.js Conf 2022 Photos + + + +
+ {photoId && ( + { + setLastViewedPhoto(photoId) + }} + /> + )} + +
+
+
+ + + + +
+ +

+ 2022 Event Photos +

+

+ Our incredible Next.js community got together in San Francisco for + our first ever in-person conference! +

+ + Clone and Deploy + +
+ {images.map(({ id, public_id, format, blurDataUrl }) => ( + + Next.js Conf photo + + ))} +
+
+ + + ) +} + +export default Home + +export async function getStaticProps() { + const results = await cloudinary.v2.search + .expression(`folder:${process.env.CLOUDINARY_FOLDER}/*`) + .sort_by('public_id', 'desc') + .max_results(400) + .execute() + let reducedResults: ImageProps[] = [] + + let i = 0 + for (let result of results.resources) { + reducedResults.push({ + id: i, + height: result.height, + width: result.width, + public_id: result.public_id, + format: result.format, + }) + i++ + } + + const blurImagePromises = results.resources.map((image: ImageProps) => { + return getBase64ImageUrl(image) + }) + const imagesWithBlurDataUrls = await Promise.all(blurImagePromises) + + for (let i = 0; i < reducedResults.length; i++) { + reducedResults[i].blurDataUrl = imagesWithBlurDataUrls[i] + } + + return { + props: { + images: reducedResults, + }, + } +} diff --git a/examples/with-cloudinary/pages/p/[photoId].tsx b/examples/with-cloudinary/pages/p/[photoId].tsx new file mode 100644 index 000000000000..8fdb9dd494c4 --- /dev/null +++ b/examples/with-cloudinary/pages/p/[photoId].tsx @@ -0,0 +1,77 @@ +import type { GetStaticProps, NextPage } from 'next' +import Head from 'next/head' +import { useRouter } from 'next/router' +import Carousel from '../../components/Carousel' +import getResults from '../../utils/cachedImages' +import cloudinary from '../../utils/cloudinary' +import getBase64ImageUrl from '../../utils/generateBlurPlaceholder' +import type { ImageProps } from '../../utils/types' + +const Home: NextPage = ({ currentPhoto }: { currentPhoto: ImageProps }) => { + const router = useRouter() + const { photoId } = router.query + let index = Number(photoId) + + const currentPhotoUrl = `https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/c_scale,w_2560/${currentPhoto.public_id}.${currentPhoto.format}` + + return ( + <> + + Next.js Conf 2022 Photos + + + +
+ +
+ + ) +} + +export default Home + +export const getStaticProps: GetStaticProps = async (context) => { + const results = await getResults() + + let reducedResults: ImageProps[] = [] + let i = 0 + for (let result of results.resources) { + reducedResults.push({ + id: i, + height: result.height, + width: result.width, + public_id: result.public_id, + format: result.format, + }) + i++ + } + + const currentPhoto = reducedResults.find( + (img) => img.id === Number(context.params.photoId) + ) + currentPhoto.blurDataUrl = await getBase64ImageUrl(currentPhoto) + + return { + props: { + currentPhoto: currentPhoto, + }, + } +} + +export async function getStaticPaths() { + const results = await cloudinary.v2.search + .expression(`folder:${process.env.CLOUDINARY_FOLDER}/*`) + .sort_by('public_id', 'desc') + .max_results(400) + .execute() + + let fullPaths = [] + for (let i = 0; i < results.resources.length; i++) { + fullPaths.push({ params: { photoId: i.toString() } }) + } + + return { + paths: fullPaths, + fallback: false, + } +} diff --git a/examples/with-cloudinary/postcss.config.js b/examples/with-cloudinary/postcss.config.js new file mode 100644 index 000000000000..33ad091d26d8 --- /dev/null +++ b/examples/with-cloudinary/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/with-cloudinary/public/favicon.ico b/examples/with-cloudinary/public/favicon.ico new file mode 100644 index 000000000000..718d6fea4835 Binary files /dev/null and b/examples/with-cloudinary/public/favicon.ico differ diff --git a/examples/with-cloudinary/public/og-image.png b/examples/with-cloudinary/public/og-image.png new file mode 100644 index 000000000000..a1f8806389ec Binary files /dev/null and b/examples/with-cloudinary/public/og-image.png differ diff --git a/examples/with-cloudinary/styles/index.css b/examples/with-cloudinary/styles/index.css new file mode 100644 index 000000000000..c0eca5423ccc --- /dev/null +++ b/examples/with-cloudinary/styles/index.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@supports (font: -apple-system-body) and (-webkit-appearance: none) { + img[loading='lazy'] { + clip-path: inset(0.6px); + } +} diff --git a/examples/with-cloudinary/tailwind.config.js b/examples/with-cloudinary/tailwind.config.js new file mode 100644 index 000000000000..d470945ffab5 --- /dev/null +++ b/examples/with-cloudinary/tailwind.config.js @@ -0,0 +1,24 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + future: { + hoverOnlyWhenSupported: true, + }, + content: [ + './app/**/*.{js,ts,jsx,tsx}', + './pages/**/*.{js,ts,jsx,tsx}', + './components/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: { + boxShadow: { + highlight: 'inset 0 0 0 1px rgba(255, 255, 255, 0.05)', + }, + screens: { + narrow: { raw: '(max-aspect-ratio: 3 / 2)' }, + wide: { raw: '(min-aspect-ratio: 3 / 2)' }, + 'taller-than-854': { raw: '(min-height: 854px)' }, + }, + }, + }, + plugins: [], +} diff --git a/examples/with-cloudinary/tsconfig.json b/examples/with-cloudinary/tsconfig.json new file mode 100644 index 000000000000..1563f3e87857 --- /dev/null +++ b/examples/with-cloudinary/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/with-cloudinary/utils/animationVariants.ts b/examples/with-cloudinary/utils/animationVariants.ts new file mode 100644 index 000000000000..096736fea0c5 --- /dev/null +++ b/examples/with-cloudinary/utils/animationVariants.ts @@ -0,0 +1,18 @@ +export const variants = { + enter: (direction: number) => { + return { + x: direction > 0 ? 1000 : -1000, + opacity: 0, + } + }, + center: { + x: 0, + opacity: 1, + }, + exit: (direction: number) => { + return { + x: direction < 0 ? 1000 : -1000, + opacity: 0, + } + }, +} diff --git a/examples/with-cloudinary/utils/cachedImages.ts b/examples/with-cloudinary/utils/cachedImages.ts new file mode 100644 index 000000000000..b9c42b016719 --- /dev/null +++ b/examples/with-cloudinary/utils/cachedImages.ts @@ -0,0 +1,17 @@ +import cloudinary from './cloudinary' + +let cachedResults + +export default async function getResults() { + if (!cachedResults) { + const fetchedResults = await cloudinary.v2.search + .expression(`folder:${process.env.CLOUDINARY_FOLDER}/*`) + .sort_by('public_id', 'desc') + .max_results(400) + .execute() + + cachedResults = fetchedResults + } + + return cachedResults +} diff --git a/examples/with-cloudinary/utils/cloudinary.ts b/examples/with-cloudinary/utils/cloudinary.ts new file mode 100644 index 000000000000..ca1d2e3caeb8 --- /dev/null +++ b/examples/with-cloudinary/utils/cloudinary.ts @@ -0,0 +1,11 @@ +import cloudinary from 'cloudinary' + +// @ts-ignore +cloudinary.config({ + cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, + secure: true, +}) + +export default cloudinary diff --git a/examples/with-cloudinary/utils/downloadPhoto.ts b/examples/with-cloudinary/utils/downloadPhoto.ts new file mode 100644 index 000000000000..60d88902ed42 --- /dev/null +++ b/examples/with-cloudinary/utils/downloadPhoto.ts @@ -0,0 +1,24 @@ +function forceDownload(blobUrl: string, filename: string) { + let a: any = document.createElement('a') + a.download = filename + a.href = blobUrl + document.body.appendChild(a) + a.click() + a.remove() +} + +export default function downloadPhoto(url: string, filename: string) { + if (!filename) filename = url.split('\\').pop().split('/').pop() + fetch(url, { + headers: new Headers({ + Origin: location.origin, + }), + mode: 'cors', + }) + .then((response) => response.blob()) + .then((blob) => { + let blobUrl = window.URL.createObjectURL(blob) + forceDownload(blobUrl, filename) + }) + .catch((e) => console.error(e)) +} diff --git a/examples/with-cloudinary/utils/generateBlurPlaceholder.ts b/examples/with-cloudinary/utils/generateBlurPlaceholder.ts new file mode 100644 index 000000000000..613025f9d4c8 --- /dev/null +++ b/examples/with-cloudinary/utils/generateBlurPlaceholder.ts @@ -0,0 +1,20 @@ +import type { ImageProps } from './types' + +const cache = new Map() + +export default async function getBase64ImageUrl( + image: ImageProps +): Promise { + let url = cache.get(image) + if (url) { + return url + } + const response = await fetch( + `https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/f_jpg,w_8,q_70/${image.public_id}.${image.format}` + ) + const buffer = await response.arrayBuffer() + const base64 = Buffer.from(buffer).toString('base64') + url = `data:image/jpeg;base64,${base64}` + cache.set(image, url) + return url +} diff --git a/examples/with-cloudinary/utils/range.ts b/examples/with-cloudinary/utils/range.ts new file mode 100644 index 000000000000..36d5651ee1e8 --- /dev/null +++ b/examples/with-cloudinary/utils/range.ts @@ -0,0 +1,11 @@ +export const range = (start: number, end: number) => { + let output = [] + if (typeof end === 'undefined') { + end = start + start = 0 + } + for (let i = start; i < end; i += 1) { + output.push(i) + } + return output +} diff --git a/examples/with-cloudinary/utils/types.ts b/examples/with-cloudinary/utils/types.ts new file mode 100644 index 000000000000..720ae500fb7c --- /dev/null +++ b/examples/with-cloudinary/utils/types.ts @@ -0,0 +1,19 @@ +/* eslint-disable no-unused-vars */ +export interface ImageProps { + id: number + height: string + width: string + public_id: string + format: string + blurDataUrl?: string +} + +export interface SharedModalProps { + index: number + images?: ImageProps[] + currentPhoto?: ImageProps + changePhotoId: (newVal: number) => void + closeModal: () => void + navigation: boolean + direction?: number +} diff --git a/examples/with-cloudinary/utils/useLastViewedPhoto.ts b/examples/with-cloudinary/utils/useLastViewedPhoto.ts new file mode 100644 index 000000000000..7f4927233c92 --- /dev/null +++ b/examples/with-cloudinary/utils/useLastViewedPhoto.ts @@ -0,0 +1,8 @@ +import { createGlobalState } from 'react-hooks-global-state' + +const initialState = { photoToScrollTo: null } +const { useGlobalState } = createGlobalState(initialState) + +export const useLastViewedPhoto = () => { + return useGlobalState('photoToScrollTo') +} diff --git a/examples/with-sfcc/README.md b/examples/with-sfcc/README.md index fbf59760d402..7a58040c5b9d 100644 --- a/examples/with-sfcc/README.md +++ b/examples/with-sfcc/README.md @@ -12,7 +12,7 @@ Check out our [Next.js deployment documentation](https://nextjs.org/docs/deploym ## How to use -Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/with-sfcc) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:: +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:: ```bash npx create-next-app --example with-sfcc nextjs-sfcc-app