create-warlock 5.2.4 → 5.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.
Files changed (39) hide show
  1. package/esm/commands/create-warlock-app/index.mjs +1 -1
  2. package/esm/commands/create-warlock-app/index.mjs.map +1 -1
  3. package/esm/helpers/app.mjs +44 -17
  4. package/esm/helpers/app.mjs.map +1 -1
  5. package/package.json +2 -2
  6. package/templates/warlock/eslint.config.js +98 -98
  7. package/templates/warlock/postcss.config.mjs +6 -0
  8. package/templates/warlock/src/app/contact/controllers/contact.controller.ts +21 -0
  9. package/templates/warlock/src/app/contact/routes.ts +4 -0
  10. package/templates/warlock/src/app/{shared → home}/controllers/home-page.controller.ts +1 -7
  11. package/templates/warlock/src/app/home/services/home.service.ts +86 -0
  12. package/templates/warlock/src/app/locale/controllers/locale.controller.ts +16 -0
  13. package/templates/warlock/src/app/locale/routes.ts +4 -0
  14. package/templates/warlock/src/shared/contact.schema.ts +7 -0
  15. package/templates/warlock/src/shared/locale.schema.ts +11 -0
  16. package/templates/warlock/src/shared/locales.ts +7 -0
  17. package/templates/warlock/src/web/404.css +113 -0
  18. package/templates/warlock/src/web/404.page.tsx +23 -0
  19. package/templates/warlock/src/web/app.css +3 -0
  20. package/templates/warlock/src/web/home/components/contact-form-controls.tsx +42 -0
  21. package/templates/warlock/src/web/home/components/contact-section.tsx +108 -0
  22. package/templates/warlock/src/web/home/components/content-sections.tsx +61 -0
  23. package/templates/warlock/src/web/home/components/hero-section.tsx +68 -0
  24. package/templates/warlock/src/web/home/components/home-footer.tsx +35 -0
  25. package/templates/warlock/src/web/home/components/home-header.tsx +26 -0
  26. package/templates/warlock/src/web/home/components/logo.tsx +19 -0
  27. package/templates/warlock/src/web/home/components/runtime-preview.tsx +75 -0
  28. package/templates/warlock/src/web/home/hooks/use-contact-form.ts +57 -0
  29. package/templates/warlock/src/web/home/hooks/use-locale-switcher.ts +35 -0
  30. package/templates/warlock/src/web/home/index.page.tsx +44 -0
  31. package/templates/warlock/src/web/home/register.ts +30 -0
  32. package/templates/warlock/src/web/home/styles/home.css +1185 -0
  33. package/templates/warlock/src/web/root.tsx +49 -0
  34. package/templates/warlock/src/web/shared/utils/set-form-errors.ts +60 -0
  35. package/templates/warlock/tsconfig.json +3 -1
  36. package/templates/warlock/public/home.css +0 -523
  37. package/templates/warlock/src/app/shared/components/HomePageComponent.tsx +0 -229
  38. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +0 -17
  39. /package/templates/warlock/src/app/{shared → home}/routes.ts +0 -0
