Initial commit

Next.js + Express event management app for Hope Family Church.
This commit is contained in:
2026-07-23 15:26:47 +02:00
commit 3d381944d2
246 changed files with 57565 additions and 0 deletions
@@ -0,0 +1,80 @@
import { ApiImage } from "@/components/shared/ApiImage";
type Event = {
id: string;
title: string;
description?: string;
startDate: string;
endDate: string;
registrationDeadline?: string | null;
goLiveAt?: string;
price: number;
picture?: string;
isSoldOut?: boolean;
};
import { formatDateTimeRange } from "@/lib/date";
export const EventCard = ({ event }: { event: Event }) => {
const dateRange = formatDateTimeRange(event.startDate, event.endDate);
return (
<div className="border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition">
<a href={`/events/${event.id}`} className="block">
<ApiImage src={event.picture} alt={event.title} className="h-48 w-full object-cover" />
<div className="p-4">
<h3 className="text-lg font-semibold hover:underline">{event.title}</h3>
<p className="text-sm text-gray-500">{dateRange}</p>
<p className="text-sm text-gray-600 mt-2 line-clamp-2">{event.description}</p>
</div>
</a>
<div className="px-4 pb-4">
<div className="flex gap-2">
<a
href={`/events/${event.id}`}
className="flex-1 text-center text-sm text-gray-700 border py-2 rounded hover:bg-gray-50"
>
View details
</a>
{(() => {
const now = new Date();
const end = new Date(event.endDate);
const deadline = event.registrationDeadline ? new Date(event.registrationDeadline) : null;
const goLive = event.goLiveAt ? new Date(event.goLiveAt) : null;
const notYetOpen = goLive ? now < goLive : false;
const closed = (deadline ? now >= deadline : false) || now >= end;
if (notYetOpen) {
return (
<button disabled className="flex-1 text-center text-sm text-gray-500 bg-gray-200 py-2 rounded cursor-not-allowed" title="Registration not yet open">
Opens {goLive?.toLocaleString()}
</button>
);
}
if (closed) {
return (
<button disabled className="flex-1 text-center text-sm text-gray-500 bg-gray-200 py-2 rounded cursor-not-allowed" title="Registration closed">
Registration closed
</button>
);
}
if (event.isSoldOut) {
return (
<button disabled className="flex-1 text-center text-sm text-red-700 bg-red-50 border border-red-200 py-2 rounded cursor-not-allowed">
Sold Out
</button>
);
}
return (
<a
href={`/register/${event.id}`}
className="flex-1 text-center text-sm text-white bg-blue-600 py-2 rounded hover:bg-blue-700"
>
Register{event.price > 0 ? ` - R${event.price.toFixed(2)}` : ""}
</a>
);
})()}
</div>
</div>
</div>
);
};