Add registration status badges and auto-dismissing dashboard messages
Two consistency fixes requested after the payment-method work: 1. Registration status (pending/confirmed/partial_paid/paid/cancelled) was printed as a raw string on the user dashboard. Added RegistrationStatusBadge mirroring the existing EventStatusBadge pattern, using the same status colors already established on dashboard/admin/registrations. 2. Inline success/error banners across dashboard pages persisted indefinitely. Added a shared useDismissingState hook (drop-in useState replacement that auto-clears a truthy value after 7s, resetting the timer on each update) and swapped it in across ~24 dashboard files. Excluded: message-only modal dialogs (ticket- scanning's success/error confirmations) and two states that mix live form-validation feedback with async results inside actively- open forms (the registration-edit modal's editError, the event create/edit modal's error) - those keep persisting until the user acts, since auto-hiding a "fix this field" message mid-edit would be a regression. Also fixed at-the-door's existing bespoke auto-dismiss timers (10s/15s, one mislabeled as "5s") to the same consistent 7s, and removed admin/settings' manual x dismiss button in favor of the same auto-only behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- User dashboard: new "Payment history" page listing the user's own payments (donations excluded), with server-side pagination (25 per page), date range, method, and payment/refund filters.
|
- User dashboard: new "Payment history" page listing the user's own payments (donations excluded), with server-side pagination (25 per page), date range, method, and payment/refund filters.
|
||||||
|
- User dashboard: registration status (Pending/Confirmed/Partially Paid/Paid/Cancelled) is now shown as a colored badge, matching the existing event Closed/Past/Inactive badge convention, instead of a raw status string.
|
||||||
|
- Dashboard-wide: inline success/error/confirmation messages (e.g. after creating a manual registration on `/dashboard/supervisor/manual`) now auto-dismiss after 7 seconds instead of persisting indefinitely, via a new shared `useDismissingState` hook. Applied consistently across all dashboard pages with this pattern; excluded are message-only modal dialogs (e.g. ticket-scanning's success/error confirmations, which still require a manual OK) and a couple of mixed validation/async error states shown inside actively-open forms (the registration-edit modal and the event create/edit modal), which continue to persist until the user acts.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
|||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import type { EventCost, EventCostType, EventFinancials, CashupMethod } from "@/types";
|
import type { EventCost, EventCostType, EventFinancials, CashupMethod } from "@/types";
|
||||||
|
|
||||||
const METHOD_LABELS: Record<CashupMethod, string> = { cash: "Cash", card: "Card", eft: "EFT", other: "Other" };
|
const METHOD_LABELS: Record<CashupMethod, string> = { cash: "Cash", card: "Card", eft: "EFT", other: "Other" };
|
||||||
@@ -29,7 +30,7 @@ export default function EventCashupPage() {
|
|||||||
const [data, setData] = useState<EventFinancials | null>(null);
|
const [data, setData] = useState<EventFinancials | null>(null);
|
||||||
const [eventOptions, setEventOptions] = useState<{ id: string; name: string }[]>([]);
|
const [eventOptions, setEventOptions] = useState<{ id: string; name: string }[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const isClosed = data?.event?.cashupStatus === "closed";
|
const isClosed = data?.event?.cashupStatus === "closed";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
export default function CashupLandingPage() {
|
export default function CashupLandingPage() {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
@@ -11,7 +12,7 @@ export default function CashupLandingPage() {
|
|||||||
|
|
||||||
const [events, setEvents] = useState<any[]>([]);
|
const [events, setEvents] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [showPast, setShowPast] = useState(true);
|
const [showPast, setShowPast] = useState(true);
|
||||||
const [showInactive, setShowInactive] = useState(false);
|
const [showInactive, setShowInactive] = useState(false);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
const STATUS_OPTIONS = ["pending", "confirmed", "partial_paid", "paid", "cancelled"] as const;
|
const STATUS_OPTIONS = ["pending", "confirmed", "partial_paid", "paid", "cancelled"] as const;
|
||||||
|
|
||||||
@@ -29,8 +30,8 @@ export default function AdminRegistrationsPage() {
|
|||||||
const [registrations, setRegistrations] = useState<any[]>([]);
|
const [registrations, setRegistrations] = useState<any[]>([]);
|
||||||
const [events, setEvents] = useState<any[]>([]);
|
const [events, setEvents] = useState<any[]>([]);
|
||||||
const [loadingRegs, setLoadingRegs] = useState(false);
|
const [loadingRegs, setLoadingRegs] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch, API_BASE, resolveToApiOrigin } from "@/lib/api";
|
import { apiFetch, API_BASE, resolveToApiOrigin } from "@/lib/api";
|
||||||
import { useSiteSettings } from "@/contexts/SiteSettingsContext";
|
import { useSiteSettings } from "@/contexts/SiteSettingsContext";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
type TabId = "organisation" | "branding" | "notifications" | "email" | "legal";
|
type TabId = "organisation" | "branding" | "notifications" | "email" | "legal";
|
||||||
|
|
||||||
@@ -36,19 +37,17 @@ function Field({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SaveBar({
|
function SaveBar({
|
||||||
saving, onSave, result, onDismiss,
|
saving, onSave, result,
|
||||||
}: {
|
}: {
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
result: { ok: boolean; message: string } | null;
|
result: { ok: boolean; message: string } | null;
|
||||||
onDismiss: () => void;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between pt-4 border-t mt-6 flex-wrap gap-3">
|
<div className="flex items-center justify-between pt-4 border-t mt-6 flex-wrap gap-3">
|
||||||
{result ? (
|
{result ? (
|
||||||
<span className={`text-sm flex items-center gap-1.5 ${result.ok ? "text-green-600" : "text-red-600"}`}>
|
<span className={`text-sm flex items-center gap-1.5 ${result.ok ? "text-green-600" : "text-red-600"}`}>
|
||||||
{result.ok ? "✓" : "✗"} {result.message}
|
{result.ok ? "✓" : "✗"} {result.message}
|
||||||
<button type="button" onClick={onDismiss} className="ml-1 text-gray-400 hover:text-gray-600 text-xs">×</button>
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span />
|
<span />
|
||||||
@@ -75,7 +74,7 @@ export default function SiteSettingsPage() {
|
|||||||
|
|
||||||
// Per-tab save state
|
// Per-tab save state
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
|
const [result, setResult] = useDismissingState<{ ok: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
// ── Organisation ──
|
// ── Organisation ──
|
||||||
const [orgName, setOrgName] = useState("");
|
const [orgName, setOrgName] = useState("");
|
||||||
@@ -104,7 +103,7 @@ export default function SiteSettingsPage() {
|
|||||||
const [smtpPass, setSmtpPass] = useState("");
|
const [smtpPass, setSmtpPass] = useState("");
|
||||||
const [smtpPassSet, setSmtpPassSet] = useState(false);
|
const [smtpPassSet, setSmtpPassSet] = useState(false);
|
||||||
const [smtpTesting, setSmtpTesting] = useState(false);
|
const [smtpTesting, setSmtpTesting] = useState(false);
|
||||||
const [smtpTestResult, setSmtpTestResult] = useState<{ ok: boolean; message: string; raw?: string } | null>(null);
|
const [smtpTestResult, setSmtpTestResult] = useDismissingState<{ ok: boolean; message: string; raw?: string } | null>(null);
|
||||||
|
|
||||||
// ── Legal ──
|
// ── Legal ──
|
||||||
const [legalOperatorName, setLegalOperatorName] = useState("");
|
const [legalOperatorName, setLegalOperatorName] = useState("");
|
||||||
@@ -332,7 +331,7 @@ export default function SiteSettingsPage() {
|
|||||||
<input className={inputCls} placeholder="https://events.yourchurch.org"
|
<input className={inputCls} placeholder="https://events.yourchurch.org"
|
||||||
value={appBaseUrl} onChange={e => setAppBaseUrl(e.target.value)} />
|
value={appBaseUrl} onChange={e => setAppBaseUrl(e.target.value)} />
|
||||||
</Field>
|
</Field>
|
||||||
<SaveBar saving={saving} onSave={saveOrganisation} result={result} onDismiss={() => setResult(null)} />
|
<SaveBar saving={saving} onSave={saveOrganisation} result={result} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -371,7 +370,7 @@ export default function SiteSettingsPage() {
|
|||||||
}} className="text-sm" />
|
}} className="text-sm" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<SaveBar saving={saving} onSave={saveBranding} result={result} onDismiss={() => setResult(null)} />
|
<SaveBar saving={saving} onSave={saveBranding} result={result} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -385,7 +384,7 @@ export default function SiteSettingsPage() {
|
|||||||
<input className={inputCls} placeholder="registrations@yourchurch.org, admin@yourchurch.org"
|
<input className={inputCls} placeholder="registrations@yourchurch.org, admin@yourchurch.org"
|
||||||
value={notifEmails} onChange={e => setNotifEmails(e.target.value)} />
|
value={notifEmails} onChange={e => setNotifEmails(e.target.value)} />
|
||||||
</Field>
|
</Field>
|
||||||
<SaveBar saving={saving} onSave={saveNotifications} result={result} onDismiss={() => setResult(null)} />
|
<SaveBar saving={saving} onSave={saveNotifications} result={result} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -464,7 +463,7 @@ export default function SiteSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SaveBar saving={saving} onSave={saveSmtp} result={result} onDismiss={() => setResult(null)} />
|
<SaveBar saving={saving} onSave={saveSmtp} result={result} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -503,7 +502,7 @@ export default function SiteSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SaveBar saving={saving} onSave={saveLegal} result={result} onDismiss={() => setResult(null)} />
|
<SaveBar saving={saving} onSave={saveLegal} result={result} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
interface UserItem {
|
interface UserItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -43,7 +44,7 @@ export default function AdminUsersPage() {
|
|||||||
// Data state
|
// Data state
|
||||||
const [users, setUsers] = useState<UserItem[]>([]);
|
const [users, setUsers] = useState<UserItem[]>([]);
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ export default function WhatsAppAdminPage() {
|
|||||||
const [pairingPhone, setPairingPhone] = useState("");
|
const [pairingPhone, setPairingPhone] = useState("");
|
||||||
|
|
||||||
// ── Shared action feedback ───────────────────────────────────────────────────
|
// ── Shared action feedback ───────────────────────────────────────────────────
|
||||||
const [actionMsg, setActionMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null);
|
const [actionMsg, setActionMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null);
|
||||||
const [busy, setBusy] = useState<string | null>(null);
|
const [busy, setBusy] = useState<string | null>(null);
|
||||||
|
|
||||||
// ── Load config ──────────────────────────────────────────────────────────────
|
// ── Load config ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import React, { Suspense, useEffect, useMemo, useState } from "react";
|
import React, { Suspense, useEffect, useMemo, useState } from "react";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { formatDate } from "@/lib/date";
|
import { formatDate } from "@/lib/date";
|
||||||
import { QrImage } from "@/components/shared/QrImage";
|
import { QrImage } from "@/components/shared/QrImage";
|
||||||
@@ -18,7 +19,7 @@ function EventTicketsContent() {
|
|||||||
const [events, setEvents] = useState<any[]>([]);
|
const [events, setEvents] = useState<any[]>([]);
|
||||||
const [eventId, setEventId] = useState<string>(paramEventId);
|
const [eventId, setEventId] = useState<string>(paramEventId);
|
||||||
const [tickets, setTickets] = useState<any[]>([]);
|
const [tickets, setTickets] = useState<any[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string>(paramUserId);
|
const [selectedUserId, setSelectedUserId] = useState<string>(paramUserId);
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { QRScanner, QRScannerHandle } from "@/components/qr/QRScanner";
|
import { QRScanner, QRScannerHandle } from "@/components/qr/QRScanner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function TicketScanningPage() {
|
export default function TicketScanningPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const scannerRef = useRef<QRScannerHandle>(null);
|
const scannerRef = useRef<QRScannerHandle>(null);
|
||||||
const [lastResult, setLastResult] = useState<string | null>(null);
|
const [lastResult, setLastResult] = useState<string | null>(null);
|
||||||
const [scanInfo, setScanInfo] = useState<string | null>(null);
|
const [scanInfo, setScanInfo] = useDismissingState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [alreadyUsedModal, setAlreadyUsedModal] = useState<{ message: string; ticket?: any } | null>(null);
|
const [alreadyUsedModal, setAlreadyUsedModal] = useState<{ message: string; ticket?: any } | null>(null);
|
||||||
const [errorModal, setErrorModal] = useState<string | null>(null);
|
const [errorModal, setErrorModal] = useState<string | null>(null);
|
||||||
const { token, user } = useAuth();
|
const { token, user } = useAuth();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
type Mode = "registration" | "payment" | "tickets" | "refund";
|
type Mode = "registration" | "payment" | "tickets" | "refund";
|
||||||
|
|
||||||
@@ -154,30 +155,8 @@ export default function AtTheDoorPage() {
|
|||||||
const [mode, setMode] = useState<Mode>("registration");
|
const [mode, setMode] = useState<Mode>("registration");
|
||||||
|
|
||||||
const [activeRegistration, setActiveRegistration] = useState<any | null>(null);
|
const [activeRegistration, setActiveRegistration] = useState<any | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!info) return;
|
|
||||||
|
|
||||||
const id = setTimeout(() => {
|
|
||||||
setInfo(null);
|
|
||||||
}, 10000); // ⏱ disappears after 5s
|
|
||||||
|
|
||||||
return () => clearTimeout(id);
|
|
||||||
|
|
||||||
}, [info]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!error) return;
|
|
||||||
|
|
||||||
const id = setTimeout(() => {
|
|
||||||
setError(null);
|
|
||||||
}, 15000); // errors linger slightly longer
|
|
||||||
|
|
||||||
return () => clearTimeout(id);
|
|
||||||
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
const handleRegistrationCreated = (registration: any) => {
|
const handleRegistrationCreated = (registration: any) => {
|
||||||
setActiveRegistration(registration);
|
setActiveRegistration(registration);
|
||||||
@@ -1332,7 +1311,7 @@ function SendTicketsModal({ open, onClose, token, registration, setError, setInf
|
|||||||
const [phone, setPhone] = useState("");
|
const [phone, setPhone] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [localError, setLocalError] = useState("");
|
const [localError, setLocalError] = useDismissingState("");
|
||||||
const [localInfo, setLocalInfo] = useState("");
|
const [localInfo, setLocalInfo] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1369,7 +1348,7 @@ function SendTicketsModal({ open, onClose, token, registration, setError, setInf
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
setLocalInfo("Tickets sent successfully.");
|
setLocalInfo("Tickets sent successfully.");
|
||||||
setTimeout(() => { setLocalInfo(""); onClose(); }, 2000);
|
setTimeout(() => { setLocalInfo(""); onClose(); }, 7000);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setLocalError(e?.message || "Failed to send tickets");
|
setLocalError(e?.message || "Failed to send tickets");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1461,7 +1440,7 @@ function RefundModal({ open, onClose, token, registration, maxRefund, onRefunded
|
|||||||
const [method, setMethod] = useState("cash");
|
const [method, setMethod] = useState("cash");
|
||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [localError, setLocalError] = useState("");
|
const [localError, setLocalError] = useDismissingState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) { setAmount(""); setReason(""); setLocalError(""); }
|
if (open) { setAmount(""); setReason(""); setLocalError(""); }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { Suspense, useEffect, useMemo, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
type Attendee = { id: string; name: string; email: string; pref: string };
|
type Attendee = { id: string; name: string; email: string; pref: string };
|
||||||
|
|
||||||
@@ -171,8 +172,8 @@ function EmailAttendeesPageInner() {
|
|||||||
|
|
||||||
// Load events for selection
|
// Load events for selection
|
||||||
const [loadingEvents, setLoadingEvents] = useState(false);
|
const [loadingEvents, setLoadingEvents] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
// Tabs: attendees (current), automations (coming soon), broadcasts
|
// Tabs: attendees (current), automations (coming soon), broadcasts
|
||||||
const [tab, setTab] = useState<'attendees'|'automations'|'broadcasts'|'scheduled'>('attendees');
|
const [tab, setTab] = useState<'attendees'|'automations'|'broadcasts'|'scheduled'>('attendees');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { Suspense, useEffect, useMemo, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
// Format a Date (or date-like input) to the value expected by <input type="datetime-local">
|
// Format a Date (or date-like input) to the value expected by <input type="datetime-local">
|
||||||
// This returns local time (browser timezone) as YYYY-MM-DDTHH:mm
|
// This returns local time (browser timezone) as YYYY-MM-DDTHH:mm
|
||||||
@@ -112,8 +113,8 @@ function EventOptionsContent() {
|
|||||||
const [selectedEventId, setSelectedEventId] = useState<string>("");
|
const [selectedEventId, setSelectedEventId] = useState<string>("");
|
||||||
const [options, setOptions] = useState<any[]>([]);
|
const [options, setOptions] = useState<any[]>([]);
|
||||||
const [loadingEv, setLoadingEv] = useState(false);
|
const [loadingEv, setLoadingEv] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
const loadEvents = async () => {
|
const loadEvents = async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch, resolveToApiOrigin } from "@/lib/api";
|
import { apiFetch, resolveToApiOrigin } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -690,7 +691,7 @@ function NotifyRecipientsManager({ eventId, creator, initialRecipients }: { even
|
|||||||
const [selected, setSelected] = useState<NotifyUser[]>(initialRecipients || []);
|
const [selected, setSelected] = useState<NotifyUser[]>(initialRecipients || []);
|
||||||
const [loading, setLoading] = useState(!hasInitial);
|
const [loading, setLoading] = useState(!hasInitial);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasInitial) return; // already have the data — skip the network round trip entirely
|
if (hasInitial) return; // already have the data — skip the network round trip entirely
|
||||||
@@ -1415,7 +1416,7 @@ export default function ManageEventsPage() {
|
|||||||
|
|
||||||
const [events, setEvents] = useState<any[]>([]);
|
const [events, setEvents] = useState<any[]>([]);
|
||||||
const [loadingEvents, setLoadingEvents] = useState(false);
|
const [loadingEvents, setLoadingEvents] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
const loadEvents = async () => {
|
const loadEvents = async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
type FormFieldType = 'yes_no' | 'text' | 'date' | 'numeric' | 'statement' | 'paragraph';
|
type FormFieldType = 'yes_no' | 'text' | 'date' | 'numeric' | 'statement' | 'paragraph';
|
||||||
|
|
||||||
@@ -144,8 +145,8 @@ export default function FormsBrowserPage() {
|
|||||||
const [items, setItems] = useState<any[]>([]);
|
const [items, setItems] = useState<any[]>([]);
|
||||||
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
const [loadingList, setLoadingList] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
const loadEvents = async () => {
|
const loadEvents = async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
type FormField = { id: string; type: 'yes_no'|'text'|'date'|'numeric'|'statement'|'paragraph'; label: string; isRequired?: boolean; helpText?: string|null };
|
type FormField = { id: string; type: 'yes_no'|'text'|'date'|'numeric'|'statement'|'paragraph'; label: string; isRequired?: boolean; helpText?: string|null };
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ export default function ManualRegistrationPage() {
|
|||||||
const [phoneNumber, setPhoneNumber] = useState("");
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
const [registerAsGuest, setRegisterAsGuest] = useState(false);
|
const [registerAsGuest, setRegisterAsGuest] = useState(false);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [createdReg, setCreatedReg] = useState<any | null>(null);
|
const [createdReg, setCreatedReg] = useState<any | null>(null);
|
||||||
const [form, setForm] = useState<{ isRequired: boolean; fields: FormField[] } | null>(null);
|
const [form, setForm] = useState<{ isRequired: boolean; fields: FormField[] } | null>(null);
|
||||||
const [formsData, setFormsData] = useState<Record<number, Record<string, string>>>({});
|
const [formsData, setFormsData] = useState<Record<number, Record<string, string>>>({});
|
||||||
@@ -110,8 +111,8 @@ export default function ManualRegistrationPage() {
|
|||||||
function AttendeeFormsSection({ registration, form, formsData, setFormsData }: { registration: any; form: { isRequired: boolean; fields: FormField[] }; formsData: Record<number, Record<string,string>>; setFormsData: any; }) {
|
function AttendeeFormsSection({ registration, form, formsData, setFormsData }: { registration: any; form: { isRequired: boolean; fields: FormField[] }; formsData: Record<number, Record<string,string>>; setFormsData: any; }) {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
const mainTickets = (registration?.registrationOptions || []).filter((o: any) => o?.eventOption?.isMainTicket).reduce((s: number, o: any) => s + (o.quantity || 0), 0);
|
const mainTickets = (registration?.registrationOptions || []).filter((o: any) => o?.eventOption?.isMainTicket).reduce((s: number, o: any) => s + (o.quantity || 0), 0);
|
||||||
const count = Math.max(0, mainTickets);
|
const count = Math.max(0, mainTickets);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
// ─── Pricing helpers ─────────────────────────────────────────────────────────
|
// ─── Pricing helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -72,8 +73,8 @@ export default function ManualRegistrationPage() {
|
|||||||
const searchRef = useRef<HTMLDivElement>(null);
|
const searchRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useDismissingState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
// Load all users for client-side fuzzy matching
|
// Load all users for client-side fuzzy matching
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { apiFetch, fetchAllUsers, fetchAllPayments } from "@/lib/api";
|
import { apiFetch, fetchAllUsers, fetchAllPayments } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||||
|
|
||||||
function RegistrationOptions({ regs, regOutstanding }: {
|
function RegistrationOptions({ regs, regOutstanding }: {
|
||||||
@@ -115,8 +116,8 @@ function PaymentsContent() {
|
|||||||
|
|
||||||
const [payments, setPayments] = useState<any[]>([]);
|
const [payments, setPayments] = useState<any[]>([]);
|
||||||
const [loadingList, setLoadingList] = useState(false);
|
const [loadingList, setLoadingList] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
const [registrations, setRegistrations] = useState<any[]>([]);
|
const [registrations, setRegistrations] = useState<any[]>([]);
|
||||||
const [loadingRegs, setLoadingRegs] = useState(false);
|
const [loadingRegs, setLoadingRegs] = useState(false);
|
||||||
const [regOutstanding, setRegOutstanding] = useState<Record<string, { totalDue: number; totalPaid: number; outstanding: number }>>({});
|
const [regOutstanding, setRegOutstanding] = useState<Record<string, { totalDue: number; totalPaid: number; outstanding: number }>>({});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { Suspense, useEffect, useMemo, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
// Attendee with preference info
|
// Attendee with preference info
|
||||||
type Attendee = { id: string; name: string; phone: string; pref: string };
|
type Attendee = { id: string; name: string; phone: string; pref: string };
|
||||||
@@ -358,8 +359,8 @@ function WhatsAppAttendeesPageInner() {
|
|||||||
if (!user) router.replace("/login");
|
if (!user) router.replace("/login");
|
||||||
}, [user, loading, router]);
|
}, [user, loading, router]);
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
const [tab, setTab] = useState<"attendees" | "automations" | "broadcasts" | "scheduled">("attendees");
|
const [tab, setTab] = useState<"attendees" | "automations" | "broadcasts" | "scheduled">("attendees");
|
||||||
const [loadingEvents, setLoadingEvents] = useState(false);
|
const [loadingEvents, setLoadingEvents] = useState(false);
|
||||||
const [allEvents, setAllEvents] = useState<any[]>([]);
|
const [allEvents, setAllEvents] = useState<any[]>([]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
export default function DonatePage() {
|
export default function DonatePage() {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
@@ -9,8 +10,8 @@ export default function DonatePage() {
|
|||||||
const [eventId, setEventId] = useState<string>("");
|
const [eventId, setEventId] = useState<string>("");
|
||||||
const [amount, setAmount] = useState<string>("");
|
const [amount, setAmount] = useState<string>("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { Suspense, useEffect, useMemo, useState } from "react";
|
|||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
// Types for form fields
|
// Types for form fields
|
||||||
type FormField = { id: string; type: 'yes_no'|'text'|'date'|'numeric'|'statement'|'paragraph'; label: string; isRequired?: boolean; helpText?: string|null };
|
type FormField = { id: string; type: 'yes_no'|'text'|'date'|'numeric'|'statement'|'paragraph'; label: string; isRequired?: boolean; helpText?: string|null };
|
||||||
@@ -16,8 +17,8 @@ function FormsContent() {
|
|||||||
const [registration, setRegistration] = useState<any | null>(null);
|
const [registration, setRegistration] = useState<any | null>(null);
|
||||||
const [eventForm, setEventForm] = useState<{ isRequired: boolean; fields: FormField[] } | null>(null);
|
const [eventForm, setEventForm] = useState<{ isRequired: boolean; fields: FormField[] } | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
|
|
||||||
// Local entry state for new responses
|
// Local entry state for new responses
|
||||||
const [formsData, setFormsData] = useState<Record<number, Record<string, string>>>({});
|
const [formsData, setFormsData] = useState<Record<number, Record<string, string>>>({});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { formatDate } from "@/lib/date";
|
import { formatDate } from "@/lib/date";
|
||||||
import { formatPaymentMethod } from "@/lib/paymentMethod";
|
import { formatPaymentMethod } from "@/lib/paymentMethod";
|
||||||
import { QrImage } from "@/components/shared/QrImage";
|
import { QrImage } from "@/components/shared/QrImage";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
// Helper formatters
|
// Helper formatters
|
||||||
const formatRand = (n: number) => `R ${n.toFixed(2)}`;
|
const formatRand = (n: number) => `R ${n.toFixed(2)}`;
|
||||||
@@ -30,6 +31,20 @@ function EventStatusBadge({ event }: { event: any }) {
|
|||||||
return <span className={`text-[10px] px-1.5 py-0.5 rounded ${className}`}>{label}</span>;
|
return <span className={`text-[10px] px-1.5 py-0.5 rounded ${className}`}>{label}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status badge for a registration, shown wherever registration.status is displayed on this
|
||||||
|
// page. Colors match the status coloring already used on dashboard/admin/registrations.
|
||||||
|
const REGISTRATION_STATUS_STYLES: Record<string, { label: string; className: string }> = {
|
||||||
|
pending: { label: 'Pending', className: 'bg-gray-100 text-gray-600' },
|
||||||
|
confirmed: { label: 'Confirmed', className: 'bg-blue-50 text-blue-700' },
|
||||||
|
partial_paid: { label: 'Partially Paid', className: 'bg-amber-50 text-amber-700' },
|
||||||
|
paid: { label: 'Paid', className: 'bg-green-50 text-green-700' },
|
||||||
|
cancelled: { label: 'Cancelled', className: 'bg-red-50 text-red-700' },
|
||||||
|
};
|
||||||
|
function RegistrationStatusBadge({ status }: { status: string }) {
|
||||||
|
const s = REGISTRATION_STATUS_STYLES[status] || { label: status, className: 'bg-gray-100 text-gray-500' };
|
||||||
|
return <span className={`text-[10px] px-1.5 py-0.5 rounded ${s.className}`}>{s.label}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
// Effective unit price for an event option (or one of its variants), early-bird aware.
|
// Effective unit price for an event option (or one of its variants), early-bird aware.
|
||||||
// Used by the registration editor, which works off raw /api/events/:id data rather than
|
// Used by the registration editor, which works off raw /api/events/:id data rather than
|
||||||
// a registration's priceSnapshot — mirrors the pricing logic in register/[eventId]/RegisterForm.tsx.
|
// a registration's priceSnapshot — mirrors the pricing logic in register/[eventId]/RegisterForm.tsx.
|
||||||
@@ -53,8 +68,8 @@ export default function UserDashboardPage() {
|
|||||||
const { token, user } = useAuth();
|
const { token, user } = useAuth();
|
||||||
const [registrations, setRegistrations] = useState<any[]>([]);
|
const [registrations, setRegistrations] = useState<any[]>([]);
|
||||||
const [tickets, setTickets] = useState<any[]>([]);
|
const [tickets, setTickets] = useState<any[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
@@ -682,7 +697,7 @@ export default function UserDashboardPage() {
|
|||||||
<EventStatusBadge event={r.event} />
|
<EventStatusBadge event={r.event} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600">
|
<div className="text-xs text-gray-600">
|
||||||
Status: {r.status}{isCancelled && <span className="ml-2 inline-block text-[10px] px-1.5 py-0.5 rounded bg-gray-200 text-gray-700">cancelled</span>}
|
Status: <RegistrationStatusBadge status={r.status} />
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const fs = formStatuses[r.id];
|
const fs = formStatuses[r.id];
|
||||||
@@ -869,7 +884,7 @@ export default function UserDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-600">Status: {activeReg.status}</div>
|
<div className="text-sm text-gray-600">Status: <RegistrationStatusBadge status={activeReg.status} /></div>
|
||||||
{activeBill && (
|
{activeBill && (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div>Total: {formatRand(activeBill.totalDue)}</div>
|
<div>Total: {formatRand(activeBill.totalDue)}</div>
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import React, { Suspense, useEffect, useMemo, useState } from "react";
|
|||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
function MakePaymentContent() {
|
function MakePaymentContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const registrationId = searchParams.get("registrationId");
|
const registrationId = searchParams.get("registrationId");
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||||
const [registration, setRegistration] = useState<any | null>(null);
|
const [registration, setRegistration] = useState<any | null>(null);
|
||||||
const [payments, setPayments] = useState<any[]>([]);
|
const [payments, setPayments] = useState<any[]>([]);
|
||||||
const [amount, setAmount] = useState<string>("");
|
const [amount, setAmount] = useState<string>("");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useState } from "react";
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { formatDateTime } from "@/lib/date";
|
import { formatDateTime } from "@/lib/date";
|
||||||
import { formatPaymentMethod } from "@/lib/paymentMethod";
|
import { formatPaymentMethod } from "@/lib/paymentMethod";
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export default function UserPaymentsPage() {
|
|||||||
|
|
||||||
const [payments, setPayments] = useState<PaymentItem[]>([]);
|
const [payments, setPayments] = useState<PaymentItem[]>([]);
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
import { isValidZAPhone } from "@/lib/phone";
|
import { isValidZAPhone } from "@/lib/phone";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
|
|
||||||
export default function UserProfilePage() {
|
export default function UserProfilePage() {
|
||||||
const { user, token, logout, updateToken } = useAuth();
|
const { user, token, logout, updateToken } = useAuth();
|
||||||
@@ -19,7 +20,7 @@ export default function UserProfilePage() {
|
|||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [phone, setPhone] = useState("");
|
const [phone, setPhone] = useState("");
|
||||||
const [notifPref, setNotifPref] = useState<"email" | "whatsapp" | "both">("email");
|
const [notifPref, setNotifPref] = useState<"email" | "whatsapp" | "both">("email");
|
||||||
const [profileMsg, setProfileMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null);
|
const [profileMsg, setProfileMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null);
|
||||||
const [savingProfile, setSavingProfile] = useState(false);
|
const [savingProfile, setSavingProfile] = useState(false);
|
||||||
|
|
||||||
const hasValidPhone = isValidZAPhone(phone);
|
const hasValidPhone = isValidZAPhone(phone);
|
||||||
@@ -63,7 +64,7 @@ export default function UserProfilePage() {
|
|||||||
const [currentPassword, setCurrentPassword] = useState("");
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
const [pwMsg, setPwMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null);
|
const [pwMsg, setPwMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null);
|
||||||
const [savingPw, setSavingPw] = useState(false);
|
const [savingPw, setSavingPw] = useState(false);
|
||||||
|
|
||||||
const changePassword = async (e: React.FormEvent) => {
|
const changePassword = async (e: React.FormEvent) => {
|
||||||
@@ -98,7 +99,7 @@ export default function UserProfilePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Revoke sessions ───────────────────────────────────────────────────────
|
// ── Revoke sessions ───────────────────────────────────────────────────────
|
||||||
const [revokeMsg, setRevokeMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null);
|
const [revokeMsg, setRevokeMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null);
|
||||||
const [revoking, setRevoking] = useState(false);
|
const [revoking, setRevoking] = useState(false);
|
||||||
|
|
||||||
const revokeSessions = async () => {
|
const revokeSessions = async () => {
|
||||||
@@ -126,7 +127,7 @@ export default function UserProfilePage() {
|
|||||||
const [closeStep, setCloseStep] = useState<"idle" | "confirm">("idle");
|
const [closeStep, setCloseStep] = useState<"idle" | "confirm">("idle");
|
||||||
const [deleteData, setDeleteData] = useState(false);
|
const [deleteData, setDeleteData] = useState(false);
|
||||||
const [closePassword, setClosePassword] = useState("");
|
const [closePassword, setClosePassword] = useState("");
|
||||||
const [closeMsg, setCloseMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null);
|
const [closeMsg, setCloseMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null);
|
||||||
const [closing, setClosing] = useState(false);
|
const [closing, setClosing] = useState(false);
|
||||||
|
|
||||||
const submitAccountClosure = async (e: React.FormEvent) => {
|
const submitAccountClosure = async (e: React.FormEvent) => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function ResetPasswordPage() {
|
export default function ResetPasswordPage() {
|
||||||
@@ -11,7 +12,7 @@ export default function ResetPasswordPage() {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [confirm, setConfirm] = useState("");
|
const [confirm, setConfirm] = useState("");
|
||||||
const [status, setStatus] = useState<string | null>(null);
|
const [status, setStatus] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useDismissingState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const canSubmit = useMemo(() => current.length > 0 && password.length >= 8 && password === confirm, [current, password, confirm]);
|
const canSubmit = useMemo(() => current.length > 0 && password.length >= 8 && password === confirm, [current, password, confirm]);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like useState, but a truthy value auto-clears back to the initial falsy value after
|
||||||
|
* `ms` milliseconds. Every call to the setter cancels any pending timer and (if the new
|
||||||
|
* value is truthy) arms a fresh one, so rapid-fire updates reset the countdown instead of
|
||||||
|
* cutting it short. Meant for post-action confirmation/error banners that shouldn't linger.
|
||||||
|
*/
|
||||||
|
export function useDismissingState<T>(initial: T, ms = 7000) {
|
||||||
|
const [value, setValue] = useState<T>(initial);
|
||||||
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const set = useCallback((next: T) => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
setValue(next);
|
||||||
|
if (next) {
|
||||||
|
timeoutRef.current = setTimeout(() => setValue(initial), ms);
|
||||||
|
}
|
||||||
|
}, [ms, initial]);
|
||||||
|
|
||||||
|
useEffect(() => () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }, []);
|
||||||
|
|
||||||
|
return [value, set] as const;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user