✅ Security separation: Move admin styles to secure admin.css (auth-only) ✅ CSS cleanup: Clean globals.css for public access only ✅ High contrast fixes: Resolve button visibility and CTA backgrounds ✅ Menu highlighting: Implement CSS variable-based approach with gold accents ✅ Accessibility: Maintain WCAG AAA compliance across all four themes ✅ Architecture: Semantic CSS classes replace inline dark: classes - Created frontend/src/styles/admin.css with secure admin-only styles - Cleaned frontend/src/app/globals.css removing 1200+ lines of admin code - Updated admin components to use semantic CSS classes - Fixed high contrast active menu highlighting with gold borders - Implemented CSS containment and injection prevention - All admin dashboard migration objectives achieved
266 lines
9.5 KiB
TypeScript
266 lines
9.5 KiB
TypeScript
import React from 'react';
|
|
import Link from 'next/link';
|
|
import type { Metadata } from 'next';
|
|
|
|
export const metadata: Metadata = {
|
|
title: 'Admin Dashboard | Olathe Club of the Deaf',
|
|
description: 'Admin dashboard for the Olathe Club of the Deaf website',
|
|
};
|
|
|
|
// Stats Card Component
|
|
const StatCard = ({ title, value, icon, href }: { title: string; value: string; icon: React.ReactNode; href: string }) => (
|
|
<Link href={href} className="admin-stat-card admin-gold-accent hover:shadow-md transition-shadow">
|
|
<div className="flex items-center">
|
|
<div className="admin-stat-icon">
|
|
{icon}
|
|
</div>
|
|
<div className="ml-5">
|
|
<p className="admin-stat-title">{title}</p>
|
|
<p className="admin-stat-value">{value}</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
|
|
// Simple icons
|
|
const MembersIcon = () => (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
);
|
|
|
|
const EventsIcon = () => (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
);
|
|
|
|
const ContactIcon = () => (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
);
|
|
|
|
const VideosIcon = () => (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
);
|
|
|
|
export default function AdminDashboardPage() {
|
|
// In a real app, these would be fetched from the API
|
|
const stats = {
|
|
members: "47",
|
|
activeMembers: "41",
|
|
events: "12",
|
|
videos: "8",
|
|
documents: "15",
|
|
unreadContacts: "3"
|
|
};
|
|
|
|
// Upcoming events - in a real app, these would be fetched from the API
|
|
const upcomingEvents = [
|
|
{
|
|
id: '1',
|
|
title: 'Monthly Social',
|
|
date: 'Apr 15, 2025',
|
|
location: 'Olathe Community Center',
|
|
status: 'Published'
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'ASL Workshop',
|
|
date: 'Apr 22, 2025',
|
|
location: 'OCD Meeting Room',
|
|
status: 'Draft'
|
|
},
|
|
{
|
|
id: '3',
|
|
title: 'Board Meeting',
|
|
date: 'May 5, 2025',
|
|
location: 'Zoom Meeting',
|
|
status: 'Published'
|
|
}
|
|
];
|
|
|
|
// Membership expirations - in a real app, these would be fetched from the API
|
|
const expiringMemberships = [
|
|
{
|
|
id: '101',
|
|
name: 'John Smith',
|
|
expiryDate: 'Apr 30, 2025',
|
|
type: 'Regular',
|
|
status: 'Expiring Soon'
|
|
},
|
|
{
|
|
id: '102',
|
|
name: 'Sarah Johnson',
|
|
expiryDate: 'May 15, 2025',
|
|
type: 'Family',
|
|
status: 'Expiring Soon'
|
|
},
|
|
{
|
|
id: '103',
|
|
name: 'Robert Lee',
|
|
expiryDate: 'Mar 28, 2025',
|
|
type: 'Regular',
|
|
status: 'Expired'
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-8">
|
|
<h2 className="admin-page-title text-lg">Overview</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<StatCard
|
|
title="Total Members"
|
|
value={stats.members}
|
|
icon={<MembersIcon />}
|
|
href="/admin/members"
|
|
/>
|
|
<StatCard
|
|
title="Active Members"
|
|
value={stats.activeMembers}
|
|
icon={<MembersIcon />}
|
|
href="/admin/members/active"
|
|
/>
|
|
<StatCard
|
|
title="Upcoming Events"
|
|
value={stats.events}
|
|
icon={<EventsIcon />}
|
|
href="/admin/events"
|
|
/>
|
|
<StatCard
|
|
title="Unread Messages"
|
|
value={stats.unreadContacts}
|
|
icon={<ContactIcon />}
|
|
href="/admin/contact"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
{/* Upcoming Events */}
|
|
<div className="admin-card admin-gold-accent">
|
|
<div className="admin-card-header">
|
|
<h3 className="admin-card-title admin-gold-title">Upcoming Events</h3>
|
|
<Link href="/admin/events" className="admin-card-link">
|
|
View All
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="admin-list">
|
|
{upcomingEvents.map((event) => (
|
|
<div key={event.id} className="admin-list-item">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h4 className="admin-list-title">{event.title}</h4>
|
|
<p className="admin-list-subtitle">{event.date} • {event.location}</p>
|
|
</div>
|
|
<span className={`admin-badge ${
|
|
event.status === 'Published'
|
|
? 'admin-badge-success'
|
|
: 'admin-badge-default'
|
|
}`}>
|
|
{event.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<Link
|
|
href="/admin/events/create"
|
|
className="btn-primary inline-flex items-center text-sm py-2 px-4"
|
|
>
|
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Add Event
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Membership Expirations */}
|
|
<div className="admin-card admin-gold-accent">
|
|
<div className="admin-card-header">
|
|
<h3 className="admin-card-title admin-gold-title">Membership Expirations</h3>
|
|
<Link href="/admin/members" className="admin-card-link">
|
|
View All
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="admin-list">
|
|
{expiringMemberships.map((member) => (
|
|
<div key={member.id} className="admin-list-item">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h4 className="admin-list-title">{member.name}</h4>
|
|
<p className="admin-list-subtitle">{member.expiryDate} • {member.type}</p>
|
|
</div>
|
|
<span className={`admin-badge ${
|
|
member.status === 'Expired'
|
|
? 'admin-badge-error'
|
|
: 'admin-badge-warning'
|
|
}`}>
|
|
{member.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<Link
|
|
href="/admin/members/notify"
|
|
className="btn-primary inline-flex items-center text-sm py-2 px-4"
|
|
>
|
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
Send Renewal Notices
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="mb-8">
|
|
<h3 className="admin-page-title text-lg">Quick Actions</h3>
|
|
<div className="admin-quick-actions">
|
|
<Link href="/admin/events/create" className="admin-quick-action">
|
|
<div className="admin-quick-action-icon">
|
|
<EventsIcon />
|
|
</div>
|
|
<span className="admin-quick-action-text">New Event</span>
|
|
</Link>
|
|
|
|
<Link href="/admin/members/add" className="admin-quick-action">
|
|
<div className="admin-quick-action-icon">
|
|
<MembersIcon />
|
|
</div>
|
|
<span className="admin-quick-action-text">Add Member</span>
|
|
</Link>
|
|
|
|
<Link href="/admin/videos/upload" className="admin-quick-action">
|
|
<div className="admin-quick-action-icon">
|
|
<VideosIcon />
|
|
</div>
|
|
<span className="admin-quick-action-text">Upload Video</span>
|
|
</Link>
|
|
|
|
<Link href="/admin/content/edit" className="admin-quick-action">
|
|
<div className="admin-quick-action-icon">
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</div>
|
|
<span className="admin-quick-action-text">Edit Pages</span>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|