Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c2a3daee0 | ||
|
|
19ece90c6f | ||
|
|
b9efedbb88 | ||
|
|
204e6edca5 | ||
|
|
918357c00c | ||
|
|
4fbe8399df | ||
|
|
61bad8d30c | ||
|
|
966bcb084d | ||
|
|
71a05c51bf | ||
|
|
e4eadc858c | ||
|
|
0024510fdb | ||
|
|
c9fbab3bbb | ||
|
|
35d64ef3c0 | ||
|
|
6dc90f1745 | ||
|
|
02452c11e6 | ||
|
|
dd78602dd6 | ||
|
|
0a55438bce | ||
|
|
51df7f57ec | ||
|
|
feacef2a81 | ||
|
|
77546a5017 | ||
|
|
d38f0a4cc2 | ||
|
|
b6764b5ed6 | ||
|
|
e84d0f5f8f | ||
|
|
fda66a7703 | ||
|
|
2d9fbd186b | ||
|
|
bbd0cfde9c | ||
|
|
16e22f330a | ||
|
|
efa2f2a8d8 |
No files matched your search
@@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
|
|||||||
// POST - Mark goal as complete
|
// POST - Mark goal as complete
|
||||||
export async function POST(
|
export async function POST(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
|
|||||||
// GET - Get specific goal
|
// GET - Get specific goal
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
@@ -40,7 +40,7 @@ export async function GET(
|
|||||||
// PUT - Update goal
|
// PUT - Update goal
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
@@ -82,7 +82,7 @@ export async function PUT(
|
|||||||
// DELETE - Delete goal
|
// DELETE - Delete goal
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|||||||
@@ -25,11 +25,34 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<ClerkProvider>
|
<ClerkProvider>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={`${inter.className} bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100 min-h-screen`}>
|
||||||
<div className="flex min-h-screen bg-gradient-to-br from-white via-blue-50 to-slate-50">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 ml-64 p-8">
|
<main className="flex-1 ml-64">
|
||||||
{children}
|
<div className="sticky top-0 z-40 backdrop-blur-xl bg-white/75 border-b border-slate-200/50 shadow-sm">
|
||||||
|
<div className="px-8 py-4 flex items-center justify-between max-w-7xl mx-auto">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-sm font-semibold text-gray-600 uppercase tracking-wide">FitAI Pro</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<SignedIn>
|
||||||
|
<UserButton
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
avatarBox: "w-10 h-10 rounded-full ring-2 ring-blue-200"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SignedIn>
|
||||||
|
<SignedOut>
|
||||||
|
<SignInButton mode="modal" />
|
||||||
|
</SignedOut>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-8">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+67
-119
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Users, Activity, Zap, TrendingUp, Trophy, Target } from "lucide-react";
|
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
|
||||||
import { StatsCard } from "@/components/ui/StatsCard";
|
import { StatsCard } from "@/components/ui/StatsCard";
|
||||||
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
|
||||||
import { UserManagement } from "@/components/users/UserManagement";
|
import { UserManagement } from "@/components/users/UserManagement";
|
||||||
|
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
interface DashboardStats {
|
interface DashboardStats {
|
||||||
@@ -46,129 +46,77 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-white via-slate-50 to-blue-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100">
|
||||||
{/* Premium Header */}
|
<div className="space-y-8 max-w-7xl">
|
||||||
<div className="bg-gradient-to-r from-slate-900 via-blue-900 to-slate-900 shadow-2xl">
|
{/* Hero Section */}
|
||||||
<div className="max-w-7xl mx-auto px-8 py-12">
|
<div className="pt-8 pb-4">
|
||||||
<div className="flex items-end justify-between">
|
<div className="space-y-3">
|
||||||
<div className="flex items-end gap-6">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-400 to-cyan-400 rounded-3xl flex items-center justify-center shadow-lg">
|
<div className="h-12 w-1 bg-gradient-to-b from-blue-600 to-cyan-600 rounded-full"></div>
|
||||||
<Trophy className="w-9 h-9 text-white" />
|
<h1 className="text-5xl font-black bg-gradient-to-r from-blue-600 via-blue-700 to-cyan-600 bg-clip-text text-transparent">
|
||||||
</div>
|
FitAI Dashboard
|
||||||
|
</h1>
|
||||||
|
</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-white rounded-2xl shadow-lg border border-slate-200/80 p-7 hover:shadow-xl transition-shadow">
|
||||||
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-5xl font-black text-white tracking-tight">FitAI</h1>
|
<h2 className="text-2xl font-bold text-slate-900">Active Athletes</h2>
|
||||||
<p className="text-blue-200 text-sm font-semibold mt-2">Premium Sports Management Platform</p>
|
<p className="text-sm text-gray-500 mt-1">Manage and monitor your fitness clients</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<UserManagement />
|
||||||
<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>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content with Proper Spacing */}
|
{/* Analytics - 3 Columns Horizontal Layout */}
|
||||||
<div className="max-w-7xl mx-auto px-8 py-16">
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
{/* Section: Performance Metrics */}
|
<h2 className="text-2xl font-bold text-slate-900">Analytics</h2>
|
||||||
<div className="mb-20">
|
<p className="text-sm text-gray-500 mt-1">Performance metrics and insights</p>
|
||||||
<div className="mb-10">
|
|
||||||
<h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2>
|
|
||||||
<p className="text-slate-600 mt-2 font-medium">Real-time insights into your platform</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
||||||
<StatsCard
|
|
||||||
title="Total Members"
|
|
||||||
value={loading ? "..." : stats.totalUsers}
|
|
||||||
change="+12% this month"
|
|
||||||
trend="up"
|
|
||||||
icon={Users}
|
|
||||||
color="blue"
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title="Active Athletes"
|
|
||||||
value={loading ? "..." : stats.activeClients}
|
|
||||||
change="+8% this week"
|
|
||||||
trend="up"
|
|
||||||
icon={Activity}
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title="Revenue"
|
|
||||||
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
|
|
||||||
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
|
|
||||||
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
|
|
||||||
icon={Zap}
|
|
||||||
color="amber"
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title="Growth"
|
|
||||||
value="24%"
|
|
||||||
change="+3% vs last month"
|
|
||||||
trend="up"
|
|
||||||
icon={TrendingUp}
|
|
||||||
color="cyan"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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>
|
</div>
|
||||||
|
<AnalyticsDashboard />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -67,74 +67,75 @@ export function AnalyticsDashboard() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center h-64">
|
<div className="flex justify-center items-center h-64">
|
||||||
<div className="text-lg">Loading analytics...</div>
|
<div className="text-center">
|
||||||
|
<div className="inline-block animate-spin">⚡</div>
|
||||||
|
<p className="text-gray-600 mt-2 text-sm">Loading analytics...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
<h2 className="text-2xl font-bold">Analytics Dashboard</h2>
|
{/* Key Metrics Cards - 3 columns */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<div className="bg-gradient-to-br from-blue-600 via-cyan-500 to-teal-500 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
|
||||||
|
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Athletes</p>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<div className="text-2xl font-black text-white">{totalUsers}</div>
|
||||||
|
<span className="text-xs text-white font-semibold">active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Key Metrics */}
|
<div className="bg-gradient-to-br from-emerald-600 via-teal-500 to-cyan-500 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Revenue</p>
|
||||||
<Card>
|
<div className="flex items-baseline gap-2">
|
||||||
<CardContent>
|
<div className="text-2xl font-black text-white">${totalRevenue.toLocaleString()}</div>
|
||||||
<div className="text-center">
|
<span className="text-xs text-white font-semibold">ytd</span>
|
||||||
<div className="text-3xl font-bold text-blue-600">{totalUsers}</div>
|
</div>
|
||||||
<div className="text-gray-600">Total Users</div>
|
</div>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<div className="bg-gradient-to-br from-purple-600 via-pink-500 to-blue-500 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
|
||||||
<CardContent>
|
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Active Members</p>
|
||||||
<div className="text-center">
|
<div className="flex items-baseline gap-2">
|
||||||
<div className="text-3xl font-bold text-green-600">${totalRevenue.toLocaleString()}</div>
|
<div className="text-2xl font-black text-white">{activeMembers}</div>
|
||||||
<div className="text-gray-600">Total Revenue</div>
|
<span className="text-xs text-white font-semibold">members</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-bold text-purple-600">{activeMembers}</div>
|
|
||||||
<div className="text-gray-600">Active Members</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Charts */}
|
{/* Charts - 3 Columns Horizontal */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<Card>
|
<div className="bg-gradient-to-br from-blue-600 via-cyan-500 to-teal-500 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all">
|
||||||
<CardHeader>
|
<div className="mb-3">
|
||||||
<h3 className="text-lg font-semibold">User Growth</h3>
|
<h3 className="text-sm font-bold text-white">User Growth Trend</h3>
|
||||||
</CardHeader>
|
<p className="text-xs text-blue-100">Last 6 months performance</p>
|
||||||
<CardContent>
|
</div>
|
||||||
|
<div className="h-48 overflow-auto">
|
||||||
<UserGrowthChart data={userGrowthData} />
|
<UserGrowthChart data={userGrowthData} />
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<div className="bg-gradient-to-br from-emerald-600 via-teal-500 to-cyan-500 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all">
|
||||||
<CardHeader>
|
<div className="mb-3">
|
||||||
<h3 className="text-lg font-semibold">Membership Distribution</h3>
|
<h3 className="text-sm font-bold text-white">Membership Mix</h3>
|
||||||
</CardHeader>
|
<p className="text-xs text-emerald-100">Distribution breakdown</p>
|
||||||
<CardContent>
|
</div>
|
||||||
|
<div className="h-48 overflow-auto">
|
||||||
<MembershipDistributionChart data={membershipData} />
|
<MembershipDistributionChart data={membershipData} />
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<div className="bg-gradient-to-br from-purple-600 via-pink-500 to-blue-500 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all">
|
||||||
<CardHeader>
|
<div className="mb-3">
|
||||||
<h3 className="text-lg font-semibold">Monthly Revenue</h3>
|
<h3 className="text-sm font-bold text-white">Revenue Stream</h3>
|
||||||
</CardHeader>
|
<p className="text-xs text-purple-100">Monthly earnings</p>
|
||||||
<CardContent>
|
</div>
|
||||||
<RevenueChart data={revenueData} />
|
<div className="h-48 overflow-auto">
|
||||||
</CardContent>
|
<RevenueChart data={revenueData} />
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -65,15 +65,24 @@ export function Sidebar() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-64 bg-gradient-to-b from-white to-slate-50 h-screen fixed left-0 top-0 flex flex-col border-r border-slate-200/50">
|
<aside className="w-64 bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800/50 shadow-2xl">
|
||||||
<div className="p-6 border-b border-slate-200">
|
{/* Logo Section */}
|
||||||
<h1 className="text-2xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
|
<div className="p-6 border-b border-slate-800/50">
|
||||||
FitAI
|
<div className="flex items-center gap-3">
|
||||||
</h1>
|
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center font-bold text-white shadow-lg">
|
||||||
<p className="text-xs text-slate-600 font-medium mt-1">Sports Admin</p>
|
⚡
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-black bg-gradient-to-r from-blue-400 via-cyan-400 to-blue-300 bg-clip-text text-transparent">
|
||||||
|
FitAI
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-slate-400 font-semibold tracking-wide">ADMIN PRO</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 p-4 space-y-1">
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
||||||
{menuItems.map((item) => {
|
{menuItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = pathname === item.href;
|
const isActive = pathname === item.href;
|
||||||
@@ -82,26 +91,30 @@ export function Sidebar() {
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-200 group font-medium ${isActive
|
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-500 to-cyan-500 text-white shadow-md shadow-blue-500/20"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white shadow-lg shadow-blue-900/30"
|
||||||
: "text-slate-700 hover:bg-slate-100 hover:text-blue-600"}`}
|
: "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
|
||||||
>
|
>
|
||||||
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-blue-600"} />
|
{isActive && <div className="absolute inset-0 bg-white/10 blur-xl"></div>}
|
||||||
<span className="text-sm">{label}</span>
|
<Icon size={20} className={`${isActive ? "text-white" : "text-slate-500 group-hover:text-white"} relative z-10`} />
|
||||||
|
<span className="font-semibold relative z-10">{label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="p-4 border-t border-slate-200">
|
{/* User Section */}
|
||||||
<div className="flex items-center gap-3 px-4 py-3 rounded-2xl bg-slate-100">
|
<div className="p-4 border-t border-slate-800/50 bg-slate-800/20">
|
||||||
<UserButton afterSignOutUrl="/" />
|
<div className="flex items-center gap-3 px-3 py-3 rounded-xl bg-slate-800/50 hover:bg-slate-800/70 transition-colors">
|
||||||
|
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex-shrink-0 overflow-hidden">
|
||||||
|
<UserButton afterSignOutUrl="/" />
|
||||||
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-semibold text-slate-900 truncate">
|
<p className="text-sm font-semibold text-white truncate">
|
||||||
{user?.fullName || "Admin User"}
|
{user?.fullName || "Admin"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-600 truncate">
|
<p className="text-xs text-slate-400 truncate">
|
||||||
{user?.primaryEmailAddress?.emailAddress}
|
{user?.emailAddresses[0]?.emailAddress || "admin@fitai.com"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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;
|
||||||
@@ -6,67 +7,68 @@ interface StatsCardProps {
|
|||||||
change?: string;
|
change?: string;
|
||||||
trend?: "up" | "down" | "neutral";
|
trend?: "up" | "down" | "neutral";
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
color?: "blue" | "green" | "amber" | "cyan";
|
color?: "blue" | "green" | "purple" | "orange";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
|
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
|
||||||
const colorStyles = {
|
const colorStyles = {
|
||||||
blue: {
|
blue: {
|
||||||
bg: "from-blue-50 via-blue-100 to-blue-50",
|
bg: "from-blue-600 via-cyan-500 to-teal-500",
|
||||||
text: "text-blue-950",
|
text: "text-white",
|
||||||
icon: "bg-gradient-to-br from-blue-500 to-blue-600",
|
light: "from-blue-50 to-cyan-50",
|
||||||
border: "border-blue-200",
|
badge: "bg-blue-100 text-blue-700",
|
||||||
label: "text-blue-700"
|
icon: "bg-blue-100/50 text-blue-600",
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
bg: "from-emerald-50 via-emerald-100 to-emerald-50",
|
bg: "from-emerald-600 via-teal-500 to-cyan-500",
|
||||||
text: "text-emerald-950",
|
text: "text-white",
|
||||||
icon: "bg-gradient-to-br from-emerald-500 to-emerald-600",
|
light: "from-emerald-50 to-teal-50",
|
||||||
border: "border-emerald-200",
|
badge: "bg-emerald-100 text-emerald-700",
|
||||||
label: "text-emerald-700"
|
icon: "bg-emerald-100/50 text-emerald-600",
|
||||||
},
|
},
|
||||||
amber: {
|
purple: {
|
||||||
bg: "from-amber-50 via-amber-100 to-amber-50",
|
bg: "from-purple-600 via-pink-500 to-blue-500",
|
||||||
text: "text-amber-950",
|
text: "text-white",
|
||||||
icon: "bg-gradient-to-br from-amber-500 to-amber-600",
|
light: "from-purple-50 to-blue-50",
|
||||||
border: "border-amber-200",
|
badge: "bg-purple-100 text-purple-700",
|
||||||
label: "text-amber-700"
|
icon: "bg-purple-100/50 text-purple-600",
|
||||||
},
|
},
|
||||||
cyan: {
|
orange: {
|
||||||
bg: "from-cyan-50 via-cyan-100 to-cyan-50",
|
bg: "from-orange-600 via-red-500 to-pink-500",
|
||||||
text: "text-cyan-950",
|
text: "text-white",
|
||||||
icon: "bg-gradient-to-br from-cyan-500 to-cyan-600",
|
light: "from-orange-50 to-red-50",
|
||||||
border: "border-cyan-200",
|
badge: "bg-orange-100 text-orange-700",
|
||||||
label: "text-cyan-700"
|
icon: "bg-orange-100/50 text-orange-600",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const style = colorStyles[color];
|
const styles = colorStyles[color];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`bg-gradient-to-br ${style.bg} rounded-3xl border ${style.border} p-6 hover:shadow-xl transition-all duration-300 cursor-default group`}>
|
<Card className={`bg-gradient-to-br ${styles.bg} border-0 shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden group h-full`}>
|
||||||
<div className="flex items-start justify-between mb-4 gap-3">
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2.5 pt-5 px-5">
|
||||||
<div className="flex-1 min-w-0">
|
<CardTitle className={`text-xs font-bold uppercase tracking-wider ${styles.text} leading-tight max-w-[70%]`}>
|
||||||
<p className={`text-xs ${style.label} uppercase font-black tracking-widest truncate`}>
|
{title}
|
||||||
{title}
|
</CardTitle>
|
||||||
</p>
|
<div className={`p-1.5 rounded-lg ${styles.icon} flex-shrink-0`}>
|
||||||
|
<Icon size={16} strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`p-3 ${style.icon} rounded-2xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform flex-shrink-0`}>
|
</CardHeader>
|
||||||
<Icon size={20} className="text-white" />
|
<CardContent className="px-5 pb-5 space-y-2.5">
|
||||||
|
<div className={`text-2xl font-black ${styles.text}`}>
|
||||||
|
{value}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{change && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<div className={`text-4xl font-black ${style.text} mb-3 truncate`}>
|
<span
|
||||||
{value}
|
className={`inline-flex items-center gap-0.5 font-bold px-2 py-0.5 rounded-md text-xs tracking-wide ${styles.badge}`}
|
||||||
</div>
|
>
|
||||||
|
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
|
||||||
{change && (
|
</span>
|
||||||
<div className="flex items-center gap-1 truncate">
|
<span className="text-xs text-gray-600">vs month</span>
|
||||||
<span className={`text-xs font-black uppercase tracking-wide ${style.label} truncate`}>
|
</div>
|
||||||
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
|
)}
|
||||||
</span>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Re-export Button component with lowercase filename for compatibility
|
||||||
|
export { Button } from './Button';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Re-export Card components with lowercase filename for compatibility
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
|
||||||
@@ -31,7 +31,23 @@ export function CardContent({ children, className = '' }: CardProps) {
|
|||||||
|
|
||||||
export function CardTitle({ children, className = '' }: CardProps) {
|
export function CardTitle({ children, className = '' }: CardProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`text-sm font-medium ${className}`}>
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
interface Recommendation {
|
interface Recommendation {
|
||||||
|
|||||||
@@ -265,45 +265,72 @@ export function UserGrid({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
{/* Search and Actions Bar */}
|
||||||
<input
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-gradient-to-r from-slate-50 to-blue-50 p-4 rounded-xl border border-slate-200">
|
||||||
type="text"
|
<div className="w-full sm:w-auto flex-1">
|
||||||
placeholder="Search users..."
|
<div className="relative">
|
||||||
className="border border-gray-300 rounded px-4 py-2"
|
<svg className="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
value={searchQuery}
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
</svg>
|
||||||
/>
|
<input
|
||||||
<div className="flex gap-2">
|
type="text"
|
||||||
|
placeholder="Search athletes..."
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white shadow-sm"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 w-full sm:w-auto">
|
||||||
<button
|
<button
|
||||||
className="bg-green-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
className="flex-1 sm:flex-none bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-4 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed font-semibold transition-all shadow-md hover:shadow-lg"
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
disabled={selectedUsers.length !== 1}
|
disabled={selectedUsers.length !== 1}
|
||||||
>
|
>
|
||||||
Edit User
|
✏️ Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-red-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
className="flex-1 sm:flex-none bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-4 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed font-semibold transition-all shadow-md hover:shadow-lg"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={selectedUsers.length !== 1}
|
disabled={selectedUsers.length !== 1}
|
||||||
>
|
>
|
||||||
Delete User
|
🗑️ Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-yellow-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
className="flex-1 sm:flex-none bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white px-4 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed font-semibold transition-all shadow-md hover:shadow-lg"
|
||||||
onClick={handleBulkDelete}
|
onClick={handleBulkDelete}
|
||||||
disabled={selectedUsers.length === 0}
|
disabled={selectedUsers.length === 0}
|
||||||
>
|
>
|
||||||
Bulk Delete
|
⚡ Bulk
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="ag-theme-alpine"
|
{/* Table */}
|
||||||
style={{ height: "600px", width: "100%" }}
|
<div className="bg-white rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
|
||||||
>
|
<div
|
||||||
<AgGridReact<User> {...gridOptions} ref={gridRef} />
|
className="ag-theme-alpine"
|
||||||
|
style={{ height: "600px", width: "100%" }}
|
||||||
|
>
|
||||||
|
<AgGridReact<User> {...gridOptions} ref={gridRef} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Selected Count */}
|
||||||
|
{selectedUsers.length > 0 && (
|
||||||
|
<div className="flex items-center justify-between bg-gradient-to-r from-blue-500/10 to-cyan-500/10 border border-blue-200/50 px-4 py-3 rounded-lg">
|
||||||
|
<p className="text-sm font-semibold text-blue-700">
|
||||||
|
{selectedUsers.length} athlete{selectedUsers.length !== 1 ? 's' : ''} selected
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedUsers([])}
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
Clear selection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,6 @@ export function UserManagement() {
|
|||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<"users" | "details">("users");
|
|
||||||
const [editForm, setEditForm] = useState<{
|
const [editForm, setEditForm] = useState<{
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
@@ -207,227 +206,117 @@ export function UserManagement() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Header with Count */}
|
{/* Header Section */}
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<h3 className="text-lg font-black text-slate-900">
|
<h3 className="text-lg font-bold text-gray-900">User Management</h3>
|
||||||
{users.length} {users.length === 1 ? 'user' : 'users'}
|
<p className="text-sm text-gray-600 mt-1">Manage and monitor your fitness clients</p>
|
||||||
</h3>
|
</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
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<p className="text-sm text-blue-600 font-semibold">
|
<span className="ml-4 text-blue-600 font-medium">
|
||||||
Selected: {selectedUser.firstName} {selectedUser.lastName}
|
Selected: {selectedUser.firstName} {selectedUser.lastName}
|
||||||
</p>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compact Controls - Always Visible but Organized */}
|
{/* User Grid */}
|
||||||
<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">
|
<Card>
|
||||||
{/* Filter Row */}
|
<CardContent className="p-0">
|
||||||
<div>
|
<UserGrid
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Roles</p>
|
users={users}
|
||||||
<div className="flex flex-wrap gap-2">
|
onUserSelect={(user) => handleUserSelect(user)}
|
||||||
<button
|
onEditUser={handleEditUser}
|
||||||
onClick={() => setFilter("all")}
|
onDeleteUser={handleDeleteUser}
|
||||||
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
|
onBulkDelete={handleBulkDelete}
|
||||||
filter === "all"
|
loading={loading}
|
||||||
? "bg-blue-500 text-white shadow-md"
|
/>
|
||||||
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
|
</CardContent>
|
||||||
}`}
|
</Card>
|
||||||
>
|
|
||||||
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 && (
|
{isEditing && editForm && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
@@ -563,64 +452,98 @@ export function UserManagement() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<div className="mt-8 pt-8 border-t border-slate-200">
|
<Card>
|
||||||
<div className="mb-6">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<h3 className="text-lg font-black text-slate-900">
|
<h3 className="text-lg font-semibold">User Details</h3>
|
||||||
{selectedUser.firstName} {selectedUser.lastName}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-slate-600 mt-1">{selectedUser.email}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Role</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.role}</p>
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Joined</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedUser.client && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Membership</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipType}</p>
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Status</p>
|
|
||||||
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipStatus}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Check-ins This Month</p>
|
|
||||||
<p className="text-2xl font-black text-blue-600">{selectedUser.checkInsThisMonth || 0}</p>
|
|
||||||
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Last Check-in</p>
|
|
||||||
<p className="text-sm text-slate-900">
|
|
||||||
{selectedUser.lastCheckInTime
|
|
||||||
? new Date(selectedUser.lastCheckInTime).toLocaleDateString()
|
|
||||||
: "Never"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 mt-6">
|
|
||||||
<button
|
|
||||||
onClick={() => selectedUser && handleEditUser(selectedUser)}
|
|
||||||
className="px-5 py-2 bg-blue-50 text-blue-600 rounded-lg font-semibold text-sm hover:bg-blue-100 transition-all"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
|
|
||||||
className="px-5 py-2 bg-red-50 text-red-600 rounded-lg font-semibold text-sm hover:bg-red-100 transition-all"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
<a
|
<a
|
||||||
href={`/users/${selectedUser.id}`}
|
href={`/users/${selectedUser.id}`}
|
||||||
className="px-5 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg font-semibold text-sm hover:shadow-lg transition-all"
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
|
||||||
>
|
>
|
||||||
Full Profile
|
View Full Profile & Recommendations
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</CardHeader>
|
||||||
</div>
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Basic Information</h4>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Name:</span>{" "}
|
||||||
|
{selectedUser.firstName} {selectedUser.lastName}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Email:</span>{" "}
|
||||||
|
{selectedUser.email}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Phone:</span>{" "}
|
||||||
|
{selectedUser.phone || "N/A"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Role:</span>{" "}
|
||||||
|
{selectedUser.role}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Joined:</span>{" "}
|
||||||
|
{new Date(selectedUser.createdAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedUser.client && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Client Information</h4>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Membership:</span>{" "}
|
||||||
|
{selectedUser.client.membershipType}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Status:</span>{" "}
|
||||||
|
{selectedUser.client.membershipStatus}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Member Since:</span>{" "}
|
||||||
|
{new Date(
|
||||||
|
selectedUser.client.joinDate,
|
||||||
|
).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Last Visit:</span>{" "}
|
||||||
|
{selectedUser.client.lastVisit
|
||||||
|
? new Date(
|
||||||
|
selectedUser.client.lastVisit,
|
||||||
|
).toLocaleDateString()
|
||||||
|
: "Never"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Check-In Statistics</h4>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Last Check-In:</span>{" "}
|
||||||
|
{selectedUser.lastCheckInTime
|
||||||
|
? new Date(
|
||||||
|
selectedUser.lastCheckInTime,
|
||||||
|
).toLocaleString()
|
||||||
|
: "Never"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">This Week:</span>{" "}
|
||||||
|
{selectedUser.checkInsThisWeek || 0} check-ins
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">This Month:</span>{" "}
|
||||||
|
{selectedUser.checkInsThisMonth || 0} check-ins
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TrackMealModal } from "../../components/TrackMealModal";
|
|||||||
import { AddWaterModal } from "../../components/AddWaterModal";
|
import { AddWaterModal } from "../../components/AddWaterModal";
|
||||||
import { HydrationWidget } from "../../components/HydrationWidget";
|
import { HydrationWidget } from "../../components/HydrationWidget";
|
||||||
import { ScanFoodModal } from "../../components/ScanFoodModal";
|
import { ScanFoodModal } from "../../components/ScanFoodModal";
|
||||||
|
import { PerformanceMetrics } from "../../components/PerformanceMetrics";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
@@ -131,6 +132,9 @@ export default function HomeScreen() {
|
|||||||
duration={45}
|
duration={45}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Performance Metrics */}
|
||||||
|
<PerformanceMetrics />
|
||||||
|
|
||||||
{/* Hydration Widget */}
|
{/* Hydration Widget */}
|
||||||
<HydrationWidget
|
<HydrationWidget
|
||||||
current={waterIntake}
|
current={waterIntake}
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { theme } from '../styles/theme';
|
||||||
|
|
||||||
|
interface MetricProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
change: string;
|
||||||
|
trend: 'up' | 'down';
|
||||||
|
icon: string;
|
||||||
|
colorScheme: 'blue' | 'green' | 'purple' | 'orange';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColorScheme = (colorScheme: 'blue' | 'green' | 'purple' | 'orange') => {
|
||||||
|
const schemes = {
|
||||||
|
blue: {
|
||||||
|
colors: ['#1e3a8a', '#0c4a6e'],
|
||||||
|
text: '#ffffff',
|
||||||
|
label: '#93c5fd',
|
||||||
|
badge: '#3b82f6',
|
||||||
|
badgeText: '#ffffff',
|
||||||
|
icon: '#60a5fa',
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
colors: ['#064e3b', '#047857'],
|
||||||
|
text: '#ffffff',
|
||||||
|
label: '#86efac',
|
||||||
|
badge: '#10b981',
|
||||||
|
badgeText: '#ffffff',
|
||||||
|
icon: '#34d399',
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
colors: ['#581c87', '#7c3aed'],
|
||||||
|
text: '#ffffff',
|
||||||
|
label: '#d8b4fe',
|
||||||
|
badge: '#a855f7',
|
||||||
|
badgeText: '#ffffff',
|
||||||
|
icon: '#c084fc',
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
colors: ['#92400e', '#b45309'],
|
||||||
|
text: '#ffffff',
|
||||||
|
label: '#fcd34d',
|
||||||
|
badge: '#f97316',
|
||||||
|
badgeText: '#ffffff',
|
||||||
|
icon: '#fb923c',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return schemes[colorScheme];
|
||||||
|
};
|
||||||
|
|
||||||
|
const MetricCard: React.FC<MetricProps> = ({ title, value, change, trend, icon, colorScheme }) => {
|
||||||
|
const colors = getColorScheme(colorScheme);
|
||||||
|
const isPositive = trend === 'up';
|
||||||
|
const trendIcon = isPositive ? 'arrow-up' : 'arrow-down';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinearGradient
|
||||||
|
colors={colors.colors as any}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
style={styles.card}
|
||||||
|
>
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={[styles.label, { color: colors.label }]}>{title}</Text>
|
||||||
|
<View style={[styles.iconContainer, { backgroundColor: colors.icon + '40' }]}>
|
||||||
|
<Ionicons name={icon as any} size={20} color={colors.text} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[styles.value, { color: colors.text }]}>{value}</Text>
|
||||||
|
|
||||||
|
<View style={styles.changeContainer}>
|
||||||
|
<View style={[styles.trendBadge, { backgroundColor: colors.badge }]}>
|
||||||
|
<Ionicons
|
||||||
|
name={trendIcon}
|
||||||
|
size={12}
|
||||||
|
color={colors.badgeText}
|
||||||
|
style={{ marginRight: 4 }}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.changeText, { color: colors.badgeText, fontWeight: '700' }]}>
|
||||||
|
{trend === 'up' ? '↑' : '↓'} {change}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.compareText, { color: colors.text }]}>vs month</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PerformanceMetrics: React.FC = () => {
|
||||||
|
const metrics: MetricProps[] = [
|
||||||
|
{
|
||||||
|
title: 'Total Users',
|
||||||
|
value: '0',
|
||||||
|
change: '+12%',
|
||||||
|
trend: 'up',
|
||||||
|
icon: 'people',
|
||||||
|
colorScheme: 'blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Active Clients',
|
||||||
|
value: '0',
|
||||||
|
change: '+5%',
|
||||||
|
trend: 'up',
|
||||||
|
icon: 'person-add',
|
||||||
|
colorScheme: 'green',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Revenue',
|
||||||
|
value: '$0.00',
|
||||||
|
change: '0%',
|
||||||
|
trend: 'down',
|
||||||
|
icon: 'wallet',
|
||||||
|
colorScheme: 'purple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Growth',
|
||||||
|
value: '24%',
|
||||||
|
change: '-2%',
|
||||||
|
trend: 'down',
|
||||||
|
icon: 'trending-up',
|
||||||
|
colorScheme: 'orange',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Performance metrics & athlete insights</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.metricsGrid}>
|
||||||
|
{metrics.map((metric, index) => (
|
||||||
|
<View key={index} style={styles.metricWrapper}>
|
||||||
|
<MetricCard {...metric} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#4b5563',
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
metricsGrid: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
metricWrapper: {
|
||||||
|
width: '48%',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
borderRadius: 20,
|
||||||
|
overflow: 'hidden',
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 12,
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
padding: 16,
|
||||||
|
minHeight: 160,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 10,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
marginBottom: 12,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: '800',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
changeContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
},
|
||||||
|
trendBadge: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 6,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
changeText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
compareText: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, Modal, TouchableOpacity, TextInput, Alert } from 'react-native';
|
import { View, Text, StyleSheet, Modal, TouchableOpacity, TextInput, Alert, Platform } from 'react-native';
|
||||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
@@ -20,7 +20,7 @@ const FOOD_DATABASE: { [key: string]: { name: string; calories: number; servingS
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProps) {
|
export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProps) {
|
||||||
const [permission, requestPermission] = useCameraPermissions();
|
const [permission, requestPermission] = Platform.OS === 'web' ? [null, null] : useCameraPermissions() as any;
|
||||||
const [scanned, setScanned] = useState(false);
|
const [scanned, setScanned] = useState(false);
|
||||||
const [foodData, setFoodData] = useState<{ name: string; calories: number; servingSize: string } | null>(null);
|
const [foodData, setFoodData] = useState<{ name: string; calories: number; servingSize: string } | null>(null);
|
||||||
const [servings, setServings] = useState('1');
|
const [servings, setServings] = useState('1');
|
||||||
@@ -68,10 +68,29 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!permission) {
|
if (!permission) {
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
// On web, show normal modal without permissions
|
||||||
|
return (
|
||||||
|
<Modal visible={visible} animationType="slide">
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||||
|
<Ionicons name="close" size={28} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.title}>Scan Food Barcode</Text>
|
||||||
|
<View style={{ width: 28 }} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.webPlaceholder}>
|
||||||
|
<Text style={styles.webText}>Barcode scanning is only available on mobile devices</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permission.granted) {
|
if (!permission.granted && Platform.OS !== 'web') {
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} transparent animationType="slide">
|
<Modal visible={visible} transparent animationType="slide">
|
||||||
<View style={styles.permissionContainer}>
|
<View style={styles.permissionContainer}>
|
||||||
@@ -111,19 +130,25 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
|
|||||||
<View style={{ width: 28 }} />
|
<View style={{ width: 28 }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<CameraView
|
{Platform.OS === 'web' ? (
|
||||||
style={styles.camera}
|
<View style={styles.webPlaceholder}>
|
||||||
facing="back"
|
<Text style={styles.webText}>Barcode scanning is only available on mobile devices</Text>
|
||||||
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
|
|
||||||
barcodeScannerSettings={{
|
|
||||||
barcodeTypes: ['ean13', 'ean8', 'upc_a', 'upc_e', 'code128', 'code39'],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={styles.scanOverlay}>
|
|
||||||
<View style={styles.scanFrame} />
|
|
||||||
<Text style={styles.scanText}>Position barcode within frame</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</CameraView>
|
) : (
|
||||||
|
<CameraView
|
||||||
|
style={styles.camera}
|
||||||
|
facing="back"
|
||||||
|
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
|
||||||
|
barcodeScannerSettings={{
|
||||||
|
barcodeTypes: ['ean13', 'ean8', 'upc_a', 'upc_e', 'code128', 'code39'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={styles.scanOverlay}>
|
||||||
|
<View style={styles.scanFrame} />
|
||||||
|
<Text style={styles.scanText}>Position barcode within frame</Text>
|
||||||
|
</View>
|
||||||
|
</CameraView>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.resultContainer}>
|
<View style={styles.resultContainer}>
|
||||||
@@ -230,6 +255,18 @@ const styles = StyleSheet.create({
|
|||||||
camera: {
|
camera: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
webPlaceholder: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
},
|
||||||
|
webText: {
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingHorizontal: 40,
|
||||||
|
},
|
||||||
scanOverlay: {
|
scanOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as WebBrowser from "expo-web-browser";
|
import * as WebBrowser from "expo-web-browser";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
export const useWarmUpBrowser = () => {
|
export const useWarmUpBrowser = () => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Warm up the android browser to improve UX
|
// Warm up the android browser to improve UX
|
||||||
// https://docs.expo.dev/guides/authentication/#improving-user-experience
|
// https://docs.expo.dev/guides/authentication/#improving-user-experience
|
||||||
void WebBrowser.warmUpAsync();
|
// Only available on native platforms (iOS/Android), not on web
|
||||||
return () => {
|
if (Platform.OS !== "web") {
|
||||||
void WebBrowser.coolDownAsync();
|
void WebBrowser.warmUpAsync();
|
||||||
};
|
return () => {
|
||||||
|
void WebBrowser.coolDownAsync();
|
||||||
|
};
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
};
|
};
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
|
||||||
|
|
||||||
Commands marked with * may be preceded by a number, _N.
|
|
||||||
Notes in parentheses indicate the behavior if _N is given.
|
|
||||||
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
|
||||||
|
|
||||||
h H Display this help.
|
|
||||||
q :q Q :Q ZZ Exit.
|
|
||||||
---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
MMOOVVIINNGG
|
|
||||||
|
|
||||||
e ^E j ^N CR * Forward one line (or _N lines).
|
|
||||||
y ^Y k ^K ^P * Backward one line (or _N lines).
|
|
||||||
f ^F ^V SPACE * Forward one window (or _N lines).
|
|
||||||
Reference in new issue
Block a user