From 4cc24ec12e597a8633f5bd1a39de12aae7496312 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Wed, 28 Jan 2026 22:03:30 -0500 Subject: [PATCH 1/5] 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 --- frontend/src/components/Dashboard.tsx | 8 +- frontend/src/pages/SettingsPage.tsx | 319 ++++++++++++++++++++++++++ 2 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/SettingsPage.tsx diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 08cbb9b..cabafc5 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,10 +1,16 @@ +import { Routes, Route } from 'react-router-dom' import { DashboardLayout } from './dashboard/DashboardLayout' import { DashboardHome } from './dashboard/DashboardHome' +import { SettingsPage } from '../pages/SettingsPage' export function Dashboard() { return ( - + + } /> + } /> + } /> + ) } diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..b1e35a1 --- /dev/null +++ b/frontend/src/pages/SettingsPage.tsx @@ -0,0 +1,319 @@ +import { useEffect, useState } from 'react' +import { User, Github, FolderGit2, AlertTriangle, Loader2, Check } from 'lucide-react' +import { useAuth } from '@/contexts/AuthContext' +import { useGitHubRepos } from '@/hooks/useGitHubRepos' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { Skeleton } from '@/components/ui/Skeleton' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { toast } from 'sonner' +import { API_URL } from '@/config/api' + +interface Repository { + id: string + name: string +} + +const MAX_REPOS = 3 + +export function SettingsPage() { + const { user, session, signOut } = useAuth() + const { status, checkStatus, disconnect, loading: githubLoading } = useGitHubRepos() + + const [repos, setRepos] = useState([]) + const [reposLoading, setReposLoading] = useState(true) + const [disconnectLoading, setDisconnectLoading] = useState(false) + const [deleteReposDialog, setDeleteReposDialog] = useState(false) + const [deleteAccountDialog, setDeleteAccountDialog] = useState(false) + const [deleteReposLoading, setDeleteReposLoading] = useState(false) + const [deleteAccountLoading, setDeleteAccountLoading] = useState(false) + + useEffect(() => { + checkStatus() + fetchRepos() + }, []) + + const fetchRepos = async () => { + if (!session?.access_token) return + try { + const response = await fetch(`${API_URL}/repos`, { + headers: { Authorization: `Bearer ${session.access_token}` }, + }) + const data = await response.json() + setRepos(data.repositories || []) + } catch (error) { + console.error('Failed to fetch repos:', error) + } finally { + setReposLoading(false) + } + } + + const handleDisconnectGitHub = async () => { + setDisconnectLoading(true) + const success = await disconnect() + setDisconnectLoading(false) + if (success) { + toast.success('GitHub disconnected successfully') + } else { + toast.error('Failed to disconnect GitHub') + } + } + + const handleDeleteAllRepos = async () => { + setDeleteReposLoading(true) + try { + for (const repo of repos) { + await fetch(`${API_URL}/repos/${repo.id}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${session?.access_token}` }, + }) + } + setRepos([]) + setDeleteReposDialog(false) + toast.success('All repositories deleted') + } catch (error) { + toast.error('Failed to delete repositories') + } finally { + setDeleteReposLoading(false) + } + } + + const handleDeleteAccount = async () => { + setDeleteAccountLoading(true) + try { + toast.info('Account deletion coming soon. Signing you out for now.') + await signOut() + } catch (error) { + toast.error('Failed to delete account') + } finally { + setDeleteAccountLoading(false) + setDeleteAccountDialog(false) + } + } + + const formatDate = (dateString: string | undefined) => { + if (!dateString) return 'Unknown' + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + } + + return ( +
+
+

Settings

+

Manage your account and connections

+
+ + {/* Profile Section */} + + +
+
+ +
+
+ Profile + Your account information +
+
+
+ +
+ Email + {user?.email || 'Not set'} +
+ +
+ Member since + {formatDate(user?.created_at)} +
+
+
+ + {/* Connections Section */} + + +
+
+ +
+
+ Connections + Manage connected accounts +
+
+
+ +
+
+ +
+

GitHub

+ {githubLoading ? ( + + ) : status?.connected ? ( +

+ Connected as @{status.username} +

+ ) : ( +

Not connected

+ )} +
+
+ + {githubLoading ? ( + + ) : status?.connected ? ( + + ) : ( + + + Use dashboard import + + )} +
+
+
+ + {/* Repositories Section */} + + +
+
+ +
+
+ Repositories + Your indexed repository slots +
+
+
+ +
+
+

Repository slots

+

Free tier limit

+
+
+ {reposLoading ? ( + + ) : ( + <> +

+ {repos.length} + / {MAX_REPOS} +

+

+ {MAX_REPOS - repos.length} slot{MAX_REPOS - repos.length !== 1 ? 's' : ''} available +

+ + )} +
+
+
+
+ + {/* Danger Zone */} + + + Danger Zone + +
+
+

Delete all repositories

+

Remove all indexed repos and their data

+
+ +
+ +
+
+

Delete account

+

Permanently delete your account and all data

+
+ +
+
+
+ + {/* Delete Repos Dialog */} + + + + Delete all repositories? + + This will permanently delete {repos.length} repositor{repos.length === 1 ? 'y' : 'ies'} and + all indexed data. This action cannot be undone. + + + + + + + + + + {/* Delete Account Dialog */} + + + + Delete your account? + + This will permanently delete your account, all repositories, and all associated data. This + action cannot be undone. + + + + + + + + +
+ ) +} From aab62d580326b5dbe38e0bad71be88644c1f9a14 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Wed, 28 Jan 2026 23:53:33 -0500 Subject: [PATCH 2/5] fix(settings): improve UX and remove broken features - Remove delete account button (was non-functional placeholder) - Add typing confirmation for delete repos ('delete all' required) - Remove Settings and Global Search from sidebar (Settings in avatar) - Replace custom dropdown with shadcn DropdownMenu (fixes click-outside) - Cleaner sidebar with just Repositories and Documentation --- frontend/src/components/dashboard/Sidebar.tsx | 4 - frontend/src/components/dashboard/TopNav.tsx | 127 +++++++----------- frontend/src/pages/SettingsPage.tsx | 91 +++++-------- 3 files changed, 85 insertions(+), 137 deletions(-) diff --git a/frontend/src/components/dashboard/Sidebar.tsx b/frontend/src/components/dashboard/Sidebar.tsx index 47f9457..48c557b 100644 --- a/frontend/src/components/dashboard/Sidebar.tsx +++ b/frontend/src/components/dashboard/Sidebar.tsx @@ -1,8 +1,6 @@ import { Link, useLocation } from 'react-router-dom' import { FolderGit2, - Search, - Settings, BookOpen, ChevronLeft, ChevronRight, @@ -26,12 +24,10 @@ interface NavItem { const mainNavItems: NavItem[] = [ { name: 'Repositories', href: '/dashboard', icon: }, - { name: 'Global Search', href: '/dashboard/search', icon: }, ] const bottomNavItems: NavItem[] = [ { name: 'Documentation', href: '/docs', icon: , external: true }, - { name: 'Settings', href: '/dashboard/settings', icon: }, ] export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: SidebarProps) { diff --git a/frontend/src/components/dashboard/TopNav.tsx b/frontend/src/components/dashboard/TopNav.tsx index 2f3ed17..84891c2 100644 --- a/frontend/src/components/dashboard/TopNav.tsx +++ b/frontend/src/components/dashboard/TopNav.tsx @@ -1,19 +1,16 @@ import { Link } from 'react-router-dom' import { useAuth } from '../../contexts/AuthContext' -import { useState } from 'react' -import { - Menu, - Search, - Github, - Sun, - Moon, - LogOut, - Settings, - BookOpen, - ExternalLink -} from 'lucide-react' +import { Menu, Search, Github, Sun, Moon, LogOut, Settings, BookOpen, ExternalLink } from 'lucide-react' import { useTheme } from 'next-themes' import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' interface TopNavProps { onToggleSidebar: () => void @@ -24,7 +21,6 @@ interface TopNavProps { export function TopNav({ onToggleSidebar, sidebarCollapsed, onOpenCommandPalette }: TopNavProps) { const { session, signOut } = useAuth() const { theme, setTheme } = useTheme() - const [showUserMenu, setShowUserMenu] = useState(false) const userEmail = session?.user?.email || 'User' const userInitial = userEmail.charAt(0).toUpperCase() @@ -55,7 +51,7 @@ export function TopNav({ onToggleSidebar, sidebarCollapsed, onOpenCommandPalette {/* Center - Command Palette Trigger */} - - {/* User Menu */} -
- - - {showUserMenu && ( - <> -
setShowUserMenu(false)} - /> -
-
-

{userEmail}

-

Free Plan

-
-
- setShowUserMenu(false)} - > - - Settings - - setShowUserMenu(false)} - > - - Documentation - - -
-
- -
+ {/* User Menu - using shadcn DropdownMenu for proper click-outside handling */} + + + + + + +
+

{userEmail}

+

Free Plan

- - )} -
+ + + + + + Settings + + + + + + Documentation + + + + + signOut()} + className="text-destructive focus:text-destructive cursor-pointer" + > + + Sign out + + +
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index b1e35a1..55761d8 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -7,6 +7,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' import { Skeleton } from '@/components/ui/Skeleton' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' import { Dialog, DialogContent, @@ -25,18 +27,18 @@ interface Repository { } const MAX_REPOS = 3 +const DELETE_CONFIRMATION_TEXT = 'delete all' export function SettingsPage() { - const { user, session, signOut } = useAuth() + const { user, session } = useAuth() const { status, checkStatus, disconnect, loading: githubLoading } = useGitHubRepos() const [repos, setRepos] = useState([]) const [reposLoading, setReposLoading] = useState(true) const [disconnectLoading, setDisconnectLoading] = useState(false) const [deleteReposDialog, setDeleteReposDialog] = useState(false) - const [deleteAccountDialog, setDeleteAccountDialog] = useState(false) const [deleteReposLoading, setDeleteReposLoading] = useState(false) - const [deleteAccountLoading, setDeleteAccountLoading] = useState(false) + const [deleteConfirmation, setDeleteConfirmation] = useState('') useEffect(() => { checkStatus() @@ -70,6 +72,8 @@ export function SettingsPage() { } const handleDeleteAllRepos = async () => { + if (deleteConfirmation !== DELETE_CONFIRMATION_TEXT) return + setDeleteReposLoading(true) try { for (const repo of repos) { @@ -80,6 +84,7 @@ export function SettingsPage() { } setRepos([]) setDeleteReposDialog(false) + setDeleteConfirmation('') toast.success('All repositories deleted') } catch (error) { toast.error('Failed to delete repositories') @@ -88,17 +93,9 @@ export function SettingsPage() { } } - const handleDeleteAccount = async () => { - setDeleteAccountLoading(true) - try { - toast.info('Account deletion coming soon. Signing you out for now.') - await signOut() - } catch (error) { - toast.error('Failed to delete account') - } finally { - setDeleteAccountLoading(false) - setDeleteAccountDialog(false) - } + const handleCloseDeleteDialog = () => { + setDeleteReposDialog(false) + setDeleteConfirmation('') } const formatDate = (dateString: string | undefined) => { @@ -110,6 +107,8 @@ export function SettingsPage() { }) } + const isDeleteEnabled = deleteConfirmation === DELETE_CONFIRMATION_TEXT + return (
@@ -237,7 +236,7 @@ export function SettingsPage() { Danger Zone - +

Delete all repositories

@@ -253,26 +252,11 @@ export function SettingsPage() { Delete All
- -
-
-

Delete account

-

Permanently delete your account and all data

-
- -
- {/* Delete Repos Dialog */} - + {/* Delete Repos Dialog with typing confirmation */} + Delete all repositories? @@ -281,39 +265,34 @@ export function SettingsPage() { all indexed data. This action cannot be undone. +
+ + setDeleteConfirmation(e.target.value)} + placeholder={DELETE_CONFIRMATION_TEXT} + className="font-mono" + autoComplete="off" + /> +
- -
- - {/* Delete Account Dialog */} - - - - Delete your account? - - This will permanently delete your account, all repositories, and all associated data. This - action cannot be undone. - - - - - - - -
) } From 4338747184d6b2972c48f5afc190b197a578f919 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 29 Jan 2026 12:53:22 -0500 Subject: [PATCH 3/5] fix(settings): proper error handling for fetch and delete operations - fetchRepos: check response.ok before parsing JSON, log errors properly - handleDeleteAllRepos: track failed deletions, refetch on partial failure - Show toast with failed repo names on partial failure - Reconcile local state with server on any error --- frontend/src/pages/SettingsPage.tsx | 33 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 55761d8..2340351 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -51,6 +51,13 @@ export function SettingsPage() { const response = await fetch(`${API_URL}/repos`, { headers: { Authorization: `Bearer ${session.access_token}` }, }) + + if (!response.ok) { + const errorBody = await response.text() + console.error('Failed to fetch repos:', response.status, errorBody) + return + } + const data = await response.json() setRepos(data.repositories || []) } catch (error) { @@ -75,18 +82,34 @@ export function SettingsPage() { if (deleteConfirmation !== DELETE_CONFIRMATION_TEXT) return setDeleteReposLoading(true) + const failedRepos: string[] = [] + try { for (const repo of repos) { - await fetch(`${API_URL}/repos/${repo.id}`, { + const response = await fetch(`${API_URL}/repos/${repo.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${session?.access_token}` }, }) + + if (!response.ok) { + failedRepos.push(repo.name) + } + } + + if (failedRepos.length > 0) { + // Partial failure - refetch to get accurate state + await fetchRepos() + toast.error(`Failed to delete ${failedRepos.length} repo(s): ${failedRepos.join(', ')}`) + } else { + // All succeeded + setRepos([]) + setDeleteReposDialog(false) + setDeleteConfirmation('') + toast.success('All repositories deleted') } - setRepos([]) - setDeleteReposDialog(false) - setDeleteConfirmation('') - toast.success('All repositories deleted') } catch (error) { + // Network or unexpected error - refetch to reconcile state + await fetchRepos() toast.error('Failed to delete repositories') } finally { setDeleteReposLoading(false) From 39b7c433be6b018c468bc8676607b7cac6602b6c Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 29 Jan 2026 12:56:30 -0500 Subject: [PATCH 4/5] fix: address CodeRabbit review nitpicks - Add checkStatus to useEffect dependency array (avoid stale closure) - Redirect unknown dashboard paths to /dashboard instead of showing home --- frontend/src/components/Dashboard.tsx | 4 ++-- frontend/src/pages/SettingsPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index cabafc5..9f46503 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,4 +1,4 @@ -import { Routes, Route } from 'react-router-dom' +import { Routes, Route, Navigate } from 'react-router-dom' import { DashboardLayout } from './dashboard/DashboardLayout' import { DashboardHome } from './dashboard/DashboardHome' import { SettingsPage } from '../pages/SettingsPage' @@ -9,7 +9,7 @@ export function Dashboard() { } /> } /> - } /> + } /> ) diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 2340351..e2d35bd 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -43,7 +43,7 @@ export function SettingsPage() { useEffect(() => { checkStatus() fetchRepos() - }, []) + }, [checkStatus]) const fetchRepos = async () => { if (!session?.access_token) return From b4127d05af3152639f52e188f39635dfbd97c73a Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 29 Jan 2026 13:12:40 -0500 Subject: [PATCH 5/5] fix(settings): handle edge cases for session timing and slot count - Add session?.access_token to useEffect deps (re-runs when auth loads) - Set reposLoading=false on early return (prevents stuck loading state) - Set reposLoading=true at start of fetch (consistent state) - Clamp available slots to 0 (prevents negative display if repos > MAX) --- frontend/src/pages/SettingsPage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index e2d35bd..c3ef9d2 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -43,10 +43,14 @@ export function SettingsPage() { useEffect(() => { checkStatus() fetchRepos() - }, [checkStatus]) + }, [checkStatus, session?.access_token]) const fetchRepos = async () => { - if (!session?.access_token) return + if (!session?.access_token) { + setReposLoading(false) + return + } + setReposLoading(true) try { const response = await fetch(`${API_URL}/repos`, { headers: { Authorization: `Bearer ${session.access_token}` }, @@ -131,6 +135,7 @@ export function SettingsPage() { } const isDeleteEnabled = deleteConfirmation === DELETE_CONFIRMATION_TEXT + const availableSlots = Math.max(0, MAX_REPOS - repos.length) return (
@@ -246,7 +251,7 @@ export function SettingsPage() { / {MAX_REPOS}

- {MAX_REPOS - repos.length} slot{MAX_REPOS - repos.length !== 1 ? 's' : ''} available + {availableSlots} slot{availableSlots !== 1 ? 's' : ''} available

)}