Compare commits

..
1 Commits
14 changed files with 560 additions and 687 deletions

No files matched your search

+47
View File
@@ -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

+35
View File
@@ -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

+1 -1
View File
@@ -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}
+105 -118
View File
@@ -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,129 +46,116 @@ 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>
<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"> <div>
<Trophy className="w-9 h-9 text-white" /> <h1 className="text-4xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
</div> NextForm
<div> </h1>
<h1 className="text-5xl font-black text-white tracking-tight">FitAI</h1> <p className="text-sm text-slate-500 font-medium">Fitness Management Platform</p>
<p className="text-blue-200 text-sm font-semibold mt-2">Premium Sports Management Platform</p> </div>
</div> </div>
{/* Performance Metrics Section */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2>
<p className="text-slate-600 mt-2 font-medium">Track key indicators and member activity</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatsCard
title="Total Members"
value={loading ? "..." : stats.totalUsers}
change="+12% this month"
trend="up"
icon={Users}
color="blue"
/>
<StatsCard
title="Active Athletes"
value={loading ? "..." : stats.activeClients}
change="+5% vs last week"
trend="up"
icon={CalendarCheck}
color="emerald"
/>
<StatsCard
title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={CreditCard}
color="amber"
/>
<StatsCard
title="Growth"
value="24%"
change="+3% vs last month"
trend="up"
icon={TrendingUp}
color="cyan"
/>
</div>
</div>
{/* Members Directory Section */}
<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>
{/* Key Insights Section */}
<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">Active Subscriptions</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">Premium members active</p>
</div> </div>
<div className="text-right"> </div>
<p className="text-blue-100 text-sm">Welcome</p>
<p className="text-2xl font-bold text-white">Admin Control</p> <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 Activity</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 training</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">Total 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 earnings</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Main Content with Proper Spacing */} {/* Detailed Report Section */}
<div className="max-w-7xl mx-auto px-8 py-16"> <div className="mb-20">
<div className="mb-8">
{/* Section: Performance Metrics */} <h2 className="text-3xl font-black text-slate-900">Detailed Report</h2>
<div className="mb-20"> <p className="text-slate-600 mt-2 font-medium">Analytics Dashboard</p>
<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 Members"
value={loading ? "..." : stats.totalUsers}
change="+12% this month"
trend="up"
icon={Users}
color="blue"
/>
<StatsCard
title="Active Athletes"
value={loading ? "..." : stats.activeClients}
change="+8% this week"
trend="up"
icon={Activity}
color="green"
/>
<StatsCard
title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={Zap}
color="amber"
/>
<StatsCard
title="Growth"
value="24%"
change="+3% vs last month"
trend="up"
icon={TrendingUp}
color="cyan"
/>
</div>
</div> </div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-10">
{/* Section: Member Management */} <div className="bg-slate-50 rounded-2xl p-8">
<div className="mb-20"> <AnalyticsDashboard />
<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>
</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
+21 -16
View File
@@ -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">
</h1> <img src="/nextform-logo.png" alt="NextForm" className="w-full h-full object-contain" />
<p className="text-xs text-slate-600 font-medium mt-1">Sports Admin</p> </div>
<h1 className="text-xl font-black bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
NextForm
</h1>
</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>
+31 -37
View File
@@ -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>
</div>
<div className={`${styles.icon} opacity-60`}>
<Icon size={24} />
</div>
</div>
<p className={`text-4xl font-black ${styles.value} mt-4 truncate`}>{value}</p>
{change && (
<p className={`text-sm ${styles.label} mt-3 font-semibold`}>
{change}
</p> </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>
<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>
)}
</div> </div>
); );
} }
+52 -19
View File
@@ -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 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;
} }
export function Button({ variant = 'primary', children, className = '', ...props }: ButtonProps) { const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors' ({ className, variant, size, asChild = false, ...props }, ref) => {
const variantClasses = { const Comp = asChild ? Slot : "button";
primary: 'bg-blue-600 text-white hover:bg-blue-700', return (
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300', <Comp
} className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
return ( export { Button, buttonVariants };
<button
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</button>
)
}
+71 -33
View File
@@ -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;
+183 -274
View File
@@ -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,228 +205,104 @@ 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 && ( onClick={() => setFilter("all")}
<p className="text-sm text-blue-600 font-semibold"> >
Selected: {selectedUser.firstName} {selectedUser.lastName} All Users
</p> </Button>
)} <Button
</div> variant="secondary"
onClick={() => selectedUser && handleEditUser(selectedUser)}
{/* Compact Controls - Always Visible but Organized */} disabled={!selectedUser}
<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 */} Edit User
<div> </Button>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Roles</p> <Button
<div className="flex flex-wrap gap-2"> variant="secondary"
<button onClick={() => {
onClick={() => setFilter("all")} setEditForm({
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${ firstName: "",
filter === "all" lastName: "",
? "bg-blue-500 text-white shadow-md" email: "",
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200" role: "client",
}`} phone: "",
> });
All setSelectedUser(null);
</button> setIsEditing(true);
<button }}
onClick={() => setFilter("client")} >
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${ Invite User
filter === "client" </Button>
? "bg-blue-500 text-white shadow-md" <Button
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200" variant="secondary"
}`} onClick={() => selectedUser && handleDeleteUser(selectedUser)}
> disabled={!selectedUser}
Clients >
</button> Delete User
<button </Button>
onClick={() => setFilter("trainer")} <Button
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${ variant={filter === "client" ? "primary" : "secondary"}
filter === "trainer" onClick={() => setFilter("client")}
? "bg-blue-500 text-white shadow-md" >
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200" Clients
}`} </Button>
> <Button
Trainers variant={filter === "trainer" ? "primary" : "secondary"}
</button> onClick={() => setFilter("trainer")}
<button >
onClick={() => setFilter("admin")} Trainers
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${ </Button>
filter === "admin" <Button
? "bg-blue-500 text-white shadow-md" variant={filter === "admin" ? "primary" : "secondary"}
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200" onClick={() => setFilter("admin")}
}`} >
> Admins
Admins </Button>
</button> <Button
<button variant={filter === "superAdmin" ? "primary" : "secondary"}
onClick={() => setFilter("superAdmin")} onClick={() => setFilter("superAdmin")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${ >
filter === "superAdmin" Super Admins
? "bg-blue-500 text-white shadow-md" </Button>
: "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: "",
lastName: "",
email: "",
role: "client",
phone: "",
});
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
</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
</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 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>
</div> </div>
{/* Tab Navigation */} <div className="flex justify-between items-center">
<div className="flex gap-2 border-b border-slate-200"> <div className="text-sm text-gray-600">
<button Showing {users.length} users
onClick={() => setActiveTab("users")} {selectedUser && (
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${ <span className="ml-4 text-blue-600">
activeTab === "users" Selected: {selectedUser.firstName} {selectedUser.lastName}
? "border-blue-500 text-blue-600" </span>
: "border-transparent text-slate-600 hover:text-slate-900" )}
}`} </div>
> <div className="flex gap-2">
User List ({users.length}) <Button variant="secondary" onClick={handleRefresh}>
</button> Refresh
<button </Button>
onClick={() => setActiveTab("details")} <Button variant="secondary" onClick={handleExport}>
disabled={!selectedUser} Export CSV
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${ </Button>
activeTab === "details" </div>
? "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> </div>
{/* Tab Content */} <Card>
{activeTab === "users" && ( <CardContent className="p-0">
<Card className="border border-slate-200"> <UserGrid
<CardContent className="p-0"> users={users}
<UserGrid onUserSelect={(user) => handleUserSelect(user)}
users={users} onEditUser={handleEditUser}
onUserSelect={(user) => { onDeleteUser={handleDeleteUser}
handleUserSelect(user); onBulkDelete={handleBulkDelete}
setActiveTab("details"); loading={loading}
}} />
onEditUser={handleEditUser} </CardContent>
onDeleteUser={handleDeleteUser} </Card>
onBulkDelete={handleBulkDelete}
loading={loading}
/>
</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 && ( {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,64 +438,98 @@ 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>
{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-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 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 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>
</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 <a
href={`/users/${selectedUser.id}`} 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" className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
> >
Full Profile View Full Profile & Recommendations
</a> </a>
</div> </CardHeader>
</div> <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}
</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>
</div>
</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>
)}
<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
View File
@@ -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
```
-16
View File
@@ -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).