mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
replaced login page with modal
This commit is contained in:
@@ -19,15 +19,18 @@ const DEMO_USERS = [
|
|||||||
{ name: "User 2", username: "u2", password: "a" },
|
{ name: "User 2", username: "u2", password: "a" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function LogInForm() {
|
export default function LogInForm({
|
||||||
|
showWarning,
|
||||||
|
setShowWarning,
|
||||||
|
}: {
|
||||||
|
showWarning: boolean;
|
||||||
|
setShowWarning: (value: boolean) => void;
|
||||||
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { setUser } = useSession();
|
const { setUser } = useSession();
|
||||||
|
|
||||||
const [loginDetailsOpen, setLoginDetailsOpen] = useState(false);
|
const [loginDetailsOpen, setLoginDetailsOpen] = useState(false);
|
||||||
const [showWarning, setShowWarning] = useState(() => {
|
|
||||||
return localStorage.getItem("hide-under-construction") !== "true";
|
|
||||||
});
|
|
||||||
|
|
||||||
const [mode, setMode] = useState<"login" | "register">("login");
|
const [mode, setMode] = useState<"login" | "register">("login");
|
||||||
|
|
||||||
@@ -143,7 +146,7 @@ export default function LogInForm() {
|
|||||||
<>
|
<>
|
||||||
{/* under construction warning */}
|
{/* under construction warning */}
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<div className="relative flex flex-col border p-4 items-center border-border/50 bg-border/10 gap-2 max-w-lg">
|
<div className="relative flex flex-col items-center gap-2 max-w-lg">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="md"
|
size="md"
|
||||||
className="absolute top-2 right-2"
|
className="absolute top-2 right-2"
|
||||||
@@ -168,7 +171,7 @@ export default function LogInForm() {
|
|||||||
<DialogTrigger className="text-primary hover:text-personality cursor-pointer mt-2">
|
<DialogTrigger className="text-primary hover:text-personality cursor-pointer mt-2">
|
||||||
Login Details
|
Login Details
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="w-xs" showCloseButton={false}>
|
<DialogContent showCloseButton={false}>
|
||||||
<DialogTitle className="sr-only">Demo Login Credentials</DialogTitle>
|
<DialogTitle className="sr-only">Demo Login Credentials</DialogTitle>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{DEMO_USERS.map((user) => (
|
{DEMO_USERS.map((user) => (
|
||||||
@@ -208,7 +211,7 @@ export default function LogInForm() {
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex flex-col gap-2 items-center border p-6 pb-4",
|
"relative flex flex-col gap-2 items-center p-4 pb-2",
|
||||||
error !== "" && "border-destructive",
|
error !== "" && "border-destructive",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
55
packages/frontend/src/components/login-modal.tsx
Normal file
55
packages/frontend/src/components/login-modal.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
import LogInForm from "@/components/login-form";
|
||||||
|
import { useSession } from "@/components/session-provider";
|
||||||
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface LoginModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
dismissible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginModal({ open, onOpenChange, onSuccess, dismissible = true }: LoginModalProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { user, isLoading } = useSession();
|
||||||
|
const [hasRedirected, setHasRedirected] = useState(false);
|
||||||
|
const [showWarning, setShowWarning] = useState(() => {
|
||||||
|
return localStorage.getItem("hide-under-construction") !== "true";
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && !isLoading && user && !hasRedirected) {
|
||||||
|
setHasRedirected(true);
|
||||||
|
const next = searchParams.get("next") || "/issues";
|
||||||
|
navigate(next, { replace: true });
|
||||||
|
onSuccess?.();
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
}, [open, user, isLoading, navigate, searchParams, onSuccess, onOpenChange, hasRedirected]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setHasRedirected(false);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
if (!dismissible && !newOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onOpenChange(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent showCloseButton={false} className={cn("p-0 w-xs py-8", showWarning && "w-md pt-4")}>
|
||||||
|
<DialogTitle className="sr-only">Log In or Register</DialogTitle>
|
||||||
|
<LogInForm showWarning={showWarning} setShowWarning={setShowWarning} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { UserRecord } from "@sprint/shared";
|
import type { UserRecord } from "@sprint/shared";
|
||||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { Navigate, useLocation } from "react-router-dom";
|
|
||||||
import Loading from "@/components/loading";
|
import Loading from "@/components/loading";
|
||||||
|
import { LoginModal } from "@/components/login-modal";
|
||||||
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||||
|
|
||||||
interface SessionContextValue {
|
interface SessionContextValue {
|
||||||
@@ -74,15 +75,22 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
const { user, isLoading } = useSession();
|
const { user, isLoading } = useSession();
|
||||||
const location = useLocation();
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !user) {
|
||||||
|
setLoginModalOpen(true);
|
||||||
|
} else if (user) {
|
||||||
|
setLoginModalOpen(false);
|
||||||
|
}
|
||||||
|
}, [user, isLoading]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading message={"Checking authentication"} />;
|
return <Loading message={"Checking authentication"} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const next = encodeURIComponent(location.pathname + location.search);
|
return <LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} dismissible={false} />;
|
||||||
return <Navigate to={`/login?next=${next}`} replace />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { Toaster } from "@/components/ui/sonner";
|
|||||||
import Font from "@/pages/Font";
|
import Font from "@/pages/Font";
|
||||||
import Issues from "@/pages/Issues";
|
import Issues from "@/pages/Issues";
|
||||||
import Landing from "@/pages/Landing";
|
import Landing from "@/pages/Landing";
|
||||||
import Login from "@/pages/Login";
|
|
||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
import Test from "@/pages/Test";
|
import Test from "@/pages/Test";
|
||||||
import Timeline from "@/pages/Timeline";
|
import Timeline from "@/pages/Timeline";
|
||||||
@@ -27,7 +26,6 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
{/* public routes */}
|
{/* public routes */}
|
||||||
<Route path="/" element={<Landing />} />
|
<Route path="/" element={<Landing />} />
|
||||||
<Route path="/font" element={<Font />} />
|
<Route path="/font" element={<Font />} />
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
|
|
||||||
{/* authed routes */}
|
{/* authed routes */}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { LoginModal } from "@/components/login-modal";
|
||||||
import { useSession } from "@/components/session-provider";
|
import { useSession } from "@/components/session-provider";
|
||||||
import ThemeToggle from "@/components/theme-toggle";
|
import ThemeToggle from "@/components/theme-toggle";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -24,7 +25,6 @@ const pricingTiers = [
|
|||||||
"Email support",
|
"Email support",
|
||||||
],
|
],
|
||||||
cta: "Get started free",
|
cta: "Get started free",
|
||||||
ctaLink: "/login",
|
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,6 @@ const pricingTiers = [
|
|||||||
"Priority email support",
|
"Priority email support",
|
||||||
],
|
],
|
||||||
cta: "Try pro free for 14 days",
|
cta: "Try pro free for 14 days",
|
||||||
ctaLink: "/login",
|
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -88,6 +87,7 @@ const faqs = [
|
|||||||
export default function Landing() {
|
export default function Landing() {
|
||||||
const { user, isLoading } = useSession();
|
const { user, isLoading } = useSession();
|
||||||
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly");
|
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly");
|
||||||
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col" id="top">
|
<div className="min-h-screen flex flex-col" id="top">
|
||||||
@@ -129,8 +129,8 @@ export default function Landing() {
|
|||||||
<Link to="/issues">Open app</Link>
|
<Link to="/issues">Open app</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button variant="outline" size="sm" onClick={() => setLoginModalOpen(true)}>
|
||||||
<Link to="/login">Sign in</Link>
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -162,8 +162,8 @@ export default function Landing() {
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button asChild size="lg" className="text-lg px-8 py-6">
|
<Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}>
|
||||||
<Link to="/login">Start free trial</Link>
|
Start free trial
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">
|
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">
|
||||||
<a href="#pricing">See pricing</a>
|
<a href="#pricing">See pricing</a>
|
||||||
@@ -381,14 +381,14 @@ export default function Landing() {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
|
||||||
variant={tier.highlighted ? "default" : "outline"}
|
variant={tier.highlighted ? "default" : "outline"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"font-700 py-6",
|
"font-700 py-6",
|
||||||
tier.highlighted ? "bg-personality hover:bg-personality/90 text-background" : "",
|
tier.highlighted ? "bg-personality hover:bg-personality/90 text-background" : "",
|
||||||
)}
|
)}
|
||||||
|
onClick={() => setLoginModalOpen(true)}
|
||||||
>
|
>
|
||||||
<Link to={tier.ctaLink}>{tier.cta}</Link>
|
{tier.cta}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -472,8 +472,8 @@ export default function Landing() {
|
|||||||
<Link to="/issues">Open app</Link>
|
<Link to="/issues">Open app</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild size="lg" className="text-lg px-8 py-6">
|
<Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}>
|
||||||
<Link to="/login">Start your free trial</Link>
|
Start your free trial
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -484,6 +484,8 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
|
||||||
|
|
||||||
<footer className="flex justify-center gap-2 items-center py-1 border-t">
|
<footer className="flex justify-center gap-2 items-center py-1 border-t">
|
||||||
<span className="font-300 text-lg text-muted-foreground">
|
<span className="font-300 text-lg text-muted-foreground">
|
||||||
Built by{" "}
|
Built by{" "}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
||||||
import Loading from "@/components/loading";
|
|
||||||
import LogInForm from "@/components/login-form";
|
|
||||||
import { useSession } from "@/components/session-provider";
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const { user, isLoading } = useSession();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && user) {
|
|
||||||
const next = searchParams.get("next") || "/issues";
|
|
||||||
navigate(next, { replace: true });
|
|
||||||
}
|
|
||||||
}, [user, isLoading, navigate, searchParams]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loading message="Checking authentication" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
return <Loading message="Redirecting" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]">
|
|
||||||
<LogInForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user