Compare commits
4 Commits
8c2a3daee0
...
5fd91ec5ad
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fd91ec5ad | |||
| 1ad146fe57 | |||
| bf741d6d00 | |||
| ddb6933e42 |
BIN
apps/admin/public/logo.png
Normal file
BIN
apps/admin/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@ -2,9 +2,17 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Sports Theme Colors */
|
||||
:root {
|
||||
--primary-blue: #2563eb;
|
||||
--secondary-orange: #fb7a1b;
|
||||
--accent-green: #22c55e;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
/* Light Mode - Sports Theme */
|
||||
--background: 0 0% 98%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
@ -13,56 +21,63 @@
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
/* Electric Blue Primary */
|
||||
--primary: 217 91.2% 59.8%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
/* Vibrant Orange Secondary */
|
||||
--secondary: 24.6 97.4% 54.3%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
|
||||
/* Emerald Green Accent */
|
||||
--accent: 142.1 70.6% 45.3%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--ring: 217 91.2% 59.8%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark Mode - Sports Theme */
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card: 225.7 29.5% 15.3%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover: 225.7 29.5% 15.3%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
/* Electric Blue Primary */
|
||||
--primary: 217 91.2% 59.8%;
|
||||
--primary-foreground: 225.7 29.5% 15.3%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
/* Vibrant Orange Secondary */
|
||||
--secondary: 24.6 97.4% 54.3%;
|
||||
--secondary-foreground: 225.7 29.5% 15.3%;
|
||||
|
||||
/* Emerald Green Accent */
|
||||
--accent: 142.1 70.6% 45.3%;
|
||||
--accent-foreground: 225.7 29.5% 15.3%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--ring: 217 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +86,28 @@
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground transition-colors duration-300;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-bold text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.sports-gradient {
|
||||
@apply bg-gradient-to-r from-[#2563eb] via-[#fb7a1b] to-[#22c55e];
|
||||
}
|
||||
|
||||
.card-modern {
|
||||
@apply bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700;
|
||||
}
|
||||
|
||||
.btn-sports {
|
||||
@apply bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] hover:shadow-lg transform hover:scale-105 transition-all duration-200 text-white font-bold;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
@apply bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] bg-clip-text text-transparent;
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
|
||||
import { Users, CreditCard, CalendarCheck, TrendingUp, Brain, Calendar, User } from "lucide-react";
|
||||
import { StatsCard } from "@/components/ui/StatsCard";
|
||||
import { UserManagement } from "@/components/users/UserManagement";
|
||||
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
||||
import { Logo } from "@/components/ui/Logo";
|
||||
import axios from "axios";
|
||||
|
||||
interface DashboardStats {
|
||||
@ -14,6 +15,27 @@ interface DashboardStats {
|
||||
revenueGrowth: number;
|
||||
}
|
||||
|
||||
type TabType = "overview" | "users" | "analytics" | "recommendations" | "attendance" | "profile";
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
activityPlan: string;
|
||||
dietPlan: string;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface AttendanceRecord {
|
||||
id: string;
|
||||
clientId: string;
|
||||
checkInTime: string;
|
||||
checkOutTime?: string;
|
||||
type: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
totalUsers: 0,
|
||||
@ -22,6 +44,11 @@ export default function Home() {
|
||||
revenueGrowth: 0,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<TabType>("overview");
|
||||
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
|
||||
const [attendance, setAttendance] = useState<AttendanceRecord[]>([]);
|
||||
const [recommendationsLoading, setRecommendationsLoading] = useState(false);
|
||||
const [attendanceLoading, setAttendanceLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
@ -38,6 +65,46 @@ export default function Home() {
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
// Fetch recommendations when tab changes
|
||||
useEffect(() => {
|
||||
if (activeTab === "recommendations") {
|
||||
fetchRecommendations();
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
// Fetch attendance when tab changes
|
||||
useEffect(() => {
|
||||
if (activeTab === "attendance") {
|
||||
fetchAttendance();
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const fetchRecommendations = async () => {
|
||||
setRecommendationsLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/recommendations");
|
||||
const data = await response.json();
|
||||
setRecommendations(data.recommendations || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching recommendations:", error);
|
||||
} finally {
|
||||
setRecommendationsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAttendance = async () => {
|
||||
setAttendanceLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/admin/attendance");
|
||||
const data = await response.json();
|
||||
setAttendance(data || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching attendance:", error);
|
||||
} finally {
|
||||
setAttendanceLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
@ -45,18 +112,56 @@ export default function Home() {
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: "overview", label: "Overview", icon: <TrendingUp size={18} /> },
|
||||
{ id: "users", label: "Users", icon: <Users size={18} /> },
|
||||
{ id: "analytics", label: "Analytics", icon: <TrendingUp size={18} /> },
|
||||
{ id: "recommendations", label: "Recommendations", icon: <Brain size={18} /> },
|
||||
{ id: "attendance", label: "Attendance", icon: <Calendar size={18} /> },
|
||||
{ id: "profile", label: "Profile", icon: <User size={18} /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-slate-900">Dashboard</h2>
|
||||
<p className="text-slate-500 mt-2">Welcome back, here's what's happening today.</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-100 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950">
|
||||
<div className="w-full px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
|
||||
{/* Header Section */}
|
||||
<div className="mb-8 sm:mb-10">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Logo variant="full" />
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white mt-4">Admin Dashboard</h2>
|
||||
<p className="text-slate-600 dark:text-slate-400 mt-2 text-sm sm:text-base">Manage your fitness platform with complete control.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* Tab Navigation */}
|
||||
<div className="mb-8 overflow-x-auto">
|
||||
<div className="flex gap-2 pb-4 min-w-min">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-all cursor-pointer ${
|
||||
activeTab === tab.id
|
||||
? "bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] text-white shadow-lg hover:shadow-xl"
|
||||
: "bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 hover:border-slate-300 dark:hover:border-slate-600"
|
||||
}`}
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Tab */}
|
||||
{activeTab === "overview" && (
|
||||
<div className="space-y-8">
|
||||
{/* Stats Cards - Responsive Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-5 lg:gap-6">
|
||||
<StatsCard
|
||||
title="Total Users"
|
||||
value={loading ? "..." : stats.totalUsers}
|
||||
change="+12%" // Placeholder for now as we don't track historical growth yet
|
||||
change="+12%"
|
||||
trend="up"
|
||||
icon={Users}
|
||||
color="blue"
|
||||
@ -79,7 +184,7 @@ export default function Home() {
|
||||
/>
|
||||
<StatsCard
|
||||
title="Growth"
|
||||
value="24%" // Placeholder
|
||||
value="24%"
|
||||
change="-2%"
|
||||
trend="down"
|
||||
icon={TrendingUp}
|
||||
@ -87,17 +192,160 @@ export default function Home() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-6">Recent Activity</h3>
|
||||
{/* Quick Stats Section */}
|
||||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] p-6 sm:p-8">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-white">Quick Overview</h3>
|
||||
</div>
|
||||
<div className="p-6 sm:p-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="text-center p-4 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<p className="text-slate-600 dark:text-slate-400 mb-2">Total Revenue</p>
|
||||
<p className="text-3xl font-bold text-[#2563eb]">{formatCurrency(stats.totalRevenue)}</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<p className="text-slate-600 dark:text-slate-400 mb-2">Active Members</p>
|
||||
<p className="text-3xl font-bold text-[#22c55e]">{stats.activeClients}</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<p className="text-slate-600 dark:text-slate-400 mb-2">Platform Users</p>
|
||||
<p className="text-3xl font-bold text-[#fb7a1b]">{stats.totalUsers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Users Tab */}
|
||||
{activeTab === "users" && (
|
||||
<div className="space-y-6 sm:space-y-8">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-[#2563eb] to-[#fb7a1b] p-6 sm:p-8">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Users size={28} />
|
||||
User Management
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 sm:p-8 w-full overflow-hidden">
|
||||
<UserManagement />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-6">Quick Analytics</h3>
|
||||
{/* Analytics Tab */}
|
||||
{activeTab === "analytics" && (
|
||||
<div className="space-y-6 sm:space-y-8">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-[#fb7a1b] to-[#22c55e] p-6 sm:p-8">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
|
||||
<TrendingUp size={28} />
|
||||
Analytics Dashboard
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 sm:p-8 w-full overflow-hidden">
|
||||
<AnalyticsDashboard />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other Tabs - Coming Soon */}
|
||||
{activeTab === "recommendations" && (
|
||||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-[#2563eb] via-[#fb7a1b] to-[#22c55e] p-6 sm:p-8">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Brain size={28} />
|
||||
AI Recommendations
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 sm:p-8">
|
||||
{recommendationsLoading ? (
|
||||
<div className="text-center py-8">Loading recommendations...</div>
|
||||
) : recommendations.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{recommendations.slice(0, 6).map((rec) => (
|
||||
<div key={rec.id} className="p-4 border border-slate-200 dark:border-slate-600 rounded-lg hover:shadow-lg transition-all">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-bold text-sm line-clamp-1">User ID: {rec.userId}</h4>
|
||||
<span className={`text-xs px-2 py-1 rounded ${rec.status === 'pending' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' : 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'}`}>
|
||||
{rec.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 line-clamp-2 mb-2">{rec.content}</p>
|
||||
<p className="text-xs text-slate-500">Activity: {rec.activityPlan.substring(0, 50)}...</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-slate-600 dark:text-slate-400">No recommendations yet</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attendance Tab */}
|
||||
{activeTab === "attendance" && (
|
||||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-[#fb7a1b] to-[#22c55e] p-6 sm:p-8">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Calendar size={28} />
|
||||
Attendance Monitoring
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 sm:p-8 overflow-x-auto">
|
||||
{attendanceLoading ? (
|
||||
<div className="text-center py-8">Loading attendance data...</div>
|
||||
) : attendance.length > 0 ? (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 dark:border-slate-600">
|
||||
<th className="text-left py-2 px-4 font-bold">Client ID</th>
|
||||
<th className="text-left py-2 px-4 font-bold">Check In</th>
|
||||
<th className="text-left py-2 px-4 font-bold">Check Out</th>
|
||||
<th className="text-left py-2 px-4 font-bold">Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{attendance.slice(0, 10).map((record) => (
|
||||
<tr key={record.id} className="border-b border-slate-100 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700">
|
||||
<td className="py-3 px-4 line-clamp-1">{record.clientId}</td>
|
||||
<td className="py-3 px-4">{new Date(record.checkInTime).toLocaleString()}</td>
|
||||
<td className="py-3 px-4">{record.checkOutTime ? new Date(record.checkOutTime).toLocaleString() : '-'}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="px-2 py-1 rounded-full text-xs font-bold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
{record.type}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="text-center py-8 text-slate-600 dark:text-slate-400">No attendance records</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Profile Tab */}
|
||||
{activeTab === "profile" && (
|
||||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-[#22c55e] to-[#2563eb] p-6 sm:p-8">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2">
|
||||
<User size={28} />
|
||||
Admin Profile
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 sm:p-8">
|
||||
<p className="text-slate-600 dark:text-slate-400 text-lg">Profile management coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,23 +1,56 @@
|
||||
import React from 'react'
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary'
|
||||
children: React.ReactNode
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export function Button({ variant = 'primary', children, className = '', ...props }: ButtonProps) {
|
||||
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors'
|
||||
const variantClasses = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700',
|
||||
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<button
|
||||
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@ -1,30 +1,76 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
|
||||
interface CardProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function Card({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
export function CardHeader({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
export function CardContent({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
||||
69
apps/admin/src/components/ui/Logo.tsx
Normal file
69
apps/admin/src/components/ui/Logo.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface LogoProps {
|
||||
variant?: "full" | "icon" | "text";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Logo({ variant = "full", className = "" }: LogoProps) {
|
||||
if (variant === "icon") {
|
||||
return (
|
||||
<div className={`relative w-24 h-24 flex-shrink-0 ${className}`}>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="FitAI Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "text") {
|
||||
return (
|
||||
<div className={`flex items-center gap-3 ${className}`}>
|
||||
<div className="relative w-14 h-14 flex-shrink-0">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="FitAI Logo"
|
||||
width={56}
|
||||
height={56}
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="text-lg font-black text-slate-900 dark:text-white">
|
||||
NextForm
|
||||
</span>
|
||||
<span className="text-xs font-bold text-slate-500 dark:text-slate-400">Smart Fitness Twin</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Full variant (default)
|
||||
return (
|
||||
<div className={`flex items-center gap-6 ${className}`}>
|
||||
<div className="relative w-28 h-28 flex-shrink-0">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="FitAI Logo"
|
||||
width={112}
|
||||
height={112}
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-4xl font-black text-slate-900 dark:text-white">
|
||||
NextForm
|
||||
</span>
|
||||
<span className="text-base font-bold text-slate-600 dark:text-slate-400">Your Smart Fitness Twin</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -13,6 +13,7 @@ import {
|
||||
Brain,
|
||||
} from "lucide-react";
|
||||
import { UserButton, useUser } from "@clerk/nextjs";
|
||||
import { Logo } from "./Logo";
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
@ -66,10 +67,10 @@ export function Sidebar() {
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800">
|
||||
<div className="p-6 border-b border-slate-800">
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
||||
FitAI Admin
|
||||
</h1>
|
||||
<div className="p-6 border-b border-slate-800 flex items-center justify-center">
|
||||
<Link href="/" className="hover:opacity-80 transition-opacity">
|
||||
<Logo variant="text" className="justify-start" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
|
||||
@ -19,19 +19,19 @@ export function StatsCard({ title, value, change, trend, icon: Icon, color = "bl
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className="overflow-hidden h-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground line-clamp-2 break-words flex-1 pr-2">
|
||||
{title}
|
||||
</CardTitle>
|
||||
<div className={`p-2 rounded-lg ${colorStyles[color]}`}>
|
||||
<div className={`p-2 rounded-lg ${colorStyles[color]} flex-shrink-0`}>
|
||||
<Icon size={16} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
<CardContent className="overflow-hidden">
|
||||
<div className="text-2xl font-bold line-clamp-2 break-words">{value}</div>
|
||||
{change && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1 break-words">
|
||||
<span
|
||||
className={`font-medium ${trend === "up"
|
||||
? "text-green-600"
|
||||
|
||||
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
"rounded-xl border bg-card text-card-foreground shadow overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -265,42 +265,42 @@ export function UserGrid({
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="w-full overflow-hidden">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search users..."
|
||||
className="border border-gray-300 rounded px-4 py-2"
|
||||
className="border border-gray-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white rounded px-4 py-2 flex-1 sm:flex-none w-full sm:w-auto"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
className="bg-green-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50 text-sm"
|
||||
onClick={handleEdit}
|
||||
disabled={selectedUsers.length !== 1}
|
||||
>
|
||||
Edit User
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded disabled:opacity-50 text-sm"
|
||||
onClick={handleDelete}
|
||||
disabled={selectedUsers.length !== 1}
|
||||
>
|
||||
Delete User
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
className="bg-yellow-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded disabled:opacity-50 text-sm"
|
||||
onClick={handleBulkDelete}
|
||||
disabled={selectedUsers.length === 0}
|
||||
>
|
||||
Bulk Delete
|
||||
Bulk
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="ag-theme-alpine"
|
||||
style={{ height: "600px", width: "100%" }}
|
||||
className="ag-theme-alpine dark:ag-theme-alpine-dark w-full overflow-hidden rounded-lg"
|
||||
style={{ height: "400px", width: "100%" }}
|
||||
>
|
||||
<AgGridReact<User> {...gridOptions} ref={gridRef} />
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { UserGrid } from "@/components/users/UserGrid";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||
|
||||
interface User {
|
||||
|
||||
183
docs/README.md
183
docs/README.md
@ -1,174 +1,15 @@
|
||||
# FitAI
|
||||
## fitai
|
||||
|
||||
Integrated AI solution for fitness houses and their clients with Clerk authentication.
|
||||
# description
|
||||
|
||||
## Project Structure
|
||||
- fitai is integrated ai solution for fitness houses and their clients,
|
||||
its allow to easy menagment of clients, tracking of payments, usage of resourcess,
|
||||
attendance, habits etc.
|
||||
these will be phase one:
|
||||
solution is composed of a admin app, where we are doing managment tasks, we visualize and
|
||||
expose importatnt data to menagment and trainers, and a expo/reactnative mobile app for users.
|
||||
via app we will be tracking attendance and payments, we will be sending notification etc.
|
||||
|
||||
```
|
||||
fitai/
|
||||
├── apps/
|
||||
│ ├── admin/ # Next.js admin dashboard
|
||||
│ └── mobile/ # React Native mobile app (Expo)
|
||||
├── packages/
|
||||
│ └── shared/ # Shared types and utilities
|
||||
└── AGENTS.md # Development guidelines
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Node.js >= 18.0.0
|
||||
- npm >= 9.0.0
|
||||
- Clerk account (sign up at https://clerk.com)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Install root dependencies
|
||||
npm install
|
||||
|
||||
# Install admin dependencies
|
||||
cd apps/admin && npm install
|
||||
|
||||
# Install mobile dependencies
|
||||
cd apps/mobile && npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
### Authentication Setup
|
||||
|
||||
FitAI uses Clerk for authentication. Follow these steps:
|
||||
|
||||
1. **Create a Clerk account** at https://dashboard.clerk.com
|
||||
2. **Create a new application** in the Clerk dashboard
|
||||
3. **Copy your API keys** (Publishable Key and Secret Key)
|
||||
4. **Configure environment variables**:
|
||||
|
||||
**Admin App** (`apps/admin/.env.local`):
|
||||
```env
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
|
||||
CLERK_SECRET_KEY=sk_test_your_key_here
|
||||
```
|
||||
|
||||
**Mobile App** (`apps/mobile/.env`):
|
||||
```env
|
||||
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
|
||||
```
|
||||
|
||||
📖 **See [CLERK_SETUP.md](./CLERK_SETUP.md) for detailed setup instructions**
|
||||
|
||||
### Development
|
||||
|
||||
**Important**: Set up environment variables before running the apps!
|
||||
|
||||
```bash
|
||||
# Admin dashboard (http://localhost:3000)
|
||||
cd apps/admin && npm run dev
|
||||
|
||||
# Mobile app (http://localhost:8081) - Requires Expo SDK 54
|
||||
cd apps/mobile && npm start
|
||||
```
|
||||
|
||||
**First-time setup checklist**:
|
||||
- [ ] Create Clerk account and application
|
||||
- [ ] Add API keys to `.env.local` (admin) and `.env` (mobile)
|
||||
- [ ] Verify both apps start without errors
|
||||
- [ ] Test sign-up and sign-in flows
|
||||
|
||||
### Mobile App Setup
|
||||
- **Expo SDK**: 50 (stable, compatible with Expo Go)
|
||||
- **Assets**: Placeholder icons and splash screen included
|
||||
- **Navigation**: Expo Router with tab-based layout
|
||||
- **Authentication**: Secure storage with expo-secure-store
|
||||
- **Babel**: babel-preset-expo for proper transpilation
|
||||
|
||||
### Known Compatibility Notes
|
||||
- Use Expo Go with SDK 50 for mobile testing
|
||||
- For SDK 54, upgrade all dependencies to latest versions
|
||||
- Current setup prioritizes stability over latest features
|
||||
|
||||
### Build & Test
|
||||
```bash
|
||||
# Build all apps
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
|
||||
# Type checking
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Authentication (Clerk)
|
||||
- 🔐 Secure email/password authentication
|
||||
- ✉️ Email verification
|
||||
- 🔄 Session management
|
||||
- 🎨 Customizable UI components
|
||||
- 📱 Multi-platform support (Web + Mobile)
|
||||
- 🛡️ Built-in security features
|
||||
|
||||
### Admin Dashboard
|
||||
- 👥 User management (CRUD operations)
|
||||
- 📊 Analytics dashboard with charts
|
||||
- 🎯 Role-based access control
|
||||
- 📈 Data visualization with AG Grid
|
||||
- 💳 Payment tracking (coming soon)
|
||||
- 📅 Attendance monitoring (coming soon)
|
||||
|
||||
### Mobile App
|
||||
- 🔐 Secure sign-in/sign-up
|
||||
- 👤 User profile management
|
||||
- 📱 Native mobile experience
|
||||
- 🔔 Push notifications ready
|
||||
- ✅ Attendance check-in (coming soon)
|
||||
- 💰 Payment history (coming soon)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
### Authentication
|
||||
- **Clerk**: Complete authentication and user management platform
|
||||
|
||||
### Frontend
|
||||
- **Admin**: Next.js 14 (App Router), React 19, TypeScript, Tailwind CSS
|
||||
- **Mobile**: React Native, Expo SDK 54, Expo Router, TypeScript
|
||||
|
||||
### Backend & Database
|
||||
- **Database**: SQLite with Drizzle ORM
|
||||
- **API**: Next.js API Routes (REST)
|
||||
|
||||
### Development Tools
|
||||
- **State Management**: React Query, React Hook Form
|
||||
- **Validation**: Zod schemas
|
||||
- **Data Grid**: AG Grid for advanced user management
|
||||
- **Charts**: AG Charts for analytics and visualization
|
||||
- **Testing**: Jest, Testing Library (configured)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
fitai/
|
||||
├── apps/
|
||||
│ ├── admin/ # Next.js admin dashboard
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── app/ # App Router pages & API routes
|
||||
│ │ │ ├── components/
|
||||
│ │ │ └── lib/ # Database & utilities
|
||||
│ │ └── .env.local # Admin environment variables
|
||||
│ │
|
||||
│ └── mobile/ # Expo React Native app
|
||||
│ ├── src/
|
||||
│ │ ├── app/ # Expo Router screens
|
||||
│ │ │ ├── (auth)/ # Authentication screens
|
||||
│ │ │ └── (tabs)/ # Main app tabs
|
||||
│ │ └── components/
|
||||
│ └── .env # Mobile environment variables
|
||||
│
|
||||
├── packages/
|
||||
│ ├── database/ # Drizzle ORM schemas & DB client
|
||||
│ └── shared/ # Shared types & utilities
|
||||
│
|
||||
└── CLERK_SETUP.md # Detailed authentication setup guide
|
||||
```
|
||||
# phase 2
|
||||
we will be tracking user inputs via manual input and devices, backend will analyze data and propose
|
||||
excercises etc.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user