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:
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
import type { EventCost, EventCostType, EventFinancials, CashupMethod } from "@/types";
|
||||
|
||||
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 [eventOptions, setEventOptions] = useState<{ id: string; name: string }[]>([]);
|
||||
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 isClosed = data?.event?.cashupStatus === "closed";
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
|
||||
export default function CashupLandingPage() {
|
||||
const { token } = useAuth();
|
||||
@@ -11,7 +12,7 @@ export default function CashupLandingPage() {
|
||||
|
||||
const [events, setEvents] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [error, setError] = useDismissingState<string | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [showPast, setShowPast] = useState(true);
|
||||
const [showInactive, setShowInactive] = useState(false);
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
|
||||
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 [events, setEvents] = useState<any[]>([]);
|
||||
const [loadingRegs, setLoadingRegs] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<string | null>(null);
|
||||
const [error, setError] = useDismissingState<string | null>(null);
|
||||
const [info, setInfo] = useDismissingState<string | null>(null);
|
||||
|
||||
// Filters
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { apiFetch, API_BASE, resolveToApiOrigin } from "@/lib/api";
|
||||
import { useSiteSettings } from "@/contexts/SiteSettingsContext";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
|
||||
type TabId = "organisation" | "branding" | "notifications" | "email" | "legal";
|
||||
|
||||
@@ -36,19 +37,17 @@ function Field({
|
||||
}
|
||||
|
||||
function SaveBar({
|
||||
saving, onSave, result, onDismiss,
|
||||
saving, onSave, result,
|
||||
}: {
|
||||
saving: boolean;
|
||||
onSave: () => void;
|
||||
result: { ok: boolean; message: string } | null;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between pt-4 border-t mt-6 flex-wrap gap-3">
|
||||
{result ? (
|
||||
<span className={`text-sm flex items-center gap-1.5 ${result.ok ? "text-green-600" : "text-red-600"}`}>
|
||||
{result.ok ? "✓" : "✗"} {result.message}
|
||||
<button type="button" onClick={onDismiss} className="ml-1 text-gray-400 hover:text-gray-600 text-xs">×</button>
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
@@ -75,7 +74,7 @@ export default function SiteSettingsPage() {
|
||||
|
||||
// Per-tab save state
|
||||
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 ──
|
||||
const [orgName, setOrgName] = useState("");
|
||||
@@ -104,7 +103,7 @@ export default function SiteSettingsPage() {
|
||||
const [smtpPass, setSmtpPass] = useState("");
|
||||
const [smtpPassSet, setSmtpPassSet] = 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 ──
|
||||
const [legalOperatorName, setLegalOperatorName] = useState("");
|
||||
@@ -332,7 +331,7 @@ export default function SiteSettingsPage() {
|
||||
<input className={inputCls} placeholder="https://events.yourchurch.org"
|
||||
value={appBaseUrl} onChange={e => setAppBaseUrl(e.target.value)} />
|
||||
</Field>
|
||||
<SaveBar saving={saving} onSave={saveOrganisation} result={result} onDismiss={() => setResult(null)} />
|
||||
<SaveBar saving={saving} onSave={saveOrganisation} result={result} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -371,7 +370,7 @@ export default function SiteSettingsPage() {
|
||||
}} className="text-sm" />
|
||||
</Field>
|
||||
|
||||
<SaveBar saving={saving} onSave={saveBranding} result={result} onDismiss={() => setResult(null)} />
|
||||
<SaveBar saving={saving} onSave={saveBranding} result={result} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -385,7 +384,7 @@ export default function SiteSettingsPage() {
|
||||
<input className={inputCls} placeholder="registrations@yourchurch.org, admin@yourchurch.org"
|
||||
value={notifEmails} onChange={e => setNotifEmails(e.target.value)} />
|
||||
</Field>
|
||||
<SaveBar saving={saving} onSave={saveNotifications} result={result} onDismiss={() => setResult(null)} />
|
||||
<SaveBar saving={saving} onSave={saveNotifications} result={result} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -464,7 +463,7 @@ export default function SiteSettingsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SaveBar saving={saving} onSave={saveSmtp} result={result} onDismiss={() => setResult(null)} />
|
||||
<SaveBar saving={saving} onSave={saveSmtp} result={result} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -503,7 +502,7 @@ export default function SiteSettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SaveBar saving={saving} onSave={saveLegal} result={result} onDismiss={() => setResult(null)} />
|
||||
<SaveBar saving={saving} onSave={saveLegal} result={result} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
|
||||
interface UserItem {
|
||||
id: string;
|
||||
@@ -43,7 +44,7 @@ export default function AdminUsersPage() {
|
||||
// Data state
|
||||
const [users, setUsers] = useState<UserItem[]>([]);
|
||||
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 [totalPages, setTotalPages] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -123,7 +124,7 @@ export default function WhatsAppAdminPage() {
|
||||
const [pairingPhone, setPairingPhone] = useState("");
|
||||
|
||||
// ── 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);
|
||||
|
||||
// ── Load config ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user