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,112 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
import { apiFetch } from "@/lib/api";
export default function CashupLandingPage() {
const { token } = useAuth();
const router = useRouter();
const [events, setEvents] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [showPast, setShowPast] = useState(true);
const [showInactive, setShowInactive] = useState(false);
const [showClosed, setShowClosed] = useState(false);
useEffect(() => {
if (!token) return;
(async () => {
setLoading(true);
setError(null);
try {
const evs = await apiFetch<any[]>("/api/events/all?includePast=true&includeInactive=true", { authToken: token });
setEvents(Array.isArray(evs) ? evs : []);
} catch (e: any) {
setError(e?.message || "Failed to load events");
} finally {
setLoading(false);
}
})();
}, [token]);
const filtered = useMemo(() => {
const now = new Date();
return events
.filter(ev => showPast || !ev.endDate || new Date(ev.endDate) >= now)
.filter(ev => showInactive || ev.isActive !== false)
.filter(ev => showClosed || ev.cashupStatus !== "closed")
.filter(ev => !search.trim() || ev.title?.toLowerCase().includes(search.trim().toLowerCase()))
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
}, [events, showPast, showInactive, showClosed, search]);
return (
<div className="max-w-3xl mx-auto space-y-4">
<div className="flex items-center justify-between gap-3">
<div>
<h1 className="text-xl font-semibold">Post-event Cashup</h1>
<p className="text-sm text-gray-500 mt-1">Set costs, reconcile takings, and close out an event. Admin only.</p>
</div>
<button
type="button"
className="px-3 py-1.5 text-sm rounded bg-gray-100 hover:bg-gray-200 text-gray-800 shrink-0"
onClick={() => router.push("/dashboard")}
>Back</button>
</div>
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded p-2">{error}</div>}
<div className="bg-white border rounded-lg p-3 flex flex-wrap items-center gap-3">
<input
className="border rounded px-3 py-1.5 text-sm flex-1 min-w-48"
placeholder="Search events…"
value={search}
onChange={e => setSearch(e.target.value)}
/>
<label className="flex items-center gap-1.5 text-sm text-gray-600">
<input type="checkbox" checked={showPast} onChange={e => setShowPast(e.target.checked)} /> Past events
</label>
<label className="flex items-center gap-1.5 text-sm text-gray-600">
<input type="checkbox" checked={showInactive} onChange={e => setShowInactive(e.target.checked)} /> Inactive events
</label>
<label className="flex items-center gap-1.5 text-sm text-gray-600">
<input type="checkbox" checked={showClosed} onChange={e => setShowClosed(e.target.checked)} /> Closed events
</label>
</div>
{loading && <div className="text-sm text-gray-400">Loading</div>}
{!loading && (
<ul className="space-y-2">
{filtered.map(ev => {
const isClosed = ev.cashupStatus === "closed";
return (
<li
key={ev.id}
className="border rounded-lg p-3 bg-white hover:bg-indigo-50/40 cursor-pointer transition-colors flex items-center justify-between gap-3"
onClick={() => router.push(`/dashboard/admin/cashup/${ev.id}`)}
>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-sm">{ev.title}</span>
<span className={"text-[10px] px-1.5 py-0.5 rounded " + (isClosed ? "bg-rose-50 text-rose-700" : "bg-emerald-50 text-emerald-700")}>
{isClosed ? "Closed" : "Open"}
</span>
</div>
<div className="text-xs text-gray-500 mt-0.5">
{ev.startDate ? new Date(ev.startDate).toLocaleDateString() : ""}{ev.endDate ? ` ${new Date(ev.endDate).toLocaleDateString()}` : ""}
</div>
</div>
<span className="text-xs text-indigo-600 shrink-0">Manage </span>
</li>
);
})}
{filtered.length === 0 && <div className="text-sm text-gray-400">No events match the current filters.</div>}
</ul>
)}
</div>
);
}