Skip to content

Commit 4cc24ec

Browse files
committed
feat(settings): add settings page with shadcn components
- Profile section with email and member since date - GitHub connection status with disconnect button - Repository slots display (X/3 free tier) - Danger zone with delete repos and delete account - All actions have confirmation dialogs - Uses Card, Dialog, Alert, Badge, Separator, Skeleton components - Loading states and error handling included
1 parent d1db8c3 commit 4cc24ec

2 files changed

Lines changed: 326 additions & 1 deletion

File tree

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import { Routes, Route } from 'react-router-dom'
12
import { DashboardLayout } from './dashboard/DashboardLayout'
23
import { DashboardHome } from './dashboard/DashboardHome'
4+
import { SettingsPage } from '../pages/SettingsPage'
35

46
export function Dashboard() {
57
return (
68
<DashboardLayout>
7-
<DashboardHome />
9+
<Routes>
10+
<Route index element={<DashboardHome />} />
11+
<Route path="settings" element={<SettingsPage />} />
12+
<Route path="*" element={<DashboardHome />} />
13+
</Routes>
814
</DashboardLayout>
915
)
1016
}
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { useEffect, useState } from 'react'
2+
import { User, Github, FolderGit2, AlertTriangle, Loader2, Check } from 'lucide-react'
3+
import { useAuth } from '@/contexts/AuthContext'
4+
import { useGitHubRepos } from '@/hooks/useGitHubRepos'
5+
import { Button } from '@/components/ui/button'
6+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7+
import { Badge } from '@/components/ui/badge'
8+
import { Separator } from '@/components/ui/separator'
9+
import { Skeleton } from '@/components/ui/Skeleton'
10+
import {
11+
Dialog,
12+
DialogContent,
13+
DialogDescription,
14+
DialogFooter,
15+
DialogHeader,
16+
DialogTitle,
17+
} from '@/components/ui/dialog'
18+
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
19+
import { toast } from 'sonner'
20+
import { API_URL } from '@/config/api'
21+
22+
interface Repository {
23+
id: string
24+
name: string
25+
}
26+
27+
const MAX_REPOS = 3
28+
29+
export function SettingsPage() {
30+
const { user, session, signOut } = useAuth()
31+
const { status, checkStatus, disconnect, loading: githubLoading } = useGitHubRepos()
32+
33+
const [repos, setRepos] = useState<Repository[]>([])
34+
const [reposLoading, setReposLoading] = useState(true)
35+
const [disconnectLoading, setDisconnectLoading] = useState(false)
36+
const [deleteReposDialog, setDeleteReposDialog] = useState(false)
37+
const [deleteAccountDialog, setDeleteAccountDialog] = useState(false)
38+
const [deleteReposLoading, setDeleteReposLoading] = useState(false)
39+
const [deleteAccountLoading, setDeleteAccountLoading] = useState(false)
40+
41+
useEffect(() => {
42+
checkStatus()
43+
fetchRepos()
44+
}, [])
45+
46+
const fetchRepos = async () => {
47+
if (!session?.access_token) return
48+
try {
49+
const response = await fetch(`${API_URL}/repos`, {
50+
headers: { Authorization: `Bearer ${session.access_token}` },
51+
})
52+
const data = await response.json()
53+
setRepos(data.repositories || [])
54+
} catch (error) {
55+
console.error('Failed to fetch repos:', error)
56+
} finally {
57+
setReposLoading(false)
58+
}
59+
}
60+
61+
const handleDisconnectGitHub = async () => {
62+
setDisconnectLoading(true)
63+
const success = await disconnect()
64+
setDisconnectLoading(false)
65+
if (success) {
66+
toast.success('GitHub disconnected successfully')
67+
} else {
68+
toast.error('Failed to disconnect GitHub')
69+
}
70+
}
71+
72+
const handleDeleteAllRepos = async () => {
73+
setDeleteReposLoading(true)
74+
try {
75+
for (const repo of repos) {
76+
await fetch(`${API_URL}/repos/${repo.id}`, {
77+
method: 'DELETE',
78+
headers: { Authorization: `Bearer ${session?.access_token}` },
79+
})
80+
}
81+
setRepos([])
82+
setDeleteReposDialog(false)
83+
toast.success('All repositories deleted')
84+
} catch (error) {
85+
toast.error('Failed to delete repositories')
86+
} finally {
87+
setDeleteReposLoading(false)
88+
}
89+
}
90+
91+
const handleDeleteAccount = async () => {
92+
setDeleteAccountLoading(true)
93+
try {
94+
toast.info('Account deletion coming soon. Signing you out for now.')
95+
await signOut()
96+
} catch (error) {
97+
toast.error('Failed to delete account')
98+
} finally {
99+
setDeleteAccountLoading(false)
100+
setDeleteAccountDialog(false)
101+
}
102+
}
103+
104+
const formatDate = (dateString: string | undefined) => {
105+
if (!dateString) return 'Unknown'
106+
return new Date(dateString).toLocaleDateString('en-US', {
107+
year: 'numeric',
108+
month: 'long',
109+
day: 'numeric',
110+
})
111+
}
112+
113+
return (
114+
<div className="max-w-2xl space-y-6">
115+
<div>
116+
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
117+
<p className="text-muted-foreground">Manage your account and connections</p>
118+
</div>
119+
120+
{/* Profile Section */}
121+
<Card>
122+
<CardHeader className="pb-3">
123+
<div className="flex items-center gap-3">
124+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
125+
<User className="h-5 w-5 text-primary" />
126+
</div>
127+
<div>
128+
<CardTitle className="text-lg">Profile</CardTitle>
129+
<CardDescription>Your account information</CardDescription>
130+
</div>
131+
</div>
132+
</CardHeader>
133+
<CardContent className="space-y-1">
134+
<div className="flex items-center justify-between py-3">
135+
<span className="text-muted-foreground">Email</span>
136+
<span className="font-medium">{user?.email || 'Not set'}</span>
137+
</div>
138+
<Separator />
139+
<div className="flex items-center justify-between py-3">
140+
<span className="text-muted-foreground">Member since</span>
141+
<span className="font-medium">{formatDate(user?.created_at)}</span>
142+
</div>
143+
</CardContent>
144+
</Card>
145+
146+
{/* Connections Section */}
147+
<Card>
148+
<CardHeader className="pb-3">
149+
<div className="flex items-center gap-3">
150+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
151+
<Github className="h-5 w-5 text-primary" />
152+
</div>
153+
<div>
154+
<CardTitle className="text-lg">Connections</CardTitle>
155+
<CardDescription>Manage connected accounts</CardDescription>
156+
</div>
157+
</div>
158+
</CardHeader>
159+
<CardContent>
160+
<div className="flex items-center justify-between py-2">
161+
<div className="flex items-center gap-3">
162+
<Github className="h-5 w-5 text-muted-foreground" />
163+
<div>
164+
<p className="font-medium">GitHub</p>
165+
{githubLoading ? (
166+
<Skeleton className="mt-1 h-4 w-32" />
167+
) : status?.connected ? (
168+
<p className="text-sm text-muted-foreground">
169+
Connected as <span className="text-foreground">@{status.username}</span>
170+
</p>
171+
) : (
172+
<p className="text-sm text-muted-foreground">Not connected</p>
173+
)}
174+
</div>
175+
</div>
176+
177+
{githubLoading ? (
178+
<Skeleton className="h-9 w-24" />
179+
) : status?.connected ? (
180+
<Button
181+
variant="outline"
182+
size="sm"
183+
onClick={handleDisconnectGitHub}
184+
disabled={disconnectLoading}
185+
>
186+
{disconnectLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Disconnect'}
187+
</Button>
188+
) : (
189+
<Badge variant="secondary" className="gap-1">
190+
<Check className="h-3 w-3" />
191+
Use dashboard import
192+
</Badge>
193+
)}
194+
</div>
195+
</CardContent>
196+
</Card>
197+
198+
{/* Repositories Section */}
199+
<Card>
200+
<CardHeader className="pb-3">
201+
<div className="flex items-center gap-3">
202+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
203+
<FolderGit2 className="h-5 w-5 text-primary" />
204+
</div>
205+
<div>
206+
<CardTitle className="text-lg">Repositories</CardTitle>
207+
<CardDescription>Your indexed repository slots</CardDescription>
208+
</div>
209+
</div>
210+
</CardHeader>
211+
<CardContent>
212+
<div className="flex items-center justify-between py-2">
213+
<div>
214+
<p className="font-medium">Repository slots</p>
215+
<p className="text-sm text-muted-foreground">Free tier limit</p>
216+
</div>
217+
<div className="text-right">
218+
{reposLoading ? (
219+
<Skeleton className="h-8 w-16" />
220+
) : (
221+
<>
222+
<p className="text-2xl font-bold">
223+
{repos.length}
224+
<span className="text-lg font-normal text-muted-foreground"> / {MAX_REPOS}</span>
225+
</p>
226+
<p className="text-sm text-muted-foreground">
227+
{MAX_REPOS - repos.length} slot{MAX_REPOS - repos.length !== 1 ? 's' : ''} available
228+
</p>
229+
</>
230+
)}
231+
</div>
232+
</div>
233+
</CardContent>
234+
</Card>
235+
236+
{/* Danger Zone */}
237+
<Alert variant="destructive" className="border-destructive/30 bg-destructive/5">
238+
<AlertTriangle className="h-5 w-5" />
239+
<AlertTitle className="text-lg font-semibold">Danger Zone</AlertTitle>
240+
<AlertDescription className="mt-4 space-y-4">
241+
<div className="flex items-center justify-between">
242+
<div>
243+
<p className="font-medium text-foreground">Delete all repositories</p>
244+
<p className="text-sm text-muted-foreground">Remove all indexed repos and their data</p>
245+
</div>
246+
<Button
247+
variant="outline"
248+
size="sm"
249+
className="border-destructive/50 text-destructive hover:bg-destructive hover:text-destructive-foreground"
250+
onClick={() => setDeleteReposDialog(true)}
251+
disabled={repos.length === 0 || reposLoading}
252+
>
253+
Delete All
254+
</Button>
255+
</div>
256+
<Separator className="bg-destructive/20" />
257+
<div className="flex items-center justify-between">
258+
<div>
259+
<p className="font-medium text-foreground">Delete account</p>
260+
<p className="text-sm text-muted-foreground">Permanently delete your account and all data</p>
261+
</div>
262+
<Button
263+
variant="outline"
264+
size="sm"
265+
className="border-destructive/50 text-destructive hover:bg-destructive hover:text-destructive-foreground"
266+
onClick={() => setDeleteAccountDialog(true)}
267+
>
268+
Delete Account
269+
</Button>
270+
</div>
271+
</AlertDescription>
272+
</Alert>
273+
274+
{/* Delete Repos Dialog */}
275+
<Dialog open={deleteReposDialog} onOpenChange={setDeleteReposDialog}>
276+
<DialogContent>
277+
<DialogHeader>
278+
<DialogTitle>Delete all repositories?</DialogTitle>
279+
<DialogDescription>
280+
This will permanently delete {repos.length} repositor{repos.length === 1 ? 'y' : 'ies'} and
281+
all indexed data. This action cannot be undone.
282+
</DialogDescription>
283+
</DialogHeader>
284+
<DialogFooter>
285+
<Button variant="outline" onClick={() => setDeleteReposDialog(false)}>
286+
Cancel
287+
</Button>
288+
<Button variant="destructive" onClick={handleDeleteAllRepos} disabled={deleteReposLoading}>
289+
{deleteReposLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
290+
Delete All Repositories
291+
</Button>
292+
</DialogFooter>
293+
</DialogContent>
294+
</Dialog>
295+
296+
{/* Delete Account Dialog */}
297+
<Dialog open={deleteAccountDialog} onOpenChange={setDeleteAccountDialog}>
298+
<DialogContent>
299+
<DialogHeader>
300+
<DialogTitle>Delete your account?</DialogTitle>
301+
<DialogDescription>
302+
This will permanently delete your account, all repositories, and all associated data. This
303+
action cannot be undone.
304+
</DialogDescription>
305+
</DialogHeader>
306+
<DialogFooter>
307+
<Button variant="outline" onClick={() => setDeleteAccountDialog(false)}>
308+
Cancel
309+
</Button>
310+
<Button variant="destructive" onClick={handleDeleteAccount} disabled={deleteAccountLoading}>
311+
{deleteAccountLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
312+
Delete Account
313+
</Button>
314+
</DialogFooter>
315+
</DialogContent>
316+
</Dialog>
317+
</div>
318+
)
319+
}

0 commit comments

Comments
 (0)