Compare commits
1
Commits
feature/ui
..
ace1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7427c97c9c |
No files matched your search
@@ -0,0 +1,47 @@
|
|||||||
|
<svg viewBox="0 0 600 700" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#1e40af;stop-opacity:1" />
|
||||||
|
<stop offset="50%" style="stop-color:#0369a1;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Main rounded shape background -->
|
||||||
|
<path d="M 150 80 Q 350 50 450 150 L 480 300 Q 500 450 380 550 Q 200 650 100 550 Q 30 450 50 300 L 80 150 Q 100 80 150 80 Z" fill="url(#logoGrad)"/>
|
||||||
|
|
||||||
|
<!-- Arrow pointing up-right (top right) -->
|
||||||
|
<g transform="translate(340, 100)">
|
||||||
|
<path d="M 0 80 L 0 0 M 0 0 L -25 25 M 0 0 L 25 25" stroke="white" stroke-width="20" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Hand holding dumbbell (left side) -->
|
||||||
|
<g transform="translate(80, 180)">
|
||||||
|
<!-- Upper arm -->
|
||||||
|
<ellipse cx="40" cy="30" rx="25" ry="60" fill="white" opacity="0.95" transform="rotate(-30 40 30)"/>
|
||||||
|
|
||||||
|
<!-- Forearm and hand -->
|
||||||
|
<path d="M 60 80 Q 100 100 140 85" stroke="white" stroke-width="35" fill="none" stroke-linecap="round" opacity="0.95"/>
|
||||||
|
|
||||||
|
<!-- Fingers -->
|
||||||
|
<circle cx="145" cy="75" r="15" fill="white" opacity="0.95"/>
|
||||||
|
<circle cx="155" cy="90" r="12" fill="white" opacity="0.95"/>
|
||||||
|
<circle cx="150" cy="105" r="13" fill="white" opacity="0.95"/>
|
||||||
|
|
||||||
|
<!-- Dumbbell bar -->
|
||||||
|
<rect x="155" y="75" width="110" height="28" rx="14" fill="white" opacity="0.95"/>
|
||||||
|
|
||||||
|
<!-- Left weight plate -->
|
||||||
|
<circle cx="155" cy="89" r="45" fill="white" opacity="0.95"/>
|
||||||
|
<circle cx="155" cy="89" r="38" fill="url(#logoGrad)" opacity="0.4"/>
|
||||||
|
<rect x="145" y="130" width="20" height="30" fill="white" opacity="0.95" rx="3"/>
|
||||||
|
|
||||||
|
<!-- Right weight plate -->
|
||||||
|
<circle cx="265" cy="89" r="45" fill="white" opacity="0.95"/>
|
||||||
|
<circle cx="265" cy="89" r="38" fill="url(#logoGrad)" opacity="0.4"/>
|
||||||
|
<rect x="255" y="130" width="20" height="30" fill="white" opacity="0.95" rx="3"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Text at bottom -->
|
||||||
|
<text x="300" y="630" font-size="85" font-weight="900" fill="url(#logoGrad)" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" letter-spacing="2">NextForm</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,35 @@
|
|||||||
|
<svg viewBox="0 0 600 700" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Main Shape Background -->
|
||||||
|
<path d="M 150 150 L 350 150 L 450 250 L 450 450 L 350 550 L 150 550 L 50 450 L 50 250 Z" fill="url(logoGradient)" opacity="0.15"/>
|
||||||
|
|
||||||
|
<!-- Dumbbell Icon -->
|
||||||
|
<g transform="translate(200, 200)">
|
||||||
|
<!-- Left Weight -->
|
||||||
|
<circle cx="20" cy="60" r="35" fill="url(logoGradient)"/>
|
||||||
|
<rect x="10" y="70" width="20" height="80" fill="url(logoGradient)"/>
|
||||||
|
|
||||||
|
<!-- Bar -->
|
||||||
|
<rect x="35" y="100" width="130" height="25" rx="12" fill="url(logoGradient)"/>
|
||||||
|
|
||||||
|
<!-- Right Weight -->
|
||||||
|
<circle cx="180" cy="60" r="35" fill="url(logoGradient)"/>
|
||||||
|
<rect x="170" y="70" width="20" height="80" fill="url(logoGradient)"/>
|
||||||
|
|
||||||
|
<!-- Hand -->
|
||||||
|
<ellipse cx="80" cy="20" rx="25" ry="30" fill="url(logoGradient)"/>
|
||||||
|
<path d="M 70 0 Q 65 -15 60 -20" stroke="url(logoGradient)" stroke-width="8" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M 85 -5 Q 88 -20 90 -28" stroke="url(logoGradient)" stroke-width="8" fill="none" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Arrow/Growth -->
|
||||||
|
<g transform="translate(320, 150)">
|
||||||
|
<path d="M 20 100 L 80 40 L 80 70 L 120 30 L 80 30 L 80 0 Z" fill="url(logoGradient)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -26,7 +26,7 @@ export default function RootLayout({
|
|||||||
<ClerkProvider>
|
<ClerkProvider>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<div className="flex min-h-screen bg-gradient-to-br from-white via-blue-50 to-slate-50">
|
<div className="flex min-h-screen bg-slate-50">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 ml-64 p-8">
|
<main className="flex-1 ml-64 p-8">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
+28
-41
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Users, Activity, Zap, TrendingUp, Trophy, Target } from "lucide-react";
|
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
|
||||||
import { StatsCard } from "@/components/ui/StatsCard";
|
import { StatsCard } from "@/components/ui/StatsCard";
|
||||||
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
|
||||||
import { UserManagement } from "@/components/users/UserManagement";
|
import { UserManagement } from "@/components/users/UserManagement";
|
||||||
|
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
interface DashboardStats {
|
interface DashboardStats {
|
||||||
@@ -46,39 +46,27 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-white via-slate-50 to-blue-50">
|
<div className="space-y-8">
|
||||||
{/* Premium Header */}
|
{/* Header with Logo and App Name */}
|
||||||
<div className="bg-gradient-to-r from-slate-900 via-blue-900 to-slate-900 shadow-2xl">
|
<div className="flex items-center gap-4 pb-6 border-b border-slate-200">
|
||||||
<div className="max-w-7xl mx-auto px-8 py-12">
|
<div className="w-20 h-20">
|
||||||
<div className="flex items-end justify-between">
|
<img src="/nextform-logo.png" alt="NextForm Logo" className="w-full h-full object-contain" />
|
||||||
<div className="flex items-end gap-6">
|
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-400 to-cyan-400 rounded-3xl flex items-center justify-center shadow-lg">
|
|
||||||
<Trophy className="w-9 h-9 text-white" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-5xl font-black text-white tracking-tight">FitAI</h1>
|
<h1 className="text-4xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
|
||||||
<p className="text-blue-200 text-sm font-semibold mt-2">Premium Sports Management Platform</p>
|
NextForm
|
||||||
</div>
|
</h1>
|
||||||
</div>
|
<p className="text-sm text-slate-500 font-medium">Fitness Management Platform</p>
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-blue-100 text-sm">Welcome</p>
|
|
||||||
<p className="text-2xl font-bold text-white">Admin Control</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content with Proper Spacing */}
|
{/* Performance Metrics Section */}
|
||||||
<div className="max-w-7xl mx-auto px-8 py-16">
|
|
||||||
|
|
||||||
{/* Section: Performance Metrics */}
|
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<div className="mb-10">
|
<div className="mb-8">
|
||||||
<h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2>
|
<h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2>
|
||||||
<p className="text-slate-600 mt-2 font-medium">Real-time insights into your platform</p>
|
<p className="text-slate-600 mt-2 font-medium">Track key indicators and member activity</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Total Members"
|
title="Total Members"
|
||||||
value={loading ? "..." : stats.totalUsers}
|
value={loading ? "..." : stats.totalUsers}
|
||||||
@@ -90,17 +78,17 @@ export default function Home() {
|
|||||||
<StatsCard
|
<StatsCard
|
||||||
title="Active Athletes"
|
title="Active Athletes"
|
||||||
value={loading ? "..." : stats.activeClients}
|
value={loading ? "..." : stats.activeClients}
|
||||||
change="+8% this week"
|
change="+5% vs last week"
|
||||||
trend="up"
|
trend="up"
|
||||||
icon={Activity}
|
icon={CalendarCheck}
|
||||||
color="green"
|
color="emerald"
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Revenue"
|
title="Revenue"
|
||||||
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
|
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
|
||||||
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
|
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
|
||||||
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
|
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
|
||||||
icon={Zap}
|
icon={CreditCard}
|
||||||
color="amber"
|
color="amber"
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
@@ -114,7 +102,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section: Member Management */}
|
{/* Members Directory Section */}
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="text-3xl font-black text-slate-900">Members Directory</h2>
|
<h2 className="text-3xl font-black text-slate-900">Members Directory</h2>
|
||||||
@@ -125,7 +113,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section: Key Insights */}
|
{/* Key Insights Section */}
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="text-3xl font-black text-slate-900">Key Insights</h2>
|
<h2 className="text-3xl font-black text-slate-900">Key Insights</h2>
|
||||||
@@ -135,31 +123,31 @@ export default function Home() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
|
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
|
||||||
<div className="p-6 bg-gradient-to-br from-blue-50 via-blue-100 to-blue-50 rounded-2xl border border-blue-200 hover:shadow-md transition-shadow">
|
<div className="p-6 bg-gradient-to-br from-blue-50 via-blue-100 to-blue-50 rounded-2xl border border-blue-200 hover:shadow-md transition-shadow">
|
||||||
<p className="text-xs text-blue-700 uppercase font-black tracking-wider">Members</p>
|
<p className="text-xs text-blue-700 uppercase font-black tracking-wider">Active Subscriptions</p>
|
||||||
<p className="text-3xl font-black text-blue-900 mt-4 truncate">{loading ? "..." : stats.totalUsers}</p>
|
<p className="text-3xl font-black text-blue-900 mt-4 truncate">{loading ? "..." : stats.totalUsers}</p>
|
||||||
<p className="text-sm text-blue-700 mt-2 font-semibold">Active subscriptions</p>
|
<p className="text-sm text-blue-700 mt-2 font-semibold">Premium members active</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
|
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
|
||||||
<div className="p-6 bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-50 rounded-2xl border border-emerald-200 hover:shadow-md transition-shadow">
|
<div className="p-6 bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-50 rounded-2xl border border-emerald-200 hover:shadow-md transition-shadow">
|
||||||
<p className="text-xs text-emerald-700 uppercase font-black tracking-wider">Training Now</p>
|
<p className="text-xs text-emerald-700 uppercase font-black tracking-wider">Training Activity</p>
|
||||||
<p className="text-3xl font-black text-emerald-900 mt-4 truncate">{loading ? "..." : stats.activeClients}</p>
|
<p className="text-3xl font-black text-emerald-900 mt-4 truncate">{loading ? "..." : stats.activeClients}</p>
|
||||||
<p className="text-sm text-emerald-700 mt-2 font-semibold">Currently active</p>
|
<p className="text-sm text-emerald-700 mt-2 font-semibold">Currently training</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
|
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
|
||||||
<div className="p-6 bg-gradient-to-br from-amber-50 via-amber-100 to-amber-50 rounded-2xl border border-amber-200 hover:shadow-md transition-shadow">
|
<div className="p-6 bg-gradient-to-br from-amber-50 via-amber-100 to-amber-50 rounded-2xl border border-amber-200 hover:shadow-md transition-shadow">
|
||||||
<p className="text-xs text-amber-700 uppercase font-black tracking-wider">Revenue</p>
|
<p className="text-xs text-amber-700 uppercase font-black tracking-wider">Total Revenue</p>
|
||||||
<p className="text-2xl font-black text-amber-900 mt-4 truncate">{loading ? "..." : formatCurrency(stats.totalRevenue)}</p>
|
<p className="text-2xl font-black text-amber-900 mt-4 truncate">{loading ? "..." : formatCurrency(stats.totalRevenue)}</p>
|
||||||
<p className="text-sm text-amber-700 mt-2 font-semibold">This period</p>
|
<p className="text-sm text-amber-700 mt-2 font-semibold">This period earnings</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section: Detailed Report - Full Width */}
|
{/* Detailed Report Section */}
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="text-3xl font-black text-slate-900">Detailed Report</h2>
|
<h2 className="text-3xl font-black text-slate-900">Detailed Report</h2>
|
||||||
@@ -172,6 +160,5 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { UserGrowthChart } from '@/components/charts/UserGrowthChart'
|
import { UserGrowthChart } from '@/components/charts/UserGrowthChart'
|
||||||
import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart'
|
import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart'
|
||||||
import { RevenueChart } from '@/components/charts/RevenueChart'
|
import { RevenueChart } from '@/components/charts/RevenueChart'
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/card'
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card'
|
||||||
|
|
||||||
interface ChartData {
|
interface ChartData {
|
||||||
label: string
|
label: string
|
||||||
|
|||||||
@@ -65,15 +65,20 @@ export function Sidebar() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-64 bg-gradient-to-b from-white to-slate-50 h-screen fixed left-0 top-0 flex flex-col border-r border-slate-200/50">
|
<aside className="w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800">
|
||||||
<div className="p-6 border-b border-slate-200">
|
<div className="p-6 border-b border-slate-800">
|
||||||
<h1 className="text-2xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
FitAI
|
<div className="w-12 h-12">
|
||||||
|
<img src="/nextform-logo.png" alt="NextForm" className="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-black bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
|
||||||
|
NextForm
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-slate-600 font-medium mt-1">Sports Admin</p>
|
</div>
|
||||||
|
<p className="text-xs text-slate-400">Fitness Admin</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 p-4 space-y-1">
|
<nav className="flex-1 p-4 space-y-2">
|
||||||
{menuItems.map((item) => {
|
{menuItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = pathname === item.href;
|
const isActive = pathname === item.href;
|
||||||
@@ -82,25 +87,25 @@ export function Sidebar() {
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-200 group font-medium ${isActive
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 group ${isActive
|
||||||
? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-md shadow-blue-500/20"
|
? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
|
||||||
: "text-slate-700 hover:bg-slate-100 hover:text-blue-600"}`}
|
: "text-slate-400 hover:bg-slate-800 hover:text-white"}`}
|
||||||
>
|
>
|
||||||
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-blue-600"} />
|
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} />
|
||||||
<span className="text-sm">{label}</span>
|
<span className="font-medium">{label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="p-4 border-t border-slate-200">
|
<div className="p-4 border-t border-slate-800">
|
||||||
<div className="flex items-center gap-3 px-4 py-3 rounded-2xl bg-slate-100">
|
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-slate-800/50">
|
||||||
<UserButton afterSignOutUrl="/" />
|
<UserButton afterSignOutUrl="/" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-semibold text-slate-900 truncate">
|
<p className="text-sm font-medium text-white truncate">
|
||||||
{user?.fullName || "Admin User"}
|
{user?.fullName || "Admin User"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-600 truncate">
|
<p className="text-xs text-slate-400 truncate">
|
||||||
{user?.primaryEmailAddress?.emailAddress}
|
{user?.primaryEmailAddress?.emailAddress}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,67 +6,61 @@ interface StatsCardProps {
|
|||||||
change?: string;
|
change?: string;
|
||||||
trend?: "up" | "down" | "neutral";
|
trend?: "up" | "down" | "neutral";
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
color?: "blue" | "green" | "amber" | "cyan";
|
color?: "blue" | "emerald" | "amber" | "cyan";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
|
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
|
||||||
const colorStyles = {
|
const colorStyles = {
|
||||||
blue: {
|
blue: {
|
||||||
bg: "from-blue-50 via-blue-100 to-blue-50",
|
bg: "from-blue-50 via-blue-100 to-blue-50",
|
||||||
text: "text-blue-950",
|
|
||||||
icon: "bg-gradient-to-br from-blue-500 to-blue-600",
|
|
||||||
border: "border-blue-200",
|
border: "border-blue-200",
|
||||||
label: "text-blue-700"
|
icon: "text-blue-700",
|
||||||
|
label: "text-blue-700",
|
||||||
|
value: "text-blue-900"
|
||||||
},
|
},
|
||||||
green: {
|
emerald: {
|
||||||
bg: "from-emerald-50 via-emerald-100 to-emerald-50",
|
bg: "from-emerald-50 via-emerald-100 to-emerald-50",
|
||||||
text: "text-emerald-950",
|
|
||||||
icon: "bg-gradient-to-br from-emerald-500 to-emerald-600",
|
|
||||||
border: "border-emerald-200",
|
border: "border-emerald-200",
|
||||||
label: "text-emerald-700"
|
icon: "text-emerald-700",
|
||||||
|
label: "text-emerald-700",
|
||||||
|
value: "text-emerald-900"
|
||||||
},
|
},
|
||||||
amber: {
|
amber: {
|
||||||
bg: "from-amber-50 via-amber-100 to-amber-50",
|
bg: "from-amber-50 via-amber-100 to-amber-50",
|
||||||
text: "text-amber-950",
|
|
||||||
icon: "bg-gradient-to-br from-amber-500 to-amber-600",
|
|
||||||
border: "border-amber-200",
|
border: "border-amber-200",
|
||||||
label: "text-amber-700"
|
icon: "text-amber-700",
|
||||||
|
label: "text-amber-700",
|
||||||
|
value: "text-amber-900"
|
||||||
},
|
},
|
||||||
cyan: {
|
cyan: {
|
||||||
bg: "from-cyan-50 via-cyan-100 to-cyan-50",
|
bg: "from-cyan-50 via-cyan-100 to-cyan-50",
|
||||||
text: "text-cyan-950",
|
|
||||||
icon: "bg-gradient-to-br from-cyan-500 to-cyan-600",
|
|
||||||
border: "border-cyan-200",
|
border: "border-cyan-200",
|
||||||
label: "text-cyan-700"
|
icon: "text-cyan-700",
|
||||||
},
|
label: "text-cyan-700",
|
||||||
|
value: "text-cyan-900"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const style = colorStyles[color];
|
const styles = colorStyles[color];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`bg-gradient-to-br ${style.bg} rounded-3xl border ${style.border} p-6 hover:shadow-xl transition-all duration-300 cursor-default group`}>
|
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8 hover:shadow-xl transition-shadow">
|
||||||
<div className="flex items-start justify-between mb-4 gap-3">
|
<div className={`p-6 bg-gradient-to-br ${styles.bg} rounded-2xl border ${styles.border} hover:shadow-md transition-shadow`}>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex items-start justify-between mb-4">
|
||||||
<p className={`text-xs ${style.label} uppercase font-black tracking-widest truncate`}>
|
<div>
|
||||||
{title}
|
<p className={`text-xs ${styles.label} uppercase font-black tracking-wider`}>{title}</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`p-3 ${style.icon} rounded-2xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform flex-shrink-0`}>
|
<div className={`${styles.icon} opacity-60`}>
|
||||||
<Icon size={20} className="text-white" />
|
<Icon size={24} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className={`text-4xl font-black ${styles.value} mt-4 truncate`}>{value}</p>
|
||||||
<div className={`text-4xl font-black ${style.text} mb-3 truncate`}>
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{change && (
|
{change && (
|
||||||
<div className="flex items-center gap-1 truncate">
|
<p className={`text-sm ${styles.label} mt-3 font-semibold`}>
|
||||||
<span className={`text-xs font-black uppercase tracking-wide ${style.label} truncate`}>
|
{change}
|
||||||
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
|
</p>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,56 @@
|
|||||||
import React from 'react'
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
import { cn } from "@/lib/utils";
|
||||||
variant?: 'primary' | 'secondary'
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Button({ variant = 'primary', children, className = '', ...props }: ButtonProps) {
|
const buttonVariants = cva(
|
||||||
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors'
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
const variantClasses = {
|
{
|
||||||
primary: 'bg-blue-600 text-white hover:bg-blue-700',
|
variants: {
|
||||||
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
return (
|
return (
|
||||||
<button
|
<Comp
|
||||||
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
/>
|
||||||
{children}
|
);
|
||||||
</button>
|
},
|
||||||
)
|
);
|
||||||
}
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
@@ -1,38 +1,76 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
|
|
||||||
interface CardProps {
|
import { cn } from "@/lib/utils"
|
||||||
children: React.ReactNode
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Card({ children, className = '' }: CardProps) {
|
const Card = React.forwardRef<
|
||||||
return (
|
HTMLDivElement,
|
||||||
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
{children}
|
>(({ className, ...props }, ref) => (
|
||||||
</div>
|
<div
|
||||||
)
|
ref={ref}
|
||||||
}
|
className={cn(
|
||||||
|
"rounded-xl border bg-card text-card-foreground shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
export function CardHeader({ children, className = '' }: CardProps) {
|
const CardHeader = React.forwardRef<
|
||||||
return (
|
HTMLDivElement,
|
||||||
<div className={`mb-4 ${className}`}>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
{children}
|
>(({ className, ...props }, ref) => (
|
||||||
</div>
|
<div
|
||||||
)
|
ref={ref}
|
||||||
}
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
export function CardContent({ children, className = '' }: CardProps) {
|
const CardTitle = React.forwardRef<
|
||||||
return (
|
HTMLParagraphElement,
|
||||||
<div className={className}>
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
{children}
|
>(({ className, ...props }, ref) => (
|
||||||
</div>
|
<h3
|
||||||
)
|
ref={ref}
|
||||||
}
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
export function CardTitle({ children, className = '' }: CardProps) {
|
const CardDescription = React.forwardRef<
|
||||||
return (
|
HTMLParagraphElement,
|
||||||
<div className={`text-sm font-medium ${className}`}>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
{children}
|
>(({ className, ...props }, ref) => (
|
||||||
</div>
|
<p
|
||||||
)
|
ref={ref}
|
||||||
}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||||
|
|
||||||
interface Recommendation {
|
interface Recommendation {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { UserGrid } from "@/components/users/UserGrid";
|
import { UserGrid } from "@/components/users/UserGrid";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -34,7 +34,6 @@ export function UserManagement() {
|
|||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<"users" | "details">("users");
|
|
||||||
const [editForm, setEditForm] = useState<{
|
const [editForm, setEditForm] = useState<{
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
@@ -206,83 +205,25 @@ export function UserManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
{/* Header with Count */}
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center justify-between">
|
<h2 className="text-2xl font-bold">User Management</h2>
|
||||||
<h3 className="text-lg font-black text-slate-900">
|
<div className="flex gap-2">
|
||||||
{users.length} {users.length === 1 ? 'user' : 'users'}
|
<Button
|
||||||
</h3>
|
variant={filter === "all" ? "primary" : "secondary"}
|
||||||
{selectedUser && (
|
|
||||||
<p className="text-sm text-blue-600 font-semibold">
|
|
||||||
Selected: {selectedUser.firstName} {selectedUser.lastName}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compact Controls - Always Visible but Organized */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-4 bg-gradient-to-r from-slate-50 to-blue-50 rounded-lg border border-slate-200">
|
|
||||||
{/* Filter Row */}
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Roles</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setFilter("all")}
|
onClick={() => setFilter("all")}
|
||||||
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
|
|
||||||
filter === "all"
|
|
||||||
? "bg-blue-500 text-white shadow-md"
|
|
||||||
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
All
|
All Users
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setFilter("client")}
|
variant="secondary"
|
||||||
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
|
onClick={() => selectedUser && handleEditUser(selectedUser)}
|
||||||
filter === "client"
|
disabled={!selectedUser}
|
||||||
? "bg-blue-500 text-white shadow-md"
|
|
||||||
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Clients
|
Edit User
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setFilter("trainer")}
|
variant="secondary"
|
||||||
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
|
|
||||||
filter === "trainer"
|
|
||||||
? "bg-blue-500 text-white shadow-md"
|
|
||||||
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Trainers
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilter("admin")}
|
|
||||||
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
|
|
||||||
filter === "admin"
|
|
||||||
? "bg-blue-500 text-white shadow-md"
|
|
||||||
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Admins
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilter("superAdmin")}
|
|
||||||
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
|
|
||||||
filter === "superAdmin"
|
|
||||||
? "bg-blue-500 text-white shadow-md"
|
|
||||||
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Super Admins
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons Row */}
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Quick Actions</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditForm({
|
setEditForm({
|
||||||
firstName: "",
|
firstName: "",
|
||||||
@@ -294,69 +235,67 @@ export function UserManagement() {
|
|||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}}
|
}}
|
||||||
className="px-2 py-1 text-xs font-semibold rounded-lg bg-blue-500 text-white hover:bg-blue-600"
|
|
||||||
>
|
>
|
||||||
+ Invite
|
Invite User
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => selectedUser && handleEditUser(selectedUser)}
|
variant="secondary"
|
||||||
disabled={!selectedUser}
|
|
||||||
className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
|
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
|
||||||
disabled={!selectedUser}
|
disabled={!selectedUser}
|
||||||
className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200 disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
Delete
|
Delete User
|
||||||
</button>
|
</Button>
|
||||||
<button onClick={handleRefresh} className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200">
|
<Button
|
||||||
|
variant={filter === "client" ? "primary" : "secondary"}
|
||||||
|
onClick={() => setFilter("client")}
|
||||||
|
>
|
||||||
|
Clients
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={filter === "trainer" ? "primary" : "secondary"}
|
||||||
|
onClick={() => setFilter("trainer")}
|
||||||
|
>
|
||||||
|
Trainers
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={filter === "admin" ? "primary" : "secondary"}
|
||||||
|
onClick={() => setFilter("admin")}
|
||||||
|
>
|
||||||
|
Admins
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={filter === "superAdmin" ? "primary" : "secondary"}
|
||||||
|
onClick={() => setFilter("superAdmin")}
|
||||||
|
>
|
||||||
|
Super Admins
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
Showing {users.length} users
|
||||||
|
{selectedUser && (
|
||||||
|
<span className="ml-4 text-blue-600">
|
||||||
|
Selected: {selectedUser.firstName} {selectedUser.lastName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="secondary" onClick={handleRefresh}>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</Button>
|
||||||
<button onClick={handleExport} className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200">
|
<Button variant="secondary" onClick={handleExport}>
|
||||||
Export
|
Export CSV
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
<Card>
|
||||||
<div className="flex gap-2 border-b border-slate-200">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("users")}
|
|
||||||
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
|
|
||||||
activeTab === "users"
|
|
||||||
? "border-blue-500 text-blue-600"
|
|
||||||
: "border-transparent text-slate-600 hover:text-slate-900"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
User List ({users.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("details")}
|
|
||||||
disabled={!selectedUser}
|
|
||||||
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
|
|
||||||
activeTab === "details"
|
|
||||||
? "border-blue-500 text-blue-600"
|
|
||||||
: "border-transparent text-slate-600 hover:text-slate-900"
|
|
||||||
} ${!selectedUser ? "opacity-50 cursor-not-allowed" : ""}`}
|
|
||||||
>
|
|
||||||
User Details
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
{activeTab === "users" && (
|
|
||||||
<Card className="border border-slate-200">
|
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<UserGrid
|
<UserGrid
|
||||||
users={users}
|
users={users}
|
||||||
onUserSelect={(user) => {
|
onUserSelect={(user) => handleUserSelect(user)}
|
||||||
handleUserSelect(user);
|
|
||||||
setActiveTab("details");
|
|
||||||
}}
|
|
||||||
onEditUser={handleEditUser}
|
onEditUser={handleEditUser}
|
||||||
onDeleteUser={handleDeleteUser}
|
onDeleteUser={handleDeleteUser}
|
||||||
onBulkDelete={handleBulkDelete}
|
onBulkDelete={handleBulkDelete}
|
||||||
@@ -364,70 +303,6 @@ export function UserManagement() {
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "details" && selectedUser && (
|
|
||||||
<div className="p-6 bg-white rounded-xl border border-slate-200">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h3 className="text-xl font-black text-slate-900">
|
|
||||||
{selectedUser.firstName} {selectedUser.lastName}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-slate-600 mt-1">{selectedUser.email}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Role</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.role}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Joined</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedUser.client && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Membership</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipType}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Status</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipStatus}</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Check-ins This Month</p>
|
|
||||||
<p className="text-2xl font-black text-blue-600">{selectedUser.checkInsThisMonth || 0}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 mt-8 pt-6 border-t border-slate-200">
|
|
||||||
<button
|
|
||||||
onClick={() => selectedUser && handleEditUser(selectedUser)}
|
|
||||||
className="px-5 py-2 bg-blue-50 text-blue-600 rounded-lg font-semibold text-sm hover:bg-blue-100 transition-all"
|
|
||||||
>
|
|
||||||
Edit User
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
|
|
||||||
className="px-5 py-2 bg-red-50 text-red-600 rounded-lg font-semibold text-sm hover:bg-red-100 transition-all"
|
|
||||||
>
|
|
||||||
Delete User
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
href={`/users/${selectedUser.id}`}
|
|
||||||
className="px-5 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg font-semibold text-sm hover:shadow-lg transition-all"
|
|
||||||
>
|
|
||||||
Full Profile
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isEditing && editForm && (
|
{isEditing && editForm && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
@@ -563,65 +438,99 @@ export function UserManagement() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<div className="mt-8 pt-8 border-t border-slate-200">
|
<Card>
|
||||||
<div className="mb-6">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<h3 className="text-lg font-black text-slate-900">
|
<h3 className="text-lg font-semibold">User Details</h3>
|
||||||
|
<a
|
||||||
|
href={`/users/${selectedUser.id}`}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
|
||||||
|
>
|
||||||
|
View Full Profile & Recommendations
|
||||||
|
</a>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Basic Information</h4>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Name:</span>{" "}
|
||||||
{selectedUser.firstName} {selectedUser.lastName}
|
{selectedUser.firstName} {selectedUser.lastName}
|
||||||
</h3>
|
</p>
|
||||||
<p className="text-sm text-slate-600 mt-1">{selectedUser.email}</p>
|
<p>
|
||||||
</div>
|
<span className="font-medium">Email:</span>{" "}
|
||||||
|
{selectedUser.email}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
</p>
|
||||||
<div className="space-y-3">
|
<p>
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Role</p>
|
<span className="font-medium">Phone:</span>{" "}
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.role}</p>
|
{selectedUser.phone || "N/A"}
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Joined</p>
|
</p>
|
||||||
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p>
|
<p>
|
||||||
</div>
|
<span className="font-medium">Role:</span>{" "}
|
||||||
|
{selectedUser.role}
|
||||||
{selectedUser.client && (
|
</p>
|
||||||
<div className="space-y-3">
|
<p>
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Membership</p>
|
<span className="font-medium">Joined:</span>{" "}
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipType}</p>
|
{new Date(selectedUser.createdAt).toLocaleDateString()}
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Status</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipStatus}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Check-ins This Month</p>
|
|
||||||
<p className="text-2xl font-black text-blue-600">{selectedUser.checkInsThisMonth || 0}</p>
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Last Check-in</p>
|
|
||||||
<p className="text-sm text-slate-900">
|
|
||||||
{selectedUser.lastCheckInTime
|
|
||||||
? new Date(selectedUser.lastCheckInTime).toLocaleDateString()
|
|
||||||
: "Never"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 mt-6">
|
{selectedUser.client && (
|
||||||
<button
|
<div>
|
||||||
onClick={() => selectedUser && handleEditUser(selectedUser)}
|
<h4 className="font-medium mb-2">Client Information</h4>
|
||||||
className="px-5 py-2 bg-blue-50 text-blue-600 rounded-lg font-semibold text-sm hover:bg-blue-100 transition-all"
|
<div className="space-y-1 text-sm">
|
||||||
>
|
<p>
|
||||||
Edit
|
<span className="font-medium">Membership:</span>{" "}
|
||||||
</button>
|
{selectedUser.client.membershipType}
|
||||||
<button
|
</p>
|
||||||
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
|
<p>
|
||||||
className="px-5 py-2 bg-red-50 text-red-600 rounded-lg font-semibold text-sm hover:bg-red-100 transition-all"
|
<span className="font-medium">Status:</span>{" "}
|
||||||
>
|
{selectedUser.client.membershipStatus}
|
||||||
Delete
|
</p>
|
||||||
</button>
|
<p>
|
||||||
<a
|
<span className="font-medium">Member Since:</span>{" "}
|
||||||
href={`/users/${selectedUser.id}`}
|
{new Date(
|
||||||
className="px-5 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg font-semibold text-sm hover:shadow-lg transition-all"
|
selectedUser.client.joinDate,
|
||||||
>
|
).toLocaleDateString()}
|
||||||
Full Profile
|
</p>
|
||||||
</a>
|
<p>
|
||||||
|
<span className="font-medium">Last Visit:</span>{" "}
|
||||||
|
{selectedUser.client.lastVisit
|
||||||
|
? new Date(
|
||||||
|
selectedUser.client.lastVisit,
|
||||||
|
).toLocaleDateString()
|
||||||
|
: "Never"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Check-In Statistics</h4>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Last Check-In:</span>{" "}
|
||||||
|
{selectedUser.lastCheckInTime
|
||||||
|
? new Date(
|
||||||
|
selectedUser.lastCheckInTime,
|
||||||
|
).toLocaleString()
|
||||||
|
: "Never"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">This Week:</span>{" "}
|
||||||
|
{selectedUser.checkInsThisWeek || 0} check-ins
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">This Month:</span>{" "}
|
||||||
|
{selectedUser.checkInsThisMonth || 0} check-ins
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+12
-171
@@ -1,174 +1,15 @@
|
|||||||
# FitAI
|
## fitai
|
||||||
|
|
||||||
Integrated AI solution for fitness houses and their clients with Clerk authentication.
|
# description
|
||||||
|
|
||||||
## Project Structure
|
- fitai is integrated ai solution for fitness houses and their clients,
|
||||||
|
its allow to easy menagment of clients, tracking of payments, usage of resourcess,
|
||||||
|
attendance, habits etc.
|
||||||
|
these will be phase one:
|
||||||
|
solution is composed of a admin app, where we are doing managment tasks, we visualize and
|
||||||
|
expose importatnt data to menagment and trainers, and a expo/reactnative mobile app for users.
|
||||||
|
via app we will be tracking attendance and payments, we will be sending notification etc.
|
||||||
|
|
||||||
```
|
# phase 2
|
||||||
fitai/
|
we will be tracking user inputs via manual input and devices, backend will analyze data and propose
|
||||||
├── apps/
|
excercises etc.
|
||||||
│ ├── admin/ # Next.js admin dashboard
|
|
||||||
│ └── mobile/ # React Native mobile app (Expo)
|
|
||||||
├── packages/
|
|
||||||
│ └── shared/ # Shared types and utilities
|
|
||||||
└── AGENTS.md # Development guidelines
|
|
||||||
```
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Node.js >= 18.0.0
|
|
||||||
- npm >= 9.0.0
|
|
||||||
- Clerk account (sign up at https://clerk.com)
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
```bash
|
|
||||||
# Install root dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Install admin dependencies
|
|
||||||
cd apps/admin && npm install
|
|
||||||
|
|
||||||
# Install mobile dependencies
|
|
||||||
cd apps/mobile && npm install --legacy-peer-deps
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authentication Setup
|
|
||||||
|
|
||||||
FitAI uses Clerk for authentication. Follow these steps:
|
|
||||||
|
|
||||||
1. **Create a Clerk account** at https://dashboard.clerk.com
|
|
||||||
2. **Create a new application** in the Clerk dashboard
|
|
||||||
3. **Copy your API keys** (Publishable Key and Secret Key)
|
|
||||||
4. **Configure environment variables**:
|
|
||||||
|
|
||||||
**Admin App** (`apps/admin/.env.local`):
|
|
||||||
```env
|
|
||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
|
|
||||||
CLERK_SECRET_KEY=sk_test_your_key_here
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mobile App** (`apps/mobile/.env`):
|
|
||||||
```env
|
|
||||||
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
|
|
||||||
```
|
|
||||||
|
|
||||||
📖 **See [CLERK_SETUP.md](./CLERK_SETUP.md) for detailed setup instructions**
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
**Important**: Set up environment variables before running the apps!
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Admin dashboard (http://localhost:3000)
|
|
||||||
cd apps/admin && npm run dev
|
|
||||||
|
|
||||||
# Mobile app (http://localhost:8081) - Requires Expo SDK 54
|
|
||||||
cd apps/mobile && npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
**First-time setup checklist**:
|
|
||||||
- [ ] Create Clerk account and application
|
|
||||||
- [ ] Add API keys to `.env.local` (admin) and `.env` (mobile)
|
|
||||||
- [ ] Verify both apps start without errors
|
|
||||||
- [ ] Test sign-up and sign-in flows
|
|
||||||
|
|
||||||
### Mobile App Setup
|
|
||||||
- **Expo SDK**: 50 (stable, compatible with Expo Go)
|
|
||||||
- **Assets**: Placeholder icons and splash screen included
|
|
||||||
- **Navigation**: Expo Router with tab-based layout
|
|
||||||
- **Authentication**: Secure storage with expo-secure-store
|
|
||||||
- **Babel**: babel-preset-expo for proper transpilation
|
|
||||||
|
|
||||||
### Known Compatibility Notes
|
|
||||||
- Use Expo Go with SDK 50 for mobile testing
|
|
||||||
- For SDK 54, upgrade all dependencies to latest versions
|
|
||||||
- Current setup prioritizes stability over latest features
|
|
||||||
|
|
||||||
### Build & Test
|
|
||||||
```bash
|
|
||||||
# Build all apps
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
npm test
|
|
||||||
|
|
||||||
# Lint code
|
|
||||||
npm run lint
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
npm run typecheck
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Authentication (Clerk)
|
|
||||||
- 🔐 Secure email/password authentication
|
|
||||||
- ✉️ Email verification
|
|
||||||
- 🔄 Session management
|
|
||||||
- 🎨 Customizable UI components
|
|
||||||
- 📱 Multi-platform support (Web + Mobile)
|
|
||||||
- 🛡️ Built-in security features
|
|
||||||
|
|
||||||
### Admin Dashboard
|
|
||||||
- 👥 User management (CRUD operations)
|
|
||||||
- 📊 Analytics dashboard with charts
|
|
||||||
- 🎯 Role-based access control
|
|
||||||
- 📈 Data visualization with AG Grid
|
|
||||||
- 💳 Payment tracking (coming soon)
|
|
||||||
- 📅 Attendance monitoring (coming soon)
|
|
||||||
|
|
||||||
### Mobile App
|
|
||||||
- 🔐 Secure sign-in/sign-up
|
|
||||||
- 👤 User profile management
|
|
||||||
- 📱 Native mobile experience
|
|
||||||
- 🔔 Push notifications ready
|
|
||||||
- ✅ Attendance check-in (coming soon)
|
|
||||||
- 💰 Payment history (coming soon)
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
- **Clerk**: Complete authentication and user management platform
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **Admin**: Next.js 14 (App Router), React 19, TypeScript, Tailwind CSS
|
|
||||||
- **Mobile**: React Native, Expo SDK 54, Expo Router, TypeScript
|
|
||||||
|
|
||||||
### Backend & Database
|
|
||||||
- **Database**: SQLite with Drizzle ORM
|
|
||||||
- **API**: Next.js API Routes (REST)
|
|
||||||
|
|
||||||
### Development Tools
|
|
||||||
- **State Management**: React Query, React Hook Form
|
|
||||||
- **Validation**: Zod schemas
|
|
||||||
- **Data Grid**: AG Grid for advanced user management
|
|
||||||
- **Charts**: AG Charts for analytics and visualization
|
|
||||||
- **Testing**: Jest, Testing Library (configured)
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
fitai/
|
|
||||||
├── apps/
|
|
||||||
│ ├── admin/ # Next.js admin dashboard
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ │ ├── app/ # App Router pages & API routes
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ └── lib/ # Database & utilities
|
|
||||||
│ │ └── .env.local # Admin environment variables
|
|
||||||
│ │
|
|
||||||
│ └── mobile/ # Expo React Native app
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── app/ # Expo Router screens
|
|
||||||
│ │ │ ├── (auth)/ # Authentication screens
|
|
||||||
│ │ │ └── (tabs)/ # Main app tabs
|
|
||||||
│ │ └── components/
|
|
||||||
│ └── .env # Mobile environment variables
|
|
||||||
│
|
|
||||||
├── packages/
|
|
||||||
│ ├── database/ # Drizzle ORM schemas & DB client
|
|
||||||
│ └── shared/ # Shared types & utilities
|
|
||||||
│
|
|
||||||
└── CLERK_SETUP.md # Detailed authentication setup guide
|
|
||||||
```
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
|
||||||
|
|
||||||
Commands marked with * may be preceded by a number, _N.
|
|
||||||
Notes in parentheses indicate the behavior if _N is given.
|
|
||||||
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
|
||||||
|
|
||||||
h H Display this help.
|
|
||||||
q :q Q :Q ZZ Exit.
|
|
||||||
---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
MMOOVVIINNGG
|
|
||||||
|
|
||||||
e ^E j ^N CR * Forward one line (or _N lines).
|
|
||||||
y ^Y k ^K ^P * Backward one line (or _N lines).
|
|
||||||
f ^F ^V SPACE * Forward one window (or _N lines).
|
|
||||||
Reference in new issue
Block a user