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 (
-
) : 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 (
+
+ );
+}
+
+// Screen-reader announcement wrapper for a group of skeleton blocks.
+export function SkeletonGroup({ className, children }: SkeletonProps & { children: React.ReactNode }) {
+ return (
+
+ Loading…
+ {children}
+
+ );
+}
+
+// Paragraph-style stack of text lines with a shorter last line.
+export function SkeletonText({ lines = 3, className }: SkeletonProps & { lines?: number }) {
+ return (
+
+
+ );
+}
+
+// Full admin page placeholder: title bar + table. Used by the `if (loading)`
+// gates that replace the entire page, so it includes the header row.
+export function AdminPageSkeleton({ rows = 6, cols = 5 }: { rows?: number; cols?: number }) {
+ return (
+