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 (
+
+ )
+}
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 (
+
+ )
+}
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 */}
+
+
+
+
+ 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`}
+ >
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ )
+}
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 }) => (
+
+
+
+ ))}
+
+
+
+ >
+ )
+}
+
+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