collective

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

diff --git a/frontend/apps/artcraft-website-next/src/app/mission/page.tsx b/frontend/apps/artcraft-website-next/src/app/mission/page.tsxnew file mode 100644--- /dev/null+++ b/frontend/apps/artcraft-website-next/src/app/mission/page.tsx@@ -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>+    </>+  );+}diff --git a/frontend/apps/artcraft-website-next/src/app/sitemap.ts b/frontend/apps/artcraft-website-next/src/app/sitemap.ts--- a/frontend/apps/artcraft-website-next/src/app/sitemap.ts+++ b/frontend/apps/artcraft-website-next/src/app/sitemap.ts@@ -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" },diff --git a/frontend/apps/artcraft-website-next/src/components/beta/beta-form.tsx b/frontend/apps/artcraft-website-next/src/components/beta/beta-form.tsx--- a/frontend/apps/artcraft-website-next/src/components/beta/beta-form.tsx+++ b/frontend/apps/artcraft-website-next/src/components/beta/beta-form.tsx@@ -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>+  );+}diff --git a/frontend/apps/artcraft-website-next/src/components/site-footer.tsx b/frontend/apps/artcraft-website-next/src/components/site-footer.tsx--- a/frontend/apps/artcraft-website-next/src/components/site-footer.tsx+++ b/frontend/apps/artcraft-website-next/src/components/site-footer.tsx@@ -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" },     ],   },diff --git a/frontend/apps/artcraft-website-next/src/components/site-nav.tsx b/frontend/apps/artcraft-website-next/src/components/site-nav.tsx--- a/frontend/apps/artcraft-website-next/src/components/site-nav.tsx+++ b/frontend/apps/artcraft-website-next/src/components/site-nav.tsx@@ -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" },diff --git a/frontend/apps/artcraft-website-next/src/lib/landing-data.ts b/frontend/apps/artcraft-website-next/src/lib/landing-data.ts--- a/frontend/apps/artcraft-website-next/src/lib/landing-data.ts+++ b/frontend/apps/artcraft-website-next/src/lib/landing-data.ts@@ -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" },