Compare commits

..
Author SHA1 Message Date
joshua 4bcb07f9d1 Merge branch 'fix/help-modal-mobile-overflow' into main 2026-08-07 01:17:08 +02:00
joshuaandClaude Sonnet 5 a0a3dc2416 Bump version to 1.5.3
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 01:16:53 +02:00
joshuaandClaude Sonnet 5 51639322e2 Fix help guide popups overflowing the screen on mobile
The popup card had no overall height cap, only the tab content area
did, so the header/quick-links/footer could push the close button off
screen on short mobile viewports. The card is now capped to the
screen height with the header and footer pinned in place, and only
the tab content scrolls internally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 01:14:44 +02:00
joshuaandClaude Sonnet 5 1f186d6d2a Bump version to 1.5.2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 00:47:54 +02:00
joshua 55e7ca9546 Merge branch 'fix/dashboard-mobile-styling' into main 2026-08-07 00:47:14 +02:00
joshua 8f69e58aa2 Add tab icons and fix mobile dashboard styling issues
- Add matching lucide-react icons to tab/mode switcher buttons on
  payments, at-the-door, manual, email-attendees, whatsapp-attendees,
  and admin cashup pages, mirroring icons already used in their help menus
- Fix navbar Logout button sitting lower than other nav links (missing
  border/padding classes that other links use for their active-underline)
- Fix stat card labels getting truncated on mobile by removing the
  ellipsis-cut label and widening the mobile grid to one column
- Fix Revenue trend / Top performing events rendering outside the
  viewport on mobile by containing horizontal overflow and truncating
  long event titles in the table
