-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
docs: add with-responsive-image example app #5070
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
base: main
Are you sure you want to change the base?
Changes from all commits
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,11 @@ | ||
{ | ||
"projectName": "with-responsive-image", | ||
"mode": "file-router", | ||
"typescript": true, | ||
"tailwind": false, | ||
"packageManager": "npm", | ||
"git": true, | ||
"version": 1, | ||
"framework": "react-cra", | ||
"chosenAddOns": [] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
node_modules | ||
.DS_Store | ||
dist | ||
dist-ssr | ||
*.local | ||
count.txt | ||
.env | ||
.nitro | ||
.tanstack |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# TanStack + ResponsiveImage | ||
|
||
Integrating [ResponsiveImage](https://responsive-image.dev) with TanStack Router. | ||
|
||
Run `pnpm dev` to run locally. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<link rel="icon" href="/favicon.ico" /> | ||
<meta name="theme-color" content="#000000" /> | ||
<meta | ||
name="description" | ||
content="Web site created using create-tsrouter-app" | ||
/> | ||
<link rel="apple-touch-icon" href="/logo192.png" /> | ||
<link rel="manifest" href="/manifest.json" /> | ||
<title>Create TanStack App - with-responsive-image</title> | ||
</head> | ||
<body> | ||
<div id="app"></div> | ||
<script type="module" src="/src/main.tsx"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "with-responsive-image", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite --port 3000", | ||
"start": "vite --port 3000", | ||
"build": "vite build && tsc", | ||
"serve": "vite preview" | ||
}, | ||
"dependencies": { | ||
"@responsive-image/core": "^2.1.0", | ||
"@responsive-image/react": "^1.1.2", | ||
"@tanstack/react-devtools": "^0.2.2", | ||
"@tanstack/react-router": "^1.131.32", | ||
"@tanstack/react-router-devtools": "^1.131.32", | ||
"@tanstack/router-plugin": "^1.131.32", | ||
"react": "^19.0.0", | ||
"react-dom": "^19.0.0" | ||
}, | ||
"devDependencies": { | ||
"@responsive-image/vite-plugin": "^2.0.0", | ||
"@testing-library/dom": "^10.4.0", | ||
"@testing-library/react": "^16.2.0", | ||
"@types/react": "^19.0.8", | ||
"@types/react-dom": "^19.0.3", | ||
"@vitejs/plugin-react": "^4.3.4", | ||
"jsdom": "^26.0.0", | ||
"typescript": "^5.7.2", | ||
"vite": "^7.1.3", | ||
"web-vitals": "^4.2.4" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"short_name": "TanStack App", | ||
"name": "Create TanStack App Sample", | ||
"icons": [ | ||
{ | ||
"src": "favicon.ico", | ||
"sizes": "64x64 32x32 24x24 16x16", | ||
"type": "image/x-icon" | ||
}, | ||
{ | ||
"src": "logo192.png", | ||
"type": "image/png", | ||
"sizes": "192x192" | ||
}, | ||
{ | ||
"src": "logo512.png", | ||
"type": "image/png", | ||
"sizes": "512x512" | ||
} | ||
], | ||
"start_url": ".", | ||
"display": "standalone", | ||
"theme_color": "#000000", | ||
"background_color": "#ffffff" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# https://www.robotstxt.org/robotstxt.html | ||
User-agent: * | ||
Disallow: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
declare module '*responsive' { | ||
import type { ImageData } from '@responsive-image/core' | ||
|
||
const value: ImageData | ||
export default value | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { notFound } from '@tanstack/react-router' | ||
import type { ImageData } from '@responsive-image/core' | ||
|
||
const thumbnails = import.meta.glob<{ default: ImageData }>( | ||
'./images/gallery/*.jpg', | ||
{ | ||
eager: true, // this is just generating image meta data, not need for lazy loading | ||
query: { | ||
w: '200;400', | ||
responsive: true, // opt into processing by @responsive-image/vite, see vite.config.ts. Without this, default vite asset handling applies. | ||
}, | ||
}, | ||
) | ||
|
||
const images = import.meta.glob<{ default: ImageData }>( | ||
'./images/gallery/*.jpg', | ||
{ | ||
eager: true, // this is just generating image meta data, not need for lazy loading | ||
query: { | ||
responsive: true, // opt into processing by @responsive-image/vite, see vite.config.ts. Without this, default vite asset handling applies. | ||
}, | ||
}, | ||
) | ||
|
||
export function getThumbsnails(): Record<string, ImageData> { | ||
return Object.fromEntries( | ||
Object.entries(thumbnails).map(([imageId, module]) => [ | ||
normalizeImageId(imageId), | ||
module.default, | ||
]), | ||
) | ||
} | ||
|
||
export function getImage(imageId: string): ImageData { | ||
const module = images[denormalizeImageId(imageId)] | ||
|
||
if (!module) { | ||
throw notFound({ data: { foo: 1 } }) | ||
} | ||
|
||
return module.default | ||
} | ||
|
||
// Remove leading `./images/gallery/` from import.meta.glob keys for nicer URLs | ||
function normalizeImageId(imageId: string): string { | ||
return imageId.replace(/^\.\/images\/gallery\//, '') | ||
} | ||
|
||
function denormalizeImageId(imageId: string): string { | ||
return `./images/gallery/${imageId}` | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { StrictMode } from 'react' | ||
import ReactDOM from 'react-dom/client' | ||
import { RouterProvider, createRouter } from '@tanstack/react-router' | ||
|
||
// Import the generated route tree | ||
import { routeTree } from './routeTree.gen' | ||
|
||
import './styles.css' | ||
import reportWebVitals from './reportWebVitals.ts' | ||
|
||
// Create a new router instance | ||
const router = createRouter({ | ||
routeTree, | ||
context: {}, | ||
defaultPreload: 'intent', | ||
scrollRestoration: true, | ||
defaultStructuralSharing: true, | ||
defaultPreloadStaleTime: 0, | ||
}) | ||
|
||
// Register the router instance for type safety | ||
declare module '@tanstack/react-router' { | ||
interface Register { | ||
router: typeof router | ||
} | ||
} | ||
|
||
// Render the app | ||
const rootElement = document.getElementById('app') | ||
if (rootElement && !rootElement.innerHTML) { | ||
const root = ReactDOM.createRoot(rootElement) | ||
root.render( | ||
<StrictMode> | ||
<RouterProvider router={router} /> | ||
</StrictMode>, | ||
) | ||
} | ||
|
||
// If you want to start measuring performance in your app, pass a function | ||
// to log results (for example: reportWebVitals(console.log)) | ||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals | ||
reportWebVitals() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
const reportWebVitals = (onPerfEntry?: () => void) => { | ||
if (onPerfEntry && onPerfEntry instanceof Function) { | ||
import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => { | ||
onCLS(onPerfEntry) | ||
onINP(onPerfEntry) | ||
onFCP(onPerfEntry) | ||
onLCP(onPerfEntry) | ||
onTTFB(onPerfEntry) | ||
}) | ||
} | ||
} | ||
|
||
export default reportWebVitals |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/* eslint-disable */ | ||
|
||
// @ts-nocheck | ||
|
||
// noinspection JSUnusedGlobalSymbols | ||
|
||
// This file was automatically generated by TanStack Router. | ||
// You should NOT make any changes in this file as it will be overwritten. | ||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. | ||
|
||
import { Route as rootRouteImport } from './routes/__root' | ||
import { Route as ImageIdRouteImport } from './routes/$imageId' | ||
import { Route as IndexRouteImport } from './routes/index' | ||
|
||
const ImageIdRoute = ImageIdRouteImport.update({ | ||
id: '/$imageId', | ||
path: '/$imageId', | ||
getParentRoute: () => rootRouteImport, | ||
} as any) | ||
const IndexRoute = IndexRouteImport.update({ | ||
id: '/', | ||
path: '/', | ||
getParentRoute: () => rootRouteImport, | ||
} as any) | ||
|
||
export interface FileRoutesByFullPath { | ||
'/': typeof IndexRoute | ||
'/$imageId': typeof ImageIdRoute | ||
} | ||
export interface FileRoutesByTo { | ||
'/': typeof IndexRoute | ||
'/$imageId': typeof ImageIdRoute | ||
} | ||
export interface FileRoutesById { | ||
__root__: typeof rootRouteImport | ||
'/': typeof IndexRoute | ||
'/$imageId': typeof ImageIdRoute | ||
} | ||
export interface FileRouteTypes { | ||
fileRoutesByFullPath: FileRoutesByFullPath | ||
fullPaths: '/' | '/$imageId' | ||
fileRoutesByTo: FileRoutesByTo | ||
to: '/' | '/$imageId' | ||
id: '__root__' | '/' | '/$imageId' | ||
fileRoutesById: FileRoutesById | ||
} | ||
export interface RootRouteChildren { | ||
IndexRoute: typeof IndexRoute | ||
ImageIdRoute: typeof ImageIdRoute | ||
} | ||
|
||
declare module '@tanstack/react-router' { | ||
interface FileRoutesByPath { | ||
'/$imageId': { | ||
id: '/$imageId' | ||
path: '/$imageId' | ||
fullPath: '/$imageId' | ||
preLoaderRoute: typeof ImageIdRouteImport | ||
parentRoute: typeof rootRouteImport | ||
} | ||
'/': { | ||
id: '/' | ||
path: '/' | ||
fullPath: '/' | ||
preLoaderRoute: typeof IndexRouteImport | ||
parentRoute: typeof rootRouteImport | ||
} | ||
} | ||
} | ||
|
||
const rootRouteChildren: RootRouteChildren = { | ||
IndexRoute: IndexRoute, | ||
ImageIdRoute: ImageIdRoute, | ||
} | ||
export const routeTree = rootRouteImport | ||
._addFileChildren(rootRouteChildren) | ||
._addFileTypes<FileRouteTypes>() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { createFileRoute } from '@tanstack/react-router' | ||
import { ResponsiveImage } from '@responsive-image/react' | ||
import { getImage } from '../images.ts' | ||
|
||
export const Route = createFileRoute('/$imageId')({ | ||
loader: ({ params }) => getImage(params.imageId), | ||
component: Image, | ||
}) | ||
|
||
function Image() { | ||
const image = Route.useLoaderData() | ||
|
||
return <ResponsiveImage src={image} className="large" /> | ||
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. 🛠️ Refactor suggestion Add accessible alt text (and consider a remount key to work around the ThumbHash LQIP bug). Provide meaningful alt text; optionally key by imageId to force a remount when switching images. Apply: function Image() {
- const image = Route.useLoaderData()
+ const image = Route.useLoaderData()
+ const { imageId } = Route.useParams()
- return <ResponsiveImage src={image} className="large" />
+ return (
+ <ResponsiveImage
+ key={imageId}
+ src={image}
+ className="large"
+ alt={image.alt ?? image.title ?? imageId}
+ />
+ )
} Also applies to: 12-12 🤖 Prompt for AI Agents
|
||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,38 @@ | ||||||||||||||
import { Outlet, Link, createRootRoute } from '@tanstack/react-router' | ||||||||||||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' | ||||||||||||||
import { TanstackDevtools } from '@tanstack/react-devtools' | ||||||||||||||
import { ResponsiveImage } from '@responsive-image/react' | ||||||||||||||
import './app.css' | ||||||||||||||
import { getThumbsnails } from '../images.ts' | ||||||||||||||
|
||||||||||||||
export const Route = createRootRoute({ | ||||||||||||||
component: App, | ||||||||||||||
}) | ||||||||||||||
|
||||||||||||||
function App() { | ||||||||||||||
return ( | ||||||||||||||
<> | ||||||||||||||
<aside> | ||||||||||||||
{Object.entries(getThumbsnails()).map(([imageId, image]) => ( | ||||||||||||||
<Link to="/$imageId" params={{ imageId }} key={imageId}> | ||||||||||||||
<ResponsiveImage src={image} width={200}></ResponsiveImage> | ||||||||||||||
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. 🛠️ Refactor suggestion Add alt text to thumbnails and use a self-closing tag. Improves accessibility and consistency. Apply: - <ResponsiveImage src={image} width={200}></ResponsiveImage>
+ <ResponsiveImage
+ src={image}
+ width={200}
+ alt={image.alt || image.title || imageId}
+ /> 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||
</Link> | ||||||||||||||
))} | ||||||||||||||
</aside> | ||||||||||||||
<main> | ||||||||||||||
<Outlet /> | ||||||||||||||
</main> | ||||||||||||||
<TanstackDevtools | ||||||||||||||
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. nice using the new devtools! |
||||||||||||||
config={{ | ||||||||||||||
position: 'bottom-left', | ||||||||||||||
}} | ||||||||||||||
plugins={[ | ||||||||||||||
{ | ||||||||||||||
name: 'Tanstack Router', | ||||||||||||||
render: <TanStackRouterDevtoolsPanel />, | ||||||||||||||
}, | ||||||||||||||
]} | ||||||||||||||
/> | ||||||||||||||
</> | ||||||||||||||
) | ||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
aside { | ||
display: flex; | ||
overflow: auto; | ||
justify-content: center; | ||
|
||
margin-bottom: 5px; | ||
} | ||
|
||
aside a { | ||
border: solid 1px transparent; | ||
} | ||
|
||
aside a.active { | ||
border-color: #00c2ab; | ||
} | ||
|
||
img { | ||
display: block; | ||
} | ||
|
||
main { | ||
display: flex; | ||
justify-content: center; | ||
flex-wrap: wrap; | ||
} | ||
|
||
.intro { | ||
padding: 20px; | ||
} | ||
|
||
img.large { | ||
max-height: calc(100vh - 140px); | ||
width: auto; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.