+
+
+
+
-
- {user?.fullName || "Admin User"}
+
+ {user?.fullName || "Admin"}
- {user?.primaryEmailAddress?.emailAddress}
+ {user?.emailAddresses[0]?.emailAddress || "admin@fitai.com"}
diff --git a/apps/admin/src/components/ui/StatsCard.tsx b/apps/admin/src/components/ui/StatsCard.tsx
index 76cb699..19239ba 100644
--- a/apps/admin/src/components/ui/StatsCard.tsx
+++ b/apps/admin/src/components/ui/StatsCard.tsx
@@ -12,38 +12,61 @@ interface StatsCardProps {
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
const colorStyles = {
- blue: "bg-blue-50 text-blue-600",
- green: "bg-green-50 text-green-600",
- purple: "bg-purple-50 text-purple-600",
- orange: "bg-orange-50 text-orange-600",
+ blue: {
+ bg: "from-blue-700 via-cyan-500 to-blue-600",
+ text: "text-white",
+ light: "from-blue-50 to-cyan-50",
+ badge: "bg-blue-200/60 text-blue-900 backdrop-blur-sm",
+ icon: "bg-white/20 text-white backdrop-blur-sm",
+ },
+ green: {
+ bg: "from-pink-700 via-fuchsia-500 to-pink-600",
+ text: "text-white",
+ light: "from-pink-50 to-rose-50",
+ badge: "bg-pink-200/60 text-pink-900 backdrop-blur-sm",
+ icon: "bg-white/20 text-white backdrop-blur-sm",
+ },
+ purple: {
+ bg: "from-amber-700 via-yellow-500 to-amber-600",
+ text: "text-white",
+ light: "from-amber-50 to-yellow-50",
+ badge: "bg-amber-200/60 text-amber-900 backdrop-blur-sm",
+ icon: "bg-white/20 text-white backdrop-blur-sm",
+ },
+ orange: {
+ bg: "from-emerald-700 via-teal-500 to-emerald-600",
+ text: "text-white",
+ light: "from-emerald-50 to-teal-50",
+ badge: "bg-emerald-200/60 text-emerald-900 backdrop-blur-sm",
+ icon: "bg-white/20 text-white backdrop-blur-sm",
+ },
};
+ const styles = colorStyles[color];
+
return (
-
-
-
+
+
+
{title}
-
-
+
+
-
- {value}
+
+
+ {value}
+
{change && (
-
+
- {change}
- {" "}
- vs last month
-
+ {trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
+
+ vs month
+
)}
diff --git a/apps/admin/src/components/ui/button.ts b/apps/admin/src/components/ui/button.ts
new file mode 100644
index 0000000..a21b371
--- /dev/null
+++ b/apps/admin/src/components/ui/button.ts
@@ -0,0 +1,2 @@
+// Re-export Button component with lowercase filename for compatibility
+export { Button } from './Button';
diff --git a/apps/admin/src/components/ui/button.tsx b/apps/admin/src/components/ui/button.tsx
index d6c7bcb..340f409 100644
--- a/apps/admin/src/components/ui/button.tsx
+++ b/apps/admin/src/components/ui/button.tsx
@@ -1,56 +1,23 @@
-import * as React from "react";
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
+import React from 'react'
-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,
- VariantProps {
- asChild?: boolean;
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary'
+ children: React.ReactNode
}
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button";
- return (
-
- );
- },
-);
-Button.displayName = "Button";
+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',
+ }
-export { Button, buttonVariants };
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/apps/admin/src/components/ui/card.ts b/apps/admin/src/components/ui/card.ts
new file mode 100644
index 0000000..145f611
--- /dev/null
+++ b/apps/admin/src/components/ui/card.ts
@@ -0,0 +1,2 @@
+// Re-export Card components with lowercase filename for compatibility
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
diff --git a/apps/admin/src/components/ui/card.tsx b/apps/admin/src/components/ui/card.tsx
index 59a6010..751c8f2 100644
--- a/apps/admin/src/components/ui/card.tsx
+++ b/apps/admin/src/components/ui/card.tsx
@@ -1,76 +1,78 @@
-import * as React from "react"
+import React from 'react'
-import { cn } from "@/lib/utils"
+interface CardProps {
+ children: React.ReactNode
+ className?: string
+}
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-Card.displayName = "Card"
+export function Card({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardHeader.displayName = "CardHeader"
+export function CardHeader({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
-const CardTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardTitle.displayName = "CardTitle"
+export function CardContent({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
-const CardDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardDescription.displayName = "CardDescription"
+export function CardTitle({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardContent.displayName = "CardContent"
+export function CardDescription({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardFooter.displayName = "CardFooter"
+export function CardFooter({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+export function CardTitle({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function CardDescription({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function CardFooter({ children, className = '' }: CardProps) {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/apps/admin/src/components/users/Recommendations.tsx b/apps/admin/src/components/users/Recommendations.tsx
index 13a6828..a2e4f8d 100644
--- a/apps/admin/src/components/users/Recommendations.tsx
+++ b/apps/admin/src/components/users/Recommendations.tsx
@@ -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 {
diff --git a/apps/admin/src/components/users/UserGrid.tsx b/apps/admin/src/components/users/UserGrid.tsx
index 9533350..37ab231 100644
--- a/apps/admin/src/components/users/UserGrid.tsx
+++ b/apps/admin/src/components/users/UserGrid.tsx
@@ -265,45 +265,72 @@ export function UserGrid({
};
return (
-
-
-
setSearchQuery(e.target.value)}
- />
-
+
+ {/* Search and Actions Bar */}
+
+
+
+
+
setSearchQuery(e.target.value)}
+ />
+
+
+
-
-
{...gridOptions} ref={gridRef} />
+
+ {/* Table */}
+
+
+
{...gridOptions} ref={gridRef} />
+
+
+ {/* Selected Count */}
+ {selectedUsers.length > 0 && (
+
+
+ {selectedUsers.length} athlete{selectedUsers.length !== 1 ? 's' : ''} selected
+
+
+
+ )}
);
}
diff --git a/apps/admin/src/components/users/UserManagement.tsx b/apps/admin/src/components/users/UserManagement.tsx
index 15b7066..1878123 100644
--- a/apps/admin/src/components/users/UserManagement.tsx
+++ b/apps/admin/src/components/users/UserManagement.tsx
@@ -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 {
@@ -205,92 +205,106 @@ export function UserManagement() {
};
return (
-
-
-
User Management
-
-
-
-
-
-
-
-
-
-
+
+ {/* Header Section */}
+
+
User Management
+
Manage and monitor your fitness clients
-
-
- Showing {users.length} users
- {selectedUser && (
-
- Selected: {selectedUser.firstName} {selectedUser.lastName}
-
- )}
-
-
-
-
-
+ {/* Filter Buttons */}
+
+
+
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+
+
+ {/* Stats */}
+
+ Showing {users.length} users
+ {selectedUser && (
+
+ Selected: {selectedUser.firstName} {selectedUser.lastName}
+
+ )}
+
+
+ {/* User Grid */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/mobile/src/app/(tabs)/attendance.tsx b/apps/mobile/src/app/(tabs)/attendance.tsx
index 8ff1ebe..d069810 100644
--- a/apps/mobile/src/app/(tabs)/attendance.tsx
+++ b/apps/mobile/src/app/(tabs)/attendance.tsx
@@ -6,6 +6,7 @@ import { Ionicons } from '@expo/vector-icons'
import { attendanceApi, Attendance } from '../../api/attendance'
import { theme } from '../../styles/theme'
import { Animated } from 'react-native'
+import { GeofenceStatus } from '../../components/GeofenceStatus'
export default function AttendanceScreen() {
const { getToken, userId } = useAuth()
@@ -111,6 +112,9 @@ export default function AttendanceScreen() {
Track your gym visits
+ {/* Geofencing Status Card */}
+
+
{activeCheckIn ? (
{/* Header Section */}
-
- {getGreeting()},
- {user?.firstName || "Athlete"}
+
+
+
+ {getGreeting()},
+ {user?.firstName || "Athlete"}
+
{user?.imageUrl ? (
@@ -131,6 +138,9 @@ export default function HomeScreen() {
duration={45}
/>
+ {/* Performance Metrics */}
+
+
{/* Hydration Widget */}
{
+ const [isInside, setIsInside] = useState(false)
+ const [distance, setDistance] = useState('250 m')
+ const [loading, setLoading] = useState(false)
+
+ const handleRefresh = () => {
+ setLoading(true)
+ // Simulate location check
+ setTimeout(() => {
+ setDistance(Math.random() > 0.5 ? '150 m' : '300 m')
+ setIsInside(Math.random() > 0.5)
+ setLoading(false)
+ }, 1000)
+ }
+
+ return (
+
+
+
+
+
+
+ {isInside ? 'At Gym' : 'Away from Gym'}
+
+
+
+
+
+
+
+
+
+ Distance
+ {distance}
+
+
+
+
+ )
+}
+
+
+const styles = StyleSheet.create({
+ container: {
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ card: {
+ borderRadius: 16,
+ padding: 16,
+ overflow: 'hidden',
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ titleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#fff',
+ },
+ refreshButton: {
+ padding: 8,
+ borderRadius: 8,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ },
+ content: {
+ marginTop: 12,
+ },
+ stat: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ statLabel: {
+ fontSize: 14,
+ color: 'rgba(255, 255, 255, 0.8)',
+ fontWeight: '500',
+ },
+ statValue: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#fff',
+ },
+})
diff --git a/apps/mobile/src/components/HydrationWidget.tsx b/apps/mobile/src/components/HydrationWidget.tsx
index bc91f9c..48c4b40 100644
--- a/apps/mobile/src/components/HydrationWidget.tsx
+++ b/apps/mobile/src/components/HydrationWidget.tsx
@@ -15,7 +15,7 @@ export function HydrationWidget({ current, goal }: HydrationWidgetProps) {
return (
diff --git a/apps/mobile/src/components/PerformanceMetrics.tsx b/apps/mobile/src/components/PerformanceMetrics.tsx
new file mode 100644
index 0000000..0d29a59
--- /dev/null
+++ b/apps/mobile/src/components/PerformanceMetrics.tsx
@@ -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: ['#1e40af', '#0369a1', '#06b6d4'] as const,
+ text: '#ffffff',
+ label: '#e0f2fe',
+ badge: '#0ea5e9',
+ badgeText: '#ffffff',
+ icon: '#06b6d4',
+ },
+ green: {
+ colors: ['#be185d', '#ec4899', '#f472b6'] as const,
+ text: '#ffffff',
+ label: '#fbcfe8',
+ badge: '#ec4899',
+ badgeText: '#ffffff',
+ icon: '#f472b6',
+ },
+ purple: {
+ colors: ['#b45309', '#d97706', '#fbbf24'] as const,
+ text: '#ffffff',
+ label: '#fef3c7',
+ badge: '#f59e0b',
+ badgeText: '#ffffff',
+ icon: '#fbbf24',
+ },
+ orange: {
+ colors: ['#047857', '#059669', '#10b981'] as const,
+ text: '#ffffff',
+ label: '#d1fae5',
+ badge: '#10b981',
+ badgeText: '#ffffff',
+ icon: '#34d399',
+ },
+ };
+ return schemes[colorScheme];
+};
+
+const MetricCard: React.FC = ({ title, value, change, trend, icon, colorScheme }) => {
+ const colors = getColorScheme(colorScheme);
+ const isPositive = trend === 'up';
+ const trendIcon = isPositive ? 'arrow-up' : 'arrow-down';
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+ {value}
+
+
+
+
+
+ {trend === 'up' ? '↑' : '↓'} {change}
+
+
+ vs month
+
+
+
+ );
+};
+
+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 (
+
+
+ Performance metrics & athlete insights
+
+
+
+ {metrics.map((metric, index) => (
+
+
+
+ ))}
+
+
+ );
+};
+
+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',
+ },
+});
+
diff --git a/apps/mobile/src/components/QRCode.tsx b/apps/mobile/src/components/QRCode.tsx
new file mode 100644
index 0000000..098ded5
--- /dev/null
+++ b/apps/mobile/src/components/QRCode.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import QRCode from 'react-native-qrcode-svg';
+
+interface QRCodeProps {
+ value: string;
+ size?: number;
+ logo?: string;
+}
+
+export const QRCodeGenerator: React.FC = ({
+ value,
+ size = 250
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 16,
+ },
+ qrWrapper: {
+ padding: 16,
+ backgroundColor: 'white',
+ borderRadius: 12,
+ elevation: 5,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.25,
+ shadowRadius: 3.84,
+ },
+});
diff --git a/apps/mobile/src/components/QRScanner.tsx b/apps/mobile/src/components/QRScanner.tsx
new file mode 100644
index 0000000..d9fa9f0
--- /dev/null
+++ b/apps/mobile/src/components/QRScanner.tsx
@@ -0,0 +1,92 @@
+import React, { useState, useEffect } from 'react';
+import { View, Text, StyleSheet, Button, Alert } from 'react-native';
+import { BarCodeScanner } from 'expo-barcode-scanner';
+
+interface QRScannerProps {
+ onScan: (data: string) => void;
+ onClose?: () => void;
+}
+
+export const QRScanner: React.FC = ({ onScan, onClose }) => {
+ const [hasPermission, setHasPermission] = useState(null);
+ const [scanned, setScanned] = useState(false);
+
+ useEffect(() => {
+ const getBarCodeScannerPermissions = async () => {
+ const { status } = await BarCodeScanner.requestPermissionsAsync();
+ setHasPermission(status === 'granted');
+ };
+
+ getBarCodeScannerPermissions();
+ }, []);
+
+ const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
+ setScanned(true);
+ onScan(data);
+ Alert.alert('QR Code', `Scanned: ${data}`, [
+ { text: 'OK', onPress: () => setScanned(false) },
+ ]);
+ };
+
+ if (hasPermission === null) {
+ return (
+
+ Requesting for camera permission...
+
+ );
+ }
+
+ if (hasPermission === false) {
+ return (
+
+ No access to camera
+ {onClose && }
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {onClose && (
+
+
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#000',
+ },
+ overlay: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ scanBox: {
+ width: 250,
+ height: 250,
+ borderWidth: 2,
+ borderColor: '#4CAF50',
+ borderRadius: 10,
+ backgroundColor: 'transparent',
+ },
+ buttonContainer: {
+ position: 'absolute',
+ bottom: 20,
+ left: 20,
+ right: 20,
+ },
+});
diff --git a/apps/mobile/src/components/ScanFoodModal.tsx b/apps/mobile/src/components/ScanFoodModal.tsx
index dfaa5c0..ff060db 100644
--- a/apps/mobile/src/components/ScanFoodModal.tsx
+++ b/apps/mobile/src/components/ScanFoodModal.tsx
@@ -1,5 +1,5 @@
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 { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
@@ -20,7 +20,7 @@ const FOOD_DATABASE: { [key: string]: { name: string; calories: number; servingS
};
export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProps) {
- const [permission, requestPermission] = useCameraPermissions();
+ const [permission, requestPermission] = Platform.OS === 'web' ? [null, null] : useCameraPermissions() as any;
const [scanned, setScanned] = useState(false);
const [foodData, setFoodData] = useState<{ name: string; calories: number; servingSize: string } | null>(null);
const [servings, setServings] = useState('1');
@@ -68,10 +68,29 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
};
if (!permission) {
+ if (Platform.OS === 'web') {
+ // On web, show normal modal without permissions
+ return (
+
+
+
+
+
+
+ Scan Food Barcode
+
+
+
+ Barcode scanning is only available on mobile devices
+
+
+
+ );
+ }
return null;
}
- if (!permission.granted) {
+ if (!permission.granted && Platform.OS !== 'web') {
return (
@@ -111,19 +130,25 @@ export function ScanFoodModal({ visible, onClose, onAddFood }: ScanFoodModalProp
-
-
-
- Position barcode within frame
+ {Platform.OS === 'web' ? (
+
+ Barcode scanning is only available on mobile devices
-
+ ) : (
+
+
+
+ Position barcode within frame
+
+
+ )}
>
) : (
@@ -230,6 +255,18 @@ const styles = StyleSheet.create({
camera: {
flex: 1,
},
+ webPlaceholder: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#1a1a1a',
+ },
+ webText: {
+ color: '#999',
+ fontSize: 16,
+ textAlign: 'center',
+ paddingHorizontal: 40,
+ },
scanOverlay: {
flex: 1,
justifyContent: 'center',
diff --git a/apps/mobile/src/hooks/useWarmUpBrowser.ts b/apps/mobile/src/hooks/useWarmUpBrowser.ts
index 9dd2212..a86bdcf 100644
--- a/apps/mobile/src/hooks/useWarmUpBrowser.ts
+++ b/apps/mobile/src/hooks/useWarmUpBrowser.ts
@@ -1,13 +1,17 @@
import React from "react";
import * as WebBrowser from "expo-web-browser";
+import { Platform } from "react-native";
export const useWarmUpBrowser = () => {
React.useEffect(() => {
// Warm up the android browser to improve UX
// https://docs.expo.dev/guides/authentication/#improving-user-experience
- void WebBrowser.warmUpAsync();
- return () => {
- void WebBrowser.coolDownAsync();
- };
+ // Only available on native platforms (iOS/Android), not on web
+ if (Platform.OS !== "web") {
+ void WebBrowser.warmUpAsync();
+ return () => {
+ void WebBrowser.coolDownAsync();
+ };
+ }
}, []);
};
diff --git a/apps/mobile/src/services/geofencing.ts b/apps/mobile/src/services/geofencing.ts
new file mode 100644
index 0000000..c58bb8f
--- /dev/null
+++ b/apps/mobile/src/services/geofencing.ts
@@ -0,0 +1,74 @@
+/**
+ * Geofencing Service
+ * Handles gym location validation and distance calculations
+ */
+
+// Gym location coordinates - Teretana Isaija Mazhovski, Skopje, North Macedonia
+export const GYM_LOCATION = {
+ latitude: 41.9973,
+ longitude: 21.4280,
+};
+
+// Geofence radius in meters (500m default)
+export const GEOFENCE_RADIUS_METERS = 500;
+
+/**
+ * Calculate distance between two coordinates using Haversine formula
+ * @param lat1 Starting latitude
+ * @param lon1 Starting longitude
+ * @param lat2 Ending latitude
+ * @param lon2 Ending longitude
+ * @returns Distance in meters
+ */
+export function calculateDistance(
+ lat1: number,
+ lon1: number,
+ lat2: number,
+ lon2: number
+): number {
+ const R = 6371000; // Earth's radius in meters
+ const φ1 = (lat1 * Math.PI) / 180;
+ const φ2 = (lat2 * Math.PI) / 180;
+ const Δφ = ((lat2 - lat1) * Math.PI) / 180;
+ const Δλ = ((lon2 - lon1) * Math.PI) / 180;
+
+ const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
+ Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
+
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return R * c; // Distance in meters
+}
+
+/**
+ * Check if a location is within the geofence
+ * @param userLat User's latitude
+ * @param userLon User's longitude
+ * @param radius Geofence radius in meters (defaults to GEOFENCE_RADIUS_METERS)
+ * @returns Boolean indicating if user is within geofence
+ */
+export function isWithinGeofence(
+ userLat: number,
+ userLon: number,
+ radius: number = GEOFENCE_RADIUS_METERS
+): boolean {
+ const distance = calculateDistance(
+ userLat,
+ userLon,
+ GYM_LOCATION.latitude,
+ GYM_LOCATION.longitude
+ );
+ return distance <= radius;
+}
+
+/**
+ * Get formatted distance string
+ * @param meters Distance in meters
+ * @returns Formatted distance string
+ */
+export function getFormattedDistance(meters: number): string {
+ if (meters < 1000) {
+ return `${Math.round(meters)} m`;
+ }
+ return `${(meters / 1000).toFixed(2)} km`;
+}
diff --git a/apps/mobile/src/styles/theme.ts b/apps/mobile/src/styles/theme.ts
index 4a10c78..b9bad33 100644
--- a/apps/mobile/src/styles/theme.ts
+++ b/apps/mobile/src/styles/theme.ts
@@ -64,6 +64,11 @@ export const theme = {
forest: ['#10b981', '#059669'] as const,
lavender: ['#a78bfa', '#ec4899'] as const,
dark: ['#1e293b', '#0f172a'] as const,
+ // Premium metallic gradients
+ blueMetallic: ['#1e40af', '#06b6d4', '#10b981'] as const,
+ amberMetallic: ['#b45309', '#f59e0b', '#fbbf24'] as const,
+ magentaMetallic: ['#be185d', '#ec4899', '#f472b6'] as const,
+ emeraldMetallic: ['#047857', '#10b981', '#6ee7b7'] as const,
},
// Shadow System
diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json
index a0c1326..6a0a348 100644
--- a/apps/mobile/tsconfig.json
+++ b/apps/mobile/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "ignoreDeprecations": "6.0",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
diff --git a/docs/readme.md b/docs/readme.md
index 84a6de1..e55b3e3 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -1,15 +1,174 @@
-## fitai
+# FitAI
-# description
+Integrated AI solution for fitness houses and their clients with Clerk authentication.
- - fitai is integrated ai solution for fitness houses and their clients,
- 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.
+## Project Structure
-# phase 2
-we will be tracking user inputs via manual input and devices, backend will analyze data and propose
-excercises 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
+```
\ No newline at end of file
diff --git a/update-logo.ps1 b/update-logo.ps1
new file mode 100644
index 0000000..1bc9404
--- /dev/null
+++ b/update-logo.ps1
@@ -0,0 +1,19 @@
+# Script to update NextForm logo from external folder
+$sourceFolder = "c:\Users\PC\Desktop\NextForm – Your Smart Fitness Twin\sliki"
+$adminPublic = "c:\Users\PC\Desktop\fitaiProto\apps\admin\public\nextform-logo.png"
+$mobilePublic = "c:\Users\PC\Desktop\fitaiProto\apps\mobile\public\nextform-logo.png"
+
+# Find the latest PNG in source folder
+$latestPng = Get-ChildItem "$sourceFolder\*.png" -ErrorAction SilentlyContinue |
+ Sort-Object LastWriteTime -Descending |
+ Select-Object -First 1
+
+if ($latestPng) {
+ Copy-Item $latestPng.FullName $adminPublic -Force
+ Copy-Item $latestPng.FullName $mobilePublic -Force
+ Write-Host "✅ Logo updated from: $($latestPng.Name)"
+ Write-Host "Admin: $adminPublic"
+ Write-Host "Mobile: $mobilePublic"
+} else {
+ Write-Host "❌ No PNG found in: $sourceFolder"
+}