Compare commits

..
1 Commits
40 changed files with 542 additions and 1382 deletions

No files matched your search

+40 -21
View File
@@ -1,28 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600"> <svg viewBox="0 0 600 700" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2FB7E8;stop-opacity:1" /> <stop offset="0%" style="stop-color:#1e40af;stop-opacity:1" />
<stop offset="50%" style="stop-color:#0B9FD0;stop-opacity:1" /> <stop offset="50%" style="stop-color:#0369a1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0B7FB3;stop-opacity:1" /> <stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
</linearGradient> </linearGradient>
</defs> </defs>
<!-- Main arrow shape --> <!-- Main rounded shape background -->
<path d="M 420 150 L 540 150 L 420 300 Z" fill="url(#grad1)" opacity="0.95"/> <path d="M 150 80 Q 350 50 450 150 L 480 300 Q 500 450 380 550 Q 200 650 100 550 Q 30 450 50 300 L 80 150 Q 100 80 150 80 Z" fill="url(#logoGrad)"/>
<path d="M 180 150 L 300 300 L 180 300 Z" fill="url(#grad1)" opacity="0.85"/>
<!-- Dumbbell - Left --> <!-- Arrow pointing up-right (top right) -->
<g transform="translate(200, 250)"> <g transform="translate(340, 100)">
<!-- Left disk --> <path d="M 0 80 L 0 0 M 0 0 L -25 25 M 0 0 L 25 25" stroke="white" stroke-width="20" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="0" cy="0" r="35" fill="white" opacity="0.95"/>
<!-- Bar left -->
<rect x="35" y="-15" width="80" height="30" fill="white" rx="4"/>
<!-- Right disk -->
<circle cx="115" cy="0" r="35" fill="white" opacity="0.95"/>
<!-- Grip lines -->
<line x1="45" y1="-35" x2="45" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
<line x1="65" y1="-35" x2="65" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
<line x1="85" y1="-35" x2="85" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
<line x1="105" y1="-35" x2="105" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
</g> </g>
<!-- Hand holding dumbbell (left side) -->
<g transform="translate(80, 180)">
<!-- Upper arm -->
<ellipse cx="40" cy="30" rx="25" ry="60" fill="white" opacity="0.95" transform="rotate(-30 40 30)"/>
<!-- Forearm and hand -->
<path d="M 60 80 Q 100 100 140 85" stroke="white" stroke-width="35" fill="none" stroke-linecap="round" opacity="0.95"/>
<!-- Fingers -->
<circle cx="145" cy="75" r="15" fill="white" opacity="0.95"/>
<circle cx="155" cy="90" r="12" fill="white" opacity="0.95"/>
<circle cx="150" cy="105" r="13" fill="white" opacity="0.95"/>
<!-- Dumbbell bar -->
<rect x="155" y="75" width="110" height="28" rx="14" fill="white" opacity="0.95"/>
<!-- Left weight plate -->
<circle cx="155" cy="89" r="45" fill="white" opacity="0.95"/>
<circle cx="155" cy="89" r="38" fill="url(#logoGrad)" opacity="0.4"/>
<rect x="145" y="130" width="20" height="30" fill="white" opacity="0.95" rx="3"/>
<!-- Right weight plate -->
<circle cx="265" cy="89" r="45" fill="white" opacity="0.95"/>
<circle cx="265" cy="89" r="38" fill="url(#logoGrad)" opacity="0.4"/>
<rect x="255" y="130" width="20" height="30" fill="white" opacity="0.95" rx="3"/>
</g>
<!-- Text at bottom -->
<text x="300" y="630" font-size="85" font-weight="900" fill="url(#logoGrad)" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" letter-spacing="2">NextForm</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

+28 -21
View File
@@ -1,28 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600"> <svg viewBox="0 0 600 700" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2FB7E8;stop-opacity:1" /> <stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="50%" style="stop-color:#0B9FD0;stop-opacity:1" /> <stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0B7FB3;stop-opacity:1" />
</linearGradient> </linearGradient>
</defs> </defs>
<!-- Main arrow shape --> <!-- Main Shape Background -->
<path d="M 420 150 L 540 150 L 420 300 Z" fill="url(#grad1)" opacity="0.95"/> <path d="M 150 150 L 350 150 L 450 250 L 450 450 L 350 550 L 150 550 L 50 450 L 50 250 Z" fill="url(logoGradient)" opacity="0.15"/>
<path d="M 180 150 L 300 300 L 180 300 Z" fill="url(#grad1)" opacity="0.85"/>
<!-- Dumbbell - Left --> <!-- Dumbbell Icon -->
<g transform="translate(200, 250)"> <g transform="translate(200, 200)">
<!-- Left disk --> <!-- Left Weight -->
<circle cx="0" cy="0" r="35" fill="white" opacity="0.95"/> <circle cx="20" cy="60" r="35" fill="url(logoGradient)"/>
<!-- Bar left --> <rect x="10" y="70" width="20" height="80" fill="url(logoGradient)"/>
<rect x="35" y="-15" width="80" height="30" fill="white" rx="4"/>
<!-- Right disk --> <!-- Bar -->
<circle cx="115" cy="0" r="35" fill="white" opacity="0.95"/> <rect x="35" y="100" width="130" height="25" rx="12" fill="url(logoGradient)"/>
<!-- Grip lines -->
<line x1="45" y1="-35" x2="45" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/> <!-- Right Weight -->
<line x1="65" y1="-35" x2="65" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/> <circle cx="180" cy="60" r="35" fill="url(logoGradient)"/>
<line x1="85" y1="-35" x2="85" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/> <rect x="170" y="70" width="20" height="80" fill="url(logoGradient)"/>
<line x1="105" y1="-35" x2="105" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
<!-- Hand -->
<ellipse cx="80" cy="20" rx="25" ry="30" fill="url(logoGradient)"/>
<path d="M 70 0 Q 65 -15 60 -20" stroke="url(logoGradient)" stroke-width="8" fill="none" stroke-linecap="round"/>
<path d="M 85 -5 Q 88 -20 90 -28" stroke="url(logoGradient)" stroke-width="8" fill="none" stroke-linecap="round"/>
</g>
<!-- Arrow/Growth -->
<g transform="translate(320, 150)">
<path d="M 20 100 L 80 40 L 80 70 L 120 30 L 80 30 L 80 0 Z" fill="url(logoGradient)"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

