Skip to content

Commit 1fd53ae

Browse files
committed
feat: split homepage into pending-only view and separate completed page
1 parent bb7ebeb commit 1fd53ae

File tree

3 files changed

+186
-33
lines changed

3 files changed

+186
-33
lines changed

packages/web/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import HomePage from './pages/HomePage'
77
import FormReviewPage from './pages/FormReviewPage'
88
import SelectionPage from './pages/SelectionPage'
99
import PreferencesPage from './pages/PreferencesPage'
10+
import CompletedPage from './pages/CompletedPage'
1011

1112
type Theme = 'light' | 'dark' | 'system'
1213

@@ -96,6 +97,7 @@ export default function App() {
9697
<Route path="/form-review/:id" element={<FormReviewPage />} />
9798
<Route path="/selection/:id" element={<SelectionPage />} />
9899
<Route path="/preferences" element={<PreferencesPage />} />
100+
<Route path="/completed" element={<CompletedPage />} />
99101
</Routes>
100102
</BrowserRouter>
101103
)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useEffect, useState } from 'react'
2+
import { Link, useNavigate } from 'react-router-dom'
3+
4+
interface SessionItem {
5+
id: string
6+
type: string
7+
status: 'pending' | 'completed'
8+
createdAt: number
9+
subject?: string
10+
to?: string
11+
risk?: string
12+
command?: string
13+
}
14+
15+
const TYPE_LABELS: Record<string, string> = {
16+
email_review: 'Email',
17+
code_review: 'Code',
18+
action_approval: 'Approval',
19+
form_review: 'Form',
20+
selection_review: 'Selection',
21+
}
22+
23+
function sessionPath(s: SessionItem): string {
24+
if (s.type === 'action_approval') return `/approval/${s.id}`
25+
if (s.type === 'code_review') return `/code-review/${s.id}`
26+
if (s.type === 'form_review') return `/form-review/${s.id}`
27+
if (s.type === 'selection_review') return `/selection/${s.id}`
28+
return `/review/${s.id}`
29+
}
30+
31+
function sessionTitle(s: SessionItem): string {
32+
if (s.subject) return s.subject
33+
if (s.type === 'code_review' && s.command) return s.command
34+
return TYPE_LABELS[s.type] ?? s.type
35+
}
36+
37+
function formatTime(ts: number): string {
38+
const d = new Date(ts)
39+
const now = Date.now()
40+
const diffMs = now - ts
41+
const diffMin = Math.floor(diffMs / 60000)
42+
if (diffMin < 1) return 'just now'
43+
if (diffMin < 60) return `${diffMin}m ago`
44+
const diffHr = Math.floor(diffMin / 60)
45+
if (diffHr < 24) return `${diffHr}h ago`
46+
return d.toLocaleDateString()
47+
}
48+
49+
export default function CompletedPage() {
50+
const navigate = useNavigate()
51+
const [sessions, setSessions] = useState<SessionItem[]>([])
52+
const [loading, setLoading] = useState(true)
53+
54+
useEffect(() => {
55+
fetch('/api/sessions')
56+
.then(r => r.json())
57+
.then((data: SessionItem[]) => {
58+
setSessions(data.filter(s => s.status === 'completed'))
59+
setLoading(false)
60+
})
61+
.catch(() => setLoading(false))
62+
}, [])
63+
64+
return (
65+
<div className="min-h-screen bg-gray-50 dark:bg-zinc-950">
66+
<div className="max-w-2xl mx-auto py-10 px-4">
67+
68+
<div className="flex items-center gap-2 mb-1">
69+
<button
70+
onClick={() => navigate('/')}
71+
className="text-sm text-zinc-400 dark:text-slate-500 hover:text-zinc-600 dark:hover:text-slate-300 transition-colors"
72+
>
73+
← Back
74+
</button>
75+
</div>
76+
77+
<div className="mb-8">
78+
<p className="text-xs text-zinc-400 dark:text-slate-500 uppercase tracking-wider mb-1 font-medium">agentclick</p>
79+
<h1 className="text-xl font-semibold text-zinc-900 dark:text-slate-100">Completed</h1>
80+
<p className="text-sm text-zinc-500 dark:text-slate-400 mt-1">Sessions you've already reviewed.</p>
81+
</div>
82+
83+
{loading && (
84+
<p className="text-sm text-zinc-400 dark:text-slate-500">Loading...</p>
85+
)}
86+
87+
{!loading && sessions.length === 0 && (
88+
<p className="text-sm text-zinc-400 dark:text-slate-500">No completed sessions yet.</p>
89+
)}
90+
91+
{!loading && sessions.length > 0 && (
92+
<div className="space-y-2">
93+
{sessions.map(s => (
94+
<Link
95+
key={s.id}
96+
to={sessionPath(s)}
97+
className="flex items-center justify-between p-4 bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 rounded-lg hover:border-gray-200 dark:hover:border-zinc-700 transition-colors"
98+
>
99+
<div className="min-w-0 flex items-center gap-2">
100+
<span className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500 font-medium shrink-0">
101+
{TYPE_LABELS[s.type] ?? s.type}
102+
</span>
103+
<div className="min-w-0">
104+
<p className="text-sm font-medium text-zinc-500 dark:text-slate-400 truncate">
105+
{sessionTitle(s)}
106+
</p>
107+
{s.to && (
108+
<p className="text-xs text-zinc-400 dark:text-slate-500 mt-0.5 truncate">To: {s.to}</p>
109+
)}
110+
</div>
111+
</div>
112+
<span className="text-xs text-zinc-400 dark:text-slate-500 shrink-0 ml-4">{formatTime(s.createdAt)}</span>
113+
</Link>
114+
))}
115+
</div>
116+
)}
117+
118+
</div>
119+
</div>
120+
)
121+
}

