Compare commits

..

4 Commits

Author SHA1 Message Date
5fd91ec5ad Updated UI, added new logo and public assets 2025-12-09 09:28:26 +01:00
1ad146fe57 Added cursor pointer to tab button 2025-12-09 09:00:47 +01:00
bf741d6d00 update 2025-12-06 21:24:19 +01:00
ddb6933e42 ace update 2025-12-06 11:38:49 +01:00
24 changed files with 910 additions and 1136 deletions

BIN
apps/admin/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
// POST - Mark goal as complete // POST - Mark goal as complete
export async function POST( export async function POST(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: { id: string } }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();

View File

@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
// GET - Get specific goal // GET - Get specific goal
export async function GET( export async function GET(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: { id: string } }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
@ -40,7 +40,7 @@ export async function GET(
// PUT - Update goal // PUT - Update goal
export async function PUT( export async function PUT(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: { id: string } }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
@ -82,7 +82,7 @@ export async function PUT(
// DELETE - Delete goal // DELETE - Delete goal
export async function DELETE( export async function DELETE(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: { id: string } }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();

View File

@ -2,9 +2,17 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Sports Theme Colors */
:root {
--primary-blue: #2563eb;
--secondary-orange: #fb7a1b;
--accent-green: #22c55e;
}
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; /* Light Mode - Sports Theme */
--background: 0 0% 98%;
--foreground: 222.2 84% 4.9%; --foreground: 222.2 84% 4.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
@ -13,56 +21,63 @@
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%; /* Electric Blue Primary */
--primary-foreground: 210 40% 98%; --primary: 217 91.2% 59.8%;
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; /* Vibrant Orange Secondary */
--secondary-foreground: 222.2 47.4% 11.2%; --secondary: 24.6 97.4% 54.3%;
--secondary-foreground: 0 0% 100%;
/* Emerald Green Accent */
--accent: 142.1 70.6% 45.3%;
--accent-foreground: 0 0% 100%;
--muted: 210 40% 96.1%; --muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%; --ring: 217 91.2% 59.8%;
--radius: 0.75rem; --radius: 0.75rem;
} }
.dark { .dark {
/* Dark Mode - Sports Theme */
--background: 222.2 84% 4.9%; --background: 222.2 84% 4.9%;
--foreground: 210 40% 98%; --foreground: 210 40% 98%;
--card: 222.2 84% 4.9%; --card: 225.7 29.5% 15.3%;
--card-foreground: 210 40% 98%; --card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%; --popover: 225.7 29.5% 15.3%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 210 40% 98%; /* Electric Blue Primary */
--primary-foreground: 222.2 47.4% 11.2%; --primary: 217 91.2% 59.8%;
--primary-foreground: 225.7 29.5% 15.3%;
--secondary: 217.2 32.6% 17.5%; /* Vibrant Orange Secondary */
--secondary-foreground: 210 40% 98%; --secondary: 24.6 97.4% 54.3%;
--secondary-foreground: 225.7 29.5% 15.3%;
/* Emerald Green Accent */
--accent: 142.1 70.6% 45.3%;
--accent-foreground: 225.7 29.5% 15.3%;
--muted: 217.2 32.6% 17.5%; --muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%; --ring: 217 91.2% 59.8%;
} }
} }
@ -71,6 +86,28 @@
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground transition-colors duration-300;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold text-foreground;
}
}
@layer components {
.sports-gradient {
@apply bg-gradient-to-r from-[#2563eb] via-[#fb7a1b] to-[#22c55e];
}
.card-modern {
@apply bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700;
}
.btn-sports {
@apply bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] hover:shadow-lg transform hover:scale-105 transition-all duration-200 text-white font-bold;
}
.text-gradient {
@apply bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] bg-clip-text text-transparent;
} }
} }

View File

