Compare commits

..
2 Commits
40 changed files with 585 additions and 1298 deletions

No files matched your search

-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

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

-1
View File
@@ -1 +0,0 @@
PNG - NextForm Logo
@@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
// POST - Mark goal as complete
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
{ params }: { params: { id: string } }
) {
try {
const { userId } = await auth();
@@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
// GET - Get specific goal
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
{ params }: { params: { id: string } }
) {
try {
const { userId } = await auth();
@@ -40,7 +40,7 @@ export async function GET(
// PUT - Update goal
export async function PUT(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
{ params }: { params: { id: string } }
) {
try {
const { userId } = await auth();
@@ -82,7 +82,7 @@ export async function PUT(
// DELETE - Delete goal
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
{ params }: { params: { id: string } }
) {
try {
const { userId } = await auth();
+1 -1
View File
@@ -32,7 +32,7 @@
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.75rem;
--radius: 0.5rem;
}
.dark {
+4 -27
View File
@@ -25,34 +25,11 @@ export default function RootLayout({
return (
<ClerkProvider>
<html lang="en">
<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">
<body className={inter.className}>
<div className="flex min-h-screen bg-gradient-to-br from-white via-blue-50 to-slate-50">
<Sidebar />
<main className="flex-1 ml-64">
<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 className="flex-1 ml-64 p-8">
{children}
</main>
</div>
</body>
+118 -73
View File
@@ -1,10 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
import { Users, Activity, Zap, TrendingUp, Trophy, Target } from "lucide-react";
import { StatsCard } from "@/components/ui/StatsCard";
import { UserManagement } from "@/components/users/UserManagement";
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
import { UserManagement } from "@/components/users/UserManagement";
import axios from "axios";
interface DashboardStats {
@@ -46,84 +46,129 @@ export default function Home() {
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100">
<div className="space-y-8 max-w-7xl">
{/* Hero Section */}
<div className="pt-8 pb-4">
<div className="space-y-3">
<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 className="min-h-screen bg-gradient-to-br from-white via-slate-50 to-blue-50">
{/* Premium Header */}
<div className="bg-gradient-to-r from-slate-900 via-blue-900 to-slate-900 shadow-2xl">
<div className="max-w-7xl mx-auto px-8 py-12">
<div className="flex items-end justify-between">
<div className="flex items-end gap-6">
<div className="w-16 h-16 bg-gradient-to-br from-blue-400 to-cyan-400 rounded-3xl flex items-center justify-center shadow-lg">
<Trophy className="w-9 h-9 text-white" />
</div>
</div>
<p className="text-lg text-gray-600 ml-4">Performance metrics & athlete insights</p>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
<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-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">
<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>
<h1 className="text-5xl font-black text-white tracking-tight">FitAI</h1>
<p className="text-blue-200 text-sm font-semibold mt-2">Premium Sports Management Platform</p>
</div>
<UserManagement />
</div>
<div className="text-right">
<p className="text-blue-100 text-sm">Welcome</p>
<p className="text-2xl font-bold text-white">Admin Control</p>
</div>
</div>
</div>
</div>
{/* 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>
{/* Main Content with Proper Spacing */}
<div className="max-w-7xl mx-auto px-8 py-16">
{/* Section: Performance Metrics */}
<div className="mb-20">
<div className="mb-10">
<h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2>
<p className="text-slate-600 mt-2 font-medium">Real-time insights into your platform</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<StatsCard
title="Total Members"
value={loading ? "..." : stats.totalUsers}
change="+12% this month"
trend="up"
icon={Users}
color="blue"
/>
<StatsCard
title="Active Athletes"
value={loading ? "..." : stats.activeClients}
change="+8% this week"
trend="up"
icon={Activity}
color="green"
/>
<StatsCard
title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={Zap}
color="amber"
/>
<StatsCard
title="Growth"
value="24%"
change="+3% vs last month"
trend="up"
icon={TrendingUp}
color="cyan"
/>
</div>
</div>
{/* Section: Member Management */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Members Directory</h2>
<p className="text-slate-600 mt-2 font-medium">View detailed member information and analytics</p>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-10">
<UserManagement />
</div>
</div>
{/* Section: Key Insights */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Key Insights</h2>
<p className="text-slate-600 mt-2 font-medium">Quick stats overview</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-blue-50 via-blue-100 to-blue-50 rounded-2xl border border-blue-200 hover:shadow-md transition-shadow">
<p className="text-xs text-blue-700 uppercase font-black tracking-wider">Members</p>
<p className="text-3xl font-black text-blue-900 mt-4 truncate">{loading ? "..." : stats.totalUsers}</p>
<p className="text-sm text-blue-700 mt-2 font-semibold">Active subscriptions</p>
</div>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-50 rounded-2xl border border-emerald-200 hover:shadow-md transition-shadow">
<p className="text-xs text-emerald-700 uppercase font-black tracking-wider">Training Now</p>
<p className="text-3xl font-black text-emerald-900 mt-4 truncate">{loading ? "..." : stats.activeClients}</p>
<p className="text-sm text-emerald-700 mt-2 font-semibold">Currently active</p>
</div>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-amber-50 via-amber-100 to-amber-50 rounded-2xl border border-amber-200 hover:shadow-md transition-shadow">
<p className="text-xs text-amber-700 uppercase font-black tracking-wider">Revenue</p>
<p className="text-2xl font-black text-amber-900 mt-4 truncate">{loading ? "..." : formatCurrency(stats.totalRevenue)}</p>
<p className="text-sm text-amber-700 mt-2 font-semibold">This period</p>
</div>
</div>
</div>
</div>
{/* Section: Detailed Report - Full Width */}
<div className="mb-20">
<div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Detailed Report</h2>
<p className="text-slate-600 mt-2 font-medium">Analytics Dashboard</p>
</div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-10">
<div className="bg-slate-50 rounded-2xl p-8">
<AnalyticsDashboard />
</div>
<AnalyticsDashboard />
</div>
</div>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { useUser } from "@clerk/nextjs";
import { Button } from "@/components/ui/button";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/input";
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 { SignedIn, UserButton } from "@clerk/nextjs";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Button } from "@/components/ui/Button";
interface NavItem {
href: string;
@@ -52,16 +52,9 @@ export function Navigation(): ReactElement {
{/* Logo */}
<Link
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
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>
FitAI
</Link>
{/* Navigation Items */}
@@ -67,75 +67,74 @@ export function AnalyticsDashboard() {
if (loading) {
return (
<div className="flex justify-center items-center h-64">
<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 className="text-lg">Loading analytics...</div>
</div>
)
}
return (
<div className="space-y-4">
{/* 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-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="space-y-6">
<h2 className="text-2xl font-bold">Analytics Dashboard</h2>
<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">
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Revenue</p>
<div className="flex items-baseline gap-2">
<div className="text-2xl font-black text-white">${totalRevenue.toLocaleString()}</div>
<span className="text-xs text-white font-semibold">ytd</span>
</div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">{totalUsers}</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">
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Active Members</p>
<div className="flex items-baseline gap-2">
<div className="text-2xl font-black text-white">{activeMembers}</div>
<span className="text-xs text-white font-semibold">members</span>
</div>
</div>
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-green-600">${totalRevenue.toLocaleString()}</div>
<div className="text-gray-600">Total Revenue</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="text-center">
<div className="text-3xl font-bold text-purple-600">{activeMembers}</div>
<div className="text-gray-600">Active Members</div>
</div>
</CardContent>
</Card>
</div>
{/* Charts - 3 Columns Horizontal */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<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">
<div className="mb-3">
<h3 className="text-sm font-bold text-white">User Growth Trend</h3>
<p className="text-xs text-blue-100">Last 6 months performance</p>
</div>
<div className="h-48 overflow-auto">
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">User Growth</h3>
</CardHeader>
<CardContent>
<UserGrowthChart data={userGrowthData} />
</div>
</div>
</CardContent>
</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">
<div className="mb-3">
<h3 className="text-sm font-bold text-white">Membership Mix</h3>
<p className="text-xs text-amber-100">Distribution breakdown</p>
</div>
<div className="h-48 overflow-auto">
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">Membership Distribution</h3>
</CardHeader>
<CardContent>
<MembershipDistributionChart data={membershipData} />
</div>
</div>
<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>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">Monthly Revenue</h3>
</CardHeader>
<CardContent>
<RevenueChart data={revenueData} />
</CardContent>
</Card>
</div>
)
}
+19 -33
View File
@@ -65,25 +65,15 @@ export function Sidebar() {
];
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">
{/* Logo Section */}
<div className="p-6 border-b border-slate-800/50">
<div className="flex items-center gap-3 justify-center">
<img
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>
<aside className="w-64 bg-gradient-to-b from-white to-slate-50 h-screen fixed left-0 top-0 flex flex-col border-r border-slate-200/50">
<div className="p-6 border-b border-slate-200">
<h1 className="text-2xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
FitAI
</h1>
<p className="text-xs text-slate-600 font-medium mt-1">Sports Admin</p>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
<nav className="flex-1 p-4 space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
@@ -92,30 +82,26 @@ export function Sidebar() {
<Link
key={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
? "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/40 hover:text-white"}`}
className={`flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-200 group font-medium ${isActive
? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-md shadow-blue-500/20"
: "text-slate-700 hover:bg-slate-100 hover:text-blue-600"}`}
>
{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"} relative z-10`} />
<span className="font-semibold relative z-10">{label}</span>
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-blue-600"} />
<span className="text-sm">{label}</span>
</Link>
);
})}
</nav>
{/* User Section */}
<div className="p-4 border-t border-slate-800/50 bg-slate-800/20">
<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="p-4 border-t border-slate-200">
<div className="flex items-center gap-3 px-4 py-3 rounded-2xl bg-slate-100">
<UserButton afterSignOutUrl="/" />
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-white truncate">
{user?.fullName || "Admin"}
<p className="text-sm font-semibold text-slate-900 truncate">
{user?.fullName || "Admin User"}
</p>
<p className="text-xs text-slate-400 truncate">
{user?.emailAddresses[0]?.emailAddress || "admin@fitai.com"}
<p className="text-xs text-slate-600 truncate">
{user?.primaryEmailAddress?.emailAddress}
</p>
</div>
</div>
+46 -48
View File
@@ -1,5 +1,4 @@
import { LucideIcon } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface StatsCardProps {
title: string;
@@ -7,68 +6,67 @@ interface StatsCardProps {
change?: string;
trend?: "up" | "down" | "neutral";
icon: LucideIcon;
color?: "blue" | "green" | "purple" | "orange";
color?: "blue" | "green" | "amber" | "cyan";
}
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
const colorStyles = {
blue: {
bg: "from-blue-700 via-cyan-500 to-blue-600",
text: "text-white",
light: "from-blue-50 to-cyan-50",
badge: "bg-blue-200/60 text-blue-900 backdrop-blur-sm",
icon: "bg-white/20 text-white backdrop-blur-sm",
bg: "from-blue-50 via-blue-100 to-blue-50",
text: "text-blue-950",
icon: "bg-gradient-to-br from-blue-500 to-blue-600",
border: "border-blue-200",
label: "text-blue-700"
},
green: {
bg: "from-pink-700 via-fuchsia-500 to-pink-600",
text: "text-white",
light: "from-pink-50 to-rose-50",
badge: "bg-pink-200/60 text-pink-900 backdrop-blur-sm",
icon: "bg-white/20 text-white backdrop-blur-sm",
bg: "from-emerald-50 via-emerald-100 to-emerald-50",
text: "text-emerald-950",
icon: "bg-gradient-to-br from-emerald-500 to-emerald-600",
border: "border-emerald-200",
label: "text-emerald-700"
},
purple: {
bg: "from-amber-700 via-yellow-500 to-amber-600",
text: "text-white",
light: "from-amber-50 to-yellow-50",
badge: "bg-amber-200/60 text-amber-900 backdrop-blur-sm",
icon: "bg-white/20 text-white backdrop-blur-sm",
amber: {
bg: "from-amber-50 via-amber-100 to-amber-50",
text: "text-amber-950",
icon: "bg-gradient-to-br from-amber-500 to-amber-600",
border: "border-amber-200",
label: "text-amber-700"
},
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",
text: "text-cyan-950",
icon: "bg-gradient-to-br from-cyan-500 to-cyan-600",
border: "border-cyan-200",
label: "text-cyan-700"
},
};
const styles = colorStyles[color];
const style = colorStyles[color];
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`}>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2.5 pt-5 px-5">
<CardTitle className={`text-xs font-bold uppercase tracking-wider ${styles.text} leading-tight max-w-[70%]`}>
{title}
</CardTitle>
<div className={`p-1.5 rounded-lg ${styles.icon} flex-shrink-0`}>
<Icon size={16} strokeWidth={2} />
<div className={`bg-gradient-to-br ${style.bg} rounded-3xl border ${style.border} p-6 hover:shadow-xl transition-all duration-300 cursor-default group`}>
<div className="flex items-start justify-between mb-4 gap-3">
<div className="flex-1 min-w-0">
<p className={`text-xs ${style.label} uppercase font-black tracking-widest truncate`}>
{title}
</p>
</div>
</CardHeader>
<CardContent className="px-5 pb-5 space-y-2.5">
<div className={`text-2xl font-black ${styles.text}`}>
{value}
<div className={`p-3 ${style.icon} rounded-2xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform flex-shrink-0`}>
<Icon size={20} className="text-white" />
</div>
{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>
)}
</CardContent>
</Card>
</div>
<div className={`text-4xl font-black ${style.text} mb-3 truncate`}>
{value}
</div>
{change && (
<div className="flex items-center gap-1 truncate">
<span className={`text-xs font-black uppercase tracking-wide ${style.label} truncate`}>
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
</span>
</div>
)}
</div>
);
}
-2
View File
@@ -1,2 +0,0 @@
// Re-export Button component with lowercase filename for compatibility
export { Button } from './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';
+1 -41
View File
@@ -31,47 +31,7 @@ export function CardContent({ children, className = '' }: CardProps) {
export function CardTitle({ children, className = '' }: CardProps) {
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>
)
}
export function CardTitle({ children, className = '' }: CardProps) {
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}`}>
<div className={`text-sm font-medium ${className}`}>
{children}
</div>
)
@@ -1,7 +1,7 @@
"use client";
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";
interface Recommendation {
+21 -48
View File
@@ -265,72 +265,45 @@ export function UserGrid({
};
return (
<div className="space-y-4">
{/* Search and Actions Bar */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-gradient-to-r from-slate-50 to-blue-50 p-4 rounded-xl border border-slate-200">
<div className="w-full sm:w-auto flex-1">
<div className="relative">
<svg className="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
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">
<div>
<div className="flex justify-between items-center mb-4">
<input
type="text"
placeholder="Search users..."
className="border border-gray-300 rounded px-4 py-2"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<div className="flex gap-2">
<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}
disabled={selectedUsers.length !== 1}
>
✏️ Edit
Edit User
</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}
disabled={selectedUsers.length !== 1}
>
🗑️ Delete
Delete User
</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}
disabled={selectedUsers.length === 0}
>
⚡ Bulk
Bulk Delete
</button>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
<div
className="ag-theme-alpine"
style={{ height: "600px", width: "100%" }}
>
<AgGridReact<User> {...gridOptions} ref={gridRef} />
</div>
<div
className="ag-theme-alpine"
style={{ height: "600px", width: "100%" }}
>
<AgGridReact<User> {...gridOptions} ref={gridRef} />
</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>
);
}
+271 -194
View File
@@ -34,6 +34,7 @@ export function UserManagement() {
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [activeTab, setActiveTab] = useState<"users" | "details">("users");
const [editForm, setEditForm] = useState<{
firstName: string;
lastName: string;
@@ -206,117 +207,227 @@ export function UserManagement() {
return (
<div className="space-y-4">
{/* Header Section */}
<div>
<h3 className="text-lg font-bold text-gray-900">User Management</h3>
<p className="text-sm text-gray-600 mt-1">Manage and monitor your fitness clients</p>
</div>
{/* Filter Buttons */}
<div className="flex flex-wrap gap-2">
<Button
variant={filter === "all" ? "primary" : "secondary"}
onClick={() => setFilter("all")}
className="text-xs"
>
All Users
</Button>
<Button
variant={filter === "client" ? "primary" : "secondary"}
onClick={() => setFilter("client")}
className="text-xs"
>
Clients
</Button>
<Button
variant={filter === "trainer" ? "primary" : "secondary"}
onClick={() => setFilter("trainer")}
className="text-xs"
>
Trainers
</Button>
<Button
variant={filter === "admin" ? "primary" : "secondary"}
onClick={() => setFilter("admin")}
className="text-xs"
>
Admins
</Button>
<Button
variant={filter === "superAdmin" ? "primary" : "secondary"}
onClick={() => setFilter("superAdmin")}
className="text-xs"
>
Super Admins
</Button>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-2">
<Button
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
{/* Header with Count */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-black text-slate-900">
{users.length} {users.length === 1 ? 'user' : 'users'}
</h3>
{selectedUser && (
<span className="ml-4 text-blue-600 font-medium">
<p className="text-sm text-blue-600 font-semibold">
Selected: {selectedUser.firstName} {selectedUser.lastName}
</span>
</p>
)}
</div>
{/* User Grid */}
<Card>
<CardContent className="p-0">
<UserGrid
users={users}
onUserSelect={(user) => handleUserSelect(user)}
onEditUser={handleEditUser}
onDeleteUser={handleDeleteUser}
onBulkDelete={handleBulkDelete}
loading={loading}
/>
</CardContent>
</Card>
{/* Compact Controls - Always Visible but Organized */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-4 bg-gradient-to-r from-slate-50 to-blue-50 rounded-lg border border-slate-200">
{/* Filter Row */}
<div>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Roles</p>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setFilter("all")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "all"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
All
</button>
<button
onClick={() => setFilter("client")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "client"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Clients
</button>
<button
onClick={() => setFilter("trainer")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "trainer"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Trainers
</button>
<button
onClick={() => setFilter("admin")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "admin"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Admins
</button>
<button
onClick={() => setFilter("superAdmin")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "superAdmin"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Super Admins
</button>
</div>
</div>
{/* Action Buttons Row */}
<div>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Quick Actions</p>
<div className="flex flex-wrap gap-2">
<button
onClick={() => {
setEditForm({
firstName: "",
lastName: "",
email: "",
role: "client",
phone: "",
});
setSelectedUser(null);
setIsEditing(true);
}}
className="px-2 py-1 text-xs font-semibold rounded-lg bg-blue-500 text-white hover:bg-blue-600"
>
+ Invite
</button>
<button
onClick={() => selectedUser && handleEditUser(selectedUser)}
disabled={!selectedUser}
className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200 disabled:opacity-50"
>
Edit
</button>
<button
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
disabled={!selectedUser}
className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200 disabled:opacity-50"
>
Delete
</button>
<button onClick={handleRefresh} className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200">
Refresh
</button>
<button onClick={handleExport} className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200">
Export
</button>
</div>
</div>
</div>
{/* Tab Navigation */}
<div className="flex gap-2 border-b border-slate-200">
<button
onClick={() => setActiveTab("users")}
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
activeTab === "users"
? "border-blue-500 text-blue-600"
: "border-transparent text-slate-600 hover:text-slate-900"
}`}
>
User List ({users.length})
</button>
<button
onClick={() => setActiveTab("details")}
disabled={!selectedUser}
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
activeTab === "details"
? "border-blue-500 text-blue-600"
: "border-transparent text-slate-600 hover:text-slate-900"
} ${!selectedUser ? "opacity-50 cursor-not-allowed" : ""}`}
>
User Details
</button>
</div>
{/* Tab Content */}
{activeTab === "users" && (
<Card className="border border-slate-200">
<CardContent className="p-0">
<UserGrid
users={users}
onUserSelect={(user) => {
handleUserSelect(user);
setActiveTab("details");
}}
onEditUser={handleEditUser}
onDeleteUser={handleDeleteUser}
onBulkDelete={handleBulkDelete}
loading={loading}
/>
</CardContent>
</Card>
)}
{activeTab === "details" && selectedUser && (
<div className="p-6 bg-white rounded-xl border border-slate-200">
<div className="mb-6">
<h3 className="text-xl font-black text-slate-900">
{selectedUser.firstName} {selectedUser.lastName}
</h3>
<p className="text-sm text-slate-600 mt-1">{selectedUser.email}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Role</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.role}</p>
</div>
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Joined</p>
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p>
</div>
{selectedUser.client && (
<>
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Membership</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipType}</p>
</div>
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Status</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipStatus}</p>
</div>
</>
)}
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Check-ins This Month</p>
<p className="text-2xl font-black text-blue-600">{selectedUser.checkInsThisMonth || 0}</p>
</div>
</div>
<div className="flex gap-3 mt-8 pt-6 border-t border-slate-200">
<button
onClick={() => selectedUser && handleEditUser(selectedUser)}
className="px-5 py-2 bg-blue-50 text-blue-600 rounded-lg font-semibold text-sm hover:bg-blue-100 transition-all"
>
Edit User
</button>
<button
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
className="px-5 py-2 bg-red-50 text-red-600 rounded-lg font-semibold text-sm hover:bg-red-100 transition-all"
>
Delete User
</button>
<a
href={`/users/${selectedUser.id}`}
className="px-5 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg font-semibold text-sm hover:shadow-lg transition-all"
>
Full Profile
</a>
</div>
</div>
)}
{isEditing && editForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@@ -452,98 +563,64 @@ export function UserManagement() {
)}
{selectedUser && (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<h3 className="text-lg font-semibold">User Details</h3>
<div className="mt-8 pt-8 border-t border-slate-200">
<div className="mb-6">
<h3 className="text-lg font-black text-slate-900">
{selectedUser.firstName} {selectedUser.lastName}
</h3>
<p className="text-sm text-slate-600 mt-1">{selectedUser.email}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="space-y-3">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Role</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.role}</p>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Joined</p>
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p>
</div>
{selectedUser.client && (
<div className="space-y-3">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Membership</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipType}</p>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Status</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipStatus}</p>
</div>
)}
<div className="space-y-3">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Check-ins This Month</p>
<p className="text-2xl font-black text-blue-600">{selectedUser.checkInsThisMonth || 0}</p>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Last Check-in</p>
<p className="text-sm text-slate-900">
{selectedUser.lastCheckInTime
? new Date(selectedUser.lastCheckInTime).toLocaleDateString()
: "Never"}
</p>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => selectedUser && handleEditUser(selectedUser)}
className="px-5 py-2 bg-blue-50 text-blue-600 rounded-lg font-semibold text-sm hover:bg-blue-100 transition-all"
>
Edit
</button>
<button
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
className="px-5 py-2 bg-red-50 text-red-600 rounded-lg font-semibold text-sm hover:bg-red-100 transition-all"
>
Delete
</button>
<a
href={`/users/${selectedUser.id}`}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
className="px-5 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg font-semibold text-sm hover:shadow-lg transition-all"
>
View Full Profile & Recommendations
Full Profile
</a>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div>
<h4 className="font-medium mb-2">Basic Information</h4>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Name:</span>{" "}
{selectedUser.firstName} {selectedUser.lastName}
</p>
<p>
<span className="font-medium">Email:</span>{" "}
{selectedUser.email}
</p>
<p>
<span className="font-medium">Phone:</span>{" "}
{selectedUser.phone || "N/A"}
</p>
<p>
<span className="font-medium">Role:</span>{" "}
{selectedUser.role}
</p>
<p>
<span className="font-medium">Joined:</span>{" "}
{new Date(selectedUser.createdAt).toLocaleDateString()}
</p>
</div>
</div>
{selectedUser.client && (
<div>
<h4 className="font-medium mb-2">Client Information</h4>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Membership:</span>{" "}
{selectedUser.client.membershipType}
</p>
<p>
<span className="font-medium">Status:</span>{" "}
{selectedUser.client.membershipStatus}
</p>
<p>
<span className="font-medium">Member Since:</span>{" "}
{new Date(
selectedUser.client.joinDate,
).toLocaleDateString()}
</p>
<p>
<span className="font-medium">Last Visit:</span>{" "}
{selectedUser.client.lastVisit
? new Date(
selectedUser.client.lastVisit,
).toLocaleDateString()
: "Never"}
</p>
</div>
</div>
)}
<div>
<h4 className="font-medium mb-2">Check-In Statistics</h4>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Last Check-In:</span>{" "}
{selectedUser.lastCheckInTime
? new Date(
selectedUser.lastCheckInTime,
).toLocaleString()
: "Never"}
</p>
<p>
<span className="font-medium">This Week:</span>{" "}
{selectedUser.checkInsThisWeek || 0} check-ins
</p>
<p>
<span className="font-medium">This Month:</span>{" "}
{selectedUser.checkInsThisMonth || 0} check-ins
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)}
</div>
);
-1
View File
@@ -22,7 +22,6 @@
"name": "next"
}
],
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@/*": [
-10
View File
@@ -28,7 +28,6 @@
"expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-location": "~17.0.1",
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",
"expo-secure-store": "~15.0.7",
@@ -7330,15 +7329,6 @@
"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": {
"version": "3.0.22",
"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-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-location": "~17.0.1",
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",
"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 { theme } from '../../styles/theme'
import { Animated } from 'react-native'
import { GeofenceStatus } from '../../components/GeofenceStatus'
export default function AttendanceScreen() {
const { getToken, userId } = useAuth()
@@ -112,9 +111,6 @@ export default function AttendanceScreen() {
<Text style={styles.subtitle}>Track your gym visits</Text>
</LinearGradient>
{/* Geofencing Status Card */}
<GeofenceStatus />
<View style={styles.actionContainer}>
{activeCheckIn ? (
<LinearGradient
+3 -13
View File
@@ -10,7 +10,6 @@ import { TrackMealModal } from "../../components/TrackMealModal";
import { AddWaterModal } from "../../components/AddWaterModal";
import { HydrationWidget } from "../../components/HydrationWidget";
import { ScanFoodModal } from "../../components/ScanFoodModal";
import { PerformanceMetrics } from "../../components/PerformanceMetrics";
import { Ionicons } from "@expo/vector-icons";
export default function HomeScreen() {
@@ -110,15 +109,9 @@ export default function HomeScreen() {
>
{/* Header Section */}
<View style={styles.header}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Image
source={require("../../../public/nextform-logo.png")}
style={{ width: 40, height: 40, resizeMode: 'contain' }}
/>
<View>
<Text style={styles.greeting}>{getGreeting()},</Text>
<Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
</View>
<View>
<Text style={styles.greeting}>{getGreeting()},</Text>
<Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
</View>
<View style={styles.avatarContainer}>
{user?.imageUrl ? (
@@ -138,9 +131,6 @@ export default function HomeScreen() {
duration={45}
/>
{/* Performance Metrics */}
<PerformanceMetrics />
{/* Hydration Widget */}
<HydrationWidget
current={waterIntake}
@@ -16,7 +16,7 @@ export function ActivityWidget({ steps, calories, duration }: ActivityWidgetProp
return (
<View style={styles.container}>
<LinearGradient
colors={['#0c4a6e', '#0369a1', '#0ea5e9']}
colors={theme.gradients.dark}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
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 (
<View style={styles.container}>
<LinearGradient
colors={['#0369a1', '#0ea5e9', '#06b6d4']}
colors={['#e0f2fe', '#dbeafe']} // Light blue background
style={[styles.card, theme.shadows.subtle]}
>
<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 { 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 { Ionicons } from '@expo/vector-icons';
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) {
const [permission, requestPermission] = Platform.OS === 'web' ? [null, null] : useCameraPermissions() as any;
const [permission, requestPermission] = useCameraPermissions();
const [scanned, setScanned] = useState(false);
const [foodData, setFoodData] = useState<{ name: string; calories: number; servingSize: string } | null>(null);
const [servings, setServings] = useState('1');
@@ -68,29 +68,10 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
};
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;
}
if (!permission.granted && Platform.OS !== 'web') {
if (!permission.granted) {
return (
<Modal visible={visible} transparent animationType="slide">
<View style={styles.permissionContainer}>
@@ -130,25 +111,19 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
<View style={{ width: 28 }} />
</View>
{Platform.OS === 'web' ? (
<View style={styles.webPlaceholder}>
<Text style={styles.webText}>Barcode scanning is only available on mobile devices</Text>
<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
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>
)}
</CameraView>
</>
) : (
<View style={styles.resultContainer}>
@@ -255,18 +230,6 @@ const styles = StyleSheet.create({
camera: {
flex: 1,
},
webPlaceholder: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1a1a1a',
},
webText: {
color: '#999',
fontSize: 16,
textAlign: 'center',
paddingHorizontal: 40,
},
scanOverlay: {
flex: 1,
justifyContent: 'center',
+4 -8
View File
@@ -1,17 +1,13 @@
import React from "react";
import * as WebBrowser from "expo-web-browser";
import { Platform } from "react-native";
export const useWarmUpBrowser = () => {
React.useEffect(() => {
// Warm up the android browser to improve UX
// https://docs.expo.dev/guides/authentication/#improving-user-experience
// Only available on native platforms (iOS/Android), not on web
if (Platform.OS !== "web") {
void WebBrowser.warmUpAsync();
return () => {
void WebBrowser.coolDownAsync();
};
}
void WebBrowser.warmUpAsync();
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,
lavender: ['#a78bfa', '#ec4899'] 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
-1
View File
@@ -1,6 +1,5 @@
{
"compilerOptions": {
"ignoreDeprecations": "6.0",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
+16
View File
@@ -0,0 +1,16 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
-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"
}