Browse / github.com/storytold/artcraft-services / patch
Add mission page and update beta signup form
Add mission page and update beta signup form ## Summary Marketing pages for `artcraft-website-next`: - **Mission page** (`/mission`, new): headline, three principles, the landing page's existing Ownership section, and a CTA linking to the beta form and Discord. - **Beta signup form** (`/beta`): fields are now Name, Email, and Personal or Company. Choosing Company reveals an optional company name and a required company size (1–10, 11–50, 51–200, 201–1,000, 1,000+). - **Navigation:** Mission and Beta added to the nav Resources menu and the footer; `/mission` added to the sitemap and scroll ruler. ## Google Form No changes needed
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- bflatastic (imported from pull request #133, open)
- Base
28bd2e7ee349ded3d65e81fc58929e05f24cb836- Size
- +208 −68 in 6 files
- Review
- pending
- Shared
- 2026-10-28
- Id
1643b64ac565e2c86042b5fb
How it went
No other operator has reported using it yet.
Diff
raw · frontend/apps/artcraft-website-next/src/app/mission/page.tsx frontend/apps/artcraft-website-next/src/app/sitemap.ts frontend/apps/artcraft-website-next/src/components/beta/beta-form.tsx frontend/apps/artcraft-website-next/src/components/site-footer.tsx frontend/apps/artcraft-website-next/src/components/site-nav.tsx frontend/apps/artcraft-website-next/src/lib/landing-data.ts
new file mode 100644@@ -0,0 +1,90 @@+import type { Metadata } from "next";+import DiscordButton from "@/components/discord-button";+import RevealManager from "@/components/reveal-manager";+import Ownership from "@/components/landing/ownership";+import { SectionShell, SectionEyebrow } from "@/components/landing/section-shell";+import { Accent, PageHeader } from "@/components/page/page-header";+import { Button } from "@/components/ui";++export const metadata: Metadata = {+ title: "Our Mission",+ description:+ "ArtCraft exists to put artists in control of AI image and video. An open-source studio you own, built in the open with the creators who use it.",+ alternates: { canonical: "/mission" },+ openGraph: { title: "The ArtCraft Mission" },+};++const BELIEFS = [+ {+ title: "Control over chance",+ body: "Typing a prompt and hoping isn't a creative process. Pose the characters, frame the camera, compose the shot, and let the model render what you meant.",+ },+ {+ title: "Every model, one studio",+ body: "The best models change every few months. Your workflow shouldn't. Bring each new one into the same canvas instead of rebuilding your process around it.",+ },+ {+ title: "Built with artists",+ body: "We ship early, test with our community, and let working creators decide what comes next. The people using ArtCraft shape it.",+ },+];++export default function MissionPage() {+ return (+ <>+ <RevealManager />++ <PageHeader+ id="mission"+ index="01"+ label="Mission"+ annotation="Made by artists, for artists"+ title={+ <>+ Put artists back in <Accent>control</Accent>.+ </>+ }+ lede="AI can now make an image or a shot in seconds. We're building the studio that lets artists decide exactly what ends up on screen, and own the tools they make it with."+ />++ <SectionShell id="principles">+ <SectionEyebrow index="02" label="What we believe" annotation="Three principles" />+ <ol data-reveal-group className="grid gap-px bg-line md:grid-cols-3">+ {BELIEFS.map((belief, i) => (+ <li key={belief.title} data-reveal className="bg-bg p-6 md:p-8">+ <p className="hud-label text-faint">{String(i + 1).padStart(2, "0")}</p>+ <h2 className="mt-6 font-display text-2xl font-medium tracking-[-0.02em] text-ink-strong">+ {belief.title}+ </h2>+ <p className="mt-2 leading-relaxed text-muted">{belief.body}</p>+ </li>+ ))}+ </ol>+ </SectionShell>++ <Ownership index="03" />++ <SectionShell id="community">+ <div className="flex flex-col items-center px-6 py-16 text-center md:py-24">+ <p className="hud-label text-faint">Join us</p>+ <h2+ data-reveal+ className="mt-6 max-w-2xl font-display text-4xl font-medium leading-[1.02] tracking-[-0.035em] text-ink-strong sm:text-5xl"+ >+ Help us <Accent>build</Accent> it.+ </h2>+ <p className="mt-5 max-w-lg leading-relaxed text-muted">+ Beta testers get new tools before anyone else and a direct line to+ the team building them.+ </p>+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">+ <Button href="/beta" size="lg">+ Join the beta+ </Button>+ <DiscordButton variant="secondary" size="lg" />+ </div>+ </div>+ </SectionShell>+ </>+ );+}@@ -15,6 +15,7 @@ const STATIC_ROUTES: { path: string; priority: number; changeFrequency: Entry["c { path: "/support", priority: 0.5, changeFrequency: "monthly" }, { path: "/press-kit", priority: 0.5, changeFrequency: "monthly" }, { path: "/beta", priority: 0.5, changeFrequency: "monthly" },+ { path: "/mission", priority: 0.5, changeFrequency: "monthly" }, { path: "/seedance-2", priority: 0.8, changeFrequency: "weekly" }, { path: "/seedance2-5", priority: 0.8, changeFrequency: "weekly" }, { path: "/minimax-h3", priority: 0.8, changeFrequency: "weekly" },@@ -22,19 +22,24 @@ const FORM_FIELD_IDS = { referrer: "entry.617211358", } as const; -const USER_TYPES = ["Business", "Hobbyist", "Film maker", "Agency", "Other"] as const;-type UserType = (typeof USER_TYPES)[number];+// Every Google Form field is free text, so these map onto the existing+// "Type" / "Type Detail" columns without touching the form.+const ACCOUNT_TYPES = ["Personal", "Company"] as const;+type AccountType = (typeof ACCOUNT_TYPES)[number];++const COMPANY_SIZES = ["1–10", "11–50", "51–200", "201–1,000", "1,000+"] as const;+type CompanySize = (typeof COMPANY_SIZES)[number]; const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -type FieldErrors = Partial<Record<"name" | "email" | "type" | "typeOther", string>>;+type FieldErrors = Partial<Record<"name" | "email" | "accountType" | "companySize", string>>; export default function BetaForm() { const [name, setName] = useState(""); const [company, setCompany] = useState(""); const [email, setEmail] = useState("");- const [userType, setUserType] = useState<UserType | null>(null);- const [typeOther, setTypeOther] = useState("");+ const [accountType, setAccountType] = useState<AccountType | null>(null);+ const [companySize, setCompanySize] = useState<CompanySize | null>(null); // Honeypot: hidden from humans; bots that fill it are silently dropped. const [website, setWebsite] = useState(""); @@ -50,9 +55,9 @@ export default function BetaForm() { else if (!EMAIL_PATTERN.test(email.trim())) { next.email = "That doesn't look like a valid email address."; }- if (userType === null) next.type = "Please pick the option closest to you.";- else if (userType === "Other" && !typeOther.trim()) {- next.typeOther = "Tell us a little about what you do.";+ if (accountType === null) next.accountType = "Please choose personal or company.";+ else if (accountType === "Company" && companySize === null) {+ next.companySize = "Please pick your company size."; } return next; };@@ -70,21 +75,22 @@ export default function BetaForm() { return; } + const isCompany = accountType === "Company"; setLoading(true); try { const body = new URLSearchParams({ [FORM_FIELD_IDS.name]: name.trim(),- [FORM_FIELD_IDS.company]: company.trim(),+ [FORM_FIELD_IDS.company]: isCompany ? company.trim() : "", [FORM_FIELD_IDS.email]: email.trim(),- [FORM_FIELD_IDS.type]: userType ?? "",- [FORM_FIELD_IDS.typeDetail]: userType === "Other" ? typeOther.trim() : "",+ [FORM_FIELD_IDS.type]: accountType ?? "",+ [FORM_FIELD_IDS.typeDetail]: isCompany ? (companySize ?? "") : "", [FORM_FIELD_IDS.page]: window.location.href, [FORM_FIELD_IDS.referrer]: document.referrer, }); // Google Forms sends no CORS headers, so this is fire-and-forget: the // response is opaque and "request completed" counts as success. await fetch(FORM_RESPONSE_URL, { method: "POST", mode: "no-cors", body });- trackEvent("generate_lead", { form_name: "beta", user_type: userType ?? "" });+ trackEvent("generate_lead", { form_name: "beta", user_type: accountType ?? "" }); setSubmitted(true); } catch { setSubmitError(@@ -95,9 +101,14 @@ export default function BetaForm() { } }; - const selectType = (type: UserType) => {- setUserType(type);- setErrors((prev) => ({ ...prev, type: undefined, typeOther: undefined }));+ const selectAccountType = (type: AccountType) => {+ setAccountType(type);+ setErrors((prev) => ({ ...prev, accountType: undefined, companySize: undefined }));+ };++ const selectCompanySize = (size: CompanySize) => {+ setCompanySize(size);+ setErrors((prev) => ({ ...prev, companySize: undefined })); }; if (submitted) {@@ -142,16 +153,6 @@ export default function BetaForm() { errorMessage={errors.name} /> - <Input- id="beta-company"- label="Company (optional)"- type="text"- autoComplete="organization"- value={company}- onChange={(e) => setCompany(e.target.value)}- placeholder="Studio or company name"- />- <Input id="beta-email" label="Email"@@ -164,50 +165,35 @@ export default function BetaForm() { errorMessage={errors.email} /> - <div className="flex flex-col">- <Label htmlFor="beta-type-Business">I am a…</Label>- <div className="flex flex-wrap gap-px bg-line" role="radiogroup" aria-label="I am a…">- {USER_TYPES.map((type) => {- const selected = userType === type;- return (- <button- key={type}- id={`beta-type-${type}`}- type="button"- role="radio"- aria-checked={selected}- onClick={() => selectType(type)}- className={twMerge(- "hud-label h-10 px-4 transition-colors",- selected- ? "bg-invert-bg text-invert-fg"- : "bg-bg text-muted hover:bg-bg-raised hover:text-ink",- )}- >- {type}- </button>- );- })}- </div>- {errors.type && (- <p className="mt-1 font-mono text-[10px] uppercase tracking-[0.1em] text-danger">- {errors.type}- </p>- )}- </div>+ <ChoiceGroup+ id="beta-account-type"+ label="Signing up as"+ options={ACCOUNT_TYPES}+ value={accountType}+ onChange={selectAccountType}+ error={errors.accountType}+ /> - {userType === "Other" && (- <Input- id="beta-type-other"- label="What best describes you?"- type="text"- value={typeOther}- onChange={(e) => setTypeOther(e.target.value)}- placeholder="Game developer, educator, …"- autoFocus- isError={!!errors.typeOther}- errorMessage={errors.typeOther}- />+ {accountType === "Company" && (+ <>+ <Input+ id="beta-company"+ label="Company name (optional)"+ type="text"+ autoComplete="organization"+ value={company}+ onChange={(e) => setCompany(e.target.value)}+ placeholder="Studio or company name"+ />+ <ChoiceGroup+ id="beta-company-size"+ label="Company size"+ options={COMPANY_SIZES}+ value={companySize}+ onChange={selectCompanySize}+ error={errors.companySize}+ />+ </> )} <div aria-hidden="true" className="absolute -left-[9999px] top-auto h-px w-px overflow-hidden">@@ -235,3 +221,61 @@ export default function BetaForm() { </form> ); }++// Single-select row of squared pills, ARIA radio semantics.+function ChoiceGroup<T extends string>({+ id,+ label,+ options,+ value,+ onChange,+ error,+}: {+ id: string;+ label: string;+ options: readonly T[];+ value: T | null;+ onChange: (value: T) => void;+ error?: string;+}) {+ const labelId = `${id}-label`;+ const errorId = `${id}-error`;+ return (+ <div className="flex flex-col">+ <Label id={labelId}>{label}</Label>+ <div+ className="flex flex-wrap gap-px bg-line"+ role="radiogroup"+ aria-labelledby={labelId}+ aria-invalid={!!error}+ aria-describedby={error ? errorId : undefined}+ >+ {options.map((option) => {+ const selected = value === option;+ return (+ <button+ key={option}+ type="button"+ role="radio"+ aria-checked={selected}+ onClick={() => onChange(option)}+ className={twMerge(+ "hud-label h-10 flex-1 whitespace-nowrap px-4 transition-colors",+ selected+ ? "bg-invert-bg text-invert-fg"+ : "bg-bg text-muted hover:bg-bg-raised hover:text-ink",+ )}+ >+ {option}+ </button>+ );+ })}+ </div>+ {error && (+ <p id={errorId} className="mt-1 font-mono text-[10px] uppercase tracking-[0.1em] text-danger">+ {error}+ </p>+ )}+ </div>+ );+}@@ -31,6 +31,8 @@ const LINK_COLUMNS: { { name: "News", href: "/news" }, { name: "FAQ", href: "/faq" }, { name: "Press Kit", href: "/press-kit" },+ { name: "Mission", href: "/mission" },+ { name: "Beta", href: "/beta" }, { name: "Support", href: "/support" }, ], },@@ -28,6 +28,8 @@ const NAV_ITEMS: NavEntry[] = [ { name: "News", href: "/news" }, { name: "FAQ", href: "/faq" }, { name: "Press Kit", href: "/press-kit" },+ { name: "Mission", href: "/mission" },+ { name: "Beta", href: "/beta" }, ], }, { name: "Download", href: "/download" },@@ -156,6 +156,7 @@ export const RULER_SECTIONS: RulerSection[] = [ { id: "support", label: "SUPPORT" }, { id: "press-kit", label: "PRESS KIT" }, { id: "beta", label: "BETA" },+ { id: "mission", label: "MISSION" }, { id: "plans", label: "PLANS" }, { id: "enterprise", label: "ENTERPRISE" }, { id: "credits", label: "CREDITS" },