@ -25,34 +25,11 @@ export default function RootLayout({
return ( return (
<ClerkProvider> <ClerkProvider>
<html lang="en"> <html lang="en">
<body className={`${inter.className} bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100 min-h-screen`}> <body className={inter.className}>
<div className="flex min-h-screen"> <div className="flex min-h-screen bg-slate-50">
<Sidebar /> <Sidebar />
<main className="flex-1 ml-64"> <main className="flex-1 ml-64 p-8">
<div className="sticky top-0 z-40 backdrop-blur-xl bg-white/75 border-b border-slate-200/50 shadow-sm">
<div className="px-8 py-4 flex items-center justify-between max-w-7xl mx-auto">
<div>
<h1 className="text-sm font-semibold text-gray-600 uppercase tracking-wide">FitAI Pro</h1>
</div>
<div className="flex items-center gap-4">
<SignedIn>
<UserButton
appearance={{
elements: {
avatarBox: "w-10 h-10 rounded-full ring-2 ring-blue-200"
}
}}
/>
</SignedIn>
<SignedOut>
<SignInButton mode="modal" />
</SignedOut>
</div>
</div>
</div>
<div className="p-8">
{children} {children}
</div>
</main> </main>
</div> </div>
</body> </body>

View File

@ -1,10 +1,11 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react"; import { Users, CreditCard, CalendarCheck, TrendingUp, Brain, Calendar, User } from "lucide-react";
import { StatsCard } from "@/components/ui/StatsCard"; import { StatsCard } from "@/components/ui/StatsCard";
import { UserManagement } from "@/components/users/UserManagement"; import { UserManagement } from "@/components/users/UserManagement";
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard"; import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
import { Logo } from "@/components/ui/Logo";
import axios from "axios"; import axios from "axios";
interface DashboardStats { interface DashboardStats {
@ -14,6 +15,27 @@ interface DashboardStats {
revenueGrowth: number; revenueGrowth: number;
} }
type TabType = "overview" | "users" | "analytics" | "recommendations" | "attendance" | "profile";
interface Recommendation {
id: string;
userId: string;
content: string;
activityPlan: string;
dietPlan: string;
status: string;
createdAt: Date;
}
interface AttendanceRecord {
id: string;
clientId: string;
checkInTime: string;
checkOutTime?: string;
type: string;
notes?: string;
}
export default function Home() { export default function Home() {
const [stats, setStats] = useState<DashboardStats>({ const [stats, setStats] = useState<DashboardStats>({
totalUsers: 0, totalUsers: 0,
@ -22,6 +44,11 @@ export default function Home() {
revenueGrowth: 0, revenueGrowth: 0,
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<TabType>("overview");
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [attendance, setAttendance] = useState<AttendanceRecord[]>([]);
const [recommendationsLoading, setRecommendationsLoading] = useState(false);
const [attendanceLoading, setAttendanceLoading] = useState(false);
useEffect(() => { useEffect(() => {
const fetchStats = async () => { const fetchStats = async () => {
@ -38,6 +65,46 @@ export default function Home() {
fetchStats(); fetchStats();
}, []); }, []);
// Fetch recommendations when tab changes
useEffect(() => {
if (activeTab === "recommendations") {
fetchRecommendations();
}
}, [activeTab]);
// Fetch attendance when tab changes
useEffect(() => {
if (activeTab === "attendance") {
fetchAttendance();
}
}, [activeTab]);
const fetchRecommendations = async () => {
setRecommendationsLoading(true);
try {
const response = await fetch("/api/recommendations");
const data = await response.json();
setRecommendations(data.recommendations || []);
} catch (error) {
console.error("Error fetching recommendations:", error);
} finally {
setRecommendationsLoading(false);
}
};
const fetchAttendance = async () => {
setAttendanceLoading(true);
try {
const response = await fetch("/api/admin/attendance");
const data = await response.json();
setAttendance(data || []);
} catch (error) {
console.error("Error fetching attendance:", error);
} finally {
setAttendanceLoading(false);
}
};
const formatCurrency = (value: number) => { const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
@ -45,28 +112,56 @@ export default function Home() {
}).format(value); }).format(value);
}; };
const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
{ id: "overview", label: "Overview", icon: <TrendingUp size={18} /> },
{ id: "users", label: "Users", icon: <Users size={18} /> },
{ id: "analytics", label: "Analytics", icon: <TrendingUp size={18} /> },
{ id: "recommendations", label: "Recommendations", icon: <Brain size={18} /> },
{ id: "attendance", label: "Attendance", icon: <Calendar size={18} /> },
{ id: "profile", label: "Profile", icon: <User size={18} /> },
];
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950">
<div className="space-y-8 max-w-7xl"> <div className="w-full px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
{/* Hero Section */} {/* Header Section */}
<div className="pt-8 pb-4"> <div className="mb-8 sm:mb-10">
<div className="space-y-3"> <div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-3"> <Logo variant="full" />
<div className="h-12 w-1 bg-gradient-to-b from-blue-600 to-cyan-600 rounded-full"></div>
<h1 className="text-5xl font-black bg-gradient-to-r from-blue-600 via-blue-700 to-cyan-600 bg-clip-text text-transparent">
FitAI Dashboard
</h1>
</div> </div>
<p className="text-lg text-gray-600 ml-4">Performance metrics & athlete insights</p> <h2 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white mt-4">Admin Dashboard</h2>
<p className="text-slate-600 dark:text-slate-400 mt-2 text-sm sm:text-base">Manage your fitness platform with complete control.</p>
</div>
{/* Tab Navigation */}
<div className="mb-8 overflow-x-auto">
<div className="flex gap-2 pb-4 min-w-min">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-all cursor-pointer ${
activeTab === tab.id
? "bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] text-white shadow-lg hover:shadow-xl"
: "bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 hover:border-slate-300 dark:hover:border-slate-600"
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div> </div>
</div> </div>
{/* Stats Grid */} {/* Overview Tab */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5"> {activeTab === "overview" && (
<div className="space-y-8">
{/* Stats Cards - Responsive Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-5 lg:gap-6">
<StatsCard <StatsCard
title="Total Users" title="Total Users"
value={loading ? "..." : stats.totalUsers} value={loading ? "..." : stats.totalUsers}
change="+12%" // Placeholder for now as we don't track historical growth yet change="+12%"
trend="up" trend="up"
icon={Users} icon={Users}
color="blue" color="blue"
@ -89,7 +184,7 @@ export default function Home() {
/> />
<StatsCard <StatsCard
title="Growth" title="Growth"
value="24%" // Placeholder value="24%"
change="-2%" change="-2%"
trend="down" trend="down"
icon={TrendingUp} icon={TrendingUp}
@ -97,29 +192,160 @@ export default function Home() {
/> />
</div> </div>
{/* Main Content Grid */} {/* Quick Stats Section */}
<div className="space-y-6 pb-12"> <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
{/* User Management - Full Width */} <div className="bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] p-6 sm:p-8">
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/80 p-7 hover:shadow-xl transition-shadow"> <h3 className="text-xl sm:text-2xl font-bold text-white">Quick Overview</h3>
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-slate-900">Active Athletes</h2>
<p className="text-sm text-gray-500 mt-1">Manage and monitor your fitness clients</p>
</div> </div>
<div className="p-6 sm:p-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="text-center p-4 bg-slate-50 dark:bg-slate-700 rounded-lg">
<p className="text-slate-600 dark:text-slate-400 mb-2">Total Revenue</p>
<p className="text-3xl font-bold text-[#2563eb]">{formatCurrency(stats.totalRevenue)}</p>
</div>
<div className="text-center p-4 bg-slate-50 dark:bg-slate-700 rounded-lg">
<p className="text-slate-600 dark:text-slate-400 mb-2">Active Members</p>
<p className="text-3xl font-bold text-[#22c55e]">{stats.activeClients}</p>
</div>
<div className="text-center p-4 bg-slate-50 dark:bg-slate-700 rounded-lg">
<p className="text-slate-600 dark:text-slate-400 mb-2">Platform Users</p>
<p className="text-3xl font-bold text-[#fb7a1b]">{stats.totalUsers}</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* Users Tab */}
{activeTab === "users" && (
<div className="space-y-6 sm:space-y-8">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] p-6 sm:p-8">
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
<Users size={28} />
User Management
</h3>
</div>
<div className="p-6 sm:p-8 w-full overflow-hidden">
<UserManagement /> <UserManagement />
</div> </div>
</div> </div>
{/* Analytics - 3 Columns Horizontal Layout */}
<div className="space-y-4">
<div>
<h2 className="text-2xl font-bold text-slate-900">Analytics</h2>
<p className="text-sm text-gray-500 mt-1">Performance metrics and insights</p>
</div> </div>
)}
{/* Analytics Tab */}
{activeTab === "analytics" && (
<div className="space-y-6 sm:space-y-8">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="bg-gradient-to-r from-[#fb7a1b] to-[#22c55e] p-6 sm:p-8">
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
<TrendingUp size={28} />
Analytics Dashboard
</h3>
</div>
<div className="p-6 sm:p-8 w-full overflow-hidden">
<AnalyticsDashboard /> <AnalyticsDashboard />
</div> </div>
</div> </div>
</div> </div>
)}
{/* Other Tabs - Coming Soon */}
{activeTab === "recommendations" && (
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="bg-gradient-to-r from-[#2563eb] via-[#fb7a1b] to-[#22c55e] p-6 sm:p-8">
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
<Brain size={28} />
AI Recommendations
</h3>
</div>
<div className="p-6 sm:p-8">
{recommendationsLoading ? (
<div className="text-center py-8">Loading recommendations...</div>
) : recommendations.length > 0 ? (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{recommendations.slice(0, 6).map((rec) => (
<div key={rec.id} className="p-4 border border-slate-200 dark:border-slate-600 rounded-lg hover:shadow-lg transition-all">
<div className="flex justify-between items-start mb-2">
<h4 className="font-bold text-sm line-clamp-1">User ID: {rec.userId}</h4>
<span className={`text-xs px-2 py-1 rounded ${rec.status === 'pending' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' : 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'}`}>
{rec.status}
</span>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 line-clamp-2 mb-2">{rec.content}</p>
<p className="text-xs text-slate-500">Activity: {rec.activityPlan.substring(0, 50)}...</p>
</div>
))}
</div>
</div>
) : (
<div className="text-center py-8 text-slate-600 dark:text-slate-400">No recommendations yet</div>
)}
</div>
</div>
)}
{/* Attendance Tab */}
{activeTab === "attendance" && (
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="bg-gradient-to-r from-[#fb7a1b] to-[#22c55e] p-6 sm:p-8">
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
<Calendar size={28} />
Attendance Monitoring
</h3>
</div>
<div className="p-6 sm:p-8 overflow-x-auto">
{attendanceLoading ? (
<div className="text-center py-8">Loading attendance data...</div>
) : attendance.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 dark:border-slate-600">
<th className="text-left py-2 px-4 font-bold">Client ID</th>
<th className="text-left py-2 px-4 font-bold">Check In</th>
<th className="text-left py-2 px-4 font-bold">Check Out</th>
<th className="text-left py-2 px-4 font-bold">Type</th>
</tr>
</thead>
<tbody>
{attendance.slice(0, 10).map((record) => (
<tr key={record.id} className="border-b border-slate-100 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700">
<td className="py-3 px-4 line-clamp-1">{record.clientId}</td>
<td className="py-3 px-4">{new Date(record.checkInTime).toLocaleString()}</td>
<td className="py-3 px-4">{record.checkOutTime ? new Date(record.checkOutTime).toLocaleString() : '-'}</td>
<td className="py-3 px-4">
<span className="px-2 py-1 rounded-full text-xs font-bold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{record.type}
</span>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="text-center py-8 text-slate-600 dark:text-slate-400">No attendance records</div>
)}
</div>
</div>
)}
{/* Profile Tab */}
{activeTab === "profile" && (
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="bg-gradient-to-r from-[#22c55e] to-[#2563eb] p-6 sm:p-8">
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
<User size={28} />
Admin Profile
</h3>
</div>
<div className="p-6 sm:p-8">
<p className="text-slate-600 dark:text-slate-400 text-lg">Profile management coming soon!</p>
</div>
</div>
)}
</div>
</div> </div>
); );
} }

View File

@ -67,75 +67,74 @@ export function AnalyticsDashboard() {
if (loading) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div className="text-center"> <div className="text-lg">Loading analytics...</div>
<div className="inline-block animate-spin"></div>
<p className="text-gray-600 mt-2 text-sm">Loading analytics...</p>
</div>
</div> </div>
) )
} }
return ( return (
<div className="space-y-4"> <div className="space-y-6">
{/* Key Metrics Cards - 3 columns */} <h2 className="text-2xl font-bold">Analytics Dashboard</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="bg-gradient-to-br from-blue-600 via-cyan-500 to-teal-500 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white"> {/* Key Metrics */}
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Athletes</p> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="flex items-baseline gap-2"> <Card>
<div className="text-2xl font-black text-white">{totalUsers}</div> <CardContent>
<span className="text-xs text-white font-semibold">active</span> <div className="text-center">
<div className="text-3xl font-bold text-blue-600">{totalUsers}</div>
<div className="text-gray-600">Total Users</div>
</div> </div>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-green-600">${totalRevenue.toLocaleString()}</div>
<div className="text-gray-600">Total Revenue</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600">{activeMembers}</div>
<div className="text-gray-600">Active Members</div>
</div>
</CardContent>
</Card>
</div> </div>
<div className="bg-gradient-to-br from-emerald-600 via-teal-500 to-cyan-500 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white"> {/* Charts */}
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Revenue</p> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="flex items-baseline gap-2"> <Card>
<div className="text-2xl font-black text-white">${totalRevenue.toLocaleString()}</div> <CardHeader>
<span className="text-xs text-white font-semibold">ytd</span> <h3 className="text-lg font-semibold">User Growth</h3>
</div> </CardHeader>
</div> <CardContent>
<div className="bg-gradient-to-br from-purple-600 via-pink-500 to-blue-500 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Active Members</p>
<div className="flex items-baseline gap-2">
<div className="text-2xl font-black text-white">{activeMembers}</div>
<span className="text-xs text-white font-semibold">members</span>
</div>
</div>
</div>
{/* Charts - 3 Columns Horizontal */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="bg-gradient-to-br from-blue-600 via-cyan-500 to-teal-500 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all">
<div className="mb-3">
<h3 className="text-sm font-bold text-white">User Growth Trend</h3>
<p className="text-xs text-blue-100">Last 6 months performance</p>
</div>
<div className="h-48 overflow-auto">
<UserGrowthChart data={userGrowthData} /> <UserGrowthChart data={userGrowthData} />
</div> </CardContent>
</div> </Card>
<div className="bg-gradient-to-br from-emerald-600 via-teal-500 to-cyan-500 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all"> <Card>
<div className="mb-3"> <CardHeader>
<h3 className="text-sm font-bold text-white">Membership Mix</h3> <h3 className="text-lg font-semibold">Membership Distribution</h3>
<p className="text-xs text-emerald-100">Distribution breakdown</p> </CardHeader>
</div> <CardContent>
<div className="h-48 overflow-auto">
<MembershipDistributionChart data={membershipData} /> <MembershipDistributionChart data={membershipData} />
</div> </CardContent>
</Card>
</div> </div>
<div className="bg-gradient-to-br from-purple-600 via-pink-500 to-blue-500 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all"> <Card>
<div className="mb-3"> <CardHeader>
<h3 className="text-sm font-bold text-white">Revenue Stream</h3> <h3 className="text-lg font-semibold">Monthly Revenue</h3>
<p className="text-xs text-purple-100">Monthly earnings</p> </CardHeader>
</div> <CardContent>
<div className="h-48 overflow-auto">
<RevenueChart data={revenueData} /> <RevenueChart data={revenueData} />
</div> </CardContent>
</div> </Card>
</div>
</div> </div>
) )
} }

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',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
}
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 };

View File

@ -1,30 +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"
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 }

View File

@ -0,0 +1,69 @@
import React from "react";
import Image from "next/image";
interface LogoProps {
variant?: "full" | "icon" | "text";
className?: string;
}
export function Logo({ variant = "full", className = "" }: LogoProps) {
if (variant === "icon") {
return (
<div className={`relative w-24 h-24 flex-shrink-0 ${className}`}>
<Image
src="/logo.png"
alt="FitAI Logo"
width={96}
height={96}
className="object-contain"
priority
/>
</div>
);
}
if (variant === "text") {
return (
<div className={`flex items-center gap-3 ${className}`}>
<div className="relative w-14 h-14 flex-shrink-0">
<Image
src="/logo.png"
alt="FitAI Logo"
width={56}
height={56}
className="object-contain"
priority
/>
</div>
<div className="flex flex-col leading-tight">
<span className="text-lg font-black text-slate-900 dark:text-white">
NextForm
</span>
<span className="text-xs font-bold text-slate-500 dark:text-slate-400">Smart Fitness Twin</span>
</div>
</div>
);
}
// Full variant (default)
return (
<div className={`flex items-center gap-6 ${className}`}>
<div className="relative w-28 h-28 flex-shrink-0">
<Image
src="/logo.png"
alt="FitAI Logo"
width={112}
height={112}
className="object-contain"
priority
/>
</div>
<div className="flex flex-col">
<span className="text-4xl font-black text-slate-900 dark:text-white">
NextForm
</span>
<span className="text-base font-bold text-slate-600 dark:text-slate-400">Your Smart Fitness Twin</span>
</div>
</div>
);
}

View File

@ -13,6 +13,7 @@ import {
Brain, Brain,
} from "lucide-react"; } from "lucide-react";
import { UserButton, useUser } from "@clerk/nextjs"; import { UserButton, useUser } from "@clerk/nextjs";
import { Logo } from "./Logo";
interface Recommendation { interface Recommendation {
id: string; id: string;
@ -65,24 +66,14 @@ export function Sidebar() {
]; ];
return ( return (
<aside className="w-64 bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800/50 shadow-2xl"> <aside className="w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800">
{/* Logo Section */} <div className="p-6 border-b border-slate-800 flex items-center justify-center">
<div className="p-6 border-b border-slate-800/50"> <Link href="/" className="hover:opacity-80 transition-opacity">
<div className="flex items-center gap-3"> <Logo variant="text" className="justify-start" />
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center font-bold text-white shadow-lg"> </Link>
</div>
<div>
<h1 className="text-lg font-black bg-gradient-to-r from-blue-400 via-cyan-400 to-blue-300 bg-clip-text text-transparent">
FitAI
</h1>
<p className="text-xs text-slate-400 font-semibold tracking-wide">ADMIN PRO</p>
</div>
</div>
</div> </div>
{/* Navigation */} <nav className="flex-1 p-4 space-y-2">
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
{menuItems.map((item) => { {menuItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const isActive = pathname === item.href; const isActive = pathname === item.href;
@ -91,30 +82,26 @@ 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-xl transition-all duration-200 group relative overflow-hidden ${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-600 to-cyan-600 text-white shadow-lg shadow-blue-900/30" ? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
: "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`} : "text-slate-400 hover:bg-slate-800 hover:text-white"}`}
> >
{isActive && <div className="absolute inset-0 bg-white/10 blur-xl"></div>} <Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} />
<Icon size={20} className={`${isActive ? "text-white" : "text-slate-500 group-hover:text-white"} relative z-10`} /> <span className="font-medium">{label}</span>
<span className="font-semibold relative z-10">{label}</span>
</Link> </Link>
); );
})} })}
</nav> </nav>
{/* User Section */} <div className="p-4 border-t border-slate-800">
<div className="p-4 border-t border-slate-800/50 bg-slate-800/20"> <div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-slate-800/50">
<div className="flex items-center gap-3 px-3 py-3 rounded-xl bg-slate-800/50 hover:bg-slate-800/70 transition-colors">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex-shrink-0 overflow-hidden">
<UserButton afterSignOutUrl="/" /> <UserButton afterSignOutUrl="/" />
</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-white truncate"> <p className="text-sm font-medium text-white truncate">
{user?.fullName || "Admin"} {user?.fullName || "Admin User"}
</p> </p>
<p className="text-xs text-slate-400 truncate"> <p className="text-xs text-slate-400 truncate">
{user?.emailAddresses[0]?.emailAddress || "admin@fitai.com"} {user?.primaryEmailAddress?.emailAddress}
</p> </p>
</div> </div>
</div> </div>

