Merge pull request 'theme-provider' (#18) from theme-provider into dev
Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
@@ -224,7 +224,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
${item.disabled
|
${item.disabled
|
||||||
? 'opacity-50 cursor-not-allowed text-brand-purple '
|
? 'opacity-50 cursor-not-allowed text-brand-purple '
|
||||||
: active
|
: active
|
||||||
? 'bg-[var(--orange-light)]/10 text-[var(--orange-light)]'
|
? 'bg-[var(--orange-light)]/10 text-[var(--purple-ink)]'
|
||||||
: 'text-[var(--purple-ink)] hover:bg-[var(--neutral-800)]/20'
|
: 'text-[var(--purple-ink)] hover:bg-[var(--neutral-800)]/20'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
@@ -254,7 +254,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
|
|
||||||
{/* Badge when collapsed */}
|
{/* Badge when collapsed */}
|
||||||
{!isOpen && item.badge > 0 && !item.disabled && (
|
{!isOpen && item.badge > 0 && !item.disabled && (
|
||||||
<div className="absolute -top-1 -right-1 bg-accent foreground text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
|
<div className="absolute -top-1 -right-1 bg-accent text-white foreground text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
|
||||||
{item.badge}
|
{item.badge}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
const getTodayDate = () => new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const handleChange = (field, value) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
@@ -84,8 +85,8 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
if (payload.date_of_birth === '') {
|
if (payload.date_of_birth === '') {
|
||||||
delete payload.date_of_birth;
|
delete payload.date_of_birth;
|
||||||
}
|
}
|
||||||
if (payload.member_since === '') {
|
if (!payload.member_since) {
|
||||||
delete payload.member_since;
|
payload.member_since = getTodayDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.post('/admin/users/create', payload);
|
await api.post('/admin/users/create', payload);
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
member_since: '',
|
||||||
role: 'admin'
|
role: 'admin'
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
const getTodayDate = () => new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const handleChange = (field, value) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
@@ -74,7 +76,11 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post('/admin/users/create', formData);
|
const payload = { ...formData };
|
||||||
|
if (!payload.member_since) {
|
||||||
|
payload.member_since = getTodayDate();
|
||||||
|
}
|
||||||
|
await api.post('/admin/users/create', payload);
|
||||||
toast.success('Staff member created successfully');
|
toast.success('Staff member created successfully');
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
@@ -84,6 +90,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
member_since: '',
|
||||||
role: 'admin'
|
role: 'admin'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,6 +207,20 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Member Since */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="member_since" className="text-[var(--purple-ink)]">
|
||||||
|
Member Since
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="member_since"
|
||||||
|
type="date"
|
||||||
|
value={formData.member_since}
|
||||||
|
onChange={(e) => handleChange('member_since', e.target.value)}
|
||||||
|
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Role */}
|
{/* Role */}
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="role" className="text-[var(--purple-ink)]">
|
<Label htmlFor="role" className="text-[var(--purple-ink)]">
|
||||||
|
|||||||
36
src/components/StatCard.jsx
Normal file
36
src/components/StatCard.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card } from "./ui/card";
|
||||||
|
|
||||||
|
export const StatCard = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
icon: Icon,
|
||||||
|
iconBgClass,
|
||||||
|
dataTestId,
|
||||||
|
}) => (
|
||||||
|
<Card
|
||||||
|
className="p-6 flex flex-col justify-between bg-background rounded-2xl border border-[var(--neutral-800)]"
|
||||||
|
data-testid={dataTestId}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4 mb-4 ">
|
||||||
|
<div className={`${iconBgClass} p-3 rounded-lg `}>
|
||||||
|
<Icon className="size-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
<p
|
||||||
|
className="text-6xl font-semibold text-[var(--purple-ink)] mb-1"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-sm text-brand-purple "
|
||||||
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
@@ -1,50 +1,65 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
className={cn(
|
||||||
{...props} />
|
"rounded-xl border bg-card text-card-foreground shadow",
|
||||||
))
|
className,
|
||||||
Card.displayName = "Card"
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
{...props} />
|
{...props}
|
||||||
))
|
/>
|
||||||
CardHeader.displayName = "CardHeader"
|
));
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
{...props} />
|
{...props}
|
||||||
))
|
/>
|
||||||
CardTitle.displayName = "CardTitle"
|
));
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props} />
|
{...props}
|
||||||
))
|
/>
|
||||||
CardDescription.displayName = "CardDescription"
|
));
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
))
|
));
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
{...props} />
|
{...props}
|
||||||
))
|
/>
|
||||||
CardFooter.displayName = "CardFooter"
|
));
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const Dashboard = () => {
|
|||||||
const [resendLoading, setResendLoading] = useState(false);
|
const [resendLoading, setResendLoading] = useState(false);
|
||||||
const [eventActivity, setEventActivity] = useState(null);
|
const [eventActivity, setEventActivity] = useState(null);
|
||||||
const [activityLoading, setActivityLoading] = useState(true);
|
const [activityLoading, setActivityLoading] = useState(true);
|
||||||
|
const joinedDate = user?.member_since || user?.created_at;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUpcomingEvents();
|
fetchUpcomingEvents();
|
||||||
@@ -197,7 +198,7 @@ const Dashboard = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</p>
|
<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" }}>
|
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A'}
|
{joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{user?.subscription_start_date && user?.subscription_end_date && (
|
{user?.subscription_start_date && user?.subscription_end_date && (
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ const Register = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform"
|
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-background rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
<ArrowRight className="ml-2 h-5 w-5" />
|
<ArrowRight className="ml-2 h-5 w-5" />
|
||||||
@@ -267,7 +267,7 @@ const Register = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50 disabled:cursor-not-allowed"
|
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-backgroundrounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
data-testid="submit-register-button"
|
data-testid="submit-register-button"
|
||||||
>
|
>
|
||||||
{loading ? 'Creating Account...' : 'Create Account'}
|
{loading ? 'Creating Account...' : 'Create Account'}
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import api from '../../utils/api';
|
|||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle, Globe } from 'lucide-react';
|
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle, Globe, CircleMinus } from 'lucide-react';
|
||||||
|
import { StatCard } from '../../components/StatCard';
|
||||||
|
|
||||||
|
|
||||||
const AdminDashboard = () => {
|
const AdminDashboard = () => {
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
totalMembers: 0,
|
totalMembers: 0,
|
||||||
pendingValidations: 0,
|
pendingValidations: 0,
|
||||||
activeMembers: 0
|
activeMembers: 0,
|
||||||
|
inactiveMembers: 0
|
||||||
});
|
});
|
||||||
const [usersNeedingAttention, setUsersNeedingAttention] = useState([]);
|
const [usersNeedingAttention, setUsersNeedingAttention] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -29,7 +32,8 @@ const AdminDashboard = () => {
|
|||||||
pendingValidations: users.filter(u =>
|
pendingValidations: users.filter(u =>
|
||||||
['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status)
|
['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status)
|
||||||
).length,
|
).length,
|
||||||
activeMembers: users.filter(u => u.status === 'active' && u.role === 'member').length
|
activeMembers: users.filter(u => u.status === 'active' && u.role === 'member').length,
|
||||||
|
inactiveMembers: users.filter(u => u.status === 'inactive' && u.role === 'member').length
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find users who have received 3+ reminders (may need personal outreach)
|
// Find users who have received 3+ reminders (may need personal outreach)
|
||||||
@@ -76,52 +80,42 @@ const AdminDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
|
||||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]" data-testid="stat-total-users">
|
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||||
<div className='flex items-start justify-between'>
|
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||||
<p className="text-3xl font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
Quick Overview
|
||||||
{loading ? '-' : stats.totalMembers}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
|
|
||||||
<Users className="h-6 w-6 text-brand-purple" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 ">
|
||||||
|
<StatCard
|
||||||
|
title="Total Members"
|
||||||
|
value={loading ? '-' : stats.totalMembers}
|
||||||
|
icon={Users}
|
||||||
|
iconBgClass="text-brand-purple"
|
||||||
|
dataTestId="stat-total-users"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Pending Validations"
|
||||||
|
value={loading ? '-' : stats.pendingValidations}
|
||||||
|
icon={Clock}
|
||||||
|
iconBgClass="text-brand-light-orange"
|
||||||
|
dataTestId="stat-total-users"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Active Members"
|
||||||
|
value={loading ? '-' : stats.activeMembers}
|
||||||
|
icon={CheckCircle}
|
||||||
|
iconBgClass="text-[var(--green-light)]"
|
||||||
|
dataTestId="stat-total-users"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Inactive Members"
|
||||||
|
value={loading ? '-' : stats.inactiveMembers}
|
||||||
|
icon={CircleMinus}
|
||||||
|
iconBgClass="text-brand-pink"
|
||||||
|
dataTestId="stat-total-users"
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]" data-testid="stat-pending-validations">
|
|
||||||
<div className='flex items-start justify-between'>
|
|
||||||
<p className="text-3xl font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
{loading ? '-' : stats.pendingValidations}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className=" p-3 rounded-lg">
|
|
||||||
<Clock className="h-6 w-6 text-orange-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pending Validations</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]" data-testid="stat-active-members">
|
|
||||||
<div className='flex items-start justify-between'>
|
|
||||||
<p className="text-3xl font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
{loading ? '-' : stats.activeMembers}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="bg-[var(--green-light)]/20 p-3 rounded-lg">
|
|
||||||
<CheckCircle className="h-6 w-6 text-[var(--green-light)]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active Members</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../../components/ui/dropdown-menu';
|
} from '../../components/ui/dropdown-menu';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Users, Search, User, CreditCard, Eye, CheckCircle, Calendar, AlertCircle, Clock, Mail, UserPlus, Upload, Download, FileDown, ChevronDown } from 'lucide-react';
|
import { Users, Search, User, CreditCard, Eye, CheckCircle, Calendar, AlertCircle, Clock, Mail, UserPlus, Upload, Download, FileDown, ChevronDown, CircleMinus } from 'lucide-react';
|
||||||
import PaymentActivationDialog from '../../components/PaymentActivationDialog';
|
import PaymentActivationDialog from '../../components/PaymentActivationDialog';
|
||||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||||
import CreateMemberDialog from '../../components/CreateMemberDialog';
|
import CreateMemberDialog from '../../components/CreateMemberDialog';
|
||||||
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
||||||
import WordPressImportWizard from '../../components/WordPressImportWizard';
|
import WordPressImportWizard from '../../components/WordPressImportWizard';
|
||||||
|
import { StatCard } from '@/components/StatCard';
|
||||||
|
|
||||||
const AdminMembers = () => {
|
const AdminMembers = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -319,31 +320,45 @@ const AdminMembers = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
Quick Overview
|
||||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
</div>
|
||||||
{users.length}
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
<StatCard
|
||||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active</p>
|
title="Total Members"
|
||||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
value={users.length}
|
||||||
{users.filter(u => u.status === 'active').length}
|
icon={Users}
|
||||||
</p>
|
iconBgClass="bg-[var(--blue-light)] text-[var(--blue-dark)]"
|
||||||
</Card>
|
dataTestId="stat-total-members"
|
||||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
/>
|
||||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Pending</p>
|
<StatCard
|
||||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
title="Active"
|
||||||
{users.filter(u => u.status === 'payment_pending').length}
|
value={users.filter(u => u.status === 'active').length}
|
||||||
</p>
|
icon={CheckCircle}
|
||||||
</Card>
|
iconBgClass="text-[var(--green-light)]"
|
||||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
dataTestId="stat-active-members"
|
||||||
<p className="text-sm text-brand-purple dark:text-brand-lavender mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Inactive</p>
|
/>
|
||||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<StatCard
|
||||||
{users.filter(u => u.status === 'inactive').length}
|
title="Payment Pending"
|
||||||
</p>
|
value={users.filter(u => u.status === 'payment_pending').length}
|
||||||
</Card>
|
icon={CreditCard}
|
||||||
|
iconBgClass="text-brand-light-orange"
|
||||||
|
dataTestId="stat-payment-pending-members"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Inactive"
|
||||||
|
value={users.filter(u => u.status === 'inactive').length}
|
||||||
|
icon={CircleMinus}
|
||||||
|
iconBgClass=" text-brand-pink"
|
||||||
|
dataTestId="stat-inactive-members"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
@@ -385,7 +400,9 @@ const AdminMembers = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : filteredUsers.length > 0 ? (
|
) : filteredUsers.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredUsers.map((user) => (
|
{filteredUsers.map((user) => {
|
||||||
|
const joinedDate = user.member_since || user.created_at;
|
||||||
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||||
@@ -409,7 +426,7 @@ const AdminMembers = () => {
|
|||||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple dark:text-brand-lavender " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple dark:text-brand-lavender " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<p>Email: {user.email}</p>
|
<p>Email: {user.email}</p>
|
||||||
<p>Phone: {user.phone}</p>
|
<p>Phone: {user.phone}</p>
|
||||||
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
|
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||||
{user.referred_by_member_name && (
|
{user.referred_by_member_name && (
|
||||||
<p>Referred by: {user.referred_by_member_name}</p>
|
<p>Referred by: {user.referred_by_member_name}</p>
|
||||||
)}
|
)}
|
||||||
@@ -523,7 +540,8 @@ const AdminMembers = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-20">
|
<div className="text-center py-20">
|
||||||
|
|||||||
@@ -242,7 +242,9 @@ const AdminStaff = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : filteredUsers.length > 0 ? (
|
) : filteredUsers.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredUsers.map((user) => (
|
{filteredUsers.map((user) => {
|
||||||
|
const joinedDate = user.member_since || user.created_at;
|
||||||
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||||
@@ -267,7 +269,7 @@ const AdminStaff = () => {
|
|||||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<p>Email: {user.email}</p>
|
<p>Email: {user.email}</p>
|
||||||
<p>Phone: {user.phone}</p>
|
<p>Phone: {user.phone}</p>
|
||||||
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
|
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||||
{user.last_login && (
|
{user.last_login && (
|
||||||
<p>Last Login: {new Date(user.last_login).toLocaleDateString()}</p>
|
<p>Last Login: {new Date(user.last_login).toLocaleDateString()}</p>
|
||||||
)}
|
)}
|
||||||
@@ -322,7 +324,8 @@ const AdminStaff = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-20">
|
<div className="text-center py-20">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Card } from '../../components/ui/card';
|
|||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '../../components/ui/avatar';
|
import { Avatar, AvatarImage, AvatarFallback } from '../../components/ui/avatar';
|
||||||
|
import { Input } from '../../components/ui/input';
|
||||||
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2, Shield } from 'lucide-react';
|
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2, Shield } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||||
@@ -24,15 +25,58 @@ const AdminUserView = () => {
|
|||||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
|
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
|
||||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
|
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
|
||||||
|
const [memberSince, setMemberSince] = useState('');
|
||||||
|
const [memberSinceSaving, setMemberSinceSaving] = useState(false);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [changeRoleDialogOpen, setChangeRoleDialogOpen] = useState(false);
|
const [changeRoleDialogOpen, setChangeRoleDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const formatLocalDateInputValue = (date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateInputValue = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
|
return value.slice(0, 10);
|
||||||
|
}
|
||||||
|
return formatLocalDateInputValue(parsed);
|
||||||
|
}
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return '';
|
||||||
|
return formatLocalDateInputValue(parsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateDisplayValue = (value) => {
|
||||||
|
if (!value) return 'N/A';
|
||||||
|
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||||
|
const [year, month, day] = value.split('-').map(Number);
|
||||||
|
return new Date(year, month - 1, day).toLocaleDateString();
|
||||||
|
}
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return 'N/A';
|
||||||
|
return parsed.toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
fetchSubscriptions();
|
fetchSubscriptions();
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setMemberSince(formatDateInputValue(user.member_since));
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/admin/users/${userId}`);
|
const response = await api.get(`/admin/users/${userId}`);
|
||||||
@@ -177,6 +221,27 @@ const AdminUserView = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMemberSinceSave = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
setMemberSinceSaving(true);
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
member_since: memberSince ? memberSince : null
|
||||||
|
};
|
||||||
|
const response = await api.put(`/admin/users/${userId}`, payload);
|
||||||
|
setUser(prev => ({
|
||||||
|
...prev,
|
||||||
|
...(response?.data || {}),
|
||||||
|
member_since: payload.member_since
|
||||||
|
}));
|
||||||
|
toast.success('Member since updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to update member since');
|
||||||
|
} finally {
|
||||||
|
setMemberSinceSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getActionMessage = () => {
|
const getActionMessage = () => {
|
||||||
if (!pendingAction || !user) return {};
|
if (!pendingAction || !user) return {};
|
||||||
|
|
||||||
@@ -212,6 +277,10 @@ const AdminUserView = () => {
|
|||||||
if (loading) return <div>Loading...</div>;
|
if (loading) return <div>Loading...</div>;
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
|
const joinedDate = user.member_since || user.created_at;
|
||||||
|
const memberSinceBaseline = formatDateInputValue(user.member_since);
|
||||||
|
const memberSinceHasChanges = memberSince !== memberSinceBaseline;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
@@ -262,7 +331,7 @@ const AdminUserView = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
<span>Joined {new Date(user.created_at).toLocaleDateString()}</span>
|
<span>Joined {formatDateDisplayValue(joinedDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,10 +428,31 @@ const AdminUserView = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</label>
|
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</label>
|
||||||
<p className="text-[var(--purple-ink)] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[var(--purple-ink)] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
{new Date(user.date_of_birth).toLocaleDateString()}
|
{formatDateDisplayValue(user.date_of_birth)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</label>
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={memberSince}
|
||||||
|
onChange={(e) => setMemberSince(e.target.value)}
|
||||||
|
className="max-w-[200px] border-[var(--neutral-800)]"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleMemberSinceSave}
|
||||||
|
disabled={memberSinceSaving || !memberSinceHasChanges}
|
||||||
|
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||||
|
>
|
||||||
|
{memberSinceSaving ? 'Saving...' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{user.partner_first_name && (
|
{user.partner_first_name && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Partner</label>
|
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Partner</label>
|
||||||
@@ -429,14 +519,14 @@ const AdminUserView = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Start Date</label>
|
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Start Date</label>
|
||||||
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
{new Date(sub.start_date).toLocaleDateString()}
|
{formatDateDisplayValue(sub.start_date)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{sub.end_date && (
|
{sub.end_date && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>End Date</label>
|
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>End Date</label>
|
||||||
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
{new Date(sub.end_date).toLocaleDateString()}
|
{formatDateDisplayValue(sub.end_date)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -118,7 +118,9 @@ const MembersDirectory = () => {
|
|||||||
: <div className=' border-2 w-full border-brand-purple mb-24' />
|
: <div className=' border-2 w-full border-brand-purple mb-24' />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const MemberCard = ({ member }) => (
|
const MemberCard = ({ member }) => {
|
||||||
|
const joinedDate = member.member_since || member.created_at;
|
||||||
|
return (
|
||||||
<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-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
@@ -160,11 +162,11 @@ const MembersDirectory = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Member Since */}
|
{/* Member Since */}
|
||||||
{member.created_at && (
|
{joinedDate && (
|
||||||
<div className="flex items-center justify-center gap-2 mb-4">
|
<div className="flex items-center justify-center gap-2 mb-4">
|
||||||
<Calendar className="h-4 w-4 text-brand-purple " />
|
<Calendar className="h-4 w-4 text-brand-purple " />
|
||||||
<span className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<span className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Member since {new Date(member.created_at).toLocaleDateString('en-US', {
|
Member since {new Date(joinedDate).toLocaleDateString('en-US', {
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric'
|
year: 'numeric'
|
||||||
})}
|
})}
|
||||||
@@ -277,6 +279,7 @@ const MembersDirectory = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)]">
|
<div className="min-h-screen bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)]">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
@apply hover:bg-brand-purple bg-brand-purple/10 rounded-full disabled:opacity-50 px-6 transition-transform text-brand-purple hover:text-[var(--purple-muted)];
|
@apply hover:bg-brand-purple bg-brand-purple/10 rounded-full disabled:opacity-50 px-6 transition-transform text-brand-purple hover:text-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
@@ -91,6 +91,4 @@
|
|||||||
.bg-light-lavender {
|
.bg-light-lavender {
|
||||||
@apply bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] text-white bg-[var(--neutral-800)] transition-transform dark:bg-brand-lavender dark:text-brand-dark-lavender;
|
@apply bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] text-white bg-[var(--neutral-800)] transition-transform dark:bg-brand-lavender dark:text-brand-dark-lavender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
--green-muted: #66927e;
|
--green-muted: #66927e;
|
||||||
--green-soft: #6a9680;
|
--green-soft: #6a9680;
|
||||||
--green-eucalyptus: #6a9a83;
|
--green-eucalyptus: #6a9a83;
|
||||||
--green-forest: #5e8EsOkvcwL7472374;
|
--green-forest: #5a7d68;
|
||||||
--green-fern: #6da085;
|
--green-fern: #6da085;
|
||||||
--green-mint: #6fa087;
|
--green-mint: #6fa087;
|
||||||
--green-pastel: #6fa188;
|
--green-pastel: #6fa188;
|
||||||
@@ -243,4 +243,3 @@
|
|||||||
--neutral-900: #f4f4ff; /* highest contrast text */
|
--neutral-900: #f4f4ff; /* highest contrast text */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user