From bc07baa25d9447a6355856c4f9e874372b9f6210 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Fri, 9 Jan 2026 02:08:00 +0000 Subject: [PATCH] landing page implementation needs some work, but does a great job for now --- packages/frontend/src-tauri/tauri.conf.json | 5 +- packages/frontend/src/App.tsx | 38 +++++-- .../frontend/src/components/auth-provider.tsx | 18 +-- packages/frontend/src/components/landing.tsx | 104 ++++++++++++++++++ .../frontend/src/components/login-form.tsx | 10 +- .../frontend/src/components/login-page.tsx | 52 +++++++++ .../src/components/settings-page-layout.tsx | 19 ---- todo.md | 4 +- 8 files changed, 208 insertions(+), 42 deletions(-) create mode 100644 packages/frontend/src/components/landing.tsx create mode 100644 packages/frontend/src/components/login-page.tsx delete mode 100644 packages/frontend/src/components/settings-page-layout.tsx diff --git a/packages/frontend/src-tauri/tauri.conf.json b/packages/frontend/src-tauri/tauri.conf.json index 0d00b23..3797c8a 100644 --- a/packages/frontend/src-tauri/tauri.conf.json +++ b/packages/frontend/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "identifier": "com.hex248.issue", "build": { "beforeDevCommand": "bun run dev", - "devUrl": "http://localhost:1420", + "devUrl": "http://localhost:1420/app", "beforeBuildCommand": "bun run build", "frontendDist": "../dist" }, @@ -17,7 +17,8 @@ "height": 900, "minWidth": 640, "minHeight": 360, - "decorations": false + "decorations": false, + "url": "/app" } ], "security": { diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 6b0a21c..24cba55 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -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 ( - - - - } /> - } /> - } /> - - - + + + {/* public routes */} + } /> + } /> + + {/* authed routes */} + + + + } + /> + + + + } + /> + + } /> + + ); } diff --git a/packages/frontend/src/components/auth-provider.tsx b/packages/frontend/src/components/auth-provider.tsx index f6804ac..8f34b69 100644 --- a/packages/frontend/src/components/auth-provider.tsx +++ b/packages/frontend/src/components/auth-provider.tsx @@ -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(); 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 ( -
- -
- ); - return ; + if (loggedIn === false) { + const next = encodeURIComponent(location.pathname + location.search); + return ; + } + + return ; } diff --git a/packages/frontend/src/components/landing.tsx b/packages/frontend/src/components/landing.tsx new file mode 100644 index 0000000..f3e05cd --- /dev/null +++ b/packages/frontend/src/components/landing.tsx @@ -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(() => { + // 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 ( +
+
+
Issue
+ +
+ +
+
+

+ Does your team need a snappy project management tool? +

+

+ Build your next project with Issue. +

+

+ Sick of Jira? Say hello to your new favorite project management tool. +

+
+ +
+ {authState === "authenticated" ? ( + + ) : ( + + )} +
+
+ + +
+ ); +} diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 893753d..290c48f 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -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) { diff --git a/packages/frontend/src/components/login-page.tsx b/packages/frontend/src/components/login-page.tsx new file mode 100644 index 0000000..671ee36 --- /dev/null +++ b/packages/frontend/src/components/login-page.tsx @@ -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 ; + } + + return ( +
+ +
+ ); +} diff --git a/packages/frontend/src/components/settings-page-layout.tsx b/packages/frontend/src/components/settings-page-layout.tsx deleted file mode 100644 index ab069fd..0000000 --- a/packages/frontend/src/components/settings-page-layout.tsx +++ /dev/null @@ -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 ( -
-
-
-

{title}

-
-
- -
{children}
-
- ); -} diff --git a/todo.md b/todo.md index e3b294e..4ff2ca2 100644 --- a/todo.md +++ b/todo.md @@ -1,6 +1,8 @@ - landing/marketing page - does your team need a snappy project management tool? - build your next project with Issue (might need a new name...) + - add loading state to landing CTAs during auth verification +- dedicated /register route (currently login/register are combined on /login) - real logo - org settings - sprints @@ -10,7 +12,7 @@ - admins are capable of deleting comments from members who are at their permission level or below (not sure if this should apply, or if ANYONE should have control over others' comments - people in an org tend to be trusted to not be trolls) - status - predefined statuses are added to organisation by default. list of statuses can be edited by owner/admin (maybe this should be on projects rather than organisations?) - - sprints + - sprint - time tracking (linked to issues or standalone) - user preferences - "assign to me by default" option for new issues