Skip to content
42 changes: 30 additions & 12 deletions frontend/src/components/AddRepoForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

interface AddRepoFormProps {
// Discriminated union: if isOpen is provided, onOpenChange is required
type UncontrolledProps = {
isOpen?: undefined
onOpenChange?: undefined
}

type ControlledProps = {
isOpen: boolean
onOpenChange: (open: boolean) => void
}

type AddRepoFormProps = {
onAdd: (gitUrl: string, branch: string) => Promise<void>
loading: boolean
}
} & (UncontrolledProps | ControlledProps)

export function AddRepoForm({ onAdd, loading }: AddRepoFormProps) {
export function AddRepoForm({ onAdd, loading, isOpen, onOpenChange }: AddRepoFormProps) {
const [gitUrl, setGitUrl] = useState('')
const [branch, setBranch] = useState('main')
const [showForm, setShowForm] = useState(false)
const [internalOpen, setInternalOpen] = useState(false)

const isControlled = isOpen !== undefined
const showForm = isControlled ? isOpen : internalOpen
const setShowForm = isControlled ? onOpenChange : setInternalOpen

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

return (
<>
<Button
onClick={() => setShowForm(true)}
disabled={loading}
className="bg-primary hover:bg-primary/90 text-primary-foreground gap-2"
>
<Plus className="w-4 h-4" />
Add Repository
</Button>
{/* Only show trigger button in uncontrolled mode */}
{!isControlled && (
<Button
onClick={() => setShowForm(true)}
disabled={loading}
className="bg-primary hover:bg-primary/90 text-primary-foreground gap-2"
>
<Plus className="w-4 h-4" />
Add Repository
</Button>
)}

<AnimatePresence>
{showForm && (
Expand Down
33 changes: 21 additions & 12 deletions frontend/src/components/RepoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface RepoListProps {
repos: Repository[]
selectedRepo: string | null
onSelect: (repoId: string) => void
onAddClick?: () => void
loading?: boolean
}

Expand Down Expand Up @@ -94,15 +95,31 @@ const RepoCard = ({ repo, index, onSelect }: {
)
}

export function RepoList({ repos, selectedRepo, onSelect, loading }: RepoListProps) {
export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: RepoListProps) {
// Hooks must be called before any conditional returns
const sortedRepos = useMemo(() => {
return [...repos].sort((a, b) => {
if (a.status === 'indexed' && b.status !== 'indexed') return -1
if (b.status === 'indexed' && a.status !== 'indexed') return 1
return (b.file_count || 0) - (a.file_count || 0)
})
}, [repos])

if (loading) return <RepoGridSkeleton count={3} />

if (repos.length === 0) {
const isClickable = !!onAddClick
return (
<motion.div
<motion.button
onClick={onAddClick}
disabled={!isClickable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="bg-card border border-border rounded-xl p-16 text-center"
whileHover={isClickable ? { scale: 1.01 } : undefined}
whileTap={isClickable ? { scale: 0.99 } : undefined}
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 ${
isClickable ? 'hover:border-primary/40 cursor-pointer' : 'cursor-default'
}`}
>
<div className="w-14 h-14 mx-auto mb-4 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center">
<Plus className="w-6 h-6 text-primary" />
Expand All @@ -111,18 +128,10 @@ export function RepoList({ repos, selectedRepo, onSelect, loading }: RepoListPro
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
Add your first repository to start searching code with AI
</p>
</motion.div>
</motion.button>
)
}

const sortedRepos = useMemo(() => {
return [...repos].sort((a, b) => {
if (a.status === 'indexed' && b.status !== 'indexed') return -1
if (b.status === 'indexed' && a.status !== 'indexed') return 1
return (b.file_count || 0) - (a.file_count || 0)
})
}, [repos])

return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sortedRepos.map((repo, index) => (
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ export function LoginForm() {
await signIn(email, password)
navigate('/dashboard')
} catch (err: any) {
setError(err.message || 'Login failed')
const message = err.message?.toLowerCase() || ''
if (message.includes('email not confirmed') || message.includes('not confirmed')) {
setError('Please verify your email before logging in. Check your inbox for the verification link.')
} else if (message.includes('invalid login credentials')) {
setError('Invalid email or password. Please try again.')
} else {
setError(err.message || 'Login failed')
}
} finally {
setLoading(false)
}
Expand Down
196 changes: 178 additions & 18 deletions frontend/src/components/auth/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useState } from 'react'
import { useAuth } from '@/contexts/AuthContext'
import { useNavigate, Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Navbar } from '@/components/landing'
import { Github, Loader2, Mail, Lock } from 'lucide-react'
import { Github, Loader2, Mail, Lock, CheckCircle2, Send, ArrowLeft } from 'lucide-react'

export function SignupForm() {
const [email, setEmail] = useState('')
Expand All @@ -15,9 +16,36 @@ export function SignupForm() {
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [oauthLoading, setOauthLoading] = useState<'github' | 'google' | null>(null)
const { signUp, signInWithGitHub, signInWithGoogle } = useAuth()
const [emailSent, setEmailSent] = useState(false)
const [resendLoading, setResendLoading] = useState(false)
const [resendSuccess, setResendSuccess] = useState(false)
const { signUp, signInWithGitHub, signInWithGoogle, resendVerification } = useAuth()
const navigate = useNavigate()

const handleResend = async () => {
setError('')
setResendLoading(true)
setResendSuccess(false)
try {
await resendVerification(email)
setResendSuccess(true)
setError('')
} catch (err: any) {
setError(err.message || 'Failed to resend verification email')
} finally {
setResendLoading(false)
}
}

const handleGoBack = () => {
setEmail('')
setPassword('')
setConfirmPassword('')
setEmailSent(false)
setResendSuccess(false)
setError('')
}
Comment thread
DevanshuNEU marked this conversation as resolved.

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
Expand All @@ -35,7 +63,9 @@ export function SignupForm() {
setLoading(true)
try {
await signUp(email, password)
navigate('/dashboard')
setEmailSent(true)
setPassword('')
setConfirmPassword('')
} catch (err: any) {
setError(err.message || 'Signup failed')
} finally {
Expand Down Expand Up @@ -73,22 +103,150 @@ export function SignupForm() {

<div className="flex-1 flex items-center justify-center px-6 py-12">
<div className="w-full max-w-sm">
<div className="text-center mb-8">
<h1 className="text-2xl font-semibold text-foreground mb-2">
Create your account
</h1>
<p className="text-sm text-muted-foreground">
Free for open source projects
</p>
</div>
{/* Email Verification Sent */}
{emailSent ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
{/* Animated Icon */}
<div className="relative w-24 h-24 mx-auto mb-8">
{/* Outer glow ring */}
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
className="absolute inset-0 rounded-full bg-gradient-to-r from-primary/20 to-purple-500/20 blur-xl"
/>
{/* Icon container */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 200, damping: 15, delay: 0.1 }}
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"
>
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: "spring", stiffness: 200, damping: 12, delay: 0.3 }}
>
<Send className="w-10 h-10 text-primary" />
</motion.div>
</motion.div>
</div>

{/* Title */}
<motion.h1
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="text-3xl font-bold text-foreground mb-3"
>
Check your inbox
</motion.h1>

{/* Email display */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mb-8"
>
<p className="text-muted-foreground mb-2">We sent a verification link to</p>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary/5 border border-primary/20">
<Mail className="w-4 h-4 text-primary" />
<span className="font-mono text-sm text-foreground">{email}</span>
</div>
{error && (
<p className="text-sm text-destructive mt-3">{error}</p>
)}
</motion.div>

{/* Instructions card */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="bg-card/50 backdrop-blur-sm rounded-xl border border-border p-6 mb-6"
>
<div className="flex items-start gap-4 text-left">
<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">
<CheckCircle2 className="w-4 h-4 text-green-500" />
</div>
<div className="flex-1">
<p className="text-sm text-foreground font-medium mb-1">
Click the link in the email to verify your account
</p>
<p className="text-sm text-muted-foreground mb-3">
Didn't receive it? Check your spam folder.
</p>

{/* Resend success message */}
{resendSuccess && (
<p className="text-sm text-green-500 mb-3">
✓ Verification email resent!
</p>
)}

{/* Action buttons */}
<div className="flex items-center gap-3">
<button
onClick={handleResend}
disabled={resendLoading}
className="text-sm text-primary hover:underline font-medium disabled:opacity-50"
>
{resendLoading ? 'Sending...' : 'Resend email'}
</button>
<span className="text-muted-foreground">·</span>
<button
onClick={handleGoBack}
disabled={resendLoading}
className="text-sm text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed"
>
Use different email
</button>
</div>
</div>
</div>
</motion.div>

{/* Back link */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 }}
>
<Link
to="/login"
className={`inline-flex items-center gap-2 text-sm text-muted-foreground transition-colors ${
resendLoading ? 'opacity-50 pointer-events-none' : 'hover:text-foreground'
}`}
onClick={(e) => resendLoading && e.preventDefault()}
>
<ArrowLeft className="w-4 h-4" />
Back to login
</Link>
</motion.div>
</motion.div>
) : (
<>
<div className="text-center mb-8">
<h1 className="text-2xl font-semibold text-foreground mb-2">
Create your account
</h1>
<p className="text-sm text-muted-foreground">
Free for open source projects
</p>
</div>

<div className="bg-card rounded-lg border border-border p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<Alert variant="destructive" className="bg-destructive/10 border-destructive/20">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="bg-card rounded-lg border border-border p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<Alert variant="destructive" className="bg-destructive/10 border-destructive/20">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}

<div className="space-y-2">
<Label htmlFor="email">Email</Label>
Expand Down Expand Up @@ -242,6 +400,8 @@ export function SignupForm() {
Sign in
</Link>
</p>
</>
)}
</div>
</div>
</div>
Expand Down
Loading