Skip to content

Commit 3e7237a

Browse files
committed
style: professional product-site redesign — top nav, gradient hero, textured bg, refined cards/typography
1 parent 7e3b814 commit 3e7237a

10 files changed

Lines changed: 241 additions & 136 deletions

File tree

456 KB
Loading
589 KB
Loading
514 KB
Loading

frontend/src/App.tsx

Lines changed: 69 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { useEffect, useState } from "react";
22
import { api } from "./api";
3-
import { Spinner } from "./ui";
3+
import { GhostButton, PrimaryButton, Spinner } from "./ui";
44
import { Explorer } from "./views/Explorer";
55
import { Overview } from "./views/Overview";
66
import { Review } from "./views/Review";
77

8-
const VIEWS = ["Overview", "Incident Explorer", "Review Queue"] as const;
8+
const VIEWS = ["Overview", "Explorer", "Review"] as const;
99
type View = (typeof VIEWS)[number];
1010

11+
const DOCS_URL = "https://kohsheen1234.github.io/Open-source-AI-Incident-Observatory/";
12+
const REPO_URL = "https://github.com/kohsheen1234/Open-source-AI-Incident-Observatory";
13+
const API_DOCS = "https://agentwatch-api-7mhz.onrender.com/docs";
14+
1115
export default function App() {
1216
const [view, setView] = useState<View>("Overview");
1317
const [ready, setReady] = useState<boolean | null>(null);
@@ -29,76 +33,85 @@ export default function App() {
2933
}, []);
3034

