Files
menassit/client/src/pages/LoginPage.tsx
T
ghaddaditw ecbdd49cea Allow new users to create accounts in addition to logging in
Implements registration functionality on LoginPage.tsx with fields for first name, last name, username, account type, and confirm password.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 556aa286-edd2-4cea-8583-f4fc3cfd119b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/81470e0d-8ae8-4335-9301-cd9a69e670fa/96467e4f-00f2-4d88-82b1-142a9b24784c.jpg
2025-05-30 17:33:32 +00:00

377 lines
14 KiB
TypeScript

import { useState } from "react";
import { Link, useLocation } from "wouter";
import { useAuth } from "@/hooks/useAuth";
import { useTheme } from "@/context/ThemeContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/hooks/use-toast";
import { Mic, Moon, Sun, Loader2, AlertCircle, CheckCircle2 } from "lucide-react";
import { cn } from "@/lib/utils";
export default function LoginPage() {
const [location, setLocation] = useLocation();
const { login, register, user } = useAuth();
const { theme, toggleTheme } = useTheme();
const { toast } = useToast();
const isRegisterMode = location === "/register";
const [formData, setFormData] = useState({
email: "",
password: "",
confirmPassword: "",
username: "",
firstName: "",
lastName: "",
role: "standard",
rememberMe: false,
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState("");
// Redirect if already authenticated
if (user) {
setLocation("/dashboard");
return null;
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setIsSubmitting(true);
try {
if (isRegisterMode) {
if (formData.password !== formData.confirmPassword) {
setError("Passwords do not match");
return;
}
await register({
username: formData.username,
email: formData.email,
password: formData.password,
confirmPassword: formData.confirmPassword,
firstName: formData.firstName,
lastName: formData.lastName,
role: formData.role,
});
toast({
title: "Account created!",
description: "You have successfully registered. Please sign in.",
});
setLocation("/login");
} else {
await login(formData.email, formData.password);
toast({
title: "Welcome back!",
description: "You have successfully signed in.",
});
setLocation("/dashboard");
}
} catch (error: any) {
console.error("Auth error:", error);
setError(error.message || `Failed to ${isRegisterMode ? 'register' : 'sign in'}. Please try again.`);
} finally {
setIsSubmitting(false);
}
};
const handleInputChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (error) setError(""); // Clear error on input change
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-violet-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900 flex items-center justify-center p-4">
{/* Header with Logo and Theme Toggle */}
<div className="absolute top-6 left-6 right-6 flex items-center justify-between">
<Link href="/" className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-br from-primary to-secondary rounded-lg flex items-center justify-center">
<Mic className="w-4 h-4 text-white" />
</div>
<span className="text-xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
TaskFin
</span>
</Link>
<Button
variant="ghost"
size="sm"
onClick={toggleTheme}
className="w-9 px-0"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
</div>
{/* Main Login Card */}
<Card className="w-full max-w-md shadow-2xl border-slate-200 dark:border-slate-700">
<CardHeader className="text-center space-y-2">
<CardTitle className="text-2xl font-bold">
{isRegisterMode ? "Create Account" : "Welcome Back"}
</CardTitle>
<CardDescription>
{isRegisterMode
? "Join TaskFin to manage your tasks and finances with AI"
: "Sign in to your TaskFin account to access your voice-powered productivity tools"
}
</CardDescription>
{/* Features Badge */}
<div className="flex items-center justify-center space-x-2 pt-2">
<Badge variant="secondary" className="text-xs">
<CheckCircle2 className="w-3 h-3 mr-1" />
Local Voice AI
</Badge>
<Badge variant="secondary" className="text-xs">
<CheckCircle2 className="w-3 h-3 mr-1" />
Complete Privacy
</Badge>
</div>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
{/* Error Alert */}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Registration Fields */}
{isRegisterMode && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<Input
id="firstName"
placeholder="John"
value={formData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
disabled={isSubmitting}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<Input
id="lastName"
placeholder="Doe"
value={formData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
disabled={isSubmitting}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
placeholder="johndoe"
value={formData.username}
onChange={(e) => handleInputChange("username", e.target.value)}
disabled={isSubmitting}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="role">Account Type</Label>
<select
id="role"
value={formData.role}
onChange={(e) => handleInputChange("role", e.target.value)}
disabled={isSubmitting}
className="w-full px-3 py-2 border border-input bg-background rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
<option value="standard">Standard - Basic task and financial management</option>
<option value="pro">Pro - Includes professional profiles and appointment booking</option>
</select>
</div>
</>
)}
{/* Email Field */}
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
placeholder="Enter your email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
disabled={isSubmitting}
required
className="focus-ring"
/>
</div>
{/* Password Field */}
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Enter your password"
value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
disabled={isSubmitting}
required
className="focus-ring"
/>
</div>
{/* Confirm Password Field - Registration Only */}
{isRegisterMode && (
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Confirm your password"
value={formData.confirmPassword}
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
disabled={isSubmitting}
required
className="focus-ring"
/>
</div>
)}
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id="rememberMe"
checked={formData.rememberMe}
onCheckedChange={(checked) => handleInputChange("rememberMe", !!checked)}
disabled={isSubmitting}
/>
<Label
htmlFor="rememberMe"
className="text-sm font-normal cursor-pointer"
>
Remember me
</Label>
</div>
<Button
variant="link"
className="px-0 h-auto text-sm"
type="button"
disabled={isSubmitting}
>
Forgot password?
</Button>
</div>
</CardContent>
<CardFooter className="flex flex-col space-y-4">
{/* Submit Button */}
<Button
type="submit"
className="w-full"
disabled={isSubmitting || !formData.email || !formData.password || (isRegisterMode && !formData.username)}
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{isRegisterMode ? "Creating Account..." : "Signing In..."}
</>
) : (
isRegisterMode ? "Create Account" : "Sign In"
)}
</Button>
{/* Switch between Login/Register */}
<div className="text-center">
<span className="text-sm text-muted-foreground">
{isRegisterMode ? "Already have an account? " : "Don't have an account? "}
</span>
<Link
href={isRegisterMode ? "/login" : "/register"}
className="text-sm font-medium text-primary hover:underline"
>
{isRegisterMode ? "Sign in" : "Create one"}
</Link>
</div>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-slate-300 dark:border-slate-600" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or
</span>
</div>
</div>
{/* Register Link */}
<div className="text-center">
<span className="text-sm text-muted-foreground">
Don't have an account?{" "}
</span>
<Button
variant="link"
className="px-0 h-auto text-sm font-medium"
type="button"
onClick={() => setLocation("/register")}
disabled={isSubmitting}
>
Create Account
</Button>
</div>
{/* Demo Credentials (for development) */}
<div className="text-center">
<div className="text-xs text-muted-foreground space-y-1">
<p className="font-medium">Demo Accounts:</p>
<p>Standard: user@taskfin.local / password</p>
<p>Pro: pro@taskfin.local / password</p>
<p>Admin: admin@taskfin.local / password</p>
</div>
</div>
</CardFooter>
</form>
</Card>
{/* Features Overview */}
<div className="absolute bottom-6 left-6 right-6 text-center">
<div className="flex items-center justify-center space-x-6 text-xs text-muted-foreground">
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Local STT/TTS Processing</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Zero Cloud Dependencies</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Complete Data Privacy</span>
</div>
</div>
</div>
</div>
);
}