feat(auth): add v2 login/signup pages with localized copy
This commit is contained in:
parent
93f8a3ef1e
commit
ec62b9c341
6 changed files with 742 additions and 16 deletions
|
|
@ -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 />,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
278
apps/main/src/pages/login-v2.tsx
Normal file
278
apps/main/src/pages/login-v2.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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")}
|
||||
|
|
|
|||
384
apps/main/src/pages/signup-v2.tsx
Normal file
384
apps/main/src/pages/signup-v2.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue