feat(dashboard): real user in sidebar + sign-out; strip demo/mock hints
- Sidebar footer now shows the ACE identity (was static 'James Carter') with a Sign-out menu; topbar user gets a Sign-out dropdown too. Both call useAuth().logout. - Remove all visible 'Demo:' hints from login + register and the 'Demo mode' subtitle.
This commit is contained in:
@@ -106,6 +106,11 @@
|
||||
.sb-user .av { width: 30px; height: 30px; border-radius: 8px; object-fit: cover; flex: 0 0 30px; }
|
||||
.sb-user .nm { font-size: 12.5px; font-weight: 600; }
|
||||
.sb-user .rl { font-size: 11px; color: var(--faint); }
|
||||
.sb-user .nm, .sb-user .rl { max-width: 130px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sb-user-menu { position: absolute; bottom: 56px; left: 12px; right: 12px; z-index: 30; padding: 6px; border-radius: 12px; border: 1px solid var(--border-2); background: var(--panel); box-shadow: var(--shadow), 0 8px 30px -12px rgba(0,0,0,0.55); animation: ds-rise 0.14s ease; }
|
||||
.sb-user-menu button { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: none; color: var(--text-2); font-family: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; text-align: left; }
|
||||
.sb-user-menu button:hover { background: var(--panel-2); }
|
||||
.sb-user-menu button.danger { color: var(--red, #ef4444); }
|
||||
|
||||
/* ---- main ---- */
|
||||
.dash-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronsUpDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChevronsUpDown, LogOut } from "lucide-react";
|
||||
import { useAuth } from "@abe-kap/appshell-sdk/react";
|
||||
import { Icon } from "./ui";
|
||||
import { user } from "./account-data";
|
||||
|
||||
function initialsOf(name: string): string {
|
||||
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]).join("").toUpperCase() || "?";
|
||||
}
|
||||
|
||||
export type NavItem = { key: string; label: string; icon: string; subtitle?: string };
|
||||
export type NavGroup = { title: string; items: NavItem[] };
|
||||
|
||||
@@ -61,6 +68,21 @@ export const NAV_GROUPS: NavGroup[] = [
|
||||
export const NAV_ITEMS: NavItem[] = NAV_GROUPS.flatMap((g) => g.items);
|
||||
|
||||
export function Sidebar({ active, onSelect }: { active: string; onSelect: (k: string) => void }) {
|
||||
const router = useRouter();
|
||||
const { user: me, logout } = useAuth();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
// Real signed-in identity from the App Context Envelope; fall back to the static
|
||||
// demo user only when the Shell isn't wired.
|
||||
const name = me?.displayName || user.name;
|
||||
const initials = me ? initialsOf(me.displayName) : user.initials;
|
||||
const secondary = me?.email || user.role;
|
||||
|
||||
async function signOut() {
|
||||
setMenuOpen(false);
|
||||
try { await logout(); } catch { /* ignore — proceed to portal either way */ }
|
||||
router.replace("/portal/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="dash-sidebar">
|
||||
<div className="dash-brand">
|
||||
@@ -82,12 +104,20 @@ export function Sidebar({ active, onSelect }: { active: string; onSelect: (k: st
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="sb-foot">
|
||||
<button className="sb-user">
|
||||
<span className="av" style={{ background: user.avatarGradient, display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>{user.initials}</span>
|
||||
<div style={{ flex: 1, textAlign: "left" }}>
|
||||
<div className="nm">{user.name}</div>
|
||||
<div className="rl">{user.role}</div>
|
||||
<div className="sb-foot" style={{ position: "relative" }}>
|
||||
{menuOpen && (
|
||||
<>
|
||||
<div className="tm-menu-scrim" onClick={() => setMenuOpen(false)} />
|
||||
<div className="sb-user-menu" role="menu">
|
||||
<button className="danger" onClick={signOut}><LogOut size={15} /> Sign out</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button className="sb-user" onClick={() => setMenuOpen((o) => !o)} aria-haspopup="menu" aria-expanded={menuOpen}>
|
||||
<span className="av" style={{ background: user.avatarGradient, display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>{initials}</span>
|
||||
<div style={{ flex: 1, textAlign: "left", minWidth: 0 }}>
|
||||
<div className="nm">{name}</div>
|
||||
<div className="rl" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{secondary}</div>
|
||||
</div>
|
||||
<ChevronsUpDown size={15} />
|
||||
</button>
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { Sun, Moon, ChevronDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Sun, Moon, ChevronDown, LogOut } from "lucide-react";
|
||||
import { useAuth } from "@abe-kap/appshell-sdk/react";
|
||||
import { Icon } from "./ui";
|
||||
import { user } from "./account-data";
|
||||
import { useAuth } from "@abe-kap/appshell-sdk/react";
|
||||
|
||||
function initialsOf(name: string): string {
|
||||
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]).join("").toUpperCase();
|
||||
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]).join("").toUpperCase() || "?";
|
||||
}
|
||||
|
||||
export function Topbar({ theme, onToggle, title, subtitle }: { theme: "dark" | "light"; onToggle: () => void; title: string; subtitle: string }) {
|
||||
// When signed in through the Shell, show the real identity from the App Context
|
||||
// Envelope; otherwise fall back to the static demo user.
|
||||
const { user: me } = useAuth();
|
||||
const router = useRouter();
|
||||
const { user: me, logout } = useAuth();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const name = me?.displayName || user.name;
|
||||
const initials = me ? initialsOf(me.displayName) : user.initials;
|
||||
const secondary = me?.email || user.role;
|
||||
|
||||
async function signOut() {
|
||||
setMenuOpen(false);
|
||||
try { await logout(); } catch { /* ignore */ }
|
||||
router.replace("/portal/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="dash-topbar">
|
||||
<div className="dash-title">
|
||||
@@ -30,14 +42,24 @@ export function Topbar({ theme, onToggle, title, subtitle }: { theme: "dark" | "
|
||||
<Icon name="bell" size={18} />
|
||||
<span style={{ position: "absolute", top: 9, right: 10, width: 7, height: 7, borderRadius: 99, background: "var(--orange)", border: "2px solid var(--panel)" }} />
|
||||
</button>
|
||||
<button className="top-user">
|
||||
<span className="av" style={{ background: user.avatarGradient, display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>{initials}</span>
|
||||
<div style={{ textAlign: "left" }}>
|
||||
<div className="nm">{name}</div>
|
||||
<div className="rl">{user.role}</div>
|
||||
</div>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
<div className="top-user-wrap" style={{ position: "relative" }}>
|
||||
<button className="top-user" onClick={() => setMenuOpen((o) => !o)} aria-haspopup="menu" aria-expanded={menuOpen}>
|
||||
<span className="av" style={{ background: user.avatarGradient, display: "grid", placeItems: "center", color: "#fff", fontWeight: 700, fontSize: 12 }}>{initials}</span>
|
||||
<div style={{ textAlign: "left", minWidth: 0 }}>
|
||||
<div className="nm">{name}</div>
|
||||
<div className="rl" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 150 }}>{secondary}</div>
|
||||
</div>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{menuOpen && (
|
||||
<>
|
||||
<div className="tm-menu-scrim" onClick={() => setMenuOpen(false)} />
|
||||
<div className="tm-menu-pop" role="menu">
|
||||
<button className="danger" onClick={signOut}><LogOut size={15} /> Sign out</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -151,7 +151,6 @@ export function LoginFlow() {
|
||||
<Spinner lg />
|
||||
<div>
|
||||
<h1 style={{ fontSize: 20 }}>{provider ? `Connecting to ${cap(provider)}…` : "Looking up your account…"}</h1>
|
||||
{provider && <p className="sub" style={{ marginTop: 6 }}>Demo mode — no provider keys configured.</p>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -302,7 +301,6 @@ function Identify({ email, setEmail, onSocial, onEmail, onPhone, toRegister }: {
|
||||
<span><Icon name="shield" size={13} /> Licensed & Insured</span>
|
||||
<span><Icon name="check" size={13} /> Drone-Powered</span>
|
||||
</div>
|
||||
<p className="demo-hint">Demo: any email signs in; an email starting with “new” shows “not found”.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -337,7 +335,6 @@ function Passkey({ account, onBack, onPassword, onAnother, onSuccess, onFail }:
|
||||
<button className="link" onClick={onPassword}>Use your password instead</button>
|
||||
<button className="link" onClick={onAnother}>Try another way</button>
|
||||
</div>
|
||||
<p className="demo-hint">Demo: passkey always succeeds here.</p>
|
||||
<button hidden onClick={onFail} />
|
||||
</div>
|
||||
);
|
||||
@@ -390,7 +387,6 @@ function Password({ account, email, login, onAuthenticated, flash, onBack, onFor
|
||||
<button className="link" onClick={onForgot}>Forgot password?</button>
|
||||
<button className="link" onClick={onOtp}>Sign in with a one-time code</button>
|
||||
</div>
|
||||
{!isShellConfigured() && <p className="demo-hint">Demo: any password works; type “wrong” to see the lockout. Password always needs 2-step next.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -471,7 +467,6 @@ function OtpVerify({ account, email, initialChannel = "email", remember, setReme
|
||||
<span />
|
||||
</div>
|
||||
<RememberDevice checked={remember} onChange={setRemember} note="2-step is still required at each sign-in." />
|
||||
{!shell && <p className="demo-hint">Demo: any 6 digits verify; “000000” shows an error.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -526,7 +521,6 @@ function Totp({ remember, setRemember, onBack, onVerified }: { remember: boolean
|
||||
{setupKey ? "Enter a 6-digit code instead" : "Enter setup key instead"}
|
||||
</button>
|
||||
<RememberDevice checked={remember} onChange={setRemember} />
|
||||
<p className="demo-hint">Demo: any 6 digits verify; “000000” fails.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -545,7 +539,6 @@ function PushApprove({ onBack, onApproved, onCancel }: { onBack: () => void; onA
|
||||
<p className="sub" style={{ textAlign: "center" }}>{approved ? "Your sign-in was approved." : "We sent a notification to your mobile app. Approve it to continue."}</p>
|
||||
<div className="interstitial">{approved ? <Icon name="check" size={30} /> : <Spinner lg />}</div>
|
||||
{!approved && <button className="btn" onClick={onCancel}>Cancel</button>}
|
||||
<p className="demo-hint">Demo: auto-approves after ~3 seconds.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -629,7 +622,6 @@ function ForgotCode({ onBack, onOk }: { onBack: () => void; onOk: () => void })
|
||||
<div style={{ marginTop: 18 }}><OtpBoxes onComplete={(c) => (c === "000000" ? setError(true) : onOk())} error={error} /></div>
|
||||
{error && <div style={{ marginTop: 12 }}><FlashNote tone="error">Incorrect code.</FlashNote></div>}
|
||||
<div style={{ marginTop: 14 }}><ResendLink seconds={30} /></div>
|
||||
<p className="demo-hint">Demo: any 6 digits work; “000000” fails.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -404,7 +404,6 @@ function VerifyChannel({ kind, initial, country, cc, initialPhone, verified, onV
|
||||
<div style={{ marginTop: 12 }}><ResendLink seconds={60} /></div>
|
||||
</div>
|
||||
)}
|
||||
<p className="demo-hint">Demo: any 6 digits verify; “000000” fails. Email with “full”/“bo” shows send errors.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user