diff --git a/frontend/src/app/(public)/components/HomepageFaqSection.tsx b/frontend/src/app/(public)/components/HomepageFaqSection.tsx index bf07678..a816d8f 100644 --- a/frontend/src/app/(public)/components/HomepageFaqSection.tsx +++ b/frontend/src/app/(public)/components/HomepageFaqSection.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { useLanguage } from '@/context/LanguageContext'; import { faqApi, FaqItem } from '@/lib/api'; +import { Skeleton, FaqListSkeleton } from '@/components/ui/Skeleton'; import { ChevronDownIcon } from '@heroicons/react/24/outline'; import Link from 'next/link'; import clsx from 'clsx'; @@ -23,7 +24,20 @@ export default function HomepageFaqSection() { return () => { cancelled = true; }; }, []); - if (loading || faqs.length === 0) { + if (loading) { + return ( +
+
+
+ + +
+
+
+ ); + } + + if (faqs.length === 0) { return null; } diff --git a/frontend/src/app/(public)/components/NextEventSection.tsx b/frontend/src/app/(public)/components/NextEventSection.tsx index 534df66..4dff0be 100644 --- a/frontend/src/app/(public)/components/NextEventSection.tsx +++ b/frontend/src/app/(public)/components/NextEventSection.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { eventsApi, Event } from '@/lib/api'; import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; +import { FeaturedEventSkeleton } from '@/components/ui/Skeleton'; import { CalendarIcon, MapPinIcon, ClockIcon } from '@heroicons/react/24/outline'; interface NextEventSectionProps { @@ -51,11 +52,7 @@ export default function NextEventSection({ initialEvent }: NextEventSectionProps : ''; if (loading) { - return ( -
-
-
- ); + return ; } if (!nextEvent) { diff --git a/frontend/src/app/(public)/dashboard/page.tsx b/frontend/src/app/(public)/dashboard/page.tsx index db57175..255130a 100644 --- a/frontend/src/app/(public)/dashboard/page.tsx +++ b/frontend/src/app/(public)/dashboard/page.tsx @@ -11,6 +11,7 @@ import { UserPayment, } from '@/lib/api'; import toast from 'react-hot-toast'; +import { CardListSkeleton } from '@/components/ui/Skeleton'; import OverviewTab from './components/OverviewTab'; import TicketsTab from './components/TicketsTab'; @@ -104,9 +105,7 @@ export default function DashboardPage() { {/* Tab content */} {loading ? ( -
-
-
+ ) : ( <> {activeTab === 'overview' && ( diff --git a/frontend/src/app/(public)/events/[id]/loading.tsx b/frontend/src/app/(public)/events/[id]/loading.tsx new file mode 100644 index 0000000..044ad82 --- /dev/null +++ b/frontend/src/app/(public)/events/[id]/loading.tsx @@ -0,0 +1,28 @@ +import { Skeleton, ArticleSkeleton } from '@/components/ui/Skeleton'; + +// Route-level skeleton for the event detail page: back link, banner, article +// body on the left and the booking card in the sidebar. +export default function EventDetailLoading() { + return ( +
+
+ +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/src/app/(public)/events/loading.tsx b/frontend/src/app/(public)/events/loading.tsx new file mode 100644 index 0000000..7bea981 --- /dev/null +++ b/frontend/src/app/(public)/events/loading.tsx @@ -0,0 +1,20 @@ +import { Skeleton, EventGridSkeleton } from '@/components/ui/Skeleton'; + +// Route-level skeleton shown during navigation while the server fetches the +// event list. Mirrors the EventsClient shell: title, filter tabs, card grid. +export default function EventsLoading() { + return ( +
+
+ +
+ + +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/app/(public)/faq/loading.tsx b/frontend/src/app/(public)/faq/loading.tsx new file mode 100644 index 0000000..918f1f3 --- /dev/null +++ b/frontend/src/app/(public)/faq/loading.tsx @@ -0,0 +1,16 @@ +import { Skeleton, FaqListSkeleton } from '@/components/ui/Skeleton'; + +// Route-level skeleton for the FAQ page: centered header + accordion rows. +export default function FaqLoading() { + return ( +
+
+
+ + +
+ +
+
+ ); +} diff --git a/frontend/src/app/(public)/legal/[slug]/loading.tsx b/frontend/src/app/(public)/legal/[slug]/loading.tsx new file mode 100644 index 0000000..12918ed --- /dev/null +++ b/frontend/src/app/(public)/legal/[slug]/loading.tsx @@ -0,0 +1,25 @@ +import { Skeleton, SkeletonGroup, SkeletonText } from '@/components/ui/Skeleton'; + +// Route-level skeleton for legal pages: back link, bordered header, prose body. +export default function LegalPageLoading() { + return ( +
+
+ + +
+ + +
+
+ + + + + +
+
+
+
+ ); +} diff --git a/frontend/src/app/admin/bookings/page.tsx b/frontend/src/app/admin/bookings/page.tsx index 77a902c..100aa7b 100644 --- a/frontend/src/app/admin/bookings/page.tsx +++ b/frontend/src/app/admin/bookings/page.tsx @@ -6,6 +6,7 @@ import { ticketsApi, eventsApi, paymentsApi, Ticket, Event } from '@/lib/api'; import { parseDate } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import { BottomSheet, MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { TicketIcon, @@ -246,11 +247,7 @@ export default function AdminBookingsPage() { }; if (loading) { - return ( -
-
-
- ); + return ; } return ( diff --git a/frontend/src/app/admin/contacts/page.tsx b/frontend/src/app/admin/contacts/page.tsx index 30bf0c7..940b543 100644 --- a/frontend/src/app/admin/contacts/page.tsx +++ b/frontend/src/app/admin/contacts/page.tsx @@ -5,6 +5,7 @@ import { useLanguage } from '@/context/LanguageContext'; import { contactsApi, Contact } from '@/lib/api'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { Skeleton, SkeletonText, CardListSkeleton } from '@/components/ui/Skeleton'; import { EnvelopeIcon, EnvelopeOpenIcon, CheckIcon } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; import { parseDate } from '@/lib/utils'; @@ -65,8 +66,21 @@ export default function AdminContactsPage() { if (loading) { return ( -
-
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
); } diff --git a/frontend/src/app/admin/emails/page.tsx b/frontend/src/app/admin/emails/page.tsx index 66c97ed..b002db3 100644 --- a/frontend/src/app/admin/emails/page.tsx +++ b/frontend/src/app/admin/emails/page.tsx @@ -6,6 +6,7 @@ import { emailsApi, EmailTemplate, EmailLog, EmailStats } from '@/lib/api'; import { parseDate } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import Input from '@/components/ui/Input'; import { MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { @@ -418,11 +419,7 @@ export default function AdminEmailsPage() { }; if (loading) { - return ( -
-
-
- ); + return ; } return ( diff --git a/frontend/src/app/admin/events/[id]/page.tsx b/frontend/src/app/admin/events/[id]/page.tsx index 78bf943..8e70dae 100644 --- a/frontend/src/app/admin/events/[id]/page.tsx +++ b/frontend/src/app/admin/events/[id]/page.tsx @@ -8,6 +8,7 @@ import { ticketsApi, emailsApi, adminApi, paymentsApi, siteSettingsApi, Ticket } import { formatDateLong, formatDateCompact, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { Skeleton, TableSkeleton } from '@/components/ui/Skeleton'; import { Dropdown, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { ArrowLeftIcon, @@ -420,8 +421,12 @@ export default function AdminEventDetailPage() { if (loading) { return ( -
-
+
+
+ + +
+
); } diff --git a/frontend/src/app/admin/events/page.tsx b/frontend/src/app/admin/events/page.tsx index 4f08898..54f547b 100644 --- a/frontend/src/app/admin/events/page.tsx +++ b/frontend/src/app/admin/events/page.tsx @@ -7,6 +7,7 @@ import { useLanguage } from '@/context/LanguageContext'; import { eventsApi, siteSettingsApi, Event } from '@/lib/api'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import { MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { PlusIcon, PencilIcon, TrashIcon, EyeIcon, PhotoIcon, DocumentDuplicateIcon, ArchiveBoxIcon, StarIcon, LinkIcon } from '@heroicons/react/24/outline'; import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid'; @@ -148,11 +149,7 @@ export default function AdminEventsPage() { }; if (loading) { - return ( -
-
-
- ); + return ; } return ( diff --git a/frontend/src/app/admin/faq/page.tsx b/frontend/src/app/admin/faq/page.tsx index 2010084..4f9c8dc 100644 --- a/frontend/src/app/admin/faq/page.tsx +++ b/frontend/src/app/admin/faq/page.tsx @@ -5,6 +5,7 @@ import { useLanguage } from '@/context/LanguageContext'; import { faqApi, FaqItemAdmin } from '@/lib/api'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import Input from '@/components/ui/Input'; import { MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import toast from 'react-hot-toast'; @@ -185,11 +186,7 @@ export default function AdminFaqPage() { const handleDragEnd = () => { setDraggedId(null); setDragOverId(null); }; if (loading) { - return ( -
-
-
- ); + return ; } return ( diff --git a/frontend/src/app/admin/gallery/page.tsx b/frontend/src/app/admin/gallery/page.tsx index 459c6ed..ab96b7d 100644 --- a/frontend/src/app/admin/gallery/page.tsx +++ b/frontend/src/app/admin/gallery/page.tsx @@ -6,6 +6,7 @@ import { mediaApi, Media } from '@/lib/api'; import { parseDate } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { Skeleton, ImageGridSkeleton } from '@/components/ui/Skeleton'; import { PhotoIcon, TrashIcon, @@ -126,8 +127,12 @@ export default function AdminGalleryPage() { if (loading) { return ( -
-
+
+
+ + +
+
); } diff --git a/frontend/src/app/admin/legal-pages/page.tsx b/frontend/src/app/admin/legal-pages/page.tsx index 7674d43..51bbab5 100644 --- a/frontend/src/app/admin/legal-pages/page.tsx +++ b/frontend/src/app/admin/legal-pages/page.tsx @@ -7,6 +7,7 @@ import { legalPagesApi, LegalPage } from '@/lib/api'; import { parseDate } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import Input from '@/components/ui/Input'; import toast from 'react-hot-toast'; import clsx from 'clsx'; @@ -173,11 +174,7 @@ export default function AdminLegalPagesPage() { }; if (loading) { - return ( -
-
-
- ); + return ; } // Editor view diff --git a/frontend/src/app/admin/payments/page.tsx b/frontend/src/app/admin/payments/page.tsx index 255d7ac..134b55c 100644 --- a/frontend/src/app/admin/payments/page.tsx +++ b/frontend/src/app/admin/payments/page.tsx @@ -6,6 +6,7 @@ import { paymentsApi, adminApi, eventsApi, PaymentWithDetails, Event, ExportedPa import { parseDate } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import Input from '@/components/ui/Input'; import { BottomSheet, MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { @@ -364,11 +365,7 @@ export default function AdminPaymentsPage() { const onHoldBookingsCount = getUniqueBookingsCount(onHoldPayments); if (loading) { - return ( -
-
-
- ); + return ; } return ( diff --git a/frontend/src/app/admin/tickets/page.tsx b/frontend/src/app/admin/tickets/page.tsx index 2a30e29..3216748 100644 --- a/frontend/src/app/admin/tickets/page.tsx +++ b/frontend/src/app/admin/tickets/page.tsx @@ -6,6 +6,7 @@ import { ticketsApi, eventsApi, Ticket, Event } from '@/lib/api'; import { parseDate } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import Input from '@/components/ui/Input'; import { BottomSheet, MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { CheckCircleIcon, XCircleIcon, PlusIcon, FunnelIcon, XMarkIcon } from '@heroicons/react/24/outline'; @@ -135,11 +136,7 @@ export default function AdminTicketsPage() { }; if (loading) { - return ( -
-
-
- ); + return ; } return ( diff --git a/frontend/src/app/admin/users/page.tsx b/frontend/src/app/admin/users/page.tsx index c8ef8e5..1b62b57 100644 --- a/frontend/src/app/admin/users/page.tsx +++ b/frontend/src/app/admin/users/page.tsx @@ -6,6 +6,7 @@ import { usersApi, eventsApi, User, Event } from '@/lib/api'; import { parseDate } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; +import { AdminPageSkeleton } from '@/components/ui/Skeleton'; import Input from '@/components/ui/Input'; import { MoreMenu, DropdownItem, BottomSheet, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { TrashIcon, PencilSquareIcon, FunnelIcon, XMarkIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'; @@ -166,11 +167,7 @@ export default function AdminUsersPage() { }; if (loading) { - return ( -
-
-
- ); + return ; } return ( diff --git a/frontend/src/app/linktree/page.tsx b/frontend/src/app/linktree/page.tsx index be6e740..7b8eac5 100644 --- a/frontend/src/app/linktree/page.tsx +++ b/frontend/src/app/linktree/page.tsx @@ -73,8 +73,22 @@ export default function LinktreePage() { {loading ? ( -
-
+
+ Loading… +
+
+
+
+
+
+
+
+
+
) : nextEvent ? ( diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx new file mode 100644 index 0000000..db60571 --- /dev/null +++ b/frontend/src/components/ui/Skeleton.tsx @@ -0,0 +1,227 @@ +import clsx from 'clsx'; + +// Skeleton loading previews. The base block uses Tailwind's animate-pulse and +// the light-gray surface tokens so placeholders match real card/table styling. +// Composites below mirror the layouts they stand in for (event cards, admin +// tables, FAQ accordions, article bodies) to avoid layout shift when data lands. + +interface SkeletonProps { + className?: string; +} + +export function Skeleton({ className }: SkeletonProps) { + return ( +