Compare commits

...

2 Commits

9 changed files with 704 additions and 413 deletions

View File

@ -26,7 +26,7 @@ export default function RootLayout({
<ClerkProvider>
<html lang="en">
<body className={inter.className}>
<div className="flex min-h-screen bg-slate-50">
<div className="flex min-h-screen bg-gradient-to-br from-white via-blue-50 to-slate-50">
<Sidebar />
<main className="flex-1 ml-64 p-8">
{children}

View File

@ -1,10 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
import { Users, Activity, Zap, TrendingUp, Trophy, Target } from "lucide-react";
import { StatsCard } from "@/components/ui/StatsCard";
import { UserManagement } from "@/components/users/UserManagement";
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
import { UserManagement } from "@/components/users/UserManagement";
import axios from "axios";
interface DashboardStats {
@ -46,58 +46,132 @@ export default function Home() {
};
return (
<div className="space-y-8">
<div className="min-h-screen bg-gradient-to-br from-white via-slate-50 to-blue-50">
{/* Premium Header */}
<div className="bg-gradient-to-r from-slate-900 via-blue-900 to-slate-900 shadow-2xl">
<div className="max-w-7xl mx-auto px-8 py-12">
<div className="flex items-end justify-between">
<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>
<h2 className="text-3xl font-bold text-slate-900">Dashboard</h2>
<p className="text-slate-500 mt-2">Welcome back, here's what's happening today.</p>
<h1 className="text-5xl font-black text-white tracking-tight">FitAI</h1>
<p className="text-blue-200 text-sm font-semibold mt-2">Premium Sports Management Platform</p>
</div>
</div>
<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 className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Main Content with Proper Spacing */}
<div className="max-w-7xl mx-auto px-8 py-16">
{/* Section: Performance Metrics */}
<div className="mb-20">
<div className="mb-10">
<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>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<StatsCard
title="Total Users"
title="Total Members"
value={loading ? "..." : stats.totalUsers}
change="+12%" // Placeholder for now as we don't track historical growth yet
change="+12% this month"
trend="up"
icon={Users}
color="blue"
/>
<StatsCard
title="Active Clients"
title="Active Athletes"
value={loading ? "..." : stats.activeClients}
change="+5%"
change="+8% this week"
trend="up"
icon={CalendarCheck}
icon={Activity}
color="green"
/>
<StatsCard
title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}%`}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={CreditCard}
color="purple"
icon={Zap}
color="amber"
/>
<StatsCard
title="Growth"
value="24%" // Placeholder
change="-2%"
trend="down"
value="24%"
change="+3% vs last month"
trend="up"
icon={TrendingUp}
color="orange"
color="cyan"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Recent Activity</h3>
<UserManagement />
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Quick Analytics</h3>
{/* Section: Member Management */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Members Directory</h2>
<p className="text-slate-600 mt-2 font-medium">View detailed member information and analytics</p>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-10">
<UserManagement />
</div>
</div>
{/* Section: Key Insights */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Key Insights</h2>
<p className="text-slate-600 mt-2 font-medium">Quick stats overview</p>
</div>
<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="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-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>
</div>
</div>
<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">
<p className="text-xs text-emerald-700 uppercase font-black tracking-wider">Training Now</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>
</div>
</div>
<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">
<p className="text-xs text-amber-700 uppercase font-black tracking-wider">Revenue</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>
</div>
</div>
</div>
</div>
{/* Section: Detailed Report - Full Width */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Detailed Report</h2>
<p className="text-slate-600 mt-2 font-medium">Analytics Dashboard</p>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-10">
<div className="bg-slate-50 rounded-2xl p-8">
<AnalyticsDashboard />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -65,14 +65,15 @@ export function Sidebar() {
];
return (
<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-800">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
FitAI Admin
<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">
<div className="p-6 border-b border-slate-200">
<h1 className="text-2xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
FitAI
</h1>
<p className="text-xs text-slate-600 font-medium mt-1">Sports Admin</p>
</div>
<nav className="flex-1 p-4 space-y-2">
<nav className="flex-1 p-4 space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
@ -81,25 +82,25 @@ export function Sidebar() {
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 group ${isActive
? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
: "text-slate-400 hover:bg-slate-800 hover:text-white"}`}
className={`flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-200 group font-medium ${isActive
? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-md shadow-blue-500/20"
: "text-slate-700 hover:bg-slate-100 hover:text-blue-600"}`}
>
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} />
<span className="font-medium">{label}</span>
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-blue-600"} />
<span className="text-sm">{label}</span>
</Link>
);
})}
</nav>
<div className="p-4 border-t border-slate-800">
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-slate-800/50">
<div className="p-4 border-t border-slate-200">
<div className="flex items-center gap-3 px-4 py-3 rounded-2xl bg-slate-100">
<UserButton afterSignOutUrl="/" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">
<p className="text-sm font-semibold text-slate-900 truncate">
{user?.fullName || "Admin User"}
</p>
<p className="text-xs text-slate-400 truncate">
<p className="text-xs text-slate-600 truncate">
{user?.primaryEmailAddress?.emailAddress}
</p>
</div>

