Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 114 additions & 4 deletions examples/block-extension/src/blocks/HeroBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { defineBlock, useBlockState } from '@instantcommerce/sdk';
import {
defineBlock,
InferBlockState,
useBlockState,
} from '@instantcommerce/sdk';
import './index.css';

const Hero = () => {
const { content } = useBlockState();
const { content, customizer } =
useBlockState<InferBlockState<typeof HeroBlock>>();

return (
<div className="p-8 min-h-[600px] bg-gray-800">
{content.cards![0].value}
<h1 className="text-8xl text-white">{content.title}</h1>
</div>
);
Expand All @@ -16,12 +22,116 @@ const HeroBlock = defineBlock({
customizerSchema: {
fields: {
color: { type: 'text' },
number: { type: 'number', min: 2, max: 80, fractionDigits: 3 },
padding: {
type: 'select',
options: [
{ label: 'Small', value: 'sm' },
{ label: 'Large', value: 'lg' },
],
preview: 'sm',
},
textColor: { type: 'color', preview: '#A020F0' },
text: { type: 'text', maxLength: 1, isRequired: true },
toggle: { type: 'toggle', preview: true },
},
},
} as const,
contentSchema: {
fields: {
title: { type: 'text', label: 'Title', preview: 'Hero title' },
},
select: {
type: 'select',
preview: 'value1',
options: [
{ value: 'value1', label: 'Option 1' },
{ value: 'value2', label: 'Option 2' },
],
},
date: {
type: 'date',
preview: '2022-12-12 08:12',
withTime: true,
},
image: {
type: 'image',
preview:
'https://images.unsplash.com/photo-1669962367460-00b711b2e3f3?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=700&q=80',
},
link: {
type: 'link',
preview: 'https://google.com',
},
richText: {
type: 'richText',
preview: 'Rich text',
toolbar: [],
},
cards: {
type: 'subschema',
allowed: ['card'],
max: 3,
preview: [
{
subschema: 'card',
value: {
cardTitle: 'Card title',
cardImage:
'https://a.storyblok.com/f/145828/5000x3333/564e281ca1/force-majeure-du8abwm5z2g-unsplash.jpg',
cardButtons: [
{
subschema: 'button',
value: {
text: 'Button',
link: 'https://a.storyblok.com/f/145828/5000x3333/564e281ca1/force-majeure-du8abwm5z2g-unsplash.jpg',
},
},
],
},
},
],
},
} as const,
subschemas: {
button: {
fields: {
text: {
type: 'text',
label: 'Text',
isTranslatable: true,
isRequired: true,
maxLength: 40,
},
link: {
type: 'link',
label: 'Link',
isTranslatable: true,
isRequired: true,
},
},
},
card: {
fields: {
cardImage: {
type: 'image',
label: 'Image',
isRequired: true,
},
cardTitle: {
type: 'text',
label: 'Title',
isTranslatable: true,
isRequired: true,
},
cardButtons: {
type: 'subschema',
label: 'Buttons',
isRequired: false,
max: 2,
allowed: ['button'],
},
},
},
} as const,
},
});

