diff --git a/apps/web/src/components/auth/AuthModal.tsx b/apps/web/src/components/auth/AuthModal.tsx new file mode 100644 index 0000000..e4d1331 --- /dev/null +++ b/apps/web/src/components/auth/AuthModal.tsx @@ -0,0 +1,246 @@ +import { useState, useEffect } from "react"; +import { toast } from "sonner"; +import { authClient } from "../../lib/auth-client"; + +type Tab = "login" | "register"; + +type AuthModalProps = { onClose: () => void }; + +type LoginFormProps = { onSuccess: () => void }; + +const LoginForm = ({ onSuccess }: LoginFormProps) => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isPending, setIsPending] = useState(false); + + const handleSubmit = async () => { + setIsPending(true); + await authClient.signIn.email( + { email, password }, + { + onSuccess: () => { + toast.success("Welcome back!"); + onSuccess(); + }, + onError: (ctx) => { + toast.error(ctx.error.message ?? "Something went wrong."); + setIsPending(false); + }, + }, + ); + }; + + return ( +
{ + e.preventDefault(); + void handleSubmit(); + }} + className="flex flex-col gap-3" + > + setEmail(e.target.value)} + required + className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)" + /> + setPassword(e.target.value)} + required + className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)" + /> +
+ + Forgot password? + +
+ +
+ ); +}; + +type RegisterFormProps = { onSuccess: () => void }; + +const RegisterForm = ({ onSuccess }: RegisterFormProps) => { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isPending, setIsPending] = useState(false); + + const handleSubmit = async () => { + setIsPending(true); + await authClient.signUp.email( + { name, email, password }, + { + onSuccess: () => { + toast.success("Check your email to verify your account."); + onSuccess(); + }, + onError: (ctx) => { + toast.error(ctx.error.message ?? "Something went wrong."); + setIsPending(false); + }, + }, + ); + }; + + return ( +
{ + e.preventDefault(); + void handleSubmit(); + }} + className="flex flex-col gap-3" + > + setName(e.target.value)} + required + className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)" + /> + setEmail(e.target.value)} + required + className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)" + /> + setPassword(e.target.value)} + required + minLength={8} + className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)" + /> + +
+ ); +}; + +const SocialButtons = () => { + const handleSocial = (provider: "google" | "github") => { + void authClient.signIn.social( + { provider, callbackURL: window.location.origin }, + { + onError: (ctx) => { + toast.error(ctx.error.message ?? "Something went wrong."); + }, + }, + ); + }; + + return ( +
+
+
+ + or continue with + +
+
+ + +
+ ); +}; + +export const AuthModal = ({ onClose }: AuthModalProps) => { + const [tab, setTab] = useState("login"); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + + return ( +
+
e.stopPropagation()} + > + {/* Close button */} + + + {/* Header */} +
+

+ lila +

+
+ + {/* Tabs */} +
+ {(["login", "register"] as Tab[]).map((t) => ( + + ))} +
+ + {tab === "login" ? ( + + ) : ( + + )} + + {/* Social */} + +
+
+ ); +}; diff --git a/apps/web/src/components/navbar/NavLogin.tsx b/apps/web/src/components/navbar/NavLogin.tsx deleted file mode 100644 index f28bfdd..0000000 --- a/apps/web/src/components/navbar/NavLogin.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Link } from "@tanstack/react-router"; - -const NavLogin = () => { - return ( - - Login - - ); -}; - -export default NavLogin; diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index c672ced..826aec6 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,8 +1,10 @@ import { createRootRoute, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import { Toaster } from "sonner"; import Navbar from "../components/navbar/NavBar"; import NotFound from "../components/NotFound"; import RootError from "../components/RootError"; +import { AuthModalSearchSchema } from "@lila/shared"; const RootLayout = () => { return ( @@ -11,6 +13,7 @@ const RootLayout = () => {
+ ); @@ -20,4 +23,5 @@ export const Route = createRootRoute({ component: RootLayout, notFoundComponent: NotFound, errorComponent: RootError, + validateSearch: AuthModalSearchSchema, }); diff --git a/apps/web/src/routes/reset-password.tsx b/apps/web/src/routes/reset-password.tsx index c68b3cb..837949b 100644 --- a/apps/web/src/routes/reset-password.tsx +++ b/apps/web/src/routes/reset-password.tsx @@ -5,7 +5,7 @@ import { toast } from "sonner"; import { ResetPasswordSearchSchema } from "@lila/shared"; function ResetPasswordPage() { - const token = String(Route.useSearch().token); + const { token } = Route.useSearch(); const navigate = useNavigate(); const [password, setPassword] = useState(""); const [isPending, setIsPending] = useState(false); diff --git a/eslint.config.mjs b/eslint.config.mjs index 290fa14..4d2c015 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -43,6 +43,9 @@ export default defineConfig([ rules: { "react-refresh/only-export-components": "off", "@typescript-eslint/only-throw-error": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-call": "off", }, }, { diff --git a/packages/shared/src/schemas/auth.ts b/packages/shared/src/schemas/auth.ts index 1b32783..6aaf35d 100644 --- a/packages/shared/src/schemas/auth.ts +++ b/packages/shared/src/schemas/auth.ts @@ -5,3 +5,10 @@ export const ResetPasswordSearchSchema = z.object({ }); export type ResetPasswordSearch = z.infer; + +export const AuthModalSearchSchema = z.object({ + modal: z.enum(["auth"]).optional().catch(undefined), + redirect: z.string().optional().catch(undefined), +}); + +export type AuthModalSearch = z.infer;