Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand Down Expand Up @@ -46,14 +56,16 @@ export default function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-background px-4">
<div className="w-full max-w-md">
<div className="h-32 bg-muted rounded-lg animate-pulse"></div>
<Skeleton className="h-32 rounded-lg" />
</div>
</div>
);
}

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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand All @@ -178,7 +196,8 @@ export default function LoginPage() {
variant: "destructive",
});
} finally {
setLoading(false);
inFlight.current = false;
setLoadingKind(null);
}
};

Expand All @@ -200,6 +219,7 @@ export default function LoginPage() {
return;
}

setResetting(true);
try {
await sendPasswordResetEmail(auth, email);
toast({
Expand All @@ -215,6 +235,8 @@ export default function LoginPage() {
: "Failed to send password reset email. Please try again.",
variant: "destructive",
});
} finally {
setResetting(false);
}
};

Expand Down Expand Up @@ -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?
Expand All @@ -291,11 +315,13 @@ export default function LoginPage() {
</div>
<Button
type="submit"
loading={loadingKind === "email"}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
loadingText={isSignIn ? "Signing in…" : "Creating account…"}
disabled={loading}
className="w-full"
size="lg"
>
{loading ? (isSignIn ? "Signing in..." : "Creating account...") : (isSignIn ? "Sign in" : "Create Account")}
{isSignIn ? "Sign in" : "Create Account"}
</Button>
</form>

Expand All @@ -312,12 +338,14 @@ export default function LoginPage() {

<Button
onClick={handleGoogleSignIn}
loading={loadingKind === "google"}
loadingText="Signing in…"
disabled={loading}
className="w-full"
variant="outline"
size="lg"
>
{loading ? "Signing in..." : "Sign in with Google"}
Sign in with Google
</Button>

<p className="text-xs text-muted-foreground text-center pt-4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/animal-adoptions/animal-adoptions-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
transition={reduceMotion ? instantTransition : { duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full hover:shadow-lg transition-shadow">
<Card className="h-full">
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle className="text-xl">{animal.name}</CardTitle>
Expand All @@ -167,7 +167,7 @@
<CardContent>
{animal.photos && animal.photos.length > 0 && (
<div className="aspect-square mb-4 rounded-lg overflow-hidden bg-muted">
<img

Check warning on line 170 in src/components/animal-adoptions/animal-adoptions-page.tsx

View workflow job for this annotation

GitHub Actions / Next.js app

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={animal.photos[0]}
alt={animal.name}
className="w-full h-full object-cover"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,13 @@ export function AnimalRegistration({
</div>
</div>

<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : `Submit Registration - $${totalFee}`}
<Button
type="submit"
className="w-full"
loading={isSubmitting}
loadingText="Submitting…"
>
Submit Registration - ${totalFee}
</Button>
</form>
</CardContent>
Expand Down
2 changes: 1 addition & 1 deletion src/components/consent/consent-settings-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function ConsentSettingsButton({ className }: { className?: string }) {
return (
<button
type="button"
className={className}
className={`cursor-pointer ${className ?? ""}`}
onClick={() => {
window.klaro?.show(window.klaroConfig, true);
}}
Expand Down
14 changes: 7 additions & 7 deletions src/components/contact/contact-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function ContactPageContent({ contact, social, mapEmbedUrl }: ContactPage
icon: <Phone className="h-6 w-6 text-primary" aria-hidden="true" />,
title: "Phone",
content: contact ? (
<a href={`tel:${contact.phone}`} className="text-foreground hover:text-primary">
<a href={`tel:${contact.phone}`} className="text-foreground transition-colors duration-fast hover:text-primary">
{contact.phone}
</a>
) : null,
Expand All @@ -55,7 +55,7 @@ export function ContactPageContent({ contact, social, mapEmbedUrl }: ContactPage
icon: <Mail className="h-6 w-6 text-primary" aria-hidden="true" />,
title: "Email",
content: contact ? (
<a href={`mailto:${contact.email}`} className="text-foreground hover:text-primary break-all">
<a href={`mailto:${contact.email}`} className="text-foreground transition-colors duration-fast hover:text-primary break-all">
{contact.email}
</a>
) : null,
Expand All @@ -68,7 +68,7 @@ export function ContactPageContent({ contact, social, mapEmbedUrl }: ContactPage
href={`https://wa.me/${contact.whatsapp.replace(/\D/g, "")}`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-primary"
className="text-foreground transition-colors duration-fast hover:text-primary"
>
{contact.whatsapp}
</a>
Expand Down Expand Up @@ -149,7 +149,7 @@ export function ContactPageContent({ contact, social, mapEmbedUrl }: ContactPage
viewport={{ once: true }}
className={card.wide ? "md:col-span-2" : ""}
>
<Card className="h-full hover:shadow-lg transition-shadow">
<Card className="h-full">
<CardHeader>
<div className="flex items-center gap-3">
{card.icon}
Expand Down Expand Up @@ -230,7 +230,7 @@ export function ContactPageContent({ contact, social, mapEmbedUrl }: ContactPage
href={socialLinks.facebook}
target="_blank"
rel="noopener noreferrer"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-4 transition-colors"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-4 transition-all duration-fast active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label="Facebook"
>
<FacebookIcon className="h-6 w-6" aria-hidden="true" />
Expand All @@ -241,7 +241,7 @@ export function ContactPageContent({ contact, social, mapEmbedUrl }: ContactPage
href={socialLinks.instagram}
target="_blank"
rel="noopener noreferrer"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-4 transition-colors"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-4 transition-all duration-fast active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label="Instagram"
>
<InstagramIcon className="h-6 w-6" aria-hidden="true" />
Expand All @@ -252,7 +252,7 @@ export function ContactPageContent({ contact, social, mapEmbedUrl }: ContactPage
href={socialLinks.twitter}
target="_blank"
rel="noopener noreferrer"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-4 transition-colors"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-4 transition-all duration-fast active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label="Twitter"
>
<TwitterIcon className="h-6 w-6" aria-hidden="true" />
Expand Down
19 changes: 11 additions & 8 deletions src/components/faq/faq-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { db } from "@/lib/firebase";
import { motion, AnimatePresence, useReducedMotion } from "framer-motion";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ChevronDown, ChevronUp, Phone, Mail, MapPin } from "lucide-react";
import { ChevronDown, Phone, Mail, MapPin } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { instantTransition } from "@/lib/animations";
import Link from "next/link";
import { logError } from "@/lib/logger";
Expand Down Expand Up @@ -86,7 +87,10 @@ export function FAQ() {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<Spinner
className="h-8 w-8 mx-auto mb-4 text-primary"
label="Loading FAQs…"
/>
<p className="text-muted-foreground">Loading FAQs...</p>
</div>
</div>
Expand Down Expand Up @@ -150,17 +154,16 @@ export function FAQ() {
<h3 id={headingId}>
<button
type="button"
className="flex w-full items-center justify-between gap-4 px-6 py-4 text-left font-medium text-lg cursor-pointer"
className={`flex w-full items-center justify-between gap-4 px-6 py-4 text-left font-medium text-lg cursor-pointer rounded-lg transition-colors duration-fast hover:bg-accent/40 active:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset ${isOpen ? "bg-accent/30" : ""}`}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => toggleItem(faq.id)}
>
{faq.question}
{isOpen ? (
<ChevronUp className="h-5 w-5 shrink-0" aria-hidden="true" />
) : (
<ChevronDown className="h-5 w-5 shrink-0" aria-hidden="true" />
)}
<ChevronDown
className={`h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-ui ${isOpen ? "rotate-180" : ""}`}
aria-hidden="true"
/>
</button>
</h3>
<AnimatePresence>
Expand Down
2 changes: 1 addition & 1 deletion src/components/homepage/animals-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
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"
>
<Card className="overflow-hidden h-full hover:shadow-lg transition-shadow">
<Card interactive className="overflow-hidden h-full">
{animal.photos && animal.photos.length > 0 && (
<motion.div
className="h-48 w-full bg-muted"
Expand All @@ -82,7 +82,7 @@
{/* Plain img: photo URLs are admin-entered and can
point at any host, so they can't go through
next/image's configured remotePatterns. */}
<img

Check warning on line 85 in src/components/homepage/animals-section.tsx

View workflow job for this annotation

GitHub Actions / Next.js app

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={animal.photos[0]}
alt={animal.name}
className="h-48 w-full object-cover"
Expand Down
2 changes: 1 addition & 1 deletion src/components/homepage/services-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function ServicesSection({ data }: ServicesSectionProps) {
delay: index * 0.1,
}}
>
<Card className="text-center h-full hover:shadow-lg transition-shadow flex flex-col">
<Card className="text-center h-full flex flex-col">
<CardHeader>
<div className="flex justify-center mb-4">
{iconMap[index] || <Heart className="h-10 w-10 text-primary" aria-hidden="true" />}
Expand Down
2 changes: 1 addition & 1 deletion src/components/homepage/who-we-are-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,13 @@
animate={{ opacity: 1, y: 0 }}
transition={reduceMotion ? instantTransition : { delay: index * 0.1 }}
>
<Card className="h-full bg-card shadow-lg hover:shadow-xl transition-shadow">
<Card className="h-full bg-card shadow-lg">
<CardContent className="p-6 text-center">
{/* Photo */}
<div className="mb-6">
<div className="w-32 h-32 mx-auto rounded-full overflow-hidden bg-muted">
{member.photo && safeExternalUrl(member.photo) ? (
<img

Check warning on line 169 in src/components/homepage/who-we-are-section.tsx

View workflow job for this annotation

GitHub Actions / Next.js app

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={safeExternalUrl(member.photo)!}
alt={member.name}
className="w-full h-full object-cover"
Expand Down
4 changes: 2 additions & 2 deletions src/components/lost-pets/sighting-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ export function SightingForm({ caseId }: { caseId: string }) {
Something went wrong — please try again or contact SFPCA directly.
</p>
)}
<Button type="submit" size="sm" disabled={busy}>
{busy ? "Sending…" : "Send report"}
<Button type="submit" size="sm" loading={busy} loadingText="Sending…">
Send report
</Button>
</form>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function VeterinaryServices() {
transition={reduceMotion ? instantTransition : { duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full hover:shadow-lg transition-shadow">
<Card className="h-full">
<CardHeader>
<div className="text-4xl mb-4" aria-hidden="true">{service.icon}</div>
<CardTitle className="text-xl">{service.title}</CardTitle>
Expand Down
Loading