diff --git a/apps/main/src/lib/routes.tsx b/apps/main/src/lib/routes.tsx index 63fa415..f128ede 100644 --- a/apps/main/src/lib/routes.tsx +++ b/apps/main/src/lib/routes.tsx @@ -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: , - children: [{ index: true }, { path: "create", element: }], + children: [ + { index: true }, + { path: "create", element: }, + ], }, { path: "tasks", @@ -163,10 +168,18 @@ export const routes: RouteObject[] = [ path: "login", element: , }, + { + path: "login-v2", + element: , + }, { path: "signup", element: , }, + { + path: "signup-v2", + element: , + }, { path: "reset-password", element: , diff --git a/apps/main/src/locales/en/auth.json b/apps/main/src/locales/en/auth.json index 2931e11..0bdd438 100644 --- a/apps/main/src/locales/en/auth.json +++ b/apps/main/src/locales/en/auth.json @@ -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", diff --git a/apps/main/src/locales/fr/auth.json b/apps/main/src/locales/fr/auth.json index 6cf4069..23c45b4 100644 --- a/apps/main/src/locales/fr/auth.json +++ b/apps/main/src/locales/fr/auth.json @@ -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", diff --git a/apps/main/src/pages/login-v2.tsx b/apps/main/src/pages/login-v2.tsx new file mode 100644 index 0000000..0940d22 --- /dev/null +++ b/apps/main/src/pages/login-v2.tsx @@ -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 ; + case "dark": + return ; + case "system": + return ; + default: + return ; + } + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + login({ + email: formData.email, + password: formData.password, + }); + }; + + return ( +
+ + +
+
+
+
+ + + {t("auth:common.backHome")} + + + +
+ +
+ Xtablo + Xtablo +
+ +

+ {t("auth:login.title")} +

+

+ {t("auth:login.noAccount")}{" "} + + {t("auth:login.signupLink")} + +

+ +
+
+ + + setFormData({ ...formData, email: e.target.value }) + } + required + placeholder={t("auth:login.emailPlaceholder")} + className="h-11" + /> + {errors?.email && ( + + )} +
+ +
+ + + setFormData({ ...formData, password: e.target.value }) + } + required + placeholder={t("auth:login.passwordPlaceholder")} + className="h-11" + /> + {errors?.password && ( + + )} +
+ +
+ + {t("auth:login.forgotPassword")} + +
+ + +
+ +
+
+
+
+
+ + {t("auth:common.orContinue")} + +
+
+ + + +
+

+ © {new Date().getFullYear()} Xtablo. {t("auth:common.backHome")} +

+

+ + {t("auth:signup.termsLink")} + + | + + {t("auth:signup.privacyLink")} + +

+
+
+
+ + +
+
+ ); +} diff --git a/apps/main/src/pages/login.tsx b/apps/main/src/pages/login.tsx index 3a25a4e..ae54648 100644 --- a/apps/main/src/pages/login.tsx +++ b/apps/main/src/pages/login.tsx @@ -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", )} >
@@ -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" > - + +
+ + {t("auth:login.newExperienceLink")} + +
+
-
+
setFormData({ ...formData, email: e.target.value })} + onChange={(e) => + setFormData({ ...formData, email: e.target.value }) + } required placeholder={t("auth:login.emailPlaceholder")} /> - {errors?.email && } + {errors?.email && ( + + )}
setFormData({ ...formData, password: e.target.value })} + onChange={(e) => + setFormData({ ...formData, password: e.target.value }) + } required placeholder={t("auth:login.passwordPlaceholder")} /> - {errors?.password && } + {errors?.password && ( + + )}
@@ -209,7 +236,9 @@ export function LoginPage() {
@@ -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")} diff --git a/apps/main/src/pages/signup-v2.tsx b/apps/main/src/pages/signup-v2.tsx new file mode 100644 index 0000000..a56e68d --- /dev/null +++ b/apps/main/src/pages/signup-v2.tsx @@ -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>({}); + + 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 ; + case "dark": + return ; + case "system": + return ; + default: + return ; + } + }; + + const validateForm = () => { + const nextErrors: Record = {}; + + 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 ( +
+ + +
+
+
+
+ + + {t("auth:common.backHome")} + + + +
+ +
+ Xtablo + Xtablo +
+ +

+ {t("auth:signup.title")} +

+

+ {t("auth:signup.alreadyAccount")}{" "} + + {t("auth:signup.loginLink")} + +

+ +
+
+
+ + + setFormData({ ...formData, first_name: e.target.value }) + } + required + placeholder={t("auth:signup.firstNamePlaceholder")} + className="h-11" + /> + {errors?.first_name && ( + + )} +
+ +
+ + + setFormData({ ...formData, last_name: e.target.value }) + } + required + placeholder={t("auth:signup.lastNamePlaceholder")} + className="h-11" + /> + {errors?.last_name && ( + + )} +
+
+ +
+ + + setFormData({ ...formData, email: e.target.value }) + } + required + placeholder={t("auth:signup.emailPlaceholder")} + className="h-11" + /> + {errors?.email && ( + + )} +
+ +
+ + + setFormData({ ...formData, password: e.target.value }) + } + required + placeholder={t("auth:signup.passwordPlaceholder")} + className="h-11" + /> + {errors?.password && ( + + )} +
+ +
+ + + setFormData({ + ...formData, + confirmPassword: e.target.value, + }) + } + required + placeholder={t("auth:signup.confirmPasswordPlaceholder")} + className="h-11" + /> + {errors?.confirmPassword && ( + + )} +
+ +
+
+ setTermsAccepted(e.target.checked)} + className="mt-1 h-4 w-4 rounded border border-border bg-background" + required + /> + +
+ {errors?.terms && ( + + )} +
+ + +
+ +
+
+
+
+
+ + {t("auth:common.orContinue")} + +
+
+ + +
+
+ + +
+
+ ); +}