-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
29180be
commit 8331c90
Showing
20 changed files
with
176 additions
and
348 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,23 +15,27 @@ | |
"@tomic/react": "^0.40.0", | ||
"clsx": "^2.1.1", | ||
"gray-matter": "^4.0.3", | ||
"lodash": "^4.17.21", | ||
"modern-css-reset": "^1.4.0", | ||
"next": "15.0.1", | ||
"react": "19.0.0-rc-69d4b800-20241021", | ||
"react-dom": "19.0.0-rc-69d4b800-20241021", | ||
"next": "15.0.2", | ||
"react": "19.0.0-rc-02c0e824-20241028", | ||
"react-dom": "19.0.0-rc-02c0e824-20241028", | ||
"remark": "^15.0.1", | ||
"remark-html": "^16.0.1", | ||
"zod": "^3.23.8" | ||
}, | ||
"devDependencies": { | ||
"@tomic/cli": "^0.40.0", | ||
"@types/lodash": "^4.17.10", | ||
"@types/node": "^20", | ||
"@types/react": "npm:[email protected]", | ||
"@types/react-dom": "npm:[email protected]", | ||
"eslint": "^9.13.0", | ||
"eslint-config-next": "15.0.1", | ||
"eslint-config-next": "15.0.2", | ||
"typescript": "^5" | ||
}, | ||
"pnpm": { | ||
"overrides": { | ||
"@types/react": "npm:[email protected]", | ||
"@types/react-dom": "npm:[email protected]" | ||
} | ||
} | ||
} |
26 changes: 23 additions & 3 deletions
26
browser/create-template/templates/nextjs-site/src/app/[...slug]/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 2 additions & 6 deletions
8
browser/create-template/templates/nextjs-site/src/atomic/getAllBlogposts.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
248 changes: 13 additions & 235 deletions
248
browser/create-template/templates/nextjs-site/src/components/Image.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,242 +1,20 @@ | ||
import { store } from '@/app/store'; | ||
import { type Resource, type Server, unknownSubject, server } from '@tomic/lib'; | ||
import React from 'react'; | ||
import NextImage from 'next/image'; | ||
'use client'; | ||
|
||
const imageFormatsWithBasicSupport = new Set([ | ||
'image/svg+xml', | ||
'image/vnd.adobe.photoshop', | ||
'image/heif', | ||
'image/heif-sequence', | ||
'image/heic-sequence', | ||
'image/avif-sequence', | ||
'image/gif', | ||
'image/heic', | ||
'image/heif', | ||
]); | ||
import { Image as AtomicImage } from '@tomic/react'; | ||
import NoSSR from './NoSSR'; | ||
|
||
const imageFormatsWithFullSupport = new Set([ | ||
'image/png', | ||
'image/jpeg', | ||
'image/vnd.microsoft.icon', | ||
'image/webp', | ||
'image/bmp', | ||
'image/tiff', | ||
'image/avif', | ||
]); | ||
|
||
const DEFAULT_SIZES = [100, 300, 500, 800, 1200, 1600, 2000]; | ||
|
||
type SizeIndicationKey = `${number}px`; | ||
type Unit = number | `${number}${'px' | 'vw' | 'em' | 'rem' | 'ch'}`; | ||
|
||
export type SizeIndication = | ||
| { | ||
[key: SizeIndicationKey]: Unit; | ||
default: Unit; | ||
} | ||
| Unit; | ||
|
||
interface ImageInnerProps | ||
extends Omit< | ||
React.ImgHTMLAttributes<HTMLPictureElement>, | ||
'resource' | 'src' | ||
> { | ||
resource: Resource<Server.File>; | ||
/** | ||
* SizeIndication is used to help the browser choose the right image size to fetch. | ||
* By default, the browser looks at the entire viewport width and chooses the smallest version that still covers this width. | ||
* This is often too big so we should help by giving it an approximation of the size of the image relative to the viewport. | ||
* | ||
* When the unit given is a number it is interpreted as a percentage of the viewport width. If your image is displayed in a static size you can also pass a string like '4rem'. | ||
* Note that percentages don't work as the browser doesn't know the size of the parent element yet. | ||
* | ||
* ```jsx | ||
* <Image | ||
* className='inline-image' | ||
* subject='http://myatomicserver.com/files/1664878581079-hiker.jpg' | ||
* alt='a person standing in front of a mountain' | ||
* sizeIndication={50} // the image is about 50% of the viewport width | ||
* /> | ||
* ``` | ||
* When the image size changes based on media queries we can give the browser a more detailed indication. | ||
* ```jsx | ||
* <Image | ||
* className='inline-image' | ||
* subject='http://myatomicserver.com/files/1664878581079-hiker.jpg' | ||
* alt='a person standing in front of a mountain' | ||
* sizeIndication={{ | ||
* '500px': 100, // On screens smaller than 500px the image is displayed at full width. | ||
* default: 50, // the image is about 50% of the viewport when no media query matches | ||
* }} | ||
* /> | ||
* ``` | ||
*/ | ||
sizeIndication?: SizeIndication; | ||
/** Alt text for the image, if you can't add alt text it's best practice to pass an empty string */ | ||
alt: string; | ||
/** Quality setting used by the image encoders, defaults to 60 (more than enough in most cases). Should be between 0 - 100 */ | ||
quality?: number; | ||
} | ||
|
||
export interface ImageProps extends Omit<ImageInnerProps, 'resource'> { | ||
/** Subject of the file resource */ | ||
subject: string; | ||
} | ||
|
||
/** | ||
* Takes the subject of a file resource and renders it as an image. | ||
* Uses AtomicServer to automatically generate avif and webp versions of the image and scale them to different sizes. | ||
* To help the browser choose the best size to load use the `sizeIndication` prop. | ||
* | ||
* Throws when the file is not an image. | ||
* @example | ||
* ```jsx | ||
* <Image | ||
* subject='http://myatomicserver.com/files/1664878581079-hiker.jpg' | ||
* alt='a person standing in front of a mountain' | ||
* className='article-inline-image' | ||
* loading='lazy' | ||
* sizeIndication={{ | ||
* '500px': 100, // On screens smaller than 500px the image is displayed at full width. | ||
* default: 50, // the image is about 50% of the viewport when no media query matches | ||
* }} | ||
* /> | ||
* ``` | ||
*/ | ||
export const Image: React.FC<ImageProps> = async ({ subject, ...props }) => { | ||
const resource = await store.getResource(subject); | ||
const mimeType = resource.get(server.properties.mimetype); | ||
// const [mimeType] = useString(resource, server.properties.mimetype); | ||
|
||
if (resource.loading || resource.subject === unknownSubject) { | ||
return null; | ||
} | ||
|
||
if (!resource.hasClasses(server.classes.file)) { | ||
throw new Error('Incompatible resource class, resource is not a file'); | ||
} | ||
|
||
// If the resource does have a file class but mimetype is still undefined, it's still loading so we return null until the value is available | ||
if (mimeType === undefined) { | ||
return null; | ||
} | ||
|
||
if (imageFormatsWithBasicSupport.has(mimeType)) { | ||
return <BasicImage resource={resource} {...props} />; | ||
} | ||
|
||
if (!imageFormatsWithFullSupport.has(mimeType)) { | ||
throw new Error('Incompatible or missing mime-type: ' + mimeType); | ||
} | ||
|
||
return <ImageInner resource={resource} {...props} />; | ||
}; | ||
|
||
const ImageInner: React.FC<ImageInnerProps> = ({ | ||
resource, | ||
sizeIndication, | ||
quality = 60, | ||
export const Image = ({ | ||
subject, | ||
alt, | ||
...props | ||
}: { | ||
subject: string; | ||
alt: string; | ||
[key: string]: unknown; | ||
}) => { | ||
// const [downloadUrl] = useString(resource, server.properties.downloadUrl); | ||
const downloadUrl = resource.get(server.properties.downloadUrl); | ||
const toSrcSet = buildSrcSet(downloadUrl ?? ''); | ||
|
||
return ( | ||
<picture> | ||
<source | ||
srcSet={toSrcSet('avif', quality, DEFAULT_SIZES)} | ||
type='image/avif' | ||
sizes={indicationToSizes(sizeIndication)} | ||
height={resource.props.imageHeight} | ||
width={resource.props.imageWidth} | ||
/> | ||
<source | ||
srcSet={toSrcSet('webp', quality, DEFAULT_SIZES)} | ||
type='image/webp' | ||
sizes={indicationToSizes(sizeIndication)} | ||
height={resource.props.imageHeight} | ||
width={resource.props.imageWidth} | ||
/> | ||
{/* eslint-disable-next-line jsx-a11y/alt-text */} | ||
<img | ||
src={downloadUrl} | ||
{...props} | ||
height={resource.props.imageHeight} | ||
width={resource.props.imageWidth} | ||
/> | ||
</picture> | ||
); | ||
}; | ||
|
||
const BasicImage: React.FC<ImageInnerProps> = ({ | ||
resource, | ||
sizeIndication: _sizeIndication, | ||
quality: quality, | ||
...props // html image atrributes only | ||
}) => { | ||
// const [downloadUrl] = useString(resource, server.properties.downloadUrl); | ||
const downloadUrl = resource.get(server.properties.downloadUrl); | ||
|
||
// eslint-disable-next-line jsx-a11y/alt-text | ||
return ( | ||
<NextImage | ||
src={downloadUrl} | ||
{...props} | ||
width={ | ||
typeof props.width === 'string' ? parseInt(props.width) : props.width | ||
} | ||
height={ | ||
typeof props.height === 'string' ? parseInt(props.height) : props.height | ||
} | ||
/> | ||
<NoSSR> | ||
<AtomicImage subject={subject} alt={alt} {...props} /> | ||
</NoSSR> | ||
); | ||
}; | ||
|
||
const indicationToSizes = (indication: SizeIndication | undefined): string => { | ||
if (indication === undefined) { | ||
return '100vw'; | ||
} | ||
|
||
if (typeof indication === 'number' || typeof indication === 'string') { | ||
return parseUnit(indication); | ||
} | ||
|
||
return Object.entries(indication) | ||
.map(([key, value]) => | ||
key === 'default' | ||
? parseUnit(value) | ||
: `(max-width: ${key}) ${parseUnit(value)}`, | ||
) | ||
.join(', '); | ||
}; | ||
|
||
const parseUnit = (unit: Unit): string => | ||
typeof unit === 'number' ? `${unit}vw` : unit; | ||
|
||
const toUrl = ( | ||
base: string, | ||
format?: string, | ||
quality?: number, | ||
width?: number, | ||
) => { | ||
const url = new URL(base); | ||
const queryParams = new URLSearchParams(); | ||
format && queryParams.set('f', format); | ||
width && queryParams.set('w', width.toString()); | ||
quality && queryParams.set('q', quality.toString()); | ||
url.search = queryParams.toString(); | ||
|
||
return url.toString(); | ||
}; | ||
|
||
const buildSrcSet = | ||
(base: string) => | ||
(format: string, quality: number, sizes: number[]): string => { | ||
return sizes | ||
.map(size => { | ||
return `${toUrl(base, format, quality, size)} ${size}w`; | ||
}) | ||
.join(', '); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.