diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx
index 08cbb9b..9f46503 100644
--- a/frontend/src/components/Dashboard.tsx
+++ b/frontend/src/components/Dashboard.tsx
@@ -1,10 +1,16 @@
+import { Routes, Route, Navigate } 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/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
-
-
-
-
-
+ {/* 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
new file mode 100644
index 0000000..c3ef9d2
--- /dev/null
+++ b/frontend/src/pages/SettingsPage.tsx
@@ -0,0 +1,326 @@
+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 { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+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
+const DELETE_CONFIRMATION_TEXT = 'delete all'
+
+export function SettingsPage() {
+ 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 [deleteReposLoading, setDeleteReposLoading] = useState(false)
+ const [deleteConfirmation, setDeleteConfirmation] = useState('')
+
+ useEffect(() => {
+ checkStatus()
+ fetchRepos()
+ }, [checkStatus, session?.access_token])
+
+ const fetchRepos = async () => {
+ if (!session?.access_token) {
+ setReposLoading(false)
+ return
+ }
+ setReposLoading(true)
+ try {
+ 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) {
+ 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 () => {
+ if (deleteConfirmation !== DELETE_CONFIRMATION_TEXT) return
+
+ setDeleteReposLoading(true)
+ const failedRepos: string[] = []
+
+ try {
+ for (const repo of repos) {
+ 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')
+ }
+ } catch (error) {
+ // Network or unexpected error - refetch to reconcile state
+ await fetchRepos()
+ toast.error('Failed to delete repositories')
+ } finally {
+ setDeleteReposLoading(false)
+ }
+ }
+
+ const handleCloseDeleteDialog = () => {
+ setDeleteReposDialog(false)
+ setDeleteConfirmation('')
+ }
+
+ const formatDate = (dateString: string | undefined) => {
+ if (!dateString) return 'Unknown'
+ return new Date(dateString).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+ }
+
+ const isDeleteEnabled = deleteConfirmation === DELETE_CONFIRMATION_TEXT
+ const availableSlots = Math.max(0, MAX_REPOS - repos.length)
+
+ 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}
+
+
+ {availableSlots} slot{availableSlots !== 1 ? 's' : ''} available
+
+ >
+ )}
+
+
+
+
+
+ {/* Danger Zone */}
+
+
+ Danger Zone
+
+
+
+
Delete all repositories
+
Remove all indexed repos and their data
+
+
+
+
+
+
+ {/* Delete Repos Dialog with typing confirmation */}
+
+
+ )
+}