Compare commits
14
Commits
8c2a3daee0
...
ace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb7211f230 | ||
|
|
94beb7da78 | ||
|
|
1f7b90b816 | ||
|
|
04856021b9 | ||
|
|
c238e18766 | ||
|
|
0c272502f8 | ||
|
|
231ad44cc4 | ||
|
|
2f79d1547f | ||
|
|
337351dd06 | ||
|
|
fe3f93fb9f | ||
|
|
3c810003c5 | ||
|
|
6207949e2d | ||
|
|
76b9ee24c5 | ||
|
|
ad6fdc2226 |
No files matched your search
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2FB7E8;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#0B9FD0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0B7FB3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Main arrow shape -->
|
||||
<path d="M 420 150 L 540 150 L 420 300 Z" fill="url(#grad1)" opacity="0.95"/>
|
||||
<path d="M 180 150 L 300 300 L 180 300 Z" fill="url(#grad1)" opacity="0.85"/>
|
||||
|
||||
<!-- Dumbbell - Left -->
|
||||
<g transform="translate(200, 250)">
|
||||
<!-- Left disk -->
|
||||
<circle cx="0" cy="0" r="35" fill="white" opacity="0.95"/>
|
||||
<!-- Bar left -->
|
||||
<rect x="35" y="-15" width="80" height="30" fill="white" rx="4"/>
|
||||
<!-- Right disk -->
|
||||
<circle cx="115" cy="0" r="35" fill="white" opacity="0.95"/>
|
||||
<!-- Grip lines -->
|
||||
<line x1="45" y1="-35" x2="45" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="65" y1="-35" x2="65" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="85" y1="-35" x2="85" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="105" y1="-35" x2="105" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2FB7E8;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#0B9FD0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0B7FB3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Main arrow shape -->
|
||||
<path d="M 420 150 L 540 150 L 420 300 Z" fill="url(#grad1)" opacity="0.95"/>
|
||||
<path d="M 180 150 L 300 300 L 180 300 Z" fill="url(#grad1)" opacity="0.85"/>
|
||||
|
||||
<!-- Dumbbell - Left -->
|
||||
<g transform="translate(200, 250)">
|
||||
<!-- Left disk -->
|
||||
<circle cx="0" cy="0" r="35" fill="white" opacity="0.95"/>
|
||||
<!-- Bar left -->
|
||||
<rect x="35" y="-15" width="80" height="30" fill="white" rx="4"/>
|
||||
<!-- Right disk -->
|
||||
<circle cx="115" cy="0" r="35" fill="white" opacity="0.95"/>
|
||||
<!-- Grip lines -->
|
||||
<line x1="45" y1="-35" x2="45" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="65" y1="-35" x2="65" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="85" y1="-35" x2="85" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="105" y1="-35" x2="105" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
PNG - NextForm Logo
|
||||
@@ -51,11 +51,18 @@ export default function Home() {
|
||||
{/* Hero Section */}
|
||||
<div className="pt-8 pb-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-12 w-1 bg-gradient-to-b from-blue-600 to-cyan-600 rounded-full"></div>
|
||||
<h1 className="text-5xl font-black bg-gradient-to-r from-blue-600 via-blue-700 to-cyan-600 bg-clip-text text-transparent">
|
||||
FitAI Dashboard
|
||||
</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src="/nextform-logo.png"
|
||||
alt="NextForm"
|
||||
className="h-20 w-20 object-contain"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-12 w-1 bg-gradient-to-b from-blue-600 to-cyan-600 rounded-full"></div>
|
||||
<h1 className="text-5xl font-black bg-gradient-to-r from-blue-600 via-blue-700 to-cyan-600 bg-clip-text text-transparent">
|
||||
NextForm
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-lg text-gray-600 ml-4">Performance metrics & athlete insights</p>
|
||||
</div>
|
||||
@@ -100,7 +107,7 @@ export default function Home() {
|
||||
{/* 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="bg-gradient-to-br from-slate-50 via-blue-50/30 to-slate-50 rounded-2xl shadow-lg border border-slate-200/50 p-7 hover:shadow-2xl transition-all duration-300 backdrop-blur-sm">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900">Active Athletes</h2>
|
||||
|
||||
@@ -52,9 +52,16 @@ export function Navigation(): ReactElement {
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl font-bold text-[#FF0000] hover:text-[#00FF00] transition-colors"
|
||||
className="flex items-center gap-3 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
FitAI
|
||||
<img
|
||||
src="/nextform-logo.png"
|
||||
alt="NextForm"
|
||||
className="h-10 w-10 object-contain"
|
||||
/>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent">
|
||||
NextForm
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation Items */}
|
||||
|
||||
@@ -79,7 +79,7 @@ export function AnalyticsDashboard() {
|
||||
<div className="space-y-4">
|
||||
{/* Key Metrics Cards - 3 columns */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="bg-gradient-to-br from-blue-600 via-cyan-500 to-teal-500 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
|
||||
<div className="bg-gradient-to-br from-blue-700 via-cyan-500 to-blue-600 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
|
||||
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Athletes</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-2xl font-black text-white">{totalUsers}</div>
|
||||
@@ -87,7 +87,7 @@ export function AnalyticsDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="bg-gradient-to-br from-amber-700 via-yellow-500 to-amber-600 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
|
||||
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Total Revenue</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-2xl font-black text-white">${totalRevenue.toLocaleString()}</div>
|
||||
@@ -95,7 +95,7 @@ export function AnalyticsDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="bg-gradient-to-br from-pink-700 via-fuchsia-500 to-pink-600 rounded-xl p-5 border-0 shadow-lg hover:shadow-2xl transition-all text-white">
|
||||
<p className="text-xs uppercase tracking-wide font-bold text-white mb-2">Active Members</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-2xl font-black text-white">{activeMembers}</div>
|
||||
@@ -106,7 +106,7 @@ export function AnalyticsDashboard() {
|
||||
|
||||
{/* Charts - 3 Columns Horizontal */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<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">
|
||||
<div className="bg-gradient-to-br from-blue-700 via-cyan-500 to-blue-600 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-bold text-white">User Growth Trend</h3>
|
||||
<p className="text-xs text-blue-100">Last 6 months performance</p>
|
||||
@@ -116,20 +116,20 @@ export function AnalyticsDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="bg-gradient-to-br from-amber-700 via-yellow-500 to-amber-600 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-bold text-white">Membership Mix</h3>
|
||||
<p className="text-xs text-emerald-100">Distribution breakdown</p>
|
||||
<p className="text-xs text-amber-100">Distribution breakdown</p>
|
||||
</div>
|
||||
<div className="h-48 overflow-auto">
|
||||
<MembershipDistributionChart data={membershipData} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="bg-gradient-to-br from-pink-700 via-fuchsia-500 to-pink-600 rounded-xl shadow-lg border-0 p-5 hover:shadow-2xl transition-all">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-bold text-white">Revenue Stream</h3>
|
||||
<p className="text-xs text-purple-100">Monthly earnings</p>
|
||||
<p className="text-xs text-pink-100">Monthly earnings</p>
|
||||
</div>
|
||||
<div className="h-48 overflow-auto">
|
||||
<RevenueChart data={revenueData} />
|
||||
|
||||
@@ -68,15 +68,16 @@ export function Sidebar() {
|
||||
<aside className="w-64 bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800/50 shadow-2xl">
|
||||
{/* Logo Section */}
|
||||
<div className="p-6 border-b border-slate-800/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<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">
|
||||
⚡
|
||||
</div>
|
||||
<div className="flex items-center gap-3 justify-center">
|
||||
<img
|
||||
src="/nextform-logo.png"
|
||||
alt="NextForm"
|
||||
className="h-16 w-16 object-contain"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-lg font-black bg-gradient-to-r from-blue-400 via-cyan-400 to-blue-300 bg-clip-text text-transparent">
|
||||
FitAI
|
||||
NextForm
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 font-semibold tracking-wide">ADMIN PRO</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,39 +13,39 @@ interface StatsCardProps {
|
||||
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
|
||||
const colorStyles = {
|
||||
blue: {
|
||||
bg: "from-blue-600 via-cyan-500 to-teal-500",
|
||||
bg: "from-blue-700 via-cyan-500 to-blue-600",
|
||||
text: "text-white",
|
||||
light: "from-blue-50 to-cyan-50",
|
||||
badge: "bg-blue-100 text-blue-700",
|
||||
icon: "bg-blue-100/50 text-blue-600",
|
||||
badge: "bg-blue-200/60 text-blue-900 backdrop-blur-sm",
|
||||
icon: "bg-white/20 text-white backdrop-blur-sm",
|
||||
},
|
||||
green: {
|
||||
bg: "from-emerald-600 via-teal-500 to-cyan-500",
|
||||
bg: "from-pink-700 via-fuchsia-500 to-pink-600",
|
||||
text: "text-white",
|
||||
light: "from-emerald-50 to-teal-50",
|
||||
badge: "bg-emerald-100 text-emerald-700",
|
||||
icon: "bg-emerald-100/50 text-emerald-600",
|
||||
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-purple-600 via-pink-500 to-blue-500",
|
||||
bg: "from-amber-700 via-yellow-500 to-amber-600",
|
||||
text: "text-white",
|
||||
light: "from-purple-50 to-blue-50",
|
||||
badge: "bg-purple-100 text-purple-700",
|
||||
icon: "bg-purple-100/50 text-purple-600",
|
||||
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-orange-600 via-red-500 to-pink-500",
|
||||
bg: "from-emerald-700 via-teal-500 to-emerald-600",
|
||||
text: "text-white",
|
||||
light: "from-orange-50 to-red-50",
|
||||
badge: "bg-orange-100 text-orange-700",
|
||||
icon: "bg-orange-100/50 text-orange-600",
|
||||
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 (
|
||||
<Card className={`bg-gradient-to-br ${styles.bg} border-0 shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden group h-full`}>
|
||||
<Card className={`bg-gradient-to-br ${styles.bg} border-0 shadow-2xl hover:shadow-2xl transition-all duration-500 overflow-hidden group h-full relative before:absolute before:inset-0 before:bg-gradient-to-br before:from-white/10 before:to-transparent before:pointer-events-none`}>
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2.5 pt-5 px-5">
|
||||
<CardTitle className={`text-xs font-bold uppercase tracking-wider ${styles.text} leading-tight max-w-[70%]`}>
|
||||
{title}
|
||||
|
||||
@@ -45,6 +45,30 @@ export function CardDescription({ children, className = '' }: CardProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export function CardFooter({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`mt-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardTitle({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<h2 className={`text-lg font-semibold ${className}`}>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardDescription({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<p className={`text-sm text-gray-600 ${className}`}>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardFooter({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`mt-4 ${className}`}>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"ignoreDeprecations": "6.0",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
|
||||
Generated
+10
@@ -28,6 +28,7 @@
|
||||
"expo-haptics": "^15.0.7",
|
||||
"expo-linear-gradient": "~15.0.7",
|
||||
"expo-linking": "~8.0.0",
|
||||
"expo-location": "~17.0.1",
|
||||
"expo-notifications": "~0.32.0",
|
||||
"expo-router": "~6.0.14",
|
||||
"expo-secure-store": "~15.0.7",
|
||||
@@ -7329,6 +7330,15 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-location": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-17.0.1.tgz",
|
||||
"integrity": "sha512-m+OzotzlAXO3ZZ1uqW5GC25nXW868zN+ROyBA1V4VF6jGay1ZEs4URPglCVUDzZby2F5wt24cMzqDKw2IX6nRw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-modules-autolinking": {
|
||||
"version": "3.0.22",
|
||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"expo-haptics": "^15.0.7",
|
||||
"expo-linear-gradient": "~15.0.7",
|
||||
"expo-linking": "~8.0.0",
|
||||
"expo-location": "~17.0.1",
|
||||
"expo-notifications": "~0.32.0",
|
||||
"expo-router": "~6.0.14",
|
||||
"expo-secure-store": "~15.0.7",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2FB7E8;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#0B9FD0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0B7FB3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Main arrow shape -->
|
||||
<path d="M 420 150 L 540 150 L 420 300 Z" fill="url(#grad1)" opacity="0.95"/>
|
||||
<path d="M 180 150 L 300 300 L 180 300 Z" fill="url(#grad1)" opacity="0.85"/>
|
||||
|
||||
<!-- Dumbbell - Left -->
|
||||
<g transform="translate(200, 250)">
|
||||
<!-- Left disk -->
|
||||
<circle cx="0" cy="0" r="35" fill="white" opacity="0.95"/>
|
||||
<!-- Bar left -->
|
||||
<rect x="35" y="-15" width="80" height="30" fill="white" rx="4"/>
|
||||
<!-- Right disk -->
|
||||
<circle cx="115" cy="0" r="35" fill="white" opacity="0.95"/>
|
||||
<!-- Grip lines -->
|
||||
<line x1="45" y1="-35" x2="45" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="65" y1="-35" x2="65" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="85" y1="-35" x2="85" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
<line x1="105" y1="-35" x2="105" y2="35" stroke="url(#grad1)" stroke-width="3" opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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() {
|
||||
<Text style={styles.subtitle}>Track your gym visits</Text>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Geofencing Status Card */}
|
||||
<GeofenceStatus />
|
||||
|
||||
<View style={styles.actionContainer}>
|
||||
{activeCheckIn ? (
|
||||
<LinearGradient
|
||||
|
||||
@@ -110,9 +110,15 @@ export default function HomeScreen() {
|
||||
>
|
||||
{/* Header Section */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>{getGreeting()},</Text>
|
||||
<Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
<Image
|
||||
source={require("../../../public/nextform-logo.png")}
|
||||
style={{ width: 40, height: 40, resizeMode: 'contain' }}
|
||||
/>
|
||||
<View>
|
||||
<Text style={styles.greeting}>{getGreeting()},</Text>
|
||||
<Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.avatarContainer}>
|
||||
{user?.imageUrl ? (
|
||||
|
||||
@@ -16,7 +16,7 @@ export function ActivityWidget({ steps, calories, duration }: ActivityWidgetProp
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.dark}
|
||||
colors={['#0c4a6e', '#0369a1', '#0ea5e9']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={[styles.card, theme.shadows.medium]}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const GeofenceStatus = () => {
|
||||
const [isInside, setIsInside] = useState(false)
|
||||
const [distance, setDistance] = useState('250 m')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleRefresh = () => {
|
||||
setLoading(true)
|
||||
// Simulate location check
|
||||
setTimeout(() => {
|
||||
setDistance(Math.random() > 0.5 ? '150 m' : '300 m')
|
||||
setIsInside(Math.random() > 0.5)
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={
|
||||
isInside
|
||||
? ['#10b981', '#6ee7b7', '#a7f3d0']
|
||||
: ['#64748b', '#94a3b8', '#cbd5e1']
|
||||
}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.card}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleRow}>
|
||||
<Ionicons
|
||||
name={isInside ? 'location-sharp' : 'location'}
|
||||
size={24}
|
||||
color="#fff"
|
||||
/>
|
||||
<Text style={styles.title}>
|
||||
{isInside ? 'At Gym' : 'Away from Gym'}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleRefresh}
|
||||
disabled={loading}
|
||||
style={styles.refreshButton}
|
||||
>
|
||||
<Ionicons
|
||||
name="refresh"
|
||||
size={20}
|
||||
color="#fff"
|
||||
style={loading ? { opacity: 0.5 } : {}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.stat}>
|
||||
<Text style={styles.statLabel}>Distance</Text>
|
||||
<Text style={styles.statValue}>{distance}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
refreshButton: {
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
},
|
||||
content: {
|
||||
marginTop: 12,
|
||||
},
|
||||
stat: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 14,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontWeight: '500',
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
})
|
||||
@@ -15,7 +15,7 @@ export function HydrationWidget({ current, goal }: HydrationWidgetProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={['#e0f2fe', '#dbeafe']} // Light blue background
|
||||
colors={['#0369a1', '#0ea5e9', '#06b6d4']}
|
||||
style={[styles.card, theme.shadows.subtle]}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
|
||||
@@ -16,37 +16,37 @@ interface MetricProps {
|
||||
const getColorScheme = (colorScheme: 'blue' | 'green' | 'purple' | 'orange') => {
|
||||
const schemes = {
|
||||
blue: {
|
||||
colors: ['#1e3a8a', '#0c4a6e'],
|
||||
colors: ['#1e40af', '#0369a1', '#06b6d4'] as const,
|
||||
text: '#ffffff',
|
||||
label: '#93c5fd',
|
||||
badge: '#3b82f6',
|
||||
label: '#e0f2fe',
|
||||
badge: '#0ea5e9',
|
||||
badgeText: '#ffffff',
|
||||
icon: '#60a5fa',
|
||||
icon: '#06b6d4',
|
||||
},
|
||||
green: {
|
||||
colors: ['#064e3b', '#047857'],
|
||||
colors: ['#be185d', '#ec4899', '#f472b6'] as const,
|
||||
text: '#ffffff',
|
||||
label: '#86efac',
|
||||
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',
|
||||
},
|
||||
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];
|
||||
};
|
||||
|
||||
@@ -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<QRCodeProps> = ({
|
||||
value,
|
||||
size = 250
|
||||
}) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.qrWrapper}>
|
||||
<QRCode
|
||||
value={value}
|
||||
size={size}
|
||||
color="black"
|
||||
backgroundColor="white"
|
||||
quietZone={10}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
qrWrapper: {
|
||||
padding: 16,
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 12,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
},
|
||||
});
|
||||
@@ -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<QRScannerProps> = ({ onScan, onClose }) => {
|
||||
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
||||
const [scanned, setScanned] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getBarCodeScannerPermissions = async () => {
|
||||
const { status } = await BarCodeScanner.requestPermissionsAsync();
|
||||
setHasPermission(status === 'granted');
|
||||
};
|
||||
|
||||
getBarCodeScannerPermissions();
|
||||
}, []);
|
||||
|
||||
const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
|
||||
setScanned(true);
|
||||
onScan(data);
|
||||
Alert.alert('QR Code', `Scanned: ${data}`, [
|
||||
{ text: 'OK', onPress: () => setScanned(false) },
|
||||
]);
|
||||
};
|
||||
|
||||
if (hasPermission === null) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Requesting for camera permission...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasPermission === false) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>No access to camera</Text>
|
||||
{onClose && <Button title="Close" onPress={onClose} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<BarCodeScanner
|
||||
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
/>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.scanBox} />
|
||||
</View>
|
||||
{onClose && (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button title="Close Scanner" onPress={onClose} color="#FF6B6B" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
overlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
scanBox: {
|
||||
width: 250,
|
||||
height: 250,
|
||||
borderWidth: 2,
|
||||
borderColor: '#4CAF50',
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
buttonContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
});
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Reference in new issue
Block a user