All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
Translation quality & format preservation: - Word: merge adjacent same-format runs into one unit (sentence-level coherence like inline-tag handling); translate comments/balloons; dedupe textbox collection (was translated twice); RTL no longer overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs) - PPTX: chart translations now actually reach the output file (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP); CJK typeface hints (a:ea) - Excel: sheet renames no longer break references — rewrite cell formulas (3D/quoted), defined names, data validations, cond. formats - PDF: bold/italic honored (hebo/heit/hebi); table cells never merge; unchanged blocks left untouched (typography preserved, fixes duplicate hyperlinks); attempted/changed stats + route gate now cover PDF; CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings) Features: - formality param (formal/informal) + automatic regional-variant prompts - output_mode=bilingual docx (source above translation) - per-user translation memory on Redis (falls back to LRU), context-hashed - QA report + 0-100 confidence score in job status; L0 on by default - OpenAI-compatible providers: whole chunk in ONE numbered-JSON request (~15x fewer calls) with per-item fallback; base prompt always present (custom prompt no longer replaces translation instructions) Infra & marketing alignment: - plan-based engine gating + vision gating (closes paid-engine leak); /providers/available filtered per plan; 107 languages exposed - zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash) - admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin (TestClient breakage); Prometheus test fixture fixed - marketing docs aligned with code (PDF+OCR, retention, engines, pricing) - security: .env.ionos/.env.production/provider_settings.json removed Tests: 1173 passed / 0 failed (6 network tests deselected: free Google endpoint temporarily blocked from this machine)
142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useCallback } from "react"
|
|
import { Mail, Loader2, CheckCircle2, ArrowRight } from "lucide-react"
|
|
import { track } from "@vercel/analytics"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
type Status = "idle" | "loading" | "success" | "error"
|
|
|
|
// The backend is proxied under the same origin via the rewrites in
|
|
// next.config.mjs, so the form never needs the backend origin (no CORS).
|
|
const WAITLIST_ENDPOINT = "/api/v1/waitlist"
|
|
|
|
const INTERESTS = [
|
|
"I translate documents regularly",
|
|
"I run a translation agency",
|
|
"I manage a multilingual team",
|
|
"I'm a developer (API integration)",
|
|
"Just exploring",
|
|
]
|
|
|
|
export function WaitlistSection() {
|
|
const [email, setEmail] = useState("")
|
|
const [interest, setInterest] = useState(INTERESTS[0])
|
|
const [status, setStatus] = useState<Status>("idle")
|
|
const [message, setMessage] = useState("")
|
|
|
|
const submit = useCallback(
|
|
async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!email || status === "loading") return
|
|
setStatus("loading")
|
|
setMessage("")
|
|
try {
|
|
const res = await fetch(`/api/v1/waitlist`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, interest }),
|
|
})
|
|
const data = await res.json().catch(() => ({}))
|
|
if (res.ok) {
|
|
setStatus("success")
|
|
track("waitlist_joined", { interest })
|
|
setMessage(
|
|
data?.data?.status === "already_joined"
|
|
? "You are already on the list. We will email you at launch."
|
|
: "You are on the list. We will email you at launch."
|
|
)
|
|
setEmail("")
|
|
} else {
|
|
setStatus("error")
|
|
track("waitlist_error", { status: res.status })
|
|
setMessage(data?.message ?? "Something went wrong. Please try again.")
|
|
}
|
|
} catch {
|
|
setStatus("error")
|
|
setMessage("Network error. Please try again later.")
|
|
}
|
|
},
|
|
[email, interest, status]
|
|
)
|
|
|
|
return (
|
|
<section
|
|
id="waitlist"
|
|
className="flex flex-col items-center gap-6 px-6 py-16 md:py-20"
|
|
>
|
|
<div className="flex w-full max-w-2xl flex-col items-center gap-4 rounded-2xl border border-border bg-card px-6 py-10 text-center shadow-lg md:px-12">
|
|
<div className="flex size-12 items-center justify-center rounded-xl bg-primary">
|
|
<Mail className="size-6 text-primary-foreground" />
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<h2 className="text-balance text-2xl font-bold tracking-tight text-foreground md:text-3xl">
|
|
Be first in line at launch
|
|
</h2>
|
|
<p className="text-pretty text-sm text-muted-foreground">
|
|
Join the waitlist and get early access, launch-day pricing and the
|
|
“keep your format” playbook. No spam, one email at launch.
|
|
</p>
|
|
</div>
|
|
|
|
{status === "success" ? (
|
|
<div className="flex w-full max-w-md flex-col items-center gap-2 rounded-lg border border-success/40 bg-success/10 px-5 py-4">
|
|
<CheckCircle2 className="size-5 text-success" />
|
|
<p className="text-sm font-medium text-foreground">{message}</p>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={submit} className="flex w-full max-w-md flex-col gap-3">
|
|
<div className="flex flex-col gap-3 sm:flex-row">
|
|
<Input
|
|
type="email"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="you@company.com"
|
|
aria-label="Email address"
|
|
className="flex-1"
|
|
disabled={status === "loading"}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
size="lg"
|
|
disabled={status === "loading" || !email}
|
|
>
|
|
{status === "loading" ? (
|
|
<>
|
|
<Loader2 className="size-4 animate-spin" /> Joining…
|
|
</>
|
|
) : (
|
|
<>
|
|
Join Waitlist <ArrowRight className="size-4" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<select
|
|
value={interest}
|
|
onChange={(e) => setInterest(e.target.value)}
|
|
aria-label="What best describes you?"
|
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring/50"
|
|
>
|
|
{INTERESTS.map((opt) => (
|
|
<option key={opt} value={opt}>
|
|
{opt}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{status === "error" && (
|
|
<p className="text-xs text-destructive">{message}</p>
|
|
)}
|
|
<p className="text-[11px] text-muted-foreground">
|
|
We only use your address to notify you at launch. Unsubscribe any
|
|
time.
|
|
</p>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|