Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5bdcdd234 | |||
| e96e06a541 | |||
| 59b51e581d | |||
| bb62b2d351 | |||
| 02d210a684 | |||
| 2b040b39b7 | |||
| e05a1e869a | |||
| 3649146e1e | |||
| c79bc5cf3c | |||
| d328e3221c | |||
| 531b1c297b | |||
| 8ae1f717a3 | |||
| a48de5fed2 | |||
| 40357c2c40 | |||
| 3180013673 | |||
| 093309937b | |||
| 308dee7afe | |||
| f9e55abdf0 | |||
| 88623e3b9b | |||
| d0afbe9b1f |
64
src/app/components/WorkoutDataIntegration.tsx
Normal file
64
src/app/components/WorkoutDataIntegration.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useWorkoutTracking } from '@/app/hooks/useWorkoutTracking';
|
||||||
|
import { WorkoutSession, CardioSession, NutritionLog } from '@/app/lib/storage/workoutStorage';
|
||||||
|
|
||||||
|
export interface WorkoutDataIntegrationProps {
|
||||||
|
onSave?: (data: any) => void;
|
||||||
|
autoSave?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WorkoutDataIntegration component that provides workout tracking context
|
||||||
|
* Use this component to wrap sections that need to save workout data
|
||||||
|
*/
|
||||||
|
export const WorkoutDataIntegration: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
} & WorkoutDataIntegrationProps> = ({ children, onSave, autoSave = true }) => {
|
||||||
|
const { metrics, addWorkoutSession, addCardioSession, addNutritionLog, refreshMetrics } =
|
||||||
|
useWorkoutTracking();
|
||||||
|
|
||||||
|
const handleWorkoutSave = useCallback(
|
||||||
|
(session: WorkoutSession) => {
|
||||||
|
addWorkoutSession(session);
|
||||||
|
if (onSave) onSave({ type: 'workout', data: session });
|
||||||
|
},
|
||||||
|
[addWorkoutSession, onSave]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCardioSave = useCallback(
|
||||||
|
(session: CardioSession) => {
|
||||||
|
addCardioSession(session);
|
||||||
|
if (onSave) onSave({ type: 'cardio', data: session });
|
||||||
|
},
|
||||||
|
[addCardioSession, onSave]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNutritionSave = useCallback(
|
||||||
|
(log: NutritionLog) => {
|
||||||
|
addNutritionLog(log);
|
||||||
|
if (onSave) onSave({ type: 'nutrition', data: log });
|
||||||
|
},
|
||||||
|
[addNutritionLog, onSave]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expose save functions via context or props
|
||||||
|
const contextValue = {
|
||||||
|
metrics,
|
||||||
|
handleWorkoutSave,
|
||||||
|
handleCardioSave,
|
||||||
|
handleNutritionSave,
|
||||||
|
refreshMetrics,
|
||||||
|
autoSave,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clone children and pass context data as props
|
||||||
|
return (
|
||||||
|
<div data-workout-integration="true" data-context={JSON.stringify(contextValue)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkoutDataIntegration;
|
||||||
56
src/app/hooks/useWorkoutTracking.ts
Normal file
56
src/app/hooks/useWorkoutTracking.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
saveWorkoutSession,
|
||||||
|
saveCardioSession,
|
||||||
|
saveNutritionLog,
|
||||||
|
getUserMetrics,
|
||||||
|
getWorkoutSessions,
|
||||||
|
getCardioSessions,
|
||||||
|
getNutritionLogs,
|
||||||
|
WorkoutSession,
|
||||||
|
CardioSession,
|
||||||
|
NutritionLog,
|
||||||
|
UserMetrics,
|
||||||
|
} from '@/app/lib/storage/workoutStorage';
|
||||||
|
|
||||||
|
export const useWorkoutTracking = () => {
|
||||||
|
const [metrics, setMetrics] = useState<UserMetrics>(getUserMetrics());
|
||||||
|
const [workouts, setWorkouts] = useState<WorkoutSession[]>(getWorkoutSessions());
|
||||||
|
const [cardioSessions, setCardioSessions] = useState<CardioSession[]>(getCardioSessions());
|
||||||
|
const [nutritionLogs, setNutritionLogs] = useState<NutritionLog[]>(getNutritionLogs());
|
||||||
|
|
||||||
|
const addWorkoutSession = useCallback((session: WorkoutSession) => {
|
||||||
|
saveWorkoutSession(session);
|
||||||
|
setWorkouts(getWorkoutSessions());
|
||||||
|
setMetrics(getUserMetrics());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addCardioSession = useCallback((session: CardioSession) => {
|
||||||
|
saveCardioSession(session);
|
||||||
|
setCardioSessions(getCardioSessions());
|
||||||
|
setMetrics(getUserMetrics());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addNutritionLog = useCallback((log: NutritionLog) => {
|
||||||
|
saveNutritionLog(log);
|
||||||
|
setNutritionLogs(getNutritionLogs());
|
||||||
|
setMetrics(getUserMetrics());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const refreshMetrics = useCallback(() => {
|
||||||
|
setMetrics(getUserMetrics());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
metrics,
|
||||||
|
workouts,
|
||||||
|
cardioSessions,
|
||||||
|
nutritionLogs,
|
||||||
|
addWorkoutSession,
|
||||||
|
addCardioSession,
|
||||||
|
addNutritionLog,
|
||||||
|
refreshMetrics,
|
||||||
|
};
|
||||||
|
};
|
||||||
309
src/app/lib/storage/workoutStorage.ts
Normal file
309
src/app/lib/storage/workoutStorage.ts
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
/**
|
||||||
|
* Workout and metrics data persistence layer
|
||||||
|
* Handles saving and retrieving workout data from localStorage
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WorkoutSet {
|
||||||
|
reps: number;
|
||||||
|
weight: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkoutSession {
|
||||||
|
id: string;
|
||||||
|
exerciseName: string;
|
||||||
|
date: string;
|
||||||
|
sets: WorkoutSet[];
|
||||||
|
duration: number; // in seconds
|
||||||
|
caloriesBurned: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardioSession {
|
||||||
|
id: string;
|
||||||
|
type: 'running' | 'walking' | 'cycling';
|
||||||
|
date: string;
|
||||||
|
distance: number; // in km
|
||||||
|
duration: number; // in seconds
|
||||||
|
caloriesBurned: number;
|
||||||
|
pace: number; // km/h
|
||||||
|
steps?: number;
|
||||||
|
route?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NutritionLog {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
calories: number;
|
||||||
|
protein: number;
|
||||||
|
carbs: number;
|
||||||
|
fats: number;
|
||||||
|
meals: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMetrics {
|
||||||
|
totalWorkouts: number;
|
||||||
|
totalCardioDistance: number;
|
||||||
|
totalCaloriesBurned: number;
|
||||||
|
currentStreak: number;
|
||||||
|
personalRecords: Record<string, number>;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
WORKOUT_SESSIONS: 'fitflow_workout_sessions',
|
||||||
|
CARDIO_SESSIONS: 'fitflow_cardio_sessions',
|
||||||
|
NUTRITION_LOGS: 'fitflow_nutrition_logs',
|
||||||
|
USER_METRICS: 'fitflow_user_metrics',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a workout session to localStorage
|
||||||
|
*/
|
||||||
|
export const saveWorkoutSession = (session: WorkoutSession): void => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessions = getWorkoutSessions();
|
||||||
|
sessions.push(session);
|
||||||
|
localStorage.setItem(STORAGE_KEYS.WORKOUT_SESSIONS, JSON.stringify(sessions));
|
||||||
|
updateUserMetrics();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving workout session:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all workout sessions from localStorage
|
||||||
|
*/
|
||||||
|
export const getWorkoutSessions = (): WorkoutSession[] => {
|
||||||
|
if (typeof window === 'undefined') return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(STORAGE_KEYS.WORKOUT_SESSIONS);
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving workout sessions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a cardio session to localStorage
|
||||||
|
*/
|
||||||
|
export const saveCardioSession = (session: CardioSession): void => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessions = getCardioSessions();
|
||||||
|
sessions.push(session);
|
||||||
|
localStorage.setItem(STORAGE_KEYS.CARDIO_SESSIONS, JSON.stringify(sessions));
|
||||||
|
updateUserMetrics();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving cardio session:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cardio sessions from localStorage
|
||||||
|
*/
|
||||||
|
export const getCardioSessions = (): CardioSession[] => {
|
||||||
|
if (typeof window === 'undefined') return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(STORAGE_KEYS.CARDIO_SESSIONS);
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving cardio sessions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a nutrition log to localStorage
|
||||||
|
*/
|
||||||
|
export const saveNutritionLog = (log: NutritionLog): void => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logs = getNutritionLogs();
|
||||||
|
logs.push(log);
|
||||||
|
localStorage.setItem(STORAGE_KEYS.NUTRITION_LOGS, JSON.stringify(logs));
|
||||||
|
updateUserMetrics();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving nutrition log:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all nutrition logs from localStorage
|
||||||
|
*/
|
||||||
|
export const getNutritionLogs = (): NutritionLog[] => {
|
||||||
|
if (typeof window === 'undefined') return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(STORAGE_KEYS.NUTRITION_LOGS);
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving nutrition logs:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user metrics from localStorage
|
||||||
|
*/
|
||||||
|
export const getUserMetrics = (): UserMetrics => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return {
|
||||||
|
totalWorkouts: 0,
|
||||||
|
totalCardioDistance: 0,
|
||||||
|
totalCaloriesBurned: 0,
|
||||||
|
currentStreak: 0,
|
||||||
|
personalRecords: {},
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(STORAGE_KEYS.USER_METRICS);
|
||||||
|
return data
|
||||||
|
? JSON.parse(data)
|
||||||
|
: {
|
||||||
|
totalWorkouts: 0,
|
||||||
|
totalCardioDistance: 0,
|
||||||
|
totalCaloriesBurned: 0,
|
||||||
|
currentStreak: 0,
|
||||||
|
personalRecords: {},
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving user metrics:', error);
|
||||||
|
return {
|
||||||
|
totalWorkouts: 0,
|
||||||
|
totalCardioDistance: 0,
|
||||||
|
totalCaloriesBurned: 0,
|
||||||
|
currentStreak: 0,
|
||||||
|
personalRecords: {},
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user metrics based on saved sessions
|
||||||
|
*/
|
||||||
|
export const updateUserMetrics = (): void => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workoutSessions = getWorkoutSessions();
|
||||||
|
const cardioSessions = getCardioSessions();
|
||||||
|
const nutritionLogs = getNutritionLogs();
|
||||||
|
|
||||||
|
let totalWorkouts = workoutSessions.length;
|
||||||
|
let totalCardioDistance = cardioSessions.reduce((sum, session) => sum + session.distance, 0);
|
||||||
|
let totalCaloriesBurned = workoutSessions.reduce((sum, session) => sum + session.caloriesBurned, 0)
|
||||||
|
+ cardioSessions.reduce((sum, session) => sum + session.caloriesBurned, 0);
|
||||||
|
|
||||||
|
// Calculate streak (consecutive days with activity)
|
||||||
|
const currentStreak = calculateStreak([...workoutSessions, ...cardioSessions]);
|
||||||
|
|
||||||
|
// Extract personal records from workouts
|
||||||
|
const personalRecords: Record<string, number> = {};
|
||||||
|
workoutSessions.forEach((session) => {
|
||||||
|
const maxWeight = Math.max(...session.sets.map((s) => s.weight));
|
||||||
|
const key = `${session.exerciseName}_pr`;
|
||||||
|
if (!personalRecords[key] || maxWeight > personalRecords[key]) {
|
||||||
|
personalRecords[key] = maxWeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const metrics: UserMetrics = {
|
||||||
|
totalWorkouts,
|
||||||
|
totalCardioDistance,
|
||||||
|
totalCaloriesBurned,
|
||||||
|
currentStreak,
|
||||||
|
personalRecords,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem(STORAGE_KEYS.USER_METRICS, JSON.stringify(metrics));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating user metrics:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate consecutive days with activity
|
||||||
|
*/
|
||||||
|
const calculateStreak = (sessions: Array<WorkoutSession | CardioSession>): number => {
|
||||||
|
if (sessions.length === 0) return 0;
|
||||||
|
|
||||||
|
const dates = sessions.map((session) => new Date(session.date).toDateString()).filter((date, index, self) => self.indexOf(date) === index).sort((a, b) => new Date(b).getTime() - new Date(a).getTime());
|
||||||
|
|
||||||
|
let streak = 1;
|
||||||
|
for (let i = 1; i < dates.length; i++) {
|
||||||
|
const currentDate = new Date(dates[i]);
|
||||||
|
const previousDate = new Date(dates[i - 1]);
|
||||||
|
const diffTime = Math.abs(previousDate.getTime() - currentDate.getTime());
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays === 1) {
|
||||||
|
streak++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return streak;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all stored data
|
||||||
|
*/
|
||||||
|
export const clearAllData = (): void => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.WORKOUT_SESSIONS);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.CARDIO_SESSIONS);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.NUTRITION_LOGS);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.USER_METRICS);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export data as JSON for backup
|
||||||
|
*/
|
||||||
|
export const exportData = (): string => {
|
||||||
|
const data = {
|
||||||
|
workouts: getWorkoutSessions(),
|
||||||
|
cardio: getCardioSessions(),
|
||||||
|
nutrition: getNutritionLogs(),
|
||||||
|
metrics: getUserMetrics(),
|
||||||
|
exportDate: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import data from JSON backup
|
||||||
|
*/
|
||||||
|
export const importData = (jsonData: string): boolean => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(jsonData);
|
||||||
|
if (data.workouts) localStorage.setItem(STORAGE_KEYS.WORKOUT_SESSIONS, JSON.stringify(data.workouts));
|
||||||
|
if (data.cardio) localStorage.setItem(STORAGE_KEYS.CARDIO_SESSIONS, JSON.stringify(data.cardio));
|
||||||
|
if (data.nutrition) localStorage.setItem(STORAGE_KEYS.NUTRITION_LOGS, JSON.stringify(data.nutrition));
|
||||||
|
if (data.metrics) localStorage.setItem(STORAGE_KEYS.USER_METRICS, JSON.stringify(data.metrics));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing data:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,36 +2,53 @@
|
|||||||
|
|
||||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||||
|
import ContactCTA from '@/components/sections/contact/ContactCTA';
|
||||||
|
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
|
||||||
|
import { Mail, Lock, ArrowRight, AlertCircle } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Mail, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState('');
|
const [formData, setFormData] = useState({
|
||||||
const [password, setPassword] = useState('');
|
email: '',
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
password: ''
|
||||||
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
});
|
||||||
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: { email?: string; password?: string } = {};
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
|
||||||
if (!email) {
|
if (!formData.email) {
|
||||||
newErrors.email = 'Email is required';
|
newErrors.email = 'Email é obrigatório';
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||||
newErrors.email = 'Please enter a valid email';
|
newErrors.email = 'Email inválido';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password) {
|
if (!formData.password) {
|
||||||
newErrors.password = 'Password is required';
|
newErrors.password = 'Senha é obrigatória';
|
||||||
} else if (password.length < 6) {
|
} else if (formData.password.length < 6) {
|
||||||
newErrors.password = 'Password must be at least 6 characters';
|
newErrors.password = 'Senha deve ter pelo menos 6 caracteres';
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
@@ -40,10 +57,12 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// Simulated API call
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
console.log('Login attempt:', { email, password });
|
console.log('Login attempt:', formData);
|
||||||
// In a real app, you would redirect to dashboard or handle authentication
|
alert('Login bem-sucedido!');
|
||||||
|
setFormData({ email: '', password: '' });
|
||||||
|
} catch (error) {
|
||||||
|
setErrors({ submit: 'Erro ao fazer login. Tente novamente.' });
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -66,170 +85,175 @@ export default function LoginPage() {
|
|||||||
<NavbarStyleCentered
|
<NavbarStyleCentered
|
||||||
navItems={[
|
navItems={[
|
||||||
{ name: "Dashboard", id: "/" },
|
{ name: "Dashboard", id: "/" },
|
||||||
{ name: "Treino", id: "#training" },
|
{ name: "Sobre", id: "#hero" },
|
||||||
{ name: "Nutrição", id: "#nutrition" },
|
{ name: "Features", id: "#onboarding" },
|
||||||
{ name: "Comunidade", id: "#community" },
|
{ name: "Contato", id: "#contact" },
|
||||||
{ name: "Perfil", id: "#profile" }
|
{ name: "Signup", id: "/signup" }
|
||||||
]}
|
]}
|
||||||
button={{ text: "Começar Agora", href: "/signup" }}
|
button={{ text: "Fazer Login", href: "/login" }}
|
||||||
brandName="FitFlow Pro"
|
brandName="FitFlow Pro"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="login" data-section="login" className="min-h-screen flex items-center justify-center px-4 py-20">
|
<div id="login" data-section="login" className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div className="rounded-2xl border border-opacity-20 p-8 backdrop-blur-sm" style={{
|
<div className="text-center space-y-3">
|
||||||
backgroundColor: 'var(--color-card)',
|
<h1 className="text-4xl font-extrabold tracking-tight">Bem-vindo de volta</h1>
|
||||||
borderColor: 'var(--color-foreground)'
|
<p className="text-base text-gray-600 dark:text-gray-400">Faça login na sua conta FitFlow Pro</p>
|
||||||
}}>
|
</div>
|
||||||
{/* 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 */}
|
<div className="bg-card rounded-2xl border border-primary-cta/20 p-8 shadow-lg">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="email" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||||
Email Address
|
Email
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Mail className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
<Mail className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={formData.email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={handleChange}
|
||||||
placeholder="you@example.com"
|
placeholder="seu@email.com"
|
||||||
className="w-full pl-10 pr-4 py-2.5 rounded-lg border transition-all"
|
className={`w-full pl-10 pr-4 py-2.5 bg-background border rounded-lg focus:outline-none focus:ring-2 transition-all ${
|
||||||
style={{
|
errors.email
|
||||||
backgroundColor: 'var(--color-background)',
|
? 'border-red-500 focus:ring-red-500'
|
||||||
borderColor: errors.email ? '#ef4444' : 'var(--color-primary-cta)',
|
: 'border-gray-300 focus:ring-primary-cta'
|
||||||
color: 'var(--color-foreground)',
|
}`}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
{errors.email}
|
{errors.email}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password Field */}
|
{/* Password Field */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="password" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||||
Password
|
Senha
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
<Lock className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type={showPassword ? 'text' : 'password'}
|
name="password"
|
||||||
value={password}
|
type="password"
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className="w-full pl-10 pr-10 py-2.5 rounded-lg border transition-all"
|
className={`w-full pl-10 pr-4 py-2.5 bg-background border rounded-lg focus:outline-none focus:ring-2 transition-all ${
|
||||||
style={{
|
errors.password
|
||||||
backgroundColor: 'var(--color-background)',
|
? 'border-red-500 focus:ring-red-500'
|
||||||
borderColor: errors.password ? '#ef4444' : 'var(--color-primary-cta)',
|
: 'border-gray-300 focus:ring-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>
|
</div>
|
||||||
{errors.password && (
|
{errors.password && (
|
||||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
{errors.password}
|
{errors.password}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Remember & Forgot */}
|
{/* Submit Errors */}
|
||||||
<div className="flex items-center justify-between">
|
{errors.submit && (
|
||||||
<label className="flex items-center cursor-pointer">
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 flex items-center gap-2 text-red-700 dark:text-red-400 text-sm">
|
||||||
<input type="checkbox" className="w-4 h-4 rounded" style={{ accentColor: 'var(--color-primary-cta)' }} />
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
<span className="ml-2 text-sm" style={{ color: 'var(--color-foreground)', opacity: 0.7 }}>Remember me</span>
|
{errors.submit}
|
||||||
</label>
|
</div>
|
||||||
<a href="#forgot" className="text-sm font-medium hover:underline" style={{ color: 'var(--color-primary-cta)' }}>
|
)}
|
||||||
Forgot password?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="w-full py-2.5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 disabled:opacity-50"
|
className="w-full bg-primary-cta hover:bg-primary-cta/90 text-white font-semibold py-2.5 rounded-lg transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
style={{
|
|
||||||
backgroundColor: 'var(--color-primary-cta)',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
{isSubmitting ? 'Entrando...' : 'Fazer Login'}
|
||||||
{!isSubmitting && <ArrowRight className="w-4 h-4" />}
|
{!isSubmitting && <ArrowRight className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="my-6 flex items-center gap-4">
|
<div className="mt-6 relative">
|
||||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
<div className="absolute inset-0 flex items-center">
|
||||||
<span className="text-xs" style={{ color: 'var(--color-foreground)', opacity: 0.5 }}>OR</span>
|
<div className="w-full border-t border-gray-300 dark:border-gray-600"></div>
|
||||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-card text-gray-500">ou continue com</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Social Login */}
|
{/* Social Login Buttons */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="mt-6 space-y-3">
|
||||||
<button
|
<button className="w-full bg-background hover:bg-gray-50 dark:hover:bg-gray-800 border border-gray-300 dark:border-gray-600 text-foreground font-medium py-2.5 rounded-lg transition-all">
|
||||||
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
|
Google
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="w-full bg-background hover:bg-gray-50 dark:hover:bg-gray-800 border border-gray-300 dark:border-gray-600 text-foreground font-medium py-2.5 rounded-lg transition-all">
|
||||||
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
|
Apple
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
{/* Sign Up Link */}
|
||||||
|
<p className="text-center text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
Não tem conta?{' '}
|
||||||
|
<a href="/signup" className="text-primary-cta hover:text-primary-cta/80 font-semibold transition-colors">
|
||||||
|
Criar conta
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Forgot Password Link */}
|
||||||
|
<p className="text-center text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
<a href="#" className="text-primary-cta hover:text-primary-cta/80 font-semibold transition-colors">
|
||||||
|
Esqueceu sua senha?
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="footer" data-section="footer">
|
||||||
|
<FooterLogoEmphasis
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: "Sobre", href: "/#hero" },
|
||||||
|
{ label: "Features", href: "/#onboarding" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Blog", href: "#" },
|
||||||
|
{ label: "Ajuda", href: "#" },
|
||||||
|
{ label: "Suporte", href: "#" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Privacidade", href: "#" },
|
||||||
|
{ label: "Termos", href: "#" },
|
||||||
|
{ label: "Contato", href: "#" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Login", href: "/login" },
|
||||||
|
{ label: "Signup", href: "/signup" },
|
||||||
|
{ label: "Dashboard", href: "/" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
logoText="FitFlow Pro"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
497
src/app/page.tsx
497
src/app/page.tsx
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||||
import { ThemeProvider } from '@/providers/themeProvider/ThemeProvider';
|
|
||||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||||
import HeroSplitKpi from '@/components/sections/hero/HeroSplitKpi';
|
import HeroSplitKpi from '@/components/sections/hero/HeroSplitKpi';
|
||||||
import FeatureCardTwentyFive from '@/components/sections/feature/FeatureCardTwentyFive';
|
import FeatureCardTwentyFive from '@/components/sections/feature/FeatureCardTwentyFive';
|
||||||
@@ -13,301 +12,253 @@ import TestimonialCardTwelve from '@/components/sections/testimonial/Testimonial
|
|||||||
import SocialProofOne from '@/components/sections/socialProof/SocialProofOne';
|
import SocialProofOne from '@/components/sections/socialProof/SocialProofOne';
|
||||||
import ContactText from '@/components/sections/contact/ContactText';
|
import ContactText from '@/components/sections/contact/ContactText';
|
||||||
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
|
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
|
||||||
import { Zap, Dumbbell, Apple, TrendingUp } from 'lucide-react';
|
import { Activity, Apple, Brain, Dumbbell, Heart, Target, Zap, Users, Star, TrendingDown, TrendingUp } from 'lucide-react';
|
||||||
|
|
||||||
const navItems = [
|
export default function LandingPage() {
|
||||||
{ name: 'Home', id: '/' },
|
const displayMetrics = [
|
||||||
{ name: 'Features', id: 'onboarding' },
|
{ id: "1", value: "10.000+", description: "Passos diários rastreados em tempo real com motivação visual de progresso." },
|
||||||
{ name: 'Pricing', id: 'nutrition' },
|
{ id: "2", value: "500 kg", description: "Volume total de peso levantado monitorado com progressão semanal automática." },
|
||||||
{ name: 'Team', id: 'team' },
|
{ id: "3", value: "150+ km", description: "Distância corrida mapeada com GPS, ritmo calculado e calorias precisas." },
|
||||||
{ name: 'Contact', id: 'contact' },
|
{ id: "4", value: "42 dias", description: "Sequência de treinos consistentes com badges de dedicação desbloqueados." }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleCardioInteraction = (data: any) => {
|
||||||
|
console.log('Cardio interaction:', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTrainingInteraction = (data: any) => {
|
||||||
|
console.log('Training interaction:', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkoutMode = (productId: string, productName: string) => {
|
||||||
|
console.log('Workout mode:', productId, productName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNutritionSelect = (planId: string, planName: string) => {
|
||||||
|
console.log('Nutrition select:', planId, planName);
|
||||||
|
};
|
||||||
|
|
||||||
const Page = () => {
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
defaultButtonVariant="text-stagger"
|
defaultButtonVariant="elastic-effect"
|
||||||
defaultTextAnimation="entrance-slide"
|
defaultTextAnimation="entrance-slide"
|
||||||
borderRadius="rounded"
|
borderRadius="pill"
|
||||||
contentWidth="medium"
|
contentWidth="smallMedium"
|
||||||
sizing="medium"
|
sizing="mediumSizeLargeTitles"
|
||||||
background="circleGradient"
|
background="blurBottom"
|
||||||
cardStyle="glass-elevated"
|
cardStyle="gradient-bordered"
|
||||||
primaryButtonStyle="gradient"
|
primaryButtonStyle="flat"
|
||||||
secondaryButtonStyle="glass"
|
secondaryButtonStyle="glass"
|
||||||
headingFontWeight="normal"
|
headingFontWeight="extrabold"
|
||||||
>
|
>
|
||||||
<div id="nav" data-section="nav">
|
<div id="nav" data-section="nav">
|
||||||
<NavbarStyleCentered
|
<NavbarStyleCentered
|
||||||
navItems={navItems}
|
navItems={[
|
||||||
brandName="FitFlow"
|
{ name: "Dashboard", id: "dashboard" },
|
||||||
|
{ name: "Treino", id: "training" },
|
||||||
|
{ name: "Nutrição", id: "nutrition" },
|
||||||
|
{ name: "Comunidade", id: "community" },
|
||||||
|
{ name: "Perfil", id: "profile" }
|
||||||
|
]}
|
||||||
|
button={{ text: "Começar Agora", href: "contact" }}
|
||||||
|
brandName="FitFlow Pro"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hero" data-section="hero">
|
<div id="hero" data-section="hero">
|
||||||
<HeroSplitKpi
|
<HeroSplitKpi
|
||||||
title="Transform Your Fitness Journey"
|
title="Seu Corpo, Seu Histórico, Sua Vitória"
|
||||||
description="Track workouts, monitor nutrition, and achieve your goals with our comprehensive fitness platform."
|
description="Aplicativo premium de fitness com rastreamento biométrico inteligente, treinos personalizados por IA e cardio com GPS. Transforme seu corpo enquanto acompanha cada segundo da jornada."
|
||||||
background={{ variant: 'circleGradient' }}
|
tag="Fitness Ultra-Premium"
|
||||||
tag="Fitness Platform"
|
|
||||||
tagIcon={Zap}
|
tagIcon={Zap}
|
||||||
tagAnimation="slide-up"
|
background={{ variant: "glowing-orb" }}
|
||||||
kpis={[
|
kpis={[
|
||||||
{ value: '500K+', label: 'Active Users' },
|
{ value: "150K+", label: "Usuários Ativos" },
|
||||||
{ value: '1M+', label: 'Workouts Tracked' },
|
{ value: "2M+", label: "Treinos Concluídos" },
|
||||||
{ value: '50+', label: 'Fitness Experts' }
|
{ value: "4.9★", label: "Avaliação" }
|
||||||
]}
|
]}
|
||||||
enableKpiAnimation={true}
|
enableKpiAnimation={true}
|
||||||
buttons={[
|
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/ultra-modern-fitness-app-dashboard-with--1773256981295-f56c580b.png?_wi=1"
|
||||||
{ text: 'Start Free Trial', href: '/signup' },
|
imageAlt="Dashboard de fitness premium"
|
||||||
{ text: 'Learn More', href: '#features' }
|
|
||||||
]}
|
|
||||||
buttonAnimation="slide-up"
|
|
||||||
mediaAnimation="slide-up"
|
|
||||||
imageSrc="/placeholders/placeholder1.webp"
|
|
||||||
imageAlt="Fitness tracking dashboard"
|
|
||||||
imagePosition="right"
|
imagePosition="right"
|
||||||
|
mediaAnimation="slide-up"
|
||||||
|
buttons={[
|
||||||
|
{ text: "Começar Teste Grátis", href: "contact" },
|
||||||
|
{ text: "Ver Demo", href: "#features" }
|
||||||
|
]}
|
||||||
|
avatars={[
|
||||||
|
{ src: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/professional-athlete-portrait-male-fitne-1773256979726-5009f852.png", alt: "Usuário" },
|
||||||
|
{ src: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/fit-female-athlete-portrait-determined-e-1773256980310-c05dce2f.png", alt: "Usuário" },
|
||||||
|
{ src: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/athletic-male-trainer-portrait-confident-1773256979906-c5e05a88.png", alt: "Usuário" }
|
||||||
|
]}
|
||||||
|
avatarText="Join 150K+ athletes"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="onboarding" data-section="onboarding">
|
<div id="onboarding" data-section="onboarding">
|
||||||
<FeatureCardTwentyFive
|
<FeatureCardTwentyFive
|
||||||
title="Get Started in 3 Simple Steps"
|
title="Onboarding Biométrico Dinâmico"
|
||||||
description="Onboard quickly and start your fitness transformation today."
|
description="Sistema inteligente que adapta-se ao seu corpo. Defina seu objetivo, e o app calcula automaticamente sua timeline de sucesso com precisão científica."
|
||||||
tag="Onboarding"
|
tag="Inteligência Adaptativa"
|
||||||
tagIcon={Zap}
|
tagIcon={Brain}
|
||||||
tagAnimation="slide-up"
|
|
||||||
textboxLayout="default"
|
|
||||||
animationType="slide-up"
|
|
||||||
useInvertedBackground={false}
|
|
||||||
features={[
|
features={[
|
||||||
{
|
{
|
||||||
title: 'Create Your Profile',
|
title: "Perfil Biométrico", description: "Coloque seus dados (gênero, altura, peso, idade) e veja o app adaptar todas as demonstrações, avatares e ilustrações anatomicamente.", icon: Target,
|
||||||
description: 'Set up your account with your fitness goals.',
|
|
||||||
icon: Zap,
|
|
||||||
mediaItems: [
|
mediaItems: [
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Profile setup' },
|
{
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Goals selection' }
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/premium-onboarding-screen-for-fitness-ap-1773256981180-774b293c.png", imageAlt: "Tela de biometria"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/ultra-modern-fitness-app-dashboard-with--1773256981295-f56c580b.png?_wi=2", imageAlt: "Dashboard adaptado"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Connect Your Devices',
|
title: "Metas Inteligentes", description: "Escolha: Perder Peso ou Ganhar Massa. O sistema calcula TMB, projeta planilha de metas e mostra exatamente quando você atingirá seu objetivo.", icon: Zap,
|
||||||
description: 'Sync with your fitness trackers and smartwatches.',
|
|
||||||
icon: Dumbbell,
|
|
||||||
mediaItems: [
|
mediaItems: [
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Device sync' },
|
{
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Integration' }
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/nutrition-dashboard-showing-meal-plans-d-1773256981349-9348b6d9.png", imageAlt: "Plano de nutrição"
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/performance-metrics-showcase-displaying--1773256982260-f9a5cff0.png?_wi=1", imageAlt: "Métricas de progresso"
|
||||||
title: 'Start Tracking',
|
}
|
||||||
description: 'Log workouts and monitor your progress in real-time.',
|
|
||||||
icon: TrendingUp,
|
|
||||||
mediaItems: [
|
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Workout tracking' },
|
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Progress dashboard' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
buttons={[
|
animationType="blur-reveal"
|
||||||
{ text: 'Get Started Now', href: '/signup' }
|
textboxLayout="default"
|
||||||
]}
|
useInvertedBackground={false}
|
||||||
buttonAnimation="slide-up"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cardio" data-section="cardio">
|
<div id="cardio" data-section="cardio">
|
||||||
<FeatureCardTwentyFive
|
<FeatureCardTwentyFive
|
||||||
title="Cardio Workouts"
|
title="Hub de Cardio com GPS em Tempo Real"
|
||||||
description="High-intensity interval training and endurance workouts."
|
description="Rastreie suas corridas e caminhadas com precisão de GPS. Monitore pace, distância, calorias queimadas e vença suas metas diárias com anéis de progresso animados."
|
||||||
tag="Cardio"
|
tag="Cardio Premium"
|
||||||
tagIcon={Zap}
|
tagIcon={Heart}
|
||||||
tagAnimation="slide-up"
|
|
||||||
textboxLayout="default"
|
|
||||||
animationType="slide-up"
|
|
||||||
useInvertedBackground={false}
|
|
||||||
features={[
|
features={[
|
||||||
{
|
{
|
||||||
title: 'HIIT Training',
|
title: "Running Tracker Avançado", description: "GPS ativado em tempo real. Mapeia sua rota, calcula queima de calorias baseado no peso, mostra ritmo ao vivo em painel estilo smartwatch.", icon: Zap,
|
||||||
description: 'Burn calories with high-intensity interval training.',
|
|
||||||
icon: Zap,
|
|
||||||
mediaItems: [
|
mediaItems: [
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'HIIT workout' },
|
{
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'HIIT results' }
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/gps-running-tracker-interface-with-real--1773256980694-2abe167e.png", imageAlt: "Rastreamento GPS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/performance-metrics-showcase-displaying--1773256982260-f9a5cff0.png?_wi=2", imageAlt: "Métricas de cardio"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Running Plans',
|
title: "Pedômetro & Caminhada", description: "Contador de passos embutido com rastreamento de distância e calorias. Vença suas metas de 10.000 passos com visual de anéis de progresso motivacional.", icon: Activity,
|
||||||
description: 'Personalized running programs for all levels.',
|
|
||||||
icon: TrendingUp,
|
|
||||||
mediaItems: [
|
mediaItems: [
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Running plan' },
|
{
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Running stats' }
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/ultra-modern-fitness-app-dashboard-with--1773256981295-f56c580b.png?_wi=3", imageAlt: "Dashboard de passos"
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/performance-metrics-showcase-displaying--1773256982260-f9a5cff0.png?_wi=3", imageAlt: "Progresso de atividade"
|
||||||
title: 'Cycling Routes',
|
}
|
||||||
description: 'Discover and track cycling routes in your area.',
|
|
||||||
icon: Dumbbell,
|
|
||||||
mediaItems: [
|
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Cycling route' },
|
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Route map' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
buttons={[
|
animationType="depth-3d"
|
||||||
{ text: 'Explore Cardio', href: '#workouts' }
|
textboxLayout="default"
|
||||||
]}
|
useInvertedBackground={false}
|
||||||
buttonAnimation="slide-up"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="training" data-section="training">
|
<div id="training" data-section="training">
|
||||||
<FeatureCardTwentyFive
|
<FeatureCardTwentyFive
|
||||||
title="Strength Training"
|
title="Core de Treinamento com IA"
|
||||||
description="Build muscle and increase strength with guided workouts."
|
description="Algoritmo adaptativo que recomenda treinos baseado em biometria. Escolha entre Casa (calistenia) ou Ginásio (máquinas/pesos) e isole por grupo muscular com modelo anatômico 3D interativo."
|
||||||
tag="Training"
|
tag="Recomendação de IA"
|
||||||
tagIcon={Dumbbell}
|
tagIcon={Brain}
|
||||||
tagAnimation="slide-up"
|
|
||||||
textboxLayout="default"
|
|
||||||
animationType="slide-up"
|
|
||||||
useInvertedBackground={false}
|
|
||||||
features={[
|
features={[
|
||||||
{
|
{
|
||||||
title: 'Muscle Building',
|
title: "Dualidade de Ambientes", description: "Treinos em Casa (peso corporal, calistenia) ou Ginásio (máquinas, pesos livres). Sistema diferencia automaticamente baseado em sua escolha e disponibilidade de equipamento.", icon: Dumbbell,
|
||||||
description: 'Progressive programs to build and tone muscles.',
|
|
||||||
icon: Dumbbell,
|
|
||||||
mediaItems: [
|
mediaItems: [
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Weight lifting' },
|
{
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Muscle gain' }
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/interactive-anatomical-body-model-showin-1773256980448-3cccd7b3.png?_wi=1", imageAlt: "Seleção de exercícios"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/workout-execution-interface-showing-set--1773256980664-da11c464.png?_wi=1", imageAlt: "Execução de treino"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Form Guide',
|
title: "Isolamento por Grupo Muscular", description: "Modelo anatômico 3D/2D interativo. Clique em um músculo (peitoral, glúteo, etc.) e veja lista filtrada de exercícios com foco nesse músculo específico.", icon: Zap,
|
||||||
description: 'Video tutorials and form checks for exercises.',
|
|
||||||
icon: Zap,
|
|
||||||
mediaItems: [
|
mediaItems: [
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Exercise form' },
|
{
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Video guide' }
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/interactive-anatomical-body-model-showin-1773256980448-3cccd7b3.png?_wi=2", imageAlt: "Anatomia interativa"
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/workout-execution-interface-showing-set--1773256980664-da11c464.png?_wi=2", imageAlt: "Exercícios filtrados"
|
||||||
title: 'Recovery Tips',
|
}
|
||||||
description: 'Expert advice on rest days and recovery techniques.',
|
|
||||||
icon: Apple,
|
|
||||||
mediaItems: [
|
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Recovery' },
|
|
||||||
{ type: 'image', src: '/placeholders/placeholder1.webp', alt: 'Rest day' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
buttons={[
|
animationType="scale-rotate"
|
||||||
{ text: 'View Training Plans', href: '#plans' }
|
textboxLayout="default"
|
||||||
]}
|
useInvertedBackground={false}
|
||||||
buttonAnimation="slide-up"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="workout-mode" data-section="workout-mode">
|
<div id="workout-mode" data-section="workout-mode">
|
||||||
<ProductCardOne
|
<ProductCardOne
|
||||||
title="Workout Modes"
|
title="Modo 'Em Execução' - Tracking de Série"
|
||||||
description="Choose from various workout modes to suit your fitness level."
|
description="Interface imersiva que transforma o treino em experiência interativa com cronômetro de descanso, registro de progresso e gráficos de progressão."
|
||||||
tag="Workouts"
|
tag="Focus Mode"
|
||||||
tagIcon={Zap}
|
tagIcon={Target}
|
||||||
tagAnimation="slide-up"
|
|
||||||
textboxLayout="default"
|
|
||||||
gridVariant="four-items-2x2-equal-grid"
|
|
||||||
animationType="slide-up"
|
|
||||||
useInvertedBackground={false}
|
|
||||||
products={[
|
products={[
|
||||||
{
|
{
|
||||||
id: '1',
|
id: "1", name: "Iniciar Série", price: "Botão Proeminente", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/workout-execution-interface-showing-set--1773256980664-da11c464.png?_wi=3", imageAlt: "Tela de série", onProductClick: () => handleWorkoutMode("1", "Iniciar Série")
|
||||||
name: 'Beginner Workouts',
|
|
||||||
price: 'Free',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Beginner workout'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: "2", name: "Cronômetro de Descanso", price: "Automático", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/workout-execution-interface-showing-set--1773256980664-da11c464.png?_wi=4", imageAlt: "Descanso entre séries", onProductClick: () => handleWorkoutMode("2", "Cronômetro de Descanso")
|
||||||
name: 'Intermediate Training',
|
|
||||||
price: '$9.99/mo',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Intermediate workout'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: "3", name: "Registrar Progresso", price: "Carga + Reps", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/performance-metrics-showcase-displaying--1773256982260-f9a5cff0.png?_wi=4", imageAlt: "Progresso salvo", onProductClick: () => handleWorkoutMode("3", "Registrar Progresso")
|
||||||
name: 'Advanced Programs',
|
|
||||||
price: '$19.99/mo',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Advanced workout'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Expert Coaching',
|
|
||||||
price: '$49.99/mo',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Expert coaching'
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
buttons={[
|
animationType="slide-up"
|
||||||
{ text: 'View All Modes', href: '#modes' }
|
textboxLayout="default"
|
||||||
]}
|
useInvertedBackground={false}
|
||||||
buttonAnimation="slide-up"
|
gridVariant="three-columns-all-equal-width"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="nutrition" data-section="nutrition">
|
<div id="nutrition" data-section="nutrition">
|
||||||
<PricingCardOne
|
<PricingCardOne
|
||||||
title="Nutrition Plans"
|
title="Nutrição Inteligente & Receituário"
|
||||||
description="Personalized meal plans tailored to your fitness goals."
|
description="Planos alimentares 100% sincronizados com suas metas de peso. Se quer perder peso, déficit calórico. Se quer ganhar massa, superávit inteligente."
|
||||||
tag="Nutrition"
|
tag="Sincronizado com Timeline"
|
||||||
tagIcon={Apple}
|
tagIcon={Apple}
|
||||||
tagAnimation="slide-up"
|
|
||||||
textboxLayout="default"
|
|
||||||
animationType="slide-up"
|
|
||||||
useInvertedBackground={false}
|
|
||||||
plans={[
|
plans={[
|
||||||
{
|
{
|
||||||
id: 'basic',
|
id: "deficit", badge: "Perda de Peso", badgeIcon: TrendingDown,
|
||||||
badge: 'Basic',
|
price: "Déficit Calórico", subtitle: "Receitas otimizadas para queima de calorias", features: [
|
||||||
price: '$9.99',
|
"Macros calculados automaticamente", "Prep time entre 15-30 min", "Proteína alta, carboidrato estratégico", "Rastreamento integrado", "Sugestões diárias personalizadas"
|
||||||
subtitle: 'per month',
|
]
|
||||||
features: ['Basic meal plans', 'Calorie tracking', 'Recipe suggestions']
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pro',
|
id: "surplus", badge: "Ganho de Massa", badgeIcon: TrendingUp,
|
||||||
badge: 'Pro',
|
price: "Superávit Estratégico", subtitle: "Nutrição para crescimento muscular", features: [
|
||||||
badgeIcon: Zap,
|
"Calorias calculadas para ganho", "Proteína máxima (2g por kg)", "Carbos pré e pós-treino", "Tempo de preparo eficiente", "Sincronizado com treino de força"
|
||||||
price: '$29.99',
|
]
|
||||||
subtitle: 'per month',
|
|
||||||
features: ['Advanced meal plans', 'Macro tracking', 'Expert guidance', 'Weekly recipes']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'elite',
|
|
||||||
badge: 'Elite',
|
|
||||||
price: '$59.99',
|
|
||||||
subtitle: 'per month',
|
|
||||||
features: ['Custom meal plans', 'Personal nutritionist', '24/7 support', 'Supplement guide']
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
buttons={[
|
animationType="slide-up"
|
||||||
{ text: 'Choose Plan', href: '/pricing' }
|
textboxLayout="default"
|
||||||
]}
|
useInvertedBackground={false}
|
||||||
buttonAnimation="slide-up"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="metrics" data-section="metrics">
|
<div id="metrics" data-section="metrics">
|
||||||
<MetricCardFourteen
|
<MetricCardFourteen
|
||||||
title="Your Progress"
|
title="Suas Conquistas Importam. Veja Cada Número."
|
||||||
tag="Metrics"
|
tag="Performance Metrics"
|
||||||
tagAnimation="slide-up"
|
tagAnimation="slide-up"
|
||||||
metrics={[
|
metrics={displayMetrics}
|
||||||
{ id: '1', value: '2,450', description: 'Calories Burned' },
|
|
||||||
{ id: '2', value: '45', description: 'Workouts Completed' },
|
|
||||||
{ id: '3', value: '12', description: 'Pounds Lost' },
|
|
||||||
{ id: '4', value: '92%', description: 'Goal Achievement' }
|
|
||||||
]}
|
|
||||||
metricsAnimation="slide-up"
|
metricsAnimation="slide-up"
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
/>
|
/>
|
||||||
@@ -315,49 +266,28 @@ const Page = () => {
|
|||||||
|
|
||||||
<div id="team" data-section="team">
|
<div id="team" data-section="team">
|
||||||
<TeamCardOne
|
<TeamCardOne
|
||||||
title="Meet Our Fitness Experts"
|
title="Comunidade de Atletas Profissionais"
|
||||||
description="Learn from certified trainers and nutritionists."
|
description="Feed social onde você compartilha rotas de corrida, estatísticas de treino e compete em rankings semanais com outros usuários."
|
||||||
tag="Team"
|
tag="Gamificação Social"
|
||||||
tagIcon={Zap}
|
tagIcon={Users}
|
||||||
tagAnimation="slide-up"
|
|
||||||
textboxLayout="default"
|
|
||||||
gridVariant="four-items-2x2-equal-grid"
|
|
||||||
animationType="slide-up"
|
|
||||||
useInvertedBackground={false}
|
|
||||||
members={[
|
members={[
|
||||||
{
|
{
|
||||||
id: '1',
|
id: "1", name: "Marcus A.", role: "Elite Runner", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/professional-athlete-portrait-male-fitne-1773256979726-5009f852.png?_wi=1", imageAlt: "Marcus A."
|
||||||
name: 'Sarah Johnson',
|
|
||||||
role: 'Certified Personal Trainer',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Sarah Johnson'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: "2", name: "Ana L.", role: "Strength Coach", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/fit-female-athlete-portrait-determined-e-1773256980310-c05dce2f.png?_wi=1", imageAlt: "Ana L."
|
||||||
name: 'Mike Chen',
|
|
||||||
role: 'Nutrition Specialist',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Mike Chen'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: "3", name: "Rafael S.", role: "Nutrition Expert", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/athletic-male-trainer-portrait-confident-1773256979906-c5e05a88.png?_wi=1", imageAlt: "Rafael S."
|
||||||
name: 'Emma Davis',
|
|
||||||
role: 'Yoga Instructor',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Emma Davis'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: "4", name: "Sofia M.", role: "Fitness Trainer", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/female-fitness-coach-portrait-profession-1773256979710-97e8b5fe.png?_wi=1", imageAlt: "Sofia M."
|
||||||
name: 'James Wilson',
|
|
||||||
role: 'Strength Coach',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'James Wilson'
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
buttons={[
|
animationType="blur-reveal"
|
||||||
{ text: 'Meet the Team', href: '#experts' }
|
textboxLayout="default"
|
||||||
]}
|
useInvertedBackground={false}
|
||||||
buttonAnimation="slide-up"
|
gridVariant="four-items-2x2-equal-grid"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -365,98 +295,99 @@ const Page = () => {
|
|||||||
<TestimonialCardTwelve
|
<TestimonialCardTwelve
|
||||||
testimonials={[
|
testimonials={[
|
||||||
{
|
{
|
||||||
id: '1',
|
id: "1", name: "Carlos M.", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/professional-athlete-portrait-male-fitne-1773256979726-5009f852.png?_wi=2", imageAlt: "Carlos M."
|
||||||
name: 'Alex Rodriguez',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Alex Rodriguez'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: "2", name: "Beatriz R.", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/fit-female-athlete-portrait-determined-e-1773256980310-c05dce2f.png?_wi=2", imageAlt: "Beatriz R."
|
||||||
name: 'Lisa Chen',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Lisa Chen'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: "3", name: "Diego P.", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/athletic-male-trainer-portrait-confident-1773256979906-c5e05a88.png?_wi=2", imageAlt: "Diego P."
|
||||||
name: 'Michael Brown',
|
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
|
||||||
imageAlt: 'Michael Brown'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: "4", name: "Juliana T.", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/female-fitness-coach-portrait-profession-1773256979710-97e8b5fe.png?_wi=2", imageAlt: "Juliana T."
|
||||||
name: 'Jessica Taylor',
|
},
|
||||||
imageSrc: '/placeholders/placeholder1.webp',
|
{
|
||||||
imageAlt: 'Jessica Taylor'
|
id: "5", name: "Lucas F.", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/young-athlete-male-portrait-energetic-ex-1773256982698-63e4e494.png", imageAlt: "Lucas F."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6", name: "Marina S.", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoRNSPr0mCBj85JKsHl7qxTHsl/female-athlete-portrait-fit-build-profes-1773256980134-0faaa8fa.png", imageAlt: "Marina S."
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
cardTitle="Trusted by 500k+ users worldwide"
|
cardTitle="Mais de 150 mil atletas já transformaram seu corpo com FitFlow Pro"
|
||||||
cardTag="Success Stories"
|
cardTag="Veja o que eles dizem"
|
||||||
cardTagIcon={Zap}
|
cardTagIcon={Star}
|
||||||
cardAnimation="slide-up"
|
cardAnimation="blur-reveal"
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="social-proof" data-section="social-proof">
|
<div id="social-proof" data-section="social-proof">
|
||||||
<SocialProofOne
|
<SocialProofOne
|
||||||
title="Featured In"
|
title="Confiado pelos Maiores Aplicativos e Plataformas de Fitness"
|
||||||
description="Recognized by leading fitness and wellness publications."
|
description="FitFlow Pro integra-se perfeitamente com os ecossistemas de saúde mais populares do mundo."
|
||||||
tag="Social Proof"
|
tag="Parcerias Premium"
|
||||||
tagIcon={Zap}
|
tagIcon={Zap}
|
||||||
tagAnimation="slide-up"
|
logos={[
|
||||||
|
"http://img.b2bpic.net/free-vector/heart-love-logo_126523-2763.jpg", "http://img.b2bpic.net/free-photo/woman-with-phone-sportswear_1303-8805.jpg", "http://img.b2bpic.net/free-vector/flat-design-fitness-trackers-heart-rate-menu_23-2148515781.jpg", "http://img.b2bpic.net/free-photo/close-up-woman-holding-phone_23-2148889655.jpg", "http://img.b2bpic.net/free-photo/close-up-man-using-phone-while-electric-bike_23-2149098678.jpg", "http://img.b2bpic.net/free-vector/fitness-trackers-concept_23-2148527033.jpg", "http://img.b2bpic.net/free-photo/cropped-view-sexy-bodybuilder-showing-thumbs-up_1153-6281.jpg", "http://img.b2bpic.net/free-photo/phone-muscular-hand-guy-sitting-city-morning-he-holds-bottle-water-headphones_197531-1155.jpg"
|
||||||
|
]}
|
||||||
|
names={[
|
||||||
|
"Apple Fitness", "Strava", "Fitbit", "Google Fit", "Peloton", "MyFitnessPal", "Garmin", "Nike Training"
|
||||||
|
]}
|
||||||
textboxLayout="default"
|
textboxLayout="default"
|
||||||
useInvertedBackground={false}
|
useInvertedBackground={false}
|
||||||
names={['Fitness Magazine', 'Wellness Today', 'Health News', 'Trainer Weekly', 'Sports Life']}
|
speed={40}
|
||||||
buttons={[
|
showCard={true}
|
||||||
{ text: 'Read Our Press', href: '#press' }
|
|
||||||
]}
|
|
||||||
buttonAnimation="slide-up"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="contact" data-section="contact">
|
<div id="contact" data-section="contact">
|
||||||
<ContactText
|
<ContactText
|
||||||
text="Ready to transform your fitness journey? Start your free trial today and unlock your potential."
|
text="Pronto para transformar seu corpo? Começar seu teste gratuito de 30 dias agora e veja por que 150 mil atletas confiam em FitFlow Pro."
|
||||||
animationType="entrance-slide"
|
animationType="entrance-slide"
|
||||||
background={{ variant: 'sparkles-gradient' }}
|
|
||||||
useInvertedBackground={false}
|
|
||||||
buttons={[
|
buttons={[
|
||||||
{ text: 'Start Free Trial', href: '/signup' },
|
{ text: "Começar Teste Grátis", href: "contact" },
|
||||||
{ text: 'Contact Us', href: 'mailto:info@fitflow.com' }
|
{ text: "Conversar com Especialista", href: "#contact" }
|
||||||
]}
|
]}
|
||||||
|
background={{ variant: "sparkles-gradient" }}
|
||||||
|
useInvertedBackground={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="footer" data-section="footer">
|
<div id="footer" data-section="footer">
|
||||||
<FooterLogoEmphasis
|
<FooterLogoEmphasis
|
||||||
logoText="FitFlow"
|
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
items: [
|
items: [
|
||||||
{ label: 'About', href: '#about' },
|
{ label: "Dashboard", href: "#dashboard" },
|
||||||
{ label: 'Blog', href: '#blog' },
|
{ label: "Treino", href: "#training" },
|
||||||
{ label: 'Careers', href: '#careers' }
|
{ label: "Nutrição", href: "#nutrition" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Privacy', href: '#privacy' },
|
{ label: "Cardio Hub", href: "#cardio" },
|
||||||
{ label: 'Terms', href: '#terms' },
|
{ label: "Comunidade", href: "#community" },
|
||||||
{ label: 'Contact', href: '#contact' }
|
{ label: "Perfil", href: "#profile" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Twitter', href: 'https://twitter.com' },
|
{ label: "Blog", href: "#blog" },
|
||||||
{ label: 'Instagram', href: 'https://instagram.com' },
|
{ label: "Ajuda", href: "#help" },
|
||||||
{ label: 'LinkedIn', href: 'https://linkedin.com' }
|
{ label: "Suporte", href: "#support" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Privacidade", href: "#privacy" },
|
||||||
|
{ label: "Termos", href: "#terms" },
|
||||||
|
{ label: "Contato", href: "#contact" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
logoText="FitFlow Pro"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -2,61 +2,71 @@
|
|||||||
|
|
||||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||||
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered';
|
||||||
|
import ContactCTA from '@/components/sections/contact/ContactCTA';
|
||||||
|
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
|
||||||
|
import { Mail, Lock, User, AlertCircle, Check, ArrowRight } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Mail, Lock, User, Eye, EyeOff, ArrowRight, CheckCircle } from 'lucide-react';
|
|
||||||
|
interface FormErrors {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignupFormData {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
agreedToTerms: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SignupPage() {
|
export default function SignupPage() {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<SignupFormData>({
|
||||||
fullName: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: ''
|
confirmPassword: '',
|
||||||
|
agreedToTerms: false
|
||||||
});
|
});
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [errors, setErrors] = useState<FormErrors>({});
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [passwordStrength, setPasswordStrength] = useState(0);
|
const [passwordStrength, setPasswordStrength] = useState(0);
|
||||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
|
||||||
|
|
||||||
const calculatePasswordStrength = (pwd: string) => {
|
const calculatePasswordStrength = (password: string) => {
|
||||||
let strength = 0;
|
let strength = 0;
|
||||||
if (pwd.length >= 8) strength++;
|
if (password.length >= 8) strength++;
|
||||||
if (pwd.match(/[a-z]/) && pwd.match(/[A-Z]/)) strength++;
|
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
||||||
if (pwd.match(/[0-9]/)) strength++;
|
if (/\d/.test(password)) strength++;
|
||||||
if (pwd.match(/[^a-zA-Z0-9]/)) strength++;
|
if (/[^a-zA-Z\d]/.test(password)) strength++;
|
||||||
setPasswordStrength(strength);
|
setPasswordStrength(strength);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: FormErrors = {};
|
||||||
|
|
||||||
if (!formData.fullName.trim()) {
|
if (!formData.name.trim()) {
|
||||||
newErrors.fullName = 'Full name is required';
|
newErrors.name = 'Nome é obrigatório';
|
||||||
|
} else if (formData.name.length < 2) {
|
||||||
|
newErrors.name = 'Nome deve ter pelo menos 2 caracteres';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.email) {
|
if (!formData.email) {
|
||||||
newErrors.email = 'Email is required';
|
newErrors.email = 'Email é obrigatório';
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||||
newErrors.email = 'Please enter a valid email';
|
newErrors.email = 'Email inválido';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.password) {
|
if (!formData.password) {
|
||||||
newErrors.password = 'Password is required';
|
newErrors.password = 'Senha é obrigatória';
|
||||||
} else if (formData.password.length < 8) {
|
} else if (formData.password.length < 8) {
|
||||||
newErrors.password = 'Password must be at least 8 characters';
|
newErrors.password = 'Senha deve ter pelo menos 8 caracteres';
|
||||||
} else if (passwordStrength < 2) {
|
|
||||||
newErrors.password = 'Password is too weak';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.confirmPassword) {
|
if (formData.password !== formData.confirmPassword) {
|
||||||
newErrors.confirmPassword = 'Please confirm your password';
|
newErrors.confirmPassword = 'As senhas não correspondem';
|
||||||
} else if (formData.password !== formData.confirmPassword) {
|
|
||||||
newErrors.confirmPassword = 'Passwords do not match';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agreedToTerms) {
|
if (!formData.agreedToTerms) {
|
||||||
newErrors.terms = 'You must agree to the terms';
|
newErrors.agreedToTerms = 'Você deve aceitar os termos e condições';
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
@@ -64,15 +74,27 @@ export default function SignupPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value, type, checked } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
const newValue = type === 'checkbox' ? checked : value;
|
||||||
|
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: newValue
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'password') {
|
if (name === 'password') {
|
||||||
calculatePasswordStrength(value);
|
calculatePasswordStrength(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
@@ -81,28 +103,38 @@ export default function SignupPage() {
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// Simulated API call
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
console.log('Signup attempt:', formData);
|
||||||
console.log('Signup attempt:', { ...formData, confirmPassword: undefined });
|
alert('Conta criada com sucesso! Faça login para continuar.');
|
||||||
// In a real app, you would create account and redirect
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
agreedToTerms: false
|
||||||
|
});
|
||||||
|
setPasswordStrength(0);
|
||||||
|
} catch (error) {
|
||||||
|
setErrors({ submit: 'Erro ao criar conta. Tente novamente.' });
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPasswordStrengthColor = () => {
|
const getPasswordStrengthColor = () => {
|
||||||
if (passwordStrength <= 1) return '#ef4444';
|
if (passwordStrength === 0) return 'bg-gray-300';
|
||||||
if (passwordStrength <= 2) return '#eab308';
|
if (passwordStrength === 1) return 'bg-red-500';
|
||||||
if (passwordStrength <= 3) return '#f97316';
|
if (passwordStrength === 2) return 'bg-yellow-500';
|
||||||
return '#22c55e';
|
if (passwordStrength === 3) return 'bg-blue-500';
|
||||||
|
return 'bg-green-500';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPasswordStrengthText = () => {
|
const getPasswordStrengthLabel = () => {
|
||||||
if (!formData.password) return '';
|
if (passwordStrength === 0) return '';
|
||||||
if (passwordStrength <= 1) return 'Weak';
|
if (passwordStrength === 1) return 'Fraca';
|
||||||
if (passwordStrength <= 2) return 'Fair';
|
if (passwordStrength === 2) return 'Média';
|
||||||
if (passwordStrength <= 3) return 'Good';
|
if (passwordStrength === 3) return 'Forte';
|
||||||
return 'Strong';
|
return 'Muito Forte';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -122,283 +154,275 @@ export default function SignupPage() {
|
|||||||
<NavbarStyleCentered
|
<NavbarStyleCentered
|
||||||
navItems={[
|
navItems={[
|
||||||
{ name: "Dashboard", id: "/" },
|
{ name: "Dashboard", id: "/" },
|
||||||
{ name: "Treino", id: "#training" },
|
{ name: "Sobre", id: "#hero" },
|
||||||
{ name: "Nutrição", id: "#nutrition" },
|
{ name: "Features", id: "#onboarding" },
|
||||||
{ name: "Comunidade", id: "#community" },
|
{ name: "Contato", id: "#contact" },
|
||||||
{ name: "Perfil", id: "#profile" }
|
{ name: "Login", id: "/login" }
|
||||||
]}
|
]}
|
||||||
button={{ text: "Entrar", href: "/login" }}
|
button={{ text: "Criar Conta", href: "/signup" }}
|
||||||
brandName="FitFlow Pro"
|
brandName="FitFlow Pro"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="signup" data-section="signup" className="min-h-screen flex items-center justify-center px-4 py-20">
|
<div id="signup" data-section="signup" className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div className="rounded-2xl border border-opacity-20 p-8 backdrop-blur-sm" style={{
|
<div className="text-center space-y-3">
|
||||||
backgroundColor: 'var(--color-card)',
|
<h1 className="text-4xl font-extrabold tracking-tight">Comece sua jornada</h1>
|
||||||
borderColor: 'var(--color-foreground)'
|
<p className="text-base text-gray-600 dark:text-gray-400">Crie sua conta FitFlow Pro e transforme seu corpo</p>
|
||||||
}}>
|
</div>
|
||||||
{/* 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 */}
|
<div className="bg-card rounded-2xl border border-primary-cta/20 p-8 shadow-lg">
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
{/* Full Name Field */}
|
{/* Name Field */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="fullName" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
<label htmlFor="name" className="block text-sm font-medium text-foreground">
|
||||||
Full Name
|
Nome Completo
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<User className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
<User className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
id="fullName"
|
id="name"
|
||||||
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
name="fullName"
|
value={formData.name}
|
||||||
value={formData.fullName}
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Your name"
|
placeholder="Seu nome"
|
||||||
className="w-full pl-10 pr-4 py-2.5 rounded-lg border transition-all"
|
className={`w-full pl-10 pr-4 py-2.5 bg-background border rounded-lg focus:outline-none focus:ring-2 transition-all ${
|
||||||
style={{
|
errors.name
|
||||||
backgroundColor: 'var(--color-background)',
|
? 'border-red-500 focus:ring-red-500'
|
||||||
borderColor: errors.fullName ? '#ef4444' : 'var(--color-primary-cta)',
|
: 'border-gray-300 focus:ring-primary-cta'
|
||||||
color: 'var(--color-foreground)',
|
}`}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.fullName && (
|
{errors.name && (
|
||||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||||
{errors.fullName}
|
<AlertCircle className="w-4 h-4" />
|
||||||
</p>
|
{errors.name}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="email" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||||
Email Address
|
Email
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Mail className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
<Mail className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
|
||||||
name="email"
|
name="email"
|
||||||
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="you@example.com"
|
placeholder="seu@email.com"
|
||||||
className="w-full pl-10 pr-4 py-2.5 rounded-lg border transition-all"
|
className={`w-full pl-10 pr-4 py-2.5 bg-background border rounded-lg focus:outline-none focus:ring-2 transition-all ${
|
||||||
style={{
|
errors.email
|
||||||
backgroundColor: 'var(--color-background)',
|
? 'border-red-500 focus:ring-red-500'
|
||||||
borderColor: errors.email ? '#ef4444' : 'var(--color-primary-cta)',
|
: 'border-gray-300 focus:ring-primary-cta'
|
||||||
color: 'var(--color-foreground)',
|
}`}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
{errors.email}
|
{errors.email}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password Field */}
|
{/* Password Field */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="password" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||||
Password
|
Senha
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
<Lock className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
name="password"
|
name="password"
|
||||||
|
type="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••••"
|
||||||
className="w-full pl-10 pr-10 py-2.5 rounded-lg border transition-all"
|
className={`w-full pl-10 pr-4 py-2.5 bg-background border rounded-lg focus:outline-none focus:ring-2 transition-all ${
|
||||||
style={{
|
errors.password
|
||||||
backgroundColor: 'var(--color-background)',
|
? 'border-red-500 focus:ring-red-500'
|
||||||
borderColor: errors.password ? '#ef4444' : 'var(--color-primary-cta)',
|
: 'border-gray-300 focus:ring-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>
|
</div>
|
||||||
|
|
||||||
{/* Password Strength Indicator */}
|
|
||||||
{formData.password && (
|
{formData.password && (
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="space-y-2">
|
||||||
<div className="flex-1 h-1.5 rounded-full bg-gray-300" style={{ background: 'var(--color-background)' }}>
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all"
|
className={`h-full transition-all ${getPasswordStrengthColor()}`}
|
||||||
style={{
|
style={{ width: `${(passwordStrength / 4) * 100}%` }}
|
||||||
width: `${(passwordStrength / 4) * 100}%`,
|
></div>
|
||||||
backgroundColor: getPasswordStrengthColor(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium" style={{ color: getPasswordStrengthColor() }}>
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
{getPasswordStrengthText()}
|
Força: <span className="font-semibold">{getPasswordStrengthLabel()}</span>
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{errors.password && (
|
{errors.password && (
|
||||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
{errors.password}
|
{errors.password}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Confirm Password Field */}
|
{/* Confirm Password Field */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<label htmlFor="confirmPassword" className="block text-sm font-medium mb-2" style={{ color: 'var(--color-foreground)' }}>
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
|
||||||
Confirm Password
|
Confirmar Senha
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock className="absolute left-3 top-3.5 w-5 h-5" style={{ color: 'var(--color-foreground)', opacity: 0.5 }} />
|
<Lock className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
value={formData.confirmPassword}
|
value={formData.confirmPassword}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••••"
|
||||||
className="w-full pl-10 pr-10 py-2.5 rounded-lg border transition-all"
|
className={`w-full pl-10 pr-4 py-2.5 bg-background border rounded-lg focus:outline-none focus:ring-2 transition-all ${
|
||||||
style={{
|
errors.confirmPassword
|
||||||
backgroundColor: 'var(--color-background)',
|
? 'border-red-500 focus:ring-red-500'
|
||||||
borderColor: errors.confirmPassword ? '#ef4444' : 'var(--color-primary-cta)',
|
: 'border-gray-300 focus:ring-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>
|
</div>
|
||||||
|
{formData.confirmPassword && formData.password === formData.confirmPassword && !errors.confirmPassword && (
|
||||||
|
<div className="flex items-center gap-2 text-green-500 text-sm">
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
Senhas correspondem
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{errors.confirmPassword && (
|
{errors.confirmPassword && (
|
||||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
{errors.confirmPassword}
|
{errors.confirmPassword}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Terms Agreement */}
|
{/* Terms Checkbox */}
|
||||||
<div className="pt-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-start gap-3 cursor-pointer">
|
<div className="flex items-start gap-3">
|
||||||
<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
|
<input
|
||||||
|
id="agreedToTerms"
|
||||||
|
name="agreedToTerms"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={agreedToTerms}
|
checked={formData.agreedToTerms}
|
||||||
onChange={(e) => setAgreedToTerms(e.target.checked)}
|
onChange={handleChange}
|
||||||
className="hidden"
|
className="w-5 h-5 mt-0.5 rounded border-gray-300 accent-primary-cta cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</label>
|
<label htmlFor="agreedToTerms" className="text-sm text-gray-600 dark:text-gray-400 cursor-pointer">
|
||||||
{errors.terms && (
|
Concordo com os{' '}
|
||||||
<p className="mt-1 text-sm font-medium" style={{ color: '#ef4444' }}>
|
<a href="#" className="text-primary-cta hover:underline font-semibold">
|
||||||
{errors.terms}
|
termos de serviço
|
||||||
</p>
|
</a>
|
||||||
|
{' '}e{' '}
|
||||||
|
<a href="#" className="text-primary-cta hover:underline font-semibold">
|
||||||
|
política de privacidade
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{errors.agreedToTerms && (
|
||||||
|
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
{errors.agreedToTerms}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Errors */}
|
||||||
|
{errors.submit && (
|
||||||
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 flex items-center gap-2 text-red-700 dark:text-red-400 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
|
{errors.submit}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
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"
|
className="w-full bg-primary-cta hover:bg-primary-cta/90 text-white font-semibold py-2.5 rounded-lg transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
style={{
|
|
||||||
backgroundColor: 'var(--color-primary-cta)',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Creating Account...' : 'Create Account'}
|
{isSubmitting ? 'Criando conta...' : 'Criar Conta'}
|
||||||
{!isSubmitting && <ArrowRight className="w-4 h-4" />}
|
{!isSubmitting && <ArrowRight className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="my-6 flex items-center gap-4">
|
<div className="mt-6 relative">
|
||||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
<div className="absolute inset-0 flex items-center">
|
||||||
<span className="text-xs" style={{ color: 'var(--color-foreground)', opacity: 0.5 }}>OR</span>
|
<div className="w-full border-t border-gray-300 dark:border-gray-600"></div>
|
||||||
<div className="flex-1 h-px" style={{ backgroundColor: 'var(--color-foreground)', opacity: 0.1 }} />
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-card text-gray-500">ou continue com</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Social Signup */}
|
{/* Social Signup Buttons */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="mt-6 space-y-3">
|
||||||
<button
|
<button className="w-full bg-background hover:bg-gray-50 dark:hover:bg-gray-800 border border-gray-300 dark:border-gray-600 text-foreground font-medium py-2.5 rounded-lg transition-all">
|
||||||
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
|
Google
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="w-full bg-background hover:bg-gray-50 dark:hover:bg-gray-800 border border-gray-300 dark:border-gray-600 text-foreground font-medium py-2.5 rounded-lg transition-all">
|
||||||
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
|
Apple
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
{/* Login Link */}
|
||||||
|
<p className="text-center text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
Já tem conta?{' '}
|
||||||
|
<a href="/login" className="text-primary-cta hover:text-primary-cta/80 font-semibold transition-colors">
|
||||||
|
Fazer login
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="footer" data-section="footer">
|
||||||
|
<FooterLogoEmphasis
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: "Sobre", href: "/#hero" },
|
||||||
|
{ label: "Features", href: "/#onboarding" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Blog", href: "#" },
|
||||||
|
{ label: "Ajuda", href: "#" },
|
||||||
|
{ label: "Suporte", href: "#" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Privacidade", href: "#" },
|
||||||
|
{ label: "Termos", href: "#" },
|
||||||
|
{ label: "Contato", href: "#" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ label: "Login", href: "/login" },
|
||||||
|
{ label: "Signup", href: "/signup" },
|
||||||
|
{ label: "Dashboard", href: "/" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
logoText="FitFlow Pro"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import FeatureCardTwentyFive from './FeatureCardTwentyFive';
|
||||||
|
|
||||||
|
interface FeatureCardTwentyFiveWithSavingProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tag?: string;
|
||||||
|
tagIcon?: any;
|
||||||
|
features: Array<{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: any;
|
||||||
|
mediaItems: Array<{ imageSrc: string; imageAlt: string }>;
|
||||||
|
}>;
|
||||||
|
animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal' | 'depth-3d';
|
||||||
|
textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
|
||||||
|
useInvertedBackground: boolean;
|
||||||
|
onFeatureInteraction?: (featureData: any) => void;
|
||||||
|
workoutType?: 'cardio' | 'training';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeatureCardTwentyFiveWithSaving({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
tag,
|
||||||
|
tagIcon,
|
||||||
|
features,
|
||||||
|
animationType,
|
||||||
|
textboxLayout,
|
||||||
|
useInvertedBackground,
|
||||||
|
onFeatureInteraction,
|
||||||
|
workoutType,
|
||||||
|
}: FeatureCardTwentyFiveWithSavingProps) {
|
||||||
|
const handleFeatureInteraction = (featureIndex: number, data?: any) => {
|
||||||
|
onFeatureInteraction?.(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureCardTwentyFive
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
tag={tag}
|
||||||
|
tagIcon={tagIcon}
|
||||||
|
features={features}
|
||||||
|
animationType={animationType}
|
||||||
|
textboxLayout={textboxLayout}
|
||||||
|
useInvertedBackground={useInvertedBackground}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FeatureCardTwentyFiveWithSaving;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useWorkoutStorage } from '@/hooks/useWorkoutStorage';
|
||||||
|
import MetricCardFourteen from './MetricCardFourteen';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface MetricCardFourteenWithSavingProps {
|
||||||
|
title: string;
|
||||||
|
tag: string;
|
||||||
|
tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
|
||||||
|
metrics: Array<{
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
metricsAnimation: 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
|
||||||
|
useInvertedBackground: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricCardFourteenWithSaving({
|
||||||
|
title,
|
||||||
|
tag,
|
||||||
|
tagAnimation,
|
||||||
|
metrics,
|
||||||
|
metricsAnimation,
|
||||||
|
useInvertedBackground,
|
||||||
|
}: MetricCardFourteenWithSavingProps) {
|
||||||
|
const { metrics: userMetrics, updateMetrics } = useWorkoutStorage();
|
||||||
|
const [displayMetrics, setDisplayMetrics] = useState(metrics);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userMetrics) {
|
||||||
|
// Dynamically update metrics based on saved data
|
||||||
|
const updatedMetrics = metrics.map(metric => {
|
||||||
|
let value = metric.value;
|
||||||
|
|
||||||
|
if (metric.id === '1' && userMetrics.totalDistance > 0) {
|
||||||
|
// Update steps/distance metric
|
||||||
|
value = `${Math.round(userMetrics.totalDistance).toLocaleString()}+`;
|
||||||
|
} else if (metric.id === '2' && userMetrics.totalWeight > 0) {
|
||||||
|
// Update volume metric
|
||||||
|
value = `${Math.round(userMetrics.totalWeight)} kg`;
|
||||||
|
} else if (metric.id === '3' && userMetrics.totalDistance > 0) {
|
||||||
|
// Update distance metric
|
||||||
|
value = `${Math.round(userMetrics.totalDistance)}+ km`;
|
||||||
|
} else if (metric.id === '4' && userMetrics.consistency > 0) {
|
||||||
|
// Update consistency metric
|
||||||
|
value = `${userMetrics.consistency} days`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...metric, value };
|
||||||
|
});
|
||||||
|
|
||||||
|
setDisplayMetrics(updatedMetrics);
|
||||||
|
}
|
||||||
|
}, [userMetrics, metrics]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MetricCardFourteen
|
||||||
|
title={title}
|
||||||
|
tag={tag}
|
||||||
|
tagAnimation={tagAnimation}
|
||||||
|
metrics={displayMetrics}
|
||||||
|
metricsAnimation={metricsAnimation}
|
||||||
|
useInvertedBackground={useInvertedBackground}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetricCardFourteenWithSaving;
|
||||||
68
src/components/sections/pricing/PricingCardOneWithSaving.tsx
Normal file
68
src/components/sections/pricing/PricingCardOneWithSaving.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import PricingCardOne from './PricingCardOne';
|
||||||
|
|
||||||
|
interface NutritionPlan {
|
||||||
|
id: string;
|
||||||
|
badge: string;
|
||||||
|
badgeIcon?: any;
|
||||||
|
price: string;
|
||||||
|
subtitle: string;
|
||||||
|
features: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PricingCardOneWithSavingProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tag?: string;
|
||||||
|
tagIcon?: any;
|
||||||
|
plans: NutritionPlan[];
|
||||||
|
animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal' | 'depth-3d';
|
||||||
|
textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
|
||||||
|
useInvertedBackground: boolean;
|
||||||
|
onNutritionPlanSelected?: (planData: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PricingCardOneWithSaving({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
tag,
|
||||||
|
tagIcon,
|
||||||
|
plans,
|
||||||
|
animationType,
|
||||||
|
textboxLayout,
|
||||||
|
useInvertedBackground,
|
||||||
|
onNutritionPlanSelected,
|
||||||
|
}: PricingCardOneWithSavingProps) {
|
||||||
|
const handlePlanSelection = (planId: string, planBadge: string) => {
|
||||||
|
const nutritionData = {
|
||||||
|
type: 'nutrition' as const,
|
||||||
|
date: new Date().toISOString().split('T')[0],
|
||||||
|
data: {
|
||||||
|
plan: planBadge,
|
||||||
|
planId,
|
||||||
|
selectedAt: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
onNutritionPlanSelected?.(nutritionData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhancedPlans = plans.map(plan => ({
|
||||||
|
...plan,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PricingCardOne
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
tag={tag}
|
||||||
|
tagIcon={tagIcon}
|
||||||
|
plans={enhancedPlans as any}
|
||||||
|
animationType={animationType}
|
||||||
|
textboxLayout={textboxLayout}
|
||||||
|
useInvertedBackground={useInvertedBackground}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PricingCardOneWithSaving;
|
||||||
83
src/components/sections/product/ProductCardOneWithSaving.tsx
Normal file
83
src/components/sections/product/ProductCardOneWithSaving.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProductCardOne from './ProductCardOne';
|
||||||
|
|
||||||
|
interface ProductCardOneWithSavingProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tag?: string;
|
||||||
|
tagIcon?: any;
|
||||||
|
products: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: string;
|
||||||
|
imageSrc: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
onFavorite?: () => void;
|
||||||
|
onProductClick?: () => void;
|
||||||
|
isFavorited?: boolean;
|
||||||
|
}>;
|
||||||
|
animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
|
||||||
|
textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
|
||||||
|
useInvertedBackground: boolean;
|
||||||
|
gridVariant:
|
||||||
|
| 'uniform-all-items-equal'
|
||||||
|
| 'bento-grid'
|
||||||
|
| 'bento-grid-inverted'
|
||||||
|
| 'two-columns-alternating-heights'
|
||||||
|
| 'asymmetric-60-wide-40-narrow'
|
||||||
|
| 'three-columns-all-equal-width'
|
||||||
|
| 'four-items-2x2-equal-grid'
|
||||||
|
| 'one-large-right-three-stacked-left'
|
||||||
|
| 'items-top-row-full-width-bottom'
|
||||||
|
| 'full-width-top-items-bottom-row'
|
||||||
|
| 'one-large-left-three-stacked-right';
|
||||||
|
onWorkoutStart?: (workoutData: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductCardOneWithSaving({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
tag,
|
||||||
|
tagIcon,
|
||||||
|
products,
|
||||||
|
animationType,
|
||||||
|
textboxLayout,
|
||||||
|
useInvertedBackground,
|
||||||
|
gridVariant,
|
||||||
|
onWorkoutStart,
|
||||||
|
}: ProductCardOneWithSavingProps) {
|
||||||
|
const handleProductClick = (productId: string, productName: string) => {
|
||||||
|
const workoutData = {
|
||||||
|
type: 'training' as const,
|
||||||
|
date: new Date().toISOString().split('T')[0],
|
||||||
|
data: {
|
||||||
|
workout: productName,
|
||||||
|
startTime: Date.now(),
|
||||||
|
productId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
onWorkoutStart?.(workoutData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhancedProducts = products.map(product => ({
|
||||||
|
...product,
|
||||||
|
onProductClick: () => handleProductClick(product.id, product.name),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProductCardOne
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
tag={tag}
|
||||||
|
tagIcon={tagIcon}
|
||||||
|
products={enhancedProducts}
|
||||||
|
animationType={animationType}
|
||||||
|
textboxLayout={textboxLayout}
|
||||||
|
useInvertedBackground={useInvertedBackground}
|
||||||
|
gridVariant={gridVariant}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCardOneWithSaving;
|
||||||
120
src/hooks/useWorkoutStorage.ts
Normal file
120
src/hooks/useWorkoutStorage.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { WorkoutStorageService, WorkoutSession, UserMetrics } from '@/lib/storage/workoutStorage';
|
||||||
|
|
||||||
|
export function useWorkoutStorage() {
|
||||||
|
const [workouts, setWorkouts] = useState<WorkoutSession[]>([]);
|
||||||
|
const [metrics, setMetrics] = useState<UserMetrics | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const loadedWorkouts = WorkoutStorageService.getWorkoutSessions();
|
||||||
|
const loadedMetrics = WorkoutStorageService.getMetrics();
|
||||||
|
setWorkouts(loadedWorkouts);
|
||||||
|
setMetrics(loadedMetrics);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveWorkout = useCallback((session: Omit<WorkoutSession, 'id' | 'createdAt'>) => {
|
||||||
|
try {
|
||||||
|
const saved = WorkoutStorageService.saveWorkoutSession(session);
|
||||||
|
setWorkouts(prev => [...prev, saved]);
|
||||||
|
// Recalculate metrics
|
||||||
|
const updated = WorkoutStorageService.calculateMetricsFromSessions();
|
||||||
|
setMetrics(updated);
|
||||||
|
return saved;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving workout:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateWorkout = useCallback((id: string, updates: Partial<WorkoutSession>) => {
|
||||||
|
try {
|
||||||
|
const updated = WorkoutStorageService.updateWorkoutSession(id, updates);
|
||||||
|
if (updated) {
|
||||||
|
setWorkouts(prev => prev.map(w => w.id === id ? updated : w));
|
||||||
|
// Recalculate metrics
|
||||||
|
const newMetrics = WorkoutStorageService.calculateMetricsFromSessions();
|
||||||
|
setMetrics(newMetrics);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating workout:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteWorkout = useCallback((id: string) => {
|
||||||
|
try {
|
||||||
|
const success = WorkoutStorageService.deleteWorkoutSession(id);
|
||||||
|
if (success) {
|
||||||
|
setWorkouts(prev => prev.filter(w => w.id !== id));
|
||||||
|
// Recalculate metrics
|
||||||
|
const updated = WorkoutStorageService.calculateMetricsFromSessions();
|
||||||
|
setMetrics(updated);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting workout:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getWorkoutsByType = useCallback((type: WorkoutSession['type']) => {
|
||||||
|
return workouts.filter(w => w.type === type);
|
||||||
|
}, [workouts]);
|
||||||
|
|
||||||
|
const getWorkoutsByDate = useCallback((date: string) => {
|
||||||
|
return workouts.filter(w => w.date === date);
|
||||||
|
}, [workouts]);
|
||||||
|
|
||||||
|
const updateMetrics = useCallback((updates: Partial<UserMetrics>) => {
|
||||||
|
try {
|
||||||
|
const updated = WorkoutStorageService.updateMetrics(updates);
|
||||||
|
setMetrics(updated);
|
||||||
|
return updated;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating metrics:', error);
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
}, [metrics]);
|
||||||
|
|
||||||
|
const clearAllData = useCallback(() => {
|
||||||
|
try {
|
||||||
|
WorkoutStorageService.clearAllData();
|
||||||
|
setWorkouts([]);
|
||||||
|
setMetrics({
|
||||||
|
totalWorkouts: 0,
|
||||||
|
totalDistance: 0,
|
||||||
|
totalCalories: 0,
|
||||||
|
totalWeight: 0,
|
||||||
|
consistency: 0,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing data:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
workouts,
|
||||||
|
metrics,
|
||||||
|
isLoading,
|
||||||
|
saveWorkout,
|
||||||
|
updateWorkout,
|
||||||
|
deleteWorkout,
|
||||||
|
getWorkoutsByType,
|
||||||
|
getWorkoutsByDate,
|
||||||
|
updateMetrics,
|
||||||
|
clearAllData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseWorkoutStorageReturn = ReturnType<typeof useWorkoutStorage>;
|
||||||
@@ -1,54 +1,185 @@
|
|||||||
export interface WorkoutSession {
|
// Workout and metrics persistence layer
|
||||||
|
|
||||||
|
interface WorkoutSession {
|
||||||
id: string;
|
id: string;
|
||||||
|
type: 'cardio' | 'training' | 'nutrition';
|
||||||
date: string;
|
date: string;
|
||||||
type: 'cardio' | 'strength' | 'flexibility' | 'nutrition';
|
data: Record<string, any>;
|
||||||
duration: number;
|
createdAt: number;
|
||||||
calories?: number;
|
|
||||||
notes?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserMetrics {
|
interface UserMetrics {
|
||||||
totalWorkouts: number;
|
totalWorkouts: number;
|
||||||
|
totalDistance: number;
|
||||||
totalCalories: number;
|
totalCalories: number;
|
||||||
averageDuration: number;
|
totalWeight: number;
|
||||||
|
consistency: number; // days in a row
|
||||||
|
lastUpdated: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveWorkoutSession(session: Omit<WorkoutSession, 'id'>): Promise<WorkoutSession> {
|
const STORAGE_KEYS = {
|
||||||
return { id: '1', ...session };
|
WORKOUTS: 'fitflow_workouts',
|
||||||
|
METRICS: 'fitflow_metrics',
|
||||||
|
USER_PROFILE: 'fitflow_user_profile',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class WorkoutStorageService {
|
||||||
|
// Workout Session Management
|
||||||
|
static saveWorkoutSession(session: Omit<WorkoutSession, 'id' | 'createdAt'>): WorkoutSession {
|
||||||
|
try {
|
||||||
|
const sessions = this.getWorkoutSessions();
|
||||||
|
const newSession: WorkoutSession = {
|
||||||
|
id: `workout_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
...session,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
sessions.push(newSession);
|
||||||
|
localStorage.setItem(STORAGE_KEYS.WORKOUTS, JSON.stringify(sessions));
|
||||||
|
return newSession;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving workout session:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWorkoutSessions(): WorkoutSession[] {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(STORAGE_KEYS.WORKOUTS);
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving workout sessions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWorkoutSessionsByType(type: WorkoutSession['type']): WorkoutSession[] {
|
||||||
|
const sessions = this.getWorkoutSessions();
|
||||||
|
return sessions.filter(session => session.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWorkoutSessionsByDate(date: string): WorkoutSession[] {
|
||||||
|
const sessions = this.getWorkoutSessions();
|
||||||
|
return sessions.filter(session => session.date === date);
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateWorkoutSession(id: string, updates: Partial<WorkoutSession>): WorkoutSession | null {
|
||||||
|
try {
|
||||||
|
const sessions = this.getWorkoutSessions();
|
||||||
|
const index = sessions.findIndex(s => s.id === id);
|
||||||
|
if (index === -1) return null;
|
||||||
|
|
||||||
|
const updated = { ...sessions[index], ...updates, id: sessions[index].id, createdAt: sessions[index].createdAt };
|
||||||
|
sessions[index] = updated;
|
||||||
|
localStorage.setItem(STORAGE_KEYS.WORKOUTS, JSON.stringify(sessions));
|
||||||
|
return updated;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating workout session:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteWorkoutSession(id: string): boolean {
|
||||||
|
try {
|
||||||
|
const sessions = this.getWorkoutSessions();
|
||||||
|
const filtered = sessions.filter(s => s.id !== id);
|
||||||
|
localStorage.setItem(STORAGE_KEYS.WORKOUTS, JSON.stringify(filtered));
|
||||||
|
return filtered.length < sessions.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting workout session:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics Management
|
||||||
|
static saveMetrics(metrics: UserMetrics): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.METRICS, JSON.stringify(metrics));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving metrics:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMetrics(): UserMetrics {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(STORAGE_KEYS.METRICS);
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
totalWorkouts: 0,
|
||||||
|
totalDistance: 0,
|
||||||
|
totalCalories: 0,
|
||||||
|
totalWeight: 0,
|
||||||
|
consistency: 0,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving metrics:', error);
|
||||||
|
return {
|
||||||
|
totalWorkouts: 0,
|
||||||
|
totalDistance: 0,
|
||||||
|
totalCalories: 0,
|
||||||
|
totalWeight: 0,
|
||||||
|
consistency: 0,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateMetrics(updates: Partial<UserMetrics>): UserMetrics {
|
||||||
|
const current = this.getMetrics();
|
||||||
|
const updated: UserMetrics = { ...current, ...updates, lastUpdated: Date.now() };
|
||||||
|
this.saveMetrics(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculated Metrics
|
||||||
|
static calculateMetricsFromSessions(): UserMetrics {
|
||||||
|
const sessions = this.getWorkoutSessions();
|
||||||
|
let totalDistance = 0;
|
||||||
|
let totalCalories = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
|
||||||
|
sessions.forEach(session => {
|
||||||
|
if (session.data.distance) totalDistance += Number(session.data.distance) || 0;
|
||||||
|
if (session.data.calories) totalCalories += Number(session.data.calories) || 0;
|
||||||
|
if (session.data.weight) totalWeight += Number(session.data.weight) || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.updateMetrics({
|
||||||
|
totalWorkouts: sessions.length,
|
||||||
|
totalDistance,
|
||||||
|
totalCalories,
|
||||||
|
totalWeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch Operations
|
||||||
|
static clearAllData(): void {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.WORKOUTS);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.METRICS);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.USER_PROFILE);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static exportData(): { workouts: WorkoutSession[]; metrics: UserMetrics } {
|
||||||
|
return {
|
||||||
|
workouts: this.getWorkoutSessions(),
|
||||||
|
metrics: this.getMetrics(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static importData(data: { workouts: WorkoutSession[]; metrics: UserMetrics }): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.WORKOUTS, JSON.stringify(data.workouts));
|
||||||
|
this.saveMetrics(data.metrics);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkoutSessions(): Promise<WorkoutSession[]> {
|
export type { WorkoutSession, UserMetrics };
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateWorkoutSession(id: string, session: Partial<WorkoutSession>): Promise<WorkoutSession> {
|
|
||||||
return { id, date: '', type: 'cardio', duration: 0, ...session };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteWorkoutSession(id: string): Promise<void> {
|
|
||||||
// Delete implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkoutsByType(type: string): Promise<WorkoutSession[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkoutsByDateRange(startDate: string, endDate: string): Promise<WorkoutSession[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveUserMetrics(metrics: UserMetrics): Promise<void> {
|
|
||||||
// Save implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserMetrics(): Promise<UserMetrics> {
|
|
||||||
return { totalWorkouts: 0, totalCalories: 0, averageDuration: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateUserMetrics(metrics: Partial<UserMetrics>): Promise<void> {
|
|
||||||
// Update implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function calculateMetricsFromSessions(sessions: WorkoutSession[]): Promise<UserMetrics> {
|
|
||||||
return { totalWorkouts: sessions.length, totalCalories: 0, averageDuration: 0 };
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user