From 29180beafc28985ae4097d70a15fdc8b64b9c160 Mon Sep 17 00:00:00 2001
From: Roel
Date: Thu, 31 Oct 2024 16:46:44 +0100
Subject: [PATCH] Refactor for client side
---
.../templates/nextjs-site/src/app/layout.tsx | 2 +-
.../nextjs-site/src/components/Image.tsx | 245 +++++++++++++++++-
.../nextjs-site/src/components/NoSSR.tsx | 10 -
.../src/components/ProviderWrapper.tsx | 3 +
.../nextjs-site/src/views/DefaultView.tsx | 11 +-
.../src/views/ListItem/BlogListItem.tsx | 17 +-
.../src/views/ListItem/ListItemView.tsx | 7 +-
7 files changed, 264 insertions(+), 31 deletions(-)
delete mode 100644 browser/create-template/templates/nextjs-site/src/components/NoSSR.tsx
diff --git a/browser/create-template/templates/nextjs-site/src/app/layout.tsx b/browser/create-template/templates/nextjs-site/src/app/layout.tsx
index 51b756f5..44974df5 100644
--- a/browser/create-template/templates/nextjs-site/src/app/layout.tsx
+++ b/browser/create-template/templates/nextjs-site/src/app/layout.tsx
@@ -8,7 +8,7 @@ import Footer from '@/components/Footer';
export const metadata: Metadata = {
title: 'Next.js Atomic',
- description: 'Next.js Atomic template',
+ description: 'A Next.js template for Atomic Server',
};
export default function RootLayout({
diff --git a/browser/create-template/templates/nextjs-site/src/components/Image.tsx b/browser/create-template/templates/nextjs-site/src/components/Image.tsx
index 48a84adc..6be48868 100644
--- a/browser/create-template/templates/nextjs-site/src/components/Image.tsx
+++ b/browser/create-template/templates/nextjs-site/src/components/Image.tsx
@@ -1,13 +1,242 @@
-'use client';
-
-import { Image as AtomicImage } from '@tomic/react';
-import NoSSR from './NoSSR';
+import { store } from '@/app/store';
+import { type Resource, type Server, unknownSubject, server } from '@tomic/lib';
import React from 'react';
+import NextImage from 'next/image';
+
+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',
+]);
+
+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,
+ 'resource' | 'src'
+ > {
+ resource: Resource;
+ /**
+ * 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
+ *
+ * ```
+ * When the image size changes based on media queries we can give the browser a more detailed indication.
+ * ```jsx
+ *
+ * ```
+ */
+ 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 {
+ /** 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
+ *
+ * ```
+ */
+export const Image: React.FC = 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 ;
+ }
+
+ if (!imageFormatsWithFullSupport.has(mimeType)) {
+ throw new Error('Incompatible or missing mime-type: ' + mimeType);
+ }
+
+ return ;
+};
-export const Image = ({ subject, alt }: { subject: string; alt: string }) => {
+const ImageInner: React.FC = ({
+ resource,
+ sizeIndication,
+ quality = 60,
+ ...props
+}) => {
+ // const [downloadUrl] = useString(resource, server.properties.downloadUrl);
+ const downloadUrl = resource.get(server.properties.downloadUrl);
+ const toSrcSet = buildSrcSet(downloadUrl ?? '');
+
+ return (
+
+ );
+};
+
+const BasicImage: React.FC = ({
+ 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 (
-
-
-
+
);
};
+
+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(', ');
+ };
diff --git a/browser/create-template/templates/nextjs-site/src/components/NoSSR.tsx b/browser/create-template/templates/nextjs-site/src/components/NoSSR.tsx
deleted file mode 100644
index 06fbf435..00000000
--- a/browser/create-template/templates/nextjs-site/src/components/NoSSR.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import dynamic from 'next/dynamic';
-import React from 'react';
-
-const NoSsr = (props: { children: React.ReactNode }) => (
- {props.children}
-);
-
-export default dynamic(() => Promise.resolve(NoSsr), {
- ssr: false,
-});
diff --git a/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx b/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx
index a561b50a..16edf855 100644
--- a/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx
+++ b/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx
@@ -3,12 +3,15 @@
import { StoreContext } from '@tomic/react';
import { CurrentSubjectProvider } from '@/app/context/CurrentSubjectContext';
import { store } from '@/app/store';
+import { initOntologies } from '@/ontologies';
const ProviderWrapper = ({
children,
}: {
children: Readonly;
}) => {
+ initOntologies();
+
return (
{children}
diff --git a/browser/create-template/templates/nextjs-site/src/views/DefaultView.tsx b/browser/create-template/templates/nextjs-site/src/views/DefaultView.tsx
index 686b6226..f53a205b 100644
--- a/browser/create-template/templates/nextjs-site/src/views/DefaultView.tsx
+++ b/browser/create-template/templates/nextjs-site/src/views/DefaultView.tsx
@@ -1,10 +1,11 @@
-import { store } from '@/app/store';
-import { Resource } from '@tomic/react';
+'use client';
-const DefaultView = async ({ resource }: { resource: Resource }) => {
- const subjectResource = await store.getResource(resource.subject);
+import { useResource } from '@tomic/react';
- return No supported view for {subjectResource.title}.
;
+const DefaultView = ({ subject }: { subject: string }) => {
+ const resource = useResource(subject);
+
+ return No supported view for {resource.title}.
;
};
export default DefaultView;
diff --git a/browser/create-template/templates/nextjs-site/src/views/ListItem/BlogListItem.tsx b/browser/create-template/templates/nextjs-site/src/views/ListItem/BlogListItem.tsx
index c5cce22e..48f72720 100644
--- a/browser/create-template/templates/nextjs-site/src/views/ListItem/BlogListItem.tsx
+++ b/browser/create-template/templates/nextjs-site/src/views/ListItem/BlogListItem.tsx
@@ -1,17 +1,22 @@
+'use client';
+
import { Blogpost } from '@/ontologies/website';
-import type { Resource } from '@tomic/react';
+import { core, useResource, useString, Image } from '@tomic/react';
import styles from './BlogListItem.module.css';
-import { Image } from '@/components/Image';
-const BlogListItem = async ({ resource }: { resource: Resource }) => {
+const BlogListItem = ({ subject }: { subject: string }) => {
const formatter = new Intl.DateTimeFormat('default', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
- const date = formatter.format(new Date(resource.props.publishedAt));
+ const resource = useResource(subject);
+ const [title] = useString(resource, core.properties.name);
+ const date = resource.props.publishedAt
+ ? formatter.format(new Date(resource.props.publishedAt))
+ : '';
return (
@@ -19,9 +24,9 @@ const BlogListItem = async ({ resource }: { resource: Resource }) => {
{date}
-
{resource.title}
+
{title}
- {resource.props.description.slice(0, 300)}...
+ {resource.props.description?.slice(0, 300)}...
diff --git a/browser/create-template/templates/nextjs-site/src/views/ListItem/ListItemView.tsx b/browser/create-template/templates/nextjs-site/src/views/ListItem/ListItemView.tsx
index 51db199c..932022a0 100644
--- a/browser/create-template/templates/nextjs-site/src/views/ListItem/ListItemView.tsx
+++ b/browser/create-template/templates/nextjs-site/src/views/ListItem/ListItemView.tsx
@@ -2,6 +2,7 @@ import { website } from '@/ontologies/website';
import BlogListItem from './BlogListItem';
import DefaultView from '@/views/DefaultView';
import { store } from '@/app/store';
+import { Suspense } from 'react';
const ListItemView = async ({ subject }: { subject: string }) => {
const listItem = await store.getResource(subject);
@@ -13,7 +14,11 @@ const ListItemView = async ({ subject }: { subject: string }) => {
DefaultView,
);
- return ;
+ return (
+ loading...
}>
+
+
+ );
};
export default ListItemView;