Expand Down
6 changes: 5 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./src": {
"import": "./src/index.ts"
}
},
"main": "./dist/index.cjs",
Expand All @@ -35,13 +38,14 @@
"react-dom": "18.2.0",
"storyblok-rich-text-react-renderer": "2.6.1",
"tsconfig": "*",
"ts-toolbelt": "9.6.0",
"types": "*"
},
"devDependencies": {
"@types/react": "18.0.5",
"@types/react-dom": "18.0.1",
"@vitejs/plugin-react": "2.0.0",
"tsup": "6.5.0",
"typescript": "4.3.5"
"typescript": "4.9.3"
}
}
162 changes: 154 additions & 8 deletions packages/client/src/defineBlock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createElement, ReactElement, ReactNode } from 'react';
import { render as remoteRender, RemoteRoot } from '@remote-ui/react';
import { Narrow } from 'ts-toolbelt/out/Function/Narrow';
/** This relative import forces types from this package to be included in this bundle */
import {
DefineContentSchema,
Expand All @@ -24,7 +25,7 @@ function render(
(self as any).render(callback, contentSchema, customizerSchema);
}

interface DefineBlockParams {
type DefineBlockParams = {
/** React component rendered by the block */
component: () => RenderElement | ReactElement;
preview?: {
Expand All @@ -35,14 +36,16 @@ interface DefineBlockParams {
customizerSchema?: DefineCustomizerSchema;
/** The content schema that is synced to the CMS */
contentSchema?: DefineContentSchema;
}
};

export const defineBlock = <T>(params: Narrow<T>) => {
const {
component,
preview: { decorators } = {},
customizerSchema,
contentSchema,
} = params as DefineBlockParams;

export const defineBlock = ({
component,
preview: { decorators } = {},
customizerSchema,
contentSchema,
}: DefineBlockParams) => {
render(
(root, blockProps) => {
const renderedComponent =
Expand All @@ -64,4 +67,147 @@ export const defineBlock = ({
contentSchema,
customizerSchema,
);

return params;
};

export interface AssetStoryblok {
alt?: string;
copyright?: string;
id: number;
filename: string;
name: string;
title?: string;
focus?: string;
[k: string]: any;
}

export type LinkStoryblok =
| {
cached_url?: string;
linktype?: string;
[k: string]: any;
}
| {
id?: string;
cached_url?: string;
linktype?: 'story';
[k: string]: any;
}
| {
url?: string;
cached_url?: string;
linktype?: 'asset' | 'url';
[k: string]: any;
}
| {
email?: string;
linktype?: 'email';
[k: string]: any;
};

export interface RichTextStoryblok {
content?: RichTextStoryblok[];
marks?: RichTextStoryblok[];
attrs?: any;
text?: string;
type: string;
}

type ContentFieldValues<
C extends DefineContentSchema<
C['subschemas'] extends { subschemas: any } ? C['subschemas'] : {}
>,
T extends C['fields'][keyof C['fields']],
S extends C['subschemas'],
> = T extends {
type: 'image';
}
? AssetStoryblok
: T extends {
type: 'link';
}
? LinkStoryblok
: T extends {
type: 'richText';
}
? RichTextStoryblok
: T extends {
type: 'select';
}
? T['options'][number]['value']
: T extends {
type: 'subschema';
}
? ContentSubschema<C, T, S>
: string;

type ContentSubschema<
C extends DefineContentSchema<
C['subschemas'] extends { subschemas: any } ? C['subschemas'] : {}
>,
T extends C['fields'][keyof C['fields']],
S extends C['subschemas'],
A extends keyof S = keyof S,
> = {
subschema: A;
value: ContentSubschemaValues<C, T, S, A>;
}[];

type ContentSubschemaValues<
C extends DefineContentSchema<
C['subschemas'] extends { subschemas: any } ? C['subschemas'] : {}
>,
T extends C['fields'][keyof C['fields']],
S extends C['subschemas'],
A extends keyof S,
B extends keyof S[A] = keyof S[A],
D = S[A][B],
> = {
[K in keyof D]: D[K]; // ContentFieldValues<C, S[A]['fields'][K], S>;
};

type ContentFields<
T extends DefineContentSchema<
T['subschemas'] extends { subschemas: any } ? T['subschemas'] : {}
>,
> = {
[name in keyof T['fields']]: T['fields'][name] extends {
isRequired: true;
}
? ContentFieldValues<T, T['fields'][name], T['subschemas']>
: ContentFieldValues<T, T['fields'][name], T['subschemas']> | undefined;
};

type CustomizerFieldValues<
T extends DefineCustomizerSchema['fields'][keyof DefineCustomizerSchema['fields']],
> = T extends {
type: 'number';
}
? number
: T extends {
type: 'toggle';
}
? boolean
: T extends {
type: 'select';
}
? T['options'][number]['value']
: string;

type CustomizerFields<T extends DefineCustomizerSchema['fields']> = {
[name in keyof T]: T[name] extends {
isRequired: true;
}
? CustomizerFieldValues<T[name]>
: CustomizerFieldValues<T[name]> | undefined;
};

export interface InferBlockState<T extends DefineBlockParams> {
content: T['contentSchema'] extends { fields: any }
? ContentFields<T['contentSchema']>
: {};
customizer: T['customizerSchema'] extends { fields: any }
? CustomizerFields<T['customizerSchema']['fields']>
: {};
}
12 changes: 10 additions & 2 deletions packages/client/src/hooks/useBlockState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { useBlockContext } from '../BlockProvider';

export function useBlockState() {
export function useBlockState<
T extends { content: any; customizer: any } = {
content: any;
customizer: any;
},
>() {
const { content, customizer } = useBlockContext();

return { content, customizer };
return { content, customizer } as {
content: T['content'];
customizer: T['customizer'];
};
}
Loading