Compare commits

...

28 Commits

Author SHA1 Message Date
8c2a3daee0 style: apply kameleon gradient transitions to analytics cards - all metrics and charts with via-colors 2025-12-11 14:16:41 +01:00
19ece90c6f style: adjust card sizing to balanced medium - between original and enlarged 2025-12-11 14:15:28 +01:00
b9efedbb88 style: add chameleon gradient transitions with via colors for smooth color blending 2025-12-11 14:13:10 +01:00
204e6edca5 style: apply strong gradient colors to stats cards instead of light backgrounds 2025-12-11 14:11:22 +01:00
918357c00c fix: add missing CardTitle, CardDescription and CardFooter exports to Card component 2025-12-11 14:10:33 +01:00
4fbe8399df fix: add card.ts compatibility layer for lowercase Card imports 2025-12-11 14:09:37 +01:00
61bad8d30c fix: add button.ts compatibility layer and fix all Button imports to use lowercase 2025-12-11 14:08:58 +01:00
966bcb084d style: implement dark gradient metric cards with high contrast white text and colored labels 2025-12-11 14:01:32 +01:00
71a05c51bf fix: update mobile metric cards with proper 500-range vibrant colors matching admin dashboard 2025-12-11 14:00:03 +01:00
e4eadc858c style: strengthen performance metrics colors with darker gradients and brighter white text 2025-12-11 13:56:18 +01:00
0024510fdb feat: add performance metrics dashboard to mobile home screen with gradient cards 2025-12-11 13:54:01 +01:00
c9fbab3bbb style: revert background to light theme and update card colors to match sidebar palette 2025-12-11 13:51:36 +01:00
35d64ef3c0 style: apply bold modern colors throughout dashboard - dark backgrounds, vibrant gradients, white text 2025-12-11 13:48:30 +01:00
6dc90f1745 feat: reorganize dashboard layout with UserManagement full-width and horizontal analytics charts 2025-12-11 13:44:02 +01:00
02452c11e6 fix: simplify AnalyticsDashboard layout to prevent text overflow and improve stability 2025-12-11 13:41:57 +01:00
dd78602dd6 style: improve UserManagement layout with flex-wrap and better organization 2025-12-11 13:39:42 +01:00
0a55438bce fix: remove extra closing brace in StatsCard 2025-12-11 13:38:03 +01:00
51df7f57ec style: compact stats cards with better colors and layout 2025-12-11 13:37:01 +01:00
feacef2a81 fix: remove all duplicate closing tags in StatsCard 2025-12-11 13:35:37 +01:00
77546a5017 fix: remove duplicate code in StatsCard 2025-12-11 13:34:40 +01:00
d38f0a4cc2 style: enhance card colors and improve typography hierarchy 2025-12-11 13:32:35 +01:00
b6764b5ed6 fix: remove duplicate closing brackets in Sidebar 2025-12-11 13:29:36 +01:00
e84d0f5f8f feat: modernize admin dashboard with premium sports design 2025-12-11 13:27:57 +01:00
fda66a7703 fix: completely disable camera/barcode on web platform 2025-12-11 13:23:56 +01:00
2d9fbd186b fix: disable barcode scanner on web platform 2025-12-11 13:19:09 +01:00
bbd0cfde9c fix: add platform check for WebBrowser warmUpAsync on web 2025-12-11 13:15:33 +01:00
16e22f330a fix: update route params to Promise type for Next.js 16 compatibility 2025-12-11 13:10:51 +01:00
efa2f2a8d8 fix: correct Button import paths to use lowercase button 2025-12-11 13:04:50 +01:00
22 changed files with 911 additions and 410 deletions

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: { id: string } } { params }: { params: Promise<{ 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: { id: string } } { params }: { params: Promise<{ 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: { id: string } } { params }: { params: Promise<{ 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: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();

View File

@ -32,7 +32,7 @@
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%; --ring: 222.2 84% 4.9%;
--radius: 0.5rem; --radius: 0.75rem;
} }
.dark { .dark {

View File

@ -25,11 +25,34 @@ export default function RootLayout({
return ( return (
<ClerkProvider> <ClerkProvider>
<html lang="en"> <html lang="en">
<body className={inter.className}> <body className={`${inter.className} bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100 min-h-screen`}>
<div className="flex min-h-screen bg-slate-50"> <div className="flex min-h-screen">
<Sidebar /> <Sidebar />
<main className="flex-1 ml-64 p-8"> <main className="flex-1 ml-64">
{children} <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}
</div>
</main> </main>
</div> </div>
</body> </body>

View File

@ -46,56 +46,78 @@ export default function Home() {
}; };
return ( return (
<div className="space-y-8"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100">
<div> <div className="space-y-8 max-w-7xl">
<h2 className="text-3xl font-bold text-slate-900">Dashboard</h2> {/* Hero Section */}
<p className="text-slate-500 mt-2">Welcome back, here's what's happening today.</p> <div className="pt-8 pb-4">
</div> <div className="space-y-3">
<div className="flex items-center gap-3">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="h-12 w-1 bg-gradient-to-b from-blue-600 to-cyan-600 rounded-full"></div>
<StatsCard <h1 className="text-5xl font-black bg-gradient-to-r from-blue-600 via-blue-700 to-cyan-600 bg-clip-text text-transparent">
title="Total Users" FitAI Dashboard
value={loading ? "..." : stats.totalUsers} </h1>
change="+12%" // Placeholder for now as we don't track historical growth yet </div>
trend="up" <p className="text-lg text-gray-600 ml-4">Performance metrics & athlete insights</p>
icon={Users} </div>
color="blue"
/>
<StatsCard
title="Active Clients"
value={loading ? "..." : stats.activeClients}
change="+5%"
trend="up"
icon={CalendarCheck}
color="green"
/>
<StatsCard
title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}%`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={CreditCard}
color="purple"
/>
<StatsCard
title="Growth"
value="24%" // Placeholder
change="-2%"
trend="down"
icon={TrendingUp}
color="orange"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Recent Activity</h3>
<UserManagement />
</div> </div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6"> {/* Stats Grid */}
<h3 className="text-xl font-bold text-slate-900 mb-6">Quick Analytics</h3> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
<AnalyticsDashboard /> <StatsCard
title="Total Users"
value={loading ? "..." : stats.totalUsers}
change="+12%" // Placeholder for now as we don't track historical growth yet
trend="up"
icon={Users}
color="blue"
/>
<StatsCard
title="Active Clients"
value={loading ? "..." : stats.activeClients}
change="+5%"
trend="up"
icon={CalendarCheck}
color="green"
/>
<StatsCard
title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}%`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={CreditCard}
color="purple"
/>
<StatsCard
title="Growth"
value="24%" // Placeholder
change="-2%"
trend="down"
icon={TrendingUp}
color="orange"
/>
</div>
{/* Main Content Grid */}
<div className="space-y-6 pb-12">
{/* User Management - Full Width */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/80 p-7 hover:shadow-xl transition-shadow">
<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>
<UserManagement />
</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>
<AnalyticsDashboard />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
interface UserProfile { interface UserProfile {

View File

@ -6,7 +6,7 @@ import { usePathname } from "next/navigation";
import { Home, Users, BarChart3, User, Brain } from "lucide-react"; import { Home, Users, BarChart3, User, Brain } from "lucide-react";
import { SignedIn, UserButton } from "@clerk/nextjs"; import { SignedIn, UserButton } from "@clerk/nextjs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/button";
interface NavItem { interface NavItem {
href: string; href: string;

View File

@ -67,74 +67,75 @@ 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-lg">Loading analytics...</div> <div className="text-center">
<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-6"> <div className="space-y-4">
<h2 className="text-2xl font-bold">Analytics Dashboard</h2> {/* Key Metrics Cards - 3 columns */}
<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">
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Athletes</p>
<div className="flex items-baseline gap-2">
<div className="text-2xl font-black text-white">{totalUsers}</div>
<span className="text-xs text-white font-semibold">active</span>
</div>
</div>
{/* Key Metrics */} <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">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Revenue</p>
<Card> <div className="flex items-baseline gap-2">
<CardContent> <div className="text-2xl font-black text-white">${totalRevenue.toLocaleString()}</div>
<div className="text-center"> <span className="text-xs text-white font-semibold">ytd</span>
<div className="text-3xl font-bold text-blue-600">{totalUsers}</div> </div>
<div className="text-gray-600">Total Users</div> </div>
</div>
</CardContent>
</Card>
<Card> <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">
<CardContent> <p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Active Members</p>
<div className="text-center"> <div className="flex items-baseline gap-2">
<div className="text-3xl font-bold text-green-600">${totalRevenue.toLocaleString()}</div> <div className="text-2xl font-black text-white">{activeMembers}</div>
<div className="text-gray-600">Total Revenue</div> <span className="text-xs text-white font-semibold">members</span>
</div> </div>
</CardContent> </div>
</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>
{/* Charts */} {/* Charts - 3 Columns Horizontal */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Card> <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">
<CardHeader> <div className="mb-3">
<h3 className="text-lg font-semibold">User Growth</h3> <h3 className="text-sm font-bold text-white">User Growth Trend</h3>
</CardHeader> <p className="text-xs text-blue-100">Last 6 months performance</p>
<CardContent> </div>
<div className="h-48 overflow-auto">
<UserGrowthChart data={userGrowthData} /> <UserGrowthChart data={userGrowthData} />
</CardContent> </div>
</Card> </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">
<CardHeader> <div className="mb-3">
<h3 className="text-lg font-semibold">Membership Distribution</h3> <h3 className="text-sm font-bold text-white">Membership Mix</h3>
</CardHeader> <p className="text-xs text-emerald-100">Distribution breakdown</p>
<CardContent> </div>
<div className="h-48 overflow-auto">
<MembershipDistributionChart data={membershipData} /> <MembershipDistributionChart data={membershipData} />
</CardContent> </div>
</Card> </div>
</div>
<Card> <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">
<CardHeader> <div className="mb-3">
<h3 className="text-lg font-semibold">Monthly Revenue</h3> <h3 className="text-sm font-bold text-white">Revenue Stream</h3>
</CardHeader> <p className="text-xs text-purple-100">Monthly earnings</p>
<CardContent> </div>
<RevenueChart data={revenueData} /> <div className="h-48 overflow-auto">
</CardContent> <RevenueChart data={revenueData} />
</Card> </div>
</div>
</div>
</div> </div>
) )
} }

View File

@ -65,14 +65,24 @@ export function Sidebar() {
]; ];
return ( return (
<aside className="w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800"> <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">
<div className="p-6 border-b border-slate-800"> {/* Logo Section */}
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent"> <div className="p-6 border-b border-slate-800/50">
FitAI Admin <div className="flex items-center gap-3">
</h1> <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">
</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>
<nav className="flex-1 p-4 space-y-2"> {/* Navigation */}
<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;
@ -81,26 +91,30 @@ 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-lg transition-all duration-200 group ${isActive className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group relative overflow-hidden ${isActive
? "bg-blue-600 text-white shadow-lg shadow-blue-900/20" ? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white shadow-lg shadow-blue-900/30"
: "text-slate-400 hover:bg-slate-800 hover:text-white"}`} : "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
> >
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} /> {isActive && <div className="absolute inset-0 bg-white/10 blur-xl"></div>}
<span className="font-medium">{label}</span> <Icon size={20} className={`${isActive ? "text-white" : "text-slate-500 group-hover:text-white"} relative z-10`} />
<span className="font-semibold relative z-10">{label}</span>
</Link> </Link>
); );
})} })}
</nav> </nav>
<div className="p-4 border-t border-slate-800"> {/* User Section */}
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-slate-800/50"> <div className="p-4 border-t border-slate-800/50 bg-slate-800/20">
<UserButton afterSignOutUrl="/" /> <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="/" />
</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate"> <p className="text-sm font-semibold text-white truncate">
{user?.fullName || "Admin User"} {user?.fullName || "Admin"}
</p> </p>
<p className="text-xs text-slate-400 truncate"> <p className="text-xs text-slate-400 truncate">
{user?.primaryEmailAddress?.emailAddress} {user?.emailAddresses[0]?.emailAddress || "admin@fitai.com"}
</p> </p>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

@ -1,7 +1,7 @@
"use client"; "use client";
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 {

View File

@ -265,45 +265,72 @@ export function UserGrid({
}; };
return ( return (
<div> <div className="space-y-4">
<div className="flex justify-between items-center mb-4"> {/* Search and Actions Bar */}
<input <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">
type="text" <div className="w-full sm:w-auto flex-1">
placeholder="Search users..." <div className="relative">
className="border border-gray-300 rounded px-4 py-2" <svg className="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
value={searchQuery} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
onChange={(e) => setSearchQuery(e.target.value)} </svg>
/> <input
<div className="flex gap-2"> type="text"
placeholder="Search athletes..."
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"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<div className="flex gap-2 w-full sm:w-auto">
<button <button
className="bg-green-500 text-white px-4 py-2 rounded disabled:opacity-50" 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"
onClick={handleEdit} onClick={handleEdit}
disabled={selectedUsers.length !== 1} disabled={selectedUsers.length !== 1}
> >
Edit User Edit
</button> </button>
<button <button
className="bg-red-500 text-white px-4 py-2 rounded disabled:opacity-50" 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"
onClick={handleDelete} onClick={handleDelete}
disabled={selectedUsers.length !== 1} disabled={selectedUsers.length !== 1}
> >
Delete User 🗑 Delete
</button> </button>
<button <button
className="bg-yellow-500 text-white px-4 py-2 rounded disabled:opacity-50" 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"
onClick={handleBulkDelete} onClick={handleBulkDelete}
disabled={selectedUsers.length === 0} disabled={selectedUsers.length === 0}
> >
Bulk Delete Bulk
</button> </button>
</div> </div>
</div> </div>
<div
className="ag-theme-alpine" {/* Table */}
style={{ height: "600px", width: "100%" }} <div className="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
> <div
<AgGridReact<User> {...gridOptions} ref={gridRef} /> className="ag-theme-alpine"
style={{ height: "600px", width: "100%" }}
>
<AgGridReact<User> {...gridOptions} ref={gridRef} />
</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> </div>
); );
} }

View File

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

View File

@ -10,6 +10,7 @@ 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() {
@ -131,6 +132,9 @@ export default function HomeScreen() {
duration={45} duration={45}
/> />
{/* Performance Metrics */}
<PerformanceMetrics />
{/* Hydration Widget */} {/* Hydration Widget */}
<HydrationWidget <HydrationWidget
current={waterIntake} current={waterIntake}

View File

@ -0,0 +1,224 @@
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 } from 'react-native'; import { View, Text, StyleSheet, Modal, TouchableOpacity, TextInput, Alert, Platform } 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] = useCameraPermissions(); const [permission, requestPermission] = Platform.OS === 'web' ? [null, null] : useCameraPermissions() as any;
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,10 +68,29 @@ 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) { if (!permission.granted && Platform.OS !== 'web') {
return ( return (
<Modal visible={visible} transparent animationType="slide"> <Modal visible={visible} transparent animationType="slide">
<View style={styles.permissionContainer}> <View style={styles.permissionContainer}>
@ -111,19 +130,25 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
<View style={{ width: 28 }} /> <View style={{ width: 28 }} />
</View> </View>
<CameraView {Platform.OS === 'web' ? (
style={styles.camera} <View style={styles.webPlaceholder}>
facing="back" <Text style={styles.webText}>Barcode scanning is only available on mobile devices</Text>
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ['ean13', 'ean8', 'upc_a', 'upc_e', 'code128', 'code39'],
}}
>
<View style={styles.scanOverlay}>
<View style={styles.scanFrame} />
<Text style={styles.scanText}>Position barcode within frame</Text>
</View> </View>
</CameraView> ) : (
<CameraView
style={styles.camera}
facing="back"
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ['ean13', 'ean8', 'upc_a', 'upc_e', 'code128', 'code39'],
}}
>
<View style={styles.scanOverlay}>
<View style={styles.scanFrame} />
<Text style={styles.scanText}>Position barcode within frame</Text>
</View>
</CameraView>
)}
</> </>
) : ( ) : (
<View style={styles.resultContainer}> <View style={styles.resultContainer}>
@ -230,6 +255,18 @@ 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,13 +1,17 @@
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
void WebBrowser.warmUpAsync(); // Only available on native platforms (iOS/Android), not on web
return () => { if (Platform.OS !== "web") {
void WebBrowser.coolDownAsync(); void WebBrowser.warmUpAsync();
}; return () => {
void WebBrowser.coolDownAsync();
};
}
}, []); }, []);
}; };

View File

@ -1,15 +1,174 @@
## fitai # FitAI
# description Integrated AI solution for fitness houses and their clients with Clerk authentication.
- fitai is integrated ai solution for fitness houses and their clients, ## Project Structure
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 ```
we will be tracking user inputs via manual input and devices, backend will analyze data and propose fitai/
excercises etc. ├── apps/
│ ├── admin/ # Next.js admin dashboard
│ └── mobile/ # React Native mobile app (Expo)
├── packages/
│ └── shared/ # Shared types and utilities
└── AGENTS.md # Development guidelines
```
## Getting Started
### Prerequisites
- Node.js >= 18.0.0
- npm >= 9.0.0
- Clerk account (sign up at https://clerk.com)
### Installation
```bash
# Install root dependencies
npm install
# Install admin dependencies
cd apps/admin && npm install
# Install mobile dependencies
cd apps/mobile && npm install --legacy-peer-deps
```
### Authentication Setup
FitAI uses Clerk for authentication. Follow these steps:
1. **Create a Clerk account** at https://dashboard.clerk.com
2. **Create a new application** in the Clerk dashboard
3. **Copy your API keys** (Publishable Key and Secret Key)
4. **Configure environment variables**:
**Admin App** (`apps/admin/.env.local`):
```env
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
CLERK_SECRET_KEY=sk_test_your_key_here
```
**Mobile App** (`apps/mobile/.env`):
```env
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
```
📖 **See [CLERK_SETUP.md](./CLERK_SETUP.md) for detailed setup instructions**
### Development
**Important**: Set up environment variables before running the apps!
```bash
# Admin dashboard (http://localhost:3000)
cd apps/admin && npm run dev
# Mobile app (http://localhost:8081) - Requires Expo SDK 54
cd apps/mobile && npm start
```
**First-time setup checklist**:
- [ ] Create Clerk account and application
- [ ] Add API keys to `.env.local` (admin) and `.env` (mobile)
- [ ] Verify both apps start without errors
- [ ] Test sign-up and sign-in flows
### Mobile App Setup
- **Expo SDK**: 50 (stable, compatible with Expo Go)
- **Assets**: Placeholder icons and splash screen included
- **Navigation**: Expo Router with tab-based layout
- **Authentication**: Secure storage with expo-secure-store
- **Babel**: babel-preset-expo for proper transpilation
### Known Compatibility Notes
- Use Expo Go with SDK 50 for mobile testing
- For SDK 54, upgrade all dependencies to latest versions
- Current setup prioritizes stability over latest features
### Build & Test
```bash
# Build all apps
npm run build
# Run tests
npm test
# Lint code
npm run lint
# Type checking
npm run typecheck
```
## Features
### Authentication (Clerk)
- 🔐 Secure email/password authentication
- ✉️ Email verification
- 🔄 Session management
- 🎨 Customizable UI components
- 📱 Multi-platform support (Web + Mobile)
- 🛡️ Built-in security features
### Admin Dashboard
- 👥 User management (CRUD operations)
- 📊 Analytics dashboard with charts
- 🎯 Role-based access control
- 📈 Data visualization with AG Grid
- 💳 Payment tracking (coming soon)
- 📅 Attendance monitoring (coming soon)
### Mobile App
- 🔐 Secure sign-in/sign-up
- 👤 User profile management
- 📱 Native mobile experience
- 🔔 Push notifications ready
- ✅ Attendance check-in (coming soon)
- 💰 Payment history (coming soon)
## Tech Stack
### Authentication
- **Clerk**: Complete authentication and user management platform
### Frontend
- **Admin**: Next.js 14 (App Router), React 19, TypeScript, Tailwind CSS
- **Mobile**: React Native, Expo SDK 54, Expo Router, TypeScript
### Backend & Database
- **Database**: SQLite with Drizzle ORM
- **API**: Next.js API Routes (REST)
### Development Tools
- **State Management**: React Query, React Hook Form
- **Validation**: Zod schemas
- **Data Grid**: AG Grid for advanced user management
- **Charts**: AG Charts for analytics and visualization
- **Testing**: Jest, Testing Library (configured)
## Project Structure
```
fitai/
├── apps/
│ ├── admin/ # Next.js admin dashboard
│ │ ├── src/
│ │ │ ├── app/ # App Router pages & API routes
│ │ │ ├── components/
│ │ │ └── lib/ # Database & utilities
│ │ └── .env.local # Admin environment variables
│ │
│ └── mobile/ # Expo React Native app
│ ├── src/
│ │ ├── app/ # Expo Router screens
│ │ │ ├── (auth)/ # Authentication screens
│ │ │ └── (tabs)/ # Main app tabs
│ │ └── components/
│ └── .env # Mobile environment variables
├── packages/
│ ├── database/ # Drizzle ORM schemas & DB client
│ └── shared/ # Shared types & utilities
└── CLERK_SETUP.md # Detailed authentication setup guide
```