fimo 2.2.0-staging.2 → 2.2.0-staging.4
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 +3 -2
- package/assets/skills/fimo-cli/SKILL.md +4 -4
- package/assets/skills/fimo-cli/references/init.md +30 -39
- package/assets/skills/fimo-code/references/frameworks/react-router.md +4 -3
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +90 -91
- package/dist/runtime/templates.js +1 -1
- package/package.json +7 -7
- package/templates/astro/package.json +2 -2
- package/templates/nextjs/AGENTS.md +1 -1
- package/templates/nextjs/package.json +3 -3
- package/templates/react-router/.fimo/config.json +1 -2
- package/templates/react-router/AGENTS.md +1 -1
- package/templates/react-router/package.json +4 -4
- package/templates/sveltekit/package.json +2 -2
- package/templates/vite/package.json +4 -4
- package/assets/starters/fimo-site/manifest.json +0 -64
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/GOAL.md +0 -8
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/capabilities.yaml +0 -1
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/config.yaml +0 -9
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/GOAL.md +0 -14
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/capabilities.yaml +0 -3
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/config.yaml +0 -9
- package/assets/starters/fimo-site/template/src/forms/contact.json +0 -14
- package/assets/starters/fimo-site/template/src/forms/contact.ts +0 -46
- package/assets/starters/fimo-site/template/src/pages/Contact.tsx +0 -193
- package/assets/starters/fimo-site/template/src/pages/Index.tsx +0 -27
- package/assets/starters/fimo-site/template/src/pages/SiteContent.tsx +0 -191
- package/assets/starters/fimo-site/template/src/schemas/HomePage.json +0 -28
- package/assets/starters/fimo-site/template/src/schemas/HomePage.ts +0 -29
- package/assets/starters/fimo-site/template/src/schemas/HomePage.types.ts +0 -64
|
@@ -28,7 +28,7 @@ const PACKAGE_ROOT = path.resolve(MODULE_DIR, '..', '..');
|
|
|
28
28
|
const RELEASE_FILE = path.join(PACKAGE_ROOT, 'release.json');
|
|
29
29
|
const PACKAGE_JSON_FILE = path.join(PACKAGE_ROOT, 'package.json');
|
|
30
30
|
export const TEMPLATE_FRAMEWORKS = ['react-router', 'vite', 'astro', 'nextjs', 'sveltekit'];
|
|
31
|
-
export const DEFAULT_TEMPLATE = '
|
|
31
|
+
export const DEFAULT_TEMPLATE = 'nextjs';
|
|
32
32
|
/**
|
|
33
33
|
* Absolute path to the requested framework's template directory.
|
|
34
34
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "2.2.0-staging.
|
|
3
|
+
"version": "2.2.0-staging.4",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro-component"
|
|
@@ -184,12 +184,12 @@
|
|
|
184
184
|
"devDependencies": {
|
|
185
185
|
"@clack/core": "^1.2.0",
|
|
186
186
|
"@clack/prompts": "^1.2.0",
|
|
187
|
-
"@fimo/agents-bundle": "2.2.0-staging.
|
|
188
|
-
"@fimo/api-contracts": "2.2.0-staging.
|
|
189
|
-
"@fimo/cli-kit": "2.2.0-staging.
|
|
190
|
-
"@fimo/framework-detect": "2.2.0-staging.
|
|
191
|
-
"@fimo/routes": "2.2.0-staging.
|
|
192
|
-
"@fimo/tsconfig": "2.2.0-staging.
|
|
187
|
+
"@fimo/agents-bundle": "2.2.0-staging.4",
|
|
188
|
+
"@fimo/api-contracts": "2.2.0-staging.4",
|
|
189
|
+
"@fimo/cli-kit": "2.2.0-staging.4",
|
|
190
|
+
"@fimo/framework-detect": "2.2.0-staging.4",
|
|
191
|
+
"@fimo/routes": "2.2.0-staging.4",
|
|
192
|
+
"@fimo/tsconfig": "2.2.0-staging.4",
|
|
193
193
|
"@types/node": "22.18.6",
|
|
194
194
|
"agent-install": "^0.0.5",
|
|
195
195
|
"astro": "^7.1.6",
|
|
@@ -37,7 +37,7 @@ The scaffolded homepage is disposable. Replace it on the first real generation.
|
|
|
37
37
|
- **Deploy is on-demand**, not a save step. Batch related edits into one deploy.
|
|
38
38
|
- Use the selected package manager's committed build-script policy. pnpm projects pin `pnpm@10.26.0` and approve `esbuild` and `fimo` through `pnpm-workspace.yaml#allowBuilds`; Bun projects trust the same packages through `package.json#trustedDependencies`. Run a plain install with the selected manager so local installation matches Preview and production publishing. Review new dependencies before extending the policy; all three paths stop on unreviewed scripts.
|
|
39
39
|
- Treat the user's language request as part of setup. The first explicitly requested locale is the primary locale; put it first and set it as `i18n.defaultLocale` in `.fimo/config.json`, with every requested locale in `i18n.locales`. If no language is requested, keep `en` and state that choice. That file is the only place to change: `src/locales.ts` reads it, so the served language and `<html lang>` follow. It does not create locale URLs; implement a URL strategy only when the product asks for one. Never introduce a default-locale environment variable.
|
|
40
|
-
- Next's `app` tree is the router and stays authoritative: the
|
|
40
|
+
- Next's `app` tree is the router and stays authoritative: the default shell is one ordinary root layout plus `/` as the homepage, and unknown paths get Next's native 404. When a product requires locale URLs, implement the framework-native strategy (`[locale]` segments, middleware, cookies, domains, or an i18n library), pass the resolved locale into Fimo server reads and `FimoProvider`, and load `fimo skills get fimo-code --ref frameworks/next/locales` first. Do not impose locale-prefixed URLs on a default-only project.
|
|
41
41
|
- `FimoProvider` owns the optional Studio preview runtime. Do not add a preview bridge component, read Fimo preview environment variables, or inject Fimo scripts in application code.
|
|
42
42
|
- Keep `.fimo/config.json#routes` as Studio metadata, not as the Next.js router. Describe only URLs the application actually serves; add a `:locale` segment to a route's metadata only after the application implements that locale routing. There, `params.locale.optional: true` means omit the configured default locale from the URL; use `optional: false` only when every locale, including the default, must be prefixed.
|
|
43
43
|
- The application owns routes, locale resolution, links, HTML language/direction, and SEO. Fimo connects to those decisions.
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@tanstack/react-query": "^5.80.6",
|
|
15
|
-
"fimo": "2.2.0-staging.
|
|
16
|
-
"fimo-next": "2.2.0-staging.
|
|
17
|
-
"fimo-react": "2.2.0-staging.
|
|
15
|
+
"fimo": "2.2.0-staging.4",
|
|
16
|
+
"fimo-next": "2.2.0-staging.4",
|
|
17
|
+
"fimo-react": "2.2.0-staging.4",
|
|
18
18
|
"next": "^16.3.0",
|
|
19
19
|
"react": "^19.2.7",
|
|
20
20
|
"react-dom": "^19.2.7"
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
"runtime": { "framework": "react-router" },
|
|
3
3
|
"seo": {
|
|
4
4
|
"title": "Welcome to your Fimo website",
|
|
5
|
-
"description": "A default homepage
|
|
6
|
-
"siteName": "Fimo Starter Site",
|
|
5
|
+
"description": "A default homepage for Fimo projects.",
|
|
7
6
|
"url": "https://fimo.ai/",
|
|
8
7
|
"image": "https://fimo.ai/socials/og_cover.png",
|
|
9
8
|
"favicon": "https://fimo.ai/favicon-orange.svg",
|
|
@@ -47,6 +47,6 @@ breaks that. Load `fimo-code` for the exact primitives, but the rule is:
|
|
|
47
47
|
- Do not add a second deploy tool (Vercel, Netlify, etc.).
|
|
48
48
|
- **Never edit `.fimo/project.json`** — it links this folder to the remote project.
|
|
49
49
|
- **Keep `.fimo/config.json#runtime` true to the code** — framework, `dev`/`build`/`install` commands, output directory. Update it when you rename a package script, move the build output, or change framework; the reference is `fimo skills get fimo-code --ref frameworks/hosting`.
|
|
50
|
-
- The scaffolded homepage is disposable
|
|
50
|
+
- The scaffolded homepage is disposable template content. Replace it on the first real generation.
|
|
51
51
|
- **Secrets live in the Fimo dashboard**, not in a committed `.env`.
|
|
52
52
|
- When scripting, pass flags to `fimo` commands (`fimo deploy -m "…"`, `fimo delete -y`) — bare interactive forms hang in non-TTY shells.
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"cmdk": "^1.1.1",
|
|
22
22
|
"date-fns": "^4.1.0",
|
|
23
23
|
"embla-carousel-react": "^8.6.0",
|
|
24
|
-
"fimo": "2.2.0-staging.
|
|
25
|
-
"fimo-react": "2.2.0-staging.
|
|
26
|
-
"fimo-react-router": "2.2.0-staging.
|
|
27
|
-
"fimo-vite": "2.2.0-staging.
|
|
24
|
+
"fimo": "2.2.0-staging.4",
|
|
25
|
+
"fimo-react": "2.2.0-staging.4",
|
|
26
|
+
"fimo-react-router": "2.2.0-staging.4",
|
|
27
|
+
"fimo-vite": "2.2.0-staging.4",
|
|
28
28
|
"input-otp": "^1.4.2",
|
|
29
29
|
"isbot": "^5",
|
|
30
30
|
"lucide-react": "^0.577.0",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"prepare": "svelte-kit sync"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"fimo": "2.2.0-staging.
|
|
15
|
-
"fimo-sveltekit": "2.2.0-staging.
|
|
14
|
+
"fimo": "2.2.0-staging.4",
|
|
15
|
+
"fimo-sveltekit": "2.2.0-staging.4"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@sveltejs/kit": "^2.70.2",
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@tanstack/react-query": "^5.80.6",
|
|
14
|
-
"fimo": "2.2.0-staging.
|
|
15
|
-
"fimo-react": "2.2.0-staging.
|
|
16
|
-
"fimo-react-router": "2.2.0-staging.
|
|
17
|
-
"fimo-vite": "2.2.0-staging.
|
|
14
|
+
"fimo": "2.2.0-staging.4",
|
|
15
|
+
"fimo-react": "2.2.0-staging.4",
|
|
16
|
+
"fimo-react-router": "2.2.0-staging.4",
|
|
17
|
+
"fimo-vite": "2.2.0-staging.4",
|
|
18
18
|
"react": "^19.2.7",
|
|
19
19
|
"react-dom": "^19.2.7",
|
|
20
20
|
"react-router": "^8.3.0"
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "fimo-site",
|
|
3
|
-
"version": 1,
|
|
4
|
-
"description": "A content-led site with a homepage, contact form, labels, and manual review agents.",
|
|
5
|
-
"singletons": [
|
|
6
|
-
{
|
|
7
|
-
"type": "HomePage",
|
|
8
|
-
"data": {
|
|
9
|
-
"eyebrow": "Field notes",
|
|
10
|
-
"title": "Your website, ready to edit.",
|
|
11
|
-
"intro": {
|
|
12
|
-
"type": "doc",
|
|
13
|
-
"content": [
|
|
14
|
-
{
|
|
15
|
-
"type": "paragraph",
|
|
16
|
-
"content": [
|
|
17
|
-
{
|
|
18
|
-
"type": "text",
|
|
19
|
-
"text": "This starter is real website content. Edit the copy in Studio, change the code with your favorite agent, and keep both surfaces in sync."
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
|
-
"primaryCtaLabel": "Read the field notes",
|
|
26
|
-
"primaryCtaHref": "#stories",
|
|
27
|
-
"featured": [
|
|
28
|
-
{
|
|
29
|
-
"label": "Start here",
|
|
30
|
-
"title": "A calmer content workflow",
|
|
31
|
-
"summary": "Keep content, code, previews, and the team in one editable project.",
|
|
32
|
-
"href": "#stories"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"label": "For agents",
|
|
36
|
-
"title": "Build once, keep iterating",
|
|
37
|
-
"summary": "Give your coding agent a real CMS surface to work with after the first deploy.",
|
|
38
|
-
"href": "#stories"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"label": "For teams",
|
|
42
|
-
"title": "Make every edit reviewable",
|
|
43
|
-
"summary": "Let teammates shape the site without asking them to open a code editor.",
|
|
44
|
-
"href": "#stories"
|
|
45
|
-
}
|
|
46
|
-
],
|
|
47
|
-
"contactHeading": "Make something useful",
|
|
48
|
-
"contactIntro": "Tell us what you are building and we will help you find the shortest path from first draft to a site your team can own."
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
],
|
|
52
|
-
"labels": [
|
|
53
|
-
{ "key": "nav.stories", "value": "Field notes", "locale": "en" },
|
|
54
|
-
{ "key": "nav.contact", "value": "Start a conversation", "locale": "en" },
|
|
55
|
-
{ "key": "hero.scroll", "value": "Explore the starter", "locale": "en" },
|
|
56
|
-
{ "key": "contact.name", "value": "Your name", "locale": "en" },
|
|
57
|
-
{ "key": "contact.email", "value": "Email address", "locale": "en" },
|
|
58
|
-
{ "key": "contact.message", "value": "What are you building?", "locale": "en" },
|
|
59
|
-
{ "key": "contact.submit", "value": "Send a note", "locale": "en" },
|
|
60
|
-
{ "key": "contact.sent", "value": "Thanks. Your note is on its way.", "locale": "en" },
|
|
61
|
-
{ "key": "contact.failed", "value": "That did not send. Please try again.", "locale": "en" }
|
|
62
|
-
],
|
|
63
|
-
"forms": ["contact"]
|
|
64
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Content guide
|
|
2
|
-
|
|
3
|
-
Inspect the site's content model and homepage copy. Suggest a small editorial
|
|
4
|
-
backlog that makes the site more useful to its intended audience.
|
|
5
|
-
|
|
6
|
-
Return three suggestions, each with the content type or field it touches and
|
|
7
|
-
why it matters. Use the existing content as evidence. Do not write files,
|
|
8
|
-
change entries, or create labels.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
- cms:read
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Keep starter agents manual-only. Adding a schedule or webhook and pushing to
|
|
2
|
-
# main registers an automation for the project.
|
|
3
|
-
version: 1
|
|
4
|
-
description: 'Suggest useful editorial improvements from the existing CMS content.'
|
|
5
|
-
data_sources:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
triggers:
|
|
9
|
-
- type: manual
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Site review
|
|
2
|
-
|
|
3
|
-
Review the starter project as a teammate would before the first publish.
|
|
4
|
-
|
|
5
|
-
Read the declared routes, the CMS content, the labels, and the public site.
|
|
6
|
-
Return a short report with:
|
|
7
|
-
|
|
8
|
-
- one thing that already feels clear;
|
|
9
|
-
- the three highest-value improvements;
|
|
10
|
-
- any missing content or broken route you can prove;
|
|
11
|
-
- one next action that a human or coding agent can review.
|
|
12
|
-
|
|
13
|
-
This is a read-only review. Do not edit files, change CMS content, create
|
|
14
|
-
branches, or publish anything.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Keep starter agents manual-only. Adding a schedule or webhook and pushing to
|
|
2
|
-
# main registers an automation for the project.
|
|
3
|
-
version: 1
|
|
4
|
-
description: 'Review the starter site and return a concise, read-only launch report.'
|
|
5
|
-
data_sources:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
triggers:
|
|
9
|
-
- type: manual
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "contact",
|
|
3
|
-
"label": "Start a conversation",
|
|
4
|
-
"fields": {
|
|
5
|
-
"name": { "type": "text", "label": "Your name", "required": true },
|
|
6
|
-
"email": { "type": "email", "label": "Email address", "required": true },
|
|
7
|
-
"message": {
|
|
8
|
-
"type": "textarea",
|
|
9
|
-
"label": "What are you building?",
|
|
10
|
-
"required": true,
|
|
11
|
-
"validation": { "minLength": 10, "maxLength": 2000 }
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED ⚠️ — do not edit by hand
|
|
2
|
-
// Source: src/forms/contact.json
|
|
3
|
-
// The CLI replaces the placeholder form id while seeding the starter. `fimo
|
|
4
|
-
// validate` regenerates this file from the JSON spec on later edits.
|
|
5
|
-
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
|
|
8
|
-
export const FORM_ID = '__FIMO_FORM_ID__';
|
|
9
|
-
|
|
10
|
-
export const ContactSchema = z.object({
|
|
11
|
-
name: z.string().min(1, 'Your name is required'),
|
|
12
|
-
email: z.string().min(1, 'Email address is required').email('Invalid email address'),
|
|
13
|
-
message: z.string().min(1, 'What are you building? is required').min(10).max(2000),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export type ContactData = z.infer<typeof ContactSchema>;
|
|
17
|
-
|
|
18
|
-
// @ts-ignore - meta.env is a vite feature
|
|
19
|
-
const base = import.meta.env.VITE_API_URL;
|
|
20
|
-
|
|
21
|
-
export async function submitContact(data: ContactData): Promise<{
|
|
22
|
-
success: boolean;
|
|
23
|
-
errors?: Record<string, string>;
|
|
24
|
-
}> {
|
|
25
|
-
let response: Response;
|
|
26
|
-
try {
|
|
27
|
-
response = await fetch(`${base}/forms/${FORM_ID}/submit`, {
|
|
28
|
-
method: 'POST',
|
|
29
|
-
headers: { 'Content-Type': 'application/json' },
|
|
30
|
-
body: JSON.stringify(data),
|
|
31
|
-
});
|
|
32
|
-
} catch {
|
|
33
|
-
return { success: false };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (response.ok) {
|
|
37
|
-
return { success: true };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (response.status === 422) {
|
|
41
|
-
const json = (await response.json()) as { errors?: Record<string, string> };
|
|
42
|
-
return { success: false, errors: json.errors };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { success: false };
|
|
46
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { type FimoString, Text, useLabels } from 'fimo-react';
|
|
2
|
-
import { ArrowUpRight, Check } from 'lucide-react';
|
|
3
|
-
import { type FormEvent, type ReactNode, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
import { Button } from '@/components/ui/button';
|
|
6
|
-
import { Input } from '@/components/ui/input';
|
|
7
|
-
import { Label } from '@/components/ui/label';
|
|
8
|
-
import { Spinner } from '@/components/ui/spinner';
|
|
9
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
10
|
-
import { ContactSchema, submitContact } from '@/forms/contact';
|
|
11
|
-
import { cn } from '@/lib/utils';
|
|
12
|
-
import type { HomePage } from '@/schemas/HomePage';
|
|
13
|
-
|
|
14
|
-
import { Bar } from './SiteContent';
|
|
15
|
-
|
|
16
|
-
export function Contact({ page, loading }: { page?: HomePage; loading: boolean }) {
|
|
17
|
-
return (
|
|
18
|
-
<section id="contact" className="dark bg-background text-foreground scroll-mt-16">
|
|
19
|
-
<div className="mx-auto grid w-full max-w-6xl gap-12 px-6 py-20 lg:grid-cols-12 lg:gap-16 lg:px-10 lg:py-28">
|
|
20
|
-
<div className="lg:col-span-5">
|
|
21
|
-
{page ? (
|
|
22
|
-
<>
|
|
23
|
-
<Text
|
|
24
|
-
value={page.contactHeading}
|
|
25
|
-
as="h2"
|
|
26
|
-
className="text-4xl font-semibold tracking-[-0.03em] text-balance lg:text-5xl"
|
|
27
|
-
/>
|
|
28
|
-
<Text value={page.contactIntro} as="p" className="text-muted-foreground mt-6 max-w-md leading-relaxed" />
|
|
29
|
-
</>
|
|
30
|
-
) : (
|
|
31
|
-
<div aria-hidden>
|
|
32
|
-
<div className="grid gap-3">
|
|
33
|
-
<Bar loading={loading} className="h-10 w-4/5" />
|
|
34
|
-
<Bar loading={loading} className="h-10 w-2/5" />
|
|
35
|
-
</div>
|
|
36
|
-
<div className="mt-8 grid gap-2">
|
|
37
|
-
<Bar loading={loading} className="h-4 w-full max-w-md" />
|
|
38
|
-
<Bar loading={loading} className="h-4 w-11/12 max-w-md" />
|
|
39
|
-
<Bar loading={loading} className="h-4 w-2/3 max-w-md" />
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
)}
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<div className="lg:col-span-7">
|
|
46
|
-
<ContactForm />
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</section>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type SubmitState = 'idle' | 'submitting' | 'sent' | 'invalid' | 'failed';
|
|
54
|
-
|
|
55
|
-
function ContactForm() {
|
|
56
|
-
const { t } = useLabels();
|
|
57
|
-
const [state, setState] = useState<SubmitState>('idle');
|
|
58
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
59
|
-
|
|
60
|
-
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
61
|
-
event.preventDefault();
|
|
62
|
-
// `currentTarget` is cleared once the handler yields, so keep the node.
|
|
63
|
-
const form = event.currentTarget;
|
|
64
|
-
setErrors({});
|
|
65
|
-
|
|
66
|
-
const parsed = ContactSchema.safeParse(Object.fromEntries(new FormData(form)));
|
|
67
|
-
if (!parsed.success) {
|
|
68
|
-
const fieldErrors: Record<string, string> = {};
|
|
69
|
-
for (const issue of parsed.error.issues) {
|
|
70
|
-
fieldErrors[issue.path[0] as string] = issue.message;
|
|
71
|
-
}
|
|
72
|
-
setErrors(fieldErrors);
|
|
73
|
-
setState('invalid');
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
setState('submitting');
|
|
78
|
-
const result = await submitContact(parsed.data);
|
|
79
|
-
if (result.success) {
|
|
80
|
-
form.reset();
|
|
81
|
-
setState('sent');
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// A rejected submission comes back with per-field messages; a network or
|
|
86
|
-
// server failure has nothing to attach to a field, so it needs its own.
|
|
87
|
-
const fieldErrors = result.errors ?? {};
|
|
88
|
-
setErrors(fieldErrors);
|
|
89
|
-
setState(Object.keys(fieldErrors).length > 0 ? 'invalid' : 'failed');
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const busy = state === 'submitting';
|
|
93
|
-
const sent = state === 'sent';
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<form
|
|
97
|
-
onSubmit={handleSubmit}
|
|
98
|
-
noValidate
|
|
99
|
-
aria-busy={busy}
|
|
100
|
-
className="border-border bg-card rounded-xl border p-6 sm:p-8"
|
|
101
|
-
>
|
|
102
|
-
<div className="grid gap-6 sm:grid-cols-2">
|
|
103
|
-
<FormField id="contact-name" label={t('contact.name')} error={errors.name}>
|
|
104
|
-
<Input
|
|
105
|
-
id="contact-name"
|
|
106
|
-
name="name"
|
|
107
|
-
type="text"
|
|
108
|
-
autoComplete="name"
|
|
109
|
-
disabled={busy || sent}
|
|
110
|
-
aria-invalid={Boolean(errors.name)}
|
|
111
|
-
aria-describedby={errors.name ? 'contact-name-error' : undefined}
|
|
112
|
-
/>
|
|
113
|
-
</FormField>
|
|
114
|
-
|
|
115
|
-
<FormField id="contact-email" label={t('contact.email')} error={errors.email}>
|
|
116
|
-
<Input
|
|
117
|
-
id="contact-email"
|
|
118
|
-
name="email"
|
|
119
|
-
type="email"
|
|
120
|
-
autoComplete="email"
|
|
121
|
-
disabled={busy || sent}
|
|
122
|
-
aria-invalid={Boolean(errors.email)}
|
|
123
|
-
aria-describedby={errors.email ? 'contact-email-error' : undefined}
|
|
124
|
-
/>
|
|
125
|
-
</FormField>
|
|
126
|
-
|
|
127
|
-
<div className="sm:col-span-2">
|
|
128
|
-
<FormField id="contact-message" label={t('contact.message')} error={errors.message}>
|
|
129
|
-
<Textarea
|
|
130
|
-
id="contact-message"
|
|
131
|
-
name="message"
|
|
132
|
-
rows={5}
|
|
133
|
-
disabled={busy || sent}
|
|
134
|
-
aria-invalid={Boolean(errors.message)}
|
|
135
|
-
aria-describedby={errors.message ? 'contact-message-error' : undefined}
|
|
136
|
-
/>
|
|
137
|
-
</FormField>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div className="mt-8 flex flex-wrap items-center gap-x-4 gap-y-3">
|
|
142
|
-
<Button
|
|
143
|
-
type="submit"
|
|
144
|
-
size="lg"
|
|
145
|
-
disabled={busy || sent}
|
|
146
|
-
className={cn('group', state === 'invalid' && 'ring-destructive/40 ring-[3px]')}
|
|
147
|
-
>
|
|
148
|
-
{busy ? <Spinner /> : null}
|
|
149
|
-
{sent ? <Check /> : null}
|
|
150
|
-
<Text value={t('contact.submit')} />
|
|
151
|
-
{!busy && !sent ? (
|
|
152
|
-
<ArrowUpRight className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
|
153
|
-
) : null}
|
|
154
|
-
</Button>
|
|
155
|
-
|
|
156
|
-
{/* Both regions stay mounted and empty: a live region inserted at the same
|
|
157
|
-
moment as its text is announced unreliably. */}
|
|
158
|
-
<p role="status" className="text-muted-foreground text-sm">
|
|
159
|
-
{sent ? <Text value={t('contact.sent')} /> : null}
|
|
160
|
-
</p>
|
|
161
|
-
<p role="alert" className="text-destructive text-sm">
|
|
162
|
-
{state === 'failed' ? <Text value={t('contact.failed')} /> : null}
|
|
163
|
-
</p>
|
|
164
|
-
</div>
|
|
165
|
-
</form>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function FormField({
|
|
170
|
-
id,
|
|
171
|
-
label,
|
|
172
|
-
error,
|
|
173
|
-
children,
|
|
174
|
-
}: {
|
|
175
|
-
id: string;
|
|
176
|
-
label: FimoString;
|
|
177
|
-
error?: string;
|
|
178
|
-
children: ReactNode;
|
|
179
|
-
}) {
|
|
180
|
-
return (
|
|
181
|
-
<div className="grid gap-2">
|
|
182
|
-
<Label htmlFor={id} className="text-muted-foreground text-[11px] font-medium tracking-[0.16em] uppercase">
|
|
183
|
-
<Text value={label} />
|
|
184
|
-
</Label>
|
|
185
|
-
{children}
|
|
186
|
-
{error ? (
|
|
187
|
-
<p id={`${id}-error`} role="alert" className="text-destructive text-xs">
|
|
188
|
-
{error}
|
|
189
|
-
</p>
|
|
190
|
-
) : null}
|
|
191
|
-
</div>
|
|
192
|
-
);
|
|
193
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import HomePageContent from '@/schemas/HomePage';
|
|
2
|
-
|
|
3
|
-
import { Contact } from './Contact';
|
|
4
|
-
import { ContentFallback, Hero, SiteHeader, Stories } from './SiteContent';
|
|
5
|
-
|
|
6
|
-
const Index = () => {
|
|
7
|
-
const { data: page, isLoading } = HomePageContent.useGet();
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<div className="bg-background text-foreground min-h-screen">
|
|
11
|
-
<SiteHeader />
|
|
12
|
-
<main>
|
|
13
|
-
{page ? (
|
|
14
|
-
<>
|
|
15
|
-
<Hero page={page} />
|
|
16
|
-
<Stories items={page.featured ?? []} />
|
|
17
|
-
</>
|
|
18
|
-
) : (
|
|
19
|
-
<ContentFallback loading={isLoading} />
|
|
20
|
-
)}
|
|
21
|
-
<Contact page={page} loading={isLoading} />
|
|
22
|
-
</main>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default Index;
|