feat(auth): add v2 login/signup pages with localized copy

This commit is contained in:
Arthur Belleville 2026-02-24 12:31:25 +01:00
parent 93f8a3ef1e
commit ec62b9c341
No known key found for this signature in database
6 changed files with 742 additions and 16 deletions

View file

@ -11,6 +11,7 @@ import { FeedbackPage } from "../pages/feedback";
import { JoinPage } from "../pages/join";
import { LegalNoticePage } from "../pages/legal-notice";
import { LoginPage } from "../pages/login";
import { LoginV2Page } from "../pages/login-v2";
import { NotFoundPage } from "../pages/NotFoundPage";
// import NotesPage from "../pages/notes"; // Notes feature temporarily hidden
import { OAuthSigninPage } from "../pages/oauth-signin";
@ -19,6 +20,7 @@ import { PrivacyPolicyPage } from "../pages/privacy-policy";
import { ResetPasswordPage } from "../pages/reset-password";
import SettingsPage from "../pages/settings";
import { SignUpPage } from "../pages/signup";
import { SignUpV2Page } from "../pages/signup-v2";
import { TabloPage } from "../pages/tablo";
import { TabloDetailsPage } from "../pages/tablo-details";
import { TablosPage } from "../pages/tablos";
@ -96,7 +98,10 @@ export const routes: RouteObject[] = [
{
path: "events",
element: <EventsPage />,
children: [{ index: true }, { path: "create", element: <EventModal mode="create" /> }],
children: [
{ index: true },
{ path: "create", element: <EventModal mode="create" /> },
],
},
{
path: "tasks",
@ -163,10 +168,18 @@ export const routes: RouteObject[] = [
path: "login",
element: <LoginPage />,
},
{
path: "login-v2",
element: <LoginV2Page />,
},
{
path: "signup",
element: <SignUpPage />,
},
{
path: "signup-v2",
element: <SignUpV2Page />,
},
{
path: "reset-password",
element: <ResetPasswordPage />,

View file

@ -13,7 +13,13 @@
"forgotPassword": "Forgot password?",
"loginButton": "Log in",
"noAccount": "Don't have an account?",
"signupLink": "Sign up"
"signupLink": "Sign up",
"newExperienceLink": "Take a look at the new login experience",
"asideTitle": "Centralize your projects with Xtablo",
"asideDescription": "Plan tasks, events and collaboration from one clean workspace.",
"feature1": "Track every deadline with shared timelines",
"feature2": "Keep files, discussions and decisions in context",
"feature3": "Give your team one reliable source of truth"
},
"signup": {
"title": "Create an Xtablo account",
@ -29,6 +35,11 @@
"signupButton": "Create my account",
"alreadyAccount": "Already have an account?",
"loginLink": "Log in",
"asideTitle": "Build a calmer workflow from day one",
"asideDescription": "Create your workspace, invite your team, and keep every project in sync.",
"feature1": "Shared task boards with clear ownership",
"feature2": "Planning views for milestones and deadlines",
"feature3": "Fast collaboration around files and updates",
"termsAccept": "I accept the",
"termsLink": "legal notice",
"termsAnd": "and the",

View file

@ -13,7 +13,13 @@
"forgotPassword": "Mot de passe oublié ?",
"loginButton": "Se connecter",
"noAccount": "Pas encore de compte ?",
"signupLink": "S'inscrire"
"signupLink": "S'inscrire",
"newExperienceLink": "Découvrez la nouvelle expérience de connexion",
"asideTitle": "Centralisez vos projets avec Xtablo",
"asideDescription": "Planifiez tâches, événements et collaboration depuis un espace unique.",
"feature1": "Suivez chaque échéance avec des timelines partagées",
"feature2": "Gardez fichiers, discussions et décisions au même endroit",
"feature3": "Donnez à votre équipe une source unique de vérité"
},
"signup": {
"title": "Créer un compte Xtablo",
@ -29,6 +35,11 @@
"signupButton": "Créer mon compte",
"alreadyAccount": "Déjà un compte ?",
"loginLink": "Se connecter",
"asideTitle": "Créez un workflow plus serein dès le premier jour",
"asideDescription": "Créez votre espace, invitez votre équipe et synchronisez vos projets.",
"feature1": "Des tableaux de tâches partagés avec des responsabilités claires",
"feature2": "Des vues planning pour les jalons et échéances",
"feature3": "Une collaboration rapide autour des fichiers et mises à jour",
"termsAccept": "J'accepte les",
"termsLink": "mentions légales",
"termsAnd": "et la",

View file

@ -0,0 +1,278 @@
import { AnimatedBackground } from "@ui/components/AnimatedBackground";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
import { Button } from "@xtablo/ui/components/button";
import { FieldError } from "@xtablo/ui/components/field";
import { Input } from "@xtablo/ui/components/input";
import { Label } from "@xtablo/ui/components/label";
import {
ArrowLeftIcon,
MonitorIcon,
MoonIcon,
SparklesIcon,
SunIcon,
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useSearchParams } from "react-router-dom";
import { useLoginEmail } from "../hooks/auth";
export function LoginV2Page() {
const [searchParams] = useSearchParams();
const emailParam = searchParams.get("email");
const { t } = useTranslation(["auth", "common"]);
const redirectUrl = localStorage.getItem("redirectUrl");
const {
mutate: login,
isPending,
errors,
} = useLoginEmail({
redirectUrl: redirectUrl ?? null,
});
const [formData, setFormData] = useState({
email: emailParam ?? "",
password: "",
});
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
if (theme === "light") {
setTheme("dark");
} else if (theme === "dark") {
setTheme("system");
} else {
setTheme("light");
}
};
const getThemeIcon = () => {
switch (theme) {
case "light":
return <SunIcon className="w-5 h-5" />;
case "dark":
return <MoonIcon className="w-5 h-5" />;
case "system":
return <MonitorIcon className="w-5 h-5" />;
default:
return <SunIcon className="w-5 h-5" />;
}
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
login({
email: formData.email,
password: formData.password,
});
};
return (
<div className="relative min-h-screen overflow-hidden bg-background text-foreground">
<AnimatedBackground />
<div className="relative z-10 min-h-screen md:grid md:grid-cols-[minmax(0,560px)_1fr]">
<main className="flex min-h-screen items-center px-6 py-10 md:px-10 lg:px-14">
<div className="w-full max-w-md mx-auto">
<div className="mb-7 flex items-center justify-between">
<a
href="https://www.xtablo.com"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeftIcon className="w-4 h-4" />
{t("auth:common.backHome")}
</a>
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="text-muted-foreground hover:text-foreground"
aria-label={t("auth:common.themeToggle", { theme })}
>
{getThemeIcon()}
</Button>
</div>
<div className="mb-6">
<img
src="/logo_dark.png"
alt="Xtablo"
className="h-10 w-auto block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="h-10 w-auto hidden dark:block"
/>
</div>
<h1 className="text-3xl font-bold tracking-tight mb-2">
{t("auth:login.title")}
</h1>
<p className="text-sm text-muted-foreground mb-8">
{t("auth:login.noAccount")}{" "}
<Link
to="/signup-v2"
className="text-[#804EEC] hover:text-[#6f3fd4] font-semibold"
>
{t("auth:login.signupLink")}
</Link>
</p>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-2">
<Label htmlFor="email">
{t("common:labels.email")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
placeholder={t("auth:login.emailPlaceholder")}
className="h-11"
/>
{errors?.email && (
<FieldError errors={[{ message: errors.email }]} />
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">
{t("common:labels.password")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
required
placeholder={t("auth:login.passwordPlaceholder")}
className="h-11"
/>
{errors?.password && (
<FieldError errors={[{ message: errors.password }]} />
)}
</div>
<div className="flex items-center justify-end">
<Link
to="/reset-password"
className="text-sm text-[#804EEC] hover:text-[#6f3fd4] transition-colors"
>
{t("auth:login.forgotPassword")}
</Link>
</div>
<Button
className="w-full h-11 bg-[#804EEC] hover:bg-[#6f3fd4] text-white"
type="submit"
>
{isPending
? t("auth:common.connecting")
: t("auth:login.loginButton")}
</Button>
</form>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border" />
</div>
<div className="relative flex justify-center">
<span className="px-3 bg-background text-xs text-muted-foreground">
{t("auth:common.orContinue")}
</span>
</div>
</div>
<LoginWithGoogle />
<div className="mt-10 text-xs text-muted-foreground space-y-2">
<p>
© {new Date().getFullYear()} Xtablo. {t("auth:common.backHome")}
</p>
<p className="flex gap-3">
<Link
to="/legal-notice"
target="_blank"
className="hover:text-foreground transition-colors"
>
{t("auth:signup.termsLink")}
</Link>
<span>|</span>
<Link
to="/privacy-policy"
target="_blank"
className="hover:text-foreground transition-colors"
>
{t("auth:signup.privacyLink")}
</Link>
</p>
</div>
</div>
</main>
<aside className="hidden md:flex min-h-screen items-center justify-center px-8 lg:px-14 border-l border-border bg-gradient-to-br from-[#F5F0FF] via-[#ECE4FF] to-[#DCCEFF] dark:from-[#201933] dark:via-[#271F3E] dark:to-[#2F2548]">
<div className="relative w-full max-w-xl rounded-3xl border border-white/50 dark:border-white/10 bg-white/80 dark:bg-[#171224]/85 backdrop-blur-sm shadow-2xl p-10">
<SparklesIcon className="absolute top-5 left-5 w-5 h-5 text-[#804EEC] opacity-70" />
<SparklesIcon className="absolute top-5 right-5 w-5 h-5 text-[#804EEC] opacity-70" />
<SparklesIcon className="absolute bottom-5 left-5 w-5 h-5 text-[#804EEC] opacity-70" />
<SparklesIcon className="absolute bottom-5 right-5 w-5 h-5 text-[#804EEC] opacity-70" />
<div className="flex justify-center mb-6">
<div className="w-20 h-20 rounded-full border border-border bg-card flex items-center justify-center shadow-sm">
<img
src="/logo_dark.png"
alt="Xtablo"
className="w-12 h-12 object-contain block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-12 h-12 object-contain hidden dark:block"
/>
</div>
</div>
<h2 className="text-2xl font-bold text-center mb-3">
{t("auth:login.asideTitle")}
</h2>
<p className="text-center text-muted-foreground mb-8">
{t("auth:login.asideDescription")}
</p>
<div className="space-y-3 mb-8">
<div className="rounded-xl border border-border/70 bg-background/80 px-4 py-3 text-sm">
{t("auth:login.feature1")}
</div>
<div className="rounded-xl border border-border/70 bg-background/80 px-4 py-3 text-sm">
{t("auth:login.feature2")}
</div>
<div className="rounded-xl border border-border/70 bg-background/80 px-4 py-3 text-sm">
{t("auth:login.feature3")}
</div>
</div>
<Link
to="/signup-v2"
className="inline-flex w-full items-center justify-center rounded-xl bg-[#804EEC] px-5 py-3 text-sm font-semibold text-white hover:bg-[#6f3fd4] transition-colors"
>
{t("auth:login.signupLink")}
</Link>
</div>
</aside>
</div>
</div>
);
}

View file

@ -50,7 +50,7 @@ export function LoginPage() {
const rotateY = ((x - centerX) / centerX) * 1;
setTransform(
`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.002, 1.002, 1.002)`
`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.002, 1.002, 1.002)`,
);
setIsHovered(true);
};
@ -101,7 +101,7 @@ export function LoginPage() {
ref={cardRef}
className={twMerge(
"w-full max-w-lg rounded-2xl relative",
"transition-transform duration-200 ease-out will-change-transform"
"transition-transform duration-200 ease-out will-change-transform",
)}
style={{ transform }}
onMouseMove={handleMouseMove}
@ -116,7 +116,7 @@ export function LoginPage() {
"relative w-full h-full p-8 bg-card/80 backdrop-blur-md rounded-2xl border border-border z-10 transition-shadow duration-200",
isHovered
? "shadow-[0_15px_35px_rgba(0,0,0,0.15)] dark:shadow-[0_15px_35px_rgba(0,0,0,0.3)]"
: "shadow-xl shadow-black/10 dark:shadow-black/25"
: "shadow-xl shadow-black/10 dark:shadow-black/25",
)}
>
<div className="mb-6 flex items-center justify-between">
@ -124,7 +124,12 @@ export function LoginPage() {
href="https://www.xtablo.com"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -165,38 +170,60 @@ export function LoginPage() {
{t("auth:login.title")}
</h1>
<div className="mb-6 text-center">
<Link
to="/login-v2"
className="inline-flex items-center text-sm text-[#804EEC] hover:text-[#6f3fd4] font-medium transition-colors"
>
{t("auth:login.newExperienceLink")}
</Link>
</div>
<div className="space-y-4 flex flex-col items-center">
<form className="space-y-4 w-95 max-w-md mx-auto" onSubmit={onSubmit}>
<form
className="space-y-4 w-95 max-w-md mx-auto"
onSubmit={onSubmit}
>
<div className="space-y-2">
<Label htmlFor="email">
{t("common:labels.email")} <span className="text-red-500">*</span>
{t("common:labels.email")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
placeholder={t("auth:login.emailPlaceholder")}
/>
{errors?.email && <FieldError errors={[{ message: errors.email }]} />}
{errors?.email && (
<FieldError errors={[{ message: errors.email }]} />
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">
{t("common:labels.password")} <span className="text-red-500">*</span>
{t("common:labels.password")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
required
placeholder={t("auth:login.passwordPlaceholder")}
/>
{errors?.password && <FieldError errors={[{ message: errors.password }]} />}
{errors?.password && (
<FieldError errors={[{ message: errors.password }]} />
)}
</div>
<div className="flex items-center justify-end">
@ -209,7 +236,9 @@ export function LoginPage() {
</div>
<Button className="w-full" type="submit">
{isPending ? t("auth:common.connecting") : t("auth:login.loginButton")}
{isPending
? t("auth:common.connecting")
: t("auth:login.loginButton")}
</Button>
</form>
@ -226,7 +255,7 @@ export function LoginPage() {
"rounded-full",
"relative z-10",
"before:absolute before:w-[100px] before:h-px before:bg-border before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-px after:bg-border after:right-[-110px] after:top-1/2"
"after:absolute after:w-[100px] after:h-px after:bg-border after:right-[-110px] after:top-1/2",
)}
>
{t("auth:common.orContinue")}

View file

@ -0,0 +1,384 @@
import { AnimatedBackground } from "@ui/components/AnimatedBackground";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
import { Button } from "@xtablo/ui/components/button";
import { FieldError } from "@xtablo/ui/components/field";
import { Input } from "@xtablo/ui/components/input";
import { Label } from "@xtablo/ui/components/label";
import {
ArrowLeftIcon,
MonitorIcon,
MoonIcon,
SparklesIcon,
SunIcon,
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useSignUp } from "../hooks/auth";
export function SignUpV2Page() {
const { t } = useTranslation(["auth", "common"]);
const redirectUrl = localStorage.getItem("redirectUrl");
const { mutate: signUp, isPending } = useSignUp({
redirectUrl: redirectUrl ?? null,
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [formData, setFormData] = useState({
email: "",
password: "",
confirmPassword: "",
username: "",
first_name: "",
last_name: "",
business_name: "",
});
const [termsAccepted, setTermsAccepted] = useState(false);
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
if (theme === "light") {
setTheme("dark");
} else if (theme === "dark") {
setTheme("system");
} else {
setTheme("light");
}
};
const getThemeIcon = () => {
switch (theme) {
case "light":
return <SunIcon className="w-5 h-5" />;
case "dark":
return <MoonIcon className="w-5 h-5" />;
case "system":
return <MonitorIcon className="w-5 h-5" />;
default:
return <SunIcon className="w-5 h-5" />;
}
};
const validateForm = () => {
const nextErrors: Record<string, string> = {};
if (formData.password.length < 8) {
nextErrors.password = t("auth:signup.errors.passwordLength");
}
if (formData.password !== formData.confirmPassword) {
nextErrors.confirmPassword = t("auth:signup.errors.passwordMatch");
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
nextErrors.email = t("auth:signup.errors.invalidEmail");
}
if (!termsAccepted) {
nextErrors.terms = t("auth:signup.errors.termsRequired");
}
return Object.keys(nextErrors).length === 0 ? null : nextErrors;
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
const validationErrors = validateForm();
if (validationErrors) {
setErrors(validationErrors);
return;
}
setErrors({});
signUp({
email: formData.email,
password: formData.password,
first_name: formData.first_name,
last_name: formData.last_name,
confirm_password: formData.confirmPassword,
business_name: formData.business_name,
});
};
return (
<div className="relative min-h-screen overflow-hidden bg-background text-foreground">
<AnimatedBackground />
<div className="relative z-10 min-h-screen md:grid md:grid-cols-[minmax(0,640px)_1fr]">
<main className="flex min-h-screen items-center px-6 py-10 md:px-10 lg:px-14">
<div className="w-full max-w-lg mx-auto">
<div className="mb-6 flex items-center justify-between">
<a
href="https://www.xtablo.com"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeftIcon className="w-4 h-4" />
{t("auth:common.backHome")}
</a>
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="text-muted-foreground hover:text-foreground"
aria-label={t("auth:common.themeToggle", { theme })}
>
{getThemeIcon()}
</Button>
</div>
<div className="mb-5">
<img
src="/logo_dark.png"
alt="Xtablo"
className="h-10 w-auto block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="h-10 w-auto hidden dark:block"
/>
</div>
<h1 className="text-3xl font-bold tracking-tight mb-2">
{t("auth:signup.title")}
</h1>
<p className="text-sm text-muted-foreground mb-7">
{t("auth:signup.alreadyAccount")}{" "}
<Link
to="/login-v2"
className="text-[#804EEC] hover:text-[#6f3fd4] font-semibold"
>
{t("auth:signup.loginLink")}
</Link>
</p>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="first_name">
{t("auth:signup.firstName")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="first_name"
name="first_name"
type="text"
value={formData.first_name}
onChange={(e) =>
setFormData({ ...formData, first_name: e.target.value })
}
required
placeholder={t("auth:signup.firstNamePlaceholder")}
className="h-11"
/>
{errors?.first_name && (
<FieldError errors={[{ message: errors.first_name }]} />
)}
</div>
<div className="space-y-2">
<Label htmlFor="last_name">
{t("auth:signup.lastName")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="last_name"
name="last_name"
type="text"
value={formData.last_name}
onChange={(e) =>
setFormData({ ...formData, last_name: e.target.value })
}
required
placeholder={t("auth:signup.lastNamePlaceholder")}
className="h-11"
/>
{errors?.last_name && (
<FieldError errors={[{ message: errors.last_name }]} />
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">
{t("auth:signup.email")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
placeholder={t("auth:signup.emailPlaceholder")}
className="h-11"
/>
{errors?.email && (
<FieldError errors={[{ message: errors.email }]} />
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">
{t("common:labels.password")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
required
placeholder={t("auth:signup.passwordPlaceholder")}
className="h-11"
/>
{errors?.password && (
<FieldError errors={[{ message: errors.password }]} />
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">
{t("auth:signup.confirmPassword")}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={(e) =>
setFormData({
...formData,
confirmPassword: e.target.value,
})
}
required
placeholder={t("auth:signup.confirmPasswordPlaceholder")}
className="h-11"
/>
{errors?.confirmPassword && (
<FieldError errors={[{ message: errors.confirmPassword }]} />
)}
</div>
<div className="space-y-2">
<div className="flex items-start gap-2">
<input
type="checkbox"
id="terms"
name="terms"
checked={termsAccepted}
onChange={(e) => setTermsAccepted(e.target.checked)}
className="mt-1 h-4 w-4 rounded border border-border bg-background"
required
/>
<Label
htmlFor="terms"
className="text-xs text-muted-foreground leading-relaxed"
>
{t("auth:signup.termsAccept")}{" "}
<Link
to="/legal-notice"
target="_blank"
className="text-foreground hover:text-foreground/80 underline"
>
{t("auth:signup.termsLink")}
</Link>{" "}
{t("auth:signup.termsAnd")}{" "}
<Link
to="/privacy-policy"
target="_blank"
className="text-foreground hover:text-foreground/80 underline"
>
{t("auth:signup.privacyLink")}
</Link>
</Label>
</div>
{errors?.terms && (
<FieldError errors={[{ message: errors.terms }]} />
)}
</div>
<Button
className="w-full h-11 bg-[#804EEC] hover:bg-[#6f3fd4] text-white"
type="submit"
>
{isPending
? t("auth:common.creatingAccount")
: t("auth:signup.signupButton")}
</Button>
</form>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border" />
</div>
<div className="relative flex justify-center">
<span className="px-3 bg-background text-xs text-muted-foreground">
{t("auth:common.orContinue")}
</span>
</div>
</div>
<LoginWithGoogle />
</div>
</main>
<aside className="hidden md:flex min-h-screen items-center justify-center px-8 lg:px-14 border-l border-border bg-gradient-to-br from-[#F5F0FF] via-[#ECE4FF] to-[#DCCEFF] dark:from-[#201933] dark:via-[#271F3E] dark:to-[#2F2548]">
<div className="relative w-full max-w-xl rounded-3xl border border-white/50 dark:border-white/10 bg-white/80 dark:bg-[#171224]/85 backdrop-blur-sm shadow-2xl p-10">
<SparklesIcon className="absolute top-5 left-5 w-5 h-5 text-[#804EEC] opacity-70" />
<SparklesIcon className="absolute top-5 right-5 w-5 h-5 text-[#804EEC] opacity-70" />
<SparklesIcon className="absolute bottom-5 left-5 w-5 h-5 text-[#804EEC] opacity-70" />
<SparklesIcon className="absolute bottom-5 right-5 w-5 h-5 text-[#804EEC] opacity-70" />
<div className="flex justify-center mb-6">
<div className="w-20 h-20 rounded-full border border-border bg-card flex items-center justify-center shadow-sm">
<img
src="/logo_dark.png"
alt="Xtablo"
className="w-12 h-12 object-contain block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-12 h-12 object-contain hidden dark:block"
/>
</div>
</div>
<h2 className="text-2xl font-bold text-center mb-3">
{t("auth:signup.asideTitle")}
</h2>
<p className="text-center text-muted-foreground mb-8">
{t("auth:signup.asideDescription")}
</p>
<div className="space-y-3">
<div className="rounded-xl border border-border/70 bg-background/80 px-4 py-3 text-sm">
{t("auth:signup.feature1")}
</div>
<div className="rounded-xl border border-border/70 bg-background/80 px-4 py-3 text-sm">
{t("auth:signup.feature2")}
</div>
<div className="rounded-xl border border-border/70 bg-background/80 px-4 py-3 text-sm">
{t("auth:signup.feature3")}
</div>
</div>
</div>
</aside>
</div>
</div>
);
}