View File

@ -12,61 +12,38 @@ interface StatsCardProps {
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-blue-50 text-blue-600",
bg: "from-blue-600 via-cyan-500 to-teal-500", green: "bg-green-50 text-green-600",
text: "text-white", purple: "bg-purple-50 text-purple-600",
light: "from-blue-50 to-cyan-50", orange: "bg-orange-50 text-orange-600",
badge: "bg-blue-100 text-blue-700",
icon: "bg-blue-100/50 text-blue-600",
},
green: {
bg: "from-emerald-600 via-teal-500 to-cyan-500",
text: "text-white",
light: "from-emerald-50 to-teal-50",
badge: "bg-emerald-100 text-emerald-700",
icon: "bg-emerald-100/50 text-emerald-600",
},
purple: {
bg: "from-purple-600 via-pink-500 to-blue-500",
text: "text-white",
light: "from-purple-50 to-blue-50",
badge: "bg-purple-100 text-purple-700",
icon: "bg-purple-100/50 text-purple-600",
},
orange: {
bg: "from-orange-600 via-red-500 to-pink-500",
text: "text-white",
light: "from-orange-50 to-red-50",
badge: "bg-orange-100 text-orange-700",
icon: "bg-orange-100/50 text-orange-600",
},
}; };
const styles = colorStyles[color];
return ( return (
<Card className={`bg-gradient-to-br ${styles.bg} border-0 shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden group h-full`}> <Card className="overflow-hidden h-full">
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2.5 pt-5 px-5"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className={`text-xs font-bold uppercase tracking-wider ${styles.text} leading-tight max-w-[70%]`}> <CardTitle className="text-sm font-medium text-muted-foreground line-clamp-2 break-words flex-1 pr-2">
{title} {title}
</CardTitle> </CardTitle>
<div className={`p-1.5 rounded-lg ${styles.icon} flex-shrink-0`}> <div className={`p-2 rounded-lg ${colorStyles[color]} flex-shrink-0`}>
<Icon size={16} strokeWidth={2} /> <Icon size={16} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-5 pb-5 space-y-2.5"> <CardContent className="overflow-hidden">
<div className={`text-2xl font-black ${styles.text}`}> <div className="text-2xl font-bold line-clamp-2 break-words">{value}</div>
{value}
</div>
{change && ( {change && (
<div className="flex items-center gap-1"> <p className="text-xs text-muted-foreground mt-1 line-clamp-1 break-words">
<span <span
className={`inline-flex items-center gap-0.5 font-bold px-2 py-0.5 rounded-md text-xs tracking-wide ${styles.badge}`} className={`font-medium ${trend === "up"
? "text-green-600"
: trend === "down"
? "text-red-600"
: "text-slate-600"
}`}
> >
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change} {change}
</span> </span>{" "}
<span className="text-xs text-gray-600">vs month</span> vs last month
</div> </p>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,2 +0,0 @@
// Re-export Button component with lowercase filename for compatibility
export { Button } from './Button';

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',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
}
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 };

View File

@ -1,2 +0,0 @@
// Re-export Card components with lowercase filename for compatibility
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';

View File

@ -1,54 +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 overflow-hidden",
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,
<h2 className={`text-lg font-semibold ${className}`}> React.HTMLAttributes<HTMLParagraphElement>
{children} >(({ className, ...props }, ref) => (
</h2> <p
) ref={ref}
} className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
export function CardDescription({ children, className = '' }: CardProps) { const CardContent = React.forwardRef<
return ( HTMLDivElement,
<p className={`text-sm text-gray-600 ${className}`}> React.HTMLAttributes<HTMLDivElement>
{children} >(({ className, ...props }, ref) => (
</p> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
) ))
} CardContent.displayName = "CardContent"
export function CardFooter({ children, className = '' }: CardProps) { const CardFooter = React.forwardRef<
return ( HTMLDivElement,
<div className={`mt-4 ${className}`}> React.HTMLAttributes<HTMLDivElement>
{children} >(({ className, ...props }, ref) => (
</div> <div
) ref={ref}
} className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -265,72 +265,45 @@ export function UserGrid({
}; };
return ( return (
<div className="space-y-4"> <div className="w-full overflow-hidden">
{/* Search and Actions Bar */} <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-gradient-to-r from-slate-50 to-blue-50 p-4 rounded-xl border border-slate-200">
<div className="w-full sm:w-auto flex-1">
<div className="relative">
<svg className="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input <input
type="text" type="text"
placeholder="Search athletes..." placeholder="Search users..."
className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white shadow-sm" className="border border-gray-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white rounded px-4 py-2 flex-1 sm:flex-none w-full sm:w-auto"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> <div className="flex gap-2 flex-wrap">
</div>
<div className="flex gap-2 w-full sm:w-auto">
<button <button
className="flex-1 sm:flex-none bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-4 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed font-semibold transition-all shadow-md hover:shadow-lg" className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50 text-sm"
onClick={handleEdit} onClick={handleEdit}
disabled={selectedUsers.length !== 1} disabled={selectedUsers.length !== 1}
> >
Edit Edit
</button> </button>
<button <button
className="flex-1 sm:flex-none bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-4 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed font-semibold transition-all shadow-md hover:shadow-lg" className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded disabled:opacity-50 text-sm"
onClick={handleDelete} onClick={handleDelete}
disabled={selectedUsers.length !== 1} disabled={selectedUsers.length !== 1}
> >
🗑 Delete Delete
</button> </button>
<button <button
className="flex-1 sm:flex-none bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white px-4 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed font-semibold transition-all shadow-md hover:shadow-lg" className="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded disabled:opacity-50 text-sm"
onClick={handleBulkDelete} onClick={handleBulkDelete}
disabled={selectedUsers.length === 0} disabled={selectedUsers.length === 0}
> >
Bulk Bulk
</button> </button>
</div> </div>
</div> </div>
{/* Table */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
<div <div
className="ag-theme-alpine" className="ag-theme-alpine dark:ag-theme-alpine-dark w-full overflow-hidden rounded-lg"
style={{ height: "600px", width: "100%" }} style={{ height: "400px", width: "100%" }}
> >
<AgGridReact<User> {...gridOptions} ref={gridRef} /> <AgGridReact<User> {...gridOptions} ref={gridRef} />
</div> </div>
</div> </div>
{/* Selected Count */}
{selectedUsers.length > 0 && (
<div className="flex items-center justify-between bg-gradient-to-r from-blue-500/10 to-cyan-500/10 border border-blue-200/50 px-4 py-3 rounded-lg">
<p className="text-sm font-semibold text-blue-700">
{selectedUsers.length} athlete{selectedUsers.length !== 1 ? 's' : ''} selected
</p>
<button
onClick={() => setSelectedUsers([])}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
Clear selection
</button>
</div>
)}
</div>
); );
} }

View File

@ -205,59 +205,20 @@ export function UserManagement() {
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-6">
{/* Header Section */} <div className="flex justify-between items-center">
<div> <h2 className="text-2xl font-bold">User Management</h2>
<h3 className="text-lg font-bold text-gray-900">User Management</h3> <div className="flex gap-2">
<p className="text-sm text-gray-600 mt-1">Manage and monitor your fitness clients</p>
</div>
{/* Filter Buttons */}
<div className="flex flex-wrap gap-2">
<Button <Button
variant={filter === "all" ? "primary" : "secondary"} variant={filter === "all" ? "primary" : "secondary"}
onClick={() => setFilter("all")} onClick={() => setFilter("all")}
className="text-xs"
> >
All Users All Users
</Button> </Button>
<Button
variant={filter === "client" ? "primary" : "secondary"}
onClick={() => setFilter("client")}
className="text-xs"
>
Clients
</Button>
<Button
variant={filter === "trainer" ? "primary" : "secondary"}
onClick={() => setFilter("trainer")}
className="text-xs"
>
Trainers
</Button>
<Button
variant={filter === "admin" ? "primary" : "secondary"}
onClick={() => setFilter("admin")}
className="text-xs"
>
Admins
</Button>
<Button
variant={filter === "superAdmin" ? "primary" : "secondary"}
onClick={() => setFilter("superAdmin")}
className="text-xs"
>
Super Admins
</Button>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-2">
<Button <Button
variant="secondary" variant="secondary"
onClick={() => selectedUser && handleEditUser(selectedUser)} onClick={() => selectedUser && handleEditUser(selectedUser)}
disabled={!selectedUser} disabled={!selectedUser}
className="text-xs"
> >
Edit User Edit User
</Button> </Button>
@ -274,7 +235,6 @@ export function UserManagement() {
setSelectedUser(null); setSelectedUser(null);
setIsEditing(true); setIsEditing(true);
}} }}
className="text-xs"
> >
Invite User Invite User
</Button> </Button>
@ -282,29 +242,55 @@ export function UserManagement() {
variant="secondary" variant="secondary"
onClick={() => selectedUser && handleDeleteUser(selectedUser)} onClick={() => selectedUser && handleDeleteUser(selectedUser)}
disabled={!selectedUser} disabled={!selectedUser}
className="text-xs"
> >
Delete User Delete User
</Button> </Button>
<Button variant="secondary" onClick={handleRefresh} className="text-xs"> <Button
Refresh variant={filter === "client" ? "primary" : "secondary"}
onClick={() => setFilter("client")}
>
Clients
</Button> </Button>
<Button variant="secondary" onClick={handleExport} className="text-xs"> <Button
Export CSV 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> </Button>
</div> </div>
</div>
{/* Stats */} <div className="flex justify-between items-center">
<div className="text-sm text-gray-600 px-4 py-2 bg-gray-50 rounded-lg"> <div className="text-sm text-gray-600">
Showing {users.length} users Showing {users.length} users
{selectedUser && ( {selectedUser && (
<span className="ml-4 text-blue-600 font-medium"> <span className="ml-4 text-blue-600">
Selected: {selectedUser.firstName} {selectedUser.lastName} Selected: {selectedUser.firstName} {selectedUser.lastName}
</span> </span>
)} )}
</div> </div>
<div className="flex gap-2">
<Button variant="secondary" onClick={handleRefresh}>
Refresh
</Button>
<Button variant="secondary" onClick={handleExport}>
Export CSV
</Button>
</div>
</div>
{/* User Grid */}
<Card> <Card>
<CardContent className="p-0"> <CardContent className="p-0">
<UserGrid <UserGrid

View File

@ -10,7 +10,6 @@ import { TrackMealModal } from "../../components/TrackMealModal";
import { AddWaterModal } from "../../components/AddWaterModal"; import { AddWaterModal } from "../../components/AddWaterModal";
import { HydrationWidget } from "../../components/HydrationWidget"; import { HydrationWidget } from "../../components/HydrationWidget";
import { ScanFoodModal } from "../../components/ScanFoodModal"; import { ScanFoodModal } from "../../components/ScanFoodModal";
import { PerformanceMetrics } from "../../components/PerformanceMetrics";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
export default function HomeScreen() { export default function HomeScreen() {
@ -132,9 +131,6 @@ export default function HomeScreen() {
duration={45} duration={45}
/> />
{/* Performance Metrics */}
<PerformanceMetrics />
{/* Hydration Widget */} {/* Hydration Widget */}
<HydrationWidget <HydrationWidget
current={waterIntake} current={waterIntake}

View File

@ -1,224 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
interface MetricProps {
title: string;
value: string | number;
change: string;
trend: 'up' | 'down';
icon: string;
colorScheme: 'blue' | 'green' | 'purple' | 'orange';
}
const getColorScheme = (colorScheme: 'blue' | 'green' | 'purple' | 'orange') => {
const schemes = {
blue: {
colors: ['#1e3a8a', '#0c4a6e'],
text: '#ffffff',
label: '#93c5fd',
badge: '#3b82f6',
badgeText: '#ffffff',
icon: '#60a5fa',
},
green: {
colors: ['#064e3b', '#047857'],
text: '#ffffff',
label: '#86efac',
badge: '#10b981',
badgeText: '#ffffff',
icon: '#34d399',
},
purple: {
colors: ['#581c87', '#7c3aed'],
text: '#ffffff',
label: '#d8b4fe',
badge: '#a855f7',
badgeText: '#ffffff',
icon: '#c084fc',
},
orange: {
colors: ['#92400e', '#b45309'],
text: '#ffffff',
label: '#fcd34d',
badge: '#f97316',
badgeText: '#ffffff',
icon: '#fb923c',
},
};
return schemes[colorScheme];
};
const MetricCard: React.FC<MetricProps> = ({ title, value, change, trend, icon, colorScheme }) => {
const colors = getColorScheme(colorScheme);
const isPositive = trend === 'up';
const trendIcon = isPositive ? 'arrow-up' : 'arrow-down';
return (
<LinearGradient
colors={colors.colors as any}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.card}
>
<View style={styles.cardContent}>
<View style={styles.header}>
<Text style={[styles.label, { color: colors.label }]}>{title}</Text>
<View style={[styles.iconContainer, { backgroundColor: colors.icon + '40' }]}>
<Ionicons name={icon as any} size={20} color={colors.text} />
</View>
</View>
<Text style={[styles.value, { color: colors.text }]}>{value}</Text>
<View style={styles.changeContainer}>
<View style={[styles.trendBadge, { backgroundColor: colors.badge }]}>
<Ionicons
name={trendIcon}
size={12}
color={colors.badgeText}
style={{ marginRight: 4 }}
/>
<Text style={[styles.changeText, { color: colors.badgeText, fontWeight: '700' }]}>
{trend === 'up' ? '↑' : '↓'} {change}
</Text>
</View>
<Text style={[styles.compareText, { color: colors.text }]}>vs month</Text>
</View>
</View>
</LinearGradient>
);
};
export const PerformanceMetrics: React.FC = () => {
const metrics: MetricProps[] = [
{
title: 'Total Users',
value: '0',
change: '+12%',
trend: 'up',
icon: 'people',
colorScheme: 'blue',
},
{
title: 'Active Clients',
value: '0',
change: '+5%',
trend: 'up',
icon: 'person-add',
colorScheme: 'green',
},
{
title: 'Revenue',
value: '$0.00',
change: '0%',
trend: 'down',
icon: 'wallet',
colorScheme: 'purple',
},
{
title: 'Growth',
value: '24%',
change: '-2%',
trend: 'down',
icon: 'trending-up',
colorScheme: 'orange',
},
];
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Performance metrics & athlete insights</Text>
</View>
<View style={styles.metricsGrid}>
{metrics.map((metric, index) => (
<View key={index} style={styles.metricWrapper}>
<MetricCard {...metric} />
</View>
))}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
marginBottom: 24,
},
header: {
marginBottom: 16,
},
title: {
fontSize: 14,
fontWeight: '600',
color: '#4b5563',
letterSpacing: 0.3,
},
metricsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
gap: 12,
},
metricWrapper: {
width: '48%',
},
card: {
borderRadius: 20,
overflow: 'hidden',
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 12,
},
cardContent: {
padding: 16,
minHeight: 160,
justifyContent: 'space-between',
},
iconContainer: {
width: 36,
height: 36,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
label: {
fontSize: 11,
fontWeight: '700',
letterSpacing: 0.6,
marginBottom: 12,
textTransform: 'uppercase',
},
value: {
fontSize: 28,
fontWeight: '800',
marginBottom: 12,
},
changeContainer: {
alignItems: 'flex-start',
},
trendBadge: {
flexDirection: 'row',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
alignItems: 'center',
marginBottom: 6,
},
changeText: {
fontSize: 11,
fontWeight: '700',
},
compareText: {
fontSize: 10,
fontWeight: '500',
},
});

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Modal, TouchableOpacity, TextInput, Alert, Platform } from 'react-native'; import { View, Text, StyleSheet, Modal, TouchableOpacity, TextInput, Alert } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera'; import { CameraView, useCameraPermissions } from 'expo-camera';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
@ -20,7 +20,7 @@ const FOOD_DATABASE: { [key: string]: { name: string; calories: number; servingS
}; };
export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProps) { export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProps) {
const [permission, requestPermission] = Platform.OS === 'web' ? [null, null] : useCameraPermissions() as any; const [permission, requestPermission] = useCameraPermissions();
const [scanned, setScanned] = useState(false); const [scanned, setScanned] = useState(false);
const [foodData, setFoodData] = useState<{ name: string; calories: number; servingSize: string } | null>(null); const [foodData, setFoodData] = useState<{ name: string; calories: number; servingSize: string } | null>(null);
const [servings, setServings] = useState('1'); const [servings, setServings] = useState('1');
@ -68,29 +68,10 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
}; };
if (!permission) { if (!permission) {
if (Platform.OS === 'web') {
// On web, show normal modal without permissions
return (
<Modal visible={visible} animationType="slide">
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Ionicons name="close" size={28} color="#fff" />
</TouchableOpacity>
<Text style={styles.title}>Scan Food Barcode</Text>
<View style={{ width: 28 }} />
</View>
<View style={styles.webPlaceholder}>
<Text style={styles.webText}>Barcode scanning is only available on mobile devices</Text>
</View>
</View>
</Modal>
);
}
return null; return null;
} }
if (!permission.granted && Platform.OS !== 'web') { if (!permission.granted) {
return ( return (
<Modal visible={visible} transparent animationType="slide"> <Modal visible={visible} transparent animationType="slide">
<View style={styles.permissionContainer}> <View style={styles.permissionContainer}>
@ -130,11 +111,6 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
<View style={{ width: 28 }} /> <View style={{ width: 28 }} />
</View> </View>
{Platform.OS === 'web' ? (
<View style={styles.webPlaceholder}>
<Text style={styles.webText}>Barcode scanning is only available on mobile devices</Text>
</View>
) : (
<CameraView <CameraView
style={styles.camera} style={styles.camera}
facing="back" facing="back"
@ -148,7 +124,6 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
<Text style={styles.scanText}>Position barcode within frame</Text> <Text style={styles.scanText}>Position barcode within frame</Text>
</View> </View>
</CameraView> </CameraView>
)}
</> </>
) : ( ) : (
<View style={styles.resultContainer}> <View style={styles.resultContainer}>
@ -255,18 +230,6 @@ const styles = StyleSheet.create({
camera: { camera: {
flex: 1, flex: 1,
}, },
webPlaceholder: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1a1a1a',
},
webText: {
color: '#999',
fontSize: 16,
textAlign: 'center',
paddingHorizontal: 40,
},
scanOverlay: { scanOverlay: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',

View File

@ -1,17 +1,13 @@
import React from "react"; import React from "react";
import * as WebBrowser from "expo-web-browser"; import * as WebBrowser from "expo-web-browser";
import { Platform } from "react-native";
export const useWarmUpBrowser = () => { export const useWarmUpBrowser = () => {
React.useEffect(() => { React.useEffect(() => {
// Warm up the android browser to improve UX // Warm up the android browser to improve UX
// https://docs.expo.dev/guides/authentication/#improving-user-experience // https://docs.expo.dev/guides/authentication/#improving-user-experience
// Only available on native platforms (iOS/Android), not on web
if (Platform.OS !== "web") {
void WebBrowser.warmUpAsync(); void WebBrowser.warmUpAsync();
return () => { return () => {
void WebBrowser.coolDownAsync(); void WebBrowser.coolDownAsync();
}; };
}
}, []); }, []);
}; };

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
```

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
```