@agility/create-next-app 1.0.0-beta.2
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/.claude/settings.json +7 -0
- package/.claude/settings.local.json +24 -0
- package/FEATURE_ROADMAP.md +343 -0
- package/README.md +205 -0
- package/TESTING.md +131 -0
- package/bin/create-agility-app.js +48 -0
- package/dist/agility/api-keys/generateApiKeys.d.ts +9 -0
- package/dist/agility/api-keys/generateApiKeys.d.ts.map +1 -0
- package/dist/agility/api-keys/generateApiKeys.js +99 -0
- package/dist/agility/api-keys/generateApiKeys.js.map +1 -0
- package/dist/agility/api-keys/getApiKeys.d.ts +9 -0
- package/dist/agility/api-keys/getApiKeys.d.ts.map +1 -0
- package/dist/agility/api-keys/getApiKeys.js +14 -0
- package/dist/agility/api-keys/getApiKeys.js.map +1 -0
- package/dist/agility/index.d.ts +3 -0
- package/dist/agility/index.d.ts.map +1 -0
- package/dist/agility/index.js +8 -0
- package/dist/agility/index.js.map +1 -0
- package/dist/agility/instance/createNewInstance.d.ts +8 -0
- package/dist/agility/instance/createNewInstance.d.ts.map +1 -0
- package/dist/agility/instance/createNewInstance.js +65 -0
- package/dist/agility/instance/createNewInstance.js.map +1 -0
- package/dist/agility/instance/getAvailableInstances.d.ts +8 -0
- package/dist/agility/instance/getAvailableInstances.d.ts.map +1 -0
- package/dist/agility/instance/getAvailableInstances.js +43 -0
- package/dist/agility/instance/getAvailableInstances.js.map +1 -0
- package/dist/agility/instance/manageInstance.d.ts +9 -0
- package/dist/agility/instance/manageInstance.d.ts.map +1 -0
- package/dist/agility/instance/manageInstance.js +82 -0
- package/dist/agility/instance/manageInstance.js.map +1 -0
- package/dist/agility/utils/getMgmtAPIUrl.d.ts +20 -0
- package/dist/agility/utils/getMgmtAPIUrl.d.ts.map +1 -0
- package/dist/agility/utils/getMgmtAPIUrl.js +61 -0
- package/dist/agility/utils/getMgmtAPIUrl.js.map +1 -0
- package/dist/auth/api-key/authenticateWithApiKey.d.ts +6 -0
- package/dist/auth/api-key/authenticateWithApiKey.d.ts.map +1 -0
- package/dist/auth/api-key/authenticateWithApiKey.js +28 -0
- package/dist/auth/api-key/authenticateWithApiKey.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth/authenticate.d.ts +6 -0
- package/dist/auth/oauth/authenticate.d.ts.map +1 -0
- package/dist/auth/oauth/authenticate.js +162 -0
- package/dist/auth/oauth/authenticate.js.map +1 -0
- package/dist/auth/oauth/constants.d.ts +5 -0
- package/dist/auth/oauth/constants.d.ts.map +1 -0
- package/dist/auth/oauth/constants.js +9 -0
- package/dist/auth/oauth/constants.js.map +1 -0
- package/dist/auth/oauth/exchangeCodeForToken.d.ts +7 -0
- package/dist/auth/oauth/exchangeCodeForToken.d.ts.map +1 -0
- package/dist/auth/oauth/exchangeCodeForToken.js +39 -0
- package/dist/auth/oauth/exchangeCodeForToken.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +290 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/promptForMissingOptions.d.ts +8 -0
- package/dist/cli/promptForMissingOptions.d.ts.map +1 -0
- package/dist/cli/promptForMissingOptions.js +92 -0
- package/dist/cli/promptForMissingOptions.js.map +1 -0
- package/dist/config/env/createEnvFile.d.ts +6 -0
- package/dist/config/env/createEnvFile.d.ts.map +1 -0
- package/dist/config/env/createEnvFile.js +31 -0
- package/dist/config/env/createEnvFile.js.map +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/mcp/createMcpConfig.d.ts +5 -0
- package/dist/config/mcp/createMcpConfig.d.ts.map +1 -0
- package/dist/config/mcp/createMcpConfig.js +32 -0
- package/dist/config/mcp/createMcpConfig.js.map +1 -0
- package/dist/config/packages/installAgilityPackages.d.ts +6 -0
- package/dist/config/packages/installAgilityPackages.d.ts.map +1 -0
- package/dist/config/packages/installAgilityPackages.js +61 -0
- package/dist/config/packages/installAgilityPackages.js.map +1 -0
- package/dist/config/setupProject.d.ts +8 -0
- package/dist/config/setupProject.d.ts.map +1 -0
- package/dist/config/setupProject.js +32 -0
- package/dist/config/setupProject.js.map +1 -0
- package/dist/create-next-app/createNextApp.d.ts +9 -0
- package/dist/create-next-app/createNextApp.d.ts.map +1 -0
- package/dist/create-next-app/createNextApp.js +83 -0
- package/dist/create-next-app/createNextApp.js.map +1 -0
- package/dist/create-next-app/index.d.ts +3 -0
- package/dist/create-next-app/index.d.ts.map +1 -0
- package/dist/create-next-app/index.js +8 -0
- package/dist/create-next-app/index.js.map +1 -0
- package/dist/scaffold/components/createPageComponents.d.ts +6 -0
- package/dist/scaffold/components/createPageComponents.d.ts.map +1 -0
- package/dist/scaffold/components/createPageComponents.js +62 -0
- package/dist/scaffold/components/createPageComponents.js.map +1 -0
- package/dist/scaffold/containers/createContainers.d.ts +6 -0
- package/dist/scaffold/containers/createContainers.d.ts.map +1 -0
- package/dist/scaffold/containers/createContainers.js +48 -0
- package/dist/scaffold/containers/createContainers.js.map +1 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +6 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/instance/createBlankInstance.d.ts +8 -0
- package/dist/scaffold/instance/createBlankInstance.d.ts.map +1 -0
- package/dist/scaffold/instance/createBlankInstance.js +51 -0
- package/dist/scaffold/instance/createBlankInstance.js.map +1 -0
- package/dist/scaffold/models/createContentModels.d.ts +6 -0
- package/dist/scaffold/models/createContentModels.d.ts.map +1 -0
- package/dist/scaffold/models/createContentModels.js +70 -0
- package/dist/scaffold/models/createContentModels.js.map +1 -0
- package/dist/templates/copyDirectory.d.ts +5 -0
- package/dist/templates/copyDirectory.d.ts.map +1 -0
- package/dist/templates/copyDirectory.js +28 -0
- package/dist/templates/copyDirectory.js.map +1 -0
- package/dist/templates/copyTemplates.d.ts +8 -0
- package/dist/templates/copyTemplates.d.ts.map +1 -0
- package/dist/templates/copyTemplates.js +58 -0
- package/dist/templates/copyTemplates.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +6 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +50 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +71 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/validation.d.ts +45 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +180 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +45 -0
- package/src/agility/api-keys/generateApiKeys.ts +100 -0
- package/src/agility/api-keys/getApiKeys.ts +13 -0
- package/src/agility/index.ts +3 -0
- package/src/agility/instance/createNewInstance.ts +67 -0
- package/src/agility/instance/getAvailableInstances.ts +49 -0
- package/src/agility/instance/manageInstance.ts +90 -0
- package/src/agility/utils/getMgmtAPIUrl.ts +68 -0
- package/src/auth/api-key/authenticateWithApiKey.ts +24 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/oauth/authenticate.ts +165 -0
- package/src/auth/oauth/constants.ts +6 -0
- package/src/auth/oauth/exchangeCodeForToken.ts +43 -0
- package/src/cli/index.ts +281 -0
- package/src/cli/promptForMissingOptions.ts +104 -0
- package/src/config/env/createEnvFile.ts +30 -0
- package/src/config/index.ts +2 -0
- package/src/config/mcp/createMcpConfig.ts +30 -0
- package/src/config/packages/installAgilityPackages.ts +63 -0
- package/src/config/setupProject.ts +31 -0
- package/src/create-next-app/createNextApp.ts +75 -0
- package/src/create-next-app/index.ts +3 -0
- package/src/scaffold/components/createPageComponents.ts +74 -0
- package/src/scaffold/containers/createContainers.ts +55 -0
- package/src/scaffold/index.ts +2 -0
- package/src/scaffold/instance/createBlankInstance.ts +55 -0
- package/src/scaffold/models/createContentModels.ts +83 -0
- package/src/templates/copyDirectory.ts +24 -0
- package/src/templates/copyTemplates.ts +57 -0
- package/src/templates/index.ts +2 -0
- package/src/types/index.ts +55 -0
- package/src/utils/git.ts +74 -0
- package/src/utils/validation.ts +184 -0
- package/templates/.claude/QUICK-START.md +230 -0
- package/templates/.claude/README.md +32 -0
- package/templates/.claude/settings.json +8 -0
- package/templates/BLANK-INSTANCE-SETUP.md +375 -0
- package/templates/DEVELOPMENT.md +160 -0
- package/templates/EXAMPLE-PROMPTS.md +643 -0
- package/templates/PROMPTS.md +410 -0
- package/templates/README.md +281 -0
- package/templates/agents.md +429 -0
- package/templates/app/[locale]/[...slug]/error.tsx +17 -0
- package/templates/app/[locale]/[...slug]/not-found.tsx +9 -0
- package/templates/app/[locale]/[...slug]/page.tsx +102 -0
- package/templates/app/[locale]/layout.tsx +22 -0
- package/templates/app/[locale]/page.tsx +12 -0
- package/templates/app/api/dynamic-redirect/route.ts +24 -0
- package/templates/app/api/preview/exit/route.ts +34 -0
- package/templates/app/api/preview/route.ts +63 -0
- package/templates/app/api/revalidate/route.ts +118 -0
- package/templates/components/agility-components/RichTextArea.tsx +66 -0
- package/templates/components/agility-components/index.ts +30 -0
- package/templates/components/agility-pages/MainTemplate.tsx +36 -0
- package/templates/components/agility-pages/index.ts +11 -0
- package/templates/docs/01-agility-cms-overview.md +139 -0
- package/templates/docs/02-page-routing.md +251 -0
- package/templates/docs/03-creating-components.md +462 -0
- package/templates/docs/04-data-fetching.md +484 -0
- package/templates/docs/05-containers-and-lists.md +596 -0
- package/templates/docs/06-localization.md +561 -0
- package/templates/docs/07-caching-strategies.md +410 -0
- package/templates/docs/08-common-components.md +756 -0
- package/templates/docs/09-whats-included.md +279 -0
- package/templates/docs/10-mcp-server-setup.md +153 -0
- package/templates/docs/11-linked-nested-content.md +611 -0
- package/templates/docs/README.md +164 -0
- package/templates/lib/cms/getAgilityContext.ts +28 -0
- package/templates/lib/cms/getAgilityPage.ts +51 -0
- package/templates/lib/cms/getAgilitySDK.ts +22 -0
- package/templates/lib/cms/getContentItem.ts +20 -0
- package/templates/lib/cms/getContentList.ts +19 -0
- package/templates/lib/cms/getRedirections.ts +85 -0
- package/templates/lib/cms/getSitemapFlat.ts +19 -0
- package/templates/lib/cms/getSitemapNested.ts +19 -0
- package/templates/lib/env.ts +99 -0
- package/templates/lib/i18n/config.ts +28 -0
- package/templates/proxy.ts +101 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
# Common Components and Patterns
|
|
2
|
+
|
|
3
|
+
This document provides ready-to-use component examples for common Agility CMS patterns.
|
|
4
|
+
|
|
5
|
+
## ⚠️ CRITICAL: Image Handling with AgilityPic
|
|
6
|
+
|
|
7
|
+
**ALL images from Agility CMS MUST be rendered using the `<AgilityPic>` component!**
|
|
8
|
+
|
|
9
|
+
### Why AgilityPic?
|
|
10
|
+
|
|
11
|
+
The `<AgilityPic>` component provides:
|
|
12
|
+
- **Automatic optimization**: Serves appropriately sized images
|
|
13
|
+
- **Responsive images**: Built-in support for different screen sizes
|
|
14
|
+
- **CMS integration**: Maintains compatibility with Agility's inline editing
|
|
15
|
+
- **Performance**: Lazy loading and modern image formats
|
|
16
|
+
|
|
17
|
+
### Basic Usage
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { AgilityPic } from "@agility/nextjs";
|
|
21
|
+
import type { ImageField } from "@agility/nextjs";
|
|
22
|
+
|
|
23
|
+
interface MyComponentProps {
|
|
24
|
+
module: {
|
|
25
|
+
fields: {
|
|
26
|
+
image: ImageField; // Always use ImageField type
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function MyComponent({ module }: MyComponentProps) {
|
|
32
|
+
return (
|
|
33
|
+
<AgilityPic
|
|
34
|
+
image={module.fields.image}
|
|
35
|
+
fallbackWidth={600}
|
|
36
|
+
className="w-full h-auto"
|
|
37
|
+
data-agility-field="image"
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Key Props
|
|
44
|
+
|
|
45
|
+
- **`image`** (required): The `ImageField` object from Agility CMS
|
|
46
|
+
- **`fallbackWidth`**: Width in pixels for fallback (default: 640)
|
|
47
|
+
- **`alt`**: Alt text override (uses CMS alt text by default)
|
|
48
|
+
- **`className`**: CSS classes for styling
|
|
49
|
+
- **`priority`**: Set `true` for above-the-fold images (loads eagerly)
|
|
50
|
+
- **`sources`**: Array of responsive sources with media queries
|
|
51
|
+
- **`data-agility-field`**: Field name for CMS inline editing
|
|
52
|
+
|
|
53
|
+
### Responsive Images Example
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<AgilityPic
|
|
57
|
+
image={imageField}
|
|
58
|
+
fallbackWidth={800}
|
|
59
|
+
sources={[
|
|
60
|
+
{ media: "(max-width: 639px)", width: 640 }, // Mobile
|
|
61
|
+
{ media: "(max-width: 767px)", width: 800 }, // Tablet
|
|
62
|
+
{ media: "(max-width: 1023px)", width: 1200 }, // Desktop
|
|
63
|
+
]}
|
|
64
|
+
className="w-full h-full object-cover"
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### ❌ WRONG - Don't Do This
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// ❌ NEVER use Next.js Image for Agility images
|
|
72
|
+
import Image from "next/image";
|
|
73
|
+
<Image src={imageField.url} alt="..." />
|
|
74
|
+
|
|
75
|
+
// ❌ NEVER use plain img tags for Agility images
|
|
76
|
+
<img src={imageField.url} alt="..." />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Header Component
|
|
82
|
+
|
|
83
|
+
A reusable header with navigation from Agility CMS:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// src/components/Header.tsx
|
|
87
|
+
|
|
88
|
+
import { getSitemapNested } from "@/lib/cms/getSitemapNested";
|
|
89
|
+
import LocaleSwitcher from "./LocaleSwitcher";
|
|
90
|
+
|
|
91
|
+
interface HeaderProps {
|
|
92
|
+
locale: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default async function Header({ locale }: HeaderProps) {
|
|
96
|
+
const sitemap = await getSitemapNested({ locale });
|
|
97
|
+
|
|
98
|
+
// Get top-level nav items (children of home)
|
|
99
|
+
const navItems = sitemap[0]?.children || [];
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<header className="bg-white shadow">
|
|
103
|
+
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
|
|
104
|
+
<a href="/" className="text-2xl font-bold">
|
|
105
|
+
Your Site
|
|
106
|
+
</a>
|
|
107
|
+
|
|
108
|
+
<nav className="flex gap-6">
|
|
109
|
+
{navItems.map((item: any) => (
|
|
110
|
+
<a
|
|
111
|
+
key={item.pageID}
|
|
112
|
+
href={item.path}
|
|
113
|
+
className="hover:text-blue-600"
|
|
114
|
+
>
|
|
115
|
+
{item.menuText || item.title}
|
|
116
|
+
</a>
|
|
117
|
+
))}
|
|
118
|
+
</nav>
|
|
119
|
+
|
|
120
|
+
<LocaleSwitcher currentLocale={locale} />
|
|
121
|
+
</div>
|
|
122
|
+
</header>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Footer Component
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// src/components/Footer.tsx
|
|
131
|
+
|
|
132
|
+
import { getContentItem } from "@/lib/cms/getContentItem";
|
|
133
|
+
|
|
134
|
+
interface FooterProps {
|
|
135
|
+
locale: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default async function Footer({ locale }: FooterProps) {
|
|
139
|
+
const footer = await getContentItem({
|
|
140
|
+
referenceName: "footerSettings",
|
|
141
|
+
locale,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!footer) return null;
|
|
145
|
+
|
|
146
|
+
const { copyrightText, socialLinks, columns } = footer.fields;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<footer className="bg-gray-900 text-white py-12">
|
|
150
|
+
<div className="container mx-auto px-4">
|
|
151
|
+
<div className="grid md:grid-cols-4 gap-8">
|
|
152
|
+
{columns?.map((column: any, index: number) => (
|
|
153
|
+
<div key={index}>
|
|
154
|
+
<h3 className="font-bold mb-4">{column.title}</h3>
|
|
155
|
+
<ul className="space-y-2">
|
|
156
|
+
{column.links?.map((link: any, i: number) => (
|
|
157
|
+
<li key={i}>
|
|
158
|
+
<a href={link.href} className="hover:text-gray-300">
|
|
159
|
+
{link.text}
|
|
160
|
+
</a>
|
|
161
|
+
</li>
|
|
162
|
+
))}
|
|
163
|
+
</ul>
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="mt-8 pt-8 border-t border-gray-800 flex justify-between items-center">
|
|
169
|
+
<p>{copyrightText}</p>
|
|
170
|
+
|
|
171
|
+
{socialLinks && (
|
|
172
|
+
<div className="flex gap-4">
|
|
173
|
+
{socialLinks.map((social: any, i: number) => (
|
|
174
|
+
<a
|
|
175
|
+
key={i}
|
|
176
|
+
href={social.href}
|
|
177
|
+
target="_blank"
|
|
178
|
+
rel="noopener noreferrer"
|
|
179
|
+
className="hover:text-gray-300"
|
|
180
|
+
>
|
|
181
|
+
{social.text}
|
|
182
|
+
</a>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</footer>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Hero Component
|
|
194
|
+
|
|
195
|
+
**IMPORTANT**: Always use `<AgilityPic>` for images from Agility CMS!
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// src/components/agility-components/Hero.tsx
|
|
199
|
+
|
|
200
|
+
import { AgilityPic } from "@agility/nextjs";
|
|
201
|
+
import type { ImageField, URLField } from "@agility/nextjs";
|
|
202
|
+
|
|
203
|
+
interface HeroProps {
|
|
204
|
+
module: {
|
|
205
|
+
fields: {
|
|
206
|
+
title: string;
|
|
207
|
+
subtitle: string;
|
|
208
|
+
ctaButton?: URLField;
|
|
209
|
+
backgroundImage?: ImageField; // Use ImageField type
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
locale: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default function Hero({ module, locale }: HeroProps) {
|
|
216
|
+
const { title, subtitle, ctaButton, backgroundImage } = module.fields;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<section className="relative h-screen flex items-center justify-center text-white">
|
|
220
|
+
{backgroundImage && (
|
|
221
|
+
<div className="absolute inset-0 z-0">
|
|
222
|
+
{/* ✅ CORRECT: Use AgilityPic for Agility CMS images */}
|
|
223
|
+
<AgilityPic
|
|
224
|
+
image={backgroundImage}
|
|
225
|
+
fallbackWidth={1920}
|
|
226
|
+
className="w-full h-full object-cover"
|
|
227
|
+
priority={true} // Above-the-fold image
|
|
228
|
+
data-agility-field="backgroundImage"
|
|
229
|
+
/>
|
|
230
|
+
<div className="absolute inset-0 bg-black bg-opacity-50" />
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
<div className="relative z-10 text-center max-w-4xl px-4">
|
|
235
|
+
<h1 className="text-6xl font-bold mb-6" data-agility-field="title">
|
|
236
|
+
{title}
|
|
237
|
+
</h1>
|
|
238
|
+
<p className="text-2xl mb-8" data-agility-field="subtitle">
|
|
239
|
+
{subtitle}
|
|
240
|
+
</p>
|
|
241
|
+
|
|
242
|
+
{ctaButton && (
|
|
243
|
+
<a
|
|
244
|
+
href={ctaButton.href}
|
|
245
|
+
target={ctaButton.target}
|
|
246
|
+
className="inline-block bg-blue-600 hover:bg-blue-700 px-8 py-4 rounded-lg text-lg font-semibold transition"
|
|
247
|
+
data-agility-field="ctaButton"
|
|
248
|
+
>
|
|
249
|
+
{ctaButton.text}
|
|
250
|
+
</a>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Blog Post Listing
|
|
259
|
+
|
|
260
|
+
**IMPORTANT**: Always use `<AgilityPic>` for post images with responsive sources!
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
// src/components/agility-components/PostListing.tsx
|
|
264
|
+
|
|
265
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
266
|
+
import { AgilityPic } from "@agility/nextjs";
|
|
267
|
+
import type { ImageField } from "@agility/nextjs";
|
|
268
|
+
import Link from "next/link";
|
|
269
|
+
|
|
270
|
+
interface Post {
|
|
271
|
+
contentID: number;
|
|
272
|
+
fields: {
|
|
273
|
+
title: string;
|
|
274
|
+
slug: string;
|
|
275
|
+
date: string;
|
|
276
|
+
excerpt: string;
|
|
277
|
+
image?: ImageField; // Use ImageField type
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
interface PostListingProps {
|
|
282
|
+
module: {
|
|
283
|
+
fields: {
|
|
284
|
+
title: string;
|
|
285
|
+
numberOfPosts: number;
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
locale: string;
|
|
289
|
+
searchParams?: {
|
|
290
|
+
page?: string;
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default async function PostListing({
|
|
295
|
+
module,
|
|
296
|
+
locale,
|
|
297
|
+
searchParams,
|
|
298
|
+
}: PostListingProps) {
|
|
299
|
+
const { title, numberOfPosts } = module.fields;
|
|
300
|
+
const page = parseInt(searchParams?.page || "1");
|
|
301
|
+
const perPage = numberOfPosts || 9;
|
|
302
|
+
|
|
303
|
+
const posts = await getContentList<Post>({
|
|
304
|
+
referenceName: "posts",
|
|
305
|
+
locale,
|
|
306
|
+
take: perPage,
|
|
307
|
+
skip: (page - 1) * perPage,
|
|
308
|
+
sort: "fields.date desc",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<section className="py-16">
|
|
313
|
+
<div className="container mx-auto px-4">
|
|
314
|
+
<h2 className="text-4xl font-bold mb-12 text-center" data-agility-field="title">
|
|
315
|
+
{title}
|
|
316
|
+
</h2>
|
|
317
|
+
|
|
318
|
+
<div className="grid md:grid-cols-3 gap-8">
|
|
319
|
+
{posts.map((post) => (
|
|
320
|
+
<article
|
|
321
|
+
key={post.contentID}
|
|
322
|
+
className="border rounded-lg overflow-hidden hover:shadow-lg transition"
|
|
323
|
+
>
|
|
324
|
+
{/* ✅ CORRECT: Use AgilityPic with responsive sources */}
|
|
325
|
+
{post.fields.image && (
|
|
326
|
+
<AgilityPic
|
|
327
|
+
image={post.fields.image}
|
|
328
|
+
fallbackWidth={400}
|
|
329
|
+
className="w-full h-48 object-cover"
|
|
330
|
+
sources={[
|
|
331
|
+
{ media: "(max-width: 639px)", width: 640 },
|
|
332
|
+
{ media: "(max-width: 1023px)", width: 800 },
|
|
333
|
+
]}
|
|
334
|
+
/>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
<div className="p-6">
|
|
338
|
+
<time className="text-sm text-gray-600">
|
|
339
|
+
{new Date(post.fields.date).toLocaleDateString(locale)}
|
|
340
|
+
</time>
|
|
341
|
+
|
|
342
|
+
<h3 className="text-xl font-semibold mt-2 mb-3">
|
|
343
|
+
{post.fields.title}
|
|
344
|
+
</h3>
|
|
345
|
+
|
|
346
|
+
<p className="text-gray-600 mb-4">{post.fields.excerpt}</p>
|
|
347
|
+
|
|
348
|
+
<Link
|
|
349
|
+
href={`/blog/${post.fields.slug}`}
|
|
350
|
+
className="text-blue-600 hover:underline"
|
|
351
|
+
>
|
|
352
|
+
Read More →
|
|
353
|
+
</Link>
|
|
354
|
+
</div>
|
|
355
|
+
</article>
|
|
356
|
+
))}
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{/* Pagination */}
|
|
360
|
+
<nav className="flex justify-center gap-2 mt-12">
|
|
361
|
+
{page > 1 && (
|
|
362
|
+
<a
|
|
363
|
+
href={`?page=${page - 1}`}
|
|
364
|
+
className="px-4 py-2 border rounded hover:bg-gray-100"
|
|
365
|
+
>
|
|
366
|
+
Previous
|
|
367
|
+
</a>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
<span className="px-4 py-2">Page {page}</span>
|
|
371
|
+
|
|
372
|
+
{posts.length === perPage && (
|
|
373
|
+
<a
|
|
374
|
+
href={`?page=${page + 1}`}
|
|
375
|
+
className="px-4 py-2 border rounded hover:bg-gray-100"
|
|
376
|
+
>
|
|
377
|
+
Next
|
|
378
|
+
</a>
|
|
379
|
+
)}
|
|
380
|
+
</nav>
|
|
381
|
+
</div>
|
|
382
|
+
</section>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Contact Form
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// src/components/agility-components/ContactForm.server.tsx
|
|
391
|
+
|
|
392
|
+
import ContactFormClient from "./ContactForm.client";
|
|
393
|
+
|
|
394
|
+
interface ContactFormProps {
|
|
395
|
+
module: {
|
|
396
|
+
fields: {
|
|
397
|
+
title: string;
|
|
398
|
+
subtitle: string;
|
|
399
|
+
submitButtonText: string;
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default function ContactForm({ module }: ContactFormProps) {
|
|
405
|
+
const { title, subtitle, submitButtonText } = module.fields;
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<section className="py-16 bg-gray-50">
|
|
409
|
+
<div className="container mx-auto px-4 max-w-2xl">
|
|
410
|
+
<div className="text-center mb-8">
|
|
411
|
+
<h2 className="text-4xl font-bold mb-4">{title}</h2>
|
|
412
|
+
<p className="text-xl text-gray-600">{subtitle}</p>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<ContactFormClient submitButtonText={submitButtonText} />
|
|
416
|
+
</div>
|
|
417
|
+
</section>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
// src/components/agility-components/ContactForm.client.tsx
|
|
424
|
+
"use client";
|
|
425
|
+
|
|
426
|
+
import { useState } from "react";
|
|
427
|
+
|
|
428
|
+
export default function ContactFormClient({ submitButtonText }: any) {
|
|
429
|
+
const [formData, setFormData] = useState({
|
|
430
|
+
name: "",
|
|
431
|
+
email: "",
|
|
432
|
+
message: "",
|
|
433
|
+
});
|
|
434
|
+
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
|
435
|
+
|
|
436
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
setStatus("loading");
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const response = await fetch("/api/contact", {
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers: { "Content-Type": "application/json" },
|
|
444
|
+
body: JSON.stringify(formData),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (response.ok) {
|
|
448
|
+
setStatus("success");
|
|
449
|
+
setFormData({ name: "", email: "", message: "" });
|
|
450
|
+
} else {
|
|
451
|
+
setStatus("error");
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
setStatus("error");
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
460
|
+
<div>
|
|
461
|
+
<label htmlFor="name" className="block font-semibold mb-2">
|
|
462
|
+
Name
|
|
463
|
+
</label>
|
|
464
|
+
<input
|
|
465
|
+
id="name"
|
|
466
|
+
type="text"
|
|
467
|
+
value={formData.name}
|
|
468
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
469
|
+
required
|
|
470
|
+
className="w-full p-3 border rounded"
|
|
471
|
+
/>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<div>
|
|
475
|
+
<label htmlFor="email" className="block font-semibold mb-2">
|
|
476
|
+
Email
|
|
477
|
+
</label>
|
|
478
|
+
<input
|
|
479
|
+
id="email"
|
|
480
|
+
type="email"
|
|
481
|
+
value={formData.email}
|
|
482
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
483
|
+
required
|
|
484
|
+
className="w-full p-3 border rounded"
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<div>
|
|
489
|
+
<label htmlFor="message" className="block font-semibold mb-2">
|
|
490
|
+
Message
|
|
491
|
+
</label>
|
|
492
|
+
<textarea
|
|
493
|
+
id="message"
|
|
494
|
+
value={formData.message}
|
|
495
|
+
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
|
496
|
+
required
|
|
497
|
+
rows={5}
|
|
498
|
+
className="w-full p-3 border rounded"
|
|
499
|
+
/>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<button
|
|
503
|
+
type="submit"
|
|
504
|
+
disabled={status === "loading"}
|
|
505
|
+
className="w-full bg-blue-600 text-white py-3 rounded font-semibold hover:bg-blue-700 disabled:bg-gray-400"
|
|
506
|
+
>
|
|
507
|
+
{status === "loading" ? "Sending..." : submitButtonText}
|
|
508
|
+
</button>
|
|
509
|
+
|
|
510
|
+
{status === "success" && (
|
|
511
|
+
<p className="text-green-600 text-center">Message sent successfully!</p>
|
|
512
|
+
)}
|
|
513
|
+
{status === "error" && (
|
|
514
|
+
<p className="text-red-600 text-center">Error sending message. Please try again.</p>
|
|
515
|
+
)}
|
|
516
|
+
</form>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## FAQ Accordion
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
// src/components/agility-components/FAQ.server.tsx
|
|
525
|
+
|
|
526
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
527
|
+
import FAQClient from "./FAQ.client";
|
|
528
|
+
|
|
529
|
+
export default async function FAQ({ module, locale }: any) {
|
|
530
|
+
const { title } = module.fields;
|
|
531
|
+
|
|
532
|
+
const faqs = await getContentList({
|
|
533
|
+
referenceName: "faqs",
|
|
534
|
+
locale,
|
|
535
|
+
sort: "fields.order asc",
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<section className="py-16">
|
|
540
|
+
<div className="container mx-auto px-4 max-w-4xl">
|
|
541
|
+
<h2 className="text-4xl font-bold mb-12 text-center">{title}</h2>
|
|
542
|
+
<FAQClient faqs={faqs} />
|
|
543
|
+
</div>
|
|
544
|
+
</section>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
```tsx
|
|
550
|
+
// src/components/agility-components/FAQ.client.tsx
|
|
551
|
+
"use client";
|
|
552
|
+
|
|
553
|
+
import { useState } from "react";
|
|
554
|
+
|
|
555
|
+
export default function FAQClient({ faqs }: any) {
|
|
556
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
557
|
+
|
|
558
|
+
return (
|
|
559
|
+
<div className="space-y-4">
|
|
560
|
+
{faqs.map((faq: any, index: number) => (
|
|
561
|
+
<div key={faq.contentID} className="border rounded-lg">
|
|
562
|
+
<button
|
|
563
|
+
onClick={() => setOpenIndex(openIndex === index ? null : index)}
|
|
564
|
+
className="w-full p-6 text-left flex justify-between items-center hover:bg-gray-50"
|
|
565
|
+
>
|
|
566
|
+
<span className="font-semibold text-lg">{faq.fields.question}</span>
|
|
567
|
+
<span className="text-2xl">{openIndex === index ? "−" : "+"}</span>
|
|
568
|
+
</button>
|
|
569
|
+
|
|
570
|
+
{openIndex === index && (
|
|
571
|
+
<div
|
|
572
|
+
className="p-6 pt-0 text-gray-600"
|
|
573
|
+
dangerouslySetInnerHTML={{ __html: faq.fields.answer }}
|
|
574
|
+
/>
|
|
575
|
+
)}
|
|
576
|
+
</div>
|
|
577
|
+
))}
|
|
578
|
+
</div>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## Image Gallery
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
// src/components/agility-components/ImageGallery.server.tsx
|
|
587
|
+
|
|
588
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
589
|
+
import ImageGalleryClient from "./ImageGallery.client";
|
|
590
|
+
|
|
591
|
+
export default async function ImageGallery({ module, locale }: any) {
|
|
592
|
+
const { title, gallery } = module.fields;
|
|
593
|
+
|
|
594
|
+
const images = await getContentList({
|
|
595
|
+
referenceName: gallery.referenceName,
|
|
596
|
+
locale,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<section className="py-16">
|
|
601
|
+
<div className="container mx-auto px-4">
|
|
602
|
+
<h2 className="text-4xl font-bold mb-12 text-center">{title}</h2>
|
|
603
|
+
<ImageGalleryClient images={images} />
|
|
604
|
+
</div>
|
|
605
|
+
</section>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
// src/components/agility-components/ImageGallery.client.tsx
|
|
612
|
+
"use client";
|
|
613
|
+
|
|
614
|
+
import { useState } from "react";
|
|
615
|
+
|
|
616
|
+
export default function ImageGalleryClient({ images }: any) {
|
|
617
|
+
const [selectedImage, setSelectedImage] = useState<any>(null);
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<>
|
|
621
|
+
<div className="grid md:grid-cols-4 gap-4">
|
|
622
|
+
{images.map((image: any) => (
|
|
623
|
+
<button
|
|
624
|
+
key={image.contentID}
|
|
625
|
+
onClick={() => setSelectedImage(image)}
|
|
626
|
+
className="aspect-square overflow-hidden rounded-lg hover:opacity-75 transition"
|
|
627
|
+
>
|
|
628
|
+
<img
|
|
629
|
+
src={image.fields.image.url}
|
|
630
|
+
alt={image.fields.image.label}
|
|
631
|
+
className="w-full h-full object-cover"
|
|
632
|
+
/>
|
|
633
|
+
</button>
|
|
634
|
+
))}
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
{/* Lightbox */}
|
|
638
|
+
{selectedImage && (
|
|
639
|
+
<div
|
|
640
|
+
className="fixed inset-0 z-50 bg-black bg-opacity-90 flex items-center justify-center p-4"
|
|
641
|
+
onClick={() => setSelectedImage(null)}
|
|
642
|
+
>
|
|
643
|
+
<button
|
|
644
|
+
className="absolute top-4 right-4 text-white text-4xl"
|
|
645
|
+
onClick={() => setSelectedImage(null)}
|
|
646
|
+
>
|
|
647
|
+
×
|
|
648
|
+
</button>
|
|
649
|
+
<img
|
|
650
|
+
src={selectedImage.fields.image.url}
|
|
651
|
+
alt={selectedImage.fields.image.label}
|
|
652
|
+
className="max-w-full max-h-full"
|
|
653
|
+
/>
|
|
654
|
+
</div>
|
|
655
|
+
)}
|
|
656
|
+
</>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Testimonial Slider
|
|
662
|
+
|
|
663
|
+
```tsx
|
|
664
|
+
// src/components/agility-components/Testimonials.server.tsx
|
|
665
|
+
|
|
666
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
667
|
+
import TestimonialsClient from "./Testimonials.client";
|
|
668
|
+
|
|
669
|
+
export default async function Testimonials({ module, locale }: any) {
|
|
670
|
+
const { title } = module.fields;
|
|
671
|
+
|
|
672
|
+
const testimonials = await getContentList({
|
|
673
|
+
referenceName: "testimonials",
|
|
674
|
+
locale,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
return (
|
|
678
|
+
<section className="py-16 bg-gray-50">
|
|
679
|
+
<div className="container mx-auto px-4">
|
|
680
|
+
<h2 className="text-4xl font-bold mb-12 text-center">{title}</h2>
|
|
681
|
+
<TestimonialsClient testimonials={testimonials} />
|
|
682
|
+
</div>
|
|
683
|
+
</section>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
```tsx
|
|
689
|
+
// src/components/agility-components/Testimonials.client.tsx
|
|
690
|
+
"use client";
|
|
691
|
+
|
|
692
|
+
import { useState, useEffect } from "react";
|
|
693
|
+
|
|
694
|
+
export default function TestimonialsClient({ testimonials }: any) {
|
|
695
|
+
const [current, setCurrent] = useState(0);
|
|
696
|
+
|
|
697
|
+
useEffect(() => {
|
|
698
|
+
const timer = setInterval(() => {
|
|
699
|
+
setCurrent((i) => (i + 1) % testimonials.length);
|
|
700
|
+
}, 5000);
|
|
701
|
+
return () => clearInterval(timer);
|
|
702
|
+
}, [testimonials.length]);
|
|
703
|
+
|
|
704
|
+
const testimonial = testimonials[current];
|
|
705
|
+
|
|
706
|
+
return (
|
|
707
|
+
<div className="max-w-4xl mx-auto">
|
|
708
|
+
<div className="text-center">
|
|
709
|
+
<p className="text-2xl italic mb-6">"{testimonial.fields.quote}"</p>
|
|
710
|
+
|
|
711
|
+
<div className="flex items-center justify-center gap-4">
|
|
712
|
+
{testimonial.fields.photo && (
|
|
713
|
+
<img
|
|
714
|
+
src={testimonial.fields.photo.url}
|
|
715
|
+
alt={testimonial.fields.name}
|
|
716
|
+
className="w-16 h-16 rounded-full"
|
|
717
|
+
/>
|
|
718
|
+
)}
|
|
719
|
+
<div className="text-left">
|
|
720
|
+
<p className="font-semibold">{testimonial.fields.name}</p>
|
|
721
|
+
<p className="text-gray-600">{testimonial.fields.company}</p>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
<div className="flex justify-center gap-2 mt-8">
|
|
727
|
+
{testimonials.map((_: any, index: number) => (
|
|
728
|
+
<button
|
|
729
|
+
key={index}
|
|
730
|
+
onClick={() => setCurrent(index)}
|
|
731
|
+
className={`w-3 h-3 rounded-full transition ${
|
|
732
|
+
index === current ? "bg-blue-600" : "bg-gray-300"
|
|
733
|
+
}`}
|
|
734
|
+
/>
|
|
735
|
+
))}
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
## Best Practices
|
|
743
|
+
|
|
744
|
+
1. **Server Components**: Use for data fetching (default)
|
|
745
|
+
2. **Client Components**: Use for interactivity only
|
|
746
|
+
3. **Hybrid Pattern**: Server fetches, client renders interactive parts
|
|
747
|
+
4. **Type Safety**: Define interfaces for props
|
|
748
|
+
5. **Accessibility**: Use semantic HTML and ARIA labels
|
|
749
|
+
6. **Responsive**: Use Tailwind responsive classes
|
|
750
|
+
7. **Loading States**: Show loading indicators for async actions
|
|
751
|
+
|
|
752
|
+
## Next Steps
|
|
753
|
+
|
|
754
|
+
- Read [03-creating-components.md](./03-creating-components.md) for module creation
|
|
755
|
+
- Read [09-example-components.md](./09-example-components.md) for more examples
|
|
756
|
+
- Read [04-data-fetching.md](./04-data-fetching.md) for data patterns
|