-1
View File
@@ -1 +0,0 @@
PNG - NextForm Logo
@@ -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();
@@ -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();
+1 -1
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.75rem; --radius: 0.5rem;
} }
.dark { .dark {
+4 -27
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"> {children}
<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>
+80 -48
View File
@@ -46,83 +46,115 @@ export default function Home() {
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100"> <div className="space-y-8">
<div className="space-y-8 max-w-7xl"> {/* Header with Logo and App Name */}
{/* Hero Section */} <div className="flex items-center gap-4 pb-6 border-b border-slate-200">
<div className="pt-8 pb-4"> <div className="w-20 h-20">
<div className="space-y-3"> <img src="/nextform-logo.png" alt="NextForm Logo" className="w-full h-full object-contain" />
<div className="flex items-center gap-4">
<img
src="/nextform-logo.png"
alt="NextForm"
className="h-20 w-20 object-contain"
/>
<div className="flex items-center gap-3">
<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">
NextForm
</h1>
</div>
</div>
<p className="text-lg text-gray-600 ml-4">Performance metrics & athlete insights</p>
</div>
</div> </div>
<div>
<h1 className="text-4xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
NextForm
</h1>
<p className="text-sm text-slate-500 font-medium">Fitness Management Platform</p>
</div>
</div>
{/* Stats Grid */} {/* Performance Metrics Section */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5"> <div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2>
<p className="text-slate-600 mt-2 font-medium">Track key indicators and member activity</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatsCard <StatsCard
title="Total Users" title="Total Members"
value={loading ? "..." : stats.totalUsers} value={loading ? "..." : stats.totalUsers}
change="+12%" // Placeholder for now as we don't track historical growth yet change="+12% this month"
trend="up" trend="up"
icon={Users} icon={Users}
color="blue" color="blue"
/> />
<StatsCard <StatsCard
title="Active Clients" title="Active Athletes"
value={loading ? "..." : stats.activeClients} value={loading ? "..." : stats.activeClients}
change="+5%" change="+5% vs last week"
trend="up" trend="up"
icon={CalendarCheck} icon={CalendarCheck}
color="green" color="emerald"
/> />
<StatsCard <StatsCard
title="Revenue" title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)} value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}%`} change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"} trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={CreditCard} icon={CreditCard}
color="purple" color="amber"
/> />
<StatsCard <StatsCard
title="Growth" title="Growth"
value="24%" // Placeholder value="24%"
change="-2%" change="+3% vs last month"
trend="down" trend="up"
icon={TrendingUp} icon={TrendingUp}
color="orange" color="cyan"
/> />
</div> </div>
</div>
{/* Main Content Grid */} {/* Members Directory Section */}
<div className="space-y-6 pb-12"> <div className="mb-20">
{/* User Management - Full Width */} <div className="mb-8">
<div className="bg-gradient-to-br from-slate-50 via-blue-50/30 to-slate-50 rounded-2xl shadow-lg border border-slate-200/50 p-7 hover:shadow-2xl transition-all duration-300 backdrop-blur-sm"> <h2 className="text-3xl font-black text-slate-900">Members Directory</h2>
<div className="space-y-6"> <p className="text-slate-600 mt-2 font-medium">View detailed member information and analytics</p>
<div> </div>
<h2 className="text-2xl font-bold text-slate-900">Active Athletes</h2> <div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-10">
<p className="text-sm text-gray-500 mt-1">Manage and monitor your fitness clients</p> <UserManagement />
</div> </div>
<UserManagement /> </div>
{/* Key Insights Section */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Key Insights</h2>
<p className="text-slate-600 mt-2 font-medium">Quick stats overview</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-blue-50 via-blue-100 to-blue-50 rounded-2xl border border-blue-200 hover:shadow-md transition-shadow">
<p className="text-xs text-blue-700 uppercase font-black tracking-wider">Active Subscriptions</p>
<p className="text-3xl font-black text-blue-900 mt-4 truncate">{loading ? "..." : stats.totalUsers}</p>
<p className="text-sm text-blue-700 mt-2 font-semibold">Premium members active</p>
</div> </div>
</div> </div>
{/* Analytics - 3 Columns Horizontal Layout */} <div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="space-y-4"> <div className="p-6 bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-50 rounded-2xl border border-emerald-200 hover:shadow-md transition-shadow">
<div> <p className="text-xs text-emerald-700 uppercase font-black tracking-wider">Training Activity</p>
<h2 className="text-2xl font-bold text-slate-900">Analytics</h2> <p className="text-3xl font-black text-emerald-900 mt-4 truncate">{loading ? "..." : stats.activeClients}</p>
<p className="text-sm text-gray-500 mt-1">Performance metrics and insights</p> <p className="text-sm text-emerald-700 mt-2 font-semibold">Currently training</p>
</div> </div>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-amber-50 via-amber-100 to-amber-50 rounded-2xl border border-amber-200 hover:shadow-md transition-shadow">
<p className="text-xs text-amber-700 uppercase font-black tracking-wider">Total Revenue</p>
<p className="text-2xl font-black text-amber-900 mt-4 truncate">{loading ? "..." : formatCurrency(stats.totalRevenue)}</p>
<p className="text-sm text-amber-700 mt-2 font-semibold">This period earnings</p>
</div>
</div>
</div>
</div>
{/* Detailed Report Section */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Detailed Report</h2>
<p className="text-slate-600 mt-2 font-medium">Analytics Dashboard</p>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-10">
<div className="bg-slate-50 rounded-2xl p-8">
<AnalyticsDashboard /> <AnalyticsDashboard />
</div> </div>
</div> </div>
+1 -1
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 {
+3 -10
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;
@@ -52,16 +52,9 @@ export function Navigation(): ReactElement {
{/* Logo */} {/* Logo */}
<Link <Link
href="/" href="/"
className="flex items-center gap-3 hover:opacity-80 transition-opacity" className="text-xl font-bold text-[#FF0000] hover:text-[#00FF00] transition-colors"
> >
<img FitAI
src="/nextform-logo.png"
alt="NextForm"
className="h-10 w-10 object-contain"
/>
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent">
NextForm
</span>
</Link> </Link>
{/* Navigation Items */} {/* Navigation Items */}
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
import { UserGrowthChart } from '@/components/charts/UserGrowthChart' import { UserGrowthChart } from '@/components/charts/UserGrowthChart'
import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart' import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart'
import { RevenueChart } from '@/components/charts/RevenueChart' import { RevenueChart } from '@/components/charts/RevenueChart'
import { Card, CardHeader, CardContent } from '@/components/ui/card' import { Card, CardHeader, CardContent } from '@/components/ui/Card'
interface ChartData { interface ChartData {
label: string label: string
@@ -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-700 via-cyan-500 to-blue-600 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>
<div className="bg-gradient-to-br from-amber-700 via-yellow-500 to-amber-600 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 Revenue</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">${totalRevenue.toLocaleString()}</div> <CardContent>
<span className="text-xs text-white font-semibold">ytd</span> <div className="text-center">
</div> <div className="text-3xl font-bold text-blue-600">{totalUsers}</div>
</div> <div className="text-gray-600">Total Users</div>
</div>
</CardContent>
</Card>
<div className="bg-gradient-to-br from-pink-700 via-fuchsia-500 to-pink-600 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white"> <Card>
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Active Members</p> <CardContent>
<div className="flex items-baseline gap-2"> <div className="text-center">
<div className="text-2xl font-black text-white">{activeMembers}</div> <div className="text-3xl font-bold text-green-600">${totalRevenue.toLocaleString()}</div>
<span className="text-xs text-white font-semibold">members</span> <div className="text-gray-600">Total Revenue</div>
</div> </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>
{/* Charts - 3 Columns Horizontal */} {/* Charts */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-gradient-to-br from-blue-700 via-cyan-500 to-blue-600 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">User Growth Trend</h3> <h3 className="text-lg font-semibold">User Growth</h3>
<p className="text-xs text-blue-100">Last 6 months performance</p> </CardHeader>
</div> <CardContent>
<div className="h-48 overflow-auto">
<UserGrowthChart data={userGrowthData} /> <UserGrowthChart data={userGrowthData} />
</div> </CardContent>
</div> </Card>
<div className="bg-gradient-to-br from-amber-700 via-yellow-500 to-amber-600 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-amber-100">Distribution breakdown</p> </CardHeader>
</div> <CardContent>
<div className="h-48 overflow-auto">
<MembershipDistributionChart data={membershipData} /> <MembershipDistributionChart data={membershipData} />
</div> </CardContent>
</div> </Card>
<div className="bg-gradient-to-br from-pink-700 via-fuchsia-500 to-pink-600 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">Revenue Stream</h3>
<p className="text-xs text-pink-100">Monthly earnings</p>
</div>
<div className="h-48 overflow-auto">
<RevenueChart data={revenueData} />
</div>
</div>
</div> </div>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">Monthly Revenue</h3>
</CardHeader>
<CardContent>
<RevenueChart data={revenueData} />
</CardContent>
</Card>
</div> </div>
) )
} }
+21 -30
View File
@@ -65,25 +65,20 @@ 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">
<div className="p-6 border-b border-slate-800/50"> <div className="flex items-center gap-3 mb-2">
<div className="flex items-center gap-3 justify-center"> <div className="w-12 h-12">
<img <img src="/nextform-logo.png" alt="NextForm" className="w-full h-full object-contain" />
src="/nextform-logo.png"
alt="NextForm"
className="h-16 w-16 object-contain"
/>
<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">
NextForm
</h1>
</div> </div>
<h1 className="text-xl font-black bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
NextForm
</h1>
</div> </div>
<p className="text-xs text-slate-400">Fitness Admin</p>
</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;
@@ -92,30 +87,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"> <UserButton afterSignOutUrl="/" />
<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-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>
+41 -49
View File
@@ -1,5 +1,4 @@
import { LucideIcon } from "lucide-react"; import { LucideIcon } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface StatsCardProps { interface StatsCardProps {
title: string; title: string;
@@ -7,68 +6,61 @@ interface StatsCardProps {
change?: string; change?: string;
trend?: "up" | "down" | "neutral"; trend?: "up" | "down" | "neutral";
icon: LucideIcon; icon: LucideIcon;
color?: "blue" | "green" | "purple" | "orange"; color?: "blue" | "emerald" | "amber" | "cyan";
} }
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) { export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
const colorStyles = { const colorStyles = {
blue: { blue: {
bg: "from-blue-700 via-cyan-500 to-blue-600", bg: "from-blue-50 via-blue-100 to-blue-50",
text: "text-white", border: "border-blue-200",
light: "from-blue-50 to-cyan-50", icon: "text-blue-700",
badge: "bg-blue-200/60 text-blue-900 backdrop-blur-sm", label: "text-blue-700",
icon: "bg-white/20 text-white backdrop-blur-sm", value: "text-blue-900"
}, },
green: { emerald: {
bg: "from-pink-700 via-fuchsia-500 to-pink-600", bg: "from-emerald-50 via-emerald-100 to-emerald-50",
text: "text-white", border: "border-emerald-200",
light: "from-pink-50 to-rose-50", icon: "text-emerald-700",
badge: "bg-pink-200/60 text-pink-900 backdrop-blur-sm", label: "text-emerald-700",
icon: "bg-white/20 text-white backdrop-blur-sm", value: "text-emerald-900"
}, },
purple: { amber: {
bg: "from-amber-700 via-yellow-500 to-amber-600", bg: "from-amber-50 via-amber-100 to-amber-50",
text: "text-white", border: "border-amber-200",
light: "from-amber-50 to-yellow-50", icon: "text-amber-700",
badge: "bg-amber-200/60 text-amber-900 backdrop-blur-sm", label: "text-amber-700",
icon: "bg-white/20 text-white backdrop-blur-sm", value: "text-amber-900"
},
orange: {
bg: "from-emerald-700 via-teal-500 to-emerald-600",
text: "text-white",
light: "from-emerald-50 to-teal-50",
badge: "bg-emerald-200/60 text-emerald-900 backdrop-blur-sm",
icon: "bg-white/20 text-white backdrop-blur-sm",
}, },
cyan: {
bg: "from-cyan-50 via-cyan-100 to-cyan-50",
border: "border-cyan-200",
icon: "text-cyan-700",
label: "text-cyan-700",
value: "text-cyan-900"
}
}; };
const styles = colorStyles[color]; const styles = colorStyles[color];
return ( return (
<Card className={`bg-gradient-to-br ${styles.bg} border-0 shadow-2xl hover:shadow-2xl transition-all duration-500 overflow-hidden group h-full relative before:absolute before:inset-0 before:bg-gradient-to-br before:from-white/10 before:to-transparent before:pointer-events-none`}> <div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8 hover:shadow-xl transition-shadow">
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2.5 pt-5 px-5"> <div className={`p-6 bg-gradient-to-br ${styles.bg} rounded-2xl border ${styles.border} hover:shadow-md transition-shadow`}>
<CardTitle className={`text-xs font-bold uppercase tracking-wider ${styles.text} leading-tight max-w-[70%]`}> <div className="flex items-start justify-between mb-4">
{title} <div>
</CardTitle> <p className={`text-xs ${styles.label} uppercase font-black tracking-wider`}>{title}</p>
<div className={`p-1.5 rounded-lg ${styles.icon} flex-shrink-0`}>
<Icon size={16} strokeWidth={2} />
</div>
</CardHeader>
<CardContent className="px-5 pb-5 space-y-2.5">
<div className={`text-2xl font-black ${styles.text}`}>
{value}
</div>
{change && (
<div className="flex items-center gap-1">
<span
className={`inline-flex items-center gap-0.5 font-bold px-2 py-0.5 rounded-md text-xs tracking-wide ${styles.badge}`}
>
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
</span>
<span className="text-xs text-gray-600">vs month</span>
</div> </div>
<div className={`${styles.icon} opacity-60`}>
<Icon size={24} />
</div>
</div>
<p className={`text-4xl font-black ${styles.value} mt-4 truncate`}>{value}</p>
{change && (
<p className={`text-sm ${styles.label} mt-3 font-semibold`}>
{change}
</p>
)} )}
</CardContent> </div>
</Card> </div>
); );
} }
-2
View File
@@ -1,2 +0,0 @@
// Re-export Button component with lowercase filename for compatibility
export { Button } from './Button';
+52 -19
View File
@@ -1,23 +1,56 @@
import React from 'react' import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { import { cn } from "@/lib/utils";
variant?: 'primary' | 'secondary'
children: React.ReactNode const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
} }
export function Button({ variant = 'primary', children, className = '', ...props }: ButtonProps) { const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors' ({ className, variant, size, asChild = false, ...props }, ref) => {
const variantClasses = { const Comp = asChild ? Slot : "button";
primary: 'bg-blue-600 text-white hover:bg-blue-700', return (
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300', <Comp
} className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
return ( export { Button, buttonVariants };
<button
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</button>
)
}
-2
View File
@@ -1,2 +0,0 @@
// Re-export Card components with lowercase filename for compatibility
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
+68 -70
View File
@@ -1,78 +1,76 @@
import React from 'react' import * as React from "react"
interface CardProps { import { cn } from "@/lib/utils"
children: React.ReactNode
className?: string
}
export function Card({ children, className = '' }: CardProps) { const Card = React.forwardRef<
return ( HTMLDivElement,
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}> React.HTMLAttributes<HTMLDivElement>
{children} >(({ className, ...props }, ref) => (
</div> <div
) ref={ref}
} className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
export function CardHeader({ children, className = '' }: CardProps) { const CardHeader = React.forwardRef<
return ( HTMLDivElement,
<div className={`mb-4 ${className}`}> React.HTMLAttributes<HTMLDivElement>
{children} >(({ className, ...props }, ref) => (
</div> <div
) ref={ref}
} className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
export function CardContent({ children, className = '' }: CardProps) { const CardTitle = React.forwardRef<
return ( HTMLParagraphElement,
<div className={className}> React.HTMLAttributes<HTMLHeadingElement>
{children} >(({ className, ...props }, ref) => (
</div> <h3
) ref={ref}
} className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
export function CardTitle({ children, className = '' }: CardProps) { const CardDescription = React.forwardRef<
return ( HTMLParagraphElement,
<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 function CardTitle({ children, className = '' }: CardProps) { export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
return (
<h2 className={`text-lg font-semibold ${className}`}>
{children}
</h2>
)
}
export function CardDescription({ children, className = '' }: CardProps) {
return (
<p className={`text-sm text-gray-600 ${className}`}>
{children}
</p>
)
}
export function CardFooter({ children, className = '' }: CardProps) {
return (
<div className={`mt-4 ${className}`}>
{children}
</div>
)
}
@@ -1,8 +1,8 @@
"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 {
id: string; id: string;
+21 -48
View File
@@ -265,72 +265,45 @@ export function UserGrid({
}; };
return ( return (
<div className="space-y-4"> <div>
{/* Search and Actions Bar */} <div className="flex justify-between items-center 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"> <input
<div className="w-full sm:w-auto flex-1"> type="text"
<div className="relative"> placeholder="Search users..."
<svg className="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> className="border border-gray-300 rounded px-4 py-2"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> value={searchQuery}
</svg> onChange={(e) => setSearchQuery(e.target.value)}
<input />
type="text" <div className="flex gap-2">
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="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-green-500 text-white px-4 py-2 rounded disabled:opacity-50"
onClick={handleEdit} onClick={handleEdit}
disabled={selectedUsers.length !== 1} disabled={selectedUsers.length !== 1}
> >
✏️ Edit Edit User
</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 text-white px-4 py-2 rounded disabled:opacity-50"
onClick={handleDelete} onClick={handleDelete}
disabled={selectedUsers.length !== 1} disabled={selectedUsers.length !== 1}
> >
🗑️ Delete Delete User
</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-yellow-500 text-white px-4 py-2 rounded disabled:opacity-50"
onClick={handleBulkDelete} onClick={handleBulkDelete}
disabled={selectedUsers.length === 0} disabled={selectedUsers.length === 0}
> >
⚡ Bulk Bulk Delete
</button> </button>
</div> </div>
</div> </div>
<div
{/* Table */} className="ag-theme-alpine"
<div className="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden"> style={{ height: "600px", width: "100%" }}
<div >
className="ag-theme-alpine" <AgGridReact<User> {...gridOptions} ref={gridRef} />
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>
); );
} }
@@ -2,8 +2,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { UserGrid } from "@/components/users/UserGrid"; import { UserGrid } from "@/components/users/UserGrid";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/Button";
import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardContent } from "@/components/ui/Card";
interface User { interface User {
id: string; id: string;
@@ -205,106 +205,92 @@ 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> <Button
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>
{/* Filter Buttons */} <div className="flex justify-between items-center">
<div className="flex flex-wrap gap-2"> <div className="text-sm text-gray-600">
<Button Showing {users.length} users
variant={filter === "all" ? "primary" : "secondary"} {selectedUser && (
onClick={() => setFilter("all")} <span className="ml-4 text-blue-600">
className="text-xs" Selected: {selectedUser.firstName} {selectedUser.lastName}
> </span>
All Users )}
</Button> </div>
<Button <div className="flex gap-2">
variant={filter === "client" ? "primary" : "secondary"} <Button variant="secondary" onClick={handleRefresh}>
onClick={() => setFilter("client")} Refresh
className="text-xs" </Button>
> <Button variant="secondary" onClick={handleExport}>
Clients Export CSV
</Button> </Button>
<Button </div>
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
-1
View File
@@ -22,7 +22,6 @@
"name": "next" "name": "next"
} }
], ],
"ignoreDeprecations": "6.0",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": [ "@/*": [
-10
View File
@@ -28,7 +28,6 @@
"expo-haptics": "^15.0.7", "expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7", "expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0", "expo-linking": "~8.0.0",
"expo-location": "~17.0.1",
"expo-notifications": "~0.32.0", "expo-notifications": "~0.32.0",
"expo-router": "~6.0.14", "expo-router": "~6.0.14",
"expo-secure-store": "~15.0.7", "expo-secure-store": "~15.0.7",
@@ -7330,15 +7329,6 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-location": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-17.0.1.tgz",
"integrity": "sha512-m+OzotzlAXO3ZZ1uqW5GC25nXW868zN+ROyBA1V4VF6jGay1ZEs4URPglCVUDzZby2F5wt24cMzqDKw2IX6nRw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-modules-autolinking": { "node_modules/expo-modules-autolinking": {
"version": "3.0.22", "version": "3.0.22",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz",
-1
View File
@@ -34,7 +34,6 @@
"expo-haptics": "^15.0.7", "expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7", "expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0", "expo-linking": "~8.0.0",
"expo-location": "~17.0.1",
"expo-notifications": "~0.32.0", "expo-notifications": "~0.32.0",
"expo-router": "~6.0.14", "expo-router": "~6.0.14",
"expo-secure-store": "~15.0.7", "expo-secure-store": "~15.0.7",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

-28
View File
@@ -1,28 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2FB7E8;stop-opacity:1" />
<stop offset="50%" style="stop-color:#0B9FD0;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0B7FB3;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Main arrow shape -->
<path d="M 420 150 L 540 150 L 420 300 Z" fill="url(#grad1)" opacity="0.95"/>
<path d="M 180 150 L 300 300 L 180 300 Z" fill="url(#grad1)" opacity="0.85"/>
<!-- Dumbbell - Left -->
<g transform="translate(200, 250)">
<!-- Left disk -->
<circle cx="0" cy="0" r="35" fill="white" opacity="0.95"/>
<!-- Bar left -->
<rect x="35" y="-15" width="80" height="30" fill="white" rx="4"/>
<!-- Right disk -->
<circle cx="115" cy="0" r="35" fill="white" opacity="0.95"/>
<!-- Grip lines -->
<line x1="45" y1="-35" x2="45" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
<line x1="65" y1="-35" x2="65" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
<line x1="85" y1="-35" x2="85" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
<line x1="105" y1="-35" x2="105" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -6,7 +6,6 @@ import { Ionicons } from '@expo/vector-icons'
import { attendanceApi, Attendance } from '../../api/attendance' import { attendanceApi, Attendance } from '../../api/attendance'
import { theme } from '../../styles/theme' import { theme } from '../../styles/theme'
import { Animated } from 'react-native' import { Animated } from 'react-native'
import { GeofenceStatus } from '../../components/GeofenceStatus'
export default function AttendanceScreen() { export default function AttendanceScreen() {
const { getToken, userId } = useAuth() const { getToken, userId } = useAuth()
@@ -112,9 +111,6 @@ export default function AttendanceScreen() {
<Text style={styles.subtitle}>Track your gym visits</Text> <Text style={styles.subtitle}>Track your gym visits</Text>
</LinearGradient> </LinearGradient>
{/* Geofencing Status Card */}
<GeofenceStatus />
<View style={styles.actionContainer}> <View style={styles.actionContainer}>
{activeCheckIn ? ( {activeCheckIn ? (
<LinearGradient <LinearGradient
+3 -13
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() {
@@ -110,15 +109,9 @@ export default function HomeScreen() {
> >
{/* Header Section */} {/* Header Section */}
<View style={styles.header}> <View style={styles.header}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}> <View>
<Image <Text style={styles.greeting}>{getGreeting()},</Text>
source={require("../../../public/nextform-logo.png")} <Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
style={{ width: 40, height: 40, resizeMode: 'contain' }}
/>
<View>
<Text style={styles.greeting}>{getGreeting()},</Text>
<Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
</View>
</View> </View>
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
{user?.imageUrl ? ( {user?.imageUrl ? (
@@ -138,9 +131,6 @@ export default function HomeScreen() {
duration={45} duration={45}
/> />
{/* Performance Metrics */}
<PerformanceMetrics />
{/* Hydration Widget */} {/* Hydration Widget */}
<HydrationWidget <HydrationWidget
current={waterIntake} current={waterIntake}
@@ -16,7 +16,7 @@ export function ActivityWidget({ steps, calories, duration }: ActivityWidgetProp
return ( return (
<View style={styles.container}> <View style={styles.container}>
<LinearGradient <LinearGradient
colors={['#0c4a6e', '#0369a1', '#0ea5e9']} colors={theme.gradients.dark}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
style={[styles.card, theme.shadows.medium]} style={[styles.card, theme.shadows.medium]}
@@ -1,119 +0,0 @@
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'
import { LinearGradient } from 'expo-linear-gradient'
import { Ionicons } from '@expo/vector-icons'
import { useState } from 'react'
export const GeofenceStatus = () => {
const [isInside, setIsInside] = useState(false)
const [distance, setDistance] = useState('250 m')
const [loading, setLoading] = useState(false)
const handleRefresh = () => {
setLoading(true)
// Simulate location check
setTimeout(() => {
setDistance(Math.random() > 0.5 ? '150 m' : '300 m')
setIsInside(Math.random() > 0.5)
setLoading(false)
}, 1000)
}
return (
<View style={styles.container}>
<LinearGradient
colors={
isInside
? ['#10b981', '#6ee7b7', '#a7f3d0']
: ['#64748b', '#94a3b8', '#cbd5e1']
}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.card}
>
<View style={styles.header}>
<View style={styles.titleRow}>
<Ionicons
name={isInside ? 'location-sharp' : 'location'}
size={24}
color="#fff"
/>
<Text style={styles.title}>
{isInside ? 'At Gym' : 'Away from Gym'}
</Text>
</View>
<TouchableOpacity
onPress={handleRefresh}
disabled={loading}
style={styles.refreshButton}
>
<Ionicons
name="refresh"
size={20}
color="#fff"
style={loading ? { opacity: 0.5 } : {}}
/>
</TouchableOpacity>
</View>
<View style={styles.content}>
<View style={styles.stat}>
<Text style={styles.statLabel}>Distance</Text>
<Text style={styles.statValue}>{distance}</Text>
</View>
</View>
</LinearGradient>
</View>
)
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingVertical: 12,
},
card: {
borderRadius: 16,
padding: 16,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#fff',
},
refreshButton: {
padding: 8,
borderRadius: 8,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
},
content: {
marginTop: 12,
},
stat: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
statLabel: {
fontSize: 14,
color: 'rgba(255, 255, 255, 0.8)',
fontWeight: '500',
},
statValue: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
})
@@ -15,7 +15,7 @@ export function HydrationWidget({ current, goal }: HydrationWidgetProps) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<LinearGradient <LinearGradient
colors={['#0369a1', '#0ea5e9', '#06b6d4']} colors={['#e0f2fe', '#dbeafe']} // Light blue background
style={[styles.card, theme.shadows.subtle]} style={[styles.card, theme.shadows.subtle]}
> >
<View style={styles.content}> <View style={styles.content}>
@@ -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: ['#1e40af', '#0369a1', '#06b6d4'] as const,
text: '#ffffff',
label: '#e0f2fe',
badge: '#0ea5e9',
badgeText: '#ffffff',
icon: '#06b6d4',
},
green: {
colors: ['#be185d', '#ec4899', '#f472b6'] as const,
text: '#ffffff',
label: '#fbcfe8',
badge: '#ec4899',
badgeText: '#ffffff',
icon: '#f472b6',
},
purple: {
colors: ['#b45309', '#d97706', '#fbbf24'] as const,
text: '#ffffff',
label: '#fef3c7',
badge: '#f59e0b',
badgeText: '#ffffff',
icon: '#fbbf24',
},
orange: {
colors: ['#047857', '#059669', '#10b981'] as const,
text: '#ffffff',
label: '#d1fae5',
badge: '#10b981',
badgeText: '#ffffff',
icon: '#34d399',
},
};
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',
},
});
-47
View File
@@ -1,47 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import QRCode from 'react-native-qrcode-svg';
interface QRCodeProps {
value: string;
size?: number;
logo?: string;
}
export const QRCodeGenerator: React.FC<QRCodeProps> = ({
value,
size = 250
}) => {
return (
<View style={styles.container}>
<View style={styles.qrWrapper}>
<QRCode
value={value}
size={size}
color="black"
backgroundColor="white"
quietZone={10}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
qrWrapper: {
padding: 16,
backgroundColor: 'white',
borderRadius: 12,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
});
-92
View File
@@ -1,92 +0,0 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Button, Alert } from 'react-native';
import { BarCodeScanner } from 'expo-barcode-scanner';
interface QRScannerProps {
onScan: (data: string) => void;
onClose?: () => void;
}
export const QRScanner: React.FC<QRScannerProps> = ({ onScan, onClose }) => {
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [scanned, setScanned] = useState(false);
useEffect(() => {
const getBarCodeScannerPermissions = async () => {
const { status } = await BarCodeScanner.requestPermissionsAsync();
setHasPermission(status === 'granted');
};
getBarCodeScannerPermissions();
}, []);
const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
setScanned(true);
onScan(data);
Alert.alert('QR Code', `Scanned: ${data}`, [
{ text: 'OK', onPress: () => setScanned(false) },
]);
};
if (hasPermission === null) {
return (
<View style={styles.container}>
<Text>Requesting for camera permission...</Text>
</View>
);
}
if (hasPermission === false) {
return (
<View style={styles.container}>
<Text>No access to camera</Text>
{onClose && <Button title="Close" onPress={onClose} />}
</View>
);
}
return (
<View style={styles.container}>
<BarCodeScanner
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
style={StyleSheet.absoluteFillObject}
/>
<View style={styles.overlay}>
<View style={styles.scanBox} />
</View>
{onClose && (
<View style={styles.buttonContainer}>
<Button title="Close Scanner" onPress={onClose} color="#FF6B6B" />
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#000',
},
overlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
scanBox: {
width: 250,
height: 250,
borderWidth: 2,
borderColor: '#4CAF50',
borderRadius: 10,
backgroundColor: 'transparent',
},
buttonContainer: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
},
});
+15 -52
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,25 +111,19 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
<View style={{ width: 28 }} /> <View style={{ width: 28 }} />
</View> </View>
{Platform.OS === 'web' ? ( <CameraView
<View style={styles.webPlaceholder}> style={styles.camera}
<Text style={styles.webText}>Barcode scanning is only available on mobile devices</Text> 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> </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}>
@@ -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',
+4 -8
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 void WebBrowser.warmUpAsync();
if (Platform.OS !== "web") { return () => {
void WebBrowser.warmUpAsync(); void WebBrowser.coolDownAsync();
return () => { };
void WebBrowser.coolDownAsync();
};
}
}, []); }, []);
}; };
-74
View File
@@ -1,74 +0,0 @@
/**
* Geofencing Service
* Handles gym location validation and distance calculations
*/
// Gym location coordinates - Teretana Isaija Mazhovski, Skopje, North Macedonia
export const GYM_LOCATION = {
latitude: 41.9973,
longitude: 21.4280,
};
// Geofence radius in meters (500m default)
export const GEOFENCE_RADIUS_METERS = 500;
/**
* Calculate distance between two coordinates using Haversine formula
* @param lat1 Starting latitude
* @param lon1 Starting longitude
* @param lat2 Ending latitude
* @param lon2 Ending longitude
* @returns Distance in meters
*/
export function calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371000; // Earth's radius in meters
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}
/**
* Check if a location is within the geofence
* @param userLat User's latitude
* @param userLon User's longitude
* @param radius Geofence radius in meters (defaults to GEOFENCE_RADIUS_METERS)
* @returns Boolean indicating if user is within geofence
*/
export function isWithinGeofence(
userLat: number,
userLon: number,
radius: number = GEOFENCE_RADIUS_METERS
): boolean {
const distance = calculateDistance(
userLat,
userLon,
GYM_LOCATION.latitude,
GYM_LOCATION.longitude
);
return distance <= radius;
}
/**
* Get formatted distance string
* @param meters Distance in meters
* @returns Formatted distance string
*/
export function getFormattedDistance(meters: number): string {
if (meters < 1000) {
return `${Math.round(meters)} m`;
}
return `${(meters / 1000).toFixed(2)} km`;
}
-5
View File
@@ -64,11 +64,6 @@ export const theme = {
forest: ['#10b981', '#059669'] as const, forest: ['#10b981', '#059669'] as const,
lavender: ['#a78bfa', '#ec4899'] as const, lavender: ['#a78bfa', '#ec4899'] as const,
dark: ['#1e293b', '#0f172a'] as const, dark: ['#1e293b', '#0f172a'] as const,
// Premium metallic gradients
blueMetallic: ['#1e40af', '#06b6d4', '#10b981'] as const,
amberMetallic: ['#b45309', '#f59e0b', '#fbbf24'] as const,
magentaMetallic: ['#be185d', '#ec4899', '#f472b6'] as const,
emeraldMetallic: ['#047857', '#10b981', '#6ee7b7'] as const,
}, },
// Shadow System // Shadow System
-1
View File
@@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"ignoreDeprecations": "6.0",
"allowJs": true, "allowJs": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
+12 -171
View File
@@ -1,174 +1,15 @@
# FitAI ## fitai
Integrated AI solution for fitness houses and their clients with Clerk authentication. # description
## Project Structure - fitai is integrated ai solution for fitness houses and their clients,
its allow to easy menagment of clients, tracking of payments, usage of resourcess,
attendance, habits etc.
these will be phase one:
solution is composed of a admin app, where we are doing managment tasks, we visualize and
expose importatnt data to menagment and trainers, and a expo/reactnative mobile app for users.
via app we will be tracking attendance and payments, we will be sending notification etc.
``` # phase 2
fitai/ we will be tracking user inputs via manual input and devices, backend will analyze data and propose
├── apps/ excercises etc.
│ ├── admin/ # Next.js admin dashboard
│ └── mobile/ # React Native mobile app (Expo)
├── packages/
│ └── shared/ # Shared types and utilities
└── AGENTS.md # Development guidelines
```
## Getting Started
### Prerequisites
- Node.js >= 18.0.0
- npm >= 9.0.0
- Clerk account (sign up at https://clerk.com)
### Installation
```bash
# Install root dependencies
npm install
# Install admin dependencies
cd apps/admin && npm install
# Install mobile dependencies
cd apps/mobile && npm install --legacy-peer-deps
```
### Authentication Setup
FitAI uses Clerk for authentication. Follow these steps:
1. **Create a Clerk account** at https://dashboard.clerk.com
2. **Create a new application** in the Clerk dashboard
3. **Copy your API keys** (Publishable Key and Secret Key)
4. **Configure environment variables**:
**Admin App** (`apps/admin/.env.local`):
```env
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
CLERK_SECRET_KEY=sk_test_your_key_here
```
**Mobile App** (`apps/mobile/.env`):
```env
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
```
📖 **See [CLERK_SETUP.md](./CLERK_SETUP.md) for detailed setup instructions**
### Development
**Important**: Set up environment variables before running the apps!
```bash
# Admin dashboard (http://localhost:3000)
cd apps/admin && npm run dev
# Mobile app (http://localhost:8081) - Requires Expo SDK 54
cd apps/mobile && npm start
```
**First-time setup checklist**:
- [ ] Create Clerk account and application
- [ ] Add API keys to `.env.local` (admin) and `.env` (mobile)
- [ ] Verify both apps start without errors
- [ ] Test sign-up and sign-in flows
### Mobile App Setup
- **Expo SDK**: 50 (stable, compatible with Expo Go)
- **Assets**: Placeholder icons and splash screen included
- **Navigation**: Expo Router with tab-based layout
- **Authentication**: Secure storage with expo-secure-store
- **Babel**: babel-preset-expo for proper transpilation
### Known Compatibility Notes
- Use Expo Go with SDK 50 for mobile testing
- For SDK 54, upgrade all dependencies to latest versions
- Current setup prioritizes stability over latest features
### Build & Test
```bash
# Build all apps
npm run build
# Run tests
npm test
# Lint code
npm run lint
# Type checking
npm run typecheck
```
## Features
### Authentication (Clerk)
- 🔐 Secure email/password authentication
- ✉️ Email verification
- 🔄 Session management
- 🎨 Customizable UI components
- 📱 Multi-platform support (Web + Mobile)
- 🛡️ Built-in security features
### Admin Dashboard
- 👥 User management (CRUD operations)
- 📊 Analytics dashboard with charts
- 🎯 Role-based access control
- 📈 Data visualization with AG Grid
- 💳 Payment tracking (coming soon)
- 📅 Attendance monitoring (coming soon)
### Mobile App
- 🔐 Secure sign-in/sign-up
- 👤 User profile management
- 📱 Native mobile experience
- 🔔 Push notifications ready
- ✅ Attendance check-in (coming soon)
- 💰 Payment history (coming soon)
## Tech Stack
### Authentication
- **Clerk**: Complete authentication and user management platform
### Frontend
- **Admin**: Next.js 14 (App Router), React 19, TypeScript, Tailwind CSS
- **Mobile**: React Native, Expo SDK 54, Expo Router, TypeScript
### Backend & Database
- **Database**: SQLite with Drizzle ORM
- **API**: Next.js API Routes (REST)
### Development Tools
- **State Management**: React Query, React Hook Form
- **Validation**: Zod schemas
- **Data Grid**: AG Grid for advanced user management
- **Charts**: AG Charts for analytics and visualization
- **Testing**: Jest, Testing Library (configured)
## Project Structure
```
fitai/
├── apps/
│ ├── admin/ # Next.js admin dashboard
│ │ ├── src/
│ │ │ ├── app/ # App Router pages & API routes
│ │ │ ├── components/
│ │ │ └── lib/ # Database & utilities
│ │ └── .env.local # Admin environment variables
│ │
│ └── mobile/ # Expo React Native app
│ ├── src/
│ │ ├── app/ # Expo Router screens
│ │ │ ├── (auth)/ # Authentication screens
│ │ │ └── (tabs)/ # Main app tabs
│ │ └── components/
│ └── .env # Mobile environment variables
│
├── packages/
│ ├── database/ # Drizzle ORM schemas & DB client
│ └── shared/ # Shared types & utilities
│
└── CLERK_SETUP.md # Detailed authentication setup guide
```
-19
View File
@@ -1,19 +0,0 @@
# Script to update NextForm logo from external folder
$sourceFolder = "c:\Users\PC\Desktop\NextForm – Your Smart Fitness Twin\sliki"
$adminPublic = "c:\Users\PC\Desktop\fitaiProto\apps\admin\public\nextform-logo.png"
$mobilePublic = "c:\Users\PC\Desktop\fitaiProto\apps\mobile\public\nextform-logo.png"
# Find the latest PNG in source folder
$latestPng = Get-ChildItem "$sourceFolder\*.png" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($latestPng) {
Copy-Item $latestPng.FullName $adminPublic -Force
Copy-Item $latestPng.FullName $mobilePublic -Force
Write-Host "✅ Logo updated from: $($latestPng.Name)"
Write-Host "Admin: $adminPublic"
Write-Host "Mobile: $mobilePublic"
} else {
Write-Host "❌ No PNG found in: $sourceFolder"
}