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:
parent
32ee1edf80
commit
dc11213cb5
8 changed files with 41 additions and 79 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
|
||||||
|
|
@ -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 };
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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" } });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue