@androbinco/library-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/add.js +107 -0
- package/src/commands/list.js +51 -0
- package/src/index.js +38 -73
- package/src/templates/carousel/components/navigation-buttons.tsx +1 -0
- package/src/templates/carousel/components/pagination/bullet.pagination.carousel.tsx +69 -0
- package/src/templates/carousel/components/pagination/number.pagination.carousel.tsx +30 -0
- package/src/templates/carousel/components/pagination/progress/progress.pagination.carousel.tsx +99 -0
- package/src/templates/carousel/components/pagination/progress/use-slide-progress.tsx +31 -0
- package/src/templates/carousel/components/pagination.tsx +47 -82
- package/src/templates/faqs-accordion/examples/faqs-showcase.tsx +42 -0
- package/src/templates/faqs-accordion/faqs-accordion.tsx +70 -0
- package/src/templates/faqs-accordion/mock-data.ts +38 -0
- package/src/templates/faqs-accordion/types.ts +18 -0
- package/src/templates/in-view/data.in-view.ts +1 -1
- package/src/templates/in-view/in-view-animation.tsx +7 -7
- package/src/templates/in-view/in-view-grid.tsx +22 -20
- package/src/templates/in-view/in-view-hidden-text.tsx +7 -7
- package/src/templates/in-view/in-view-stroke-line.tsx +6 -5
- package/src/templates/lenis/examples/providers.tsx +23 -0
- package/src/templates/lenis/lenis-provider.tsx +46 -0
- package/src/templates/scroll-components/hooks/use-client-dimensions.ts +21 -0
- package/src/templates/scroll-components/parallax/examples/parallax-showcase.tsx +87 -0
- package/src/templates/scroll-components/parallax/parallax.css +36 -0
- package/src/templates/scroll-components/parallax/parallax.tsx +67 -0
- package/src/templates/scroll-components/scale-gallery/components/expanding-element.tsx +40 -0
- package/src/templates/scroll-components/scale-gallery/examples/scale-gallery-showcase.tsx +68 -0
- package/src/templates/scroll-components/scale-gallery/scale-gallery.tsx +57 -0
- package/src/templates/scroll-components/scroll-tracker-showcase.tsx +44 -0
- package/src/templates/strapi-dynamic-zone/README.md +157 -0
- package/src/templates/strapi-dynamic-zone/dynamic-zone.tsx +113 -0
- package/src/templates/strapi-dynamic-zone/examples/page.tsx +53 -0
- package/src/templates/strapi-dynamic-zone/examples/renderers.tsx +74 -0
- package/src/templates/strapi-dynamic-zone/examples/types.ts +41 -0
- package/src/templates/strapi-dynamic-zone/index.ts +11 -0
- package/src/templates/strapi-dynamic-zone/types.ts +73 -0
- package/src/utils/components.js +187 -5
- package/src/utils/files.js +13 -12
- package/src/templates/scroll-tracker/examples/scroll-tracker-showcase.tsx +0 -90
- /package/src/templates/{scroll-tracker → scroll-components}/scroll-tracker-provider.tsx +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { motion, useTransform } from 'motion/react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/common/utils/classname-builder';
|
|
4
|
+
import { useScrollTrackerContext } from '../../scroll-tracker-provider';
|
|
5
|
+
|
|
6
|
+
export const ExpandingElement = ({
|
|
7
|
+
scaleOffset,
|
|
8
|
+
height,
|
|
9
|
+
children,
|
|
10
|
+
widthOffset,
|
|
11
|
+
className,
|
|
12
|
+
}: {
|
|
13
|
+
scaleOffset: [number, number, number];
|
|
14
|
+
widthOffset: [number, number, number];
|
|
15
|
+
height: number | string;
|
|
16
|
+
className?: string;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}) => {
|
|
19
|
+
const { scrollYProgress } = useScrollTrackerContext();
|
|
20
|
+
|
|
21
|
+
const width = useTransform(scrollYProgress, scaleOffset, widthOffset);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="absolute inset-1/2 -translate-x-1/2 -translate-y-1/2" style={{ height }}>
|
|
25
|
+
<motion.div
|
|
26
|
+
className="absolute inset-1/2 flex h-full -translate-x-1/2 -translate-y-1/2 items-center justify-center overflow-hidden"
|
|
27
|
+
style={{ width, height }}
|
|
28
|
+
>
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
'absolute inset-1/2 h-full w-full min-w-100 -translate-x-1/2 -translate-y-1/2 object-cover',
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
</motion.div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Text } from "@/common/ui/text/text";
|
|
3
|
+
|
|
4
|
+
import { ScrollTrackerProvider } from "../scroll-tracker-provider";
|
|
5
|
+
import { ExpandingElement } from "../scale-gallery/components/expanding-element";
|
|
6
|
+
import { useClientDimensions } from "../hooks/use-client-dimensions";
|
|
7
|
+
|
|
8
|
+
export const ScaleGalleryShowcase = () => {
|
|
9
|
+
const { width, height } = useClientDimensions();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-12">
|
|
13
|
+
<div className="h-screen flex items-center justify-center bg-bg-primary">
|
|
14
|
+
<Text variant="title.4">Scale Gallery Example</Text>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<ScrollTrackerProvider height={500} offset={["0 0", "1 1"]}>
|
|
18
|
+
<Text className="max-lg:px-4 px-8 mb-8" variant="title.6">
|
|
19
|
+
Gallery with Scale Animation
|
|
20
|
+
</Text>
|
|
21
|
+
<div className="flex h-full w-full flex-col items-center justify-center">
|
|
22
|
+
<ExpandingElement
|
|
23
|
+
className="flex flex-col items-center justify-between gap-2"
|
|
24
|
+
height={height * 0.5}
|
|
25
|
+
scaleOffset={[0.05, 0.2, 0.4]}
|
|
26
|
+
widthOffset={[0, width / 2, width * 0.8]}
|
|
27
|
+
>
|
|
28
|
+
<img
|
|
29
|
+
alt="gallery-1"
|
|
30
|
+
className="h-full w-full object-cover rounded-lg"
|
|
31
|
+
src="https://images.pexels.com/photos/34533069/pexels-photo-34533069.jpeg"
|
|
32
|
+
/>
|
|
33
|
+
</ExpandingElement>
|
|
34
|
+
|
|
35
|
+
<ExpandingElement
|
|
36
|
+
className="flex items-center justify-center"
|
|
37
|
+
height={height * 0.5}
|
|
38
|
+
scaleOffset={[0.3, 0.5, 0.7]}
|
|
39
|
+
widthOffset={[0, width / 2, width * 0.8]}
|
|
40
|
+
>
|
|
41
|
+
<img
|
|
42
|
+
alt="gallery-2"
|
|
43
|
+
className="h-full w-full object-cover rounded-lg"
|
|
44
|
+
src="https://images.pexels.com/photos/34712722/pexels-photo-34712722.jpeg"
|
|
45
|
+
/>
|
|
46
|
+
</ExpandingElement>
|
|
47
|
+
|
|
48
|
+
<ExpandingElement
|
|
49
|
+
className="flex items-center justify-center"
|
|
50
|
+
height={height * 0.5}
|
|
51
|
+
scaleOffset={[0.5, 0.8, 0.97]}
|
|
52
|
+
widthOffset={[0, width / 2, width * 0.8]}
|
|
53
|
+
>
|
|
54
|
+
<img
|
|
55
|
+
alt="gallery-3"
|
|
56
|
+
className="h-full w-full object-cover rounded-lg"
|
|
57
|
+
src="https://images.pexels.com/photos/3833517/pexels-photo-3833517.jpeg"
|
|
58
|
+
/>
|
|
59
|
+
</ExpandingElement>
|
|
60
|
+
</div>
|
|
61
|
+
</ScrollTrackerProvider>
|
|
62
|
+
|
|
63
|
+
<div className="h-screen flex items-center justify-center bg-bg-primary">
|
|
64
|
+
<Text variant="title.4">Gallery ends here</Text>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Text } from '@/common/ui/text/text';
|
|
3
|
+
|
|
4
|
+
import { ScrollTrackerProvider } from '../scroll-tracker-provider';
|
|
5
|
+
import { useClientDimensions } from '../hooks/use-client-dimensions';
|
|
6
|
+
|
|
7
|
+
import { ExpandingElement } from './components/expanding-element';
|
|
8
|
+
|
|
9
|
+
export const ScaleGallery = () => {
|
|
10
|
+
const { width, height } = useClientDimensions();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<ScrollTrackerProvider height={500} offset={['0 0', '1 1']}>
|
|
14
|
+
<Text className="max-lg:px-4 px-8" variant="title.6">
|
|
15
|
+
Scale Gallery
|
|
16
|
+
</Text>
|
|
17
|
+
<div className='flex h-full w-full flex-col items-center justify-center'>
|
|
18
|
+
<ExpandingElement
|
|
19
|
+
className="flex flex-col items-center justify-between gap-2"
|
|
20
|
+
height={height * 0.5}
|
|
21
|
+
scaleOffset={[0.05, 0.2, 0.4]}
|
|
22
|
+
widthOffset={[0, width / 2, width * 0.8]}
|
|
23
|
+
>
|
|
24
|
+
<img
|
|
25
|
+
alt="gallery-image-1"
|
|
26
|
+
className="h-full w-full object-cover"
|
|
27
|
+
src="/img/gallery/gallery-example1.jpg"
|
|
28
|
+
/>
|
|
29
|
+
</ExpandingElement>
|
|
30
|
+
<ExpandingElement
|
|
31
|
+
className="flex items-center justify-center"
|
|
32
|
+
height={height * 0.5}
|
|
33
|
+
scaleOffset={[0.3, 0.5, 0.7]}
|
|
34
|
+
widthOffset={[0, width / 2, width * 0.8]}
|
|
35
|
+
>
|
|
36
|
+
<img
|
|
37
|
+
alt="gallery-image-2"
|
|
38
|
+
className="h-full w-full object-cover"
|
|
39
|
+
src="https://images.pexels.com/photos/34533069/pexels-photo-34533069.jpeg"
|
|
40
|
+
/>
|
|
41
|
+
</ExpandingElement>
|
|
42
|
+
<ExpandingElement
|
|
43
|
+
className="flex items-center justify-center"
|
|
44
|
+
height={height * 0.5}
|
|
45
|
+
scaleOffset={[0.5, 0.8, 0.97]}
|
|
46
|
+
widthOffset={[0, width / 2, width * 0.8]}
|
|
47
|
+
>
|
|
48
|
+
<img
|
|
49
|
+
alt="gallery-image-3"
|
|
50
|
+
className="h-full w-full object-cover"
|
|
51
|
+
src="https://images.pexels.com/photos/34712722/pexels-photo-34712722.jpeg"
|
|
52
|
+
/>
|
|
53
|
+
</ExpandingElement>
|
|
54
|
+
</div>
|
|
55
|
+
</ScrollTrackerProvider>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { motion, useTransform } from 'motion/react';
|
|
3
|
+
import { ScrollTrackerProvider, useScrollTrackerContext } from './scroll-tracker-provider';
|
|
4
|
+
import { Text } from '@/common/ui/text/text';
|
|
5
|
+
|
|
6
|
+
const ScrollProgressBar = () => {
|
|
7
|
+
const { scrollYProgress } = useScrollTrackerContext();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<motion.div
|
|
11
|
+
className="fixed top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 to-purple-500 origin-left"
|
|
12
|
+
style={{ scaleX: scrollYProgress }}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ScrollContent = () => {
|
|
18
|
+
const { scrollYProgress } = useScrollTrackerContext();
|
|
19
|
+
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0.3, 1, 0.3]);
|
|
20
|
+
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<motion.div
|
|
24
|
+
className="flex flex-col items-center justify-center h-screen gap-4"
|
|
25
|
+
style={{ opacity, scale }}
|
|
26
|
+
>
|
|
27
|
+
<Text className="text-center" variant="title.6">
|
|
28
|
+
Scroll Progress: {scrollYProgress}
|
|
29
|
+
</Text>
|
|
30
|
+
<div className="w-64 h-64 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-lg" />
|
|
31
|
+
</motion.div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ScrollTrackerShowcase = () => {
|
|
36
|
+
return (
|
|
37
|
+
<ScrollTrackerProvider height={500}>
|
|
38
|
+
<ScrollProgressBar />
|
|
39
|
+
<div className='flex h-full w-full flex-col items-center justify-center'>
|
|
40
|
+
<ScrollContent />
|
|
41
|
+
</div>
|
|
42
|
+
</ScrollTrackerProvider>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Strapi Dynamic Zone Renderer
|
|
2
|
+
|
|
3
|
+
A type-safe, composable component for rendering Strapi Dynamic Zones in Next.js applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Full TypeScript support** - Leverages discriminated unions for type-safe component mapping
|
|
8
|
+
- **Single source of truth** - Your Strapi types define both data shape and rendering
|
|
9
|
+
- **Composable** - Mix and match components, add extra props, customize wrappers
|
|
10
|
+
- **Zero dependencies** - Just React
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Copy the `strapi-dynamic-renderer` folder to your project (e.g., `src/lib/strapi-dynamic-renderer`).
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Define your component types
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// types/page-components.ts
|
|
22
|
+
export type HeroComponent = {
|
|
23
|
+
__component: 'hero.hero';
|
|
24
|
+
id: number;
|
|
25
|
+
title: string;
|
|
26
|
+
subtitle: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CTAComponent = {
|
|
30
|
+
__component: 'cta.cta';
|
|
31
|
+
id: number;
|
|
32
|
+
heading: string;
|
|
33
|
+
buttonText: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Union of all dynamic zone components
|
|
37
|
+
export type PageComponent = HeroComponent | CTAComponent;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Create your renderers
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// renderers/page-renderers.tsx
|
|
44
|
+
import type { ComponentRendererMap } from '@/lib/strapi-dynamic-renderer';
|
|
45
|
+
import type { PageComponent } from '@/types/page-components';
|
|
46
|
+
|
|
47
|
+
export const pageRenderers: ComponentRendererMap<PageComponent> = {
|
|
48
|
+
'hero.hero': ({ title, subtitle }) => (
|
|
49
|
+
<Hero title={title} subtitle={subtitle} />
|
|
50
|
+
),
|
|
51
|
+
'cta.cta': ({ heading, buttonText }) => (
|
|
52
|
+
<CTA heading={heading} buttonText={buttonText} />
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Use in your pages
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// app/page.tsx
|
|
61
|
+
import { DynamicZone } from '@/lib/strapi-dynamic-renderer';
|
|
62
|
+
import { pageRenderers } from '@/renderers/page-renderers';
|
|
63
|
+
|
|
64
|
+
export default function Page({ data }) {
|
|
65
|
+
return (
|
|
66
|
+
<DynamicZone
|
|
67
|
+
components={data.dynamicZone}
|
|
68
|
+
renderers={pageRenderers}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API Reference
|
|
75
|
+
|
|
76
|
+
### `DynamicZone`
|
|
77
|
+
|
|
78
|
+
Renders an array of Strapi dynamic zone components.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<DynamicZone
|
|
82
|
+
components={PageComponent[]} // Required: Array of components
|
|
83
|
+
renderers={ComponentRendererMap} // Required: Renderer map
|
|
84
|
+
getExtraProps={(comp, idx) => {}} // Optional: Extra props per component
|
|
85
|
+
fallback={(comp) => <div />} // Optional: Fallback for unknown types
|
|
86
|
+
getKey={(comp, idx) => string} // Optional: Custom key generator
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `createDynamicZone`
|
|
91
|
+
|
|
92
|
+
Factory function to create a pre-bound DynamicZone component.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// Create once
|
|
96
|
+
export const PageZone = createDynamicZone(pageRenderers);
|
|
97
|
+
|
|
98
|
+
// Use anywhere
|
|
99
|
+
<PageZone components={data.Page} />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Type Utilities
|
|
103
|
+
|
|
104
|
+
### `ExtractComponentProps<TUnion, TKey>`
|
|
105
|
+
|
|
106
|
+
Extracts props for a specific component type:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
type HeroProps = ExtractComponentProps<PageComponent, 'hero.hero'>;
|
|
110
|
+
// { id: number; title: string; subtitle: string }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `ComponentRendererMap<TUnion, TExtra>`
|
|
114
|
+
|
|
115
|
+
Creates a type-safe renderer map:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const renderers: ComponentRendererMap<PageComponent, { index: number }> = {
|
|
119
|
+
'hero.hero': ({ title, index }) => <Hero title={title} position={index} />,
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Patterns
|
|
124
|
+
|
|
125
|
+
### Adding extra props to all renderers
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
type ExtraProps = { index: number; dataSource: string };
|
|
129
|
+
|
|
130
|
+
const renderers: ComponentRendererMap<PageComponent, ExtraProps> = {
|
|
131
|
+
'hero.hero': ({ title, index, dataSource }) => (
|
|
132
|
+
<Hero title={title} position={index} source={dataSource} />
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
<DynamicZone
|
|
137
|
+
components={data}
|
|
138
|
+
renderers={renderers}
|
|
139
|
+
getExtraProps={(_, index) => ({ index, dataSource: 'homepage' })}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Development fallback
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<DynamicZone
|
|
147
|
+
components={data}
|
|
148
|
+
renderers={renderers}
|
|
149
|
+
fallback={(comp) => (
|
|
150
|
+
process.env.NODE_ENV === 'development' ? (
|
|
151
|
+
<div className="border-2 border-dashed border-red-500 p-4">
|
|
152
|
+
Missing renderer: {comp.__component}
|
|
153
|
+
</div>
|
|
154
|
+
) : null
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Fragment } from "react";
|
|
2
|
+
import type { StrapiComponent, DynamicZoneProps } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders an array of Strapi dynamic zone components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <DynamicZone
|
|
10
|
+
* components={page.dynamicZone}
|
|
11
|
+
* renderers={componentRenderers}
|
|
12
|
+
* getExtraProps={(_, index) => ({ index })}
|
|
13
|
+
* />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function DynamicZone<
|
|
17
|
+
TUnion extends StrapiComponent,
|
|
18
|
+
TExtra extends object = object
|
|
19
|
+
>({
|
|
20
|
+
components,
|
|
21
|
+
renderers,
|
|
22
|
+
getExtraProps,
|
|
23
|
+
fallback,
|
|
24
|
+
getKey,
|
|
25
|
+
}: DynamicZoneProps<TUnion, TExtra>): React.ReactElement | null {
|
|
26
|
+
if (!components || components.length === 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultGetKey = (component: TUnion, index: number) =>
|
|
31
|
+
component.id ?? `dynamic-${index}`;
|
|
32
|
+
|
|
33
|
+
const keyGenerator = getKey ?? defaultGetKey;
|
|
34
|
+
|
|
35
|
+
const content = components.map((component, index) => {
|
|
36
|
+
const key = keyGenerator(component, index);
|
|
37
|
+
const extraProps = getExtraProps?.(component, index) as TExtra | undefined;
|
|
38
|
+
const { __component, ...componentProps } = component;
|
|
39
|
+
|
|
40
|
+
const Renderer = renderers[__component as keyof typeof renderers] as
|
|
41
|
+
| ((props: Record<string, unknown> & TExtra) => React.ReactElement | null)
|
|
42
|
+
| undefined;
|
|
43
|
+
|
|
44
|
+
if (!Renderer) {
|
|
45
|
+
if (fallback) {
|
|
46
|
+
return <Fragment key={key}>{fallback(component)}</Fragment>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.env.NODE_ENV === "development") {
|
|
50
|
+
console.warn(
|
|
51
|
+
`[DynamicZone] No renderer found for component: "${__component}"`,
|
|
52
|
+
component
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const props = {
|
|
60
|
+
...componentProps,
|
|
61
|
+
...extraProps,
|
|
62
|
+
} as Parameters<typeof Renderer>[0];
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Fragment key={key}>
|
|
66
|
+
<Renderer {...props} />
|
|
67
|
+
</Fragment>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return <>{content}</>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates a type-safe DynamicZone component bound to specific renderers.
|
|
76
|
+
* Useful when you want to pre-configure renderers and reuse across the app.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* // In your renderers file
|
|
81
|
+
* export const PageZone = createDynamicZone(pageRenderers);
|
|
82
|
+
*
|
|
83
|
+
* // In your page
|
|
84
|
+
* <PageZone
|
|
85
|
+
* components={data.Page}
|
|
86
|
+
* getExtraProps={(_, i) => ({ index: i })}
|
|
87
|
+
* />
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function createDynamicZone<
|
|
91
|
+
TUnion extends StrapiComponent,
|
|
92
|
+
TExtra extends object = object
|
|
93
|
+
>(
|
|
94
|
+
renderers: DynamicZoneProps<TUnion, TExtra>["renderers"],
|
|
95
|
+
defaultFallback?: DynamicZoneProps<TUnion, TExtra>["fallback"]
|
|
96
|
+
) {
|
|
97
|
+
return function BoundDynamicZone({
|
|
98
|
+
components,
|
|
99
|
+
getExtraProps,
|
|
100
|
+
fallback = defaultFallback,
|
|
101
|
+
getKey,
|
|
102
|
+
}: Omit<DynamicZoneProps<TUnion, TExtra>, "renderers">) {
|
|
103
|
+
return (
|
|
104
|
+
<DynamicZone
|
|
105
|
+
components={components}
|
|
106
|
+
renderers={renderers}
|
|
107
|
+
getExtraProps={getExtraProps}
|
|
108
|
+
fallback={fallback}
|
|
109
|
+
getKey={getKey}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using DynamicZone in a Next.js page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DynamicZone } from "../dynamic-zone";
|
|
6
|
+
import { pageRenderers } from "./renderers";
|
|
7
|
+
import type { PageComponent } from "./types";
|
|
8
|
+
|
|
9
|
+
// Example page data type
|
|
10
|
+
type PageData = {
|
|
11
|
+
title: string;
|
|
12
|
+
dynamicZone: PageComponent[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Alternative: Create a pre-bound component in renderers.tsx for cleaner usage
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Then use it more concisely:
|
|
20
|
+
import { PageDynamicZone } from "./renderers";
|
|
21
|
+
|
|
22
|
+
export function ExamplePageAlt({ data }: { data: PageData }) {
|
|
23
|
+
return (
|
|
24
|
+
<main>
|
|
25
|
+
<h1>{data.title}</h1>
|
|
26
|
+
|
|
27
|
+
<PageDynamicZone
|
|
28
|
+
components={data.dynamicZone}
|
|
29
|
+
getExtraProps={(_, index) => ({ index })}
|
|
30
|
+
/>
|
|
31
|
+
</main>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* With custom fallback for unknown components
|
|
37
|
+
*/
|
|
38
|
+
export function ExamplePageWithFallback({ data }: { data: PageData }) {
|
|
39
|
+
return (
|
|
40
|
+
<main>
|
|
41
|
+
<DynamicZone
|
|
42
|
+
components={data.dynamicZone}
|
|
43
|
+
renderers={pageRenderers}
|
|
44
|
+
getExtraProps={(_, index) => ({ index })}
|
|
45
|
+
fallback={(component) => (
|
|
46
|
+
<div style={{ padding: 16, background: "#fee", borderRadius: 8 }}>
|
|
47
|
+
Unknown component: {component.__component}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
/>
|
|
51
|
+
</main>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Define your component renderers here.
|
|
3
|
+
* Each key matches a __component value from your Strapi types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentRendererMap } from "../types";
|
|
7
|
+
import type { PageComponent, PageExtraProps } from "./types";
|
|
8
|
+
|
|
9
|
+
// Example placeholder components - replace with your actual components
|
|
10
|
+
const Hero = ({ title, subtitle }: { title: string; subtitle: string }) => (
|
|
11
|
+
<section>
|
|
12
|
+
<h1>{title}</h1>
|
|
13
|
+
<p>{subtitle}</p>
|
|
14
|
+
</section>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const CTA = ({
|
|
18
|
+
heading,
|
|
19
|
+
buttonText,
|
|
20
|
+
buttonLink,
|
|
21
|
+
}: {
|
|
22
|
+
heading: string;
|
|
23
|
+
buttonText: string;
|
|
24
|
+
buttonLink: string;
|
|
25
|
+
}) => (
|
|
26
|
+
<section>
|
|
27
|
+
<h2>{heading}</h2>
|
|
28
|
+
<a href={buttonLink}>{buttonText}</a>
|
|
29
|
+
</section>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const FeatureGrid = ({
|
|
33
|
+
title,
|
|
34
|
+
features,
|
|
35
|
+
}: {
|
|
36
|
+
title: string;
|
|
37
|
+
features: Array<{ id: number; title: string; description: string }>;
|
|
38
|
+
}) => (
|
|
39
|
+
<section>
|
|
40
|
+
<h2>{title}</h2>
|
|
41
|
+
<div>
|
|
42
|
+
{features.map((f) => (
|
|
43
|
+
<div key={f.id}>
|
|
44
|
+
<h3>{f.title}</h3>
|
|
45
|
+
<p>{f.description}</p>
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Component renderer map - fully type-safe!
|
|
54
|
+
* TypeScript will error if:
|
|
55
|
+
* - You miss a component type
|
|
56
|
+
* - You use wrong props for a component
|
|
57
|
+
* - You add a component that doesn't exist in the union
|
|
58
|
+
*/
|
|
59
|
+
const pageRenderers: ComponentRendererMap<PageComponent, PageExtraProps> = {
|
|
60
|
+
"hero.hero": ({ title, subtitle, index }) => (
|
|
61
|
+
<Hero title={title} subtitle={subtitle} key={index} />
|
|
62
|
+
),
|
|
63
|
+
|
|
64
|
+
"cta.cta": ({ heading, buttonText, buttonLink }) => (
|
|
65
|
+
<CTA heading={heading} buttonText={buttonText} buttonLink={buttonLink} />
|
|
66
|
+
),
|
|
67
|
+
|
|
68
|
+
"features.grid": ({ title, features }) => (
|
|
69
|
+
<FeatureGrid title={title} features={features} />
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
// Create a pre-bound component for cleaner usage
|
|
73
|
+
import { createDynamicZone } from "../dynamic-zone";
|
|
74
|
+
export const PageDynamicZone = createDynamicZone(pageRenderers);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Define your Strapi component types here.
|
|
3
|
+
* This is your single source of truth for component shapes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Example component types - replace with your actual Strapi types
|
|
7
|
+
export type HeroComponent = {
|
|
8
|
+
__component: 'hero.hero';
|
|
9
|
+
id: number;
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle: string;
|
|
12
|
+
backgroundImage: { url: string };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CTAComponent = {
|
|
16
|
+
__component: 'cta.cta';
|
|
17
|
+
id: number;
|
|
18
|
+
heading: string;
|
|
19
|
+
buttonText: string;
|
|
20
|
+
buttonLink: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type FeatureGridComponent = {
|
|
24
|
+
__component: 'features.grid';
|
|
25
|
+
id: number;
|
|
26
|
+
title: string;
|
|
27
|
+
features: Array<{
|
|
28
|
+
id: number;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
icon: string;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Union of all possible dynamic zone components
|
|
36
|
+
export type PageComponent = HeroComponent | CTAComponent | FeatureGridComponent;
|
|
37
|
+
|
|
38
|
+
// Extra props that get passed to every renderer
|
|
39
|
+
export type PageExtraProps = {
|
|
40
|
+
index: number;
|
|
41
|
+
};
|