Member tiers implementation intact. Icons updated to be Lucide React. Create/edit member tiers. Display member badge. Transaction history now in My profile dashboard. Adjusted Icons for badges. Added badges on my profile page
This commit is contained in:
@@ -10,7 +10,8 @@ import MemberFooter from '../components/MemberFooter';
|
||||
import { Calendar, User, CheckCircle, Clock, AlertCircle, Mail, Users, Image, FileText, DollarSign, Scale, Receipt, Heart, CreditCard } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import TransactionHistory from '../components/TransactionHistory';
|
||||
|
||||
import MemberBadge from '@/components/MemberBadge';
|
||||
import useMemberTiers from '../hooks/use-member-tiers'
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user, resendVerificationEmail, refreshUser } = useAuth();
|
||||
@@ -23,6 +24,7 @@ const Dashboard = () => {
|
||||
const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
|
||||
const [activeTransactionTab, setActiveTransactionTab] = useState('all');
|
||||
const joinedDate = user?.member_since || user?.created_at;
|
||||
const { tiers, loading: tiersLoading } = useMemberTiers();
|
||||
|
||||
useEffect(() => {
|
||||
fetchUpcomingEvents();
|
||||
@@ -131,6 +133,8 @@ const Dashboard = () => {
|
||||
return messages[status] || '';
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
@@ -209,20 +213,31 @@ const Dashboard = () => {
|
||||
Quick Info
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* member date and badge */}
|
||||
<div className='flex justify-between'>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
{!tiersLoading && (
|
||||
<div className='lg:mr-10'>
|
||||
<MemberBadge memberSince={joinedDate} tiers={tiers} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* email */}
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.email}</p>
|
||||
</div>
|
||||
{/* role */}
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.role}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,43 +1,361 @@
|
||||
import React from 'react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { DEFAULT_MEMBER_TIERS } from '../../config/MemberTiers';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Input } from '../../components/ui/input';
|
||||
import { Label } from '../../components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../../components/ui/select';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '../../components/ui/alert-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import useMemberTiers from '../../hooks/use-member-tiers';
|
||||
import { TIER_ICON_OPTIONS, BADGE_COLOR_PRESETS } from '../../config/MemberTiers';
|
||||
import { getTierIcon } from '../../config/memberTierIcons';
|
||||
import { Save, RotateCcw, Plus, Trash2, GripVertical, AlertTriangle, Users } from 'lucide-react';
|
||||
|
||||
const AdminMemberTiers = () => {
|
||||
const { user } = useAuth();
|
||||
const { tiers, loading, saving, updateTiers, resetToDefaults } = useMemberTiers({ isAdmin: true });
|
||||
const [editedTiers, setEditedTiers] = useState([]);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [showResetDialog, setShowResetDialog] = useState(false);
|
||||
|
||||
const isSuperAdmin = user?.role === 'superadmin';
|
||||
|
||||
// Initialize edited tiers when tiers load
|
||||
useEffect(() => {
|
||||
if (tiers && tiers.length > 0) {
|
||||
setEditedTiers(JSON.parse(JSON.stringify(tiers)));
|
||||
setHasChanges(false);
|
||||
}
|
||||
}, [tiers]);
|
||||
|
||||
// Check for changes
|
||||
useEffect(() => {
|
||||
if (tiers && editedTiers.length > 0) {
|
||||
const changed = JSON.stringify(tiers) !== JSON.stringify(editedTiers);
|
||||
setHasChanges(changed);
|
||||
}
|
||||
}, [tiers, editedTiers]);
|
||||
|
||||
const handleTierChange = useCallback((index, field, value) => {
|
||||
setEditedTiers(prev => {
|
||||
const updated = [...prev];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAddTier = useCallback(() => {
|
||||
const newTier = {
|
||||
id: `tier_${Date.now()}`,
|
||||
label: 'New Tier',
|
||||
minYears: editedTiers.length > 0
|
||||
? Math.max(...editedTiers.map(t => t.maxYears || 0)) + 0.001
|
||||
: 0,
|
||||
maxYears: 999,
|
||||
iconKey: 'star',
|
||||
badgeClass: 'bg-gray-100 text-gray-800 border-gray-200',
|
||||
};
|
||||
setEditedTiers(prev => [...prev, newTier]);
|
||||
}, [editedTiers]);
|
||||
|
||||
const handleRemoveTier = useCallback((index) => {
|
||||
if (editedTiers.length <= 1) {
|
||||
toast.error('You must have at least one tier');
|
||||
return;
|
||||
}
|
||||
setEditedTiers(prev => prev.filter((_, i) => i !== index));
|
||||
}, [editedTiers.length]);
|
||||
|
||||
const validateTiers = useCallback(() => {
|
||||
for (let i = 0; i < editedTiers.length; i++) {
|
||||
const tier = editedTiers[i];
|
||||
if (!tier.label?.trim()) {
|
||||
toast.error(`Tier ${i + 1} must have a label`);
|
||||
return false;
|
||||
}
|
||||
if (tier.minYears < 0) {
|
||||
toast.error(`Tier "${tier.label}" has invalid minimum years`);
|
||||
return false;
|
||||
}
|
||||
if (tier.maxYears <= tier.minYears) {
|
||||
toast.error(`Tier "${tier.label}" max years must be greater than min years`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overlapping ranges
|
||||
const sorted = [...editedTiers].sort((a, b) => a.minYears - b.minYears);
|
||||
for (let i = 0; i < sorted.length - 1; i++) {
|
||||
if (sorted[i].maxYears >= sorted[i + 1].minYears) {
|
||||
toast.error(`Tier ranges overlap between "${sorted[i].label}" and "${sorted[i + 1].label}"`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [editedTiers]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateTiers()) return;
|
||||
|
||||
const success = await updateTiers(editedTiers);
|
||||
if (success) {
|
||||
toast.success('Member tiers saved successfully');
|
||||
} else {
|
||||
toast.error('Failed to save member tiers');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
const success = await resetToDefaults();
|
||||
if (success) {
|
||||
toast.success('Member tiers reset to defaults');
|
||||
setShowResetDialog(false);
|
||||
} else {
|
||||
toast.error('Failed to reset member tiers');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscardChanges = () => {
|
||||
setEditedTiers(JSON.parse(JSON.stringify(tiers)));
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-muted-foreground">
|
||||
Configure tier names, time ranges, and badges used in the members directory.
|
||||
</p>
|
||||
{/* Header and Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<p className="text-muted-foreground">
|
||||
Configure tier names, time ranges, and badges displayed in the members directory.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{hasChanges && (
|
||||
<Button variant="outline" onClick={handleDiscardChanges}>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
{isSuperAdmin && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowResetDialog(true)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSave} disabled={saving || !hasChanges}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</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`;
|
||||
{/* Tier Cards */}
|
||||
<div className="space-y-4">
|
||||
{editedTiers.map((tier, index) => {
|
||||
const IconComponent = getTierIcon(tier.iconKey);
|
||||
|
||||
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}
|
||||
return (
|
||||
<Card key={tier.id} className="bg-background">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Drag Handle & Remove */}
|
||||
<div className="flex lg:flex-col items-center gap-2 lg:pt-6">
|
||||
<GripVertical className="h-5 w-5 text-muted-foreground cursor-move" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveTier(index)}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
disabled={editedTiers.length <= 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{rangeLabel}
|
||||
|
||||
{/* Tier Configuration */}
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Label */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`tier-label-${index}`}>Label</Label>
|
||||
<Input
|
||||
id={`tier-label-${index}`}
|
||||
value={tier.label}
|
||||
onChange={(e) => handleTierChange(index, 'label', e.target.value)}
|
||||
placeholder="Tier Name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Years */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`tier-min-${index}`}>Min Years</Label>
|
||||
<Input
|
||||
id={`tier-min-${index}`}
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
value={tier.minYears}
|
||||
onChange={(e) => handleTierChange(index, 'minYears', parseFloat(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Years */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`tier-max-${index}`}>Max Years</Label>
|
||||
<Input
|
||||
id={`tier-max-${index}`}
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
value={tier.maxYears}
|
||||
onChange={(e) => handleTierChange(index, 'maxYears', parseFloat(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`tier-icon-${index}`}>Icon</Label>
|
||||
<Select
|
||||
value={tier.iconKey}
|
||||
onValueChange={(value) => handleTierChange(index, 'iconKey', value)}
|
||||
>
|
||||
<SelectTrigger id={`tier-icon-${index}`}>
|
||||
<SelectValue placeholder="Select icon" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIER_ICON_OPTIONS.map((option) => {
|
||||
const OptionIcon = getTierIcon(option.key);
|
||||
return (
|
||||
<SelectItem key={option.key} value={option.key}>
|
||||
<div className="flex items-center gap-2">
|
||||
<OptionIcon className="h-4 w-4" />
|
||||
{option.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Badge Color */}
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor={`tier-badge-${index}`}>Badge Style</Label>
|
||||
<Select
|
||||
value={tier.badgeClass}
|
||||
onValueChange={(value) => handleTierChange(index, 'badgeClass', value)}
|
||||
>
|
||||
<SelectTrigger id={`tier-badge-${index}`}>
|
||||
<SelectValue placeholder="Select color" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BADGE_COLOR_PRESETS.map((preset) => (
|
||||
<SelectItem key={preset.label} value={preset.badgeClass}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-4 h-4 rounded ${preset.badgeClass}`} />
|
||||
{preset.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="space-y-2 md:col-span-2 flex flex-col">
|
||||
<Label>Preview</Label>
|
||||
<div className="flex-1 flex items-center">
|
||||
<Badge className={`px-3 py-1.5 rounded-md text-sm flex items-center gap-2 border ${tier.badgeClass}`}>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
{tier.label || 'Tier Name'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={`px-3 py-1 rounded-md text-sm ${tier.badgeClass}`}>
|
||||
{tier.icon}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Add Tier Button */}
|
||||
<Button variant="outline" onClick={handleAddTier} className="w-full border-dashed">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Tier
|
||||
</Button>
|
||||
|
||||
{/* Info Card */}
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
How Member Tiers Work
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground space-y-2">
|
||||
<p>
|
||||
Member tiers are automatically assigned based on how long a member has been active.
|
||||
The tier badge appears on member profiles and in the member directory.
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>Tiers are matched based on membership duration in years</li>
|
||||
<li>Each tier should have non-overlapping year ranges</li>
|
||||
<li>The last tier typically uses a high max value (e.g., 999) to catch all long-term members</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Reset Confirmation Dialog */}
|
||||
<AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
Reset Tiers to Defaults?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will delete all custom tier configurations and restore the default member tiers.
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleReset}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Reset to Defaults
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
} from '../../components/ui/dialog';
|
||||
import { User, Search, Mail, MapPin, Phone, Heart, Facebook, Instagram, Twitter, Linkedin, UserCircle, Calendar } from 'lucide-react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import StatusBadge from '@/components/StatusBadge';
|
||||
import MemberCard from '../../components/MemberCard';
|
||||
import MemberBadge from '../../components/MemberBadge';
|
||||
import useMembers from '../../hooks/use-members';
|
||||
import useMemberTiers from '../../hooks/use-member-tiers';
|
||||
|
||||
const MembersDirectory = () => {
|
||||
const [selectedMember, setSelectedMember] = useState(null);
|
||||
@@ -25,7 +26,27 @@ const MembersDirectory = () => {
|
||||
const { toast } = useToast();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 12;
|
||||
const { tiers } = useMemberTiers();
|
||||
const allowedRoles = useMemo(() => [], []);
|
||||
const normalizeStatus = useCallback((status) => {
|
||||
if (typeof status === 'string') {
|
||||
return status.toLowerCase();
|
||||
}
|
||||
return status;
|
||||
}, []);
|
||||
const normalizeMembers = useCallback(
|
||||
(data) => {
|
||||
const list = Array.isArray(data)
|
||||
? data
|
||||
: data?.members || data?.results || data?.items || data?.data || [];
|
||||
|
||||
return list.map((member) => ({
|
||||
...member,
|
||||
status: normalizeStatus(member.status ?? member.membership_status ?? member.member_status),
|
||||
}));
|
||||
},
|
||||
[normalizeStatus]
|
||||
);
|
||||
const searchAccessor = useCallback(
|
||||
(member) => [
|
||||
`${member.first_name} ${member.last_name}`,
|
||||
@@ -47,12 +68,14 @@ const MembersDirectory = () => {
|
||||
loading,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
filterValue,
|
||||
} = useMembers({
|
||||
endpoint: '/members/directory',
|
||||
initialFilter: 'active',
|
||||
initialFilter: 'all',
|
||||
filterKey: 'status',
|
||||
allowedRoles,
|
||||
searchAccessor,
|
||||
transform: normalizeMembers,
|
||||
fetchErrorMessage: 'Failed to load members directory. Please try again.',
|
||||
onFetchError: handleFetchError
|
||||
});
|
||||
@@ -67,7 +90,12 @@ const MembersDirectory = () => {
|
||||
|
||||
const paginatedMembers = filteredMembers.slice(pageStart, pageStart + pageSize);
|
||||
|
||||
const totalMembers = members.length;
|
||||
const totalMembers = useMemo(() => {
|
||||
if (!filterValue || filterValue === 'all') {
|
||||
return members.length;
|
||||
}
|
||||
return members.filter((member) => member.status === filterValue).length;
|
||||
}, [members, filterValue]);
|
||||
|
||||
|
||||
|
||||
@@ -158,7 +186,7 @@ const MembersDirectory = () => {
|
||||
) : filteredMembers.length > 0 ? (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{paginatedMembers.map((member) => (
|
||||
<MemberCard key={member.id} member={member} onViewProfile={handleViewProfile} />
|
||||
<MemberCard key={member.id} member={member} onViewProfile={handleViewProfile} tiers={tiers} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -210,7 +238,7 @@ const MembersDirectory = () => {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-3xl font-semibold text-[var(--purple-ink)] flex items-center justify-between mr-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedMember.first_name} {selectedMember.last_name}
|
||||
<StatusBadge status={selectedMember.membership_status || selectedMember.status} />
|
||||
<MemberBadge memberSince={selectedMember.member_since || selectedMember.created_at} tiers={tiers} />
|
||||
</DialogTitle>
|
||||
{selectedMember.directory_partner_name && (
|
||||
<DialogDescription className="flex items-center gap-2 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
|
||||
Reference in New Issue
Block a user