View File

@ -1,5 +1,4 @@
import { LucideIcon } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface StatsCardProps {
title: string;
@ -7,45 +6,67 @@ interface StatsCardProps {
change?: string;
trend?: "up" | "down" | "neutral";
icon: LucideIcon;
color?: "blue" | "green" | "purple" | "orange";
color?: "blue" | "green" | "amber" | "cyan";
}
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
const colorStyles = {
blue: "bg-blue-50 text-blue-600",
green: "bg-green-50 text-green-600",
purple: "bg-purple-50 text-purple-600",
orange: "bg-orange-50 text-orange-600",
blue: {
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",
label: "text-blue-700"
},
green: {
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",
label: "text-emerald-700"
},
amber: {
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",
label: "text-amber-700"
},
cyan: {
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",
label: "text-cyan-700"
},
};
const style = colorStyles[color];
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
<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="flex items-start justify-between mb-4 gap-3">
<div className="flex-1 min-w-0">
<p className={`text-xs ${style.label} uppercase font-black tracking-widest truncate`}>
{title}
</CardTitle>
<div className={`p-2 rounded-lg ${colorStyles[color]}`}>
<Icon size={16} />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{change && (
<p className="text-xs text-muted-foreground mt-1">
<span
className={`font-medium ${trend === "up"
? "text-green-600"
: trend === "down"
? "text-red-600"
: "text-slate-600"
}`}
>
{change}
</span>{" "}
vs last month
</p>
</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`}>
<Icon size={20} className="text-white" />
</div>
</div>
<div className={`text-4xl font-black ${style.text} mb-3 truncate`}>
{value}
</div>
{change && (
<div className="flex items-center gap-1 truncate">
<span className={`text-xs font-black uppercase tracking-wide ${style.label} truncate`}>
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
</span>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -1,56 +1,23 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import React from 'react'
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"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",
{
variants: {
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;
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary'
children: React.ReactNode
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export function Button({ variant = 'primary', children, className = '', ...props }: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
}
export { Button, buttonVariants };
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</button>
)
}

View File

@ -1,76 +1,38 @@
import * as React from "react"
import React from 'react'
import { cn } from "@/lib/utils"
interface CardProps {
children: React.ReactNode
className?: string
}
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
export function Card({ children, className = '' }: CardProps) {
return (
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
{children}
</div>
)
}
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
export function CardHeader({ children, className = '' }: CardProps) {
return (
<div className={`mb-4 ${className}`}>
{children}
</div>
)
}
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
export function CardContent({ children, className = '' }: CardProps) {
return (
<div className={className}>
{children}
</div>
)
}
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<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 }
export function CardTitle({ children, className = '' }: CardProps) {
return (
<div className={`text-sm font-medium ${className}`}>
{children}
</div>
)
}

View File

@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
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";
interface User {
@ -34,6 +34,7 @@ export function UserManagement() {
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [activeTab, setActiveTab] = useState<"users" | "details">("users");
const [editForm, setEditForm] = useState<{
firstName: string;
lastName: string;
@ -205,25 +206,83 @@ export function UserManagement() {
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">User Management</h2>
<div className="flex gap-2">
<Button
variant={filter === "all" ? "primary" : "secondary"}
<div className="space-y-4">
{/* Header with Count */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-black text-slate-900">
{users.length} {users.length === 1 ? 'user' : 'users'}
</h3>
{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")}
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 Users
</Button>
<Button
variant="secondary"
onClick={() => selectedUser && handleEditUser(selectedUser)}
disabled={!selectedUser}
All
</button>
<button
onClick={() => setFilter("client")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "client"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Edit User
</Button>
<Button
variant="secondary"
Clients
</button>
<button
onClick={() => setFilter("trainer")}
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={() => {
setEditForm({
firstName: "",
@ -235,67 +294,69 @@ export function UserManagement() {
setSelectedUser(null);
setIsEditing(true);
}}
className="px-2 py-1 text-xs font-semibold rounded-lg bg-blue-500 text-white hover:bg-blue-600"
>
Invite User
</Button>
<Button
variant="secondary"
+ Invite
</button>
<button
onClick={() => selectedUser && handleEditUser(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"
>
Edit
</button>
<button
onClick={() => selectedUser && handleDeleteUser(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 User
</Button>
<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}>
Delete
</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">
Refresh
</Button>
<Button variant="secondary" onClick={handleExport}>
Export CSV
</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">
Export
</button>
</div>
</div>
</div>
<Card>
{/* Tab Navigation */}
<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">
<UserGrid
users={users}
onUserSelect={(user) => handleUserSelect(user)}
onUserSelect={(user) => {
handleUserSelect(user);
setActiveTab("details");
}}
onEditUser={handleEditUser}
onDeleteUser={handleDeleteUser}
onBulkDelete={handleBulkDelete}
@ -303,6 +364,70 @@ export function UserManagement() {
/>
</CardContent>
</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 && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@ -438,98 +563,64 @@ export function UserManagement() {
)}
{selectedUser && (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<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>{" "}
<div className="mt-8 pt-8 border-t border-slate-200">
<div className="mb-6">
<h3 className="text-lg font-black text-slate-900">
{selectedUser.firstName} {selectedUser.lastName}
</p>
<p>
<span className="font-medium">Email:</span>{" "}
{selectedUser.email}
</p>
<p>
<span className="font-medium">Phone:</span>{" "}
{selectedUser.phone || "N/A"}
</p>
<p>
<span className="font-medium">Role:</span>{" "}
{selectedUser.role}
</p>
<p>
<span className="font-medium">Joined:</span>{" "}
{new Date(selectedUser.createdAt).toLocaleDateString()}
</p>
</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-3">
<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>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Joined</p>
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p>
</div>
{selectedUser.client && (
<div>
<h4 className="font-medium mb-2">Client Information</h4>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Membership:</span>{" "}
{selectedUser.client.membershipType}
</p>
<p>
<span className="font-medium">Status:</span>{" "}
{selectedUser.client.membershipStatus}
</p>
<p>
<span className="font-medium">Member Since:</span>{" "}
{new Date(
selectedUser.client.joinDate,
).toLocaleDateString()}
</p>
<p>
<span className="font-medium">Last Visit:</span>{" "}
{selectedUser.client.lastVisit
? new Date(
selectedUser.client.lastVisit,
).toLocaleDateString()
: "Never"}
</p>
</div>
<div className="space-y-3">
<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>
<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>
<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>{" "}
<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,
).toLocaleString()
? new Date(selectedUser.lastCheckInTime).toLocaleDateString()
: "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 className="flex gap-3 mt-6">
<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
</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
</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>
</CardContent>
</Card>
)}
</div>
);

View File

@ -1,15 +1,174 @@
## fitai
# FitAI
# description
Integrated AI solution for fitness houses and their clients with Clerk authentication.
- 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.
## Project Structure
# phase 2
we will be tracking user inputs via manual input and devices, backend will analyze data and propose
excercises etc.
```
fitai/
├── apps/
│ ├── 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
```

16
ter Normal file
View File

@ -0,0 +1,16 @@
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).