2026-08-07 00:41:52 +02:00
joshuaandClaude Sonnet 5 1b2f2a7c79 Stop tracking banner.json runtime data store
Same reasoning as scheduled-emails.json: it's mutated by the running
app (site banner content), not source. readBanner() already falls
back to sane defaults when the file is missing, so untracking it is
safe for fresh checkouts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 00:04:45 +02:00
joshuaandClaude Sonnet 5 7906248c67 Stop tracking scheduled-emails.json runtime data store
It's mutated by the running app (scheduled job queue), not source -
committing it churns the repo with live recipient/message content.
Kept on disk, just untracked; a fresh checkout will get an empty
store created on demand by ensureStore().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 00:03:04 +02:00
joshua 22ffbf201e Merge branch 'fix/whatsapp-scheduling-channel-bug' into main (1.5.1) 2026-08-07 00:01:01 +02:00
18 changed files with 119 additions and 72 deletions
+5 -1
View File
@@ -33,4 +33,8 @@ Thumbs.db
# misc scratch / generated files
temp/
backend/public/uploads/
backend/public/uploads/
# runtime data stores (mutated by the running app, not source)
backend/data/scheduled-emails.json
backend/data/banner.json
+18
View File
@@ -7,6 +7,24 @@ and this project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [1.5.3] - 2026-08-07
### Fixed
- Help guide popups could render taller than the screen on mobile, with no way to reach the close button or "Got it" button: only the tab content area had a height cap, so the header, quick links, and footer weren't accounted for. The whole popup is now capped to the screen height, with just the tab content scrolling internally.
## [1.5.2] - 2026-08-07
### Fixed
- Stat card labels on the Admin and Supervisor dashboards (e.g. "Registrations (past month)") were getting cut off mid-word on mobile, where a cramped 2-column grid left too little room for the text. The mobile layout now shows one card per row, and labels wrap onto two lines instead of truncating.
- The "Revenue trend" and "Top performing events" cards on the Admin and Supervisor dashboards could render outside the viewport on mobile: a wide events table (long titles, four columns) forced the containing grid column past the screen width instead of scrolling internally. Long event titles are now truncated in the table, and the page properly contains horizontal overflow.
- The navbar Logout button sat about 2px lower than the other nav links (Home/Events/Contact/Dashboard) because it was missing the same underline-spacing classes those links use.
### Added
- Tab/mode switcher buttons on the Payments, At the door, Manual registration, Email attendees, WhatsApp attendees, and Cashup detail pages now show icons, matching the icons already used for the same tabs in each page's help guide.
## [1.5.1] - 2026-08-06
### Fixed
-6
View File
@@ -1,6 +0,0 @@
{
"message": "",
"type": "info",
"liveFrom": null,
"liveTill": null
}
-3
View File
@@ -1,3 +0,0 @@
{
"jobs": []
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "event-management-backend",
"version": "1.5.1",
"version": "1.5.3",
"description": "Event Management System Backend",
"main": "src/index.js",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hope-events-frontend",
"version": "1.5.1",
"version": "1.5.3",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
@@ -6,7 +6,7 @@ import { useAuth } from "@/hooks/useAuth";
import { apiFetch } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import type { EventCost, EventCostType, EventFinancials, CashupMethod } from "@/types";
import { Wallet } from "lucide-react";
import { Wallet, DollarSign, ClipboardCheck, FileBarChart } from "lucide-react";
const METHOD_LABELS: Record<CashupMethod, string> = { cash: "Cash", card: "Card", eft: "EFT", other: "Other" };
const METHODS: CashupMethod[] = ["cash", "card", "eft", "other"];
@@ -76,9 +76,9 @@ export default function EventCashupPage() {
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded p-2">{error}</div>}
<div className="flex gap-2 border-b">
<button className={"px-3 py-2 text-sm " + (tab === "costs" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("costs")}>Costs</button>
<button className={"px-3 py-2 text-sm " + (tab === "reconciliation" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("reconciliation")}>Reconciliation</button>
<button className={"px-3 py-2 text-sm " + (tab === "report" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("report")}>Report</button>
<button className={"inline-flex items-center gap-1.5 px-3 py-2 text-sm " + (tab === "costs" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("costs")}><DollarSign className="w-4 h-4" />Costs</button>
<button className={"inline-flex items-center gap-1.5 px-3 py-2 text-sm " + (tab === "reconciliation" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("reconciliation")}><ClipboardCheck className="w-4 h-4" />Reconciliation</button>
<button className={"inline-flex items-center gap-1.5 px-3 py-2 text-sm " + (tab === "report" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("report")}><FileBarChart className="w-4 h-4" />Report</button>
</div>
{loading && <div className="text-sm text-gray-400">Loading</div>}
+4 -4
View File
@@ -102,7 +102,7 @@ export default function AdminDashboardPage() {
: 0;
return (
<div className="max-w-6xl mx-auto w-full">
<div className="max-w-6xl mx-auto w-full overflow-x-hidden">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-gray-900">Welcome back{user ? `, ${user.name}` : ""} 👋</h1>
<p className="text-sm text-gray-500 mt-1">Here&apos;s what&apos;s happening with your events today.</p>
@@ -132,7 +132,7 @@ export default function AdminDashboardPage() {
</div>
<div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<div className="lg:col-span-2 space-y-6 min-w-0">
<div className="border rounded-xl p-4 bg-white shadow-sm">
<h2 className="text-lg font-semibold mb-3">Revenue trend past month</h2>
{overview && overview.trend.length > 0 ? (
@@ -157,7 +157,7 @@ export default function AdminDashboardPage() {
<TableBody>
{overview.topEvents.map(e => (
<TableRow key={e.eventId}>
<TableCell className="font-medium">{e.title}</TableCell>
<TableCell className="font-medium max-w-[140px] sm:max-w-[220px] truncate" title={e.title}>{e.title}</TableCell>
<TableCell className="text-right">{formatCount(e.registrations)}</TableCell>
<TableCell className="text-right">{formatRand(e.revenue)}</TableCell>
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
@@ -171,7 +171,7 @@ export default function AdminDashboardPage() {
</div>
</div>
<div className="space-y-6">
<div className="space-y-6 min-w-0">
<div className="border rounded-xl p-4 bg-white shadow-sm">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold">Payments overview</h2>
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
import { apiFetch } from "@/lib/api";
import { scoreUser } from "@/lib/fuzzyMatch";
import { useDismissingState } from "@/hooks/useDismissingState";
import { DoorOpen } from "lucide-react";
import { DoorOpen, UserPlus, CreditCard, CheckSquare, Ticket, RotateCcw, type LucideIcon } from "lucide-react";
type Mode = "registration" | "payment" | "checkin" | "tickets" | "refund";
@@ -18,6 +18,14 @@ const MODE_LABELS: Record<Mode, string> = {
refund: "REFUND",
};
const MODE_ICONS: Record<Mode, LucideIcon> = {
registration: UserPlus,
payment: CreditCard,
checkin: CheckSquare,
tickets: Ticket,
refund: RotateCcw,
};
// ─── Fuzzy search helpers ─────────────────────────────────────────────────────
function fuzzyFilterRegs(allRegs: any[], search: string): any[] {
@@ -390,21 +398,25 @@ export default function AtTheDoorPage() {
{/* Mode Buttons */}
<div className="flex gap-2 mb-4 flex-wrap">
{(["registration", "payment", "checkin", "tickets", "refund"] as Mode[]).map(m => (
<button
key={m}
onClick={() => setMode(m)}
className={`px-4 py-2 rounded text-sm font-medium border ${
mode === m
? m === "refund"
? "bg-red-600 text-white border-red-600"
: "bg-brand-600 text-white border-brand-600"
: "bg-white hover:bg-gray-50"
}`}
>
{MODE_LABELS[m]}
</button>
))}
{(["registration", "payment", "checkin", "tickets", "refund"] as Mode[]).map(m => {
const Icon = MODE_ICONS[m];
return (
<button
key={m}
onClick={() => setMode(m)}
className={`inline-flex items-center gap-1.5 px-4 py-2 rounded text-sm font-medium border ${
mode === m
? m === "refund"
? "bg-red-600 text-white border-red-600"
: "bg-brand-600 text-white border-brand-600"
: "bg-white hover:bg-gray-50"
}`}
>
<Icon className="w-4 h-4" />
{MODE_LABELS[m]}
</button>
);
})}
</div>
{/* ✅ Panels */}
@@ -5,7 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
import { useRouter, useSearchParams } from "next/navigation";
import { apiFetch, fetchAllUsers } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import { Mail } from "lucide-react";
import { Mail, Users, Zap, Clock } from "lucide-react";
type Attendee = { id: string; name: string; email: string; pref: string };
@@ -503,20 +503,24 @@ function EmailAttendeesPageInner() {
{/* Tabs like on Payments page */}
<div className="mb-4 flex items-center gap-2 flex-wrap">
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'attendees' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'attendees' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="emailTab" value="attendees" className="hidden" checked={tab==='attendees'} onChange={() => setTab('attendees')} />
<Users className="w-4 h-4" />
Attendees
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'automations' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'automations' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="emailTab" value="automations" className="hidden" checked={tab==='automations'} onChange={() => setTab('automations')} />
<Zap className="w-4 h-4" />
Automations
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'broadcasts' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'broadcasts' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="emailTab" value="broadcasts" className="hidden" checked={tab==='broadcasts'} onChange={() => setTab('broadcasts')} />
<Mail className="w-4 h-4" />
Broadcasts
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'scheduled' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'scheduled' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="emailTab" value="scheduled" className="hidden" checked={tab==='scheduled'} onChange={() => setTab('scheduled')} />
<Clock className="w-4 h-4" />
Scheduled
</label>
</div>
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
import { apiFetch, fetchAllUsers } from "@/lib/api";
import { scoreUser } from "@/lib/fuzzyMatch";
import { useDismissingState } from "@/hooks/useDismissingState";
import { UserPlus } from "lucide-react";
import { UserPlus, CreditCard } from "lucide-react";
// ─── Pricing helpers ─────────────────────────────────────────────────────────
@@ -355,12 +355,14 @@ export default function ManualRegistrationPage() {
)}
<div className="mb-4 flex items-center gap-2">
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'register' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'register' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="tab" value="register" className="hidden" checked={tab==='register'} onChange={() => setTab('register')} />
<UserPlus className="w-4 h-4" />
Register
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="tab" value="payment" className="hidden" checked={tab==='payment'} onChange={() => setTab('payment')} />
<CreditCard className="w-4 h-4" />
Record Payment
</label>
</div>
@@ -101,7 +101,7 @@ export default function SupervisorDashboardPage() {
}, 15000, !!token);
return (
<div className="max-w-6xl mx-auto w-full">
<div className="max-w-6xl mx-auto w-full overflow-x-hidden">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-gray-900">Welcome back{user ? `, ${user.name}` : ""} 👋</h1>
<p className="text-sm text-gray-500 mt-1">Here&apos;s what&apos;s happening with your events today.</p>
@@ -131,7 +131,7 @@ export default function SupervisorDashboardPage() {
</div>
<div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<div className="lg:col-span-2 space-y-6 min-w-0">
<div className="border rounded-xl p-4 bg-white shadow-sm">
<h2 className="text-lg font-semibold mb-3">Revenue trend past month</h2>
{overview && overview.trend.length > 0 ? (
@@ -156,7 +156,7 @@ export default function SupervisorDashboardPage() {
<TableBody>
{overview.topEvents.map(e => (
<TableRow key={e.eventId}>
<TableCell className="font-medium">{e.title}</TableCell>
<TableCell className="font-medium max-w-[140px] sm:max-w-[220px] truncate" title={e.title}>{e.title}</TableCell>
<TableCell className="text-right">{formatCount(e.registrations)}</TableCell>
<TableCell className="text-right">{formatRand(e.revenue)}</TableCell>
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
@@ -190,7 +190,7 @@ export default function SupervisorDashboardPage() {
</div>
</div>
<div className="space-y-6">
<div className="space-y-6 min-w-0">
<div className="border rounded-xl p-4 bg-white shadow-sm">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold">Scanner activity</h2>
@@ -6,7 +6,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { apiFetch, fetchAllUsers, fetchAllPayments } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import { scoreUser } from "@/lib/fuzzyMatch";
import { Wallet } from "lucide-react";
import { Wallet, RotateCcw, HandHeart, ScanLine, Link2 } from "lucide-react";
// A donation is never mutated once created — assigning it to a registration creates a separate
// "leg" Payment row (isDonation:false, originalPaymentId -> the donation, amount > 0) instead.
@@ -566,24 +566,29 @@ function PaymentsContent() {
{info && <div className="p-3 mb-3 border rounded bg-emerald-50 text-emerald-800 text-sm">{info}</div>}
<div className="mb-4 flex flex-wrap items-center gap-2">
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="mode" value="payment" className="hidden" checked={mode==='payment'} onChange={() => setMode('payment')} />
<Wallet className="w-4 h-4" />
Payment
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'refund' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'refund' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="mode" value="refund" className="hidden" checked={mode==='refund'} onChange={() => setMode('refund')} />
<RotateCcw className="w-4 h-4" />
Refund
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'donation' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'donation' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="mode" value="donation" className="hidden" checked={mode==='donation'} onChange={() => setMode('donation')} />
<HandHeart className="w-4 h-4" />
Donations
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'reconcile' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'reconcile' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="mode" value="reconcile" className="hidden" checked={mode==='reconcile'} onChange={() => setMode('reconcile')} />
<ScanLine className="w-4 h-4" />
Reconcile
</label>
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'link' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'link' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
<input type="radio" name="mode" value="link" className="hidden" checked={mode==='link'} onChange={() => setMode('link')} />
<Link2 className="w-4 h-4" />
Payment Link
</label>
</div>
@@ -5,12 +5,19 @@ import { useAuth } from "@/hooks/useAuth";
import { useRouter, useSearchParams } from "next/navigation";
import { apiFetch, fetchAllUsers } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import { MessageCircle } from "lucide-react";
import { MessageCircle, Users, Zap, Send, Clock, type LucideIcon } from "lucide-react";
// Attendee with preference info
type Attendee = { id: string; name: string; phone: string; pref: string };
type UserEntry = { id: string; name: string; phone: string; pref: string };
const WA_TAB_ICONS: Record<"attendees" | "automations" | "broadcasts" | "scheduled", LucideIcon> = {
attendees: Users,
automations: Zap,
broadcasts: Send,
scheduled: Clock,
};
function toLocalInputValue(d: Date) {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
@@ -612,12 +619,16 @@ function WhatsAppAttendeesPageInner() {
{/* Tabs */}
<div className="mb-4 flex items-center gap-2 flex-wrap">
{(["attendees", "automations", "broadcasts", "scheduled"] as const).map((t) => (
<label key={t} className={`px-3 py-1.5 text-sm rounded border cursor-pointer ${tab === t ? "bg-green-600 text-white border-green-600" : "bg-white text-gray-800 border-gray-200"}`}>
<input type="radio" name="waTab" value={t} className="hidden" checked={tab === t} onChange={() => setTab(t)} />
{t.charAt(0).toUpperCase() + t.slice(1)}
</label>
))}
{(["attendees", "automations", "broadcasts", "scheduled"] as const).map((t) => {
const Icon = WA_TAB_ICONS[t];
return (
<label key={t} className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border cursor-pointer ${tab === t ? "bg-green-600 text-white border-green-600" : "bg-white text-gray-800 border-gray-200"}`}>
<input type="radio" name="waTab" value={t} className="hidden" checked={tab === t} onChange={() => setTab(t)} />
<Icon className="w-4 h-4" />
{t.charAt(0).toUpperCase() + t.slice(1)}
</label>
);
})}
</div>
{/* ── Attendees Tab ── */}
+1 -1
View File
@@ -108,7 +108,7 @@ export const Navbar = () => {
<button
key={link.label}
onClick={link.onClick}
className="text-sm text-gray-700 hover:text-brand-600"
className="text-sm pb-0.5 border-b-2 border-transparent text-gray-700 hover:text-brand-600 transition-colors"
>
{link.label}
</button>
@@ -17,10 +17,10 @@ export default function HelpGuideModal({ content, onClose }: { content: HelpCont
<div className="absolute inset-0 bg-black/40 animate-in fade-in duration-200" onClick={onClose} />
<div className="absolute inset-0 flex items-center justify-center p-4">
<div
className="w-full max-w-3xl bg-white rounded-2xl shadow-2xl animate-in fade-in zoom-in-95 slide-in-from-bottom-2 duration-200 overflow-hidden"
className="w-full max-w-3xl max-h-full bg-white rounded-2xl shadow-2xl animate-in fade-in zoom-in-95 slide-in-from-bottom-2 duration-200 overflow-hidden flex flex-col"
onClick={e => e.stopPropagation()}
>
<div className="flex items-start justify-between px-5 py-4 border-b bg-gradient-to-r from-brand-50/60 to-white">
<div className="flex items-start justify-between px-5 py-4 border-b bg-gradient-to-r from-brand-50/60 to-white shrink-0">
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-full bg-brand-50 flex items-center justify-center shrink-0">
<MessageCircleQuestion className="w-5 h-5 text-brand-600" />
@@ -36,7 +36,7 @@ export default function HelpGuideModal({ content, onClose }: { content: HelpCont
</div>
{content.quickLinks && content.quickLinks.length > 0 && (
<div className="flex flex-wrap gap-2 px-5 py-3 border-b bg-gray-50">
<div className="flex flex-wrap gap-2 px-5 py-3 border-b bg-gray-50 shrink-0">
{content.quickLinks.map(link => (
<Link
key={link.href}
@@ -51,9 +51,9 @@ export default function HelpGuideModal({ content, onClose }: { content: HelpCont
</div>
)}
<div className="flex flex-col sm:flex-row">
<div className="flex flex-col sm:flex-row flex-1 min-h-0 overflow-y-auto sm:overflow-visible">
{content.tabs.length > 1 && (
<nav className="sm:w-56 shrink-0 border-b sm:border-b-0 sm:border-r p-3 space-y-1 bg-gray-50/50">
<nav className="sm:w-56 shrink-0 border-b sm:border-b-0 sm:border-r p-3 space-y-1 bg-gray-50/50 sm:overflow-y-auto">
{content.tabs.map(t => {
const Icon = t.icon;
const active = activeTab?.key === t.key;
@@ -72,12 +72,12 @@ export default function HelpGuideModal({ content, onClose }: { content: HelpCont
</nav>
)}
<div className="flex-1 min-w-0 p-5 text-sm text-gray-700 max-h-[60vh] overflow-auto">
<div className="flex-1 min-w-0 p-5 text-sm text-gray-700 sm:overflow-y-auto">
{activeTab?.content}
</div>
</div>
<div className="border-t px-5 py-3 space-y-2 bg-gray-50/50">
<div className="border-t px-5 py-3 space-y-2 bg-gray-50/50 shrink-0">
{content.supportContact && (
<div className="flex items-center gap-2 text-xs text-gray-500">
<Mail className="w-3.5 h-3.5 shrink-0" />
+2 -2
View File
@@ -39,7 +39,7 @@ export function StatCard({
<Icon className={"w-5 h-5 " + t.icon} />
</div>
<div className="min-w-0">
<div className="text-xs text-gray-500 truncate">{label}</div>
<div className="text-xs text-gray-500 leading-snug">{label}</div>
<div className="text-xl font-semibold text-gray-900 truncate">{value}</div>
</div>
</div>
@@ -67,5 +67,5 @@ export function StatCard({
}
export function StatCardRow({ children }: { children: React.ReactNode }) {
return <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">{children}</div>;
return <div className="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5 gap-4">{children}</div>;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hope-events",
"version": "1.5.1",
"version": "1.5.3",
"main": "index.js",
"scripts": {
"dev:backend": "cd backend && npm run dev",