Skip to content

Commit

Permalink
Implement feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
RoelLeijser committed Nov 5, 2024
1 parent 29180be commit 8331c90
Show file tree
Hide file tree
Showing 20 changed files with 176 additions and 348 deletions.
16 changes: 10 additions & 6 deletions browser/create-template/templates/nextjs-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { getCurrentResource } from '@/atomic/getCurrentResource';
import { env } from '@/env';
import FullPageView from '@/views/FullPage/FullPageView';
import { core } from '@tomic/lib';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

const Page = async (props: {
type Props = {
params: Promise<{
slug: string[];
}>;
searchParams?: Promise<{
search: string;
[key: string]: string | string[] | undefined;
}>;
}) => {
};

export const generateMetadata = async ({
params,
}: Props): Promise<Metadata> => {
const slug = (await params).slug;

const resourceUrl = new URL(
`${env.NEXT_PUBLIC_ATOMIC_SERVER_URL}/${slug.join('/')}`,
);
const resource = await getCurrentResource(resourceUrl);

return {
title: resource?.title,
description: resource?.get(core.properties.description),
};
};

const Page = async (props: Props) => {
const params = await props.params;
const searchParams = await props.searchParams;
const resourceUrl = new URL(
Expand Down
12 changes: 12 additions & 0 deletions browser/create-template/templates/nextjs-site/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import FullPageView from '@/views/FullPage/FullPageView';
import { notFound } from 'next/navigation';
import { getCurrentResource } from '@/atomic/getCurrentResource';
import { env } from '@/env';
import { Metadata } from 'next';
import { core } from '@tomic/lib';

export const generateMetadata = async (): Promise<Metadata> => {
const url = new URL(env.NEXT_PUBLIC_ATOMIC_SERVER_URL);
const resource = await getCurrentResource(url);

return {
title: resource?.title,
description: resource?.get(core.properties.description),
};
};

export default async function Page() {
const url = new URL(env.NEXT_PUBLIC_ATOMIC_SERVER_URL);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { env } from '@/env';
import { website } from '@/ontologies/website';
import { CollectionBuilder, core, Store } from '@tomic/lib';
import { CollectionBuilder, core } from '@tomic/lib';
import { store } from '@/app/store';

export async function getAllBlogposts(): Promise<string[]> {
const store = new Store({
serverUrl: env.NEXT_PUBLIC_ATOMIC_SERVER_URL,
});

const collection = new CollectionBuilder(store)
.setProperty(core.properties.isA)
.setValue(website.classes.blogpost)
Expand Down
248 changes: 13 additions & 235 deletions browser/create-template/templates/nextjs-site/src/components/Image.tsx
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(', ');
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode } from 'react';
import React, { FC, ReactNode } from 'react';
import clsx from 'clsx';
import styles from './HStack.module.css';

Expand All @@ -11,14 +11,14 @@ interface HStackProps {
children: ReactNode;
}

const HStack: React.FC<HStackProps> = ({
const HStack = ({
gap = '1rem',
align = 'start',
justify = 'start',
fullWidth = false,
wrap = false,
children,
}) => {
}: HStackProps) => {
const inlineStyles: {
[key: string]: string | number;
} = {
Expand Down
Loading

0 comments on commit 8331c90

Please sign in to comment.