@androbinco/library-cli 0.1.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/README.md +86 -37
- package/package.json +11 -16
- package/src/commands/add.js +107 -0
- package/src/commands/list.js +51 -0
- package/src/index.js +46 -15
- 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 +89 -0
- package/src/templates/in-view/examples/in-view-examples.home.tsx +101 -0
- package/src/templates/in-view/examples/in-view-grid-showcase.tsx +41 -0
- package/src/templates/in-view/in-view-animation.tsx +72 -0
- package/src/templates/in-view/in-view-grid.tsx +81 -0
- package/src/templates/in-view/in-view-hidden-text.tsx +45 -0
- package/src/templates/in-view/in-view-stroke-line.tsx +30 -0
- 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-provider.tsx +78 -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/templates/ticker/css-ticker/css-ticker.tsx +61 -0
- package/src/templates/ticker/css-ticker/ticker.keyframes.css +86 -0
- package/src/templates/ticker/examples/ticker-hover-showcase.home.tsx +57 -0
- package/src/templates/ticker/examples/ticker-static-showcase.home.tsx +56 -0
- package/src/templates/ticker/hooks/use-ticker-clones.tsx +70 -0
- package/src/templates/ticker/hooks/use-ticker-incremental.tsx +72 -0
- package/src/templates/ticker/motion-ticker.tsx +93 -0
- package/src/utils/components.js +587 -54
- package/src/utils/files.js +89 -5
- package/src/templates/button/button.tsx +0 -5
- package/src/templates/card/card.tsx +0 -5
- package/src/templates/example/example.tsx +0 -5
- package/src/templates/hero/hero.tsx +0 -5
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strapi Dynamic Zone Type Utilities
|
|
3
|
+
*
|
|
4
|
+
* These types provide type-safe rendering for Strapi Dynamic Zones.
|
|
5
|
+
* Define your component union type with `__component` discriminator,
|
|
6
|
+
* then use these utilities for type-safe prop extraction.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base constraint for Strapi dynamic zone components.
|
|
11
|
+
* All components must have a `__component` string identifier.
|
|
12
|
+
*/
|
|
13
|
+
export type StrapiComponent = {
|
|
14
|
+
__component: string;
|
|
15
|
+
id?: number | string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extracts the props for a specific component type from a union,
|
|
20
|
+
* omitting the `__component` discriminator.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* type HeroProps = ExtractComponentProps<PageComponent, 'hero.hero'>;
|
|
24
|
+
* // Results in: { title: string; subtitle: string; image: ImageType }
|
|
25
|
+
*/
|
|
26
|
+
export type ExtractComponentProps<
|
|
27
|
+
TUnion extends StrapiComponent,
|
|
28
|
+
TComponentKey extends TUnion['__component']
|
|
29
|
+
> = Omit<Extract<TUnion, { __component: TComponentKey }>, '__component'>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a type-safe renderer map from a component union.
|
|
33
|
+
* Each key is a `__component` value, each value is a render function.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const renderers: ComponentRendererMap<PageComponent> = {
|
|
37
|
+
* 'hero.hero': (props) => <Hero {...props} />,
|
|
38
|
+
* 'cta.cta': (props) => <CTA {...props} />,
|
|
39
|
+
* };
|
|
40
|
+
*/
|
|
41
|
+
export type ComponentRendererMap<
|
|
42
|
+
TUnion extends StrapiComponent,
|
|
43
|
+
TExtra = object
|
|
44
|
+
> = {
|
|
45
|
+
[K in TUnion['__component']]: (
|
|
46
|
+
props: ExtractComponentProps<TUnion, K> & TExtra
|
|
47
|
+
) => React.ReactElement | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Utility type to get all component keys from a union.
|
|
52
|
+
*/
|
|
53
|
+
export type ComponentKeys<TUnion extends StrapiComponent> =
|
|
54
|
+
TUnion['__component'];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Props for the DynamicZone component (renders multiple components).
|
|
58
|
+
*/
|
|
59
|
+
export type DynamicZoneProps<
|
|
60
|
+
TUnion extends StrapiComponent,
|
|
61
|
+
TExtra = object
|
|
62
|
+
> = {
|
|
63
|
+
/** Array of components from Strapi Dynamic Zone */
|
|
64
|
+
components: TUnion[];
|
|
65
|
+
/** Map of component renderers */
|
|
66
|
+
renderers: ComponentRendererMap<TUnion, TExtra>;
|
|
67
|
+
/** Function to generate extra props per component (receives component and index) */
|
|
68
|
+
getExtraProps?: (component: TUnion, index: number) => TExtra;
|
|
69
|
+
/** Optional fallback for unknown components */
|
|
70
|
+
fallback?: (component: TUnion) => React.ReactElement | null;
|
|
71
|
+
/** Optional key generator (defaults to component.id or index) */
|
|
72
|
+
getKey?: (component: TUnion, index: number) => string | number;
|
|
73
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/common/utils/classname-builder';
|
|
5
|
+
|
|
6
|
+
import { useTickerClones } from '../hooks/use-ticker-clones';
|
|
7
|
+
import './ticker.keyframes.css';
|
|
8
|
+
|
|
9
|
+
export const TickerStatic = ({
|
|
10
|
+
direction = 'right',
|
|
11
|
+
children,
|
|
12
|
+
speed = 50,
|
|
13
|
+
}: {
|
|
14
|
+
direction?: 'left' | 'right';
|
|
15
|
+
speed?: number;
|
|
16
|
+
delta?: number;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}) => {
|
|
19
|
+
const tickerClone = useRef<HTMLDivElement>(null);
|
|
20
|
+
const tickerCard = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
const { clonesNeeded } = useTickerClones({
|
|
23
|
+
tickerCard,
|
|
24
|
+
tickerClone,
|
|
25
|
+
direction,
|
|
26
|
+
onCloneWrapper(clone) {
|
|
27
|
+
clone.classList.remove('animate-base-ticker');
|
|
28
|
+
clone.classList.remove('animate-base-ticker-left');
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const baseTickerClass = cn(
|
|
33
|
+
'flex w-max flex-row',
|
|
34
|
+
direction === 'right' ? 'animate-base-ticker' : 'animate-base-ticker-left',
|
|
35
|
+
);
|
|
36
|
+
const clonedTickerClass = cn(
|
|
37
|
+
'absolute top-0 h-full w-max',
|
|
38
|
+
direction === 'right' ? 'animate-cloned-ticker' : 'animate-cloned-ticker-left',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="ticker group w-full overflow-hidden" role="group">
|
|
43
|
+
<div className="relative w-full">
|
|
44
|
+
<div
|
|
45
|
+
ref={tickerCard}
|
|
46
|
+
className={cn(baseTickerClass)}
|
|
47
|
+
style={{ '--ticker-speed': `${speed}s` } as React.CSSProperties}
|
|
48
|
+
>
|
|
49
|
+
{Array.from({ length: clonesNeeded }).map((_, index) => {
|
|
50
|
+
return <Fragment key={index}>{children}</Fragment>;
|
|
51
|
+
})}
|
|
52
|
+
</div>
|
|
53
|
+
<div
|
|
54
|
+
ref={tickerClone}
|
|
55
|
+
className={cn(clonedTickerClass)}
|
|
56
|
+
style={{ '--ticker-speed': `${speed}s` } as React.CSSProperties}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|