Skip to content

Commit 73c3ec0

Browse files
committed
feat(frontend): add Supabase authentication UI with shadcn
Authentication Components: - LoginForm: Email/password login with shadcn Card, Input, Button - SignupForm: User registration with password confirmation - AuthContext: Supabase client integration with session management - UserNav: Profile dropdown with avatar, email display, logout - LoginPage/SignupPage: Full-page auth layouts Features: - Protected route wrapper for authenticated pages - Automatic redirect to /login for unauthenticated users - Session persistence with Supabase Auth - Beautiful gradient backgrounds and card-based forms - User avatar with initials in navbar - Dropdown menu with account info and logout Routing: - / → Dashboard (protected, requires auth) - /login → Login page (redirects to / if authenticated) - /signup → Signup page (redirects to / if authenticated) Integration: - Supabase Auth for user management - JWT token storage in session - Real-time auth state updates - Clean error handling with alerts UI: Professional shadcn components with accessibility and animations
1 parent 8d595c3 commit 73c3ec0

8 files changed

Lines changed: 418 additions & 273 deletions

File tree

frontend/src/App.tsx

Lines changed: 49 additions & 272 deletions
Original file line numberDiff line numberDiff line change
@@ -1,281 +1,58 @@
1-
import { useState, useEffect } from 'react'
2-
import './index.css'
3-
import { Toaster } from '@/components/ui/sonner'
4-
import { toast } from 'sonner'
5-
import { RepoList } from './components/RepoList'
6-
import { AddRepoForm } from './components/AddRepoForm'
7-
import { SearchPanel } from './components/SearchPanel'
8-
import { DependencyGraph } from './components/DependencyGraph'
9-
import { RepoOverview } from './components/RepoOverview'
10-
import { StyleInsights } from './components/StyleInsights'
11-
import { ImpactAnalyzer } from './components/ImpactAnalyzer'
12-
import { PerformanceDashboard } from './components/PerformanceDashboard'
13-
import type { Repository } from './types'
14-
15-
const API_URL = 'http://localhost:8000'
16-
const API_KEY = 'dev-secret-key'
17-
18-
type RepoTab = 'overview' | 'search' | 'dependencies' | 'insights' | 'impact'
19-
20-
function App() {
21-
const [repos, setRepos] = useState<Repository[]>([])
22-
const [selectedRepo, setSelectedRepo] = useState<string | null>(null)
23-
const [activeTab, setActiveTab] = useState<RepoTab>('overview')
24-
const [loading, setLoading] = useState(false)
25-
const [showPerformance, setShowPerformance] = useState(false)
26-
27-
const fetchRepos = async () => {
28-
try {
29-
const response = await fetch(`${API_URL}/api/repos`, {
30-
headers: { 'Authorization': `Bearer ${API_KEY}` }
31-
})
32-
const data = await response.json()
33-
setRepos(data.repositories || [])
34-
} catch (error) {
35-
console.error('Error fetching repos:', error)
36-
}
1+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
2+
import { AuthProvider, useAuth } from './contexts/AuthContext';
3+
import { LoginPage } from './pages/LoginPage';
4+
import { SignupPage } from './pages/SignupPage';
5+
import { Dashboard } from './components/Dashboard';
6+
7+
function ProtectedRoute({ children }: { children: React.ReactNode }) {
8+
const { user, loading } = useAuth();
9+
10+
if (loading) {
11+
return (
12+
<div className="min-h-screen flex items-center justify-center">
13+
<div className="text-lg">Loading...</div>
14+
</div>
15+
);
3716
}
3817

39-
useEffect(() => {
40-
fetchRepos()
41-
const interval = setInterval(fetchRepos, 30000)
42-
return () => clearInterval(interval)
43-
}, [])
44-
45-
const handleAddRepo = async (gitUrl: string, branch: string) => {
46-
try {
47-
setLoading(true)
48-
const name = gitUrl.split('/').pop()?.replace('.git', '') || 'unknown'
49-
50-
const response = await fetch(`${API_URL}/api/repos`, {
51-
method: 'POST',
52-
headers: {
53-
'Authorization': `Bearer ${API_KEY}`,
54-
'Content-Type': 'application/json'
55-
},
56-
body: JSON.stringify({ name, git_url: gitUrl, branch })
57-
})
58-
59-
const data = await response.json()
60-
61-
await fetch(`${API_URL}/api/repos/${data.repo_id}/index`, {
62-
method: 'POST',
63-
headers: { 'Authorization': `Bearer ${API_KEY}` }
64-
})
65-
66-
await fetchRepos()
67-
toast.success('Repository added!', {
68-
description: `${name} is now being indexed`
69-
})
70-
} catch (error) {
71-
console.error('Error adding repo:', error)
72-
toast.error('Failed to add repository', {
73-
description: 'Please check the Git URL and try again'
74-
})
75-
} finally {
76-
setLoading(false)
77-
}
18+
if (!user) {
19+
return <Navigate to="/login" replace />;
7820
}
7921

80-
const handleReindex = async () => {
81-
if (!selectedRepo) return
82-
83-
try {
84-
setLoading(true)
85-
await fetch(`${API_URL}/api/repos/${selectedRepo}/index`, {
86-
method: 'POST',
87-
headers: { 'Authorization': `Bearer ${API_KEY}` }
88-
})
89-
await fetchRepos()
90-
// RepoOverview component will show the toast and progress
91-
} catch (error) {
92-
toast.error('Re-indexing failed', {
93-
description: 'Please check the console for details'
94-
})
95-
} finally {
96-
setLoading(false)
97-
}
98-
}
22+
return <>{children}</>;
23+
}
9924

100-
const selectedRepoData = repos.find(r => r.id === selectedRepo)
101-
const isRepoView = selectedRepo && selectedRepoData
25+
function AppRoutes() {
26+
const { user } = useAuth();
10227

10328
return (
104-
<div className="min-h-screen bg-gray-50">
105-
<nav className="bg-white border-b border-gray-200">
106-
<div className="max-w-7xl mx-auto px-6">
107-
<div className="flex items-center justify-between h-16">
108-
<div className="flex items-center gap-2">
109-
<div className="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center">
110-
<span className="text-white font-bold text-sm">CI</span>
111-
</div>
112-
<span className="font-semibold text-gray-900">CodeIntel</span>
113-
<span className="text-xs text-gray-400 ml-2">MCP Server</span>
114-
</div>
115-
116-
<div className="flex items-center gap-4">
117-
<button
118-
onClick={() => setShowPerformance(!showPerformance)}
119-
className={`text-xs px-3 py-1.5 rounded-md transition-colors ${
120-
showPerformance
121-
? 'bg-blue-100 text-blue-700 font-medium'
122-
: 'text-gray-600 hover:bg-gray-100'
123-
}`}
124-
>
125-
📊 Performance
126-
</button>
127-
<div className="flex items-center gap-2 text-xs text-gray-500">
128-
<div className="w-2 h-2 rounded-full bg-green-500" />
129-
<span>API Connected</span>
130-
</div>
131-
<div className="text-xs text-gray-400 border-l border-gray-200 pl-4">
132-
{repos.length} repos • {repos.filter(r => r.status === 'indexed').length} indexed
133-
</div>
134-
</div>
135-
</div>
136-
</div>
137-
</nav>
138-
139-
<main className="max-w-7xl mx-auto px-6 py-8">
140-
{/* Performance Dashboard Overlay */}
141-
{showPerformance && (
142-
<div className="mb-6">
143-
<PerformanceDashboard apiUrl={API_URL} apiKey={API_KEY} />
144-
</div>
145-
)}
146-
147-
{!isRepoView && (
148-
<div className="space-y-6">
149-
<div className="flex items-center justify-between">
150-
<div>
151-
<h1 className="text-2xl font-semibold text-gray-900">Repositories</h1>
152-
<p className="text-sm text-gray-600 mt-1">Manage and index your codebases</p>
153-
</div>
154-
<AddRepoForm onAdd={handleAddRepo} loading={loading} />
155-
</div>
156-
157-
<RepoList
158-
repos={repos}
159-
selectedRepo={selectedRepo}
160-
onSelect={(id) => {
161-
setSelectedRepo(id)
162-
setActiveTab('overview')
163-
}}
164-
/>
165-
</div>
166-
)}
167-
168-
{isRepoView && (
169-
<div>
170-
<div className="mb-6">
171-
<button
172-
onClick={() => {
173-
setSelectedRepo(null)
174-
setActiveTab('overview')
175-
}}
176-
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 mb-4"
177-
>
178-
<span></span>
179-
<span>Back to Repositories</span>
180-
</button>
181-
182-
<div className="flex items-center justify-between">
183-
<div>
184-
<h1 className="text-2xl font-semibold text-gray-900">{selectedRepoData.name}</h1>
185-
<p className="text-sm text-gray-600 mt-1 font-mono">{selectedRepoData.git_url}</p>
186-
</div>
187-
188-
{selectedRepoData.status === 'indexed' && (
189-
<span className="badge-success">✓ Indexed</span>
190-
)}
191-
</div>
192-
</div>
193-
194-
<div className="border-b border-gray-200 mb-6">
195-
<div className="flex gap-6 overflow-x-auto">
196-
<button
197-
onClick={() => setActiveTab('overview')}
198-
className={`pb-3 border-b-2 transition-colors whitespace-nowrap ${
199-
activeTab === 'overview'
200-
? 'border-blue-600 text-gray-900 font-medium'
201-
: 'border-transparent text-gray-600 hover:text-gray-900'
202-
}`}
203-
>
204-
Overview
205-
</button>
206-
<button
207-
onClick={() => setActiveTab('search')}
208-
className={`pb-3 border-b-2 transition-colors whitespace-nowrap ${
209-
activeTab === 'search'
210-
? 'border-blue-600 text-gray-900 font-medium'
211-
: 'border-transparent text-gray-600 hover:text-gray-900'
212-
}`}
213-
>
214-
Search
215-
</button>
216-
<button
217-
onClick={() => setActiveTab('dependencies')}
218-
className={`pb-3 border-b-2 transition-colors whitespace-nowrap ${
219-
activeTab === 'dependencies'
220-
? 'border-blue-600 text-gray-900 font-medium'
221-
: 'border-transparent text-gray-600 hover:text-gray-900'
222-
}`}
223-
>
224-
Dependencies
225-
</button>
226-
<button
227-
onClick={() => setActiveTab('insights')}
228-
className={`pb-3 border-b-2 transition-colors whitespace-nowrap ${
229-
activeTab === 'insights'
230-
? 'border-blue-600 text-gray-900 font-medium'
231-
: 'border-transparent text-gray-600 hover:text-gray-900'
232-
}`}
233-
>
234-
Code Style
235-
</button>
236-
<button
237-
onClick={() => setActiveTab('impact')}
238-
className={`pb-3 border-b-2 transition-colors whitespace-nowrap ${
239-
activeTab === 'impact'
240-
? 'border-blue-600 text-gray-900 font-medium'
241-
: 'border-transparent text-gray-600 hover:text-gray-900'
242-
}`}
243-
>
244-
Impact
245-
</button>
246-
</div>
247-
</div>
248-
249-
{activeTab === 'overview' && (
250-
<RepoOverview
251-
repo={selectedRepoData}
252-
onReindex={handleReindex}
253-
apiUrl={API_URL}
254-
apiKey={API_KEY}
255-
/>
256-
)}
257-
258-
{activeTab === 'search' && (
259-
<SearchPanel repoId={selectedRepo} apiUrl={API_URL} apiKey={API_KEY} />
260-
)}
261-
262-
{activeTab === 'dependencies' && (
263-
<DependencyGraph repoId={selectedRepo} apiUrl={API_URL} apiKey={API_KEY} />
264-
)}
265-
266-
{activeTab === 'insights' && (
267-
<StyleInsights repoId={selectedRepo} apiUrl={API_URL} apiKey={API_KEY} />
268-
)}
269-
270-
{activeTab === 'impact' && (
271-
<ImpactAnalyzer repoId={selectedRepo} apiUrl={API_URL} apiKey={API_KEY} />
272-
)}
273-
</div>
274-
)}
275-
</main>
276-
<Toaster />
277-
</div>
278-
)
29+
<Routes>
30+
<Route
31+
path="/login"
32+
element={user ? <Navigate to="/" replace /> : <LoginPage />}
33+
/>
34+
<Route
35+
path="/signup"
36+
element={user ? <Navigate to="/" replace /> : <SignupPage />}
37+
/>
38+
<Route
39+
path="/"
40+
element={
41+
<ProtectedRoute>
42+
<Dashboard />
43+
</ProtectedRoute>
44+
}
45+
/>
46+
</Routes>
47+
);
27948
}
28049

281-
export default App
50+
export function App() {
51+
return (
52+
<BrowserRouter>
53+
<AuthProvider>
54+
<AppRoutes />
55+
</AuthProvider>
56+
</BrowserRouter>
57+
);
58+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useAuth } from '../contexts/AuthContext';
2+
import { Avatar, AvatarFallback } from './ui/avatar';
3+
import {
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuItem,
7+
DropdownMenuLabel,
8+
DropdownMenuSeparator,
9+
DropdownMenuTrigger,
10+
} from './ui/dropdown-menu';
11+
import { Button } from './ui/button';
12+
import { LogOut } from 'lucide-react';
13+
14+
export function UserNav() {
15+
const { user, signOut } = useAuth();
16+
17+
if (!user) return null;
18+
19+
const email = user.email || '';
20+
const initials = email.split('@')[0].substring(0, 2).toUpperCase();
21+
22+
return (
23+
<DropdownMenu>
24+
<DropdownMenuTrigger asChild>
25+
<Button variant="ghost" className="relative h-10 w-10 rounded-full">
26+
<Avatar className="h-10 w-10">
27+
<AvatarFallback className="bg-primary text-primary-foreground">
28+
{initials}
29+
</AvatarFallback>
30+
</Avatar>
31+
</Button>
32+
</DropdownMenuTrigger>
33+
<DropdownMenuContent className="w-56" align="end">
34+
<DropdownMenuLabel>
35+
<div className="flex flex-col space-y-1">
36+
<p className="text-sm font-medium">Account</p>
37+
<p className="text-xs text-muted-foreground">{email}</p>
38+
</div>
39+
</DropdownMenuLabel>
40+
<DropdownMenuSeparator />
41+
<DropdownMenuItem onClick={() => signOut()}>
42+
<LogOut className="mr-2 h-4 w-4" />
43+
Log out
44+
</DropdownMenuItem>
45+
</DropdownMenuContent>
46+
</DropdownMenu>
47+
);
48+
}

0 commit comments

Comments
 (0)