dev #19
@@ -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 && (
|
||||||
|
|||||||
@@ -385,7 +385,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 +411,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 +525,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,31 @@ 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 formatDateInputValue = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.slice(0, 10);
|
||||||
|
}
|
||||||
|
return new Date(value).toISOString().slice(0, 10);
|
||||||
|
};
|
||||||
|
|
||||||
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 +194,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 +250,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 +304,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 {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,6 +405,27 @@ const AdminUserView = () => {
|
|||||||
</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>
|
||||||
|
|||||||
@@ -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)]">
|
||||||
|
|||||||
Reference in New Issue
Block a user