performance improvments

This commit is contained in:
dimitar committed 2026-03-18 02:04:09 +01:00
1 parent 912981e3f3
commit 02c9681aca
13 files changed
+805 -474

No files matched your search

@@ -1,75 +1,34 @@
'use client'
"use client";
import { useState, useEffect } from 'react'
import { UserGrowthChart } from '@/components/charts/UserGrowthChart'
import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart'
import { RevenueChart } from '@/components/charts/RevenueChart'
import { Card, CardHeader, CardContent } from '@/components/ui/card'
interface ChartData {
label: string
value: number
color?: string
}
import { UserGrowthChart } from "@/components/charts/UserGrowthChart";
import { MembershipDistributionChart } from "@/components/charts/MembershipDistributionChart";
import { RevenueChart } from "@/components/charts/RevenueChart";
import { Card, CardHeader, CardContent } from "@/components/ui/card";
import { useAnalytics } from "@/hooks/use-api";
export function AnalyticsDashboard() {
const [userGrowthData, setUserGrowthData] = useState<ChartData[]>([])
const [membershipData, setMembershipData] = useState<ChartData[]>([])
const [revenueData, setRevenueData] = useState<ChartData[]>([])
const [loading, setLoading] = useState(true)
const { data: analytics, isLoading } = useAnalytics(6);
useEffect(() => {
fetchAnalyticsData()
}, [])
const userGrowthData = analytics?.userGrowth ?? [];
const membershipData = analytics?.membershipDistribution ?? [];
const revenueData = analytics?.revenue ?? [];
const fetchAnalyticsData = async () => {
setLoading(true)
try {
// Mock data for demonstration - replace with real API calls
const mockUserGrowth = [
{ label: 'Jan', value: 45 },
{ label: 'Feb', value: 52 },
{ label: 'Mar', value: 61 },
{ label: 'Apr', value: 58 },
{ label: 'May', value: 67 },
{ label: 'Jun', value: 74 },
]
const totalUsers =
userGrowthData.length > 0
? userGrowthData[userGrowthData.length - 1].value
: 0;
const totalRevenue = revenueData.reduce((sum, item) => sum + item.value, 0);
const activeMembers = membershipData.reduce(
(sum, item) => sum + item.value,
0,
);
const mockMembershipData = [
{ label: 'Basic', value: 45, color: '#6b7280' },
{ label: 'Premium', value: 28, color: '#3b82f6' },
{ label: 'VIP', value: 12, color: '#f59e0b' },
]
const mockRevenueData = [
{ label: 'Jan', value: 12500, color: '#10b981' },
{ label: 'Feb', value: 14200, color: '#10b981' },
{ label: 'Mar', value: 16800, color: '#10b981' },
{ label: 'Apr', value: 15900, color: '#10b981' },
{ label: 'May', value: 18200, color: '#10b981' },
{ label: 'Jun', value: 19400, color: '#10b981' },
]
setUserGrowthData(mockUserGrowth)
setMembershipData(mockMembershipData)
setRevenueData(mockRevenueData)
} catch (error) {
console.error('Failed to fetch analytics data:', error)
} finally {
setLoading(false)
}
}
const totalUsers = userGrowthData.length > 0 ? userGrowthData[userGrowthData.length - 1].value : 0
const totalRevenue = revenueData.reduce((sum, item) => sum + item.value, 0)
const activeMembers = membershipData.reduce((sum, item) => sum + item.value, 0)
if (loading) {
if (isLoading) {
return (
<div className="flex justify-center items-center h-64">
<div className="text-lg">Loading analytics...</div>
</div>
)
);
}
return (
@@ -81,7 +40,9 @@ export function AnalyticsDashboard() {
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">{totalUsers}</div>
<div className="text-3xl font-bold text-blue-600">
{totalUsers}
</div>
<div className="text-gray-600">Total Users</div>
</div>
</CardContent>
@@ -90,7 +51,9 @@ export function AnalyticsDashboard() {
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-green-600">${totalRevenue.toLocaleString()}</div>
<div className="text-3xl font-bold text-green-600">
${totalRevenue.toLocaleString()}
</div>
<div className="text-gray-600">Total Revenue</div>
</div>
</CardContent>
@@ -99,7 +62,9 @@ export function AnalyticsDashboard() {
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600">{activeMembers}</div>
<div className="text-3xl font-bold text-purple-600">
{activeMembers}
</div>
<div className="text-gray-600">Active Members</div>
</div>
</CardContent>
@@ -133,14 +98,14 @@ export function AnalyticsDashboard() {
</CardHeader>
<CardContent>
<RevenueChart
data={revenueData.map(item => ({
data={revenueData.map((item) => ({
category: item.label,
value: item.value,
color: item.color
color: item.color,
}))}
/>
</CardContent>
</Card>
</div>
)
);
}
@@ -0,0 +1,26 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";
interface QueryProviderProps {
children: ReactNode;
}
export function QueryProvider({ children }: QueryProviderProps) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
}),
);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
+2 -24
View File
@@ -13,40 +13,18 @@ import {
Brain,
} from "lucide-react";
import { UserButton, useUser } from "@clerk/nextjs";
interface Recommendation {
id: string;
status: string;
}
import { usePendingRecommendationsCount } from "@/hooks/use-api";
export function Sidebar() {
const pathname = usePathname();
const { user } = useUser();
const [pendingCount, setPendingCount] = useState(0);
const { data: pendingCount = 0 } = usePendingRecommendationsCount();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
const fetchPending = async () => {
try {
const res = await fetch("/api/recommendations");
if (res.ok) {
const data = await res.json();
const pending = (data.recommendations || []).filter(
(r: Recommendation) => r.status === "pending",
);
setPendingCount(pending.length);
}
} catch (e) {
console.error("Failed to fetch pending recommendations", e);
}
};
fetchPending();
}, []);
const menuItems = [
{ icon: LayoutDashboard, label: "Dashboard", href: "/" },
{ icon: Users, label: "Users", href: "/users" },
+57
View File
@@ -0,0 +1,57 @@
import { cn } from "@/lib/utils";
interface SkeletonProps {
className?: string;
}
export function Skeleton({ className }: SkeletonProps) {
return (
<div
className={cn(
"animate-pulse rounded-md bg-slate-200 dark:bg-slate-700",
className,
)}
/>
);
}
export function StatsCardSkeleton() {
return (
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-8 w-16" />
</div>
</div>
</div>
);
}
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
return (
<div className="space-y-3">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex gap-4">
<Skeleton className="h-12 flex-1" />
<Skeleton className="h-12 w-32" />
<Skeleton className="h-12 w-24" />
</div>
))}
</div>
);
}
export function CardSkeleton() {
return (
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6 space-y-4">
<Skeleton className="h-6 w-40" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-5/6" />
</div>
</div>
);
}
+11 -7
View File
@@ -9,9 +9,10 @@ import { formatDate } from "@/lib/utils";
ModuleRegistry.registerModules([AllCommunityModule]);
function getTimeAgo(date: Date): string {
function getTimeAgo(date: Date | string): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const d = typeof date === "string" ? new Date(date) : date;
const diffMs = now.getTime() - d.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
@@ -22,7 +23,7 @@ function getTimeAgo(date: Date): string {
return `${diffDays}d ago`;
}
interface User {
export interface User {
id: string;
email: string;
firstName: string;
@@ -31,15 +32,18 @@ interface User {
phone?: string;
gymId?: string;
gymName?: string | null;
createdAt: Date;
createdAt?: string | Date;
isCheckedIn?: boolean;
checkInTime?: Date;
checkInTime?: string | Date;
lastCheckInTime?: string | Date;
checkInsThisWeek?: number;
checkInsThisMonth?: number;
client?: {
id: string;
membershipType: string;
membershipStatus: string;
joinDate: Date;
lastVisit?: Date;
joinDate: string | Date;
lastVisit?: string | Date;
};
}
@@ -1,43 +1,23 @@
"use client";
import { useState, useEffect } from "react";
import { UserGrid } from "@/components/users/UserGrid";
// import { Button } from "@/components/ui/button";
import { useState } from "react";
import { UserGrid, type User } from "@/components/users/UserGrid";
import { Card, CardHeader, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useUser } from "@clerk/nextjs";
import { getGymIdFromUser } from "@/lib/error-helpers";
import log from "@/lib/logger";
import { toast } from "@/lib/toast";
import { CreateUserModal } from "./CreateUserModal";
interface User {
id: string;
email: string;
firstName: string;
lastName: string;
role: string;
phone?: string;
gymId?: string;
createdAt: Date;
isCheckedIn?: boolean;
checkInTime?: Date;
lastCheckInTime?: Date;
checkInsThisWeek?: number;
checkInsThisMonth?: number;
client?: {
id: string;
membershipType: string;
membershipStatus: string;
joinDate: Date;
lastVisit?: Date;
};
}
import {
useUsers,
useGyms,
useUpdateUser,
useDeleteUser,
useSendInvitation,
} from "@/hooks/use-api";
export function UserManagement() {
const { user } = useUser();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<string>("all");
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isEditing, setIsEditing] = useState(false);
@@ -52,68 +32,17 @@ export function UserManagement() {
gymId: string;
} | null>(null);
// Active gyms for dropdown
const [gyms, setGyms] = useState<Array<{ id: string; name: string }>>([]);
// Load gyms when modal opens or refreshes
useEffect(() => {
if (isEditing) {
(async () => {
try {
const res = await fetch("/api/gyms");
const data = await res.json();
if (Array.isArray(data)) {
// map down to id and name to avoid extra payload use here
setGyms(data.map((g: any) => ({ id: g.id, name: g.name })));
} else {
setGyms([]);
}
} catch {
setGyms([]);
}
})();
}
}, [isEditing]);
useEffect(() => {
fetchUsers();
}, [filter]);
const fetchUsers = async () => {
setLoading(true);
try {
const ts = Date.now();
const url =
filter === "all"
? `/api/users?ts=${ts}`
: `/api/users?role=${filter}&ts=${ts}`;
log.debug("Fetching users", { url });
const response = await fetch(url, { cache: "no-store" });
log.debug("Users fetch response", {
ok: response.ok,
status: response.status,
});
const responseData = await response.json();
const data = responseData.data || responseData; // Handle both old and new API response formats
log.debug("Received users data", {
count: Array.isArray(data.users) ? data.users.length : 0,
sample:
data.users && data.users[0]
? {
id: data.users[0].id,
gymId: data.users[0].gymId,
role: data.users[0].role,
}
: null,
});
setUsers(data.users || []);
} catch (error) {
log.error("Failed to fetch users", error);
} finally {
setLoading(false);
}
};
const {
data: users = [],
isLoading,
refetch,
} = useUsers({
role: filter !== "all" ? filter : undefined,
});
const { data: gyms = [] } = useGyms();
const updateUser = useUpdateUser();
const deleteUser = useDeleteUser();
const sendInvitation = useSendInvitation();
const handleUserSelect = (user: User | null) => {
setSelectedUser(user);
@@ -144,17 +73,12 @@ export function UserManagement() {
return;
try {
const response = await fetch("/api/users", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: users.map((u) => u.id) }),
});
if (response.ok) {
fetchUsers();
toast.success("Users deleted successfully");
} else {
toast.error("Error deleting users");
}
const deletePromises = users.map((u) =>
fetch(`/api/users?id=${u.id}`, { method: "DELETE" }),
);
await Promise.all(deletePromises);
refetch();
toast.success("Users deleted successfully");
} catch (error) {
log.error("Failed to delete users", error);
}
@@ -196,7 +120,7 @@ export function UserManagement() {
};
const handleRefresh = () => {
fetchUsers();
refetch();
};
const handleSaveEdit = async () => {
@@ -204,7 +128,6 @@ export function UserManagement() {
try {
if (selectedUser) {
// Update existing user
const payload = {
id: selectedUser.id,
email: editForm.email,
@@ -214,80 +137,21 @@ export function UserManagement() {
phone: editForm.phone,
gymId: editForm.gymId === "" ? null : editForm.gymId,
};
log.debug("Updating user", payload);
const response = await fetch("/api/users", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
log.debug("User update response", {
ok: response.ok,
status: response.status,
});
if (response.ok) {
// Optimistically update local state so grid reflects changes immediately
setUsers((prev) =>
prev.map((u) =>
u.id === selectedUser.id
? {
...u,
email: editForm.email,
firstName: editForm.firstName,
lastName: editForm.lastName,
role: editForm.role,
phone: editForm.phone || undefined,
gymId: editForm.gymId === "" ? undefined : editForm.gymId,
}
: u,
),
);
setSelectedUser((prev) =>
prev
? {
...prev,
email: editForm.email,
firstName: editForm.firstName,
lastName: editForm.lastName,
role: editForm.role,
phone: editForm.phone || undefined,
gymId: editForm.gymId === "" ? undefined : editForm.gymId,
}
: prev,
);
setIsEditing(false);
setEditForm(null);
// Still re-fetch from server to ensure consistency
log.debug("Re-fetching users after successful edit");
fetchUsers();
toast.success("User updated successfully");
} else {
const errText = await response.text().catch(() => "");
log.error("User update failed", new Error(errText), {
status: response.status,
});
toast.error("Error updating user");
}
} else {
// Create (Invite) new user
const response = await fetch("/api/invitations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
inviteeEmail: editForm.email,
roleAssigned: editForm.role,
gymId: editForm.gymId || undefined,
}),
});
if (response.ok) {
setIsEditing(false);
setEditForm(null);
fetchUsers();
toast.success("Invitation sent successfully!");
} else {
const errorData = await response.json();
toast.error(`Error sending invitation: ${errorData.error}`);
}
await updateUser.mutateAsync(payload);
setIsEditing(false);
setEditForm(null);
refetch();
toast.success("User updated successfully");
} else {
await sendInvitation.mutateAsync({
email: editForm.email,
role: editForm.role,
});
setIsEditing(false);
setEditForm(null);
refetch();
toast.success("Invitation sent successfully!");
}
} catch (error) {
console.error(error);
@@ -299,17 +163,11 @@ export function UserManagement() {
if (!selectedUser) return;
try {
const response = await fetch(`/api/users?id=${selectedUser.id}`, {
method: "DELETE",
});
if (response.ok) {
setIsDeleting(false);
setSelectedUser(null);
fetchUsers();
toast.success("User deleted successfully");
} else {
toast.error("Error deleting user");
}
await deleteUser.mutateAsync(selectedUser.id);
setIsDeleting(false);
setSelectedUser(null);
refetch();
toast.success("User deleted successfully");
} catch (error) {
log.error("Failed to delete user", error);
}
@@ -397,7 +255,7 @@ export function UserManagement() {
onEditUser={handleEditUser}
onDeleteUser={handleDeleteUser}
onBulkDelete={handleBulkDelete}
loading={loading}
loading={isLoading}
/>
</CardContent>
</Card>
@@ -591,7 +449,9 @@ export function UserManagement() {
</p>
<p>
<span className="font-medium">Joined:</span>{" "}
{new Date(selectedUser.createdAt).toLocaleDateString()}
{selectedUser.createdAt
? new Date(selectedUser.createdAt).toLocaleDateString()
: "N/A"}
</p>
</div>
</div>
@@ -653,7 +513,7 @@ export function UserManagement() {
<CreateUserModal
open={createModalOpen}
onOpenChange={setCreateModalOpen}
onSuccess={() => fetchUsers()}
onSuccess={() => refetch()}
/>
</div>
);