3135
return (
32-
<div className="min-h-screen flex">
33-
<aside className="w-64 shrink-0 border-r border-border bg-[#171717] hidden md:flex flex-col p-6">
34-
<div className="flex items-center gap-2 text-lg font-bold text-white">🔭 AgentWatch</div>
35-
<div className="text-xs text-muted mt-1">Public AI-agent incident observatory</div>
36+
<div className="relative min-h-screen flex flex-col">
37+
<div className="page-bg" />
3638

37-
<nav className="mt-8 flex flex-col gap-1">
38-
{VIEWS.map((v) => (
39-
<button
40-
key={v}
41-
onClick={() => setView(v)}
42-
className={`text-left px-3 py-2 rounded-md text-sm transition-colors ${
43-
view === v ? "bg-brand/10 text-brand font-medium" : "text-muted hover:text-ink hover:bg-panel"
44-
}`}
45-
>
46-
{v}
47-
</button>
48-
))}
49-
</nav>
39+
<header className="sticky top-0 z-30 border-b border-border bg-bg/70 backdrop-blur-xl">
40+
<div className="max-w-content mx-auto px-6 h-16 flex items-center justify-between">
41+
<button onClick={() => setView("Overview")} className="flex items-center gap-2.5 group">
42+
<span className="text-xl">🔭</span>
43+
<span className="font-bold text-ink tracking-tight">AgentWatch</span>
44+
<span className="hidden sm:inline eyebrow ml-1">Observatory</span>
45+
</button>
5046

51-
<div className="mt-auto pt-8 text-xs text-muted space-y-3">
52-
<div className="border border-border rounded-lg p-3 bg-surface">
53-
<div className="font-mono uppercase tracking-wider text-[0.65rem] text-brand mb-1">Data sources</div>
54-
Hacker News (live), Reddit (optional), and a bundled sample set. A demo instance —
55-
classifications are automated and unverified until reviewed.
56-
</div>
57-
<div className="flex gap-3">
58-
<a href="https://kohsheen1234.github.io/Open-source-AI-Incident-Observatory/">Docs</a>
59-
<a href="https://github.com/kohsheen1234/Open-source-AI-Incident-Observatory">Code</a>
47+
<nav className="hidden md:flex items-center gap-1">
48+
{VIEWS.map((v) => (
49+
<button
50+
key={v}
51+
onClick={() => setView(v)}
52+
className={`px-3.5 py-1.5 rounded-lg text-sm transition-colors ${
53+
view === v ? "text-ink bg-panel" : "text-muted hover:text-ink"
54+
}`}
55+
>
56+
{v}
57+
</button>
58+
))}
59+
</nav>
60+
61+
<div className="flex items-center gap-2">
62+
<a className="text-sm text-muted hover:text-ink no-underline hidden sm:inline" href={API_DOCS} target="_blank" rel="noreferrer">
63+
API
64+
</a>
65+
<a className="text-sm text-muted hover:text-ink no-underline hidden sm:inline" href={DOCS_URL} target="_blank" rel="noreferrer">
66+
Docs
67+
</a>
68+
<GhostButton href={REPO_URL}>GitHub ↗</GhostButton>
6069
</div>
6170
</div>
62-
</aside>
63-
64-
<main className="flex-1 min-w-0">
65-
{/* Mobile nav */}
66-
<div className="md:hidden flex gap-1 p-3 border-b border-border bg-[#171717] overflow-x-auto">
71+
{/* mobile nav */}
72+
<div className="md:hidden flex gap-1 px-4 pb-3 overflow-x-auto">
6773
{VIEWS.map((v) => (
6874
<button
6975
key={v}
7076
onClick={() => setView(v)}
71-
className={`px-3 py-1.5 rounded-md text-xs whitespace-nowrap ${
72-
view === v ? "bg-brand/10 text-brand" : "text-muted"
77+
className={`px-3 py-1.5 rounded-lg text-xs whitespace-nowrap ${
78+
view === v ? "text-ink bg-panel" : "text-muted"
7379
}`}
7480
>
7581
{v}
7682
</button>
7783
))}
7884
</div>
85+
</header>
7986

80-
<div className="max-w-6xl mx-auto px-6 py-10">
81-
{ready === null && (
82-
<Spinner label="Connecting to the API… the first load after idle can take up to ~60s while the free-tier backend wakes up. Thanks for your patience." />
83-
)}
84-
{ready === false && (
85-
<div className="text-center py-24">
86-
<p className="text-muted mb-4">
87-
The API is still waking up (free-tier cold start). It should be ready shortly.
88-
</p>
89-
<button
90-
onClick={waitForApi}
91-
className="bg-brand hover:bg-brand-hover text-[#0b0f0d] font-semibold px-4 py-2 rounded-md"
92-
>
93-
Retry
94-
</button>
95-
</div>
96-
)}
97-
{ready === true && view === "Overview" && <Overview />}
98-
{ready === true && view === "Incident Explorer" && <Explorer />}
99-
{ready === true && view === "Review Queue" && <Review />}
100-
</div>
87+
<main className="relative z-10 flex-1 w-full max-w-content mx-auto px-6 py-12">
88+
{ready === null && (
89+
<Spinner label="Connecting to the API… the first load after idle can take up to ~60s while the free-tier backend wakes up. Thanks for your patience." />
90+
)}
91+
{ready === false && (
92+
<div className="text-center py-28">
93+
<p className="text-muted mb-5 max-w-md mx-auto">
94+
The API is still waking up (free-tier cold start). It should be ready in a few
95+
seconds.
96+
</p>
97+
<PrimaryButton onClick={waitForApi}>Retry</PrimaryButton>
98+
</div>
99+
)}
100+
{ready === true && view === "Overview" && <Overview onExplore={() => setView("Explorer")} />}
101+
{ready === true && view === "Explorer" && <Explorer />}
102+
{ready === true && view === "Review" && <Review />}
101103
</main>
104+
105+
<footer className="relative z-10 border-t border-border">
106+
<div className="max-w-content mx-auto px-6 py-8 flex flex-col sm:flex-row gap-3 items-center justify-between text-sm text-faint">
107+
<div>AgentWatch — open-source observatory for public AI-agent incidents.</div>
108+
<div className="flex gap-4">
109+
<a href={REPO_URL} target="_blank" rel="noreferrer">GitHub</a>
110+
<a href={DOCS_URL} target="_blank" rel="noreferrer">Docs</a>
111+
<a href={API_DOCS} target="_blank" rel="noreferrer">API</a>
112+
</div>
113+
</div>
114+
</footer>
102115
</div>
103116
);
104117
}

frontend/src/index.css

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,49 @@ body,
1414

1515
body {
1616
margin: 0;
17-
background-color: #1c1c1c;
18-
color: #ededed;
17+
background-color: #0d0d0f;
18+
color: #f2f2f3;
1919
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
2020
-webkit-font-smoothing: antialiased;
21+
text-rendering: optimizeLegibility;
2122
}
2223

23-
/* Subtle green glow behind the hero, like Supabase. */
24-
.hero-glow::before {
25-
content: "";
26-
position: absolute;
27-
top: -180px;
28-
left: 50%;
29-
transform: translateX(-50%);
30-
width: 900px;
31-
height: 380px;
24+
/* Ambient page texture: faint dotted grid + top gradient glows. */
25+
.page-bg {
26+
position: fixed;
27+
inset: 0;
28+
z-index: 0;
3229
pointer-events: none;
33-
background: radial-gradient(ellipse at center, rgba(62, 207, 142, 0.14), rgba(62, 207, 142, 0) 70%);
30+
background-image:
31+
radial-gradient(circle at 18% -10%, rgba(62, 207, 142, 0.10), transparent 42%),
32+
radial-gradient(circle at 82% -6%, rgba(34, 211, 238, 0.08), transparent 40%),
33+
radial-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px);
34+
background-size: 100% 100%, 100% 100%, 26px 26px;
35+
}
36+
37+
/* Gradient display text — the site's signature. */
38+
.grad-text {
39+
background: linear-gradient(100deg, #ffffff 0%, #baf7dc 40%, #3ecf8e 70%, #22d3ee 100%);
40+
-webkit-background-clip: text;
41+
background-clip: text;
42+
-webkit-text-fill-color: transparent;
43+
color: transparent;
44+
}
45+
46+
.eyebrow {
47+
font-family: "JetBrains Mono", ui-monospace, monospace;
48+
color: #3ecf8e;
49+
font-size: 0.72rem;
50+
letter-spacing: 0.18em;
51+
text-transform: uppercase;
3452
}
3553

3654
::-webkit-scrollbar {
3755
width: 10px;
3856
height: 10px;
3957
}
4058
::-webkit-scrollbar-thumb {
41-
background: #2e2e2e;
59+
background: #262629;
4260
border-radius: 6px;
4361
}
4462
::-webkit-scrollbar-track {
@@ -52,3 +70,16 @@ a {
5270
a:hover {
5371
text-decoration: underline;
5472
}
73+
74+
*:focus-visible {
75+
outline: 2px solid #3ecf8e;
76+
outline-offset: 2px;
77+
border-radius: 4px;
78+
}
79+
80+
@media (prefers-reduced-motion: reduce) {
81+
* {
82+
animation-duration: 0.01ms !important;
83+
transition-duration: 0.01ms !important;
84+
}
85+
}

frontend/src/ui.tsx

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ import type { ReactNode } from "react";
22
import { sevColor, typeColor } from "./theme";
33

44
export function Eyebrow({ children }: { children: ReactNode }) {
5-
return (
6-
<div className="font-mono text-brand text-xs uppercase tracking-[0.14em] mb-3">{children}</div>
7-
);
5+
return <div className="eyebrow mb-3">{children}</div>;
86
}
97

108
export function Card({ children, className = "" }: { children: ReactNode; className?: string }) {
119
return (
12-
<div className={`bg-surface border border-border rounded-xl ${className}`}>{children}</div>
10+
<div
11+
className={`bg-surface border border-border rounded-2xl shadow-card transition-colors hover:border-border-strong ${className}`}
12+
>
13+
{children}
14+
</div>
1315
);
1416
}
1517

1618
export function StatCard({ label, value, hint }: { label: string; value: ReactNode; hint?: string }) {
1719
return (
18-
<Card className="p-4">
19-
<div className="font-mono text-[0.7rem] uppercase tracking-wider text-muted">{label}</div>
20-
<div className="text-3xl font-bold text-ink mt-1">{value}</div>
20+
<Card className="p-5">
21+
<div className="font-mono text-[0.68rem] uppercase tracking-[0.12em] text-faint">{label}</div>
22+
<div className="text-3xl font-bold text-ink mt-2 tabular-nums">{value}</div>
2123
{hint && <div className="text-xs text-muted mt-1">{hint}</div>}
2224
</Card>
2325
);
@@ -40,19 +42,62 @@ export function TypeBadge({ type }: { type: string | null }) {
4042

4143
export function SeverityChip({ severity }: { severity: number | null }) {
4244
return (
43-
<Pill text={severity == null ? "severity —" : `severity ${severity}/5`} color={sevColor(severity)} />
45+
<Pill
46+
text={severity == null ? "severity —" : `severity ${severity}/5`}
47+
color={sevColor(severity)}
48+
/>
49+
);
50+
}
51+
52+
export function PrimaryButton({
53+
children,
54+
onClick,
55+
href,
56+
}: {
57+
children: ReactNode;
58+
onClick?: () => void;
59+
href?: string;
60+
}) {
61+
const cls =
62+
"inline-flex items-center gap-1.5 rounded-lg bg-brand px-4 py-2 text-sm font-semibold text-[#04140c] hover:bg-[#57d99e] transition-colors no-underline";
63+
return href ? (
64+
<a className={cls} href={href} target={href.startsWith("http") ? "_blank" : undefined} rel="noreferrer">
65+
{children}
66+
</a>
67+
) : (
68+
<button className={cls} onClick={onClick}>
69+
{children}
70+
</button>
71+
);
72+
}
73+
74+
export function GhostButton({ children, href }: { children: ReactNode; href: string }) {
75+
return (
76+
<a
77+
className="inline-flex items-center gap-1.5 rounded-lg border border-border-strong px-4 py-2 text-sm font-medium text-ink hover:border-brand hover:text-brand transition-colors no-underline"
78+
href={href}
79+
target={href.startsWith("http") ? "_blank" : undefined}
80+
rel="noreferrer"
81+
>
82+
{children}
83+
</a>
4484
);
4585
}
4686

4787
export function Spinner({ label }: { label: string }) {
4888
return (
49-
<div className="flex flex-col items-center justify-center gap-4 py-24 text-center">
89+
<div className="flex flex-col items-center justify-center gap-4 py-28 text-center">
5090
<div className="h-10 w-10 rounded-full border-2 border-border border-t-brand animate-spin" />
5191
<p className="text-muted max-w-md">{label}</p>
5292
</div>
5393
);
5494
}
5595

56-
export function SectionTitle({ children }: { children: ReactNode }) {
57-
return <h2 className="text-xl font-bold text-ink tracking-tight mb-3">{children}</h2>;
96+
export function SectionHeader({ eyebrow, title }: { eyebrow?: string; title: string }) {
97+
return (
98+
<div className="mb-4">
99+
{eyebrow && <div className="eyebrow mb-1.5">{eyebrow}</div>}
100+
<h2 className="text-2xl font-bold text-ink tracking-tight">{title}</h2>
101+
</div>
102+
);
58103
}

frontend/src/views/Explorer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
22
import { api } from "../api";
33
import { INCIDENT_TYPES, cleanText } from "../theme";
44
import type { IncidentDetail, Page } from "../types";
5-
import { Card, SectionTitle, SeverityChip, TypeBadge } from "../ui";
5+
import { Card, SectionHeader, SeverityChip, TypeBadge } from "../ui";
66

77
export function Explorer() {
88
const [page, setPage] = useState<Page | null>(null);
@@ -32,8 +32,8 @@ export function Explorer() {
3232

3333
return (
3434
<div>
35-
<SectionTitle>Incident Explorer</SectionTitle>
36-
<p className="text-muted mb-5">
35+
<SectionHeader eyebrow="Browse" title="Incident Explorer" />
36+
<p className="text-muted mb-5 -mt-2">
3737
Browse and filter every collected incident. <span className="text-ink">Each row is a real
3838
public post.</span>
3939
</p>

0 commit comments

Comments
 (0)