From 7f5d2da6a24e479da85a35f4d60c28dc0063e62b Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 25 Jul 2026 18:42:23 +0200 Subject: [PATCH] frontend: add Create-profile setup screen for brand-new accounts LoginGate now shows CreateProfile (display name only) instead of App when an authenticated session has no provisioned geniusrun profile yet, backed by the new POST /api/setup endpoint and session/me's has_profile flag. --- frontend/src/CreateProfile.css | 43 ++++++++++++++++++++++++++++ frontend/src/CreateProfile.tsx | 52 ++++++++++++++++++++++++++++++++++ frontend/src/LoginGate.tsx | 15 ++++++++-- frontend/src/api/client.ts | 5 ++++ frontend/src/types/api.ts | 2 ++ 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 frontend/src/CreateProfile.css create mode 100644 frontend/src/CreateProfile.tsx diff --git a/frontend/src/CreateProfile.css b/frontend/src/CreateProfile.css new file mode 100644 index 0000000..01bda25 --- /dev/null +++ b/frontend/src/CreateProfile.css @@ -0,0 +1,43 @@ +.create-profile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + gap: 1rem; + text-align: center; +} + +.create-profile form { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; + max-width: 320px; +} + +.create-profile input { + padding: 0.6rem 0.8rem; + font-size: 1rem; + border-radius: 0.5rem; + border: 1px solid #ccc; +} + +.create-profile button { + padding: 0.6rem 0.8rem; + font-size: 1rem; + border-radius: 0.5rem; + border: none; + background: #3b82f6; + color: white; + cursor: pointer; +} + +.create-profile button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.create-profile-error { + color: #ef4444; +} diff --git a/frontend/src/CreateProfile.tsx b/frontend/src/CreateProfile.tsx new file mode 100644 index 0000000..ae621b3 --- /dev/null +++ b/frontend/src/CreateProfile.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { api } from "./api/client"; +import "./CreateProfile.css"; + +// Shown once, right after a brand-new OIDC login, before the account has a +// geniusrun profile at all. The only thing asked for is a display name -- +// Garmin credentials and every other tunable are filled in afterward via +// the existing Profile screen, same as a fresh single-user install today. +export function CreateProfile({ onCreated }: { onCreated: (displayName: string) => void }) { + const [displayName, setDisplayName] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const trimmed = displayName.trim(); + if (!trimmed) { + setError("Please enter a display name."); + return; + } + setSubmitting(true); + setError(null); + try { + const result = await api.setup(trimmed); + onCreated(result.display_name); + } catch (err) { + setError(err instanceof Error ? err.message : "Something went wrong, please try again."); + setSubmitting(false); + } + }; + + return ( +
+

🧞‍♀️ Welcome to geniusrun

+

Let's set up your profile. You can add your Garmin account afterward.

+
+ setDisplayName(e.target.value)} + disabled={submitting} + autoFocus + /> + {error &&

{error}

} + +
+
+ ); +} diff --git a/frontend/src/LoginGate.tsx b/frontend/src/LoginGate.tsx index 96a3558..73e63f1 100644 --- a/frontend/src/LoginGate.tsx +++ b/frontend/src/LoginGate.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { api, BASE_URL } from "./api/client"; import "./LoginGate.css"; import App from "./App"; +import { CreateProfile } from "./CreateProfile"; import type { SessionInfo } from "./types/api"; type Status = "loading" | "authenticated" | "unauthenticated"; @@ -13,8 +14,10 @@ const AUTH_ERROR_MESSAGES: Record = { // 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." +// this is the first fork -- "show the login screen" vs "show the app." A +// second fork, once authenticated, is whether the session's account has a +// provisioned profile yet (session.has_profile) -- a brand-new OIDC login +// sees CreateProfile instead of App until it submits one. export function LoginGate() { const [status, setStatus] = useState("loading"); const [session, setSession] = useState(null); @@ -46,5 +49,13 @@ export function LoginGate() { ); } + if (!session!.has_profile) { + return ( + setSession((s) => (s ? { ...s, has_profile: true, display_name: displayName } : s))} + /> + ); + } + return ; } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 1d16f26..e21cdb7 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -46,6 +46,11 @@ export const api = { // browser navigation. Logout must POST (see server.go's route), which an // can't do, hence the form. getSessionInfo: () => request("/api/session/me"), + setup: (displayName: string) => + request<{ user_id: number; display_name: string }>("/api/setup", { + method: "POST", + body: JSON.stringify({ display_name: displayName }), + }), // Auth login: () => request("/api/auth/login", { method: "POST" }), diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 348bfa0..0c2359d 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -189,6 +189,8 @@ export interface AuthResponse { export interface SessionInfo { name: string; email: string; + has_profile: boolean; + display_name?: string; } export interface DetailFillProgress {