diff --git a/src/app/admin/animal-adoptions/page.tsx b/src/app/admin/animal-adoptions/page.tsx index 7174771..1b9eb38 100644 --- a/src/app/admin/animal-adoptions/page.tsx +++ b/src/app/admin/animal-adoptions/page.tsx @@ -12,6 +12,7 @@ import { useToast } from "@/hooks/use-toast"; import { useMutation } from "@/hooks/use-mutation"; import { useUnsavedChangesGuard } from "@/hooks/use-unsaved-changes"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { logError } from "@/lib/logger"; import { AnimalAdoptionsContent, @@ -117,7 +118,7 @@ export default function AnimalAdoptionsAdminPage() { }; if (loading) { - return
Loading...
; + return ; } if (loadError) { @@ -128,8 +129,8 @@ export default function AnimalAdoptionsAdminPage() {

Edit Animal Adoptions Page

-
@@ -313,8 +314,8 @@ export default function AnimalAdoptionsAdminPage() {
-
diff --git a/src/app/admin/animal-registration/page.tsx b/src/app/admin/animal-registration/page.tsx index eaec9e7..a52eb5b 100644 --- a/src/app/admin/animal-registration/page.tsx +++ b/src/app/admin/animal-registration/page.tsx @@ -12,6 +12,7 @@ import { useToast } from "@/hooks/use-toast"; import { useMutation } from "@/hooks/use-mutation"; import { useUnsavedChangesGuard } from "@/hooks/use-unsaved-changes"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { logError } from "@/lib/logger"; import { AnimalRegistrationContent, @@ -99,7 +100,7 @@ export default function AnimalRegistrationAdminPage() { }; if (loading) { - return
Loading...
; + return ; } if (loadError) { @@ -117,8 +118,8 @@ export default function AnimalRegistrationAdminPage() { here.

- @@ -229,8 +230,8 @@ export default function AnimalRegistrationAdminPage() {
-
diff --git a/src/app/admin/animals/[id]/page.tsx b/src/app/admin/animals/[id]/page.tsx index 316b001..22bd1da 100644 --- a/src/app/admin/animals/[id]/page.tsx +++ b/src/app/admin/animals/[id]/page.tsx @@ -90,6 +90,7 @@ import { import { ArrowLeft, Plus } from "lucide-react"; import { logError } from "@/lib/logger"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; type DialogType = | "encounter" @@ -163,7 +164,7 @@ export default function AnimalMedicalPage() { }; if (loading) { - return
Loading...
; + return ; } if (loadError) { @@ -825,10 +826,11 @@ export default function AnimalMedicalPage() { )} diff --git a/src/app/admin/animals/page.tsx b/src/app/admin/animals/page.tsx index 637b910..bfa4e67 100644 --- a/src/app/admin/animals/page.tsx +++ b/src/app/admin/animals/page.tsx @@ -45,6 +45,7 @@ import { Plus, Pencil, Trash, Search } from "lucide-react"; import { logError } from "@/lib/logger"; import { ConfirmDialog } from "@/components/admin/confirm-dialog"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; const STERILIZATION_LABELS: Record = { unknown: "Unknown", @@ -469,12 +470,8 @@ export default function AnimalsManager() { Enter a direct URL to the animal photo

- @@ -533,7 +530,7 @@ export default function AnimalsManager() { {loading ? ( -
Loading...
+ ) : loadError ? ( ) : ( diff --git a/src/app/admin/chip-lookup/page.tsx b/src/app/admin/chip-lookup/page.tsx index 1cf02b2..85f1369 100644 --- a/src/app/admin/chip-lookup/page.tsx +++ b/src/app/admin/chip-lookup/page.tsx @@ -142,6 +142,9 @@ function FoundScanForm({ const [outcome, setOutcome] = useState(""); const [notes, setNotes] = useState(""); const [busy, setBusy] = useState(false); + // Which open case is being resolved — busy locks every control, but + // the spinner belongs on the row the operator actually clicked. + const [busyCaseId, setBusyCaseId] = useState(null); const [resolving, setResolving] = useState>({}); const saveNew = async () => { @@ -191,6 +194,7 @@ function FoundScanForm({ const chosen = resolving[openCase.id]; if (!chosen) return; setBusy(true); + setBusyCaseId(openCase.id); try { const result = await resolveOpenCaseAction( openCase.id, @@ -216,6 +220,7 @@ function FoundScanForm({ }); } finally { setBusy(false); + setBusyCaseId(null); } }; @@ -282,6 +287,7 @@ function FoundScanForm({ {preview && (
diff --git a/src/app/admin/data-quality/merge-household/page.tsx b/src/app/admin/data-quality/merge-household/page.tsx index 2da2f53..db606be 100644 --- a/src/app/admin/data-quality/merge-household/page.tsx +++ b/src/app/admin/data-quality/merge-household/page.tsx @@ -37,6 +37,7 @@ import { } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { logError } from "@/lib/logger"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { CircleAlert, GitMerge, TriangleAlert } from "lucide-react"; function HouseholdCard({ @@ -195,7 +196,7 @@ function HouseholdMergeReview() { ); } - if (!pair) return

Loading…

; + if (!pair) return ; return (
@@ -383,7 +384,7 @@ export default function HouseholdMergePage() {

- Loading…

}> + }>
diff --git a/src/app/admin/data-quality/merge-person/page.tsx b/src/app/admin/data-quality/merge-person/page.tsx index a37325b..c6fb8c8 100644 --- a/src/app/admin/data-quality/merge-person/page.tsx +++ b/src/app/admin/data-quality/merge-person/page.tsx @@ -40,6 +40,7 @@ import { } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { logError } from "@/lib/logger"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { CircleAlert, GitMerge, KeyRound, TriangleAlert } from "lucide-react"; function PersonCard({ @@ -216,7 +217,7 @@ function PersonMergeReview() { ); } - if (!pair) return

Loading…

; + if (!pair) return ; return (
@@ -405,7 +406,7 @@ export default function PersonMergePage() {

- Loading…

}> + }> diff --git a/src/app/admin/data-quality/merge/page.tsx b/src/app/admin/data-quality/merge/page.tsx index d798ca4..290c4b1 100644 --- a/src/app/admin/data-quality/merge/page.tsx +++ b/src/app/admin/data-quality/merge/page.tsx @@ -41,6 +41,7 @@ import { } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { logError } from "@/lib/logger"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { CircleAlert, GitMerge, TriangleAlert } from "lucide-react"; function AnimalCard({ @@ -211,7 +212,7 @@ function MergeReview() { ); } - if (!pair) return

Loading…

; + if (!pair) return ; return (
@@ -417,7 +418,7 @@ export default function MergePage() {

- Loading…

}> + }> diff --git a/src/app/admin/data-quality/page.tsx b/src/app/admin/data-quality/page.tsx index d387a74..fce21c6 100644 --- a/src/app/admin/data-quality/page.tsx +++ b/src/app/admin/data-quality/page.tsx @@ -38,6 +38,7 @@ import { } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { logError } from "@/lib/logger"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { CheckCheck, CircleAlert, @@ -245,7 +246,9 @@ export default function DataQualityPage() {
- {findings === null && !loadError &&

Loading…

} + {findings === null && !loadError && ( + + )} {loadError && ( diff --git a/src/app/admin/faq/page.tsx b/src/app/admin/faq/page.tsx index 48fd801..3fad084 100644 --- a/src/app/admin/faq/page.tsx +++ b/src/app/admin/faq/page.tsx @@ -16,6 +16,7 @@ import { Plus, Pencil, Trash, GripVertical } from "lucide-react"; import { logError } from "@/lib/logger"; import { ConfirmDialog } from "@/components/admin/confirm-dialog"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; interface FAQ { id: string; @@ -210,7 +211,11 @@ export default function FAQManager() { }, {} as Record); if (loading) { - return
Loading...
; + return ( +
+ +
+ ); } if (loadError) { @@ -317,8 +322,8 @@ export default function FAQManager() {

)} - diff --git a/src/app/admin/homepage/page.tsx b/src/app/admin/homepage/page.tsx index 732656b..5ce6103 100644 --- a/src/app/admin/homepage/page.tsx +++ b/src/app/admin/homepage/page.tsx @@ -12,6 +12,7 @@ import { useToast } from "@/hooks/use-toast"; import { useMutation } from "@/hooks/use-mutation"; import { useUnsavedChangesGuard } from "@/hooks/use-unsaved-changes"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { saveHomepageData, loadHomepageData } from "./actions"; import { logError } from "@/lib/logger"; @@ -128,7 +129,7 @@ export default function HomepageEditor() { }; if (loading) { - return
Loading...
; + return ; } if (loadError) { @@ -139,8 +140,8 @@ export default function HomepageEditor() {

Edit Homepage

-
@@ -365,8 +366,8 @@ export default function HomepageEditor() {
-
diff --git a/src/app/admin/lost-found/[id]/page.tsx b/src/app/admin/lost-found/[id]/page.tsx index f7dd80e..e0bb4e5 100644 --- a/src/app/admin/lost-found/[id]/page.tsx +++ b/src/app/admin/lost-found/[id]/page.tsx @@ -60,6 +60,7 @@ import type { import { todayIsoDate } from "@/lib/vaccinations"; import { logError } from "@/lib/logger"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { ArrowLeft, PawPrint } from "lucide-react"; function OwnerBlock({ owner }: { owner: AnimalOwnerContacts }) { @@ -219,7 +220,7 @@ export default function LostFoundCasePage() { return ; } if (!detail && !notFound) { - return
Loading...
; + return ; } if (notFound || !detail) { return ( diff --git a/src/app/admin/lost-found/page.tsx b/src/app/admin/lost-found/page.tsx index 4adbd15..4260400 100644 --- a/src/app/admin/lost-found/page.tsx +++ b/src/app/admin/lost-found/page.tsx @@ -40,6 +40,7 @@ import type { LostFoundOutcome } from "@/lib/lost-found"; import { todayIsoDate } from "@/lib/vaccinations"; import { logError } from "@/lib/logger"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { Plus } from "lucide-react"; function CaseRow({ c }: { c: LostFoundCaseRecord }) { @@ -201,7 +202,7 @@ export default function LostFoundWorkspacePage() { return ; } if (!data) { - return
Loading...
; + return ; } return ( diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 7a3a562..0f30891 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -34,6 +34,7 @@ import { } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; import { ArrowRight, CircleCheck, @@ -63,7 +64,7 @@ function WorkItemRow({ item }: { item: DashboardWorkItem }) {
  • @@ -85,7 +86,7 @@ function FailureRow({ failure }: { failure: DashboardFailure }) {
  • +
    + Loading work queues…

    - - Loading work queues… + + {[0, 1, 2].map((i) => ( +
    + +
    + + +
    + +
    + ))}

    diff --git a/src/app/admin/persons/page.tsx b/src/app/admin/persons/page.tsx index bdd1552..c917c9d 100644 --- a/src/app/admin/persons/page.tsx +++ b/src/app/admin/persons/page.tsx @@ -34,6 +34,7 @@ import { SelectValue, } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; +import { Skeleton } from "@/components/ui/skeleton"; import { getPersonDetailAction, getPersonsDataAction, @@ -51,6 +52,7 @@ import type { } from "@/lib/registry/persons"; import { logError } from "@/lib/logger"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { Plus } from "lucide-react"; function PersonForm({ @@ -158,7 +160,15 @@ function PersonDetailView({ .catch((e) => logError("owners", "person-detail", e)); }, [personId]); - if (!detail) return

    Loading…

    ; + if (!detail) + return ( +
    + Loading person details… + + + +
    + ); const merged = detail.merge?.status === "merged"; @@ -279,7 +289,7 @@ export default function PersonsPage() { load(); }, []); - if (loading) return
    Loading...
    ; + if (loading) return ; if (loadError || !data) return ; const q = query.trim().toLowerCase(); diff --git a/src/app/admin/registrations/page.tsx b/src/app/admin/registrations/page.tsx index 5019f2d..34c6998 100644 --- a/src/app/admin/registrations/page.tsx +++ b/src/app/admin/registrations/page.tsx @@ -30,6 +30,7 @@ import { Badge } from "@/components/ui/badge"; import { useToast } from "@/hooks/use-toast"; import { useMutation } from "@/hooks/use-mutation"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { AnimalRegistration } from "@/lib/types"; import { Eye, CircleCheckBig, Download, CircleX, RotateCcw, Link2, Shield } from "lucide-react"; import { @@ -145,7 +146,7 @@ export default function RegistrationsPage() { variant: "destructive", }); } - }, `status-${id}`); + }, `status-${id}-${status}`); }; const handleViewReceipt = (registration: AnimalRegistration) => { @@ -363,11 +364,15 @@ export default function RegistrationsPage() { variant: "destructive", }); } - }, `link-${linking.id}`); + }, `link-${linking.id}-${animalId}`); }; if (loading) { - return
    Loading...
    ; + return ( +
    + +
    + ); } if (loadError) { @@ -483,6 +488,7 @@ export default function RegistrationsPage() { )} @@ -1077,6 +1092,7 @@ export default function RegistrationsPage() { @@ -272,8 +273,8 @@ export default function SettingsPage() {
    -
    diff --git a/src/app/admin/vet/page.tsx b/src/app/admin/vet/page.tsx index a1b9e3c..f906a7d 100644 --- a/src/app/admin/vet/page.tsx +++ b/src/app/admin/vet/page.tsx @@ -46,6 +46,7 @@ import { TableRow, } from "@/components/ui/table"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { logError } from "@/lib/logger"; type KindFilter = "all" | VetQueueItem["kind"]; @@ -238,7 +239,7 @@ export default function VetQueuePage() { }, [loadQueue]); if (loading) { - return
    Loading...
    ; + return ; } if (loadError) { return ; diff --git a/src/app/admin/veterinary-services/page.tsx b/src/app/admin/veterinary-services/page.tsx index 8c786e7..7f55e46 100644 --- a/src/app/admin/veterinary-services/page.tsx +++ b/src/app/admin/veterinary-services/page.tsx @@ -12,6 +12,7 @@ import { useToast } from "@/hooks/use-toast"; import { useMutation } from "@/hooks/use-mutation"; import { useUnsavedChangesGuard } from "@/hooks/use-unsaved-changes"; import { LoadError } from "@/components/admin/load-error"; +import { AdminLoading } from "@/components/admin/admin-loading"; import { logError } from "@/lib/logger"; interface VetService { @@ -116,7 +117,7 @@ export default function VetServicesAdminPage() { }; if (loading) { - return
    Loading...
    ; + return ; } if (loadError) { @@ -127,8 +128,8 @@ export default function VetServicesAdminPage() {

    Edit Veterinary Services Page

    -
    @@ -233,8 +234,8 @@ export default function VetServicesAdminPage() {
    -
    diff --git a/src/app/portal/portal-client.tsx b/src/app/portal/portal-client.tsx index 3b8fe5e..5c0ed38 100644 --- a/src/app/portal/portal-client.tsx +++ b/src/app/portal/portal-client.tsx @@ -107,6 +107,12 @@ export function AnimalCard({ const [requestNote, setRequestNote] = useState(""); const [receiptFile, setReceiptFile] = useState(null); const [receiptError, setReceiptError] = useState(null); + // Which action is in flight — pending already locks every control, + // but the spinner belongs on the button the owner actually pressed + // (#280). + const [busyAction, setBusyAction] = useState< + "confirm" | "report" | "missing" | "request" | null + >(null); const [reportKind, setReportKind] = useState(""); const [detail, setDetail] = useState(""); const [targetName, setTargetName] = useState(""); @@ -135,121 +141,141 @@ export function AnimalCard({ ); return; } + setBusyAction("request"); startTransition(async () => { - const submissionId = crypto.randomUUID(); - const result = await requestRegistrationAction({ - ownershipId: animal.ownershipId, - submissionId, - note: requestNote || undefined, - wantsReceipt: receiptFile !== null, - }); - if (!result.ok) { - toast({ - title: "Couldn't submit the request", - description: result.error, - variant: "destructive", + try { + const submissionId = crypto.randomUUID(); + const result = await requestRegistrationAction({ + ownershipId: animal.ownershipId, + submissionId, + note: requestNote || undefined, + wantsReceipt: receiptFile !== null, }); - return; - } - let receiptDelivered = true; - if (receiptFile) { - try { - const upload = await fetch(`/api/receipts/${submissionId}`, { - method: "POST", - headers: { "Content-Type": receiptFile.type }, - body: receiptFile, + if (!result.ok) { + toast({ + title: "Couldn't submit the request", + description: result.error, + variant: "destructive", }); - if (!upload.ok) { - if (upload.status !== 429) { - logError( - "registration", - "receipt-upload", - new Error(`upload ${upload.status}`), - ); + return; + } + let receiptDelivered = true; + if (receiptFile) { + try { + const upload = await fetch(`/api/receipts/${submissionId}`, { + method: "POST", + headers: { "Content-Type": receiptFile.type }, + body: receiptFile, + }); + if (!upload.ok) { + if (upload.status !== 429) { + logError( + "registration", + "receipt-upload", + new Error(`upload ${upload.status}`), + ); + } + receiptDelivered = false; } + } catch (uploadError) { receiptDelivered = false; + logError("registration", "receipt-upload", uploadError); } - } catch (uploadError) { - receiptDelivered = false; - logError("registration", "receipt-upload", uploadError); } + toast({ + title: "Request submitted", + description: + receiptFile && !receiptDelivered + ? `Your ${animal.registrationYear} request went through, but the receipt could not be uploaded — staff can still review the request.` + : `SFPCA staff will review your ${animal.registrationYear} request for ${animal.name}.`, + }); + setRequestOpen(false); + setRequestNote(""); + setReceiptFile(null); + setReceiptError(null); + } finally { + setBusyAction(null); } - toast({ - title: "Request submitted", - description: - receiptFile && !receiptDelivered - ? `Your ${animal.registrationYear} request went through, but the receipt could not be uploaded — staff can still review the request.` - : `SFPCA staff will review your ${animal.registrationYear} request for ${animal.name}.`, - }); - setRequestOpen(false); - setRequestNote(""); - setReceiptFile(null); - setReceiptError(null); }); }; const confirm = () => { + setBusyAction("confirm"); startTransition(async () => { - const result = await confirmAnimalAction(animal.ownershipId); - toast({ - title: result.ok ? "Confirmed" : "Couldn't confirm", - description: result.ok - ? `Thanks — ${animal.name} is confirmed as still living on Saba with you.` - : result.error, - variant: result.ok ? "default" : "destructive", - }); + try { + const result = await confirmAnimalAction(animal.ownershipId); + toast({ + title: result.ok ? "Confirmed" : "Couldn't confirm", + description: result.ok + ? `Thanks — ${animal.name} is confirmed as still living on Saba with you.` + : result.error, + variant: result.ok ? "default" : "destructive", + }); + } finally { + setBusyAction(null); + } }); }; const submitReport = () => { if (!reportKind) return; + setBusyAction("report"); startTransition(async () => { - const result = await submitOwnerReportAction({ - ownershipId: animal.ownershipId, - kind: reportKind, - detail: detail || undefined, - targetName: targetName || undefined, - targetContact: targetContact || undefined, - effectiveOn: effectiveOn || undefined, - }); - toast({ - title: result.ok ? "Request submitted" : "Couldn't submit", - description: result.ok - ? "Staff will review your report and follow up if needed." - : result.error, - variant: result.ok ? "default" : "destructive", - }); - if (result.ok) { - setReportKind(""); - setDetail(""); - setTargetName(""); - setTargetContact(""); - setEffectiveOn(""); + try { + const result = await submitOwnerReportAction({ + ownershipId: animal.ownershipId, + kind: reportKind, + detail: detail || undefined, + targetName: targetName || undefined, + targetContact: targetContact || undefined, + effectiveOn: effectiveOn || undefined, + }); + toast({ + title: result.ok ? "Request submitted" : "Couldn't submit", + description: result.ok + ? "Staff will review your report and follow up if needed." + : result.error, + variant: result.ok ? "default" : "destructive", + }); + if (result.ok) { + setReportKind(""); + setDetail(""); + setTargetName(""); + setTargetContact(""); + setEffectiveOn(""); + } + } finally { + setBusyAction(null); } }); }; const reportMissing = () => { + setBusyAction("missing"); startTransition(async () => { - const result = await reportMissingAction({ - ownershipId: animal.ownershipId, - lastSeenLocation: missingLocation || undefined, - detail: missingDetail || undefined, - }); - if (result.ok) { - setMissingReported(true); - setMissingOpen(false); - toast({ - title: "Missing report sent", - description: - "SFPCA staff have been notified and will start looking. We'll contact you if the animal is found.", - }); - } else { - toast({ - title: "Couldn't send the report", - description: result.error, - variant: "destructive", + try { + const result = await reportMissingAction({ + ownershipId: animal.ownershipId, + lastSeenLocation: missingLocation || undefined, + detail: missingDetail || undefined, }); + if (result.ok) { + setMissingReported(true); + setMissingOpen(false); + toast({ + title: "Missing report sent", + description: + "SFPCA staff have been notified and will start looking. We'll contact you if the animal is found.", + }); + } else { + toast({ + title: "Couldn't send the report", + description: result.error, + variant: "destructive", + }); + } + } finally { + setBusyAction(null); } }); }; @@ -334,7 +360,12 @@ export function AnimalCard({ Confirms {animal.name} still lives on Saba with you.

    {animal.confirmationDue && ( - )} @@ -492,7 +523,7 @@ export function AnimalCard({ {missingOpen && ( -
    +

    Report {animal.name} missing

    -
    - diff --git a/src/app/portal/portal-sign-out.tsx b/src/app/portal/portal-sign-out.tsx index 700e4f7..9c06e7f 100644 --- a/src/app/portal/portal-sign-out.tsx +++ b/src/app/portal/portal-sign-out.tsx @@ -21,7 +21,7 @@ export function PortalSignOut() { }; return ( - diff --git a/src/components/admin/admin-loading.tsx b/src/components/admin/admin-loading.tsx new file mode 100644 index 0000000..e5a2014 --- /dev/null +++ b/src/components/admin/admin-loading.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; + +// Shared admin loading state (#280). Replaces bare `Loading...` +// divs — which announced nothing to screen readers and yanked the +// layout when content arrived — with a status-announced skeleton that +// roughly matches a page: title line, a wide card, then row lines. +// The pulse is motion-safe only (see Skeleton). +export function AdminLoading({ + label, + rows = 3, + className, +}: { + // What is loading — announced to assistive tech as + // "Loading
    @@ -238,7 +239,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) { View Site @@ -246,11 +247,12 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
    @@ -268,7 +270,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) { variant="ghost" size="sm" onClick={handleLogout} - disabled={loggingOut} + loading={loggingOut} aria-label="Log out" > diff --git a/src/components/admin/confirm-dialog.tsx b/src/components/admin/confirm-dialog.tsx index a392ce7..eee1958 100644 --- a/src/components/admin/confirm-dialog.tsx +++ b/src/components/admin/confirm-dialog.tsx @@ -60,8 +60,13 @@ export function ConfirmDialog({ > Cancel - diff --git a/src/components/admin/lost-found-panel.tsx b/src/components/admin/lost-found-panel.tsx index 50f6c82..01902c3 100644 --- a/src/components/admin/lost-found-panel.tsx +++ b/src/components/admin/lost-found-panel.tsx @@ -217,7 +217,7 @@ export function LostFoundPanel({
    -
    diff --git a/src/components/admin/medical/follow-up-controls.tsx b/src/components/admin/medical/follow-up-controls.tsx index a293f52..0076f73 100644 --- a/src/components/admin/medical/follow-up-controls.tsx +++ b/src/components/admin/medical/follow-up-controls.tsx @@ -115,6 +115,7 @@ export function FollowUpResolveButtons({ variant="outline" size="sm" onClick={complete} + loading={mutation.pending} disabled={mutation.pending} aria-label={`Complete ${label}`} > diff --git a/src/components/admin/medical/mark-seen-dialog.tsx b/src/components/admin/medical/mark-seen-dialog.tsx index 4649106..ec7f19b 100644 --- a/src/components/admin/medical/mark-seen-dialog.tsx +++ b/src/components/admin/medical/mark-seen-dialog.tsx @@ -112,8 +112,8 @@ export function MarkSeenDialog({ > Back - diff --git a/src/components/admin/medical/ownership-panel.tsx b/src/components/admin/medical/ownership-panel.tsx index 305fb43..0a9d9b1 100644 --- a/src/components/admin/medical/ownership-panel.tsx +++ b/src/components/admin/medical/ownership-panel.tsx @@ -384,7 +384,7 @@ export function OwnershipPanel({ )}
    -

    JPG, PNG or GIF. Max 5MB.