← Files WixARCHIVED FILE
skills/wix-headless-templates/events/project/src/components/events/EventsView.tsx
4.84 KB · Oct 8, 2026 · 12:02 UTC
// REFERENCE listing surface: category filter + events grid + "load more" on the @theme tokens.
// Correct and complete; per the skill's model you design and build your own on useEvents.
import type { ComponentType, ReactNode } from "react";
import { useEvents } from "../../hooks/events/useEvents";
import { imgAttrs } from "../../wix/media";
import type { EventSummary } from "../../wix/events/types";
export interface LinkLikeProps {
href: string;
className?: string;
children?: ReactNode;
}
const PlainLink = ({ href, className, children }: LinkLikeProps) => (
<a href={href} className={className}>
{children}
</a>
);
export interface EventCardProps {
event: EventSummary;
eventHref?: (slug: string) => string;
LinkComponent?: ComponentType<LinkLikeProps>;
}
export function EventCard({
event,
eventHref = (slug) => `/events/${encodeURIComponent(slug)}`,
LinkComponent = PlainLink,
}: EventCardProps) {
return (
<LinkComponent href={eventHref(event.slug)} className="group block no-underline">
<div className="relative aspect-[3/2] overflow-hidden rounded-lg bg-secondary">
{event.imageUrl && (
<img
{...imgAttrs(event.imageUrl, "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw", 2 / 3)}
alt={event.title}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
/>
)}
{event.soldOut && (
<span className="absolute left-3 top-3 rounded-control bg-background/90 px-3 py-1 text-xs font-semibold text-foreground backdrop-blur">
Sold out
</span>
)}
</div>
{event.dateLabel && <p className="eyebrow mt-3">{event.dateLabel}</p>}
<p className="mt-1 text-sm font-medium text-foreground">{event.title}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{event.locationType === "ONLINE" ? "Online" : event.locationName}
{event.priceLabel ? ` · ${event.priceLabel}` : ""}
</p>
</LinkComponent>
);
}
export interface EventsViewProps {
initialEvents?: EventSummary[];
/** From fetchEventsPage — lets the list know there is more to load. */
initialTotal?: number;
emptyMessage?: string;
eventHref?: EventCardProps["eventHref"];
LinkComponent?: ComponentType<LinkLikeProps>;
CardComponent?: ComponentType<EventCardProps>;
}
const pill = (active: boolean) =>
`rounded-control border px-4 py-1.5 text-sm font-medium transition-colors ${
active
? "border-primary bg-primary text-primary-foreground"
: "border-border text-foreground hover:bg-secondary"
}`;
export default function EventsView({
initialEvents,
initialTotal,
emptyMessage = "No upcoming events — check back soon.",
eventHref,
LinkComponent,
CardComponent = EventCard,
}: EventsViewProps) {
const { events, categories, activeCategoryId, setActiveCategoryId, hasMore, loadingMore, loadMore, error } = useEvents({
initialEvents,
initialTotal,
});
return (
<div>
{categories.length > 1 && (
<div className="mb-8 flex flex-wrap gap-2" role="group" aria-label="Categories">
<button type="button" className={pill(activeCategoryId === null)} onClick={() => setActiveCategoryId(null)}>
All
</button>
{categories.map((c) => (
<button key={c.id} type="button" className={pill(activeCategoryId === c.id)} onClick={() => setActiveCategoryId(c.id)}>
{c.name}
</button>
))}
</div>
)}
{error && <p className="mb-4 text-sm text-destructive">{error}</p>}
{events === null ? (
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
{Array.from({ length: 6 }, (_, i) => (
<div key={i}>
<div className="aspect-[3/2] animate-pulse rounded-lg bg-secondary" />
<div className="mt-3 h-3.5 w-2/3 animate-pulse rounded bg-secondary" />
</div>
))}
</div>
) : events.length === 0 ? (
<p className="py-16 text-center text-muted-foreground">{loadingMore ? "Loading…" : emptyMessage}</p>
) : (
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{events.map((e) => (
<CardComponent key={e.id} event={e} eventHref={eventHref} LinkComponent={LinkComponent} />
))}
</div>
)}
{hasMore && (
<div className="mt-10 text-center">
<button
type="button"
disabled={loadingMore}
onClick={() => void loadMore()}
className="rounded-control border border-border px-6 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary disabled:opacity-50"
>
{loadingMore ? "Loading…" : "Load more"}
</button>
</div>
)}
</div>
);
}
SHA-256: 3c93d51e61b806a00f2b074beeeb86eb4b2732049d1e17c381936b61c03f9a8e