diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 6bda595..67431b5 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { signInWithPopup, GoogleAuthProvider, signInWithEmailAndPassword, createUserWithEmailAndPassword, sendPasswordResetEmail } from "firebase/auth"; import { auth } from "@/lib/firebase"; import { logError, logWarn } from "@/lib/logger"; @@ -11,9 +11,19 @@ import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/ import { useRouter } from "next/navigation"; import { useToast } from "@/hooks/use-toast"; import { ThemeToggle } from "@/components/theme-toggle"; +import { Skeleton } from "@/components/ui/skeleton"; export default function LoginPage() { - const [loading, setLoading] = useState(false); + // Which sign-in path is in flight — the pressed button is the one that + // shows progress; the other just disables. (#280) + const [loadingKind, setLoadingKind] = useState<"email" | "google" | null>( + null, + ); + const loading = loadingKind !== null; + // Synchronous in-flight guard — `disabled` only applies after React + // re-renders, so a same-tick second activation must bail here. + const inFlight = useRef(false); + const [resetting, setResetting] = useState(false); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [mounted, setMounted] = useState(false); @@ -46,14 +56,16 @@ export default function LoginPage() { return (
-
+
); } const handleGoogleSignIn = async () => { - setLoading(true); + if (inFlight.current) return; + inFlight.current = true; + setLoadingKind("google"); try { const provider = new GoogleAuthProvider(); const result = await signInWithPopup(auth, provider); @@ -96,13 +108,16 @@ export default function LoginPage() { variant: "destructive", }); } finally { - setLoading(false); + inFlight.current = false; + setLoadingKind(null); } }; const handleEmailSignIn = async (e: React.FormEvent) => { e.preventDefault(); - setLoading(true); + if (inFlight.current) return; + inFlight.current = true; + setLoadingKind("email"); try { const result = await signInWithEmailAndPassword(auth, email, password); @@ -145,13 +160,16 @@ export default function LoginPage() { variant: "destructive", }); } finally { - setLoading(false); + inFlight.current = false; + setLoadingKind(null); } }; const handleEmailSignUp = async (e: React.FormEvent) => { e.preventDefault(); - setLoading(true); + if (inFlight.current) return; + inFlight.current = true; + setLoadingKind("email"); try { const result = await createUserWithEmailAndPassword(auth, email, password); @@ -178,7 +196,8 @@ export default function LoginPage() { variant: "destructive", }); } finally { - setLoading(false); + inFlight.current = false; + setLoadingKind(null); } }; @@ -200,6 +219,7 @@ export default function LoginPage() { return; } + setResetting(true); try { await sendPasswordResetEmail(auth, email); toast({ @@ -215,6 +235,8 @@ export default function LoginPage() { : "Failed to send password reset email. Please try again.", variant: "destructive", }); + } finally { + setResetting(false); } }; @@ -282,6 +304,8 @@ export default function LoginPage() { variant="link" size="sm" className="h-auto p-0 text-xs" + loading={resetting} + loadingText="Sending reset email…" onClick={handleForgotPassword} > Forgot your password? @@ -291,11 +315,13 @@ export default function LoginPage() { @@ -312,12 +338,14 @@ export default function LoginPage() {

diff --git a/src/components/animal-adoptions/animal-adoptions-page.tsx b/src/components/animal-adoptions/animal-adoptions-page.tsx index 683e7f4..1187757 100644 --- a/src/components/animal-adoptions/animal-adoptions-page.tsx +++ b/src/components/animal-adoptions/animal-adoptions-page.tsx @@ -155,7 +155,7 @@ export function AnimalAdoptions({ transition={reduceMotion ? instantTransition : { duration: 0.5, delay: index * 0.1 }} viewport={{ once: true }} > - +

{animal.name} diff --git a/src/components/animal-registration/animal-registration-page.tsx b/src/components/animal-registration/animal-registration-page.tsx index b31a5cb..8203ef8 100644 --- a/src/components/animal-registration/animal-registration-page.tsx +++ b/src/components/animal-registration/animal-registration-page.tsx @@ -536,8 +536,13 @@ export function AnimalRegistration({
- diff --git a/src/components/consent/consent-settings-button.tsx b/src/components/consent/consent-settings-button.tsx index b7bbdc9..52f3800 100644 --- a/src/components/consent/consent-settings-button.tsx +++ b/src/components/consent/consent-settings-button.tsx @@ -8,7 +8,7 @@ export function ConsentSettingsButton({ className }: { className?: string }) { return ( diff --git a/src/components/homepage/animals-section.tsx b/src/components/homepage/animals-section.tsx index e017053..c8dc7d2 100644 --- a/src/components/homepage/animals-section.tsx +++ b/src/components/homepage/animals-section.tsx @@ -69,7 +69,7 @@ export function AnimalsSection({ animals }: AnimalsSectionProps) { aria-label={`Learn more about ${animal.name}`} className="block h-full rounded-xl focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" > - + {animal.photos && animal.photos.length > 0 && ( - +
{iconMap[index] ||