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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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;