@@ -0,0 +1,4 @@
1
+ import { router } from "@warlock.js/core";
2
+ import { localeController } from "./controllers/locale.controller";
3
+
4
+ router.post("/api/locale", localeController);
@@ -0,0 +1,7 @@
1
+ import { v } from "@warlock.js/seal";
2
+
3
+ export const contactSchema = v.object({
4
+ name: v.string().min(2),
5
+ email: v.email(),
6
+ message: v.string().min(10),
7
+ });
@@ -0,0 +1,11 @@
1
+ import { v, type Infer } from "@warlock.js/seal";
2
+ import { localeCodes } from "./locales";
3
+
4
+ export const localeSchema = v.object({
5
+ locale: v
6
+ .string()
7
+ .in([...localeCodes])
8
+ .required(),
9
+ });
10
+
11
+ export type LocaleSchema = Infer.Output<typeof localeSchema>;
@@ -0,0 +1,7 @@
1
+ export const localeCodes = ["en", "ar"] as const;
2
+
3
+ export type LocaleCode = (typeof localeCodes)[number];
4
+
5
+ export function isLocaleCode(locale: string): locale is LocaleCode {
6
+ return localeCodes.includes(locale as LocaleCode);
7
+ }
@@ -0,0 +1,113 @@
1
+ .not-found,
2
+ .not-found *,
3
+ .not-found *::before,
4
+ .not-found *::after {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ .not-found {
9
+ display: grid;
10
+ min-block-size: 100vh;
11
+ min-block-size: 100dvb;
12
+ align-items: center;
13
+ padding-block: clamp(2rem, 8vh, 6rem);
14
+ padding-inline: clamp(1.5rem, 7vw, 8rem);
15
+ background: #fafafa;
16
+ color: #18181b;
17
+ font-family:
18
+ system-ui,
19
+ -apple-system,
20
+ BlinkMacSystemFont,
21
+ "Segoe UI",
22
+ sans-serif;
23
+ }
24
+
25
+ .not-found__content {
26
+ inline-size: min(100%, 62rem);
27
+ margin-inline-start: clamp(0rem, 7vw, 8rem);
28
+ }
29
+
30
+ .not-found__title,
31
+ .not-found__description {
32
+ margin: 0;
33
+ }
34
+
35
+ .not-found h1 {
36
+ margin: 0;
37
+ color: #facc15;
38
+ font-size: clamp(8rem, 25vw, 22rem);
39
+ font-weight: 800;
40
+ letter-spacing: -0.09em;
41
+ line-height: 0.72;
42
+ }
43
+
44
+ .not-found__title {
45
+ margin-block-start: clamp(2rem, 6vw, 5rem);
46
+ font-size: clamp(1.5rem, 3vw, 2.5rem);
47
+ font-weight: 680;
48
+ letter-spacing: -0.035em;
49
+ line-height: 1.1;
50
+ }
51
+
52
+ .not-found__description {
53
+ max-inline-size: 30rem;
54
+ margin-block-start: 0.75rem;
55
+ color: #52525b;
56
+ font-size: clamp(1rem, 1.4vw, 1.125rem);
57
+ line-height: 1.55;
58
+ }
59
+
60
+ .not-found__action {
61
+ display: inline-block;
62
+ margin-block-start: clamp(1.5rem, 3vw, 2.5rem);
63
+ padding-block: 0.65rem;
64
+ padding-inline: 1rem;
65
+ background: #facc15;
66
+ color: #18181b;
67
+ font-weight: 700;
68
+ text-decoration: none;
69
+ }
70
+
71
+ .not-found__action:hover {
72
+ text-decoration: underline;
73
+ text-underline-offset: 0.18em;
74
+ }
75
+
76
+ .not-found__action:focus-visible {
77
+ outline: 3px solid #18181b;
78
+ outline-offset: 4px;
79
+ }
80
+
81
+ :dir(rtl) .not-found__content {
82
+ margin-inline-start: 0;
83
+ margin-inline-end: clamp(0rem, 7vw, 8rem);
84
+ }
85
+
86
+ @media (max-width: 40rem) {
87
+ .not-found__content {
88
+ margin-inline-start: 0;
89
+ }
90
+
91
+ :dir(rtl) .not-found__content {
92
+ margin-inline-end: 0;
93
+ }
94
+ }
95
+
96
+ @media (prefers-color-scheme: dark) {
97
+ .not-found {
98
+ background: #18181b;
99
+ color: #fafafa;
100
+ }
101
+
102
+ .not-found__description {
103
+ color: #d4d4d8;
104
+ }
105
+
106
+ .not-found__action {
107
+ color: #18181b;
108
+ }
109
+
110
+ .not-found__action:focus-visible {
111
+ outline-color: #fafafa;
112
+ }
113
+ }
@@ -0,0 +1,23 @@
1
+ import "./404.css";
2
+
3
+ export const metadata = {
4
+ title: "Page not found",
5
+ robots: "noindex",
6
+ };
7
+
8
+ export default function NotFoundPage() {
9
+ return (
10
+ <main className="not-found">
11
+ <div className="not-found__content">
12
+ <h1>404</h1>
13
+ <p className="not-found__title">Page not found.</p>
14
+ <p className="not-found__description">
15
+ The link may be outdated, or the page may have moved.
16
+ </p>
17
+ <a className="not-found__action" href="/">
18
+ Return home
19
+ </a>
20
+ </div>
21
+ </main>
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Page-level styles live with their pages; this root owns only Tailwind's base layer. */
@@ -0,0 +1,42 @@
1
+ import { type FormControlProps, useFormControl, useSubmitButton } from "@mongez/react-form";
2
+ import { type ReactNode } from "react";
3
+
4
+ type ContactFieldProps = FormControlProps & {
5
+ label: ReactNode;
6
+ multiline?: boolean;
7
+ rows?: number;
8
+ };
9
+
10
+ export function ContactField({ label, multiline, rows, ...props }: ContactFieldProps) {
11
+ const { error, getErrorProps, getInputProps, id } = useFormControl(props);
12
+ const inputProps = getInputProps(rows ? { rows } : undefined);
13
+
14
+ return (
15
+ <label htmlFor={id}>
16
+ <span>{label}</span>
17
+ {multiline ? <textarea {...inputProps} /> : <input {...inputProps} />}
18
+ {error ? (
19
+ <small className="warlock-field-error" {...getErrorProps()}>
20
+ {error}
21
+ </small>
22
+ ) : null}
23
+ </label>
24
+ );
25
+ }
26
+
27
+ export function ContactSubmitButton({
28
+ idleLabel,
29
+ submittingLabel,
30
+ }: {
31
+ idleLabel: ReactNode;
32
+ submittingLabel: ReactNode;
33
+ }) {
34
+ const { disabled, isSubmitting } = useSubmitButton();
35
+
36
+ return (
37
+ <button className="warlock-contact-submit" type="submit" disabled={disabled}>
38
+ {isSubmitting ? submittingLabel : idleLabel}
39
+ <span aria-hidden="true">→</span>
40
+ </button>
41
+ );
42
+ }
@@ -0,0 +1,108 @@
1
+ import { Form } from "@mongez/react-form";
2
+ import { useTrans } from "@warlock.js/web";
3
+ import { contactSchema } from "../../../shared/contact.schema";
4
+ import type { LocaleCode } from "../../../shared/locales";
5
+ import { useContactForm } from "../hooks/use-contact-form";
6
+ import { useLocaleSwitcher } from "../hooks/use-locale-switcher";
7
+ import { ContactField, ContactSubmitButton } from "./contact-form-controls";
8
+
9
+ type ContactSectionProps = { locale: LocaleCode };
10
+
11
+ export function ContactSection({ locale }: ContactSectionProps) {
12
+ const { status: contactStatus, submitContact } = useContactForm();
13
+ const { error: localeError, isSwitching, switchLocale } = useLocaleSwitcher();
14
+ const translate = useTrans();
15
+ return (
16
+ <section
17
+ className="warlock-section warlock-contact"
18
+ id="contact"
19
+ dir={locale === "ar" ? "rtl" : "ltr"}
20
+ >
21
+ <div className="warlock-contact-copy">
22
+ <p className="warlock-overline">{translate("contact.overline")}</p>
23
+ <h2>{translate("contact.title")}</h2>
24
+ <p>{translate("contact.description")}</p>
25
+ <ol className="warlock-contact-flow">
26
+ <li>
27
+ <span>01</span>
28
+ <div>
29
+ <strong>React submits</strong>
30
+ <small>JSON over the same origin</small>
31
+ </div>
32
+ </li>
33
+ <li>
34
+ <span>02</span>
35
+ <div>
36
+ <strong>Seal validates</strong>
37
+ <small>Name, email, and message</small>
38
+ </div>
39
+ </li>
40
+ <li>
41
+ <span>03</span>
42
+ <div>
43
+ <strong>Warlock responds</strong>
44
+ <small>Typed controller, structured JSON</small>
45
+ </div>
46
+ </li>
47
+ </ol>
48
+ </div>
49
+ <Form
50
+ id="contact-demo"
51
+ className="warlock-contact-form"
52
+ schema={contactSchema}
53
+ validateOn="blur"
54
+ focusFirstError
55
+ onSubmit={submitContact}
56
+ >
57
+ <div className="warlock-form-heading">
58
+ <div>
59
+ <small>POST</small>
60
+ <code>/api/contact</code>
61
+ </div>
62
+ <button
63
+ className="warlock-locale-toggle"
64
+ type="button"
65
+ disabled={isSwitching}
66
+ onClick={() => switchLocale(locale === "en" ? "ar" : "en")}
67
+ >
68
+ {translate("contact.toggle")}
69
+ </button>
70
+ </div>
71
+ <ContactField
72
+ name="name"
73
+ type="text"
74
+ label={translate("contact.name")}
75
+ placeholder={translate("contact.namePlaceholder")}
76
+ required
77
+ />
78
+ <ContactField
79
+ name="email"
80
+ type="email"
81
+ label={translate("contact.email")}
82
+ placeholder={translate("contact.emailPlaceholder")}
83
+ required
84
+ />
85
+ <ContactField
86
+ name="message"
87
+ label={translate("contact.message")}
88
+ placeholder={translate("contact.messagePlaceholder")}
89
+ multiline
90
+ rows={5}
91
+ required
92
+ />
93
+ <ContactSubmitButton
94
+ idleLabel={translate("contact.submit")}
95
+ submittingLabel={translate("contact.submitting")}
96
+ />
97
+ <p className={`warlock-form-status is-${contactStatus.state}`} aria-live="polite">
98
+ {contactStatus.message || translate("contact.idle")}
99
+ </p>
100
+ {localeError ? (
101
+ <p className="warlock-form-status is-error" aria-live="polite">
102
+ {localeError}
103
+ </p>
104
+ ) : null}
105
+ </Form>
106
+ </section>
107
+ );
108
+ }
@@ -0,0 +1,61 @@
1
+ import type { HomePageData } from "app/home/services/home.service";
2
+
3
+ type ContentSectionsProps = {
4
+ capabilities: HomePageData["capabilities"];
5
+ packages: HomePageData["packages"];
6
+ };
7
+
8
+ export function ContentSections({ capabilities, packages }: ContentSectionsProps) {
9
+ return (
10
+ <>
11
+ <section className="warlock-section warlock-features" id="features">
12
+ <div className="warlock-section-heading">
13
+ <p className="warlock-overline">Why Warlock</p>
14
+ <h2>A sharper way to build the whole product.</h2>
15
+ <p>
16
+ Fewer seams, fewer competing conventions, and more of your system expressed in code the
17
+ compiler can understand.
18
+ </p>
19
+ </div>
20
+ <div className="warlock-feature-grid">
21
+ {capabilities.map((capability) => (
22
+ <article className="warlock-feature-card" key={capability.index}>
23
+ <div className="warlock-feature-meta">
24
+ <span>{capability.index}</span>
25
+ <small>{capability.eyebrow}</small>
26
+ </div>
27
+ <h3>{capability.title}</h3>
28
+ <p>{capability.body}</p>
29
+ </article>
30
+ ))}
31
+ </div>
32
+ </section>
33
+ <section className="warlock-section warlock-package-section" id="packages">
34
+ <div className="warlock-package-copy">
35
+ <p className="warlock-overline">The spellbook</p>
36
+ <h2>Take the framework. Keep the choice.</h2>
37
+ <p>
38
+ Every package owns one concern and composes with the rest. Build a focused HTTP service
39
+ today, then add persistence, jobs, access control, or agents without replacing the
40
+ foundation.
41
+ </p>
42
+ <a href="https://warlock.js.org/#packages" target="_blank" rel="noreferrer">
43
+ Browse every package <span aria-hidden="true">→</span>
44
+ </a>
45
+ </div>
46
+ <div className="warlock-package-grid" aria-label="Warlock.js packages">
47
+ {packages.map((frameworkPackage) => (
48
+ <article className="warlock-package" key={frameworkPackage.name}>
49
+ <small>{frameworkPackage.area}</small>
50
+ <h3>
51
+ <span>@warlock.js/</span>
52
+ {frameworkPackage.name}
53
+ </h3>
54
+ <p>{frameworkPackage.description}</p>
55
+ </article>
56
+ ))}
57
+ </div>
58
+ </section>
59
+ </>
60
+ );
61
+ }
@@ -0,0 +1,68 @@
1
+ import { RuntimePreview } from "./runtime-preview";
2
+ type HeroSectionProps = { statusMessage: string };
3
+ export function HeroSection({ statusMessage }: HeroSectionProps) {
4
+ return (
5
+ <>
6
+ <section className="warlock-hero">
7
+ <div className="warlock-orbit warlock-orbit-one" aria-hidden="true" />
8
+ <div className="warlock-orbit warlock-orbit-two" aria-hidden="true" />
9
+ <div className="warlock-hero-copy">
10
+ <p className="warlock-kicker">
11
+ <span className="warlock-kicker-dot" />
12
+ AI-native TypeScript framework
13
+ </p>
14
+ <h1>
15
+ Build with<span> uncommon power.</span>
16
+ </h1>
17
+ <p className="warlock-hero-lede">
18
+ Production backends, server-rendered React, and intelligent agents—built on the same
19
+ typed primitives, running in one deliberate architecture.
20
+ </p>
21
+ <div className="warlock-hero-actions">
22
+ <a
23
+ className="warlock-button warlock-button-primary"
24
+ href="https://warlock.js.org/v/latest/core/getting-started/02-installation/"
25
+ target="_blank"
26
+ rel="noreferrer"
27
+ >
28
+ Start building <span aria-hidden="true">→</span>
29
+ </a>
30
+ <a
31
+ className="warlock-button warlock-button-ghost"
32
+ href="https://github.com/warlockjs"
33
+ target="_blank"
34
+ rel="noreferrer"
35
+ >
36
+ Explore the source
37
+ </a>
38
+ </div>
39
+ <dl className="warlock-hero-stats">
40
+ <div>
41
+ <dt>28</dt>
42
+ <dd>focused packages</dd>
43
+ </div>
44
+ <div>
45
+ <dt>120+</dt>
46
+ <dd>AI-readable skills</dd>
47
+ </div>
48
+ <div>
49
+ <dt>MIT</dt>
50
+ <dd>open source</dd>
51
+ </div>
52
+ </dl>
53
+ </div>
54
+ <RuntimePreview statusMessage={statusMessage} />
55
+ </section>
56
+ <section className="warlock-trust" aria-label="Framework qualities">
57
+ <p>Built for teams who expect more from the foundation</p>
58
+ <div>
59
+ <span>Type-safe</span>
60
+ <span>Full-stack</span>
61
+ <span>AI-native</span>
62
+ <span>Composable</span>
63
+ <span>Production-ready</span>
64
+ </div>
65
+ </section>
66
+ </>
67
+ );
68
+ }
@@ -0,0 +1,35 @@
1
+ import { HomeLogo } from "./logo";
2
+
3
+ export function HomeFooter() {
4
+ return (
5
+ <>
6
+ <section className="warlock-cta">
7
+ <div className="warlock-cta-mark" aria-hidden="true">
8
+ W
9
+ </div>
10
+ <p className="warlock-overline">Your next system starts here</p>
11
+ <h2>Build something formidable.</h2>
12
+ <p>Scaffold a typed Warlock application and make the first request in minutes.</p>
13
+ <div className="warlock-command">
14
+ <code>npm create warlock@latest</code>
15
+ <span>Ready when you are</span>
16
+ </div>
17
+ </section>
18
+ <footer className="warlock-footer">
19
+ <div className="warlock-brand">
20
+ <HomeLogo />
21
+ </div>
22
+ <p>Backend, web, and AI on the same typed primitives.</p>
23
+ <div>
24
+ <a href="https://warlock.js.org" target="_blank" rel="noreferrer">
25
+ Documentation
26
+ </a>
27
+ <a href="https://github.com/warlockjs" target="_blank" rel="noreferrer">
28
+ GitHub
29
+ </a>
30
+ <span>MIT © {new Date().getFullYear()}</span>
31
+ </div>
32
+ </footer>
33
+ </>
34
+ );
35
+ }
@@ -0,0 +1,26 @@
1
+ import { HomeLogo } from "./logo";
2
+
3
+ export function HomeHeader() {
4
+ return (
5
+ <header className="warlock-nav">
6
+ <a className="warlock-brand" href="/" aria-label="Warlock.js home">
7
+ <HomeLogo showVersion />
8
+ </a>
9
+ <nav className="warlock-nav-links" aria-label="Primary navigation">
10
+ <a href="#features">Features</a>
11
+ <a href="#packages">Packages</a>
12
+ <a href="https://warlock.js.org/v/latest/core/" target="_blank" rel="noreferrer">
13
+ Docs
14
+ </a>
15
+ </nav>
16
+ <a
17
+ className="warlock-nav-cta"
18
+ href="https://github.com/warlockjs"
19
+ target="_blank"
20
+ rel="noreferrer"
21
+ >
22
+ View on GitHub <span aria-hidden="true">↗</span>
23
+ </a>
24
+ </header>
25
+ );
26
+ }
@@ -0,0 +1,19 @@
1
+ type HomeLogoProps = {
2
+ showVersion?: boolean;
3
+ };
4
+
5
+ export function HomeLogo({ showVersion = false }: HomeLogoProps) {
6
+ return (
7
+ <>
8
+ <span className="warlock-logo-shell">
9
+ <img
10
+ className="warlock-logo"
11
+ src="https://warlock.js.org/_astro/logo.CdUW31XC.png"
12
+ alt=""
13
+ />
14
+ </span>
15
+ <span className="warlock-wordmark">Warlock.js</span>
16
+ {showVersion ? <span className="warlock-version">v5</span> : null}
17
+ </>
18
+ );
19
+ }
@@ -0,0 +1,75 @@
1
+ type RuntimePreviewProps = { statusMessage: string };
2
+
3
+ export function RuntimePreview({ statusMessage }: RuntimePreviewProps) {
4
+ return (
5
+ <div className="warlock-hero-stage" aria-label="Warlock application preview">
6
+ <div className="warlock-glow" aria-hidden="true" />
7
+ <div className="warlock-terminal">
8
+ <div className="warlock-terminal-bar">
9
+ <div className="warlock-terminal-dots" aria-hidden="true">
10
+ <span />
11
+ <span />
12
+ <span />
13
+ </div>
14
+ <span>src/web/index.page.tsx</span>
15
+ <span className="warlock-terminal-live">Live</span>
16
+ </div>
17
+ <div className="warlock-code" aria-label="Example Warlock page code">
18
+ <div>
19
+ <span className="code-purple">import type</span> {"{"} PageProps {"}"}{" "}
20
+ <span className="code-purple">from</span>{" "}
21
+ <span className="code-green">&quot;@warlock.js/web&quot;</span>;
22
+ </div>
23
+ <div className="warlock-code-spacer" aria-hidden="true" />
24
+ <div>
25
+ <span className="code-purple">export async function</span>{" "}
26
+ <span className="code-blue">loader</span>() {"{"}
27
+ </div>
28
+ <div className="warlock-code-indent">
29
+ <span className="code-purple">return</span> {"{"} statusMessage:{" "}
30
+ <span className="code-green">&quot;Application ready&quot;</span> {"}"};
31
+ </div>
32
+ <div>{"}"}</div>
33
+ <div className="warlock-code-spacer" aria-hidden="true" />
34
+ <div>
35
+ <span className="code-purple">type</span>{" "}
36
+ <span className="code-blue">HomePageProps</span> ={" "}
37
+ <span className="code-blue">PageProps</span>&lt;
38
+ <span className="code-purple">typeof</span> loader&gt;;
39
+ </div>
40
+ <div className="warlock-code-spacer" aria-hidden="true" />
41
+ <div>
42
+ <span className="code-purple">export default function</span>{" "}
43
+ <span className="code-blue">HomePage</span>({"{"} data {"}"}:{" "}
44
+ <span className="code-blue">HomePageProps</span>) {"{"}
45
+ </div>
46
+ <div className="warlock-code-indent">
47
+ <span className="code-purple">return</span> &lt;small&gt;{"{"}data.statusMessage{"}"}
48
+ &lt;/small&gt;;
49
+ </div>
50
+ <div>{"}"}</div>
51
+ </div>
52
+ <div className="warlock-runtime">
53
+ <div>
54
+ <span className="warlock-runtime-pulse" />
55
+ <div>
56
+ <strong>Application ready</strong>
57
+ <small>{statusMessage}</small>
58
+ </div>
59
+ </div>
60
+ <div className="warlock-runtime-marks" aria-label="Runtime capabilities">
61
+ <span>SSR</span>
62
+ <span>React</span>
63
+ <span>HMR</span>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ <div className="warlock-stage-note warlock-stage-note-top">
68
+ <span>SSR</span>First response, fully rendered
69
+ </div>
70
+ <div className="warlock-stage-note warlock-stage-note-bottom">
71
+ <span>HMR</span>State survives the edit
72
+ </div>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,57 @@
1
+ import { http } from "@mongez/http";
2
+ import { type FormSubmitOptions, type InferFormValues } from "@mongez/react-form";
3
+ import { useState } from "react";
4
+ import type { contactSchema } from "../../../shared/contact.schema";
5
+ import { setFormErrors } from "../../shared/utils/set-form-errors";
6
+
7
+ type ContactValues = InferFormValues<typeof contactSchema>;
8
+
9
+ type ContactResponse = {
10
+ message: string;
11
+ received: {
12
+ email: string;
13
+ characters: number;
14
+ };
15
+ };
16
+
17
+ type ContactStatus = {
18
+ state: "idle" | "submitting" | "success" | "error";
19
+ message: string;
20
+ };
21
+
22
+ export function useContactForm() {
23
+ const [status, setStatus] = useState<ContactStatus>({ state: "idle", message: "" });
24
+
25
+ async function submitContact({
26
+ values,
27
+ form,
28
+ }: FormSubmitOptions<typeof contactSchema>): Promise<void> {
29
+ setStatus({ state: "submitting", message: "Sending to the backend…" });
30
+
31
+ try {
32
+ const result = await http.post<ContactResponse>("/api/contact", values as ContactValues);
33
+
34
+ if (result.error) {
35
+ if (result.error.isValidationError) {
36
+ setFormErrors(result.error, form);
37
+ }
38
+
39
+ setStatus({
40
+ state: "error",
41
+ message: result.error.message || "The request was not accepted.",
42
+ });
43
+ return;
44
+ }
45
+
46
+ setStatus({ state: "success", message: result.data?.message });
47
+ form.reset();
48
+ } catch (error) {
49
+ setStatus({
50
+ state: "error",
51
+ message: error instanceof Error ? error.message : "Could not reach the backend.",
52
+ });
53
+ }
54
+ }
55
+
56
+ return { status, submitContact };
57
+ }