mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
landing page implementation
needs some work, but does a great job for now
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { Auth } from "@/components/auth-provider";
|
||||
import Landing from "@/components/landing";
|
||||
import LoginPage from "@/components/login-page";
|
||||
import NotFound from "@/components/NotFound";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import Index from "@/Index";
|
||||
@@ -8,15 +10,33 @@ import Test from "@/Test";
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<Auth>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/test" element={<Test />} />
|
||||
<Route path={"*"} element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</Auth>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* public routes */}
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
{/* authed routes */}
|
||||
<Route
|
||||
path="/app"
|
||||
element={
|
||||
<Auth>
|
||||
<Index />
|
||||
</Auth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/test"
|
||||
element={
|
||||
<Auth>
|
||||
<Test />
|
||||
</Auth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { UserRecord } from "@issue/shared";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import Loading from "@/components/loading";
|
||||
import LogInForm from "@/components/login-form";
|
||||
import { getServerURL } from "@/lib/utils";
|
||||
|
||||
export function Auth({ children }: { children: React.ReactNode; loggedInDefault?: boolean }) {
|
||||
export function Auth({ children }: { children: React.ReactNode }) {
|
||||
const [loggedIn, setLoggedIn] = useState<boolean>();
|
||||
const fetched = useRef(false);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (fetched.current) return;
|
||||
@@ -40,12 +41,11 @@ export function Auth({ children }: { children: React.ReactNode; loggedInDefault?
|
||||
if (loggedIn) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
if (loggedIn === false)
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]">
|
||||
<LogInForm />
|
||||
</div>
|
||||
);
|
||||
|
||||
return <Loading message={"Understanding your authentication state"} />;
|
||||
if (loggedIn === false) {
|
||||
const next = encodeURIComponent(location.pathname + location.search);
|
||||
return <Navigate to={`/login?next=${next}`} replace />;
|
||||
}
|
||||
|
||||
return <Loading message={"Checking authentication"} />;
|
||||
}
|
||||
|
||||
104
packages/frontend/src/components/landing.tsx
Normal file
104
packages/frontend/src/components/landing.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { UserRecord } from "@issue/shared";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getServerURL } from "@/lib/utils";
|
||||
|
||||
type AuthState = "unknown" | "authenticated" | "unauthenticated";
|
||||
|
||||
export default function Landing() {
|
||||
const [authState, setAuthState] = useState<AuthState>(() => {
|
||||
// if token exists, assume authenticated until verified
|
||||
return localStorage.getItem("token") ? "unknown" : "unauthenticated";
|
||||
});
|
||||
const verifiedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (verifiedRef.current) return;
|
||||
verifiedRef.current = true;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
setAuthState("unauthenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
// verify token in background
|
||||
fetch(`${getServerURL()}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.ok) {
|
||||
const user = (await res.json()) as UserRecord;
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
setAuthState("authenticated");
|
||||
} else {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
setAuthState("unauthenticated");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
setAuthState("unauthenticated");
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<header className="flex items-center justify-between p-2 border-b">
|
||||
<div className="text-lg font-semibold">Issue</div>
|
||||
<nav className="flex items-center gap-4">
|
||||
{authState === "authenticated" ? (
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link to="/app">Open app</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link to="/login">Sign in</Link>
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 flex flex-col items-center justify-center gap-8">
|
||||
<div className="max-w-2xl text-center space-y-4">
|
||||
<h1 className="text-4xl font-600">
|
||||
Does your team need a snappy project management tool?
|
||||
</h1>
|
||||
<p className="text-md text-muted-foreground font-500">
|
||||
Build your next project with <span className="font-700">Issue</span>.
|
||||
</p>
|
||||
<p className="text-md text-muted-foreground font-400">
|
||||
Sick of Jira? Say hello to your new favorite project management tool.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
{authState === "authenticated" ? (
|
||||
<Button asChild size="lg">
|
||||
<Link to="/app">Open app</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild size="lg">
|
||||
<Link to="/login">Get started</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="p-2 border-t text-center text-sm font-300 text-muted-foreground">
|
||||
Built by{" "}
|
||||
<a
|
||||
href="https://ob248.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline underline-offset-2"
|
||||
>
|
||||
Oliver Bryan
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { AlertTriangle, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import Avatar from "@/components/avatar";
|
||||
import { ServerConfigurationDialog } from "@/components/server-configuration-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -17,6 +18,9 @@ const DEMO_USERS = [
|
||||
];
|
||||
|
||||
export default function LogInForm() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [loginDetailsOpen, setLoginDetailsOpen] = useState(false);
|
||||
const [showWarning, setShowWarning] = useState(() => {
|
||||
return localStorage.getItem("hide-under-construction") !== "true";
|
||||
@@ -48,7 +52,8 @@ export default function LogInForm() {
|
||||
const data = await res.json();
|
||||
localStorage.setItem("token", data.token);
|
||||
localStorage.setItem("user", JSON.stringify(data.user));
|
||||
window.location.href = "";
|
||||
const next = searchParams.get("next") || "/app";
|
||||
navigate(next, { replace: true });
|
||||
}
|
||||
// unauthorized
|
||||
else if (res.status === 401) {
|
||||
@@ -84,7 +89,8 @@ export default function LogInForm() {
|
||||
const data = await res.json();
|
||||
localStorage.setItem("token", data.token);
|
||||
localStorage.setItem("user", JSON.stringify(data.user));
|
||||
window.location.href = "";
|
||||
const next = searchParams.get("next") || "/app";
|
||||
navigate(next, { replace: true });
|
||||
}
|
||||
// bad request (probably a bad user input)
|
||||
else if (res.status === 400) {
|
||||
|
||||
52
packages/frontend/src/components/login-page.tsx
Normal file
52
packages/frontend/src/components/login-page.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import Loading from "@/components/loading";
|
||||
import LogInForm from "@/components/login-form";
|
||||
import { getServerURL } from "@/lib/utils";
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [checking, setChecking] = useState(true);
|
||||
const checkedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (checkedRef.current) return;
|
||||
checkedRef.current = true;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
setChecking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${getServerURL()}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.ok) {
|
||||
// logged in, redirect to next if defined
|
||||
// fallback to /app
|
||||
const next = searchParams.get("next") || "/app";
|
||||
navigate(next, { replace: true });
|
||||
} else {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
setChecking(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setChecking(false);
|
||||
});
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
if (checking) {
|
||||
return <Loading message="Checking authentication" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]">
|
||||
<LogInForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { UserRecord } from "@issue/shared";
|
||||
import type { ReactNode } from "react";
|
||||
import Header from "@/components/header";
|
||||
|
||||
export function SettingsPageLayout({ title, children }: { title: string; children?: ReactNode }) {
|
||||
const user = JSON.parse(localStorage.getItem("user") || "{}") as UserRecord;
|
||||
|
||||
return (
|
||||
<main className="w-full h-screen flex flex-col">
|
||||
<Header user={user}>
|
||||
<div className="flex gap-1 items-center">
|
||||
<h1 className="text-3xl font-600">{title}</h1>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<div className="flex flex-col items-center justify-center w-full flex-1 text-md">{children}</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user