Merge version_2 into main
Merge version_2 into main
This commit was merged in pull request #3.
This commit is contained in:
235
src/app/login/page.tsx
Normal file
235
src/app/login/page.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
import { useState } from 'react';
|
||||
import { Mail, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { email?: string; password?: string } = {};
|
||||
|
||||
if (!email) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
newErrors.email = 'Please enter a valid email';
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else if (password.length < 6) {
|
||||
newErrors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// Simulated API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log('Login attempt:', { email, password });
|
||||
// In a real app, you would redirect to dashboard or handle authentication
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="elastic-effect"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="pill"
|
||||
contentWidth="smallMedium"
|
||||
sizing="mediumSizeLargeTitles"
|
||||
background="blurBottom"
|
||||
cardStyle="gradient-bordered"
|
||||
primaryButtonStyle="flat"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="extrabold"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
navItems={[
|
||||
{ name: "Dashboard", id: "/" },
|
||||
{ name: "Treino", id: "#training" },
|
||||
{ name: "Nutrição", id: "#nutrition" },
|
||||
{ name: "Comunidade", id: "#community" },
|
||||
{ name: "Perfil", id: "#profile" }
|
||||
]}
|
||||
button={{ text: "Começar Agora", href: "/signup" }}
|
||||
brandName="FitFlow Pro"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="login" data-section="login" className="min-h-screen flex items-center justify-center px-4 py-20">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="rounded-2xl border border-opacity-20 p-8 backdrop-blur-sm" style={{
|
||||
backgroundColor: 'var(--color-card)',
|
||||
borderColor: 'var(--color-foreground)'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Welcome Back
|
||||
</h1>
|
||||
<p className="text-sm" style={{ color: 'var(--color-foreground)', opacity: 0.7 }}>
|
||||
Sign in to your FitFlow Pro account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
className="w-full pl-10 pr-4 py-2.5 rounded-lg border transition-all"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: errors.email ? '#ef4444' : 'var(--color-primary-cta)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
||||
{errors.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-10 pr-10 py-2.5 rounded-lg border transition-all"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: errors.password ? '#ef4444' : 'var(--color-primary-cta)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-3.5 transition-opacity hover:opacity-75"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
||||
{errors.password}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remember & Forgot */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="w-4 h-4 rounded" style={{ accentColor: 'var(--color-primary-cta)' }} />
|
||||
<span className="ml-2 text-sm" style={{ color: 'var(--color-foreground)', opacity: 0.7 }}>Remember me</span>
|
||||
</label>
|
||||
<a href="#forgot" className="text-sm font-medium hover:underline" style={{ color: 'var(--color-primary-cta)' }}>
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-2.5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-primary-cta)',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
{!isSubmitting && <ArrowRight className="w-4 h-4" />}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="my-6 flex items-center gap-4">
|
||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
||||
<span className="text-xs" style={{ color: 'var(--color-foreground)', opacity: 0.5 }}>OR</span>
|
||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
||||
</div>
|
||||
|
||||
{/* Social Login */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="py-2.5 rounded-lg border font-medium transition-all hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: 'var(--color-foreground)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
>
|
||||
Google
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="py-2.5 rounded-lg border font-medium transition-all hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: 'var(--color-foreground)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
>
|
||||
Apple
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sign Up Link */}
|
||||
<p className="mt-6 text-center text-sm" style={{ color: 'var(--color-foreground)', opacity: 0.7 }}>
|
||||
Don't have an account?{' '}
|
||||
<a href="/signup" className="font-semibold hover:underline" style={{ color: 'var(--color-primary-cta)' }}>
|
||||
Sign up
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -380,4 +380,4 @@ export default function LandingPage() {
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
404
src/app/signup/page.tsx
Normal file
404
src/app/signup/page.tsx
Normal file
@@ -0,0 +1,404 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||
import { useState } from 'react';
|
||||
import { Mail, Lock, User, Eye, EyeOff, ArrowRight, CheckCircle } from 'lucide-react';
|
||||
|
||||
export default function SignupPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [passwordStrength, setPasswordStrength] = useState(0);
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
|
||||
const calculatePasswordStrength = (pwd: string) => {
|
||||
let strength = 0;
|
||||
if (pwd.length >= 8) strength++;
|
||||
if (pwd.match(/[a-z]/) && pwd.match(/[A-Z]/)) strength++;
|
||||
if (pwd.match(/[0-9]/)) strength++;
|
||||
if (pwd.match(/[^a-zA-Z0-9]/)) strength++;
|
||||
setPasswordStrength(strength);
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.fullName.trim()) {
|
||||
newErrors.fullName = 'Full name is required';
|
||||
}
|
||||
|
||||
if (!formData.email) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email';
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else if (formData.password.length < 8) {
|
||||
newErrors.password = 'Password must be at least 8 characters';
|
||||
} else if (passwordStrength < 2) {
|
||||
newErrors.password = 'Password is too weak';
|
||||
}
|
||||
|
||||
if (!formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Please confirm your password';
|
||||
} else if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
|
||||
if (!agreedToTerms) {
|
||||
newErrors.terms = 'You must agree to the terms';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
|
||||
if (name === 'password') {
|
||||
calculatePasswordStrength(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// Simulated API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log('Signup attempt:', { ...formData, confirmPassword: undefined });
|
||||
// In a real app, you would create account and redirect
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getPasswordStrengthColor = () => {
|
||||
if (passwordStrength <= 1) return '#ef4444';
|
||||
if (passwordStrength <= 2) return '#eab308';
|
||||
if (passwordStrength <= 3) return '#f97316';
|
||||
return '#22c55e';
|
||||
};
|
||||
|
||||
const getPasswordStrengthText = () => {
|
||||
if (!formData.password) return '';
|
||||
if (passwordStrength <= 1) return 'Weak';
|
||||
if (passwordStrength <= 2) return 'Fair';
|
||||
if (passwordStrength <= 3) return 'Good';
|
||||
return 'Strong';
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="elastic-effect"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="pill"
|
||||
contentWidth="smallMedium"
|
||||
sizing="mediumSizeLargeTitles"
|
||||
background="blurBottom"
|
||||
cardStyle="gradient-bordered"
|
||||
primaryButtonStyle="flat"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="extrabold"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
navItems={[
|
||||
{ name: "Dashboard", id: "/" },
|
||||
{ name: "Treino", id: "#training" },
|
||||
{ name: "Nutrição", id: "#nutrition" },
|
||||
{ name: "Comunidade", id: "#community" },
|
||||
{ name: "Perfil", id: "#profile" }
|
||||
]}
|
||||
button={{ text: "Entrar", href: "/login" }}
|
||||
brandName="FitFlow Pro"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="signup" data-section="signup" className="min-h-screen flex items-center justify-center px-4 py-20">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="rounded-2xl border border-opacity-20 p-8 backdrop-blur-sm" style={{
|
||||
backgroundColor: 'var(--color-card)',
|
||||
borderColor: 'var(--color-foreground)'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Create Account
|
||||
</h1>
|
||||
<p className="text-sm" style={{ color: 'var(--color-foreground)', opacity: 0.7 }}>
|
||||
Join 150k+ athletes transforming their bodies
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Full Name Field */}
|
||||
<div>
|
||||
<label htmlFor="fullName" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Full Name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
<input
|
||||
id="fullName"
|
||||
type="text"
|
||||
name="fullName"
|
||||
value={formData.fullName}
|
||||
onChange={handleChange}
|
||||
placeholder="Your name"
|
||||
className="w-full pl-10 pr-4 py-2.5 rounded-lg border transition-all"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: errors.fullName ? '#ef4444' : 'var(--color-primary-cta)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.fullName && (
|
||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
||||
{errors.fullName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder="you@example.com"
|
||||
className="w-full pl-10 pr-4 py-2.5 rounded-lg border transition-all"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: errors.email ? '#ef4444' : 'var(--color-primary-cta)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
||||
{errors.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-10 pr-10 py-2.5 rounded-lg border transition-all"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: errors.password ? '#ef4444' : 'var(--color-primary-cta)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-3.5 transition-opacity hover:opacity-75"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Password Strength Indicator */}
|
||||
{formData.password && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="flex-1 h-1.5 rounded-full bg-gray-300" style={{ background: 'var(--color-background)' }}>
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${(passwordStrength / 4) * 100}%`,
|
||||
backgroundColor: getPasswordStrengthColor(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-medium" style={{ color: getPasswordStrengthColor() }}>
|
||||
{getPasswordStrengthText()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
||||
{errors.password}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirm Password Field */}
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
||||
Confirm Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-10 pr-10 py-2.5 rounded-lg border transition-all"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: errors.confirmPassword ? '#ef4444' : 'var(--color-primary-cta)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute right-3 top-3.5 transition-opacity hover:opacity-75"
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<EyeOff className="w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.confirmPassword && (
|
||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
||||
{errors.confirmPassword}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Terms Agreement */}
|
||||
<div className="pt-2">
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<div className="mt-0.5">
|
||||
{agreedToTerms ? (
|
||||
<CheckCircle className="w-5 h-5" style={{ color: 'var(--color-primary-cta)' }} />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded border" style={{ borderColor: 'var(--color-foreground)', opacity: 0.3 }} />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm" style={{ color: 'var(--color-foreground)', opacity: 0.7 }}>
|
||||
I agree to the{' '}
|
||||
<a href="#terms" className="font-semibold hover:underline" style={{ color: 'var(--color-primary-cta)' }}>
|
||||
Terms of Service
|
||||
</a>
|
||||
{' '}and{' '}
|
||||
<a href="#privacy" className="font-semibold hover:underline" style={{ color: 'var(--color-primary-cta)' }}>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreedToTerms}
|
||||
onChange={(e) => setAgreedToTerms(e.target.checked)}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
{errors.terms && (
|
||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
||||
{errors.terms}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-2.5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 disabled:opacity-50 mt-6"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-primary-cta)',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? 'Creating Account...' : 'Create Account'}
|
||||
{!isSubmitting && <ArrowRight className="w-4 h-4" />}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="my-6 flex items-center gap-4">
|
||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
||||
<span className="text-xs" style={{ color: 'var(--color-foreground)', opacity: 0.5 }}>OR</span>
|
||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
||||
</div>
|
||||
|
||||
{/* Social Signup */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="py-2.5 rounded-lg border font-medium transition-all hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: 'var(--color-foreground)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
>
|
||||
Google
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="py-2.5 rounded-lg border font-medium transition-all hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderColor: 'var(--color-foreground)',
|
||||
color: 'var(--color-foreground)',
|
||||
}}
|
||||
>
|
||||
Apple
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Login Link */}
|
||||
<p className="mt-6 text-center text-sm" style={{ color: 'var(--color-foreground)', opacity: 0.7 }}>
|
||||
Already have an account?{' '}
|
||||
<a href="/login" className="font-semibold hover:underline" style={{ color: 'var(--color-primary-cta)' }}>
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
86
src/components/workout/WorkoutMetricsDisplay.tsx
Normal file
86
src/components/workout/WorkoutMetricsDisplay.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { useWorkoutData } from '@/hooks/useWorkoutData';
|
||||
import { UserMetrics } from '@/lib/storage/workoutStorage';
|
||||
|
||||
interface WorkoutMetricsDisplayProps {
|
||||
className?: string;
|
||||
showRefresh?: boolean;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
export const WorkoutMetricsDisplay: React.FC<WorkoutMetricsDisplayProps> = ({
|
||||
className = '',
|
||||
showRefresh = true,
|
||||
onRefresh
|
||||
}) => {
|
||||
const { metrics, refreshMetrics, isLoading } = useWorkoutData();
|
||||
|
||||
const handleRefresh = () => {
|
||||
refreshMetrics();
|
||||
onRefresh?.();
|
||||
};
|
||||
|
||||
if (isLoading || !metrics) {
|
||||
return <div className={className}>Loading metrics...</div>;
|
||||
}
|
||||
|
||||
const formatMetric = (value: number, unit: string) => {
|
||||
return `${value.toLocaleString()}${unit}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`workout-metrics ${className}`}>
|
||||
<div className="metrics-grid">
|
||||
<div className="metric-card">
|
||||
<h3>Total Steps</h3>
|
||||
<p>{formatMetric(metrics.totalSteps, ' steps')}</p>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<h3>Total Distance</h3>
|
||||
<p>{formatMetric(metrics.totalDistance, ' km')}</p>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<h3>Total Calories</h3>
|
||||
<p>{formatMetric(metrics.totalCalories, ' kcal')}</p>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<h3>Total Volume</h3>
|
||||
<p>{formatMetric(metrics.totalVolume, ' kg')}</p>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<h3>Workout Streak</h3>
|
||||
<p>{metrics.workoutStreak} days</p>
|
||||
</div>
|
||||
{metrics.lastWorkoutDate && (
|
||||
<div className="metric-card">
|
||||
<h3>Last Workout</h3>
|
||||
<p>{new Date(metrics.lastWorkoutDate).toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Object.keys(metrics.personalRecords).length > 0 && (
|
||||
<div className="personal-records">
|
||||
<h3>Personal Records</h3>
|
||||
<ul>
|
||||
{Object.entries(metrics.personalRecords).map(([exercise, weight]) => (
|
||||
<li key={exercise}>
|
||||
<span>{exercise}</span>
|
||||
<span>{weight} kg</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRefresh && (
|
||||
<button onClick={handleRefresh} className="refresh-button">
|
||||
Refresh Metrics
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkoutMetricsDisplay;
|
||||
129
src/components/workout/WorkoutSaver.tsx
Normal file
129
src/components/workout/WorkoutSaver.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useWorkoutData } from '@/hooks/useWorkoutData';
|
||||
import { WorkoutSession, ExerciseLog } from '@/lib/storage/workoutStorage';
|
||||
|
||||
interface WorkoutSaverProps {
|
||||
onSave?: (session: WorkoutSession) => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
export const WorkoutSaver: React.FC<WorkoutSaverProps> = ({ onSave, onError }) => {
|
||||
const { addSession } = useWorkoutData();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const saveCardioWorkout = async (data: {
|
||||
distance: number;
|
||||
duration: number;
|
||||
pace: string;
|
||||
calories: number;
|
||||
steps?: number;
|
||||
}) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const session: Omit<WorkoutSession, 'id'> = {
|
||||
date: new Date().toISOString(),
|
||||
type: 'cardio',
|
||||
distance: data.distance,
|
||||
duration: data.duration,
|
||||
pace: data.pace,
|
||||
calories: data.calories,
|
||||
steps: data.steps
|
||||
};
|
||||
const success = addSession(session);
|
||||
if (success && onSave) {
|
||||
onSave(session as WorkoutSession);
|
||||
} else if (!success && onError) {
|
||||
onError('Failed to save cardio workout');
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) onError('Error saving cardio workout');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveTrainingWorkout = async (data: {
|
||||
exercises: ExerciseLog[];
|
||||
duration: number;
|
||||
calories?: number;
|
||||
}) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const session: Omit<WorkoutSession, 'id'> = {
|
||||
date: new Date().toISOString(),
|
||||
type: 'training',
|
||||
exercises: data.exercises,
|
||||
duration: data.duration,
|
||||
calories: data.calories
|
||||
};
|
||||
const success = addSession(session);
|
||||
if (success && onSave) {
|
||||
onSave(session as WorkoutSession);
|
||||
} else if (!success && onError) {
|
||||
onError('Failed to save training workout');
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) onError('Error saving training workout');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveNutritionLog = async (data: {
|
||||
meals: Array<{
|
||||
name: string;
|
||||
calories: number;
|
||||
protein: number;
|
||||
carbs: number;
|
||||
fats: number;
|
||||
}>;
|
||||
}) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const meals = data.meals.map((meal, index) => ({
|
||||
id: `meal-${index}-${Date.now()}`,
|
||||
name: meal.name,
|
||||
calories: meal.calories,
|
||||
protein: meal.protein,
|
||||
carbs: meal.carbs,
|
||||
fats: meal.fats,
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
|
||||
const totalCalories = data.meals.reduce((sum, meal) => sum + meal.calories, 0);
|
||||
const totalProtein = data.meals.reduce((sum, meal) => sum + meal.protein, 0);
|
||||
|
||||
const session: Omit<WorkoutSession, 'id'> = {
|
||||
date: new Date().toISOString(),
|
||||
type: 'nutrition',
|
||||
meals,
|
||||
calories: totalCalories,
|
||||
notes: `Total Protein: ${totalProtein}g`
|
||||
};
|
||||
const success = addSession(session);
|
||||
if (success && onSave) {
|
||||
onSave(session as WorkoutSession);
|
||||
} else if (!success && onError) {
|
||||
onError('Failed to save nutrition log');
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) onError('Error saving nutrition log');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
saveCardioWorkout,
|
||||
saveTrainingWorkout,
|
||||
saveNutritionLog,
|
||||
isSaving
|
||||
};
|
||||
};
|
||||
|
||||
export default WorkoutSaver;
|
||||
162
src/hooks/useWorkoutData.ts
Normal file
162
src/hooks/useWorkoutData.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
WorkoutSession,
|
||||
UserMetrics,
|
||||
saveWorkoutSession,
|
||||
getWorkoutSessions,
|
||||
updateWorkoutSession,
|
||||
deleteWorkoutSession,
|
||||
getWorkoutsByType,
|
||||
getWorkoutsByDateRange,
|
||||
saveUserMetrics,
|
||||
getUserMetrics,
|
||||
updateUserMetrics,
|
||||
calculateMetricsFromSessions
|
||||
} from '@/lib/storage/workoutStorage';
|
||||
|
||||
export const useWorkoutData = () => {
|
||||
const [sessions, setSessions] = useState<WorkoutSession[]>([]);
|
||||
const [metrics, setMetrics] = useState<UserMetrics | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load initial data
|
||||
useEffect(() => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const loadedSessions = getWorkoutSessions();
|
||||
const loadedMetrics = getUserMetrics();
|
||||
setSessions(loadedSessions);
|
||||
setMetrics(loadedMetrics);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Failed to load workout data');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addSession = useCallback((session: Omit<WorkoutSession, 'id'>) => {
|
||||
try {
|
||||
const newSession: WorkoutSession = {
|
||||
...session,
|
||||
id: Date.now().toString()
|
||||
};
|
||||
const success = saveWorkoutSession(newSession);
|
||||
if (success) {
|
||||
setSessions(prev => [...prev, newSession]);
|
||||
// Recalculate metrics
|
||||
const updatedMetrics = calculateMetricsFromSessions();
|
||||
setMetrics(updatedMetrics);
|
||||
saveUserMetrics(updatedMetrics);
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError('Failed to add session');
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateSession = useCallback((id: string, updates: Partial<WorkoutSession>) => {
|
||||
try {
|
||||
const success = updateWorkoutSession(id, updates);
|
||||
if (success) {
|
||||
setSessions(prev =>
|
||||
prev.map(s => s.id === id ? { ...s, ...updates } : s)
|
||||
);
|
||||
// Recalculate metrics
|
||||
const updatedMetrics = calculateMetricsFromSessions();
|
||||
setMetrics(updatedMetrics);
|
||||
saveUserMetrics(updatedMetrics);
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError('Failed to update session');
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const removeSession = useCallback((id: string) => {
|
||||
try {
|
||||
const success = deleteWorkoutSession(id);
|
||||
if (success) {
|
||||
setSessions(prev => prev.filter(s => s.id !== id));
|
||||
// Recalculate metrics
|
||||
const updatedMetrics = calculateMetricsFromSessions();
|
||||
setMetrics(updatedMetrics);
|
||||
saveUserMetrics(updatedMetrics);
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError('Failed to delete session');
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getSessionsByType = useCallback((type: 'cardio' | 'training' | 'nutrition') => {
|
||||
try {
|
||||
return getWorkoutsByType(type);
|
||||
} catch (err) {
|
||||
setError('Failed to filter sessions');
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getSessionsByDate = useCallback((startDate: string, endDate: string) => {
|
||||
try {
|
||||
return getWorkoutsByDateRange(startDate, endDate);
|
||||
} catch (err) {
|
||||
setError('Failed to filter sessions by date');
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateMetrics = useCallback((updates: Partial<UserMetrics>) => {
|
||||
try {
|
||||
const success = updateUserMetrics(updates);
|
||||
if (success) {
|
||||
setMetrics(prev => prev ? { ...prev, ...updates } : null);
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
setError('Failed to update metrics');
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshMetrics = useCallback(() => {
|
||||
try {
|
||||
const recalculated = calculateMetricsFromSessions();
|
||||
setMetrics(recalculated);
|
||||
saveUserMetrics(recalculated);
|
||||
return recalculated;
|
||||
} catch (err) {
|
||||
setError('Failed to refresh metrics');
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
sessions,
|
||||
metrics,
|
||||
isLoading,
|
||||
error,
|
||||
addSession,
|
||||
updateSession,
|
||||
removeSession,
|
||||
getSessionsByType,
|
||||
getSessionsByDate,
|
||||
updateMetrics,
|
||||
refreshMetrics
|
||||
};
|
||||
};
|
||||
285
src/lib/storage/workoutStorage.ts
Normal file
285
src/lib/storage/workoutStorage.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
// User data persistence layer for workouts and metrics
|
||||
|
||||
export interface WorkoutSession {
|
||||
id: string;
|
||||
date: string;
|
||||
type: 'cardio' | 'training' | 'nutrition';
|
||||
duration?: number;
|
||||
distance?: number;
|
||||
calories?: number;
|
||||
pace?: string;
|
||||
steps?: number;
|
||||
exercises?: ExerciseLog[];
|
||||
meals?: MealLog[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ExerciseLog {
|
||||
id: string;
|
||||
name: string;
|
||||
muscleGroup: string;
|
||||
sets: SetLog[];
|
||||
totalVolume?: number;
|
||||
}
|
||||
|
||||
export interface SetLog {
|
||||
reps: number;
|
||||
weight: number;
|
||||
restTime?: number;
|
||||
}
|
||||
|
||||
export interface MealLog {
|
||||
id: string;
|
||||
name: string;
|
||||
calories: number;
|
||||
protein: number;
|
||||
carbs: number;
|
||||
fats: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface UserMetrics {
|
||||
totalSteps: number;
|
||||
totalDistance: number;
|
||||
totalCalories: number;
|
||||
totalVolume: number;
|
||||
workoutStreak: number;
|
||||
lastWorkoutDate?: string;
|
||||
personalRecords: Record<string, number>;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'fitflow_workouts';
|
||||
const METRICS_KEY = 'fitflow_metrics';
|
||||
|
||||
// Workout Session Management
|
||||
export const saveWorkoutSession = (session: WorkoutSession): boolean => {
|
||||
try {
|
||||
const existing = getWorkoutSessions();
|
||||
const updated = [...existing, { ...session, id: session.id || Date.now().toString() }];
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving workout session:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkoutSessions = (): WorkoutSession[] => {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch (error) {
|
||||
console.error('Error retrieving workout sessions:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkoutById = (id: string): WorkoutSession | null => {
|
||||
try {
|
||||
const sessions = getWorkoutSessions();
|
||||
return sessions.find(s => s.id === id) || null;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving workout by id:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateWorkoutSession = (id: string, updates: Partial<WorkoutSession>): boolean => {
|
||||
try {
|
||||
const sessions = getWorkoutSessions();
|
||||
const index = sessions.findIndex(s => s.id === id);
|
||||
if (index === -1) return false;
|
||||
sessions[index] = { ...sessions[index], ...updates, id };
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating workout session:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWorkoutSession = (id: string): boolean => {
|
||||
try {
|
||||
const sessions = getWorkoutSessions();
|
||||
const filtered = sessions.filter(s => s.id !== id);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error deleting workout session:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkoutsByType = (type: 'cardio' | 'training' | 'nutrition'): WorkoutSession[] => {
|
||||
try {
|
||||
const sessions = getWorkoutSessions();
|
||||
return sessions.filter(s => s.type === type);
|
||||
} catch (error) {
|
||||
console.error('Error filtering workouts by type:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkoutsByDateRange = (startDate: string, endDate: string): WorkoutSession[] => {
|
||||
try {
|
||||
const sessions = getWorkoutSessions();
|
||||
return sessions.filter(s => {
|
||||
const sessionDate = new Date(s.date);
|
||||
return sessionDate >= new Date(startDate) && sessionDate <= new Date(endDate);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error filtering workouts by date range:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Metrics Management
|
||||
export const saveUserMetrics = (metrics: UserMetrics): boolean => {
|
||||
try {
|
||||
localStorage.setItem(METRICS_KEY, JSON.stringify(metrics));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving user metrics:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserMetrics = (): UserMetrics => {
|
||||
try {
|
||||
const data = localStorage.getItem(METRICS_KEY);
|
||||
return data ? JSON.parse(data) : getDefaultMetrics();
|
||||
} catch (error) {
|
||||
console.error('Error retrieving user metrics:', error);
|
||||
return getDefaultMetrics();
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserMetrics = (updates: Partial<UserMetrics>): boolean => {
|
||||
try {
|
||||
const current = getUserMetrics();
|
||||
const updated = { ...current, ...updates };
|
||||
return saveUserMetrics(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating user metrics:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateMetricsFromSessions = (): UserMetrics => {
|
||||
try {
|
||||
const sessions = getWorkoutSessions();
|
||||
let totalSteps = 0;
|
||||
let totalDistance = 0;
|
||||
let totalCalories = 0;
|
||||
let totalVolume = 0;
|
||||
const personalRecords: Record<string, number> = {};
|
||||
|
||||
sessions.forEach(session => {
|
||||
if (session.steps) totalSteps += session.steps;
|
||||
if (session.distance) totalDistance += session.distance;
|
||||
if (session.calories) totalCalories += session.calories;
|
||||
if (session.exercises) {
|
||||
session.exercises.forEach(ex => {
|
||||
ex.sets.forEach(set => {
|
||||
totalVolume += set.weight * set.reps;
|
||||
const key = ex.name;
|
||||
if (!personalRecords[key] || set.weight > personalRecords[key]) {
|
||||
personalRecords[key] = set.weight;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const metrics: UserMetrics = {
|
||||
totalSteps,
|
||||
totalDistance,
|
||||
totalCalories,
|
||||
totalVolume,
|
||||
workoutStreak: calculateWorkoutStreak(sessions),
|
||||
lastWorkoutDate: sessions.length > 0 ? sessions[sessions.length - 1].date : undefined,
|
||||
personalRecords
|
||||
};
|
||||
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
console.error('Error calculating metrics:', error);
|
||||
return getDefaultMetrics();
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateWorkoutStreak = (sessions: WorkoutSession[]): number => {
|
||||
if (sessions.length === 0) return 0;
|
||||
|
||||
const sortedSessions = [...sessions].sort((a, b) =>
|
||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
let streak = 0;
|
||||
let currentDate = new Date();
|
||||
currentDate.setHours(0, 0, 0, 0);
|
||||
|
||||
for (const session of sortedSessions) {
|
||||
const sessionDate = new Date(session.date);
|
||||
sessionDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const dayDiff = Math.floor((currentDate.getTime() - sessionDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (dayDiff === streak) {
|
||||
streak++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return streak;
|
||||
};
|
||||
|
||||
const getDefaultMetrics = (): UserMetrics => ({
|
||||
totalSteps: 0,
|
||||
totalDistance: 0,
|
||||
totalCalories: 0,
|
||||
totalVolume: 0,
|
||||
workoutStreak: 0,
|
||||
personalRecords: {}
|
||||
});
|
||||
|
||||
// Bulk operations
|
||||
export const exportWorkoutData = (): string => {
|
||||
try {
|
||||
const sessions = getWorkoutSessions();
|
||||
const metrics = getUserMetrics();
|
||||
const data = { sessions, metrics, exportDate: new Date().toISOString() };
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
console.error('Error exporting data:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const importWorkoutData = (jsonData: string): boolean => {
|
||||
try {
|
||||
const data = JSON.parse(jsonData);
|
||||
if (data.sessions) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data.sessions));
|
||||
}
|
||||
if (data.metrics) {
|
||||
localStorage.setItem(METRICS_KEY, JSON.stringify(data.metrics));
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error importing data:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const clearAllData = (): boolean => {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(METRICS_KEY);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error clearing data:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user