feat: replace login route with auth modal

- Add AuthModal to root layout driven by ?modal=auth search param
- Update multiplayer and play beforeLoad redirects to use modal
- Update NavAuth and Hero links to use modal
- Delete login route and NavLogin component
This commit is contained in:
lila 2026-04-30 19:46:45 +02:00
parent 32ee1edf80
commit dc11213cb5
8 changed files with 41 additions and 79 deletions

View file

@ -4,7 +4,7 @@ import { authClient } from "../../lib/auth-client";
type Tab = "login" | "register"; type Tab = "login" | "register";
type AuthModalProps = { onClose: () => void }; type AuthModalProps = { onClose: () => void; onSuccess: () => void };
type LoginFormProps = { onSuccess: () => void }; type LoginFormProps = { onSuccess: () => void };
@ -142,11 +142,14 @@ const RegisterForm = ({ onSuccess }: RegisterFormProps) => {
); );
}; };
const SocialButtons = () => { type SocialButtonsProps = { onSuccess: () => void };
const SocialButtons = ({ onSuccess }: SocialButtonsProps) => {
const handleSocial = (provider: "google" | "github") => { const handleSocial = (provider: "google" | "github") => {
void authClient.signIn.social( void authClient.signIn.social(
{ provider, callbackURL: window.location.origin }, { provider, callbackURL: window.location.origin },
{ {
onSuccess,
onError: (ctx) => { onError: (ctx) => {
toast.error(ctx.error.message ?? "Something went wrong."); toast.error(ctx.error.message ?? "Something went wrong.");
}, },
@ -179,7 +182,7 @@ const SocialButtons = () => {
); );
}; };
export const AuthModal = ({ onClose }: AuthModalProps) => { export const AuthModal = ({ onClose, onSuccess }: AuthModalProps) => {
const [tab, setTab] = useState<Tab>("login"); const [tab, setTab] = useState<Tab>("login");
useEffect(() => { useEffect(() => {
@ -233,13 +236,13 @@ export const AuthModal = ({ onClose }: AuthModalProps) => {
</div> </div>
{tab === "login" ? ( {tab === "login" ? (
<LoginForm onSuccess={onClose} /> <LoginForm onSuccess={onSuccess} />
) : ( ) : (
<RegisterForm onSuccess={onClose} /> <RegisterForm onSuccess={onClose} />
)} )}
{/* Social */} {/* Social */}
<SocialButtons /> <SocialButtons onSuccess={onSuccess} />
</div> </div>
</div> </div>
); );

View file

@ -66,13 +66,15 @@ const Hero = () => {
) : ( ) : (
<> <>
<Link <Link
to="/login" to="/"
search={{ modal: "auth" }}
className="px-7 py-3 rounded-full text-white font-bold text-sm bg-(--color-primary) hover:bg-(--color-primary-dark)" className="px-7 py-3 rounded-full text-white font-bold text-sm bg-(--color-primary) hover:bg-(--color-primary-dark)"
> >
Get started Get started
</Link> </Link>
<Link <Link
to="/login" to="/"
search={{ modal: "auth" }}
className="px-7 py-3 rounded-full font-bold text-sm text-(--color-primary) border-2 border-(--color-primary) hover:bg-(--color-surface)" className="px-7 py-3 rounded-full font-bold text-sm text-(--color-primary) border-2 border-(--color-primary) hover:bg-(--color-surface)"
> >
Log in Log in

View file

@ -24,13 +24,14 @@ const NavAuth = () => {
</button> </button>
) : ( ) : (
<Link <Link
to="/login" to="/"
search={{ modal: "auth" }}
className="text-sm font-medium px-4 py-1.5 rounded-full className="text-sm font-medium px-4 py-1.5 rounded-full
text-white bg-(--color-primary) text-white bg-(--color-primary)
hover:bg-(--color-primary-dark) hover:bg-(--color-primary-dark)
transition-colors duration-200" transition-colors duration-200"
> >
Sign in Login
</Link> </Link>
)} )}
</div> </div>

View file

@ -12,7 +12,6 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as ResetPasswordRouteImport } from './routes/reset-password' import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as PlayRouteImport } from './routes/play' import { Route as PlayRouteImport } from './routes/play'
import { Route as MultiplayerRouteImport } from './routes/multiplayer' import { Route as MultiplayerRouteImport } from './routes/multiplayer'
import { Route as LoginRouteImport } from './routes/login'
import { Route as ForgotPasswordRouteImport } from './routes/forgot-password' import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
import { Route as AboutRouteImport } from './routes/about' import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
@ -35,11 +34,6 @@ const MultiplayerRoute = MultiplayerRouteImport.update({
path: '/multiplayer', path: '/multiplayer',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const ForgotPasswordRoute = ForgotPasswordRouteImport.update({ const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
id: '/forgot-password', id: '/forgot-password',
path: '/forgot-password', path: '/forgot-password',
@ -75,7 +69,6 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/forgot-password': typeof ForgotPasswordRoute '/forgot-password': typeof ForgotPasswordRoute
'/login': typeof LoginRoute
'/multiplayer': typeof MultiplayerRouteWithChildren '/multiplayer': typeof MultiplayerRouteWithChildren
'/play': typeof PlayRoute '/play': typeof PlayRoute
'/reset-password': typeof ResetPasswordRoute '/reset-password': typeof ResetPasswordRoute
@ -87,7 +80,6 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/forgot-password': typeof ForgotPasswordRoute '/forgot-password': typeof ForgotPasswordRoute
'/login': typeof LoginRoute
'/play': typeof PlayRoute '/play': typeof PlayRoute
'/reset-password': typeof ResetPasswordRoute '/reset-password': typeof ResetPasswordRoute
'/multiplayer': typeof MultiplayerIndexRoute '/multiplayer': typeof MultiplayerIndexRoute
@ -99,7 +91,6 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/forgot-password': typeof ForgotPasswordRoute '/forgot-password': typeof ForgotPasswordRoute
'/login': typeof LoginRoute
'/multiplayer': typeof MultiplayerRouteWithChildren '/multiplayer': typeof MultiplayerRouteWithChildren
'/play': typeof PlayRoute '/play': typeof PlayRoute
'/reset-password': typeof ResetPasswordRoute '/reset-password': typeof ResetPasswordRoute
@ -113,7 +104,6 @@ export interface FileRouteTypes {
| '/' | '/'
| '/about' | '/about'
| '/forgot-password' | '/forgot-password'
| '/login'
| '/multiplayer' | '/multiplayer'
| '/play' | '/play'
| '/reset-password' | '/reset-password'
@ -125,7 +115,6 @@ export interface FileRouteTypes {
| '/' | '/'
| '/about' | '/about'
| '/forgot-password' | '/forgot-password'
| '/login'
| '/play' | '/play'
| '/reset-password' | '/reset-password'
| '/multiplayer' | '/multiplayer'
@ -136,7 +125,6 @@ export interface FileRouteTypes {
| '/' | '/'
| '/about' | '/about'
| '/forgot-password' | '/forgot-password'
| '/login'
| '/multiplayer' | '/multiplayer'
| '/play' | '/play'
| '/reset-password' | '/reset-password'
@ -149,7 +137,6 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute AboutRoute: typeof AboutRoute
ForgotPasswordRoute: typeof ForgotPasswordRoute ForgotPasswordRoute: typeof ForgotPasswordRoute
LoginRoute: typeof LoginRoute
MultiplayerRoute: typeof MultiplayerRouteWithChildren MultiplayerRoute: typeof MultiplayerRouteWithChildren
PlayRoute: typeof PlayRoute PlayRoute: typeof PlayRoute
ResetPasswordRoute: typeof ResetPasswordRoute ResetPasswordRoute: typeof ResetPasswordRoute
@ -178,13 +165,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MultiplayerRouteImport preLoaderRoute: typeof MultiplayerRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/forgot-password': { '/forgot-password': {
id: '/forgot-password' id: '/forgot-password'
path: '/forgot-password' path: '/forgot-password'
@ -250,7 +230,6 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AboutRoute: AboutRoute, AboutRoute: AboutRoute,
ForgotPasswordRoute: ForgotPasswordRoute, ForgotPasswordRoute: ForgotPasswordRoute,
LoginRoute: LoginRoute,
MultiplayerRoute: MultiplayerRouteWithChildren, MultiplayerRoute: MultiplayerRouteWithChildren,
PlayRoute: PlayRoute, PlayRoute: PlayRoute,
ResetPasswordRoute: ResetPasswordRoute, ResetPasswordRoute: ResetPasswordRoute,

View file

@ -1,18 +1,38 @@
import { createRootRoute, Outlet } from "@tanstack/react-router"; import {
createRootRoute,
Outlet,
useNavigate,
useSearch,
} from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import Navbar from "../components/navbar/NavBar"; import Navbar from "../components/navbar/NavBar";
import NotFound from "../components/NotFound"; import NotFound from "../components/NotFound";
import RootError from "../components/RootError"; import RootError from "../components/RootError";
import { AuthModal } from "../components/auth/AuthModal";
import { AuthModalSearchSchema } from "@lila/shared"; import { AuthModalSearchSchema } from "@lila/shared";
const RootLayout = () => { const RootLayout = () => {
const navigate = useNavigate();
const { modal, redirect } = useSearch({ from: "__root__" });
const handleClose = () => {
void navigate({ to: "/", search: {} });
};
const handleSuccess = () => {
void navigate({ to: (redirect as string) ?? "/", search: {} });
};
return ( return (
<> <>
<Navbar /> <Navbar />
<main className="max-w-5xl mx-auto px-6 py-8"> <main className="max-w-5xl mx-auto px-6 py-8">
<Outlet /> <Outlet />
</main> </main>
{modal === "auth" && (
<AuthModal onClose={handleClose} onSuccess={handleSuccess} />
)}
<Toaster richColors position="top-center" /> <Toaster richColors position="top-center" />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</> </>

View file

@ -1,46 +0,0 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { signIn, useSession } from "../lib/auth-client";
const LoginPage = () => {
const { data: session, isPending } = useSession();
const navigate = useNavigate();
if (isPending) return <div className="p-4">Loading...</div>;
if (session) {
void navigate({ to: "/" });
return null;
}
return (
<div className="flex flex-col items-center justify-center gap-4 p-8">
<h1 className="text-2xl font-bold">sign in to lila</h1>
<button
className="w-64 rounded-2xl bg-(--color-text) px-4 py-3 text-white font-bold hover:opacity-90 shadow-sm hover:shadow-md transition-all"
onClick={() => {
void signIn
.social({ provider: "github", callbackURL: window.location.origin })
.catch((err) => {
console.error("GitHub sign in error:", err);
});
}}
>
Continue with GitHub
</button>
<button
className="w-64 rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) shadow-sm hover:shadow-md transition-all"
onClick={() => {
void signIn
.social({ provider: "google", callbackURL: window.location.origin })
.catch((err) => {
console.error("Google sign in error:", err);
});
}}
>
Continue with Google
</button>
</div>
);
};
export const Route = createFileRoute("/login")({ component: LoginPage });

View file

@ -14,7 +14,10 @@ export const Route = createFileRoute("/multiplayer")({
beforeLoad: async () => { beforeLoad: async () => {
const { data: session } = await authClient.getSession(); const { data: session } = await authClient.getSession();
if (!session) { if (!session) {
throw redirect({ to: "/login" }); throw redirect({
to: "/",
search: { modal: "auth", redirect: "/multiplayer" },
});
} }
return { session }; return { session };
}, },

View file

@ -132,7 +132,7 @@ export const Route = createFileRoute("/play")({
beforeLoad: async () => { beforeLoad: async () => {
const { data: session } = await authClient.getSession(); const { data: session } = await authClient.getSession();
if (!session) { if (!session) {
throw redirect({ to: "/login" }); throw redirect({ to: "/", search: { modal: "auth", redirect: "/play" } });
} }
}, },
}); });