Add authentication system with landing page

This commit is contained in:
Francisco Pessano
2025-06-06 21:16:33 -03:00
committed by GitHub
parent 1bb5fcd022
commit 7349ae477c
8 changed files with 523 additions and 11 deletions

164
src/components/AuthForm.tsx Normal file
View File

@@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { supabase } from '../lib/supabase';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
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 { Loader2, Mail, Lock, User } from 'lucide-react';
interface AuthFormProps {
mode: 'login' | 'signup';
}
export function AuthForm({ mode }: AuthFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [fullName, setFullName] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
setMessage('');
try {
if (mode === 'signup') {
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
full_name: fullName,
}
}
});
if (error) throw error;
setMessage('Check your email for the confirmation link!');
} else {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
// Redirect to dashboard on successful login
window.location.href = '/dashboard';
}
} catch (error: any) {
setError(error.message);
} finally {
setLoading(false);
}
};
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold text-center">
{mode === 'login' ? 'Welcome back' : 'Create account'}
</CardTitle>
<CardDescription className="text-center">
{mode === 'login'
? 'Enter your credentials to access your dashboard'
: 'Enter your details to create your account'
}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{mode === 'signup' && (
<div className="space-y-2">
<Label htmlFor="fullName">Full Name</Label>
<div className="relative">
<User className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="fullName"
type="text"
placeholder="John Doe"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
className="pl-10"
required
/>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="email"
type="email"
placeholder="john@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="pl-10"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10"
required
minLength={6}
/>
</div>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{message && (
<Alert>
<AlertDescription>{message}</AlertDescription>
</Alert>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{mode === 'login' ? 'Sign In' : 'Create Account'}
</Button>
</form>
<div className="mt-6 text-center text-sm">
{mode === 'login' ? (
<p>
Don't have an account?{' '}
<a href="/signup" className="text-primary hover:underline font-medium">
Sign up
</a>
</p>
) : (
<p>
Already have an account?{' '}
<a href="/login" className="text-primary hover:underline font-medium">
Sign in
</a>
</p>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { supabase, type Debt } from '../lib/supabase';
import { Button } from '@/components/ui/button';
import { DebtCard } from './DebtCard';
import { DebtTimeline } from './DebtTimeline';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -13,7 +14,8 @@ import {
CheckCircle,
AlertTriangle,
RefreshCw,
BarChart3
BarChart3,
LogOut
} from 'lucide-react';
export function Dashboard() {
@@ -96,6 +98,11 @@ export function Dashboard() {
});
};
const handleSignOut = async () => {
await supabase.auth.signOut();
window.location.href = '/';
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
@@ -125,14 +132,16 @@ export function Dashboard() {
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
<BarChart3 className="h-8 w-8 text-primary" />
InboxNegotiator Dashboard
</h1>
<p className="text-gray-600 mt-2">
AI-powered debt resolution platform with real-time updates
</p>
<div className="mb-8 flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
<BarChart3 className="h-8 w-8 text-primary" />
InboxNegotiator Dashboard
</h1>
<p className="text-gray-600 mt-2">
AI-powered debt resolution platform with real-time updates
</p>
</div>
</div>
{/* Stats Cards */}

102
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,102 @@
import React, { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
import type { User } from '@supabase/supabase-js';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { BarChart3, LogOut, User as UserIcon } from 'lucide-react';
export function Navbar() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
});
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
}
);
return () => subscription.unsubscribe();
}, []);
const handleSignOut = async () => {
await supabase.auth.signOut();
window.location.href = '/';
};
const getInitials = (email: string) => {
return email.substring(0, 2).toUpperCase();
};
return (
<nav className="border-b bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center gap-3">
<BarChart3 className="h-8 w-8 text-primary" />
<a href="/" className="text-xl font-bold text-gray-900">
InboxNegotiator
</a>
</div>
<div className="flex items-center gap-4">
{user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarFallback className="text-xs">
{getInitials(user.email || '')}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end">
<div className="flex items-center justify-start gap-2 p-2">
<div className="flex flex-col space-y-1 leading-none">
<p className="font-medium text-sm">{user.user_metadata?.full_name || 'User'}</p>
<p className="w-[200px] truncate text-xs text-muted-foreground">
{user.email}
</p>
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<a href="/dashboard" className="flex items-center">
<UserIcon className="mr-2 h-4 w-4" />
Dashboard
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSignOut}>
<LogOut className="mr-2 h-4 w-4" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<div className="flex items-center gap-2">
<Button variant="ghost" asChild>
<a href="/login">Sign In</a>
</Button>
<Button asChild>
<a href="/signup">Get Started</a>
</Button>
</div>
)}
</div>
</div>
</div>
</nav>
);
}

View File

@@ -0,0 +1,52 @@
import React, { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
import type { User } from '@supabase/supabase-js';
import { Loader2 } from 'lucide-react';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
setLoading(false);
});
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex items-center gap-2 text-lg">
<Loader2 className="h-5 w-5 animate-spin" />
Loading...
</div>
</div>
);
}
if (!user) {
// Redirect to login if not authenticated
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return null;
}
return <>{children}</>;
}