From 815db1ec1fdea981835d79a0b9bf124b739b2b61 Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Fri, 24 Jul 2026 21:47:41 +0200 Subject: [PATCH] frontend: add LoginGate, wire session into App, add logout link --- frontend/src/App.css | 18 +++++++++++++- frontend/src/App.tsx | 22 +++++++++++------ frontend/src/LoginGate.css | 27 ++++++++++++++++++++ frontend/src/LoginGate.tsx | 50 ++++++++++++++++++++++++++++++++++++++ frontend/src/main.tsx | 4 +-- 5 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 frontend/src/LoginGate.css create mode 100644 frontend/src/LoginGate.tsx diff --git a/frontend/src/App.css b/frontend/src/App.css index b1cafab..6bffae5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -57,7 +57,6 @@ body { alongside Activities/Progression/Training plan, so it reads more like an account chip (à la Slack/GitHub's corner avatar) than another nav tab. */ .profile-name { - margin-left: auto; display: inline-flex; align-items: center; gap: 0.4rem; @@ -87,6 +86,23 @@ body { color: white; } +.header-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.6rem; +} + +.logout-link { + color: #9aa0ab; + font-size: 0.85rem; + text-decoration: none; +} + +.logout-link:hover { + color: #3b82f6; +} + .garmin-connection { display: flex; flex-direction: column; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 991bd57..d32e67a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import { Dashboard } from "./pages/Dashboard"; import { Plan } from "./pages/Plan"; import { Profile } from "./pages/Profile"; import { ReviewQueue } from "./pages/ReviewQueue"; +import type { SessionInfo } from "./types/api"; const TABS = [ { key: "activities", label: "Activities", icon: "☰", Component: ReviewQueue }, @@ -14,7 +15,7 @@ const TABS = [ type TabKey = (typeof TABS)[number]["key"]; -function App() { +function App({ session }: { session: SessionInfo }) { const [tab, setTab] = useState("activities"); // Profile isn't a tab: it's reached via the profile name in the top-right // corner instead, since (for now, single-profile) it's account settings, @@ -47,13 +48,18 @@ function App() { ))} - +
+ + + Log out + +
{showProfile ? setProfileName(p.Name)} /> : }
diff --git a/frontend/src/LoginGate.css b/frontend/src/LoginGate.css new file mode 100644 index 0000000..aa59c14 --- /dev/null +++ b/frontend/src/LoginGate.css @@ -0,0 +1,27 @@ +.login-gate-loading { + text-align: center; + margin-top: 8rem; + color: #9aa0ab; +} + +.login-gate { + max-width: 420px; + margin: 8rem auto 0; + text-align: center; + color: #e6e6e6; +} + +.login-gate-error { + color: #f87171; +} + +.login-gate-button { + display: inline-block; + margin-top: 1rem; + padding: 0.6rem 1.5rem; + background: #3b82f6; + color: white; + border-radius: 6px; + text-decoration: none; + font-weight: 600; +} diff --git a/frontend/src/LoginGate.tsx b/frontend/src/LoginGate.tsx new file mode 100644 index 0000000..62c11b7 --- /dev/null +++ b/frontend/src/LoginGate.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; +import { api } from "./api/client"; +import "./LoginGate.css"; +import App from "./App"; +import type { SessionInfo } from "./types/api"; + +type Status = "loading" | "authenticated" | "unauthenticated"; + +const AUTH_ERROR_MESSAGES: Record = { + forbidden: "Your account isn't authorized for geniusrun.", + failed: "Login failed, please try again.", +}; + +// Wraps App: on mount, asks the backend whether this browser already has a +// valid session (GET /api/session/me). geniusrun has no anonymous view, so +// this is the only fork in the whole frontend between "show the login +// screen" and "show the app." +export function LoginGate() { + const [status, setStatus] = useState("loading"); + const [session, setSession] = useState(null); + + useEffect(() => { + api + .getSessionInfo() + .then((s) => { + setSession(s); + setStatus("authenticated"); + }) + .catch(() => setStatus("unauthenticated")); + }, []); + + if (status === "loading") { + return
Loading…
; + } + + if (status === "unauthenticated") { + const authError = new URLSearchParams(window.location.search).get("auth_error"); + return ( +
+

🧞‍♀️ geniusrun

+ {authError &&

{AUTH_ERROR_MESSAGES[authError] ?? "Login failed, please try again."}

} + + Log in + +
+ ); + } + + return ; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 4aff025..a7d052f 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,9 +1,9 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import App from './App.tsx' +import { LoginGate } from './LoginGate.tsx' createRoot(document.getElementById('root')!).render( - + , )