Spaces:
Running
Running
import { useState } from "react"; | |
import { useLocation } from "wouter"; | |
import { useAuth } from "@/hooks/use-auth"; | |
import { authApi } from "@/lib/api"; | |
import logoUrl from "@assets/assets_task_01k3qp9hccec89tp5ht8ee88x5_1756363038_img_1-removebg-preview (1)_1756364677577.png"; | |
import { useForm } from "react-hook-form"; | |
import { zodResolver } from "@hookform/resolvers/zod"; | |
import { z } from "zod"; | |
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
import { Button } from "@/components/ui/button"; | |
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; | |
import { Input } from "@/components/ui/input"; | |
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
import { Checkbox } from "@/components/ui/checkbox"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { Loader2 } from "lucide-react"; | |
import { LocationDetector } from "@/components/location-detector"; | |
const loginSchema = z.object({ | |
email: z.string().email("Please enter a valid email"), | |
password: z.string().min(6, "Password must be at least 6 characters"), | |
}); | |
const registerSchema = z.object({ | |
username: z.string().min(3, "Username must be at least 3 characters"), | |
email: z.string().email("Please enter a valid email"), | |
password: z.string().min(6, "Password must be at least 6 characters"), | |
confirmPassword: z.string().min(6, "Please confirm your password"), | |
firstName: z.string().min(1, "First name is required"), | |
lastName: z.string().min(1, "Last name is required"), | |
phone: z.string().min(10, "Phone number is required"), | |
// Location fields - manual entry (fallback) | |
street: z.string().optional(), | |
city: z.string().optional(), | |
state: z.string().optional(), | |
pinCode: z.string().optional(), | |
country: z.string().optional(), | |
// Automatic location fields | |
latitude: z.number().optional(), | |
longitude: z.number().optional(), | |
googleMapsUrl: z.string().optional(), | |
locationDetectedAutomatically: z.boolean().default(false), | |
agreeToTerms: z.boolean().refine((val) => val === true, { | |
message: "You must agree to the terms and conditions" | |
}), | |
}).refine((data) => data.password === data.confirmPassword, { | |
message: "Passwords don't match", | |
path: ["confirmPassword"], | |
}); | |
type LoginFormData = z.infer<typeof loginSchema>; | |
type RegisterFormData = z.infer<typeof registerSchema>; | |
export default function Auth() { | |
const [, setLocation] = useLocation(); | |
const { login } = useAuth(); | |
const { toast } = useToast(); | |
const [activeTab, setActiveTab] = useState("signin"); | |
const [isLoading, setIsLoading] = useState(false); | |
const [locationProvided, setLocationProvided] = useState(false); | |
const [showLocationStep, setShowLocationStep] = useState(false); | |
const loginForm = useForm<LoginFormData>({ | |
resolver: zodResolver(loginSchema), | |
defaultValues: { | |
email: "", | |
password: "", | |
}, | |
}); | |
const registerForm = useForm<RegisterFormData>({ | |
resolver: zodResolver(registerSchema), | |
defaultValues: { | |
username: "", | |
email: "", | |
password: "", | |
confirmPassword: "", | |
firstName: "", | |
lastName: "", | |
phone: "", | |
street: "", | |
city: "", | |
state: "", | |
pinCode: "", | |
country: "", | |
latitude: undefined, | |
longitude: undefined, | |
googleMapsUrl: "", | |
locationDetectedAutomatically: false, | |
agreeToTerms: false, | |
}, | |
}); | |
const onLogin = async (data: LoginFormData) => { | |
setIsLoading(true); | |
try { | |
const response = await authApi.login(data); | |
const result = await response.json(); | |
login(result.token, result.user); | |
toast({ | |
title: "Welcome back!", | |
description: "You have been successfully logged in.", | |
}); | |
setLocation("/"); | |
} catch (error) { | |
toast({ | |
variant: "destructive", | |
title: "Login failed", | |
description: "Invalid email or password. Please try again.", | |
}); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
const handleLocationDetected = (locationData: { | |
latitude: number; | |
longitude: number; | |
googleMapsUrl: string; | |
locationDetectedAutomatically: boolean; | |
}) => { | |
registerForm.setValue('latitude', locationData.latitude); | |
registerForm.setValue('longitude', locationData.longitude); | |
registerForm.setValue('googleMapsUrl', locationData.googleMapsUrl); | |
registerForm.setValue('locationDetectedAutomatically', locationData.locationDetectedAutomatically); | |
// Clear manual location fields when automatic detection is used | |
if (locationData.locationDetectedAutomatically) { | |
registerForm.setValue('street', ''); | |
registerForm.setValue('city', ''); | |
registerForm.setValue('state', ''); | |
registerForm.setValue('pinCode', ''); | |
registerForm.setValue('country', ''); | |
} | |
setLocationProvided(true); | |
setShowLocationStep(false); | |
}; | |
const handleManualLocationSubmit = (manualData: { | |
street: string; | |
city: string; | |
state: string; | |
pinCode: string; | |
country: string; | |
locationDetectedAutomatically: false; | |
}) => { | |
registerForm.setValue('street', manualData.street); | |
registerForm.setValue('city', manualData.city); | |
registerForm.setValue('state', manualData.state); | |
registerForm.setValue('pinCode', manualData.pinCode); | |
registerForm.setValue('country', manualData.country); | |
registerForm.setValue('locationDetectedAutomatically', false); | |
// Clear automatic location fields when manual entry is used | |
registerForm.setValue('latitude', undefined); | |
registerForm.setValue('longitude', undefined); | |
registerForm.setValue('googleMapsUrl', ''); | |
setLocationProvided(true); | |
setShowLocationStep(false); | |
}; | |
const onRegister = async (data: RegisterFormData) => { | |
if (!locationProvided) { | |
setShowLocationStep(true); | |
toast({ | |
title: "Location required", | |
description: "Please provide your location to continue registration.", | |
}); | |
return; | |
} | |
setIsLoading(true); | |
try { | |
const { confirmPassword, agreeToTerms, ...registerData } = data; | |
const response = await authApi.register(registerData); | |
const result = await response.json(); | |
login(result.token, result.user); | |
toast({ | |
title: "Account created!", | |
description: "Welcome to Shoposphere! Your account has been created successfully.", | |
}); | |
setLocation("/"); | |
} catch (error) { | |
toast({ | |
variant: "destructive", | |
title: "Registration failed", | |
description: "An error occurred while creating your account. Please try again.", | |
}); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
return ( | |
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> | |
<Card className="w-full max-w-md"> | |
<CardHeader className="text-center"> | |
<div className="flex justify-center mb-4" data-testid="auth-title"> | |
<img | |
src={logoUrl} | |
alt="Shoposphere" | |
className="h-16 w-auto" | |
/> | |
</div> | |
</CardHeader> | |
<CardContent> | |
<Tabs value={activeTab} onValueChange={setActiveTab}> | |
<TabsList className="grid w-full grid-cols-2" data-testid="auth-tabs"> | |
<TabsTrigger value="signin" data-testid="tab-signin">Sign In</TabsTrigger> | |
<TabsTrigger value="signup" data-testid="tab-signup">Sign Up</TabsTrigger> | |
</TabsList> | |
<TabsContent value="signin" className="space-y-4"> | |
<Form {...loginForm}> | |
<form onSubmit={loginForm.handleSubmit(onLogin)} className="space-y-4"> | |
<FormField | |
control={loginForm.control} | |
name="email" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Email</FormLabel> | |
<FormControl> | |
<Input | |
{...field} | |
type="email" | |
placeholder="your@email.com" | |
data-testid="input-email" | |
/> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={loginForm.control} | |
name="password" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Password</FormLabel> | |
<FormControl> | |
<Input | |
{...field} | |
type="password" | |
placeholder="β’β’β’β’β’β’β’β’" | |
data-testid="input-password" | |
/> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<Button | |
type="submit" | |
className="w-full" | |
disabled={isLoading} | |
data-testid="button-signin" | |
> | |
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} | |
Sign In | |
</Button> | |
</form> | |
</Form> | |
</TabsContent> | |
<TabsContent value="signup" className="space-y-4"> | |
<Form {...registerForm}> | |
<form onSubmit={registerForm.handleSubmit(onRegister)} className="space-y-4"> | |
<div className="grid grid-cols-2 gap-4"> | |
<FormField | |
control={registerForm.control} | |
name="firstName" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>First Name</FormLabel> | |
<FormControl> | |
<Input {...field} placeholder="John" data-testid="input-firstName" /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={registerForm.control} | |
name="lastName" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Last Name</FormLabel> | |
<FormControl> | |
<Input {...field} placeholder="Doe" data-testid="input-lastName" /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
</div> | |
<FormField | |
control={registerForm.control} | |
name="username" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Username</FormLabel> | |
<FormControl> | |
<Input {...field} placeholder="johndoe123" data-testid="input-username" /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={registerForm.control} | |
name="email" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Email</FormLabel> | |
<FormControl> | |
<Input {...field} type="email" placeholder="your@email.com" data-testid="input-email-register" /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={registerForm.control} | |
name="phone" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Phone</FormLabel> | |
<FormControl> | |
<Input {...field} type="tel" placeholder="+1 (555) 123-4567" data-testid="input-phone" /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
{/* Location Section */} | |
{showLocationStep ? ( | |
<div className="space-y-4" data-testid="location-step"> | |
<LocationDetector | |
onLocationDetected={handleLocationDetected} | |
onManualLocationSubmit={handleManualLocationSubmit} | |
/> | |
</div> | |
) : locationProvided ? ( | |
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg space-y-2" data-testid="location-confirmed"> | |
<p className="text-sm font-semibold text-green-700 dark:text-green-300"> | |
β Location information provided | |
</p> | |
{registerForm.getValues('locationDetectedAutomatically') ? ( | |
<div className="text-xs text-green-600 dark:text-green-400"> | |
<p>Coordinates: {registerForm.getValues('latitude')?.toFixed(6)}, {registerForm.getValues('longitude')?.toFixed(6)}</p> | |
<Button | |
type="button" | |
size="sm" | |
variant="outline" | |
onClick={() => window.open(registerForm.getValues('googleMapsUrl'), '_blank')} | |
data-testid="view-detected-location" | |
> | |
View on Maps | |
</Button> | |
</div> | |
) : ( | |
<p className="text-xs text-green-600 dark:text-green-400"> | |
Manual address: {registerForm.getValues('street')}, {registerForm.getValues('city')} | |
</p> | |
)} | |
<Button | |
type="button" | |
size="sm" | |
variant="outline" | |
onClick={() => { | |
setLocationProvided(false); | |
setShowLocationStep(true); | |
}} | |
data-testid="change-location" | |
> | |
Change Location | |
</Button> | |
</div> | |
) : ( | |
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg" data-testid="location-required"> | |
<p className="text-sm text-blue-700 dark:text-blue-300"> | |
π Location information will be requested after you fill out the basic details above. | |
</p> | |
</div> | |
)} | |
<FormField | |
control={registerForm.control} | |
name="password" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Password</FormLabel> | |
<FormControl> | |
<Input {...field} type="password" placeholder="β’β’β’β’β’β’β’β’" data-testid="input-password-register" /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={registerForm.control} | |
name="confirmPassword" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Confirm Password</FormLabel> | |
<FormControl> | |
<Input {...field} type="password" placeholder="β’β’β’β’β’β’β’β’" data-testid="input-confirmPassword" /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={registerForm.control} | |
name="agreeToTerms" | |
render={({ field }) => ( | |
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> | |
<FormControl> | |
<Checkbox | |
checked={field.value} | |
onCheckedChange={field.onChange} | |
data-testid="checkbox-terms" | |
/> | |
</FormControl> | |
<FormLabel className="text-sm"> | |
I agree to the Terms & Conditions | |
</FormLabel> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<Button | |
type="submit" | |
className="w-full" | |
disabled={isLoading} | |
data-testid="button-signup" | |
> | |
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} | |
Create Account | |
</Button> | |
</form> | |
</Form> | |
</TabsContent> | |
</Tabs> | |
</CardContent> | |
</Card> | |
</div> | |
); | |
} | |