Skip to content

Commit 6c1b5ba

Browse files
authored
Merge pull request #225 from DevanshuNEU/fix/phase1-quick-wins
fix(ux): empty repo state opens add modal, signup requires email verification
2 parents b6acf5d + 23cd242 commit 6c1b5ba

6 files changed

Lines changed: 266 additions & 45 deletions

File tree

frontend/src/components/AddRepoForm.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,30 @@ import { Button } from '@/components/ui/button'
55
import { Input } from '@/components/ui/input'
66
import { Label } from '@/components/ui/label'
77

8-
interface AddRepoFormProps {
8+
// Discriminated union: if isOpen is provided, onOpenChange is required
9+
type UncontrolledProps = {
10+
isOpen?: undefined
11+
onOpenChange?: undefined
12+
}
13+
14+
type ControlledProps = {
15+
isOpen: boolean
16+
onOpenChange: (open: boolean) => void
17+
}
18+
19+
type AddRepoFormProps = {
920
onAdd: (gitUrl: string, branch: string) => Promise<void>
1021
loading: boolean
11-
}
22+
} & (UncontrolledProps | ControlledProps)
1223

13-
export function AddRepoForm({ onAdd, loading }: AddRepoFormProps) {
24+
export function AddRepoForm({ onAdd, loading, isOpen, onOpenChange }: AddRepoFormProps) {
1425
const [gitUrl, setGitUrl] = useState('')
1526
const [branch, setBranch] = useState('main')
16-
const [showForm, setShowForm] = useState(false)
27+
const [internalOpen, setInternalOpen] = useState(false)
28+
29+
const isControlled = isOpen !== undefined
30+
const showForm = isControlled ? isOpen : internalOpen
31+
const setShowForm = isControlled ? onOpenChange : setInternalOpen
1732

1833
const handleSubmit = async (e: React.FormEvent) => {
1934
e.preventDefault()
@@ -26,14 +41,17 @@ export function AddRepoForm({ onAdd, loading }: AddRepoFormProps) {
2641

2742
return (
2843
<>
29-
<Button
30-
onClick={() => setShowForm(true)}
31-
disabled={loading}
32-
className="bg-primary hover:bg-primary/90 text-primary-foreground gap-2"
33-
>
34-
<Plus className="w-4 h-4" />
35-
Add Repository
36-
</Button>
44+
{/* Only show trigger button in uncontrolled mode */}
45+
{!isControlled && (
46+
<Button
47+
onClick={() => setShowForm(true)}
48+
disabled={loading}
49+
className="bg-primary hover:bg-primary/90 text-primary-foreground gap-2"
50+
>
51+
<Plus className="w-4 h-4" />
52+
Add Repository
53+
</Button>
54+
)}
3755

3856
<AnimatePresence>
3957
{showForm && (

frontend/src/components/RepoList.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface RepoListProps {
88
repos: Repository[]
99
selectedRepo: string | null
1010
onSelect: (repoId: string) => void
11+
onAddClick?: () => void
1112
loading?: boolean
1213
}
1314

@@ -94,15 +95,31 @@ const RepoCard = ({ repo, index, onSelect }: {
9495
)
9596
}
9697

97-
export function RepoList({ repos, selectedRepo, onSelect, loading }: RepoListProps) {
98+
export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: RepoListProps) {
99+
// Hooks must be called before any conditional returns
100+
const sortedRepos = useMemo(() => {
101+
return [...repos].sort((a, b) => {
102+
if (a.status === 'indexed' && b.status !== 'indexed') return -1
103+
if (b.status === 'indexed' && a.status !== 'indexed') return 1
104+
return (b.file_count || 0) - (a.file_count || 0)
105+
})
106+
}, [repos])
107+
98108
if (loading) return <RepoGridSkeleton count={3} />
99109

100110
if (repos.length === 0) {
111+
const isClickable = !!onAddClick
101112
return (
102-
<motion.div
113+
<motion.button
114+
onClick={onAddClick}
115+
disabled={!isClickable}
103116
initial={{ opacity: 0 }}
104117
animate={{ opacity: 1 }}
105-
className="bg-card border border-border rounded-xl p-16 text-center"
118+
whileHover={isClickable ? { scale: 1.01 } : undefined}
119+
whileTap={isClickable ? { scale: 0.99 } : undefined}
120+
className={`w-full bg-card border border-dashed border-border rounded-xl p-16 text-center transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50 ${
121+
isClickable ? 'hover:border-primary/40 cursor-pointer' : 'cursor-default'
122+
}`}
106123
>
107124
<div className="w-14 h-14 mx-auto mb-4 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center">
108125
<Plus className="w-6 h-6 text-primary" />
@@ -111,18 +128,10 @@ export function RepoList({ repos, selectedRepo, onSelect, loading }: RepoListPro
111128
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
112129
Add your first repository to start searching code with AI
113130
</p>
114-
</motion.div>
131+
</motion.button>
115132
)
116133
}
117134

118-
const sortedRepos = useMemo(() => {
119-
return [...repos].sort((a, b) => {
120-
if (a.status === 'indexed' && b.status !== 'indexed') return -1
121-
if (b.status === 'indexed' && a.status !== 'indexed') return 1
122-
return (b.file_count || 0) - (a.file_count || 0)
123-
})
124-
}, [repos])
125-
126135
return (
127136
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
128137
{sortedRepos.map((repo, index) => (

frontend/src/components/auth/LoginForm.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ export function LoginForm() {
2626
await signIn(email, password)
2727
navigate('/dashboard')
2828
} catch (err: any) {
29-
setError(err.message || 'Login failed')
29+
const message = err.message?.toLowerCase() || ''
30+
if (message.includes('email not confirmed') || message.includes('not confirmed')) {
31+
setError('Please verify your email before logging in. Check your inbox for the verification link.')
32+
} else if (message.includes('invalid login credentials')) {
33+
setError('Invalid email or password. Please try again.')
34+
} else {
35+
setError(err.message || 'Login failed')
36+
}
3037
} finally {
3138
setLoading(false)
3239
}

frontend/src/components/auth/SignupForm.tsx

Lines changed: 178 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useState } from 'react'
22
import { useAuth } from '@/contexts/AuthContext'
33
import { useNavigate, Link } from 'react-router-dom'
4+
import { motion } from 'framer-motion'
45
import { Button } from '@/components/ui/button'
56
import { Input } from '@/components/ui/input'
67
import { Label } from '@/components/ui/label'
78
import { Alert, AlertDescription } from '@/components/ui/alert'
89
import { Navbar } from '@/components/landing'
9-
import { Github, Loader2, Mail, Lock } from 'lucide-react'
10+
import { Github, Loader2, Mail, Lock, CheckCircle2, Send, ArrowLeft } from 'lucide-react'
1011

1112
export function SignupForm() {
1213
const [email, setEmail] = useState('')
@@ -15,9 +16,36 @@ export function SignupForm() {
1516
const [error, setError] = useState('')
1617
const [loading, setLoading] = useState(false)
1718
const [oauthLoading, setOauthLoading] = useState<'github' | 'google' | null>(null)
18-
const { signUp, signInWithGitHub, signInWithGoogle } = useAuth()
19+
const [emailSent, setEmailSent] = useState(false)
20+
const [resendLoading, setResendLoading] = useState(false)
21+
const [resendSuccess, setResendSuccess] = useState(false)
22+
const { signUp, signInWithGitHub, signInWithGoogle, resendVerification } = useAuth()
1923
const navigate = useNavigate()
2024

25+
const handleResend = async () => {
26+
setError('')
27+
setResendLoading(true)
28+
setResendSuccess(false)
29+
try {
30+
await resendVerification(email)
31+
setResendSuccess(true)
32+
setError('')
33+
} catch (err: any) {
34+
setError(err.message || 'Failed to resend verification email')
35+
} finally {
36+
setResendLoading(false)
37+
}
38+
}
39+
40+
const handleGoBack = () => {
41+
setEmail('')
42+
setPassword('')
43+
setConfirmPassword('')
44+
setEmailSent(false)
45+
setResendSuccess(false)
46+
setError('')
47+
}
48+
2149
const handleSubmit = async (e: React.FormEvent) => {
2250
e.preventDefault()
2351
setError('')
@@ -35,7 +63,9 @@ export function SignupForm() {
3563
setLoading(true)
3664
try {
3765
await signUp(email, password)
38-
navigate('/dashboard')
66+
setEmailSent(true)
67+
setPassword('')
68+
setConfirmPassword('')
3969
} catch (err: any) {
4070
setError(err.message || 'Signup failed')
4171
} finally {
@@ -73,22 +103,150 @@ export function SignupForm() {
73103

74104
<div className="flex-1 flex items-center justify-center px-6 py-12">
75105
<div className="w-full max-w-sm">
76-
<div className="text-center mb-8">
77-
<h1 className="text-2xl font-semibold text-foreground mb-2">
78-
Create your account
79-
</h1>
80-
<p className="text-sm text-muted-foreground">
81-
Free for open source projects
82-
</p>
83-
</div>
106+
{/* Email Verification Sent */}
107+
{emailSent ? (
108+
<motion.div
109+
initial={{ opacity: 0, y: 20 }}
110+
animate={{ opacity: 1, y: 0 }}
111+
className="text-center"
112+
>
113+
{/* Animated Icon */}
114+
<div className="relative w-24 h-24 mx-auto mb-8">
115+
{/* Outer glow ring */}
116+
<motion.div
117+
initial={{ scale: 0.8, opacity: 0 }}
118+
animate={{ scale: 1, opacity: 1 }}
119+
transition={{ delay: 0.2, duration: 0.5 }}
120+
className="absolute inset-0 rounded-full bg-gradient-to-r from-primary/20 to-purple-500/20 blur-xl"
121+
/>
122+
{/* Icon container */}
123+
<motion.div
124+
initial={{ scale: 0 }}
125+
animate={{ scale: 1 }}
126+
transition={{ type: "spring", stiffness: 200, damping: 15, delay: 0.1 }}
127+
className="relative w-24 h-24 rounded-full bg-gradient-to-br from-primary/10 to-purple-500/10 border border-primary/30 flex items-center justify-center"
128+
>
129+
<motion.div
130+
initial={{ scale: 0, rotate: -180 }}
131+
animate={{ scale: 1, rotate: 0 }}
132+
transition={{ type: "spring", stiffness: 200, damping: 12, delay: 0.3 }}
133+
>
134+
<Send className="w-10 h-10 text-primary" />
135+
</motion.div>
136+
</motion.div>
137+
</div>
138+
139+
{/* Title */}
140+
<motion.h1
141+
initial={{ opacity: 0, y: 10 }}
142+
animate={{ opacity: 1, y: 0 }}
143+
transition={{ delay: 0.4 }}
144+
className="text-3xl font-bold text-foreground mb-3"
145+
>
146+
Check your inbox
147+
</motion.h1>
148+
149+
{/* Email display */}
150+
<motion.div
151+
initial={{ opacity: 0, y: 10 }}
152+
animate={{ opacity: 1, y: 0 }}
153+
transition={{ delay: 0.5 }}
154+
className="mb-8"
155+
>
156+
<p className="text-muted-foreground mb-2">We sent a verification link to</p>
157+
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary/5 border border-primary/20">
158+
<Mail className="w-4 h-4 text-primary" />
159+
<span className="font-mono text-sm text-foreground">{email}</span>
160+
</div>
161+
{error && (
162+
<p className="text-sm text-destructive mt-3">{error}</p>
163+
)}
164+
</motion.div>
165+
166+
{/* Instructions card */}
167+
<motion.div
168+
initial={{ opacity: 0, y: 10 }}
169+
animate={{ opacity: 1, y: 0 }}
170+
transition={{ delay: 0.6 }}
171+
className="bg-card/50 backdrop-blur-sm rounded-xl border border-border p-6 mb-6"
172+
>
173+
<div className="flex items-start gap-4 text-left">
174+
<div className="w-8 h-8 rounded-lg bg-green-500/10 border border-green-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
175+
<CheckCircle2 className="w-4 h-4 text-green-500" />
176+
</div>
177+
<div className="flex-1">
178+
<p className="text-sm text-foreground font-medium mb-1">
179+
Click the link in the email to verify your account
180+
</p>
181+
<p className="text-sm text-muted-foreground mb-3">
182+
Didn't receive it? Check your spam folder.
183+
</p>
184+
185+
{/* Resend success message */}
186+
{resendSuccess && (
187+
<p className="text-sm text-green-500 mb-3">
188+
✓ Verification email resent!
189+
</p>
190+
)}
191+
192+
{/* Action buttons */}
193+
<div className="flex items-center gap-3">
194+
<button
195+
onClick={handleResend}
196+
disabled={resendLoading}
197+
className="text-sm text-primary hover:underline font-medium disabled:opacity-50"
198+
>
199+
{resendLoading ? 'Sending...' : 'Resend email'}
200+
</button>
201+
<span className="text-muted-foreground">·</span>
202+
<button
203+
onClick={handleGoBack}
204+
disabled={resendLoading}
205+
className="text-sm text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed"
206+
>
207+
Use different email
208+
</button>
209+
</div>
210+
</div>
211+
</div>
212+
</motion.div>
213+
214+
{/* Back link */}
215+
<motion.div
216+
initial={{ opacity: 0 }}
217+
animate={{ opacity: 1 }}
218+
transition={{ delay: 0.7 }}
219+
>
220+
<Link
221+
to="/login"
222+
className={`inline-flex items-center gap-2 text-sm text-muted-foreground transition-colors ${
223+
resendLoading ? 'opacity-50 pointer-events-none' : 'hover:text-foreground'
224+
}`}
225+
onClick={(e) => resendLoading && e.preventDefault()}
226+
>
227+
<ArrowLeft className="w-4 h-4" />
228+
Back to login
229+
</Link>
230+
</motion.div>
231+
</motion.div>
232+
) : (
233+
<>
234+
<div className="text-center mb-8">
235+
<h1 className="text-2xl font-semibold text-foreground mb-2">
236+
Create your account
237+
</h1>
238+
<p className="text-sm text-muted-foreground">
239+
Free for open source projects
240+
</p>
241+
</div>
84242

85-
<div className="bg-card rounded-lg border border-border p-6">
86-
<form onSubmit={handleSubmit} className="space-y-4">
87-
{error && (
88-
<Alert variant="destructive" className="bg-destructive/10 border-destructive/20">
89-
<AlertDescription>{error}</AlertDescription>
90-
</Alert>
91-
)}
243+
<div className="bg-card rounded-lg border border-border p-6">
244+
<form onSubmit={handleSubmit} className="space-y-4">
245+
{error && (
246+
<Alert variant="destructive" className="bg-destructive/10 border-destructive/20">
247+
<AlertDescription>{error}</AlertDescription>
248+
</Alert>
249+
)}
92250

93251
<div className="space-y-2">
94252
<Label htmlFor="email">Email</Label>
@@ -242,6 +400,8 @@ export function SignupForm() {
242400
Sign in
243401
</Link>
244402
</p>
403+
</>
404+
)}
245405
</div>
246406
</div>
247407
</div>

0 commit comments

Comments
 (0)