packages/web/src/pages/HomePage.tsx

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ interface SessionItem {
1212
command?: string
1313
}
1414

15+
const TYPE_LABELS: Record<string, string> = {
16+
email_review: 'Email',
17+
code_review: 'Code',
18+
action_approval: 'Approval',
19+
form_review: 'Form',
20+
selection_review: 'Selection',
21+
}
1522

1623
function sessionPath(s: SessionItem): string {
1724
if (s.type === 'action_approval') return `/approval/${s.id}`
@@ -21,6 +28,12 @@ function sessionPath(s: SessionItem): string {
2128
return `/review/${s.id}`
2229
}
2330

31+
function sessionTitle(s: SessionItem): string {
32+
if (s.subject) return s.subject
33+
if (s.type === 'code_review' && s.command) return s.command
34+
return TYPE_LABELS[s.type] ?? s.type
35+
}
36+
2437
function formatTime(ts: number): string {
2538
const d = new Date(ts)
2639
const now = Date.now()
@@ -59,14 +72,19 @@ function riskBadge(risk: 'danger' | 'warning' | 'normal') {
5972

6073
export default function HomePage() {
6174
const navigate = useNavigate()
62-
const [sessions, setSessions] = useState<SessionItem[]>([])
75+
const [pending, setPending] = useState<SessionItem[]>([])
76+
const [completedCount, setCompletedCount] = useState(0)
6377
const [loading, setLoading] = useState(true)
6478
const [prefCount, setPrefCount] = useState<number | null>(null)
6579

6680
useEffect(() => {
6781
fetch('/api/sessions')
6882
.then(r => r.json())
69-
.then(data => { setSessions(data); setLoading(false) })
83+
.then((data: SessionItem[]) => {
84+
setPending(data.filter(s => s.status === 'pending'))
85+
setCompletedCount(data.filter(s => s.status === 'completed').length)
86+
setLoading(false)
87+
})
7088
.catch(() => setLoading(false))
7189

7290
fetch('/api/preferences')
@@ -79,34 +97,49 @@ export default function HomePage() {
7997
<div className="min-h-screen bg-gray-50 dark:bg-zinc-950">
8098
<div className="max-w-2xl mx-auto py-10 px-4">
8199

82-
<div className="mb-6">
100+
<div className="mb-8">
83101
<p className="text-xs text-zinc-400 dark:text-slate-500 uppercase tracking-wider mb-1">agentclick</p>
84102
<div className="flex items-center justify-between">
85-
<h1 className="text-xl font-semibold text-zinc-900 dark:text-slate-100">Recent Sessions</h1>
86-
<button
87-
onClick={() => navigate('/preferences')}
88-
className="flex items-center gap-1.5 text-sm text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
89-
>
90-
Preferences
91-
{prefCount !== null && prefCount > 0 && (
92-
<span className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500 font-medium">{prefCount}</span>
93-
)}
94-
<span className="text-zinc-300 dark:text-zinc-600"></span>
95-
</button>
103+
<h1 className="text-xl font-semibold text-zinc-900 dark:text-slate-100">Pending</h1>
104+
<div className="flex items-center gap-4">
105+
<button
106+
onClick={() => navigate('/completed')}
107+
className="flex items-center gap-1.5 text-sm text-zinc-400 dark:text-slate-500 hover:text-zinc-600 dark:hover:text-slate-300 transition-colors"
108+
>
109+
Completed
110+
{completedCount > 0 && (
111+
<span className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500 font-medium">{completedCount}</span>
112+
)}
113+
<span className="text-zinc-300 dark:text-zinc-600"></span>
114+
</button>
115+
<button
116+
onClick={() => navigate('/preferences')}
117+
className="flex items-center gap-1.5 text-sm text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
118+
>
119+
Preferences
120+
{prefCount !== null && prefCount > 0 && (
121+
<span className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500 font-medium">{prefCount}</span>
122+
)}
123+
<span className="text-zinc-300 dark:text-zinc-600"></span>
124+
</button>
125+
</div>
96126
</div>
97127
</div>
98128

99129
{loading && (
100130
<p className="text-sm text-zinc-400 dark:text-slate-500">Loading...</p>
101131
)}
102132

103-
{!loading && sessions.length === 0 && (
104-
<p className="text-sm text-zinc-400 dark:text-slate-500">No sessions yet.</p>
133+
{!loading && pending.length === 0 && (
134+
<div className="py-12 text-center">
135+
<p className="text-sm text-zinc-400 dark:text-slate-500 mb-1">Nothing waiting for you.</p>
136+
<p className="text-xs text-zinc-300 dark:text-zinc-600">Your agent will open a tab when a review is needed.</p>
137+
</div>
105138
)}
106139

107-
{!loading && sessions.length > 0 && (
140+
{!loading && pending.length > 0 && (
108141
<div className="space-y-2">
109-
{sessions.map(s => {
142+
{pending.map(s => {
110143
const risk = sessionRisk(s)
111144
const borderClass = risk === 'danger' ? 'border-l-4 border-l-red-400' :
112145
risk === 'warning' ? 'border-l-4 border-l-amber-400' : ''
@@ -116,23 +149,21 @@ export default function HomePage() {
116149
to={sessionPath(s)}
117150
className={`flex items-center justify-between p-4 bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 rounded-lg hover:border-gray-200 dark:hover:border-zinc-700 transition-colors ${borderClass}`}
118151
>
119-
<div className="min-w-0">
120-
<p className="text-sm font-medium text-zinc-800 dark:text-slate-200 truncate">
121-
{s.subject ?? s.type}
122-
</p>
123-
{s.to && (
124-
<p className="text-xs text-zinc-400 dark:text-slate-500 mt-0.5 truncate">To: {s.to}</p>
125-
)}
152+
<div className="min-w-0 flex items-center gap-2">
153+
<span className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 font-medium shrink-0">
154+
{TYPE_LABELS[s.type] ?? s.type}
155+
</span>
156+
<div className="min-w-0">
157+
<p className="text-sm font-medium text-zinc-800 dark:text-slate-200 truncate">
158+
{sessionTitle(s)}
159+
</p>
160+
{s.to && (
161+
<p className="text-xs text-zinc-400 dark:text-slate-500 mt-0.5 truncate">To: {s.to}</p>
162+
)}
163+
</div>
126164
</div>
127165
<div className="flex items-center gap-3 shrink-0 ml-4">
128166
{riskBadge(risk)}
129-
<span className={`text-xs px-2 py-0.5 rounded font-medium ${
130-
s.status === 'completed'
131-
? 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-500'
132-
: 'bg-amber-50 dark:bg-amber-950 text-amber-600 dark:text-amber-400'
133-
}`}>
134-
{s.status}
135-
</span>
136167
<span className="text-xs text-zinc-400 dark:text-slate-500">{formatTime(s.createdAt)}</span>
137168
</div>
138169
</Link>
@@ -141,7 +172,6 @@ export default function HomePage() {
141172
</div>
142173
)}
143174

144-
145175
</div>
146176
</div>
147177
)

0 commit comments

Comments
 (0)