Mobile-friendly admin pages, redesigned homepage Next Event card
- Extract shared mobile components (BottomSheet, MoreMenu, Dropdown, etc.) into MobileComponents.tsx - Make admin pages mobile-friendly: bookings, emails, events, faq, payments, tickets, users - Redesign homepage Next Event card with banner image, responsive layout, and updated styling - Fix past events showing on homepage/linktree: use proper Date comparison, auto-unfeature expired events - Add "Over" tag to admin events list for past events - Fix backend FRONTEND_URL for cache revalidation Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { emailsApi, EmailTemplate, EmailLog, EmailStats } from '@/lib/api';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import { MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents';
|
||||
import {
|
||||
EnvelopeIcon,
|
||||
PencilIcon,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
ExclamationTriangleIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import toast from 'react-hot-toast';
|
||||
import clsx from 'clsx';
|
||||
@@ -382,7 +384,7 @@ export default function AdminEmailsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold text-primary-dark">Email Center</h1>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-primary-dark">Email Center</h1>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -436,18 +438,15 @@ export default function AdminEmailsPage() {
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-secondary-light-gray mb-6">
|
||||
<nav className="flex gap-6">
|
||||
<div className="border-b border-secondary-light-gray mb-6 overflow-x-auto scrollbar-hide">
|
||||
<nav className="flex gap-4 md:gap-6 min-w-max">
|
||||
{(['templates', 'compose', 'logs'] as TabType[]).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={clsx(
|
||||
'py-3 px-1 border-b-2 font-medium text-sm transition-colors relative',
|
||||
{
|
||||
'border-primary-yellow text-primary-dark': activeTab === tab,
|
||||
'border-transparent text-gray-500 hover:text-gray-700': activeTab !== tab,
|
||||
}
|
||||
'py-3 px-1 border-b-2 font-medium text-sm transition-colors relative whitespace-nowrap min-h-[44px]',
|
||||
activeTab === tab ? 'border-primary-yellow text-primary-dark' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
{tab === 'templates' ? 'Templates' : tab === 'compose' ? 'Compose' : 'Email Logs'}
|
||||
@@ -499,30 +498,35 @@ export default function AdminEmailsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handlePreviewTemplate(template)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn"
|
||||
title="Preview"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => handlePreviewTemplate(template)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center" title="Preview">
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEditTemplate(template)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn"
|
||||
title="Edit"
|
||||
>
|
||||
<button onClick={() => handleEditTemplate(template)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center hidden md:flex" title="Edit">
|
||||
<PencilIcon className="w-5 h-5" />
|
||||
</button>
|
||||
{!template.isSystem && (
|
||||
<button
|
||||
onClick={() => handleDeleteTemplate(template.id)}
|
||||
className="p-2 hover:bg-red-100 text-red-600 rounded-btn"
|
||||
title="Delete"
|
||||
>
|
||||
<XCircleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="hidden md:block">
|
||||
{!template.isSystem && (
|
||||
<button onClick={() => handleDeleteTemplate(template.id)}
|
||||
className="p-2 hover:bg-red-100 text-red-600 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center" title="Delete">
|
||||
<XCircleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<MoreMenu>
|
||||
<DropdownItem onClick={() => handleEditTemplate(template)}>
|
||||
<PencilIcon className="w-4 h-4 mr-2" /> Edit
|
||||
</DropdownItem>
|
||||
{!template.isSystem && (
|
||||
<DropdownItem onClick={() => handleDeleteTemplate(template.id)} className="text-red-600">
|
||||
<XCircleIcon className="w-4 h-4 mr-2" /> Delete
|
||||
</DropdownItem>
|
||||
)}
|
||||
</MoreMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -635,13 +639,17 @@ export default function AdminEmailsPage() {
|
||||
|
||||
{/* Recipient Preview Modal */}
|
||||
{showRecipientPreview && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<div className="p-4 border-b border-secondary-light-gray">
|
||||
<h2 className="text-lg font-bold">Recipient Preview</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{previewRecipients.length} recipient(s) will receive this email
|
||||
</p>
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||||
<Card className="w-full md:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col rounded-t-2xl md:rounded-card">
|
||||
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray">
|
||||
<div>
|
||||
<h2 className="text-base font-bold">Recipient Preview</h2>
|
||||
<p className="text-xs text-gray-500">{previewRecipients.length} recipient(s)</p>
|
||||
</div>
|
||||
<button onClick={() => setShowRecipientPreview(false)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
@@ -675,14 +683,10 @@ export default function AdminEmailsPage() {
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-secondary-light-gray flex gap-3">
|
||||
<Button
|
||||
onClick={handleSendEmail}
|
||||
isLoading={sending}
|
||||
disabled={previewRecipients.length === 0}
|
||||
>
|
||||
Send to {previewRecipients.length} Recipients
|
||||
<Button onClick={handleSendEmail} isLoading={sending} disabled={previewRecipients.length === 0} className="flex-1 min-h-[44px]">
|
||||
Send to {previewRecipients.length}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShowRecipientPreview(false)}>
|
||||
<Button variant="outline" onClick={() => setShowRecipientPreview(false)} className="flex-1 min-h-[44px]">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
@@ -695,51 +699,37 @@ export default function AdminEmailsPage() {
|
||||
{/* Logs Tab */}
|
||||
{activeTab === 'logs' && (
|
||||
<div>
|
||||
<Card className="overflow-hidden">
|
||||
{/* Desktop: Table */}
|
||||
<Card className="overflow-hidden hidden md:block">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-secondary-gray">
|
||||
<tr>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Status</th>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Recipient</th>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Subject</th>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Sent</th>
|
||||
<th className="text-right px-6 py-3 text-sm font-medium text-gray-600">Actions</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Recipient</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Subject</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Sent</th>
|
||||
<th className="text-right px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-secondary-light-gray">
|
||||
{logs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-12 text-center text-gray-500">
|
||||
No emails sent yet
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colSpan={5} className="px-4 py-12 text-center text-gray-500 text-sm">No emails sent yet</td></tr>
|
||||
) : (
|
||||
logs.map((log) => (
|
||||
<tr key={log.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(log.status)}
|
||||
<span className="capitalize text-sm">{log.status}</span>
|
||||
</div>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">{getStatusIcon(log.status)}<span className="capitalize text-sm">{log.status}</span></div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-medium text-sm">{log.recipientName || 'Unknown'}</p>
|
||||
<p className="text-sm text-gray-500">{log.recipientEmail}</p>
|
||||
<p className="text-xs text-gray-500">{log.recipientEmail}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 max-w-xs">
|
||||
<p className="text-sm truncate">{log.subject}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">
|
||||
{formatDate(log.sentAt || log.createdAt)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedLog(log)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn"
|
||||
title="View Email"
|
||||
>
|
||||
<td className="px-4 py-3 max-w-xs"><p className="text-sm truncate">{log.subject}</p></td>
|
||||
<td className="px-4 py-3 text-xs text-gray-500">{formatDate(log.sentAt || log.createdAt)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end">
|
||||
<button onClick={() => setSelectedLog(log)} className="p-2 hover:bg-gray-100 rounded-btn" title="View">
|
||||
<EyeIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -750,46 +740,69 @@ export default function AdminEmailsPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{logsTotal > 20 && (
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-secondary-light-gray">
|
||||
<p className="text-sm text-gray-600">
|
||||
Showing {logsOffset + 1}-{Math.min(logsOffset + 20, logsTotal)} of {logsTotal}
|
||||
</p>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-light-gray">
|
||||
<p className="text-sm text-gray-600">Showing {logsOffset + 1}-{Math.min(logsOffset + 20, logsTotal)} of {logsTotal}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={logsOffset === 0}
|
||||
onClick={() => setLogsOffset(Math.max(0, logsOffset - 20))}
|
||||
>
|
||||
<Button variant="outline" size="sm" disabled={logsOffset === 0} onClick={() => setLogsOffset(Math.max(0, logsOffset - 20))}>
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={logsOffset + 20 >= logsTotal}
|
||||
onClick={() => setLogsOffset(logsOffset + 20)}
|
||||
>
|
||||
<Button variant="outline" size="sm" disabled={logsOffset + 20 >= logsTotal} onClick={() => setLogsOffset(logsOffset + 20)}>
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Mobile: Card List */}
|
||||
<div className="md:hidden space-y-2">
|
||||
{logs.length === 0 ? (
|
||||
<div className="text-center py-10 text-gray-500 text-sm">No emails sent yet</div>
|
||||
) : (
|
||||
logs.map((log) => (
|
||||
<Card key={log.id} className="p-3" onClick={() => setSelectedLog(log)}>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<div className="mt-0.5 flex-shrink-0">{getStatusIcon(log.status)}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-sm truncate">{log.subject}</p>
|
||||
<p className="text-xs text-gray-500 truncate">{log.recipientName || 'Unknown'} <{log.recipientEmail}></p>
|
||||
<p className="text-[10px] text-gray-400 mt-1">{formatDate(log.sentAt || log.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
{logsTotal > 20 && (
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<p className="text-xs text-gray-500">{logsOffset + 1}-{Math.min(logsOffset + 20, logsTotal)} of {logsTotal}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={logsOffset === 0} onClick={() => setLogsOffset(Math.max(0, logsOffset - 20))} className="min-h-[44px]">
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={logsOffset + 20 >= logsTotal} onClick={() => setLogsOffset(logsOffset + 20)} className="min-h-[44px]">
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template Form Modal */}
|
||||
{showTemplateForm && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-4xl max-h-[90vh] overflow-y-auto p-6">
|
||||
<h2 className="text-xl font-bold mb-6">
|
||||
{editingTemplate ? 'Edit Template' : 'Create Template'}
|
||||
</h2>
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||||
<Card className="w-full md:max-w-4xl max-h-[90vh] flex flex-col overflow-hidden rounded-t-2xl md:rounded-card">
|
||||
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray flex-shrink-0">
|
||||
<h2 className="text-base font-bold">{editingTemplate ? 'Edit Template' : 'Create Template'}</h2>
|
||||
<button onClick={() => { setShowTemplateForm(false); resetTemplateForm(); }}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveTemplate} className="space-y-4">
|
||||
<form onSubmit={handleSaveTemplate} className="p-4 space-y-4 overflow-y-auto flex-1 min-h-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Template Name"
|
||||
@@ -873,14 +886,10 @@ export default function AdminEmailsPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button type="submit" isLoading={saving}>
|
||||
{editingTemplate ? 'Update Template' : 'Create Template'}
|
||||
<Button type="submit" isLoading={saving} className="flex-1 min-h-[44px]">
|
||||
{editingTemplate ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => { setShowTemplateForm(false); resetTemplateForm(); }}
|
||||
>
|
||||
<Button type="button" variant="outline" onClick={() => { setShowTemplateForm(false); resetTemplateForm(); }} className="flex-1 min-h-[44px]">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
@@ -891,16 +900,17 @@ export default function AdminEmailsPage() {
|
||||
|
||||
{/* Preview Modal */}
|
||||
{previewHtml && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||||
<Card className="w-full md:max-w-3xl max-h-[90vh] overflow-hidden flex flex-col rounded-t-2xl md:rounded-card">
|
||||
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">Email Preview</h2>
|
||||
<p className="text-sm text-gray-500">Subject: {previewSubject}</p>
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-base font-bold">Email Preview</h2>
|
||||
<p className="text-xs text-gray-500 truncate">Subject: {previewSubject}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => setPreviewHtml(null)}>
|
||||
Close
|
||||
</Button>
|
||||
<button onClick={() => setPreviewHtml(null)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center flex-shrink-0">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<iframe
|
||||
@@ -914,23 +924,26 @@ export default function AdminEmailsPage() {
|
||||
)}
|
||||
|
||||
{/* Log Detail Modal */}
|
||||
<AdminMobileStyles />
|
||||
|
||||
{selectedLog && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||||
<Card className="w-full md:max-w-3xl max-h-[90vh] overflow-hidden flex flex-col rounded-t-2xl md:rounded-card">
|
||||
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">Email Details</h2>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-base font-bold">Email Details</h2>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
{getStatusIcon(selectedLog.status)}
|
||||
<span className="capitalize text-sm">{selectedLog.status}</span>
|
||||
{selectedLog.errorMessage && (
|
||||
<span className="text-sm text-red-500">- {selectedLog.errorMessage}</span>
|
||||
<span className="text-xs text-red-500">- {selectedLog.errorMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => setSelectedLog(null)}>
|
||||
Close
|
||||
</Button>
|
||||
<button onClick={() => setSelectedLog(null)}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center flex-shrink-0">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-2 border-b border-secondary-light-gray bg-gray-50">
|
||||
<p><strong>To:</strong> {selectedLog.recipientName} <{selectedLog.recipientEmail}></p>
|
||||
|
||||
Reference in New Issue
Block a user