dev #19
@@ -224,7 +224,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
${item.disabled
|
||||
? 'opacity-50 cursor-not-allowed text-brand-purple '
|
||||
: 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'
|
||||
}
|
||||
`}
|
||||
@@ -254,7 +254,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
|
||||
{/* Badge when collapsed */}
|
||||
{!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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -31,6 +31,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
const getTodayDate = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
@@ -84,8 +85,8 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
if (payload.date_of_birth === '') {
|
||||
delete payload.date_of_birth;
|
||||
}
|
||||
if (payload.member_since === '') {
|
||||
delete payload.member_since;
|
||||
if (!payload.member_since) {
|
||||
payload.member_since = getTodayDate();
|
||||
}
|
||||
|
||||
await api.post('/admin/users/create', payload);
|
||||
|
||||
@@ -22,10 +22,12 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
phone: '',
|
||||
member_since: '',
|
||||
role: 'admin'
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
const getTodayDate = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
@@ -74,7 +76,11 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
setLoading(true);
|
||||
|
||||
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');
|
||||
|
||||
// Reset form
|
||||
@@ -84,6 +90,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
phone: '',
|
||||
member_since: '',
|
||||
role: 'admin'
|
||||
});
|
||||
|
||||
@@ -200,6 +207,20 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
)}
|
||||
</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 */}
|
||||
<div className="grid gap-2">
|
||||
<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) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
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 [eventActivity, setEventActivity] = useState(null);
|
||||
const [activityLoading, setActivityLoading] = useState(true);
|
||||
const joinedDate = user?.member_since || user?.created_at;
|
||||
|
||||
useEffect(() => {
|
||||
fetchUpcomingEvents();
|
||||
@@ -197,7 +198,7 @@ const Dashboard = () => {
|
||||
<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" }}>
|
||||
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A'}
|
||||
{joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
{user?.subscription_start_date && user?.subscription_end_date && (
|
||||
|
||||
@@ -258,7 +258,7 @@ const Register = () => {
|
||||
<Button
|
||||
type="button"
|
||||
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
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
@@ -267,7 +267,7 @@ const Register = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{loading ? 'Creating Account...' : 'Create Account'}
|
||||
|
||||
@@ -4,13 +4,16 @@ import api from '../../utils/api';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
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 [stats, setStats] = useState({
|
||||
totalMembers: 0,
|
||||
pendingValidations: 0,
|
||||
activeMembers: 0
|
||||
activeMembers: 0,
|
||||
inactiveMembers: 0
|
||||
});
|
||||
const [usersNeedingAttention, setUsersNeedingAttention] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -29,7 +32,8 @@ const AdminDashboard = () => {
|
||||
pendingValidations: users.filter(u =>
|
||||
['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status)
|
||||
).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)
|
||||
@@ -76,52 +80,42 @@ const AdminDashboard = () => {
|
||||
</div>
|
||||
|
||||
{/* 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='flex items-start justify-between'>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{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>
|
||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
||||
</Card>
|
||||
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||
Quick Overview
|
||||
</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"
|
||||
/>
|
||||
|
||||
<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 */}
|
||||
|
||||
@@ -14,12 +14,13 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../../components/ui/dropdown-menu';
|
||||
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 ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
import CreateMemberDialog from '../../components/CreateMemberDialog';
|
||||
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
||||
import WordPressImportWizard from '../../components/WordPressImportWizard';
|
||||
import { StatCard } from '@/components/StatCard';
|
||||
|
||||
const AdminMembers = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -319,31 +320,45 @@ const AdminMembers = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<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" }}>Total Members</p>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.length}
|
||||
</p>
|
||||
</Card>
|
||||
<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" }}>Active</p>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'active').length}
|
||||
</p>
|
||||
</Card>
|
||||
<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>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'payment_pending').length}
|
||||
</p>
|
||||
</Card>
|
||||
<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" }}>Inactive</p>
|
||||
<p className="text-4xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'inactive').length}
|
||||
</p>
|
||||
</Card>
|
||||
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||
Quick Overview
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
|
||||
<StatCard
|
||||
title="Total Members"
|
||||
value={users.length}
|
||||
icon={Users}
|
||||
iconBgClass="bg-[var(--blue-light)] text-[var(--blue-dark)]"
|
||||
dataTestId="stat-total-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active"
|
||||
value={users.filter(u => u.status === 'active').length}
|
||||
icon={CheckCircle}
|
||||
iconBgClass="text-[var(--green-light)]"
|
||||
dataTestId="stat-active-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Payment Pending"
|
||||
value={users.filter(u => u.status === 'payment_pending').length}
|
||||
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>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -385,145 +400,148 @@ const AdminMembers = () => {
|
||||
</div>
|
||||
) : filteredUsers.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredUsers.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`member-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
{filteredUsers.map((user) => {
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
return (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`member-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] " style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
{getStatusBadge(user.status)}
|
||||
</div>
|
||||
<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>Phone: {user.phone}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
{user.referred_by_member_name && (
|
||||
<p>Referred by: {user.referred_by_member_name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reminder Info */}
|
||||
{(() => {
|
||||
const reminderInfo = getReminderInfo(user);
|
||||
if (reminderInfo.totalReminders > 0) {
|
||||
return (
|
||||
<div className="mt-4 p-3 bg-[var(--lavender-500)] rounded-lg border border-[var(--neutral-800)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="h-4 w-4 text-[var(--orange-light)]" />
|
||||
<span className="text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{reminderInfo.totalReminders} reminder{reminderInfo.totalReminders !== 1 ? 's' : ''} sent
|
||||
{reminderInfo.totalReminders >= 3 && (
|
||||
<Badge className="ml-2 bg-[var(--orange-light)] text-white px-2 py-0.5 rounded-full text-xs">
|
||||
Needs attention
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{reminderInfo.emailReminders > 0 && (
|
||||
<p>
|
||||
<Mail className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.emailReminders} email verification
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.eventReminders > 0 && (
|
||||
<p>
|
||||
<Calendar className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.eventReminders} event attendance
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.paymentReminders > 0 && (
|
||||
<p>
|
||||
<Clock className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.paymentReminders} payment
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.renewalReminders > 0 && (
|
||||
<p>
|
||||
<CheckCircle className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.renewalReminders} renewal
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{reminderInfo.lastReminderAt && (
|
||||
<p className="mt-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Last reminder: {new Date(reminderInfo.lastReminderAt).toLocaleDateString()} at {new Date(reminderInfo.lastReminderAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] " style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
{getStatusBadge(user.status)}
|
||||
</div>
|
||||
<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>Phone: {user.phone}</p>
|
||||
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
|
||||
{user.referred_by_member_name && (
|
||||
<p>Referred by: {user.referred_by_member_name}</p>
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Link to={`/admin/users/${user.id}`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className=""
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
View Profile
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{/* Show Activate Payment button for payment_pending users */}
|
||||
{user.status === 'payment_pending' && (
|
||||
<Button
|
||||
onClick={() => handleActivatePayment(user)}
|
||||
size="sm"
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Activate Payment
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reminder Info */}
|
||||
{(() => {
|
||||
const reminderInfo = getReminderInfo(user);
|
||||
if (reminderInfo.totalReminders > 0) {
|
||||
return (
|
||||
<div className="mt-4 p-3 bg-[var(--lavender-500)] rounded-lg border border-[var(--neutral-800)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="h-4 w-4 text-[var(--orange-light)]" />
|
||||
<span className="text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{reminderInfo.totalReminders} reminder{reminderInfo.totalReminders !== 1 ? 's' : ''} sent
|
||||
{reminderInfo.totalReminders >= 3 && (
|
||||
<Badge className="ml-2 bg-[var(--orange-light)] text-white px-2 py-0.5 rounded-full text-xs">
|
||||
Needs attention
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{reminderInfo.emailReminders > 0 && (
|
||||
<p>
|
||||
<Mail className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.emailReminders} email verification
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.eventReminders > 0 && (
|
||||
<p>
|
||||
<Calendar className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.eventReminders} event attendance
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.paymentReminders > 0 && (
|
||||
<p>
|
||||
<Clock className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.paymentReminders} payment
|
||||
</p>
|
||||
)}
|
||||
{reminderInfo.renewalReminders > 0 && (
|
||||
<p>
|
||||
<CheckCircle className="inline h-3 w-3 mr-1" />
|
||||
{reminderInfo.renewalReminders} renewal
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{reminderInfo.lastReminderAt && (
|
||||
<p className="mt-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Last reminder: {new Date(reminderInfo.lastReminderAt).toLocaleDateString()} at {new Date(reminderInfo.lastReminderAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{/* Status Management */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-brand-purple dark:text-brand-lavender whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Change Status:
|
||||
</span>
|
||||
<Select
|
||||
value={user.status}
|
||||
onValueChange={(newStatus) => handleStatusChangeRequest(user.id, user.status, newStatus, user)}
|
||||
disabled={statusChanging === user.id}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-9 border-[var(--neutral-800)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="payment_pending">Payment Pending</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
<SelectItem value="canceled">Canceled</SelectItem>
|
||||
<SelectItem value="expired">Expired</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Link to={`/admin/users/${user.id}`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className=""
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
View Profile
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{/* Show Activate Payment button for payment_pending users */}
|
||||
{user.status === 'payment_pending' && (
|
||||
<Button
|
||||
onClick={() => handleActivatePayment(user)}
|
||||
size="sm"
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Activate Payment
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Management */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-brand-purple dark:text-brand-lavender whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Change Status:
|
||||
</span>
|
||||
<Select
|
||||
value={user.status}
|
||||
onValueChange={(newStatus) => handleStatusChangeRequest(user.id, user.status, newStatus, user)}
|
||||
disabled={statusChanging === user.id}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-9 border-[var(--neutral-800)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="payment_pending">Payment Pending</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
<SelectItem value="canceled">Canceled</SelectItem>
|
||||
<SelectItem value="expired">Expired</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
|
||||
@@ -242,12 +242,14 @@ const AdminStaff = () => {
|
||||
</div>
|
||||
) : filteredUsers.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredUsers.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`staff-card-${user.id}`}
|
||||
>
|
||||
{filteredUsers.map((user) => {
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
return (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`staff-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
@@ -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" }}>
|
||||
<p>Email: {user.email}</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 && (
|
||||
<p>Last Login: {new Date(user.last_login).toLocaleDateString()}</p>
|
||||
)}
|
||||
@@ -321,8 +323,9 @@ const AdminStaff = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
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 { toast } from 'sonner';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
@@ -24,15 +25,58 @@ const AdminUserView = () => {
|
||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
|
||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
|
||||
const [memberSince, setMemberSince] = useState('');
|
||||
const [memberSinceSaving, setMemberSinceSaving] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
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(() => {
|
||||
fetchConfig();
|
||||
fetchUserProfile();
|
||||
fetchSubscriptions();
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setMemberSince(formatDateInputValue(user.member_since));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
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 = () => {
|
||||
if (!pendingAction || !user) return {};
|
||||
|
||||
@@ -212,6 +277,10 @@ const AdminUserView = () => {
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (!user) return null;
|
||||
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
const memberSinceBaseline = formatDateInputValue(user.member_since);
|
||||
const memberSinceHasChanges = memberSince !== memberSinceBaseline;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Back Button */}
|
||||
@@ -262,7 +331,7 @@ const AdminUserView = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Joined {new Date(user.created_at).toLocaleDateString()}</span>
|
||||
<span>Joined {formatDateDisplayValue(joinedDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,10 +428,31 @@ const AdminUserView = () => {
|
||||
<div>
|
||||
<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" }}>
|
||||
{new Date(user.date_of_birth).toLocaleDateString()}
|
||||
{formatDateDisplayValue(user.date_of_birth)}
|
||||
</p>
|
||||
</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 && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Partner</label>
|
||||
@@ -429,14 +519,14 @@ const AdminUserView = () => {
|
||||
<div>
|
||||
<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" }}>
|
||||
{new Date(sub.start_date).toLocaleDateString()}
|
||||
{formatDateDisplayValue(sub.start_date)}
|
||||
</p>
|
||||
</div>
|
||||
{sub.end_date && (
|
||||
<div>
|
||||
<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" }}>
|
||||
{new Date(sub.end_date).toLocaleDateString()}
|
||||
{formatDateDisplayValue(sub.end_date)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -118,8 +118,10 @@ const MembersDirectory = () => {
|
||||
: <div className=' border-2 w-full border-brand-purple mb-24' />
|
||||
)
|
||||
}
|
||||
const MemberCard = ({ member }) => (
|
||||
<Card className="p-6 bg-background rounded-3xl border border-[var(--neutral-800)] hover:shadow-lg transition-all h-full">
|
||||
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">
|
||||
{/* Profile Photo */}
|
||||
<div className="flex justify-center mb-4">
|
||||
{member.profile_photo_url ? (
|
||||
@@ -160,11 +162,11 @@ const MembersDirectory = () => {
|
||||
)}
|
||||
|
||||
{/* Member Since */}
|
||||
{member.created_at && (
|
||||
{joinedDate && (
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
<Calendar className="h-4 w-4 text-brand-purple " />
|
||||
<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',
|
||||
year: 'numeric'
|
||||
})}
|
||||
@@ -276,7 +278,8 @@ const MembersDirectory = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)]">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -56,10 +56,10 @@
|
||||
@apply h-9 w-9 rounded-full disabled:opacity-50 px-6;
|
||||
}
|
||||
.btn-green {
|
||||
@apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-full px-6 ;
|
||||
@apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-full px-6;
|
||||
}
|
||||
.btn-util-green {
|
||||
@apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-xl h-12 px-6 ;
|
||||
@apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-xl h-12 px-6;
|
||||
}
|
||||
|
||||
.btn-util-purple {
|
||||
@@ -76,21 +76,19 @@
|
||||
@apply bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2 dark:hover:bg-foreground dark:hover:text-background;
|
||||
}
|
||||
.btn-light-lavender {
|
||||
@apply bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-lavender rounded-full px-6 transition-transform dark:hover:bg-brand-lavender dark:hover:text-brand-dark-lavender ;
|
||||
@apply bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-lavender rounded-full px-6 transition-transform dark:hover:bg-brand-lavender dark:hover:text-brand-dark-lavender;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
|
||||
|
||||
.badge {
|
||||
@apply px-3 py-1 rounded-full text-sm font-medium transition-transform;
|
||||
}
|
||||
.badge-green {
|
||||
@apply bg-[var(--green-light)] text-white transition-transform;
|
||||
}
|
||||
/* Backgrounds */
|
||||
.bg-light-lavender {
|
||||
/* Backgrounds */
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
--green-muted: #66927e;
|
||||
--green-soft: #6a9680;
|
||||
--green-eucalyptus: #6a9a83;
|
||||
--green-forest: #5e8EsOkvcwL7472374;
|
||||
--green-forest: #5a7d68;
|
||||
--green-fern: #6da085;
|
||||
--green-mint: #6fa087;
|
||||
--green-pastel: #6fa188;
|
||||
@@ -243,4 +243,3 @@
|
||||
--neutral-900: #f4f4ff; /* highest contrast text */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user