@krizaka/ui 3.0.0 → 3.1.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 +1 -1
- package/dist/form/server.d.ts +96 -0
- package/dist/form/server.js +82 -0
- package/dist/form/server.js.map +1 -0
- package/dist/form.d.ts +503 -0
- package/dist/form.js +760 -0
- package/dist/form.js.map +1 -0
- package/dist/native/form.d.ts +376 -0
- package/dist/native/form.js +650 -0
- package/dist/native/form.js.map +1 -0
- package/dist/native.d.ts +113 -2
- package/dist/native.js +618 -388
- package/dist/native.js.map +1 -1
- package/package.json +48 -5
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/alert.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/checkbox.json +89 -4
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/choice-grid.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +1 -1
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +1 -1
- package/registry/date-picker.json +142 -4
- package/registry/decision-list.json +1 -1
- package/registry/detail-layout.json +1 -1
- package/registry/dialog.json +1 -1
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/checkbox/native/terms.d.ts +4 -0
- package/registry/examples/checkbox/native/terms.dark.png +0 -0
- package/registry/examples/checkbox/native/terms.js +26 -0
- package/registry/examples/checkbox/native/terms.light.png +0 -0
- package/registry/examples/checkbox/native/terms.tsx +28 -0
- package/registry/examples/date-picker/native/birth-date.d.ts +4 -0
- package/registry/examples/date-picker/native/birth-date.dark.png +0 -0
- package/registry/examples/date-picker/native/birth-date.js +30 -0
- package/registry/examples/date-picker/native/birth-date.light.png +0 -0
- package/registry/examples/date-picker/native/birth-date.tsx +27 -0
- package/registry/examples/field/native/default.d.ts +4 -0
- package/registry/examples/field/native/default.dark.png +0 -0
- package/registry/examples/field/native/default.js +29 -0
- package/registry/examples/field/native/default.light.png +0 -0
- package/registry/examples/field/native/default.tsx +23 -0
- package/registry/examples/form/amount.d.ts +4 -0
- package/registry/examples/form/amount.js +30 -0
- package/registry/examples/form/amount.tsx +36 -0
- package/registry/examples/form/audience.d.ts +4 -0
- package/registry/examples/form/audience.js +29 -0
- package/registry/examples/form/audience.tsx +31 -0
- package/registry/examples/form/native/amount.d.ts +4 -0
- package/registry/examples/form/native/amount.dark.png +0 -0
- package/registry/examples/form/native/amount.js +26 -0
- package/registry/examples/form/native/amount.light.png +0 -0
- package/registry/examples/form/native/amount.tsx +30 -0
- package/registry/examples/form/native/audience.d.ts +4 -0
- package/registry/examples/form/native/audience.dark.png +0 -0
- package/registry/examples/form/native/audience.js +24 -0
- package/registry/examples/form/native/audience.light.png +0 -0
- package/registry/examples/form/native/audience.tsx +24 -0
- package/registry/examples/form/native/profile.d.ts +4 -0
- package/registry/examples/form/native/profile.dark.png +0 -0
- package/registry/examples/form/native/profile.js +33 -0
- package/registry/examples/form/native/profile.light.png +0 -0
- package/registry/examples/form/native/profile.tsx +36 -0
- package/registry/examples/form/native/settings.d.ts +4 -0
- package/registry/examples/form/native/settings.dark.png +0 -0
- package/registry/examples/form/native/settings.js +25 -0
- package/registry/examples/form/native/settings.light.png +0 -0
- package/registry/examples/form/native/settings.tsx +23 -0
- package/registry/examples/form/native/sign-up.d.ts +4 -0
- package/registry/examples/form/native/sign-up.dark.png +0 -0
- package/registry/examples/form/native/sign-up.js +38 -0
- package/registry/examples/form/native/sign-up.light.png +0 -0
- package/registry/examples/form/native/sign-up.tsx +34 -0
- package/registry/examples/form/native/upload.d.ts +4 -0
- package/registry/examples/form/native/upload.dark.png +0 -0
- package/registry/examples/form/native/upload.js +23 -0
- package/registry/examples/form/native/upload.light.png +0 -0
- package/registry/examples/form/native/upload.tsx +22 -0
- package/registry/examples/form/profile.d.ts +4 -0
- package/registry/examples/form/profile.js +35 -0
- package/registry/examples/form/profile.tsx +40 -0
- package/registry/examples/form/report.d.ts +4 -0
- package/registry/examples/form/report.js +30 -0
- package/registry/examples/form/report.tsx +30 -0
- package/registry/examples/form/settings.d.ts +4 -0
- package/registry/examples/form/settings.js +30 -0
- package/registry/examples/form/settings.tsx +31 -0
- package/registry/examples/form/sign-up.d.ts +4 -0
- package/registry/examples/form/sign-up.js +38 -0
- package/registry/examples/form/sign-up.tsx +37 -0
- package/registry/examples/form/upload.d.ts +4 -0
- package/registry/examples/form/upload.js +32 -0
- package/registry/examples/form/upload.tsx +32 -0
- package/registry/field.json +104 -4
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/form.json +2940 -0
- package/registry/gallery-page.json +1 -1
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +39 -7
- package/registry/iso.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +2 -2
- package/registry/mascots.json +2 -2
- package/registry/media-capture.json +1 -1
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +1 -1
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page.json +1 -1
- package/registry/pagination.json +1 -1
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +1 -1
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/settings-layout.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/status-page.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/video-player.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +1 -1
- package/skills/form/SKILL.md +44 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/native/profile.tsx
|
|
4
|
+
import { KrizakaQueryProvider, useMutation } from "@krizaka/query";
|
|
5
|
+
import { Button } from "@krizaka/ui/native";
|
|
6
|
+
import { createKrizakaForm } from "@krizaka/ui/native/form";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var profile = createKrizakaForm({
|
|
10
|
+
schema: z.object({ displayName: z.string().min(2).max(40), bio: z.string().max(160), category: z.enum(["travel", "music", "science", "sport"]), links: z.array(z.object({ url: z.url() })).max(3) }),
|
|
11
|
+
defaultValues: { displayName: "Elena Varga", bio: "Night skies over the Tatras.", category: "travel", links: [{ url: "https://elena.example" }] }
|
|
12
|
+
});
|
|
13
|
+
var CATEGORIES = [{ value: "travel", label: "Travel" }, { value: "music", label: "Music" }, { value: "science", label: "Science" }, { value: "sport", label: "Sport" }];
|
|
14
|
+
function ProfileForm() {
|
|
15
|
+
const mutation = useMutation({ mutationFn: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
16
|
+
const form = profile.useForm({ onSubmit: (values) => mutation.mutateAsync(values) });
|
|
17
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Your profile", style: { width: 312 }, children: [
|
|
18
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "displayName", label: "Display name", autoComplete: "name" }),
|
|
19
|
+
/* @__PURE__ */ jsx(form.TextArea, { name: "bio", label: "Bio", maxLength: 160 }),
|
|
20
|
+
/* @__PURE__ */ jsx(form.RadioGroup, { name: "category", label: "Category", options: CATEGORIES }),
|
|
21
|
+
/* @__PURE__ */ jsx(form.FieldArray, { name: "links", children: (links) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
22
|
+
links.items.map((_, i) => /* @__PURE__ */ jsx(form.TextField, { name: `links[${i}].url`, label: `Link ${i + 1}`, keyboardType: "url", autoCapitalize: "none" }, i)),
|
|
23
|
+
links.items.length < 3 ? /* @__PURE__ */ jsx(Button, { label: "Add a link", variant: "ghost", size: "sm", onPress: () => links.push({ url: "" }) }) : null
|
|
24
|
+
] }) }),
|
|
25
|
+
/* @__PURE__ */ jsx(form.Submit, { label: "Save the profile" })
|
|
26
|
+
] });
|
|
27
|
+
}
|
|
28
|
+
function NativeFormProfile() {
|
|
29
|
+
return /* @__PURE__ */ jsx(KrizakaQueryProvider, { children: /* @__PURE__ */ jsx(ProfileForm, {}) });
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
NativeFormProfile as default
|
|
33
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { KrizakaQueryProvider, useMutation } from "@krizaka/query";
|
|
2
|
+
import { Button } from "@krizaka/ui/native";
|
|
3
|
+
import { createKrizakaForm } from "@krizaka/ui/native/form";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const profile = createKrizakaForm({
|
|
7
|
+
schema: z.object({ displayName: z.string().min(2).max(40), bio: z.string().max(160), category: z.enum(["travel", "music", "science", "sport"]), links: z.array(z.object({ url: z.url() })).max(3) }),
|
|
8
|
+
defaultValues: { displayName: "Elena Varga", bio: "Night skies over the Tatras.", category: "travel" as "travel" | "music" | "science" | "sport", links: [{ url: "https://elena.example" }] },
|
|
9
|
+
});
|
|
10
|
+
const CATEGORIES = [{ value: "travel", label: "Travel" }, { value: "music", label: "Music" }, { value: "science", label: "Science" }, { value: "sport", label: "Sport" }];
|
|
11
|
+
|
|
12
|
+
function ProfileForm() {
|
|
13
|
+
const mutation = useMutation({ mutationFn: (values: unknown) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
14
|
+
const form = profile.useForm({ onSubmit: (values) => mutation.mutateAsync(values) });
|
|
15
|
+
return (
|
|
16
|
+
<form.Form aria-label="Your profile" style={{ width: 312 }}>
|
|
17
|
+
<form.TextField name="displayName" label="Display name" autoComplete="name" />
|
|
18
|
+
<form.TextArea name="bio" label="Bio" maxLength={160} />
|
|
19
|
+
<form.RadioGroup name="category" label="Category" options={CATEGORIES} />
|
|
20
|
+
<form.FieldArray name="links">
|
|
21
|
+
{(links) => (
|
|
22
|
+
<>
|
|
23
|
+
{links.items.map((_, i) => <form.TextField key={i} name={`links[${i}].url`} label={`Link ${i + 1}`} keyboardType="url" autoCapitalize="none" />)}
|
|
24
|
+
{links.items.length < 3 ? <Button label="Add a link" variant="ghost" size="sm" onPress={() => links.push({ url: "" })} /> : null}
|
|
25
|
+
</>
|
|
26
|
+
)}
|
|
27
|
+
</form.FieldArray>
|
|
28
|
+
<form.Submit label="Save the profile" />
|
|
29
|
+
</form.Form>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A profile saved by a mutation of @krizaka/query; the list of links grows.
|
|
34
|
+
export default function NativeFormProfile() {
|
|
35
|
+
return <KrizakaQueryProvider><ProfileForm /></KrizakaQueryProvider>;
|
|
36
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/native/settings.tsx
|
|
4
|
+
import { createKrizakaForm } from "@krizaka/ui/native/form";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var settings = createKrizakaForm({
|
|
8
|
+
schema: z.object({ language: z.enum(["en", "fr", "es"]), emailDigest: z.boolean(), pushMessages: z.boolean(), quality: z.enum(["auto", "data-saver", "best"]) }),
|
|
9
|
+
defaultValues: { language: "en", emailDigest: true, pushMessages: false, quality: "auto" }
|
|
10
|
+
});
|
|
11
|
+
var LANGUAGES = [{ value: "en", label: "English" }, { value: "fr", label: "Fran\xE7ais" }, { value: "es", label: "Espa\xF1ol" }];
|
|
12
|
+
var QUALITY = [{ value: "auto", label: "Automatic" }, { value: "data-saver", label: "Data saver" }, { value: "best", label: "Best" }];
|
|
13
|
+
function NativeFormSettings() {
|
|
14
|
+
const form = settings.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
15
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Settings", style: { width: 312 }, children: [
|
|
16
|
+
/* @__PURE__ */ jsx(form.Select, { name: "language", label: "Language", options: LANGUAGES }),
|
|
17
|
+
/* @__PURE__ */ jsx(form.Switch, { name: "emailDigest", label: "Weekly e-mail", hint: "What happened on your channel." }),
|
|
18
|
+
/* @__PURE__ */ jsx(form.Switch, { name: "pushMessages", label: "Message notifications" }),
|
|
19
|
+
/* @__PURE__ */ jsx(form.RadioGroup, { name: "quality", label: "Video quality", options: QUALITY }),
|
|
20
|
+
/* @__PURE__ */ jsx(form.Submit, { label: "Save the settings" })
|
|
21
|
+
] });
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
NativeFormSettings as default
|
|
25
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createKrizakaForm } from "@krizaka/ui/native/form";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const settings = createKrizakaForm({
|
|
5
|
+
schema: z.object({ language: z.enum(["en", "fr", "es"]), emailDigest: z.boolean(), pushMessages: z.boolean(), quality: z.enum(["auto", "data-saver", "best"]) }),
|
|
6
|
+
defaultValues: { language: "en" as "en" | "fr" | "es", emailDigest: true, pushMessages: false, quality: "auto" as "auto" | "data-saver" | "best" },
|
|
7
|
+
});
|
|
8
|
+
const LANGUAGES = [{ value: "en", label: "English" }, { value: "fr", label: "Français" }, { value: "es", label: "Español" }];
|
|
9
|
+
const QUALITY = [{ value: "auto", label: "Automatic" }, { value: "data-saver", label: "Data saver" }, { value: "best", label: "Best" }];
|
|
10
|
+
|
|
11
|
+
// The app's settings: the system's switches, chips for a choice — saved in one go.
|
|
12
|
+
export default function NativeFormSettings() {
|
|
13
|
+
const form = settings.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
14
|
+
return (
|
|
15
|
+
<form.Form aria-label="Settings" style={{ width: 312 }}>
|
|
16
|
+
<form.Select name="language" label="Language" options={LANGUAGES} />
|
|
17
|
+
<form.Switch name="emailDigest" label="Weekly e-mail" hint="What happened on your channel." />
|
|
18
|
+
<form.Switch name="pushMessages" label="Message notifications" />
|
|
19
|
+
<form.RadioGroup name="quality" label="Video quality" options={QUALITY} />
|
|
20
|
+
<form.Submit label="Save the settings" />
|
|
21
|
+
</form.Form>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/native/sign-up.tsx
|
|
4
|
+
import { KrizakaApiError } from "@krizaka/query";
|
|
5
|
+
import { createKrizakaForm, latestBirthDate } from "@krizaka/ui/native/form";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var signUp = createKrizakaForm({
|
|
9
|
+
schema: z.object({
|
|
10
|
+
email: z.email(),
|
|
11
|
+
username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, "signUp.username.format"),
|
|
12
|
+
password: z.string().min(8),
|
|
13
|
+
dateOfBirth: z.iso.date().refine((d) => d <= latestBirthDate(18), "form.errors.UNDERAGE"),
|
|
14
|
+
acceptTerms: z.boolean().refine(Boolean, "signUp.terms.required")
|
|
15
|
+
}),
|
|
16
|
+
defaultValues: { email: "", username: "", password: "", dateOfBirth: "", acceptTerms: false }
|
|
17
|
+
});
|
|
18
|
+
var WORDS = { "signUp.username.format": "Lowercase letters, digits and _ only.", "signUp.username.taken": "This username is taken.", "signUp.terms.required": "Accept the terms to continue.", "form.errors.UNDERAGE": "You must be 18 or older.", "form.errors.BIRTH_DATE_INVALID": "Enter your real date of birth.", "form.errors.USERNAME_RESERVED": "This username is reserved.", "apiErrors.ACCOUNT_EXISTS": "This e-mail already has an account. Sign in instead." };
|
|
19
|
+
var taken = (u) => new Promise((done) => setTimeout(() => done(["eric", "elena"].includes(u)), 300));
|
|
20
|
+
var register = async (v) => {
|
|
21
|
+
if (v.username === "admin") throw new KrizakaApiError({ status: 400, code: "USERNAME_RESERVED" });
|
|
22
|
+
if (v.email.endsWith("@orochia.com")) throw new KrizakaApiError({ status: 409, code: "ACCOUNT_EXISTS" });
|
|
23
|
+
};
|
|
24
|
+
function NativeFormSignUp() {
|
|
25
|
+
const form = signUp.useForm({ i18n: { t: (key) => WORDS[key], locale: "en-GB" }, codeFields: { UNDERAGE: "dateOfBirth", BIRTH_DATE_INVALID: "dateOfBirth", USERNAME_RESERVED: "username" }, onSubmit: register });
|
|
26
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Create your account", style: { width: 312 }, children: [
|
|
27
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "email", label: "E-mail", keyboardType: "email-address", autoComplete: "email", autoCapitalize: "none", textContentType: "emailAddress" }),
|
|
28
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "username", label: "Username", autoCapitalize: "none", autoComplete: "username", transform: (v) => v.toLowerCase().replace(/\s+/g, "_"), availableHint: "Free", validateAsync: async (u) => await taken(u) ? { key: "signUp.username.taken", suggestion: { label: `Use @${u}_${u.length}`, value: `${u}_${u.length}` } } : null }),
|
|
29
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "password", label: "Password", secureTextEntry: true, autoComplete: "new-password", textContentType: "newPassword", hint: "At least 8 characters." }),
|
|
30
|
+
/* @__PURE__ */ jsx(form.DateField, { name: "dateOfBirth", label: "Date of birth", autoComplete: "bday", hint: "Private, never shown." }),
|
|
31
|
+
/* @__PURE__ */ jsx(form.Checkbox, { name: "acceptTerms", label: "I accept the terms and the privacy policy" }),
|
|
32
|
+
/* @__PURE__ */ jsx(form.FormError, {}),
|
|
33
|
+
/* @__PURE__ */ jsx(form.Submit, { label: "Create my account", brand: "orochia" })
|
|
34
|
+
] });
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
NativeFormSignUp as default
|
|
38
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { KrizakaApiError } from "@krizaka/query";
|
|
2
|
+
import { createKrizakaForm, latestBirthDate } from "@krizaka/ui/native/form";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// The same schema as the web form (a product keeps it in packages/core): the same rules, the same keys.
|
|
6
|
+
const signUp = createKrizakaForm({
|
|
7
|
+
schema: z.object({
|
|
8
|
+
email: z.email(),
|
|
9
|
+
username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, "signUp.username.format"),
|
|
10
|
+
password: z.string().min(8),
|
|
11
|
+
dateOfBirth: z.iso.date().refine((d) => d <= latestBirthDate(18), "form.errors.UNDERAGE"),
|
|
12
|
+
acceptTerms: z.boolean().refine(Boolean, "signUp.terms.required"),
|
|
13
|
+
}),
|
|
14
|
+
defaultValues: { email: "", username: "", password: "", dateOfBirth: "", acceptTerms: false },
|
|
15
|
+
});
|
|
16
|
+
const WORDS: Record<string, string> = { "signUp.username.format": "Lowercase letters, digits and _ only.", "signUp.username.taken": "This username is taken.", "signUp.terms.required": "Accept the terms to continue.", "form.errors.UNDERAGE": "You must be 18 or older.", "form.errors.BIRTH_DATE_INVALID": "Enter your real date of birth.", "form.errors.USERNAME_RESERVED": "This username is reserved.", "apiErrors.ACCOUNT_EXISTS": "This e-mail already has an account. Sign in instead." };
|
|
17
|
+
const taken = (u: string) => new Promise<boolean>((done) => setTimeout(() => done(["eric", "elena"].includes(u)), 300));
|
|
18
|
+
const register = async (v: { email: string; username: string }) => { if (v.username === "admin") throw new KrizakaApiError({ status: 400, code: "USERNAME_RESERVED" }); if (v.email.endsWith("@orochia.com")) throw new KrizakaApiError({ status: 409, code: "ACCOUNT_EXISTS" }); };
|
|
19
|
+
|
|
20
|
+
// Sign-up in the app: the return key walks the fields, the username is checked as typed, the 18+ birth date is day · month · year.
|
|
21
|
+
export default function NativeFormSignUp() {
|
|
22
|
+
const form = signUp.useForm({ i18n: { t: (key) => WORDS[key], locale: "en-GB" }, codeFields: { UNDERAGE: "dateOfBirth", BIRTH_DATE_INVALID: "dateOfBirth", USERNAME_RESERVED: "username" }, onSubmit: register });
|
|
23
|
+
return (
|
|
24
|
+
<form.Form aria-label="Create your account" style={{ width: 312 }}>
|
|
25
|
+
<form.TextField name="email" label="E-mail" keyboardType="email-address" autoComplete="email" autoCapitalize="none" textContentType="emailAddress" />
|
|
26
|
+
<form.TextField name="username" label="Username" autoCapitalize="none" autoComplete="username" transform={(v) => v.toLowerCase().replace(/\s+/g, "_")} availableHint="Free" validateAsync={async (u) => ((await taken(u)) ? { key: "signUp.username.taken", suggestion: { label: `Use @${u}_${u.length}`, value: `${u}_${u.length}` } } : null)} />
|
|
27
|
+
<form.TextField name="password" label="Password" secureTextEntry autoComplete="new-password" textContentType="newPassword" hint="At least 8 characters." />
|
|
28
|
+
<form.DateField name="dateOfBirth" label="Date of birth" autoComplete="bday" hint="Private, never shown." />
|
|
29
|
+
<form.Checkbox name="acceptTerms" label="I accept the terms and the privacy policy" />
|
|
30
|
+
<form.FormError />
|
|
31
|
+
<form.Submit label="Create my account" brand="orochia" />
|
|
32
|
+
</form.Form>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/native/upload.tsx
|
|
4
|
+
import { createKrizakaForm } from "@krizaka/ui/native/form";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var upload = createKrizakaForm({
|
|
8
|
+
schema: z.object({ title: z.string().min(3).max(100), description: z.string().max(2e3), visibility: z.enum(["public", "followers", "private"]) }),
|
|
9
|
+
defaultValues: { title: "Sunrise over Rysy", description: "", visibility: "public" }
|
|
10
|
+
});
|
|
11
|
+
var VISIBILITY = [{ value: "public", label: "Everyone" }, { value: "followers", label: "Followers" }, { value: "private", label: "Only me" }];
|
|
12
|
+
function NativeFormUpload() {
|
|
13
|
+
const form = upload.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 600)) });
|
|
14
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Publish a video", style: { width: 312 }, children: [
|
|
15
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "title", label: "Title", maxLength: 100 }),
|
|
16
|
+
/* @__PURE__ */ jsx(form.TextArea, { name: "description", label: "Description", placeholder: "What happens in it?" }),
|
|
17
|
+
/* @__PURE__ */ jsx(form.RadioGroup, { name: "visibility", label: "Who can watch", options: VISIBILITY }),
|
|
18
|
+
/* @__PURE__ */ jsx(form.Submit, { label: "Publish" })
|
|
19
|
+
] });
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
NativeFormUpload as default
|
|
23
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createKrizakaForm } from "@krizaka/ui/native/form";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// What describes a video picked from the library (the picker is the app's: expo-image-picker): title, description, audience.
|
|
5
|
+
const upload = createKrizakaForm({
|
|
6
|
+
schema: z.object({ title: z.string().min(3).max(100), description: z.string().max(2000), visibility: z.enum(["public", "followers", "private"]) }),
|
|
7
|
+
defaultValues: { title: "Sunrise over Rysy", description: "", visibility: "public" as "public" | "followers" | "private" },
|
|
8
|
+
});
|
|
9
|
+
const VISIBILITY = [{ value: "public", label: "Everyone" }, { value: "followers", label: "Followers" }, { value: "private", label: "Only me" }];
|
|
10
|
+
|
|
11
|
+
// The details of an upload, then the button that publishes them with the file.
|
|
12
|
+
export default function NativeFormUpload() {
|
|
13
|
+
const form = upload.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 600)) });
|
|
14
|
+
return (
|
|
15
|
+
<form.Form aria-label="Publish a video" style={{ width: 312 }}>
|
|
16
|
+
<form.TextField name="title" label="Title" maxLength={100} />
|
|
17
|
+
<form.TextArea name="description" label="Description" placeholder="What happens in it?" />
|
|
18
|
+
<form.RadioGroup name="visibility" label="Who can watch" options={VISIBILITY} />
|
|
19
|
+
<form.Submit label="Publish" />
|
|
20
|
+
</form.Form>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/profile.tsx
|
|
4
|
+
import { KrizakaQueryProvider, useMutation } from "@krizaka/query";
|
|
5
|
+
import { Button } from "@krizaka/ui/button";
|
|
6
|
+
import { createKrizakaForm } from "@krizaka/ui/form";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var profile = createKrizakaForm({
|
|
10
|
+
schema: z.object({ displayName: z.string().min(2).max(40), bio: z.string().max(160), categories: z.array(z.string()).min(1).max(3), tags: z.array(z.string()).max(10), links: z.array(z.object({ url: z.url() })).max(3) }),
|
|
11
|
+
defaultValues: { displayName: "Elena Varga", bio: "Night skies over the Tatras.", categories: ["travel"], tags: ["astro", "mountains"], links: [{ url: "https://elena.example" }] }
|
|
12
|
+
});
|
|
13
|
+
var CATEGORIES = [{ value: "travel", label: "Travel" }, { value: "music", label: "Music" }, { value: "science", label: "Science" }, { value: "sport", label: "Sport" }];
|
|
14
|
+
function ProfileForm() {
|
|
15
|
+
const mutation = useMutation({ mutationFn: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
16
|
+
const form = profile.useForm({ onSubmit: (values) => mutation.mutateAsync(values) });
|
|
17
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Your profile", className: "flex w-96 flex-col gap-4", children: [
|
|
18
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "displayName", label: "Display name", autoComplete: "name" }),
|
|
19
|
+
/* @__PURE__ */ jsx(form.TextArea, { name: "bio", label: "Bio", rows: 3, maxLength: 160, hint: "Up to 160 characters." }),
|
|
20
|
+
/* @__PURE__ */ jsx(form.Combobox, { name: "categories", label: "Categories", multiple: true, options: CATEGORIES, hint: "Up to three." }),
|
|
21
|
+
/* @__PURE__ */ jsx(form.TagInput, { name: "tags", label: "Tags", max: 10, normalize: (t) => t.toLowerCase().replace(/^#/, "") }),
|
|
22
|
+
/* @__PURE__ */ jsx(form.FieldArray, { name: "links", children: (links) => /* @__PURE__ */ jsxs("fieldset", { className: "flex flex-col gap-2", children: [
|
|
23
|
+
/* @__PURE__ */ jsx("legend", { className: "text-xs font-semibold text-fg-secondary", children: "Links" }),
|
|
24
|
+
links.items.map((_, i) => /* @__PURE__ */ jsx(form.TextField, { name: `links[${i}].url`, label: `Link ${i + 1}`, type: "url" }, i)),
|
|
25
|
+
links.items.length < 3 ? /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => links.push({ url: "" }), children: "Add a link" }) : null
|
|
26
|
+
] }) }),
|
|
27
|
+
/* @__PURE__ */ jsx(form.Submit, { children: "Save the profile" })
|
|
28
|
+
] });
|
|
29
|
+
}
|
|
30
|
+
function FormProfile() {
|
|
31
|
+
return /* @__PURE__ */ jsx(KrizakaQueryProvider, { children: /* @__PURE__ */ jsx(ProfileForm, {}) });
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
FormProfile as default
|
|
35
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { KrizakaQueryProvider, useMutation } from "@krizaka/query";
|
|
4
|
+
import { Button } from "@krizaka/ui/button";
|
|
5
|
+
import { createKrizakaForm } from "@krizaka/ui/form";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
const profile = createKrizakaForm({
|
|
9
|
+
schema: z.object({ displayName: z.string().min(2).max(40), bio: z.string().max(160), categories: z.array(z.string()).min(1).max(3), tags: z.array(z.string()).max(10), links: z.array(z.object({ url: z.url() })).max(3) }),
|
|
10
|
+
defaultValues: { displayName: "Elena Varga", bio: "Night skies over the Tatras.", categories: ["travel"], tags: ["astro", "mountains"], links: [{ url: "https://elena.example" }] },
|
|
11
|
+
});
|
|
12
|
+
const CATEGORIES = [{ value: "travel", label: "Travel" }, { value: "music", label: "Music" }, { value: "science", label: "Science" }, { value: "sport", label: "Sport" }];
|
|
13
|
+
|
|
14
|
+
function ProfileForm() {
|
|
15
|
+
const mutation = useMutation({ mutationFn: (values: unknown) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
16
|
+
const form = profile.useForm({ onSubmit: (values) => mutation.mutateAsync(values) });
|
|
17
|
+
return (
|
|
18
|
+
<form.Form aria-label="Your profile" className="flex w-96 flex-col gap-4">
|
|
19
|
+
<form.TextField name="displayName" label="Display name" autoComplete="name" />
|
|
20
|
+
<form.TextArea name="bio" label="Bio" rows={3} maxLength={160} hint="Up to 160 characters." />
|
|
21
|
+
<form.Combobox name="categories" label="Categories" multiple options={CATEGORIES} hint="Up to three." />
|
|
22
|
+
<form.TagInput name="tags" label="Tags" max={10} normalize={(t) => t.toLowerCase().replace(/^#/, "")} />
|
|
23
|
+
<form.FieldArray name="links">
|
|
24
|
+
{(links) => (
|
|
25
|
+
<fieldset className="flex flex-col gap-2">
|
|
26
|
+
<legend className="text-xs font-semibold text-fg-secondary">Links</legend>
|
|
27
|
+
{links.items.map((_, i) => <form.TextField key={i} name={`links[${i}].url`} label={`Link ${i + 1}`} type="url" />)}
|
|
28
|
+
{links.items.length < 3 ? <Button type="button" size="sm" variant="ghost" onClick={() => links.push({ url: "" })}>Add a link</Button> : null}
|
|
29
|
+
</fieldset>
|
|
30
|
+
)}
|
|
31
|
+
</form.FieldArray>
|
|
32
|
+
<form.Submit>Save the profile</form.Submit>
|
|
33
|
+
</form.Form>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A profile saved by a mutation of @krizaka/query: categories, tags, a list of links that grows.
|
|
38
|
+
export default function FormProfile() {
|
|
39
|
+
return <KrizakaQueryProvider><ProfileForm /></KrizakaQueryProvider>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/report.tsx
|
|
4
|
+
import { createKrizakaForm } from "@krizaka/ui/form";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var report = createKrizakaForm({
|
|
8
|
+
schema: z.object({ reason: z.enum(["spam", "harassment", "minor", "copyright", "other"], { message: "report.reason.required" }), details: z.string().max(500), block: z.boolean() }),
|
|
9
|
+
defaultValues: { reason: "", details: "", block: false },
|
|
10
|
+
i18n: { t: (key) => key === "report.reason.required" ? "Choose what is wrong." : void 0 }
|
|
11
|
+
});
|
|
12
|
+
var REASONS = [
|
|
13
|
+
{ value: "spam", label: "Spam or a scam" },
|
|
14
|
+
{ value: "harassment", label: "Harassment" },
|
|
15
|
+
{ value: "minor", label: "It may involve a minor" },
|
|
16
|
+
{ value: "copyright", label: "It uses my work" },
|
|
17
|
+
{ value: "other", label: "Something else" }
|
|
18
|
+
];
|
|
19
|
+
function FormReport() {
|
|
20
|
+
const form = report.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
21
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Report this video", className: "flex w-80 flex-col gap-4", children: [
|
|
22
|
+
/* @__PURE__ */ jsx(form.RadioGroup, { name: "reason", label: "What is wrong?", options: REASONS }),
|
|
23
|
+
/* @__PURE__ */ jsx(form.TextArea, { name: "details", label: "Details", hint: "Optional. What should our team look at?", rows: 3, maxLength: 500 }),
|
|
24
|
+
/* @__PURE__ */ jsx(form.Checkbox, { name: "block", label: "Also block this account" }),
|
|
25
|
+
/* @__PURE__ */ jsx(form.Submit, { variant: "danger", children: "Send the report" })
|
|
26
|
+
] });
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
FormReport as default
|
|
30
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createKrizakaForm } from "@krizaka/ui/form";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const report = createKrizakaForm({
|
|
7
|
+
schema: z.object({ reason: z.enum(["spam", "harassment", "minor", "copyright", "other"], { message: "report.reason.required" }), details: z.string().max(500), block: z.boolean() }),
|
|
8
|
+
defaultValues: { reason: "" as unknown as "spam", details: "", block: false },
|
|
9
|
+
i18n: { t: (key) => (key === "report.reason.required" ? "Choose what is wrong." : undefined) },
|
|
10
|
+
});
|
|
11
|
+
const REASONS = [
|
|
12
|
+
{ value: "spam", label: "Spam or a scam" },
|
|
13
|
+
{ value: "harassment", label: "Harassment" },
|
|
14
|
+
{ value: "minor", label: "It may involve a minor" },
|
|
15
|
+
{ value: "copyright", label: "It uses my work" },
|
|
16
|
+
{ value: "other", label: "Something else" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// A report: one reason among a few, details, and blocking the account in the same step.
|
|
20
|
+
export default function FormReport() {
|
|
21
|
+
const form = report.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });
|
|
22
|
+
return (
|
|
23
|
+
<form.Form aria-label="Report this video" className="flex w-80 flex-col gap-4">
|
|
24
|
+
<form.RadioGroup name="reason" label="What is wrong?" options={REASONS} />
|
|
25
|
+
<form.TextArea name="details" label="Details" hint="Optional. What should our team look at?" rows={3} maxLength={500} />
|
|
26
|
+
<form.Checkbox name="block" label="Also block this account" />
|
|
27
|
+
<form.Submit variant="danger">Send the report</form.Submit>
|
|
28
|
+
</form.Form>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/settings.tsx
|
|
4
|
+
import { createKrizakaForm } from "@krizaka/ui/form";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var settings = createKrizakaForm({
|
|
8
|
+
schema: z.object({ language: z.enum(["en", "fr", "es"]), emailDigest: z.boolean(), pushMessages: z.boolean(), autoplayVolume: z.number().min(0).max(100), quality: z.enum(["auto", "data-saver", "best"]) }),
|
|
9
|
+
defaultValues: { language: "en", emailDigest: true, pushMessages: false, autoplayVolume: 40, quality: "auto" }
|
|
10
|
+
});
|
|
11
|
+
var LANGUAGES = [{ value: "en", label: "English" }, { value: "fr", label: "Fran\xE7ais" }, { value: "es", label: "Espa\xF1ol" }];
|
|
12
|
+
var QUALITY = [{ value: "auto", label: "Automatic" }, { value: "data-saver", label: "Data saver" }, { value: "best", label: "Best" }];
|
|
13
|
+
async function saveSettings() {
|
|
14
|
+
await new Promise((done) => setTimeout(done, 400));
|
|
15
|
+
return { ok: true };
|
|
16
|
+
}
|
|
17
|
+
function FormSettings() {
|
|
18
|
+
const form = settings.useFormWithAction(saveSettings);
|
|
19
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Settings", className: "flex w-96 flex-col gap-5", children: [
|
|
20
|
+
/* @__PURE__ */ jsx(form.Select, { name: "language", label: "Language", options: LANGUAGES }),
|
|
21
|
+
/* @__PURE__ */ jsx(form.Switch, { name: "emailDigest", label: "Weekly e-mail", hint: "What happened on your channel." }),
|
|
22
|
+
/* @__PURE__ */ jsx(form.Switch, { name: "pushMessages", label: "Message notifications" }),
|
|
23
|
+
/* @__PURE__ */ jsx(form.Slider, { name: "autoplayVolume", label: "Autoplay volume", step: 10, formatValue: (v) => `${v} %` }),
|
|
24
|
+
/* @__PURE__ */ jsx(form.RadioGroup, { name: "quality", label: "Video quality", options: QUALITY, orientation: "horizontal" }),
|
|
25
|
+
/* @__PURE__ */ jsx(form.Submit, { children: "Save the settings" })
|
|
26
|
+
] });
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
FormSettings as default
|
|
30
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createKrizakaForm, type FormActionResult } from "@krizaka/ui/form";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const settings = createKrizakaForm({
|
|
7
|
+
schema: z.object({ language: z.enum(["en", "fr", "es"]), emailDigest: z.boolean(), pushMessages: z.boolean(), autoplayVolume: z.number().min(0).max(100), quality: z.enum(["auto", "data-saver", "best"]) }),
|
|
8
|
+
defaultValues: { language: "en" as "en" | "fr" | "es", emailDigest: true, pushMessages: false, autoplayVolume: 40, quality: "auto" as "auto" | "data-saver" | "best" },
|
|
9
|
+
});
|
|
10
|
+
const LANGUAGES = [{ value: "en", label: "English" }, { value: "fr", label: "Français" }, { value: "es", label: "Español" }];
|
|
11
|
+
const QUALITY = [{ value: "auto", label: "Automatic" }, { value: "data-saver", label: "Data saver" }, { value: "best", label: "Best" }];
|
|
12
|
+
// What a Server Action answers (`"use server"` in the app): done, or a refusal the form shows.
|
|
13
|
+
async function saveSettings(): Promise<FormActionResult> {
|
|
14
|
+
await new Promise((done) => setTimeout(done, 400));
|
|
15
|
+
return { ok: true };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Settings sent by a Server Action (`useFormWithAction`): switches, a menu, a slider, a choice.
|
|
19
|
+
export default function FormSettings() {
|
|
20
|
+
const form = settings.useFormWithAction(saveSettings);
|
|
21
|
+
return (
|
|
22
|
+
<form.Form aria-label="Settings" className="flex w-96 flex-col gap-5">
|
|
23
|
+
<form.Select name="language" label="Language" options={LANGUAGES} />
|
|
24
|
+
<form.Switch name="emailDigest" label="Weekly e-mail" hint="What happened on your channel." />
|
|
25
|
+
<form.Switch name="pushMessages" label="Message notifications" />
|
|
26
|
+
<form.Slider name="autoplayVolume" label="Autoplay volume" step={10} formatValue={(v) => `${v} %`} />
|
|
27
|
+
<form.RadioGroup name="quality" label="Video quality" options={QUALITY} orientation="horizontal" />
|
|
28
|
+
<form.Submit>Save the settings</form.Submit>
|
|
29
|
+
</form.Form>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/form/sign-up.tsx
|
|
4
|
+
import { KrizakaApiError } from "@krizaka/query";
|
|
5
|
+
import { createKrizakaForm, latestBirthDate } from "@krizaka/ui/form";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var signUp = createKrizakaForm({
|
|
9
|
+
schema: z.object({
|
|
10
|
+
email: z.email(),
|
|
11
|
+
username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, "signUp.username.format"),
|
|
12
|
+
password: z.string().min(8),
|
|
13
|
+
dateOfBirth: z.iso.date().refine((d) => d <= latestBirthDate(18), "form.errors.UNDERAGE"),
|
|
14
|
+
acceptTerms: z.boolean().refine(Boolean, "signUp.terms.required")
|
|
15
|
+
}),
|
|
16
|
+
defaultValues: { email: "", username: "", password: "", dateOfBirth: "", acceptTerms: false }
|
|
17
|
+
});
|
|
18
|
+
var WORDS = { "signUp.username.format": "Lowercase letters, digits and _ only.", "signUp.username.taken": "This username is taken.", "signUp.terms.required": "Accept the terms to continue.", "form.errors.UNDERAGE": "You must be 18 or older.", "form.errors.BIRTH_DATE_INVALID": "Enter your real date of birth.", "form.errors.USERNAME_RESERVED": "This username is reserved.", "apiErrors.ACCOUNT_EXISTS": "This e-mail already has an account. Sign in instead." };
|
|
19
|
+
var taken = (u) => new Promise((done) => setTimeout(() => done(["eric", "elena"].includes(u)), 300));
|
|
20
|
+
var register = async (v) => {
|
|
21
|
+
if (v.username === "admin") throw new KrizakaApiError({ status: 400, code: "USERNAME_RESERVED" });
|
|
22
|
+
if (v.email.endsWith("@orochia.com")) throw new KrizakaApiError({ status: 409, code: "ACCOUNT_EXISTS" });
|
|
23
|
+
};
|
|
24
|
+
function FormSignUp() {
|
|
25
|
+
const form = signUp.useForm({ i18n: { t: (key) => WORDS[key] }, codeFields: { UNDERAGE: "dateOfBirth", BIRTH_DATE_INVALID: "dateOfBirth", USERNAME_RESERVED: "username" }, onSubmit: register });
|
|
26
|
+
return /* @__PURE__ */ jsxs(form.Form, { "aria-label": "Create your account", className: "flex w-80 flex-col gap-4", children: [
|
|
27
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "email", label: "E-mail", type: "email", autoComplete: "email", hint: "We send a link to confirm it." }),
|
|
28
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "username", label: "Username", start: "@", autoComplete: "username", transform: (v) => v.toLowerCase().replace(/\s+/g, "_"), availableHint: "Free", validateAsync: async (u) => await taken(u) ? { key: "signUp.username.taken", suggestion: { label: `Use @${u}_${u.length}`, value: `${u}_${u.length}` } } : null }),
|
|
29
|
+
/* @__PURE__ */ jsx(form.TextField, { name: "password", label: "Password", type: "password", autoComplete: "new-password", hint: "At least 8 characters." }),
|
|
30
|
+
/* @__PURE__ */ jsx(form.DateField, { name: "dateOfBirth", label: "Date of birth", entry: "parts", autoComplete: "bday", hint: "Private, never shown." }),
|
|
31
|
+
/* @__PURE__ */ jsx(form.Checkbox, { name: "acceptTerms", label: "I accept the terms and the privacy policy" }),
|
|
32
|
+
/* @__PURE__ */ jsx(form.FormError, {}),
|
|
33
|
+
/* @__PURE__ */ jsx(form.Submit, { children: "Create my account" })
|
|
34
|
+
] });
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
FormSignUp as default
|
|
38
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { KrizakaApiError } from "@krizaka/query";
|
|
4
|
+
import { createKrizakaForm, latestBirthDate } from "@krizaka/ui/form";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
// The schema the sign-up API parses with: the same rules, the same keys. `signUp.*` are the app's own words.
|
|
8
|
+
const signUp = createKrizakaForm({
|
|
9
|
+
schema: z.object({
|
|
10
|
+
email: z.email(),
|
|
11
|
+
username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, "signUp.username.format"),
|
|
12
|
+
password: z.string().min(8),
|
|
13
|
+
dateOfBirth: z.iso.date().refine((d) => d <= latestBirthDate(18), "form.errors.UNDERAGE"),
|
|
14
|
+
acceptTerms: z.boolean().refine(Boolean, "signUp.terms.required"),
|
|
15
|
+
}),
|
|
16
|
+
defaultValues: { email: "", username: "", password: "", dateOfBirth: "", acceptTerms: false },
|
|
17
|
+
});
|
|
18
|
+
const WORDS: Record<string, string> = { "signUp.username.format": "Lowercase letters, digits and _ only.", "signUp.username.taken": "This username is taken.", "signUp.terms.required": "Accept the terms to continue.", "form.errors.UNDERAGE": "You must be 18 or older.", "form.errors.BIRTH_DATE_INVALID": "Enter your real date of birth.", "form.errors.USERNAME_RESERVED": "This username is reserved.", "apiErrors.ACCOUNT_EXISTS": "This e-mail already has an account. Sign in instead." };
|
|
19
|
+
const taken = (u: string) => new Promise<boolean>((done) => setTimeout(() => done(["eric", "elena"].includes(u)), 300));
|
|
20
|
+
// The server's codes (Orochia's register API): the ones that belong to a field land under it, the others above the button.
|
|
21
|
+
const register = async (v: { email: string; username: string }) => { if (v.username === "admin") throw new KrizakaApiError({ status: 400, code: "USERNAME_RESERVED" }); if (v.email.endsWith("@orochia.com")) throw new KrizakaApiError({ status: 409, code: "ACCOUNT_EXISTS" }); };
|
|
22
|
+
|
|
23
|
+
// Orochia's sign-up: the username checked as it is typed (a free one offered), an 18+ birth date, the server's refusals in place.
|
|
24
|
+
export default function FormSignUp() {
|
|
25
|
+
const form = signUp.useForm({ i18n: { t: (key) => WORDS[key] }, codeFields: { UNDERAGE: "dateOfBirth", BIRTH_DATE_INVALID: "dateOfBirth", USERNAME_RESERVED: "username" }, onSubmit: register });
|
|
26
|
+
return (
|
|
27
|
+
<form.Form aria-label="Create your account" className="flex w-80 flex-col gap-4">
|
|
28
|
+
<form.TextField name="email" label="E-mail" type="email" autoComplete="email" hint="We send a link to confirm it." />
|
|
29
|
+
<form.TextField name="username" label="Username" start="@" autoComplete="username" transform={(v) => v.toLowerCase().replace(/\s+/g, "_")} availableHint="Free" validateAsync={async (u) => ((await taken(u)) ? { key: "signUp.username.taken", suggestion: { label: `Use @${u}_${u.length}`, value: `${u}_${u.length}` } } : null)} />
|
|
30
|
+
<form.TextField name="password" label="Password" type="password" autoComplete="new-password" hint="At least 8 characters." />
|
|
31
|
+
<form.DateField name="dateOfBirth" label="Date of birth" entry="parts" autoComplete="bday" hint="Private, never shown." />
|
|
32
|
+
<form.Checkbox name="acceptTerms" label="I accept the terms and the privacy policy" />
|
|
33
|
+
<form.FormError />
|
|
34
|
+
<form.Submit>Create my account</form.Submit>
|
|
35
|
+
</form.Form>
|
|
36
|
+
);
|
|
37
|
+
}
|