Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d83733af02 |
No files matched your search
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user