< Summary

Information
Class: site-header.tsx
Assembly: app.components
File(s): /home/runner/work/ClutterStock/ClutterStock/frontend/app/components/site-header.tsx
Tag: 58_25416222083
Line coverage
0%
Covered lines: 0
Uncovered lines: 45
Coverable lines: 45
Total lines: 237
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 18
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/ClutterStock/ClutterStock/frontend/app/components/site-header.tsx

#LineLine coverage
 1import { useEffect, useState, useSyncExternalStore } from "react";
 2import { Link, useNavigate, useRouteLoaderData } from "react-router";
 3import type { SessionUser } from "~/lib/session.server";
 4import { UserModal } from "./user-modal";
 5import { TuiUserPanel } from "./tui-user-panel";
 6import { useTheme } from "~/lib/theme";
 7
 8type ThemeId = "system" | "tui" | "win98" | "cde";
 09const THEMES: ThemeId[] = ["system", "tui", "win98", "cde"];
 010const THEME_LABELS: Record<ThemeId, string> = {
 11  system: "SYS",
 12  tui:    "TUI",
 13  win98:  "W98",
 14  cde:    "CDE",
 15};
 016const STORAGE_KEY = "cs-theme";
 17
 018function applyTheme(t: ThemeId) {
 019  if (t === "system") {
 020    document.documentElement.removeAttribute("data-theme");
 021    localStorage.removeItem(STORAGE_KEY);
 22  } else {
 023    document.documentElement.setAttribute("data-theme", t);
 024    localStorage.setItem(STORAGE_KEY, t);
 25  }
 26}
 27
 028export function SiteHeader() {
 029  const rootData = useRouteLoaderData("root") as { user: SessionUser | null } | undefined;
 030  const user = rootData?.user ?? null;
 31
 32  // useSyncExternalStore returns false on server, true on client — no effect needed
 033  const mounted = useSyncExternalStore(() => () => {}, () => true, () => false);
 034  const [modalOpen, setModalOpen] = useState(false);
 035  const [theme, setThemeState] = useState<ThemeId>(() => {
 036    if (typeof window === "undefined") return "system";
 037    try {
 038      const stored = localStorage.getItem(STORAGE_KEY) as ThemeId | null;
 039      if (stored && THEMES.includes(stored)) return stored;
 40    } catch { /* ignore */ }
 041    return "system";
 42  });
 043  const navigate = useNavigate();
 044  const activeTheme = useTheme();
 045  const isTui = activeTheme === "tui";
 46
 47  // Re-apply the stored theme on mount in case hydration stripped the
 48  // data-theme attribute that the pre-paint script set on <html>.
 049  useEffect(() => {
 050    applyTheme(theme);
 51  }, [theme]);
 52
 053  function cycleTheme() {
 054    const next = THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length] ?? "system";
 055    setThemeState(next);
 056    applyTheme(next);
 57  }
 58
 059  function handleSignIn() {
 060    navigate("/auth/signin");
 61  }
 62
 063  function handleSignOut() {
 064    setModalOpen(false);
 065    navigate("/auth/signout");
 66  }
 67
 068  const displayName = user?.name ?? user?.preferred_username;
 069  const initial = (displayName ?? "?")[0]?.toUpperCase() ?? "?";
 70
 071  return (
 72    <>
 73      <header style={{
 74        display: "flex",
 75        alignItems: "center",
 76        justifyContent: "space-between",
 77        padding: "0 20px",
 78        borderBottom: "1px solid var(--c-border)",
 79        background: "var(--c-bg-2)",
 80        height: 48,
 81        flexShrink: 0,
 82        position: "sticky",
 83        top: 0,
 84        zIndex: 10,
 85      }}>
 86        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
 87          <Link
 88            to="/"
 89            style={{ display: "flex", alignItems: "center", gap: 10, textDecoration: "none" }}
 90          >
 91            <img
 92              className="modern-logo"
 93              src="/brand/icon.svg"
 94              alt=""
 95              width={26}
 96              height={26}
 97              style={{ display: "block", flexShrink: 0 }}
 98            />
 99            <span className="modern-logo" style={{
 100              fontSize: 14,
 101              fontWeight: 700,
 102              color: "var(--c-fg)",
 103              fontFamily: "ui-monospace, 'JetBrains Mono', monospace",
 104              letterSpacing: "-0.02em",
 105            }}>
 106              clutter<span style={{ color: "#c4502a" }}>:stock</span>
 107            </span>
 108            <span className="tui-brand" style={{ fontSize: 13, color: "var(--c-fg)", fontFamily: "inherit" }}>
 109              ╭─[ <strong>clutter<span style={{ color: "#c4502a" }}>:stock</span></strong> ]─
 110            </span>
 111            <span className="cde-brand" style={{ fontWeight: 700, fontSize: 12, color: "var(--c-fg)" }}>
 112              clutter<span style={{ color: "#c4502a" }}>:stock</span> — Home
 113            </span>
 114          </Link>
 115        </div>
 116
 117        <div style={{ display: "flex", alignItems: "center", gap: 10, height: 32 }}>
 118          {mounted && (
 119            <span className="tui-brand" style={{ fontSize: 11, color: "var(--c-fg-2)", fontFamily: "inherit" }}>
 120              {new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
 121            </span>
 122          )}
 123          <button
 124            onClick={cycleTheme}
 125            title={`Theme: ${theme} — click to cycle`}
 126            className="theme-toggle"
 127            style={{
 128              fontFamily: "ui-monospace, monospace",
 129              fontSize: 11,
 130              padding: "3px 8px",
 131              border: "1px solid var(--c-border)",
 132              background: "transparent",
 133              color: "var(--c-fg-3)",
 134              cursor: "pointer",
 135              letterSpacing: "0.04em",
 136            }}
 137          >
 138            [{THEME_LABELS[theme]}]<span className="tui-cursor">▌</span>
 139          </button>
 140
 141          {mounted && (
 142            user ? (
 143              <>
 144                {/* Modern avatar pill — hidden under TUI via .modern-user CSS swap */}
 145                <button
 146                  type="button"
 0147                  onClick={() => setModalOpen(true)}
 148                  className="modern-user"
 149                  style={{
 150                    display: "flex",
 151                    alignItems: "center",
 152                    gap: 7,
 153                    padding: "4px 10px 4px 4px",
 154                    borderRadius: 999,
 155                    border: "1px solid var(--c-border)",
 156                    background: "var(--c-bg-3)",
 157                    cursor: "pointer",
 158                    fontSize: 12,
 159                    color: "var(--c-fg-2)",
 160                    fontWeight: 500,
 161                  }}
 162                  aria-label="Account"
 163                >
 164                  <span style={{
 165                    display: "flex",
 166                    alignItems: "center",
 167                    justifyContent: "center",
 168                    width: 22,
 169                    height: 22,
 170                    borderRadius: 999,
 171                    background: "var(--c-accent)",
 172                    color: "white",
 173                    fontSize: 10,
 174                    fontWeight: 700,
 175                    flexShrink: 0,
 176                  }}>
 177                    {initial}
 178                  </span>
 179                  <span style={{ maxWidth: 120, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
 180                    {displayName}
 181                  </span>
 182                </button>
 183                {/* TUI chip — shown only under TUI via .tui-user CSS swap */}
 184                <button
 185                  type="button"
 0186                  onClick={() => setModalOpen(o => !o)}
 187                  className="tui-user tui-user-chip"
 188                  aria-label="Account"
 189                >
 190                  <span className="cs-tui-topbar-bracket">[ </span>
 191                  <span className="tui-user-chip-name">{displayName}</span>
 192                  <span className="tui-cursor">▌</span>
 193                  <span className="cs-tui-topbar-bracket"> ]</span>
 194                </button>
 195              </>
 196            ) : (
 197              <button
 198                type="button"
 199                onClick={handleSignIn}
 200                className="btn-primary"
 201              >
 202                Sign in
 203              </button>
 204            )
 205          )}
 206          <div className="win98-wincontrols">
 0207            {["_", "□", "✕"].map(c => (
 0208              <button key={c} className="win98-wincontrol">{c}</button>
 209            ))}
 210          </div>
 211          <div className="cde-wincontrols">
 0212            {["_", "□"].map(c => (
 0213              <button key={c} className="cde-wincontrol" type="button">{c}</button>
 214            ))}
 215          </div>
 216        </div>
 217      </header>
 218
 219      {user && (
 220        <>
 221          <UserModal
 222            user={user}
 223            open={modalOpen && !isTui}
 0224            onClose={() => setModalOpen(false)}
 225            onSignOut={handleSignOut}
 226          />
 227          <TuiUserPanel
 228            user={user}
 229            open={modalOpen && isTui}
 0230            onClose={() => setModalOpen(false)}
 231            onSignOut={handleSignOut}
 232          />
 233        </>
 234      )}
 235    </>
 236  );
 237}