feat: add AdminMemberTiers page, MemberBadge component, and SettingsLayout; refactor routes and sidebar for improved navigation
This commit is contained in:
15
src/App.js
15
src/App.js
@@ -23,6 +23,7 @@ import AdminStaff from './pages/admin/AdminStaff';
|
|||||||
import AdminMembers from './pages/admin/AdminMembers';
|
import AdminMembers from './pages/admin/AdminMembers';
|
||||||
import AdminPermissions from './pages/admin/AdminPermissions';
|
import AdminPermissions from './pages/admin/AdminPermissions';
|
||||||
import AdminSettings from './pages/admin/AdminSettings';
|
import AdminSettings from './pages/admin/AdminSettings';
|
||||||
|
import AdminMemberTiers from './pages/admin/AdminMemberTiers';
|
||||||
import AdminRoles from './pages/admin/AdminRoles';
|
import AdminRoles from './pages/admin/AdminRoles';
|
||||||
import AdminEvents from './pages/admin/AdminEvents';
|
import AdminEvents from './pages/admin/AdminEvents';
|
||||||
import AdminEventAttendance from './pages/admin/AdminEventAttendance';
|
import AdminEventAttendance from './pages/admin/AdminEventAttendance';
|
||||||
@@ -31,6 +32,7 @@ import AdminPlans from './pages/admin/AdminPlans';
|
|||||||
import AdminSubscriptions from './pages/admin/AdminSubscriptions';
|
import AdminSubscriptions from './pages/admin/AdminSubscriptions';
|
||||||
import AdminDonations from './pages/admin/AdminDonations';
|
import AdminDonations from './pages/admin/AdminDonations';
|
||||||
import AdminLayout from './layouts/AdminLayout';
|
import AdminLayout from './layouts/AdminLayout';
|
||||||
|
import SettingsLayout from './layouts/SettingsLayout';
|
||||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
import MemberRoute from './components/MemberRoute';
|
import MemberRoute from './components/MemberRoute';
|
||||||
import MemberCalendar from './pages/members/MemberCalendar';
|
import MemberCalendar from './pages/members/MemberCalendar';
|
||||||
@@ -286,18 +288,21 @@ function App() {
|
|||||||
} />
|
} />
|
||||||
<Route path="/admin/permissions" element={
|
<Route path="/admin/permissions" element={
|
||||||
<PrivateRoute adminOnly>
|
<PrivateRoute adminOnly>
|
||||||
<AdminLayout>
|
<Navigate to="/admin/settings/permissions" replace />
|
||||||
<AdminRoles />
|
|
||||||
</AdminLayout>
|
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/admin/settings" element={
|
<Route path="/admin/settings" element={
|
||||||
<PrivateRoute adminOnly>
|
<PrivateRoute adminOnly>
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<AdminSettings />
|
<SettingsLayout />
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
}>
|
||||||
|
<Route index element={<Navigate to="stripe" replace />} />
|
||||||
|
<Route path="stripe" element={<AdminSettings />} />
|
||||||
|
<Route path="permissions" element={<AdminRoles />} />
|
||||||
|
<Route path="member-tiers" element={<AdminMemberTiers />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* 404 - Catch all undefined routes */}
|
{/* 404 - Catch all undefined routes */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
|||||||
@@ -169,13 +169,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
path: '/admin/bylaws',
|
path: '/admin/bylaws',
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Permissions',
|
|
||||||
icon: Shield,
|
|
||||||
path: '/admin/permissions',
|
|
||||||
disabled: false,
|
|
||||||
superadminOnly: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
|
|||||||
19
src/components/MemberBadge.js
Normal file
19
src/components/MemberBadge.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// src/components/MemberBadge.js
|
||||||
|
import React from 'react';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { getTierForMember } from '../utils/member-tiers';
|
||||||
|
import { MEMBER_TIER_ICONS } from '../config/memberTierIcons';
|
||||||
|
|
||||||
|
const MemberBadge = ({ memberSince, tiers }) => {
|
||||||
|
const tier = getTierForMember(memberSince, tiers);
|
||||||
|
const Icon = MEMBER_TIER_ICONS[tier.icon] || MEMBER_TIER_ICONS.FaUser;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge className={`px-3 py-1 rounded-md text-sm flex items-center gap-2 ${tier.badgeClass}`}>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{tier.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberBadge;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import StatusBadge from './StatusBadge';
|
|
||||||
import { Card } from './ui/card';
|
import { Card } from './ui/card';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react';
|
import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react';
|
||||||
@@ -24,7 +23,7 @@ const MemberCard = ({ member, onViewProfile }) => {
|
|||||||
<Card className="p-6 bg-background rounded-3xl border border-[var(--neutral-800)] hover:shadow-lg transition-all h-full">
|
<Card className="p-6 bg-background rounded-3xl border border-[var(--neutral-800)] hover:shadow-lg transition-all h-full">
|
||||||
{/* Profile Photo */}
|
{/* Profile Photo */}
|
||||||
<div className='flex justify-end items-center'>
|
<div className='flex justify-end items-center'>
|
||||||
<StatusBadge status={member.membership_status || member.status} />
|
member since badge
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
{member.profile_photo_url ? (
|
{member.profile_photo_url ? (
|
||||||
|
|||||||
46
src/components/SettingsSidebar.js
Normal file
46
src/components/SettingsSidebar.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { CreditCard, Shield, Settings, Star } from 'lucide-react';
|
||||||
|
|
||||||
|
const settingsItems = [
|
||||||
|
{ label: 'Stripe Integration', path: '/admin/settings/stripe', icon: CreditCard },
|
||||||
|
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
|
||||||
|
{ label: 'Member Tiers', path: '/admin/settings/member-tiers', icon: Star },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SettingsSidebar = () => {
|
||||||
|
return (
|
||||||
|
<aside className="w-full lg:w-64 shrink-0 bg-background border border-[var(--neutral-800)] rounded-2xl p-4 lg:sticky lg:top-8 h-max">
|
||||||
|
<div className="flex items-center gap-2 px-2 pb-3 border-b border-[var(--neutral-800)]">
|
||||||
|
<Settings className="h-4 w-4 text-[var(--purple-ink)]" />
|
||||||
|
<span className="text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Settings
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<nav className="mt-3 space-y-1">
|
||||||
|
{settingsItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={item.label}
|
||||||
|
to={item.path}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
[
|
||||||
|
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||||
|
isActive
|
||||||
|
? 'bg-[var(--orange-light)]/10 text-[var(--purple-ink)]'
|
||||||
|
: 'text-[var(--purple-ink)] hover:bg-[var(--neutral-800)]/10',
|
||||||
|
].join(' ')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsSidebar;
|
||||||
27
src/config/MemberTiers.js
Normal file
27
src/config/MemberTiers.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// src/config/memberTiers.js
|
||||||
|
export const DEFAULT_MEMBER_TIERS = [
|
||||||
|
{
|
||||||
|
id: 'new',
|
||||||
|
label: 'New Member',
|
||||||
|
minDays: 0,
|
||||||
|
maxDays: 364, // < 1 year
|
||||||
|
icon: 'FaSeedling',
|
||||||
|
badgeClass: 'bg-[var(--lavender-300)] text-[var(--purple-ink)]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'silver',
|
||||||
|
label: 'Silver Member',
|
||||||
|
minDays: 365,
|
||||||
|
maxDays: 729,
|
||||||
|
icon: 'FaMedal',
|
||||||
|
badgeClass: 'bg-slate-200 text-slate-900',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gold',
|
||||||
|
label: 'Gold Member',
|
||||||
|
minDays: 730,
|
||||||
|
maxDays: null, // open-ended
|
||||||
|
icon: 'FaCrown',
|
||||||
|
badgeClass: 'bg-amber-200 text-amber-900',
|
||||||
|
},
|
||||||
|
];
|
||||||
9
src/config/memberTierIcons.js
Normal file
9
src/config/memberTierIcons.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/config/memberTierIcons.js
|
||||||
|
import { FaSeedling, FaMedal, FaCrown, FaUser } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export const MEMBER_TIER_ICONS = {
|
||||||
|
FaSeedling,
|
||||||
|
FaMedal,
|
||||||
|
FaCrown,
|
||||||
|
FaUser,
|
||||||
|
};
|
||||||
16
src/layouts/SettingsLayout.js
Normal file
16
src/layouts/SettingsLayout.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import SettingsSidebar from '../components/SettingsSidebar';
|
||||||
|
|
||||||
|
const SettingsLayout = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 lg:flex-row">
|
||||||
|
<SettingsSidebar />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsLayout;
|
||||||
50
src/pages/admin/AdminMemberTiers.js
Normal file
50
src/pages/admin/AdminMemberTiers.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card } from '../../components/ui/card';
|
||||||
|
import { Badge } from '../../components/ui/badge';
|
||||||
|
import { DEFAULT_MEMBER_TIERS } from '../../config/MemberTiers';
|
||||||
|
|
||||||
|
const AdminMemberTiers = () => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Member Tiers
|
||||||
|
</h1>
|
||||||
|
<p className="text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Configure tier names, time ranges, and badges used in the members directory.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{DEFAULT_MEMBER_TIERS.map((tier) => {
|
||||||
|
const rangeLabel = tier.maxDays == null
|
||||||
|
? `${tier.minDays}+ days`
|
||||||
|
: `${tier.minDays}–${tier.maxDays} days`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tier.id}
|
||||||
|
className="flex flex-wrap items-center justify-between gap-4 border border-[var(--neutral-800)] rounded-xl p-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{tier.label}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{rangeLabel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className={`px-3 py-1 rounded-md text-sm ${tier.badgeClass}`}>
|
||||||
|
{tier.icon}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminMemberTiers;
|
||||||
23
src/utils/member-tiers.js
Normal file
23
src/utils/member-tiers.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// src/utils/member-tiers.js
|
||||||
|
import { differenceInDays } from 'date-fns';
|
||||||
|
import { DEFAULT_MEMBER_TIERS } from '../config/memberTiers';
|
||||||
|
|
||||||
|
export const getTenureDays = (memberSince) => {
|
||||||
|
if (!memberSince) return null;
|
||||||
|
const since = new Date(memberSince);
|
||||||
|
if (Number.isNaN(since.getTime())) return null;
|
||||||
|
return Math.max(0, differenceInDays(new Date(), since));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTierForMember = (memberSince, tiers = DEFAULT_MEMBER_TIERS) => {
|
||||||
|
const days = getTenureDays(memberSince);
|
||||||
|
if (days == null) return tiers[0];
|
||||||
|
|
||||||
|
const match = tiers.find(
|
||||||
|
(tier) =>
|
||||||
|
days >= tier.minDays &&
|
||||||
|
(tier.maxDays == null || days <= tier.maxDays)
|
||||||
|
);
|
||||||
|
|
||||||
|
return match || tiers[0];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user