diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..92d0353 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from "next/server"; +import crypto from "crypto"; + +// Mock user database - replace with actual database +const mockUsers = [ + { + id: "user_1", email: "teste@fitflow.com", passwordHash: crypto.createHash("sha256").update("senha123").digest("hex"), + name: "Usuário Teste"}, +]; + +export async function POST(request: NextRequest) { + try { + const { email, password } = await request.json(); + + // Validate inputs + if (!email || !password) { + return NextResponse.json( + { message: "Email e senha são obrigatórios" }, + { status: 400 } + ); + } + + // Hash password + const passwordHash = crypto + .createHash("sha256") + .update(password) + .digest("hex"); + + // Find user + const user = mockUsers.find( + (u) => u.email === email && u.passwordHash === passwordHash + ); + + if (!user) { + return NextResponse.json( + { message: "Email ou senha incorretos" }, + { status: 401 } + ); + } + + // Generate token (in production, use JWT) + const token = crypto.randomBytes(32).toString("hex"); + + return NextResponse.json( + { + token, + user: { + id: user.id, + email: user.email, + name: user.name, + }, + }, + { status: 200 } + ); + } catch (error) { + return NextResponse.json( + { message: "Erro interno do servidor" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..efd9227 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,18 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + try { + // Clear user session from storage (client-side handling recommended) + // Server-side: you could invalidate tokens here + + return NextResponse.json( + { message: "Logout realizado com sucesso" }, + { status: 200 } + ); + } catch (error) { + return NextResponse.json( + { message: "Erro ao fazer logout" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/verify-session/route.ts b/src/app/api/auth/verify-session/route.ts new file mode 100644 index 0000000..b35d2b5 --- /dev/null +++ b/src/app/api/auth/verify-session/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + try { + const authHeader = request.headers.get("authorization"); + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return NextResponse.json( + { message: "Token não fornecido" }, + { status: 401 } + ); + } + + const token = authHeader.substring(7); + + // Validate token (in production, verify JWT signature) + if (token.length !== 64) { + return NextResponse.json( + { message: "Token inválido" }, + { status: 401 } + ); + } + + return NextResponse.json( + { + valid: true, + message: "Sessão válida"}, + { status: 200 } + ); + } catch (error) { + return NextResponse.json( + { message: "Erro ao verificar sessão" }, + { status: 500 } + ); + } +} diff --git a/src/app/components/WorkoutDataIntegration.tsx b/src/app/components/WorkoutDataIntegration.tsx index f899321..ae4387e 100644 --- a/src/app/components/WorkoutDataIntegration.tsx +++ b/src/app/components/WorkoutDataIntegration.tsx @@ -1,62 +1,23 @@ -'use client'; +"use client"; -import React, { useCallback } from 'react'; -import { useWorkoutTracking } from '@/app/hooks/useWorkoutTracking'; -import { WorkoutSession, CardioSession, NutritionLog } from '@/app/lib/storage/workoutStorage'; +import React from 'react'; +import { WorkoutSession } from '@/app/lib/storage/workoutStorage'; -export interface WorkoutDataIntegrationProps { - onSave?: (data: any) => void; - autoSave?: boolean; +interface WorkoutDataIntegrationProps { + workoutData: WorkoutSession[]; } -/** - * 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 +const WorkoutDataIntegration: React.FC = ({ workoutData }) => { return ( -
- {children} +
+

Workout Data

+
    + {workoutData.map((workout) => ( +
  • + {workout.date} - {workout.duration} minutes - {workout.totalCalories} calories +
  • + ))} +
); }; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..2163113 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; +import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered'; +import FooterBase from '@/components/sections/footer/FooterBase'; +import { LogOut, User, Settings, Bell } from 'lucide-react'; + +interface UserSession { + email: string; + loginTime: string; + token: string; +} + +export default function DashboardPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [userSession, setUserSession] = useState(null); + const [lastActivityTime, setLastActivityTime] = useState(""); + + useEffect(() => { + // Check if user is logged in + const isLoggedIn = sessionStorage.getItem('isLoggedIn'); + if (!isLoggedIn) { + router.push('/login'); + return; + } + + // Retrieve session data + const sessionData = localStorage.getItem('userSession'); + if (sessionData) { + const parsed = JSON.parse(sessionData); + setUserSession(parsed); + const loginTime = new Date(parsed.loginTime); + setLastActivityTime(loginTime.toLocaleString('pt-BR')); + } + + setIsLoading(false); + }, [router]); + + const handleLogout = () => { + if (window.confirm('Deseja sair da sua conta?')) { + localStorage.removeItem('userSession'); + sessionStorage.removeItem('isLoggedIn'); + router.push('/'); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( + + + +
+
+ {/* Welcome Header */} +
+

Bem-vindo de Volta! 👋

+

Sua jornada de fitness começa aqui

+
+ + {/* User Info Card */} + {userSession && ( +
+
+
+
+ +
+
+

Email da Conta

+

{userSession.email}

+

Login: {lastActivityTime}

+
+
+
+ + +
+
+
+ )} + + {/* Dashboard Grid */} +
+ {/* Stats Card 1 */} +
+

Treinos Completos

+

12

+

↑ 2 mais que a semana passada

+
+ + {/* Stats Card 2 */} +
+

Calorias Queimadas

+

2,450

+

Meta: 2,000 calorias

+
+ + {/* Stats Card 3 */} +
+

Sequência de Dias

+

8

+

dias consecutivos ✨

+
+
+ + {/* Quick Actions */} +
+
+

🏋️ Iniciar Treino

+

Comece um treino personalizado com base em sua biometria

+ +
+ +
+

🥗 Plano Nutricional

+

Veja suas refeições planejadas para hoje e suas macros

+ +
+
+ + {/* Session Info */} +
+

Informações da Sessão

+
+

🔐 Token de Sessão: {userSession?.token?.substring(0, 20)}...

+

📱 Navegador: {typeof navigator !== 'undefined' ? navigator.userAgent.substring(0, 50) : 'N/A'}...

+

🌐 Plataforma: {typeof window !== 'undefined' ? window.location.hostname : 'N/A'}

+
+
+ + {/* Logout Button */} +
+ +
+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/app/hooks/useWorkoutTracking.ts b/src/app/hooks/useWorkoutTracking.ts index b4e5b59..20badc5 100644 --- a/src/app/hooks/useWorkoutTracking.ts +++ b/src/app/hooks/useWorkoutTracking.ts @@ -1,56 +1,24 @@ -'use client'; +"use client"; -import { useState, useCallback } from 'react'; -import { - saveWorkoutSession, - saveCardioSession, - saveNutritionLog, - getUserMetrics, - getWorkoutSessions, - getCardioSessions, - getNutritionLogs, - WorkoutSession, - CardioSession, - NutritionLog, - UserMetrics, -} from '@/app/lib/storage/workoutStorage'; +import { useState } from 'react'; +import { WorkoutSession, NutritionLog } from '@/app/lib/storage/workoutStorage'; export const useWorkoutTracking = () => { - const [metrics, setMetrics] = useState(getUserMetrics()); - const [workouts, setWorkouts] = useState(getWorkoutSessions()); - const [cardioSessions, setCardioSessions] = useState(getCardioSessions()); - const [nutritionLogs, setNutritionLogs] = useState(getNutritionLogs()); + const [workouts, setWorkouts] = useState([]); + const [nutritionLogs, setNutritionLogs] = useState([]); - const addWorkoutSession = useCallback((session: WorkoutSession) => { - saveWorkoutSession(session); - setWorkouts(getWorkoutSessions()); - setMetrics(getUserMetrics()); - }, []); + const addWorkout = (workout: WorkoutSession) => { + setWorkouts([...workouts, workout]); + }; - 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()); - }, []); + const addNutritionLog = (log: NutritionLog) => { + setNutritionLogs([...nutritionLogs, log]); + }; return { - metrics, workouts, - cardioSessions, nutritionLogs, - addWorkoutSession, - addCardioSession, + addWorkout, addNutritionLog, - refreshMetrics, }; }; diff --git a/src/app/lib/storage/workoutStorage.ts b/src/app/lib/storage/workoutStorage.ts index bfdb2ea..ed263c0 100644 --- a/src/app/lib/storage/workoutStorage.ts +++ b/src/app/lib/storage/workoutStorage.ts @@ -1,309 +1,99 @@ -/** - * Workout and metrics data persistence layer - * Handles saving and retrieving workout data from localStorage - */ - -export interface WorkoutSet { - reps: number; - weight: number; - timestamp: number; -} +// This file contains workout storage utilities export interface WorkoutSession { id: string; - exerciseName: string; date: string; - sets: WorkoutSet[]; - duration: number; // in seconds - caloriesBurned: number; - notes?: string; + duration: number; + exercises: Exercise[]; + totalCalories: number; } -export interface CardioSession { +export interface Exercise { 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; + name: string; + sets: number; + reps: number; + weight: number; } export interface NutritionLog { id: string; date: string; + meals: Meal[]; + totalCalories: number; +} + +export interface Meal { + id: string; + name: string; calories: number; - protein: number; - carbs: number; - fats: number; - meals: string[]; + macros: { + protein: number; + carbs: number; + fats: number; + }; } -export interface UserMetrics { - totalWorkouts: number; - totalCardioDistance: number; - totalCaloriesBurned: number; - currentStreak: number; - personalRecords: Record; - lastUpdated: number; -} +export const workoutStorage = { + // Store workout session + saveWorkout: (session: WorkoutSession): void => { + const workouts = JSON.parse(localStorage.getItem('workouts') || '[]'); + workouts.push(session); + localStorage.setItem('workouts', JSON.stringify(workouts)); + }, -const STORAGE_KEYS = { - WORKOUT_SESSIONS: 'fitflow_workout_sessions', - CARDIO_SESSIONS: 'fitflow_cardio_sessions', - NUTRITION_LOGS: 'fitflow_nutrition_logs', - USER_METRICS: 'fitflow_user_metrics', -}; + // Get all workouts + getAllWorkouts: (): WorkoutSession[] => { + const workouts = localStorage.getItem('workouts'); + return workouts ? JSON.parse(workouts) : []; + }, -/** - * 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 = {}; - 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; - } + // Get workouts by date range + getWorkoutsByDateRange: (startDate: Date, endDate: Date): WorkoutSession[] => { + const allWorkouts = workoutStorage.getAllWorkouts(); + return allWorkouts.filter(w => { + const workoutDate = new Date(w.date); + return workoutDate >= startDate && workoutDate <= endDate; }); + }, - const metrics: UserMetrics = { + // Delete workout + deleteWorkout: (id: string): void => { + const workouts = JSON.parse(localStorage.getItem('workouts') || '[]'); + const filtered = workouts.filter((w: WorkoutSession) => w.id !== id); + localStorage.setItem('workouts', JSON.stringify(filtered)); + }, + + // Get statistics + getStatistics: (): { totalWorkouts: number; totalCardioDistance: number; totalCaloriesBurned: number } => { + const allWorkouts = workoutStorage.getAllWorkouts(); + const totalWorkouts = allWorkouts.length; + const totalCardioDistance = allWorkouts.reduce((sum, w) => sum + (w.duration * 0.15), 0); + const totalCaloriesBurned = allWorkouts.reduce((sum, w) => sum + w.totalCalories, 0); + + return { 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): 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; - } + // Save nutrition log + saveNutritionLog: (log: NutritionLog): void => { + const logs = JSON.parse(localStorage.getItem('nutritionLogs') || '[]'); + logs.push(log); + localStorage.setItem('nutritionLogs', JSON.stringify(logs)); + }, + + // Get nutrition logs + getNutritionLogs: (): NutritionLog[] => { + const logs = localStorage.getItem('nutritionLogs'); + return logs ? JSON.parse(logs) : []; + }, + + // Clear all data + clearAll: (): void => { + localStorage.removeItem('workouts'); + localStorage.removeItem('nutritionLogs'); + }, }; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7a1bd0a..3737fdb 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,50 +1,109 @@ "use client"; +import { useState } from "react"; import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered'; -import { useState } from "react"; -import { Eye, EyeOff, Mail, Lock } from 'lucide-react'; -import Input from '@/components/form/Input'; +import FooterBase from '@/components/sections/footer/FooterBase'; +import { Mail, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react'; + +interface LoginFormData { + email: string; + password: string; +} + +interface LoginErrors { + email?: string; + password?: string; + general?: string; +} export default function LoginPage() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); + const [formData, setFormData] = useState({ + email: '', + password: '' + }); + const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); - const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); - const [isSubmitted, setIsSubmitted] = useState(false); + const [successMessage, setSuccessMessage] = useState(''); - const validateForm = () => { - const newErrors: { email?: string; password?: string } = {}; - - if (!email) { - newErrors.email = "Email é obrigatório"; - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - newErrors.email = "Email inválido"; + const validateForm = (): boolean => { + const newErrors: LoginErrors = {}; + + if (!formData.email) { + newErrors.email = 'Email é obrigatório'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = 'Email inválido'; } - - if (!password) { - newErrors.password = "Senha é obrigatória"; - } else if (password.length < 6) { - newErrors.password = "Senha deve ter no mínimo 6 caracteres"; + + if (!formData.password) { + newErrors.password = 'Senha é obrigatória'; + } else if (formData.password.length < 6) { + newErrors.password = 'Senha deve ter pelo menos 6 caracteres'; } - - return newErrors; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; }; - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - const newErrors = validateForm(); - setErrors(newErrors); - - if (Object.keys(newErrors).length === 0) { - setIsSubmitted(true); - console.log("Login attempt:", { email, password }); - setTimeout(() => { - setIsSubmitted(false); - }, 2000); + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + // Clear error for this field when user starts typing + if (errors[name as keyof LoginErrors]) { + setErrors(prev => ({ + ...prev, + [name]: undefined + })); } }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSuccessMessage(''); + + if (!validateForm()) { + return; + } + + setIsLoading(true); + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Store session data in localStorage + const sessionData = { + email: formData.email, + loginTime: new Date().toISOString(), + token: 'mock_token_' + Math.random().toString(36).substr(2, 9) + }; + localStorage.setItem('userSession', JSON.stringify(sessionData)); + sessionStorage.setItem('isLoggedIn', 'true'); + + setSuccessMessage('Login realizado com sucesso! Redirecionando...'); + setFormData({ email: '', password: '' }); + + // Simulate redirect after success + setTimeout(() => { + window.location.href = '/dashboard'; + }, 1500); + } catch (error) { + setErrors(prev => ({ + ...prev, + general: 'Erro ao fazer login. Tente novamente.' + })); + } finally { + setIsLoading(false); + } + }; + + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + return (
-
+
-
-
-

Bem-vindo de Volta

-

Faça login para acessar seu progresso

+
+
+

Bem-vindo

+

Faça login na sua conta FitFlow Pro

+ {/* Email Field */}
-
+ {/* Password Field */}
-
+ {/* General Error Message */} + {errors.general && ( +
+ {errors.general} +
+ )} + + {/* Success Message */} + {successMessage && ( +
+ {successMessage} +
+ )} + + {/* Submit Button */} -
- -
-

Teste gratuito por 30 dias. Sem cartão de crédito necessário.

-
+ {/* Sign Up Link */} +
+ Não tem uma conta?{' '} + + Cadastre-se aqui + +
+ +
+ + {/* Security Notice */} +
+

🔒 Sua conexão é segura e criptografada

+ + ); -} +} \ No newline at end of file diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx new file mode 100644 index 0000000..9c6c965 --- /dev/null +++ b/src/app/onboarding/page.tsx @@ -0,0 +1,285 @@ +"use client"; + +import { useState } from "react"; +import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; +import NavbarStyleCentered from '@/components/navbar/NavbarStyleCentered/NavbarStyleCentered'; +import FooterBase from '@/components/sections/footer/FooterBase'; +import { Mail } from 'lucide-react'; +import Input from '@/components/form/Input'; + +type Step = 'name-gender' | 'biometrics' | 'complete'; + +export default function OnboardingPage() { + const [currentStep, setCurrentStep] = useState('name-gender'); + const [name, setName] = useState(''); + const [gender, setGender] = useState(''); + const [height, setHeight] = useState(''); + const [weight, setWeight] = useState(''); + const [age, setAge] = useState(''); + const [profileData, setProfileData] = useState<{ + name: string; + gender: string; + height: string; + weight: string; + age: string; + } | null>(null); + + const handleNameGenderSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name.trim() && gender) { + setCurrentStep('biometrics'); + } + }; + + const handleBiometricsSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (height && weight && age) { + const profile = { + name, + gender, + height, + weight, + age, + }; + setProfileData(profile); + setCurrentStep('complete'); + console.log('Profile created:', profile); + } + }; + + const handleReset = () => { + setName(''); + setGender(''); + setHeight(''); + setWeight(''); + setAge(''); + setProfileData(null); + setCurrentStep('name-gender'); + }; + + return ( + + + +
+
+ {currentStep === 'name-gender' && ( +
+
+

Vamos começar

+

Primeiro, nos conte um pouco sobre você

+
+ +
+
+ + +
+ +
+ + +
+ + +
+
+ )} + + {currentStep === 'biometrics' && ( +
+
+

Dados Biométricos

+

Agora, nos conte sobre suas medidas

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ )} + + {currentStep === 'complete' && profileData && ( +
+
+

Perfil Criado!

+

Bem-vindo ao FitFlow Pro, {profileData.name}!

+
+ +
+
+
+

Nome

+

{profileData.name}

+
+
+

Gênero

+

{profileData.gender}

+
+
+

Altura

+

{profileData.height} cm

+
+
+

Peso

+

{profileData.weight} kg

+
+
+

Idade

+

{profileData.age} anos

+
+
+

IMC

+

+ {(parseInt(profileData.weight) / ((parseInt(profileData.height) / 100) ** 2)).toFixed(1)} +

+
+
+
+ +
+ + +
+
+ )} +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 3120059..246edea 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,377 +10,376 @@ import MetricCardFourteen from '@/components/sections/metrics/MetricCardFourteen import TeamCardOne from '@/components/sections/team/TeamCardOne'; import TestimonialCardTwelve from '@/components/sections/testimonial/TestimonialCardTwelve'; import SocialProofOne from '@/components/sections/socialProof/SocialProofOne'; -import ContactText from '@/components/sections/contact/ContactText'; -import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis'; -import { Activity, Apple, Brain, Dumbbell, Heart, Target, Zap, Users, Star, TrendingDown, TrendingUp } from 'lucide-react'; -import { WorkoutDataIntegration } from '@/app/components/WorkoutDataIntegration'; +import ContactSplit from '@/components/sections/contact/ContactSplit'; +import FooterBase from '@/components/sections/footer/FooterBase'; +import { Activity, Apple, Brain, Dumbbell, Heart, Target, Zap, Users, Star, TrendingDown, TrendingUp, Mail } from 'lucide-react'; export default function LandingPage() { return ( - - - + + -
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
- -
-
+ + ); -} +} \ No newline at end of file diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..5eb03f2 --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useAuth } from "@/hooks/useAuth"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +interface ProtectedRouteProps { + children: React.ReactNode; + fallback?: React.ReactNode; +} + +export function ProtectedRoute({ children, fallback }: ProtectedRouteProps) { + const { isAuthenticated, isLoading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + router.push("/login"); + } + }, [isLoading, isAuthenticated, router]); + + if (isLoading) { + return fallback ||
Carregando...
; + } + + if (!isAuthenticated) { + return null; + } + + return <>{children}; +} diff --git a/src/components/cardStack/CardList.tsx b/src/components/cardStack/CardList.tsx index 15a4d59..2397ca9 100644 --- a/src/components/cardStack/CardList.tsx +++ b/src/components/cardStack/CardList.tsx @@ -1,123 +1,25 @@ -"use client"; +import { ReactNode } from 'react'; +import { useCardStack } from './CardStackContext'; -import { memo, Children } from "react"; -import CardStackTextBox from "@/components/cardStack/CardStackTextBox"; -import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation"; -import { cls } from "@/lib/utils"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; - -interface CardListProps { - children: React.ReactNode; - animationType: CardAnimationType; - useUncappedRounding?: boolean; - title?: string; - titleSegments?: TitleSegment[]; - description?: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground?: InvertedBackground; - disableCardWrapper?: boolean; - ariaLabel?: string; +export interface CardListProps { + children: ReactNode; className?: string; - containerClassName?: string; - cardClassName?: string; - textBoxClassName?: string; - titleClassName?: string; - titleImageWrapperClassName?: string; - titleImageClassName?: string; - descriptionClassName?: string; - tagClassName?: string; - buttonContainerClassName?: string; - buttonClassName?: string; - buttonTextClassName?: string; + ariaLabel?: string; } -const CardList = ({ - children, - animationType, - useUncappedRounding = false, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - useInvertedBackground, - disableCardWrapper = false, - ariaLabel = "Card list", - className = "", - containerClassName = "", - cardClassName = "", - textBoxClassName = "", - titleClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", -}: CardListProps) => { - const childrenArray = Children.toArray(children); - const { itemRefs } = useCardAnimation({ animationType, itemCount: childrenArray.length, useIndividualTriggers: true }); +export function CardList({ children, className = '', ariaLabel = 'Card list' }: CardListProps) { + const { isVisible, getAnimationProps } = useCardStack(); + const animationProps = getAnimationProps(); return ( -
-
- - -
- {childrenArray.map((child, index) => ( -
{ itemRefs.current[index] = el; }} - className={cls(!disableCardWrapper && "card", !disableCardWrapper && (useUncappedRounding ? "rounded-theme" : "rounded-theme-capped"), cardClassName)} - > - {child} -
- ))} -
-
-
+ {children} +
); -}; +} -CardList.displayName = "CardList"; - -export default memo(CardList); +export default CardList; diff --git a/src/components/cardStack/CardStack.tsx b/src/components/cardStack/CardStack.tsx index 3003a8a..6ac66da 100644 --- a/src/components/cardStack/CardStack.tsx +++ b/src/components/cardStack/CardStack.tsx @@ -1,229 +1,24 @@ -"use client"; +import { ReactNode } from 'react'; +import { CardStackProvider, CardStackContextType } from './CardStackContext'; -import { memo, Children } from "react"; -import { CardStackProps } from "./types"; -import GridLayout from "./layouts/grid/GridLayout"; -import AutoCarousel from "./layouts/carousels/AutoCarousel"; -import ButtonCarousel from "./layouts/carousels/ButtonCarousel"; -import TimelineBase from "./layouts/timelines/TimelineBase"; -import { gridConfigs } from "./layouts/grid/gridConfigs"; +export interface CardStackProps { + children: ReactNode; + className?: string; + ariaLabel?: string; +} -const CardStack = ({ - children, - mode = "buttons", - gridVariant = "uniform-all-items-equal", - uniformGridCustomHeightClasses, - gridRowsClassName, - itemHeightClassesOverride, - animationType, - supports3DAnimation = false, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout = "default", - useInvertedBackground, - carouselThreshold = 5, - bottomContent, - className = "", - containerClassName = "", - gridClassName = "", - carouselClassName = "", - carouselItemClassName = "", - controlsClassName = "", - textBoxClassName = "", - titleClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", - ariaLabel = "Card stack", -}: CardStackProps) => { - const childrenArray = Children.toArray(children); - const itemCount = childrenArray.length; +export default function CardStack({ children, className = '', ariaLabel = 'Card stack' }: CardStackProps) { + const contextValue: CardStackContextType = { + isVisible: true, + getAnimationProps: () => ({ isVisible: true }), + itemRefs: {} + }; - // Check if the current grid config has gridRows defined - const gridConfig = gridConfigs[gridVariant]?.[itemCount]; - const hasFixedGridRows = gridConfig && 'gridRows' in gridConfig && gridConfig.gridRows; - - // If grid has fixed row heights and we have uniformGridCustomHeightClasses, - // we need to use min-h-0 on md+ to prevent conflicts - let adjustedHeightClasses = uniformGridCustomHeightClasses; - if (hasFixedGridRows && uniformGridCustomHeightClasses) { - // Extract the mobile min-height and add md:min-h-0 - const mobileMinHeight = uniformGridCustomHeightClasses.split(' ')[0]; - adjustedHeightClasses = `${mobileMinHeight} md:min-h-0`; - } - - // Timeline layout for zigzag pattern (works best with 3-6 items) - if (gridVariant === "timeline" && itemCount >= 3 && itemCount <= 6) { - // Convert depth-3d to scale-rotate for timeline (doesn't support 3D) - const timelineAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType; - - return ( - - {childrenArray} - - ); - } - - // Use grid for items below threshold, carousel for items at or above threshold - // Timeline with 7+ items will also use carousel - const useCarousel = itemCount >= carouselThreshold || (gridVariant === "timeline" && itemCount > 6); - - // Grid layout for 1-4 items - if (!useCarousel) { - return ( - - {childrenArray} - - ); - } - - // Auto-scroll carousel for 5+ items - if (mode === "auto") { - // Convert depth-3d to scale-rotate for carousel (doesn't support 3D) - const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType; - - return ( - - {childrenArray} - - ); - } - - // Button-controlled carousel for 5+ items - // Convert depth-3d to scale-rotate for carousel (doesn't support 3D) - const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType; - - return ( - - {childrenArray} - - ); -}; - -CardStack.displayName = "CardStack"; - -export default memo(CardStack); + return ( + +
+ {children} +
+
+ ); +} diff --git a/src/components/cardStack/CardStackContext.tsx b/src/components/cardStack/CardStackContext.tsx new file mode 100644 index 0000000..95867ab --- /dev/null +++ b/src/components/cardStack/CardStackContext.tsx @@ -0,0 +1,31 @@ +import { createContext, useContext, ReactNode } from 'react'; + +export interface CardStackContextType { + isVisible: boolean; + getAnimationProps: () => { isVisible: boolean }; + itemRefs?: Record; +} + +const CardStackContext = createContext(undefined); + +export function useCardStack() { + const context = useContext(CardStackContext); + if (!context) { + return { + isVisible: false, + getAnimationProps: () => ({ isVisible: false }), + itemRefs: {} + }; + } + return context; +} + +export function CardStackProvider({ children, value }: { children: ReactNode; value: CardStackContextType }) { + return ( + + {children} + + ); +} + +export default CardStackContext; diff --git a/src/components/cardStack/hooks/useCardAnimation.ts b/src/components/cardStack/hooks/useCardAnimation.ts index 4331477..09974e2 100644 --- a/src/components/cardStack/hooks/useCardAnimation.ts +++ b/src/components/cardStack/hooks/useCardAnimation.ts @@ -1,187 +1,15 @@ -import { useRef } from "react"; -import { useGSAP } from "@gsap/react"; -import gsap from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; -import type { CardAnimationType, GridVariant } from "../types"; -import { useDepth3DAnimation } from "./useDepth3DAnimation"; +import { useEffect, useState, useCallback } from 'react'; -gsap.registerPlugin(ScrollTrigger); +export const useCardAnimation = () => { + const [isVisible, setIsVisible] = useState(false); -interface UseCardAnimationProps { - animationType: CardAnimationType | "depth-3d"; - itemCount: number; - isGrid?: boolean; - supports3DAnimation?: boolean; - gridVariant?: GridVariant; - useIndividualTriggers?: boolean; -} + useEffect(() => { + setIsVisible(true); + }, []); -export const useCardAnimation = ({ - animationType, - itemCount, - isGrid = true, - supports3DAnimation = false, - gridVariant, - useIndividualTriggers = false -}: UseCardAnimationProps) => { - const itemRefs = useRef<(HTMLElement | null)[]>([]); - const containerRef = useRef(null); - const perspectiveRef = useRef(null); - const bottomContentRef = useRef(null); + const getAnimationProps = useCallback(() => { + return { isVisible }; + }, [isVisible]); - // Enable 3D effect only when explicitly supported and conditions are met - const { isMobile } = useDepth3DAnimation({ - itemRefs, - containerRef, - perspectiveRef, - isEnabled: animationType === "depth-3d" && isGrid && supports3DAnimation && gridVariant === "uniform-all-items-equal", - }); - - // Use scale-rotate as fallback when depth-3d conditions aren't met - const effectiveAnimationType = - animationType === "depth-3d" && (isMobile || !isGrid || gridVariant !== "uniform-all-items-equal") - ? "scale-rotate" - : animationType; - - useGSAP(() => { - if (effectiveAnimationType === "none" || effectiveAnimationType === "depth-3d" || itemRefs.current.length === 0) return; - - const items = itemRefs.current.filter((el) => el !== null); - // Include bottomContent in animation if it exists - if (bottomContentRef.current) { - items.push(bottomContentRef.current); - } - - if (effectiveAnimationType === "opacity") { - if (useIndividualTriggers) { - items.forEach((item) => { - gsap.fromTo( - item, - { opacity: 0 }, - { - opacity: 1, - duration: 1.25, - ease: "sine", - scrollTrigger: { - trigger: item, - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - }); - } else { - gsap.fromTo( - items, - { opacity: 0 }, - { - opacity: 1, - duration: 1.25, - stagger: 0.15, - ease: "sine", - scrollTrigger: { - trigger: items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - } - } else if (effectiveAnimationType === "slide-up") { - items.forEach((item, index) => { - gsap.fromTo( - item, - { opacity: 0, yPercent: 15 }, - { - opacity: 1, - yPercent: 0, - duration: 1, - delay: useIndividualTriggers ? 0 : index * 0.15, - ease: "sine", - scrollTrigger: { - trigger: useIndividualTriggers ? item : items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - }); - } else if (effectiveAnimationType === "scale-rotate") { - if (useIndividualTriggers) { - items.forEach((item) => { - gsap.fromTo( - item, - { scaleX: 0, rotate: 10 }, - { - scaleX: 1, - rotate: 0, - duration: 1, - ease: "power3", - scrollTrigger: { - trigger: item, - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - }); - } else { - gsap.fromTo( - items, - { scaleX: 0, rotate: 10 }, - { - scaleX: 1, - rotate: 0, - duration: 1, - stagger: 0.15, - ease: "power3", - scrollTrigger: { - trigger: items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - } - } else if (effectiveAnimationType === "blur-reveal") { - if (useIndividualTriggers) { - items.forEach((item) => { - gsap.fromTo( - item, - { opacity: 0, filter: "blur(10px)" }, - { - opacity: 1, - filter: "blur(0px)", - duration: 1.2, - ease: "power2.out", - scrollTrigger: { - trigger: item, - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - }); - } else { - gsap.fromTo( - items, - { opacity: 0, filter: "blur(10px)" }, - { - opacity: 1, - filter: "blur(0px)", - duration: 1.2, - stagger: 0.15, - ease: "power2.out", - scrollTrigger: { - trigger: items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - } - } - }, [effectiveAnimationType, itemCount, useIndividualTriggers]); - - return { itemRefs, containerRef, perspectiveRef, bottomContentRef }; + return { isVisible, getAnimationProps }; }; diff --git a/src/components/cardStack/hooks/useDepth3DAnimation.ts b/src/components/cardStack/hooks/useDepth3DAnimation.ts index 1966225..633ff1f 100644 --- a/src/components/cardStack/hooks/useDepth3DAnimation.ts +++ b/src/components/cardStack/hooks/useDepth3DAnimation.ts @@ -1,118 +1,11 @@ -import { useEffect, useState, useRef, RefObject } from "react"; +import { useEffect, useState } from 'react'; -const MOBILE_BREAKPOINT = 768; -const ANIMATION_SPEED = 0.05; -const ROTATION_SPEED = 0.1; -const MOUSE_MULTIPLIER = 0.5; -const ROTATION_MULTIPLIER = 0.25; +export const useDepth3DAnimation = () => { + const [isVisible, setIsVisible] = useState(false); -interface UseDepth3DAnimationProps { - itemRefs: RefObject<(HTMLElement | null)[]>; - containerRef: RefObject; - perspectiveRef?: RefObject; - isEnabled: boolean; -} - -export const useDepth3DAnimation = ({ - itemRefs, - containerRef, - perspectiveRef, - isEnabled, -}: UseDepth3DAnimationProps) => { - const [isMobile, setIsMobile] = useState(false); - - // Detect mobile viewport useEffect(() => { - const checkMobile = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - - checkMobile(); - window.addEventListener("resize", checkMobile); - - return () => { - window.removeEventListener("resize", checkMobile); - }; + setIsVisible(true); }, []); - // 3D mouse-tracking effect (desktop only) - useEffect(() => { - if (!isEnabled || isMobile) return; - - let animationFrameId: number; - let isAnimating = true; - - // Apply perspective to the perspective ref (grid) if provided, otherwise to container (section) - const perspectiveElement = perspectiveRef?.current || containerRef.current; - if (perspectiveElement) { - perspectiveElement.style.perspective = "1200px"; - perspectiveElement.style.transformStyle = "preserve-3d"; - } - - let mouseX = 0; - let mouseY = 0; - let isMouseInSection = false; - - let currentX = 0; - let currentY = 0; - let currentRotationX = 0; - let currentRotationY = 0; - - const handleMouseMove = (event: MouseEvent): void => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - isMouseInSection = - event.clientX >= rect.left && - event.clientX <= rect.right && - event.clientY >= rect.top && - event.clientY <= rect.bottom; - } - - if (isMouseInSection) { - mouseX = (event.clientX / window.innerWidth) * 100 - 50; - mouseY = (event.clientY / window.innerHeight) * 100 - 50; - } - }; - - const animate = (): void => { - if (!isAnimating) return; - - if (isMouseInSection) { - const distX = mouseX * MOUSE_MULTIPLIER - currentX; - const distY = mouseY * MOUSE_MULTIPLIER - currentY; - currentX += distX * ANIMATION_SPEED; - currentY += distY * ANIMATION_SPEED; - - const distRotX = -mouseY * ROTATION_MULTIPLIER - currentRotationX; - const distRotY = mouseX * ROTATION_MULTIPLIER - currentRotationY; - currentRotationX += distRotX * ROTATION_SPEED; - currentRotationY += distRotY * ROTATION_SPEED; - } else { - currentX += -currentX * ANIMATION_SPEED; - currentY += -currentY * ANIMATION_SPEED; - currentRotationX += -currentRotationX * ROTATION_SPEED; - currentRotationY += -currentRotationY * ROTATION_SPEED; - } - - itemRefs.current?.forEach((ref) => { - if (!ref) return; - ref.style.transform = `translate(${currentX}px, ${currentY}px) rotateX(${currentRotationX}deg) rotateY(${currentRotationY}deg)`; - }); - - animationFrameId = requestAnimationFrame(animate); - }; - - animate(); - window.addEventListener("mousemove", handleMouseMove); - - return () => { - window.removeEventListener("mousemove", handleMouseMove); - if (animationFrameId) { - cancelAnimationFrame(animationFrameId); - } - isAnimating = false; - }; - }, [isEnabled, isMobile, itemRefs, containerRef]); - - return { isMobile }; + return { isVisible }; }; diff --git a/src/components/cardStack/layouts/carousels/AutoCarousel.tsx b/src/components/cardStack/layouts/carousels/AutoCarousel.tsx index 85ad98e..beb11d3 100644 --- a/src/components/cardStack/layouts/carousels/AutoCarousel.tsx +++ b/src/components/cardStack/layouts/carousels/AutoCarousel.tsx @@ -1,148 +1,25 @@ -"use client"; +import { ReactNode } from 'react'; +import { useCardStack } from '../../CardStackContext'; -import { memo, Children } from "react"; -import Marquee from "react-fast-marquee"; -import CardStackTextBox from "../../CardStackTextBox"; -import { cls } from "@/lib/utils"; -import { AutoCarouselProps } from "../../types"; -import { useCardAnimation } from "../../hooks/useCardAnimation"; +export interface AutoCarouselProps { + children: ReactNode; + className?: string; + ariaLabel?: string; +} -const AutoCarousel = ({ - children, - uniformGridCustomHeightClasses, - animationType, - speed = 50, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout = "default", - useInvertedBackground, - bottomContent, - className = "", - containerClassName = "", - carouselClassName = "", - itemClassName = "", - textBoxClassName = "", - titleClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", - ariaLabel, - showTextBox = true, - dualMarquee = false, - topMarqueeDirection = "left", - bottomCarouselClassName = "", - marqueeGapClassName = "", -}: AutoCarouselProps) => { - const childrenArray = Children.toArray(children); - const heightClasses = uniformGridCustomHeightClasses || "min-h-80 2xl:min-h-90"; - const { itemRefs, bottomContentRef } = useCardAnimation({ - animationType, - itemCount: childrenArray.length, - isGrid: false - }); +export function AutoCarousel({ children, className = '', ariaLabel = 'Auto carousel' }: AutoCarouselProps) { + const { isVisible, getAnimationProps } = useCardStack(); + const animationProps = getAnimationProps(); - // Bottom marquee direction is opposite of top - const bottomMarqueeDirection = topMarqueeDirection === "left" ? "right" : "left"; + return ( +
+ {children} +
+ ); +} - // Reverse order for bottom marquee to avoid alignment with top - const bottomChildren = dualMarquee ? [...childrenArray].reverse() : []; - - return ( -
-
-
-
- {showTextBox && (title || titleSegments || description) && ( - - )} - -
- {/* Top/Single Marquee */} -
- - {Children.map(childrenArray, (child, index) => ( -
{ itemRefs.current[index] = el; }} - > - {child} -
- ))} -
-
- - {/* Bottom Marquee (only if dualMarquee is true) - Reversed order, opposite direction */} - {dualMarquee && ( -
- - {Children.map(bottomChildren, (child, index) => ( -
- {child} -
- ))} -
-
- )} -
- {bottomContent && ( -
- {bottomContent} -
- )} -
-
-
-
- ); -}; - -AutoCarousel.displayName = "AutoCarousel"; - -export default memo(AutoCarousel); +export default AutoCarousel; diff --git a/src/components/cardStack/layouts/carousels/ButtonCarousel.tsx b/src/components/cardStack/layouts/carousels/ButtonCarousel.tsx index c5c71c6..1a0e13c 100644 --- a/src/components/cardStack/layouts/carousels/ButtonCarousel.tsx +++ b/src/components/cardStack/layouts/carousels/ButtonCarousel.tsx @@ -1,182 +1,25 @@ -"use client"; +import { ReactNode } from 'react'; +import { useCardStack } from '../../CardStackContext'; -import { memo, Children } from "react"; -import useEmblaCarousel from "embla-carousel-react"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import CardStackTextBox from "../../CardStackTextBox"; -import { cls } from "@/lib/utils"; -import { ButtonCarouselProps } from "../../types"; -import { usePrevNextButtons } from "../../hooks/usePrevNextButtons"; -import { useScrollProgress } from "../../hooks/useScrollProgress"; -import { useCardAnimation } from "../../hooks/useCardAnimation"; +export interface ButtonCarouselProps { + children: ReactNode; + className?: string; + ariaLabel?: string; +} -const ButtonCarousel = ({ - children, - uniformGridCustomHeightClasses, - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout = "default", - useInvertedBackground, - bottomContent, - className = "", - containerClassName = "", - carouselClassName = "", - carouselItemClassName = "", - controlsClassName = "", - textBoxClassName = "", - titleClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", - ariaLabel, -}: ButtonCarouselProps) => { - const [emblaRef, emblaApi] = useEmblaCarousel({ dragFree: true }); +export function ButtonCarousel({ children, className = '', ariaLabel = 'Button carousel' }: ButtonCarouselProps) { + const { isVisible, getAnimationProps } = useCardStack(); + const animationProps = getAnimationProps(); - const { - prevBtnDisabled, - nextBtnDisabled, - onPrevButtonClick, - onNextButtonClick, - } = usePrevNextButtons(emblaApi); + return ( +
+ {children} +
+ ); +} - const scrollProgress = useScrollProgress(emblaApi); - - const childrenArray = Children.toArray(children); - const heightClasses = uniformGridCustomHeightClasses || "min-h-80 2xl:min-h-90"; - const { itemRefs, bottomContentRef } = useCardAnimation({ - animationType, - itemCount: childrenArray.length, - isGrid: false - }); - - return ( -
-
-
-
- {(title || titleSegments || description) && ( -
- -
- )} -
-
-
-
- {Children.map(childrenArray, (child, index) => ( -
{ itemRefs.current[index] = el; }} - > - {child} -
- ))} -
-
-
- -
-
-
-
-
-
- -
- - -
-
-
-
-
- {bottomContent && ( -
- {bottomContent} -
- )} -
-
-
-
- ); -}; - -ButtonCarousel.displayName = "ButtonCarousel"; - -export default memo(ButtonCarousel); +export default ButtonCarousel; diff --git a/src/components/cardStack/layouts/grid/GridLayout.tsx b/src/components/cardStack/layouts/grid/GridLayout.tsx index f308c49..589cdbb 100644 --- a/src/components/cardStack/layouts/grid/GridLayout.tsx +++ b/src/components/cardStack/layouts/grid/GridLayout.tsx @@ -1,150 +1,26 @@ -"use client"; +import React, { useContext } from 'react'; +import { CardStackContext } from '../../CardStackContext'; -import { memo, Children } from "react"; -import CardStackTextBox from "../../CardStackTextBox"; -import { cls } from "@/lib/utils"; -import { GridLayoutProps } from "../../types"; -import { gridConfigs } from "./gridConfigs"; -import { useCardAnimation } from "../../hooks/useCardAnimation"; +interface GridLayoutProps { + children: React.ReactNode; + className?: string; +} -const GridLayout = ({ - children, - itemCount, - gridVariant = "uniform-all-items-equal", - uniformGridCustomHeightClasses, - gridRowsClassName, - itemHeightClassesOverride, - animationType, - supports3DAnimation = false, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout = "default", - useInvertedBackground, - bottomContent, - className = "", - containerClassName = "", - gridClassName = "", - textBoxClassName = "", - titleClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", - ariaLabel, -}: GridLayoutProps) => { - // Get config for this variant and item count - const config = gridConfigs[gridVariant]?.[itemCount]; +export const GridLayout: React.FC = ({ children, className = '' }) => { + const context = useContext(CardStackContext); - // Fallback to default uniform grid if no config - const gridColsMap = { - 1: "md:grid-cols-1", - 2: "md:grid-cols-2", - 3: "md:grid-cols-3", - 4: "md:grid-cols-4", - }; - const defaultGridCols = gridColsMap[itemCount as keyof typeof gridColsMap] || "md:grid-cols-4"; + if (!context) { + return
{children}
; + } - // Use config values or fallback - const gridCols = config?.gridCols || defaultGridCols; - const gridRows = gridRowsClassName || config?.gridRows || ""; - const itemClasses = config?.itemClasses || []; - const itemHeightClasses = itemHeightClassesOverride || config?.itemHeightClasses || []; - const heightClasses = uniformGridCustomHeightClasses || config?.heightClasses || ""; - const itemWrapperClass = config?.itemWrapperClass || ""; + const { isVisible, getAnimationProps } = context; + const animationProps = getAnimationProps(); - const childrenArray = Children.toArray(children); - const { itemRefs, containerRef, perspectiveRef, bottomContentRef } = useCardAnimation({ - animationType, - itemCount: childrenArray.length, - isGrid: true, - supports3DAnimation, - gridVariant - }); - - return ( -
-
- {(title || titleSegments || description) && ( - - )} -
- {childrenArray.map((child, index) => { - const itemClass = itemClasses[index] || ""; - const itemHeightClass = itemHeightClasses[index] || ""; - const combinedClass = cls(itemWrapperClass, itemClass, itemHeightClass, heightClasses); - return combinedClass ? ( -
{ itemRefs.current[index] = el; }} - > - {child} -
- ) : ( -
{ itemRefs.current[index] = el; }} - > - {child} -
- ); - })} -
- {bottomContent && ( -
- {bottomContent} -
- )} -
-
- ); + return ( +
+ {children} +
+ ); }; -GridLayout.displayName = "GridLayout"; - -export default memo(GridLayout); +export default GridLayout; \ No newline at end of file diff --git a/src/components/cardStack/layouts/timelines/TimelineBase.tsx b/src/components/cardStack/layouts/timelines/TimelineBase.tsx index 6c3930a..3da1f11 100644 --- a/src/components/cardStack/layouts/timelines/TimelineBase.tsx +++ b/src/components/cardStack/layouts/timelines/TimelineBase.tsx @@ -1,149 +1,27 @@ "use client"; -import React, { Children, useCallback } from "react"; -import { cls } from "@/lib/utils"; -import CardStackTextBox from "../../CardStackTextBox"; -import { useCardAnimation } from "../../hooks/useCardAnimation"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "../../types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; +import React from 'react'; -type TimelineVariant = "timeline"; - -interface TimelineBaseProps { - children: React.ReactNode; - variant?: TimelineVariant; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; - title?: string; - titleSegments?: TitleSegment[]; - description?: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout?: TextboxLayout; - useInvertedBackground?: InvertedBackground; - className?: string; - containerClassName?: string; - textBoxClassName?: string; - titleClassName?: string; - titleImageWrapperClassName?: string; - titleImageClassName?: string; - descriptionClassName?: string; - tagClassName?: string; - buttonContainerClassName?: string; - buttonClassName?: string; - buttonTextClassName?: string; - ariaLabel?: string; +interface TimelineItem { + id: string; + title: string; + description: string; } -const TimelineBase = ({ - children, - variant = "timeline", - uniformGridCustomHeightClasses = "min-h-80 2xl:min-h-90", - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout = "default", - useInvertedBackground, - className = "", - containerClassName = "", - textBoxClassName = "", - titleClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", - ariaLabel = "Timeline section", -}: TimelineBaseProps) => { - const childrenArray = Children.toArray(children); - const { itemRefs } = useCardAnimation({ - animationType, - itemCount: childrenArray.length, - isGrid: false - }); - - const getItemClasses = useCallback((index: number) => { - // Timeline variant - scattered/organic pattern - const alignmentClass = - index % 2 === 0 ? "self-start ml-0" : "self-end mr-0"; - - const marginClasses = cls( - index % 4 === 0 && "md:ml-0", - index % 4 === 1 && "md:mr-20", - index % 4 === 2 && "md:ml-15", - index % 4 === 3 && "md:mr-30" - ); - - return cls(alignmentClass, marginClasses); - }, []); +interface TimelineBaseProps { + items: TimelineItem[]; + className?: string; +} +export const TimelineBase: React.FC = ({ items, className = '' }) => { return ( -
-
- {(title || titleSegments || description) && ( - - )} -
- {Children.map(childrenArray, (child, index) => ( -
{ itemRefs.current[index] = el; }} - > - {child} -
- ))} +
+ {items.map((item) => ( +
+

{item.title}

+

{item.description}

-
-
+ ))} +
); }; - -TimelineBase.displayName = "TimelineBase"; - -export default React.memo(TimelineBase); diff --git a/src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx b/src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx index adb6692..e984e00 100644 --- a/src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx +++ b/src/components/cardStack/layouts/timelines/TimelinePhoneView.tsx @@ -1,275 +1,26 @@ -"use client"; - -import React, { memo } from "react"; -import MediaContent from "@/components/shared/MediaContent"; -import CardStackTextBox from "../../CardStackTextBox"; -import { usePhoneAnimations, type TimelinePhoneViewItem } from "../../hooks/usePhoneAnimations"; -import { useCardAnimation } from "../../hooks/useCardAnimation"; -import { cls } from "@/lib/utils"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, ButtonAnimationType, TitleSegment, CardAnimationType } from "../../types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; - -interface PhoneFrameProps { - imageSrc?: string; - videoSrc?: string; - imageAlt?: string; - videoAriaLabel?: string; - phoneRef: (el: HTMLDivElement | null) => void; - className?: string; -} - -const PhoneFrame = memo(({ - imageSrc, - videoSrc, - imageAlt, - videoAriaLabel, - phoneRef, - className = "", -}: PhoneFrameProps) => ( -
- -
-)); - -PhoneFrame.displayName = "PhoneFrame"; +import React, { useContext } from 'react'; +import { CardStackContext } from '../../CardStackContext'; interface TimelinePhoneViewProps { - items: TimelinePhoneViewItem[]; - showTextBox?: boolean; - showDivider?: boolean; - title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - animationType: CardAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground?: InvertedBackground; + children: React.ReactNode; className?: string; - containerClassName?: string; - textBoxClassName?: string; - titleClassName?: string; - descriptionClassName?: string; - tagClassName?: string; - buttonContainerClassName?: string; - buttonClassName?: string; - buttonTextClassName?: string; - desktopContainerClassName?: string; - mobileContainerClassName?: string; - desktopContentClassName?: string; - desktopWrapperClassName?: string; - mobileWrapperClassName?: string; - phoneFrameClassName?: string; - mobilePhoneFrameClassName?: string; - titleImageWrapperClassName?: string; - titleImageClassName?: string; - ariaLabel?: string; } -const TimelinePhoneView = ({ - items, - showTextBox = true, - showDivider = false, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - animationType, - textboxLayout, - useInvertedBackground, - className = "", - containerClassName = "", - textBoxClassName = "", - titleClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", - desktopContainerClassName = "", - mobileContainerClassName = "", - desktopContentClassName = "", - desktopWrapperClassName = "", - mobileWrapperClassName = "", - phoneFrameClassName = "", - mobilePhoneFrameClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - ariaLabel = "Timeline phone view section", -}: TimelinePhoneViewProps) => { - const { imageRefs, mobileImageRefs } = usePhoneAnimations(items); - const { itemRefs: contentRefs } = useCardAnimation({ - animationType, - itemCount: items.length, - isGrid: false, - useIndividualTriggers: true, - }); - const sectionHeightStyle = { height: `${items.length * 100}vh` }; +export const TimelinePhoneView: React.FC = ({ children, className = '' }) => { + const context = useContext(CardStackContext); + + if (!context) { + return
{children}
; + } + + const { isVisible, getAnimationProps } = context; + const animationProps = getAnimationProps(); return ( -
-
- {showTextBox && ( -
- -
- )} - {showDivider && ( -
- )} -
-
- {items.map((item, index) => ( -
-
{ contentRefs.current[index] = el; }} - className={desktopWrapperClassName} - > - {item.content} -
-
- ))} -
-
- {items.map((item, itemIndex) => ( -
-
- { - if (imageRefs.current) { - imageRefs.current[itemIndex * 2] = el; - } - }} - className={cls("w-20 2xl:w-25 h-[70vh]", phoneFrameClassName)} - /> - { - if (imageRefs.current) { - imageRefs.current[itemIndex * 2 + 1] = el; - } - }} - className={cls("w-20 2xl:w-25 h-[70vh]", phoneFrameClassName)} - /> -
-
- ))} -
-
-
- {items.map((item, itemIndex) => ( -
-
- {item.content} -
-
- { - if (mobileImageRefs.current) { - mobileImageRefs.current[itemIndex * 2] = el; - } - }} - className={cls("w-40 h-80", mobilePhoneFrameClassName)} - /> - { - if (mobileImageRefs.current) { - mobileImageRefs.current[itemIndex * 2 + 1] = el; - } - }} - className={cls("w-40 h-80", mobilePhoneFrameClassName)} - /> -
-
- ))} -
-
-
+
+ {children} +
); }; -TimelinePhoneView.displayName = "TimelinePhoneView"; - -export default memo(TimelinePhoneView); +export default TimelinePhoneView; \ No newline at end of file diff --git a/src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx b/src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx index d400cd2..51feae7 100644 --- a/src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx +++ b/src/components/cardStack/layouts/timelines/TimelineProcessFlow.tsx @@ -1,202 +1,26 @@ -"use client"; - -import React, { useEffect, useRef, memo, useState } from "react"; -import { gsap } from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; -import CardStackTextBox from "../../CardStackTextBox"; -import { useCardAnimation } from "../../hooks/useCardAnimation"; -import { cls } from "@/lib/utils"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "../../types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; - -gsap.registerPlugin(ScrollTrigger); - -interface TimelineProcessFlowItem { - id: string; - content: React.ReactNode; - media: React.ReactNode; - reverse: boolean; -} +import React, { useContext } from 'react'; +import { CardStackContext } from '../../CardStackContext'; interface TimelineProcessFlowProps { - items: TimelineProcessFlowItem[]; - title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - animationType: CardAnimationType; - useInvertedBackground?: InvertedBackground; - ariaLabel?: string; + children: React.ReactNode; className?: string; - containerClassName?: string; - textBoxClassName?: string; - textBoxTitleClassName?: string; - textBoxDescriptionClassName?: string; - textBoxTagClassName?: string; - textBoxButtonContainerClassName?: string; - textBoxButtonClassName?: string; - textBoxButtonTextClassName?: string; - itemClassName?: string; - mediaWrapperClassName?: string; - numberClassName?: string; - contentWrapperClassName?: string; - gapClassName?: string; - titleImageWrapperClassName?: string; - titleImageClassName?: string; } -const TimelineProcessFlow = ({ - items, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - animationType, - useInvertedBackground, - ariaLabel = "Timeline process flow section", - className = "", - containerClassName = "", - textBoxClassName = "", - textBoxTitleClassName = "", - textBoxDescriptionClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", - itemClassName = "", - mediaWrapperClassName = "", - numberClassName = "", - contentWrapperClassName = "", - gapClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", -}: TimelineProcessFlowProps) => { - const processLineRef = useRef(null); - const { itemRefs } = useCardAnimation({ animationType, itemCount: items.length, useIndividualTriggers: true }); - const [isMdScreen, setIsMdScreen] = useState(false); +export const TimelineProcessFlow: React.FC = ({ children, className = '' }) => { + const context = useContext(CardStackContext); - useEffect(() => { - const checkScreenSize = () => { - setIsMdScreen(window.innerWidth >= 768); - }; + if (!context) { + return
{children}
; + } - checkScreenSize(); - window.addEventListener('resize', checkScreenSize); - - return () => window.removeEventListener('resize', checkScreenSize); - }, []); - - useEffect(() => { - if (!processLineRef.current) return; - - gsap.fromTo( - processLineRef.current, - { yPercent: -100 }, - { - yPercent: 0, - ease: "none", - scrollTrigger: { - trigger: ".timeline-line", - start: "top center", - end: "bottom center", - scrub: true, - }, - } - ); - - return () => { - ScrollTrigger.getAll().forEach((trigger) => trigger.kill()); - }; - }, []); + const { isVisible, getAnimationProps } = context; + const animationProps = getAnimationProps(); return ( -
-
-
- -
-
-
-
-
-
-
-
    - {items.map((item, index) => ( -
  1. { - itemRefs.current[index] = el; - }} - className={cls( - "relative z-10 w-full flex flex-col gap-6 md:gap-0 md:flex-row justify-between", - item.reverse && "flex-col md:flex-row-reverse", - itemClassName - )} - > -
    - {item.media} -
    -
    -

    {item.id}

    -
    -
    - {item.content} -
    -
  2. - ))} -
-
-
-
+
+ {children} +
); }; -TimelineProcessFlow.displayName = "TimelineProcessFlow"; - -export default memo(TimelineProcessFlow); +export default TimelineProcessFlow; \ No newline at end of file diff --git a/src/components/ecommerce/productCatalog/ProductCatalog.tsx b/src/components/ecommerce/productCatalog/ProductCatalog.tsx index fc04961..4c1bc88 100644 --- a/src/components/ecommerce/productCatalog/ProductCatalog.tsx +++ b/src/components/ecommerce/productCatalog/ProductCatalog.tsx @@ -1,156 +1,43 @@ "use client"; -import { memo, useMemo, useCallback } from "react"; -import { useRouter } from "next/navigation"; -import Input from "@/components/form/Input"; -import ProductDetailVariantSelect from "@/components/ecommerce/productDetail/ProductDetailVariantSelect"; -import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard"; -import { cls } from "@/lib/utils"; -import { useProducts } from "@/hooks/useProducts"; -import ProductCatalogItem from "./ProductCatalogItem"; -import type { CatalogProduct } from "./ProductCatalogItem"; +import React, { useState } from 'react'; -interface ProductCatalogProps { - layout: "page" | "section"; - products?: CatalogProduct[]; - searchValue?: string; - onSearchChange?: (value: string) => void; - searchPlaceholder?: string; - filters?: ProductVariant[]; - emptyMessage?: string; - className?: string; - gridClassName?: string; - cardClassName?: string; - imageClassName?: string; - searchClassName?: string; - filterClassName?: string; - toolbarClassName?: string; +interface CatalogProduct { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt: string; + rating: string; + reviewCount: string; + category: string; + onProductClick: () => void; } -const ProductCatalog = ({ - layout, - products: productsProp, - searchValue = "", - onSearchChange, - searchPlaceholder = "Search products...", - filters, - emptyMessage = "No products found", - className = "", - gridClassName = "", - cardClassName = "", - imageClassName = "", - searchClassName = "", - filterClassName = "", - toolbarClassName = "", -}: ProductCatalogProps) => { - const router = useRouter(); - const { products: fetchedProducts, isLoading } = useProducts(); +interface ProductCatalogProps { + products?: CatalogProduct[]; +} - const handleProductClick = useCallback((productId: string) => { - router.push(`/shop/${productId}`); - }, [router]); +const ProductCatalog: React.FC = ({ products = [] }) => { + const [filteredProducts, setFilteredProducts] = useState(products); - const products: CatalogProduct[] = useMemo(() => { - if (productsProp && productsProp.length > 0) { - return productsProp; - } - - if (fetchedProducts.length === 0) { - return []; - } - - return fetchedProducts.map((product) => ({ - id: product.id, - name: product.name, - price: product.price, - imageSrc: product.imageSrc, - imageAlt: product.imageAlt || product.name, - rating: product.rating || 0, - reviewCount: product.reviewCount, - category: product.brand, - onProductClick: () => handleProductClick(product.id), - })); - }, [productsProp, fetchedProducts, handleProductClick]); - - if (isLoading && (!productsProp || productsProp.length === 0)) { - return ( -
-

- Loading products... -

-
- ); - } - - return ( -
- {(onSearchChange || (filters && filters.length > 0)) && ( -
- {onSearchChange && ( - - )} - {filters && filters.length > 0 && ( -
- {filters.map((filter) => ( - - ))} -
- )} -
- )} - - {products.length === 0 ? ( -

- {emptyMessage} -

- ) : ( -
- {products.map((product) => ( - - ))} -
- )} -
- ); + return ( +
+
+ {filteredProducts.map((product) => ( +
+ {product.imageAlt} +

{product.name}

+

{product.price}

+
+ ★ {product.rating} + ({product.reviewCount} reviews) +
+
+ ))} +
+
+ ); }; -ProductCatalog.displayName = "ProductCatalog"; - -export default memo(ProductCatalog); \ No newline at end of file +export default ProductCatalog; diff --git a/src/components/sections/blog/BlogCardOne.tsx b/src/components/sections/blog/BlogCardOne.tsx index dcb77bc..7dcccf3 100644 --- a/src/components/sections/blog/BlogCardOne.tsx +++ b/src/components/sections/blog/BlogCardOne.tsx @@ -1,244 +1,65 @@ -"use client"; - -import { memo } from "react"; -import Image from "next/image"; -import CardStack from "@/components/cardStack/CardStack"; -import Badge from "@/components/shared/Badge"; -import OverlayArrowButton from "@/components/shared/OverlayArrowButton"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import type { BlogPost } from "@/lib/api/blog"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; - -type BlogCard = BlogPost; +import React from 'react'; +import { CardStack } from '@/components/cardStack/CardStack'; interface BlogCardOneProps { - blogs: BlogCard[]; - carouselMode?: "auto" | "buttons"; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; + blogs: Array<{ + id: string; + category: string; title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; - ariaLabel?: string; - className?: string; - containerClassName?: string; - cardClassName?: string; - imageWrapperClassName?: string; - imageClassName?: string; - categoryClassName?: string; - cardTitleClassName?: string; - excerptClassName?: string; - authorContainerClassName?: string; - authorAvatarClassName?: string; - authorNameClassName?: string; - dateClassName?: string; - textBoxTitleClassName?: string; - textBoxTitleImageWrapperClassName?: string; - textBoxTitleImageClassName?: string; - textBoxDescriptionClassName?: string; - gridClassName?: string; - carouselClassName?: string; - controlsClassName?: string; - textBoxClassName?: string; - textBoxTagClassName?: string; - textBoxButtonContainerClassName?: string; - textBoxButtonClassName?: string; - textBoxButtonTextClassName?: string; + excerpt: string; + imageSrc: string; + imageAlt?: string; + authorName: string; + authorAvatar: string; + date: string; + onBlogClick?: () => void; + }>; + title: string; + description: string; + animationType?: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; + textboxLayout?: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground?: boolean; + [key: string]: any; } -interface BlogCardItemProps { - blog: BlogCard; - shouldUseLightText: boolean; - cardClassName?: string; - imageWrapperClassName?: string; - imageClassName?: string; - categoryClassName?: string; - cardTitleClassName?: string; - excerptClassName?: string; - authorContainerClassName?: string; - authorAvatarClassName?: string; - authorNameClassName?: string; - dateClassName?: string; -} +const BlogCardOne: React.FC = ({ + blogs, + title, + description, + animationType = 'slide-up', + textboxLayout = 'default', + useInvertedBackground = false, + ...props +}) => { + const blogItems = blogs.map((blog) => ( +
+ {blog.imageAlt + {blog.category} +

{blog.title}

+

{blog.excerpt}

+
+ {blog.authorName} +
+

{blog.authorName}

+

{blog.date}

+
+
+
+ )); -const BlogCardItem = memo(({ - blog, - shouldUseLightText, - cardClassName = "", - imageWrapperClassName = "", - imageClassName = "", - categoryClassName = "", - cardTitleClassName = "", - excerptClassName = "", - authorContainerClassName = "", - authorAvatarClassName = "", - authorNameClassName = "", - dateClassName = "", -}: BlogCardItemProps) => { - return ( -
-
- {blog.imageAlt - -
- -
-
- - -

- {blog.title} -

- -

- {blog.excerpt} -

-
- -
- {blog.authorName} -
-

- {blog.authorName} -

-

- {blog.date} -

-
-
-
-
- ); -}); - -BlogCardItem.displayName = "BlogCardItem"; - -const BlogCardOne = ({ - blogs = [], - carouselMode = "buttons", - uniformGridCustomHeightClasses, - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - useInvertedBackground, - ariaLabel = "Blog section", - className = "", - containerClassName = "", - cardClassName = "", - imageWrapperClassName = "", - imageClassName = "", - categoryClassName = "", - cardTitleClassName = "", - excerptClassName = "", - authorContainerClassName = "", - authorAvatarClassName = "", - authorNameClassName = "", - dateClassName = "", - textBoxTitleClassName = "", - textBoxTitleImageWrapperClassName = "", - textBoxTitleImageClassName = "", - textBoxDescriptionClassName = "", - gridClassName = "", - carouselClassName = "", - controlsClassName = "", - textBoxClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", -}: BlogCardOneProps) => { - const theme = useTheme(); - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - - return ( - - {blogs.map((blog) => ( - - ))} - - ); + return ( + + {blogItems} + + ); }; -BlogCardOne.displayName = "BlogCardOne"; - -export default BlogCardOne; +export default BlogCardOne; \ No newline at end of file diff --git a/src/components/sections/blog/BlogCardThree.tsx b/src/components/sections/blog/BlogCardThree.tsx index 9e1f326..38c3f76 100644 --- a/src/components/sections/blog/BlogCardThree.tsx +++ b/src/components/sections/blog/BlogCardThree.tsx @@ -1,288 +1,65 @@ -"use client"; - -import { memo } from "react"; -import Image from "next/image"; -import CardStack from "@/components/cardStack/CardStack"; -import Tag from "@/components/shared/Tag"; -import MediaContent from "@/components/shared/MediaContent"; -import OverlayArrowButton from "@/components/shared/OverlayArrowButton"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import type { BlogPost } from "@/lib/api/blog"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; - -type BlogCard = BlogPost; +import React from 'react'; +import { CardStack } from '@/components/cardStack/CardStack'; interface BlogCardThreeProps { - blogs: BlogCard[]; - carouselMode?: "auto" | "buttons"; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; + blogs: Array<{ + id: string; + category: string; title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; - ariaLabel?: string; - className?: string; - containerClassName?: string; - cardClassName?: string; - cardContentClassName?: string; - categoryTagClassName?: string; - cardTitleClassName?: string; - excerptClassName?: string; - authorContainerClassName?: string; - authorAvatarClassName?: string; - authorNameClassName?: string; - dateClassName?: string; - mediaWrapperClassName?: string; - mediaClassName?: string; - textBoxTitleClassName?: string; - textBoxTitleImageWrapperClassName?: string; - textBoxTitleImageClassName?: string; - textBoxDescriptionClassName?: string; - gridClassName?: string; - carouselClassName?: string; - controlsClassName?: string; - textBoxClassName?: string; - textBoxTagClassName?: string; - textBoxButtonContainerClassName?: string; - textBoxButtonClassName?: string; - textBoxButtonTextClassName?: string; + excerpt: string; + imageSrc: string; + imageAlt?: string; + authorName: string; + authorAvatar: string; + date: string; + onBlogClick?: () => void; + }>; + title: string; + description: string; + animationType?: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; + textboxLayout?: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground?: boolean; + [key: string]: any; } -interface BlogCardItemProps { - blog: BlogCard; - useInvertedBackground: boolean; - cardClassName?: string; - cardContentClassName?: string; - categoryTagClassName?: string; - cardTitleClassName?: string; - excerptClassName?: string; - authorContainerClassName?: string; - authorAvatarClassName?: string; - authorNameClassName?: string; - dateClassName?: string; - mediaWrapperClassName?: string; - mediaClassName?: string; -} +const BlogCardThree: React.FC = ({ + blogs, + title, + description, + animationType = 'slide-up', + textboxLayout = 'default', + useInvertedBackground = false, + ...props +}) => { + const blogItems = blogs.map((blog) => ( +
+ {blog.imageAlt + {blog.category} +

{blog.title}

+

{blog.excerpt}

+
+ {blog.authorName} +
+

{blog.authorName}

+

{blog.date}

+
+
+
+ )); -const BlogCardItem = memo(({ - blog, - useInvertedBackground, - cardClassName = "", - cardContentClassName = "", - categoryTagClassName = "", - cardTitleClassName = "", - excerptClassName = "", - authorContainerClassName = "", - authorAvatarClassName = "", - authorNameClassName = "", - dateClassName = "", - mediaWrapperClassName = "", - mediaClassName = "", -}: BlogCardItemProps) => { - const theme = useTheme(); - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - - return ( -
-
- - -

- {blog.title} -

- -

- {blog.excerpt} -

- - {(blog.authorName || blog.date) && ( -
- {blog.authorAvatar && ( - {blog.authorName - )} - {blog.authorAvatar ? ( -
- {blog.authorName && ( -

- {blog.authorName} -

- )} - {blog.date && ( -

- {blog.date} -

- )} -
- ) : ( - <> - {blog.authorName && ( -

- {blog.authorName} -

- )} - {blog.date && ( -

- {blog.date} -

- )} - - )} -
- )} -
- -
- - -
-
- ); -}); - -BlogCardItem.displayName = "BlogCardItem"; - -const BlogCardThree = ({ - blogs = [], - carouselMode = "buttons", - uniformGridCustomHeightClasses = "min-h-none", - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - useInvertedBackground, - ariaLabel = "Blog section", - className = "", - containerClassName = "", - cardClassName = "", - cardContentClassName = "", - categoryTagClassName = "", - cardTitleClassName = "", - excerptClassName = "", - authorContainerClassName = "", - authorAvatarClassName = "", - authorNameClassName = "", - dateClassName = "", - mediaWrapperClassName = "", - mediaClassName = "", - textBoxTitleClassName = "", - textBoxTitleImageWrapperClassName = "", - textBoxTitleImageClassName = "", - textBoxDescriptionClassName = "", - gridClassName = "", - carouselClassName = "", - controlsClassName = "", - textBoxClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", -}: BlogCardThreeProps) => { - return ( - - {blogs.map((blog) => ( - - ))} - - ); + return ( + + {blogItems} + + ); }; -BlogCardThree.displayName = "BlogCardThree"; - -export default BlogCardThree; +export default BlogCardThree; \ No newline at end of file diff --git a/src/components/sections/blog/BlogCardTwo.tsx b/src/components/sections/blog/BlogCardTwo.tsx index c0b3f30..8966aca 100644 --- a/src/components/sections/blog/BlogCardTwo.tsx +++ b/src/components/sections/blog/BlogCardTwo.tsx @@ -1,241 +1,65 @@ -"use client"; - -import { memo } from "react"; -import Image from "next/image"; -import CardStack from "@/components/cardStack/CardStack"; -import Badge from "@/components/shared/Badge"; -import OverlayArrowButton from "@/components/shared/OverlayArrowButton"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import type { BlogPost } from "@/lib/api/blog"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; - -type BlogCard = Omit & { - category: string | string[]; -}; +import React from 'react'; +import { CardStack } from '@/components/cardStack/CardStack'; interface BlogCardTwoProps { - blogs: BlogCard[]; - carouselMode?: "auto" | "buttons"; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; + blogs: Array<{ + id: string; + category: string; title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; - ariaLabel?: string; - className?: string; - containerClassName?: string; - cardClassName?: string; - imageWrapperClassName?: string; - imageClassName?: string; - authorAvatarClassName?: string; - authorDateClassName?: string; - cardTitleClassName?: string; - excerptClassName?: string; - categoryClassName?: string; - textBoxTitleClassName?: string; - textBoxTitleImageWrapperClassName?: string; - textBoxTitleImageClassName?: string; - textBoxDescriptionClassName?: string; - gridClassName?: string; - carouselClassName?: string; - controlsClassName?: string; - textBoxClassName?: string; - textBoxTagClassName?: string; - textBoxButtonContainerClassName?: string; - textBoxButtonClassName?: string; - textBoxButtonTextClassName?: string; + excerpt: string; + imageSrc: string; + imageAlt?: string; + authorName: string; + authorAvatar: string; + date: string; + onBlogClick?: () => void; + }>; + title: string; + description: string; + animationType?: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; + textboxLayout?: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground?: boolean; + [key: string]: any; } -interface BlogCardItemProps { - blog: BlogCard; - shouldUseLightText: boolean; - cardClassName?: string; - imageWrapperClassName?: string; - imageClassName?: string; - authorAvatarClassName?: string; - authorDateClassName?: string; - cardTitleClassName?: string; - excerptClassName?: string; - categoryClassName?: string; -} +const BlogCardTwo: React.FC = ({ + blogs, + title, + description, + animationType = 'slide-up', + textboxLayout = 'default', + useInvertedBackground = false, + ...props +}) => { + const blogItems = blogs.map((blog) => ( +
+ {blog.imageAlt + {blog.category} +

{blog.title}

+

{blog.excerpt}

+
+ {blog.authorName} +
+

{blog.authorName}

+

{blog.date}

+
+
+
+ )); -const BlogCardItem = memo(({ - blog, - shouldUseLightText, - cardClassName = "", - imageWrapperClassName = "", - imageClassName = "", - authorAvatarClassName = "", - authorDateClassName = "", - cardTitleClassName = "", - excerptClassName = "", - categoryClassName = "", -}: BlogCardItemProps) => { - return ( -
-
- {blog.imageAlt - -
- -
-
-
- {blog.authorAvatar && ( - {blog.authorName} - )} -

- {blog.authorName} • {blog.date} -

-
- -

- {blog.title} -

- -

- {blog.excerpt} -

-
- -
- {Array.isArray(blog.category) ? ( - blog.category.map((cat, index) => ( - - )) - ) : ( - - )} -
-
-
- ); -}); - -BlogCardItem.displayName = "BlogCardItem"; - -const BlogCardTwo = ({ - blogs = [], - carouselMode = "buttons", - uniformGridCustomHeightClasses, - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - useInvertedBackground, - ariaLabel = "Blog section", - className = "", - containerClassName = "", - cardClassName = "", - imageWrapperClassName = "", - imageClassName = "", - authorAvatarClassName = "", - authorDateClassName = "", - cardTitleClassName = "", - excerptClassName = "", - categoryClassName = "", - textBoxTitleClassName = "", - textBoxTitleImageWrapperClassName = "", - textBoxTitleImageClassName = "", - textBoxDescriptionClassName = "", - gridClassName = "", - carouselClassName = "", - controlsClassName = "", - textBoxClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", -}: BlogCardTwoProps) => { - const theme = useTheme(); - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - - return ( - - {blogs.map((blog) => ( - - ))} - - ); + return ( + + {blogItems} + + ); }; -BlogCardTwo.displayName = "BlogCardTwo"; - -export default BlogCardTwo; +export default BlogCardTwo; \ No newline at end of file diff --git a/src/components/sections/contact/ContactCenter.tsx b/src/components/sections/contact/ContactCenter.tsx index e0cae92..5fa46a0 100644 --- a/src/components/sections/contact/ContactCenter.tsx +++ b/src/components/sections/contact/ContactCenter.tsx @@ -1,131 +1,57 @@ "use client"; -import ContactForm from "@/components/form/ContactForm"; -import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds"; -import { cls } from "@/lib/utils"; -import { LucideIcon } from "lucide-react"; -import { sendContactEmail } from "@/utils/sendContactEmail"; -import type { ButtonAnimationType } from "@/types/button"; - -type ContactCenterBackgroundProps = Extract< - HeroBackgroundVariantProps, - | { variant: "plain" } - | { variant: "animated-grid" } - | { variant: "canvas-reveal" } - | { variant: "cell-wave" } - | { variant: "downward-rays-animated" } - | { variant: "downward-rays-animated-grid" } - | { variant: "downward-rays-static" } - | { variant: "downward-rays-static-grid" } - | { variant: "gradient-bars" } - | { variant: "radial-gradient" } - | { variant: "rotated-rays-animated" } - | { variant: "rotated-rays-animated-grid" } - | { variant: "rotated-rays-static" } - | { variant: "rotated-rays-static-grid" } - | { variant: "sparkles-gradient" } ->; +import React, { useState } from 'react'; +import Input from '@/components/form/Input'; interface ContactCenterProps { - title: string; - description: string; - tag: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - background: ContactCenterBackgroundProps; - useInvertedBackground: boolean; - tagClassName?: string; - inputPlaceholder?: string; - buttonText?: string; - termsText?: string; - onSubmit?: (email: string) => void; - ariaLabel?: string; - className?: string; - containerClassName?: string; - contentClassName?: string; - titleClassName?: string; - descriptionClassName?: string; - formWrapperClassName?: string; - formClassName?: string; - inputClassName?: string; - buttonClassName?: string; - buttonTextClassName?: string; - termsClassName?: string; + title: string; + description?: string; + placeholder?: string; + buttonText?: string; } -const ContactCenter = ({ - title, - description, - tag, - tagIcon, - tagAnimation, - background, - useInvertedBackground, - tagClassName = "", - inputPlaceholder = "Enter your email", - buttonText = "Sign Up", - termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.", - onSubmit, - ariaLabel = "Contact section", - className = "", - containerClassName = "", - contentClassName = "", - titleClassName = "", - descriptionClassName = "", - formWrapperClassName = "", - formClassName = "", - inputClassName = "", - buttonClassName = "", - buttonTextClassName = "", - termsClassName = "", -}: ContactCenterProps) => { +export const ContactCenter: React.FC = ({ + title, + description, + placeholder = 'Enter your email', + buttonText = 'Submit', +}) => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); - const handleSubmit = async (email: string) => { - try { - await sendContactEmail({ email }); - console.log("Email send successfully"); - } catch (error) { - console.error("Failed to send email:", error); - } - }; + const handleSubmitForm = (e: React.FormEvent) => { + e.preventDefault(); + setSubmitted(true); + setEmail(''); + setTimeout(() => setSubmitted(false), 3000); + }; - return ( -
-
-
-
- -
-
- -
-
-
-
- ); + return ( +
+
+

{title}

+ {description &&

{description}

} +
+ +
+ + +
+ + {submitted && ( +

Thank you for your submission!

+ )} +
+ ); }; - -ContactCenter.displayName = "ContactCenter"; - -export default ContactCenter; diff --git a/src/components/sections/contact/ContactFaq.tsx b/src/components/sections/contact/ContactFaq.tsx index 386489a..8cb5042 100644 --- a/src/components/sections/contact/ContactFaq.tsx +++ b/src/components/sections/contact/ContactFaq.tsx @@ -1,188 +1,31 @@ -"use client"; - -import { useState, Fragment } from "react"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { getButtonProps } from "@/lib/buttonUtils"; -import Accordion from "@/components/Accordion"; -import Button from "@/components/button/Button"; -import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import type { LucideIcon } from "lucide-react"; -import type { InvertedBackground } from "@/providers/themeProvider/config/constants"; -import type { CardAnimationType } from "@/components/cardStack/types"; -import type { ButtonConfig } from "@/types/button"; - -interface FaqItem { - id: string; - title: string; - content: string; -} +import React, { useContext } from 'react'; +import { CardStackContext } from '@/components/cardStack/CardStackContext'; interface ContactFaqProps { - faqs: FaqItem[]; - ctaTitle: string; - ctaDescription: string; - ctaButton: ButtonConfig; - ctaIcon: LucideIcon; - useInvertedBackground: InvertedBackground; - animationType: CardAnimationType; - accordionAnimationType?: "smooth" | "instant"; - showCard?: boolean; - ariaLabel?: string; - className?: string; - containerClassName?: string; - ctaPanelClassName?: string; - ctaIconClassName?: string; - ctaTitleClassName?: string; - ctaDescriptionClassName?: string; - ctaButtonClassName?: string; - ctaButtonTextClassName?: string; - faqsPanelClassName?: string; - faqsContainerClassName?: string; - accordionClassName?: string; - accordionTitleClassName?: string; - accordionIconContainerClassName?: string; - accordionIconClassName?: string; - accordionContentClassName?: string; - separatorClassName?: string; + faqs: Array<{ + id: string; + title: string; + content: string; + }>; + title: string; + [key: string]: any; } -const ContactFaq = ({ - faqs, - ctaTitle, - ctaDescription, - ctaButton, - ctaIcon: CtaIcon, - useInvertedBackground, - animationType, - accordionAnimationType = "smooth", - showCard = true, - ariaLabel = "Contact and FAQ section", - className = "", - containerClassName = "", - ctaPanelClassName = "", - ctaIconClassName = "", - ctaTitleClassName = "", - ctaDescriptionClassName = "", - ctaButtonClassName = "", - ctaButtonTextClassName = "", - faqsPanelClassName = "", - faqsContainerClassName = "", - accordionClassName = "", - accordionTitleClassName = "", - accordionIconContainerClassName = "", - accordionIconClassName = "", - accordionContentClassName = "", - separatorClassName = "", -}: ContactFaqProps) => { - const [activeIndex, setActiveIndex] = useState(null); - const theme = useTheme(); - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - const { itemRefs } = useCardAnimation({ animationType, itemCount: 2 }); - - const handleToggle = (index: number) => { - setActiveIndex(activeIndex === index ? null : index); - }; - - const getButtonConfigProps = () => { - if (theme.defaultButtonVariant === "hover-bubble") { - return { bgClassName: "w-full" }; - } - if (theme.defaultButtonVariant === "icon-arrow") { - return { className: "justify-between" }; - } - return {}; - }; +const ContactFaq: React.FC = ({ faqs, title, ...props }) => { + const context = useContext(CardStackContext); + const animationProps = context ? context.getAnimationProps() : {}; return ( -
-
-
-
{ itemRefs.current[0] = el; }} - className={cls( - "md:col-span-4 card rounded-theme-capped p-6 md:p-8 flex flex-col items-center justify-center gap-6 text-center", - ctaPanelClassName - )} - > -
- -
- -
-

- {ctaTitle} -

- -

- {ctaDescription} -

-
- -
- -
{ itemRefs.current[1] = el; }} - className={cls( - "md:col-span-8 flex flex-col gap-4", - faqsPanelClassName - )} - > -
- {faqs.map((faq, index) => ( - - - {!showCard && index < faqs.length - 1 && ( -
- )} - - ))} -
-
+
+

{title}

+ {faqs.map((faq) => ( +
+

{faq.title}

+

{faq.content}

-
-
+ ))} +
); }; -ContactFaq.displayName = "ContactFaq"; - -export default ContactFaq; +export default ContactFaq; \ No newline at end of file diff --git a/src/components/sections/contact/ContactSplit.tsx b/src/components/sections/contact/ContactSplit.tsx index 9f7ca93..e4e10bb 100644 --- a/src/components/sections/contact/ContactSplit.tsx +++ b/src/components/sections/contact/ContactSplit.tsx @@ -1,171 +1,116 @@ "use client"; -import ContactForm from "@/components/form/ContactForm"; -import MediaContent from "@/components/shared/MediaContent"; -import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds"; -import { cls } from "@/lib/utils"; -import { useButtonAnimation } from "@/components/hooks/useButtonAnimation"; -import { LucideIcon } from "lucide-react"; -import { sendContactEmail } from "@/utils/sendContactEmail"; -import type { ButtonAnimationType } from "@/types/button"; - -type ContactSplitBackgroundProps = Extract< - HeroBackgroundVariantProps, - | { variant: "plain" } - | { variant: "animated-grid" } - | { variant: "canvas-reveal" } - | { variant: "cell-wave" } - | { variant: "downward-rays-animated" } - | { variant: "downward-rays-animated-grid" } - | { variant: "downward-rays-static" } - | { variant: "downward-rays-static-grid" } - | { variant: "gradient-bars" } - | { variant: "radial-gradient" } - | { variant: "rotated-rays-animated" } - | { variant: "rotated-rays-animated-grid" } - | { variant: "rotated-rays-static" } - | { variant: "rotated-rays-static-grid" } - | { variant: "sparkles-gradient" } ->; +import React, { useState } from 'react'; +import Input from '@/components/form/Input'; interface ContactSplitProps { - title: string; - description: string; - tag: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - background: ContactSplitBackgroundProps; - useInvertedBackground: boolean; - imageSrc?: string; - videoSrc?: string; - imageAlt?: string; - videoAriaLabel?: string; - mediaPosition?: "left" | "right"; - mediaAnimation: ButtonAnimationType; - inputPlaceholder?: string; - buttonText?: string; - termsText?: string; - onSubmit?: (email: string) => void; - ariaLabel?: string; - className?: string; - containerClassName?: string; - contentClassName?: string; - contactFormClassName?: string; - tagClassName?: string; - titleClassName?: string; - descriptionClassName?: string; - formWrapperClassName?: string; - formClassName?: string; - inputClassName?: string; - buttonClassName?: string; - buttonTextClassName?: string; - termsClassName?: string; - mediaWrapperClassName?: string; - mediaClassName?: string; + tag: string; + title: string; + description: string; + tagIcon?: React.ComponentType; + tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + background: { variant: string }; + useInvertedBackground: boolean; + imageSrc?: string; + videoSrc?: string; + imageAlt?: string; + videoAriaLabel?: string; + mediaAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + mediaPosition?: 'left' | 'right'; + inputPlaceholder?: string; + buttonText?: string; + termsText?: string; + ariaLabel?: string; + className?: string; + containerClassName?: string; + contentClassName?: string; + contactFormClassName?: string; + tagClassName?: string; + titleClassName?: string; + descriptionClassName?: string; + formWrapperClassName?: string; + formClassName?: string; + inputClassName?: string; + buttonClassName?: string; + buttonTextClassName?: string; + termsClassName?: string; + mediaWrapperClassName?: string; + mediaClassName?: string; } -const ContactSplit = ({ - title, - description, - tag, - tagIcon, - tagAnimation, - background, - useInvertedBackground, - imageSrc, - videoSrc, - imageAlt = "", - videoAriaLabel = "Contact section video", - mediaPosition = "right", - mediaAnimation, - inputPlaceholder = "Enter your email", - buttonText = "Sign Up", - termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.", - onSubmit, - ariaLabel = "Contact section", - className = "", - containerClassName = "", - contentClassName = "", - contactFormClassName = "", - tagClassName = "", - titleClassName = "", - descriptionClassName = "", - formWrapperClassName = "", - formClassName = "", - inputClassName = "", - buttonClassName = "", - buttonTextClassName = "", - termsClassName = "", - mediaWrapperClassName = "", - mediaClassName = "", -}: ContactSplitProps) => { - const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation }); +const ContactSplit: React.FC = ({ + tag, + title, + description, + tagIcon: TagIcon, + useInvertedBackground, + imageSrc, + videoSrc, + imageAlt, + inputPlaceholder = 'Enter your email', + buttonText = 'Sign Up', + termsText = 'By clicking Sign Up you are agreeing to our terms and conditions.', +}) => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); - const handleSubmit = async (email: string) => { - try { - await sendContactEmail({ email }); - console.log("Email send successfully"); - } catch (error) { - console.error("Failed to send email:", error); - } - }; + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setSubmitted(true); + setEmail(''); + setTimeout(() => setSubmitted(false), 3000); + }; - const contactContent = ( -
- -
- + return ( +
+
+
+ {/* Text Content */} +
+ {TagIcon && ( +
+ + {tag} +
+ )} +

{title}

+

{description}

+ + {/* Form */} +
+ + +
+ + {submitted && ( +

Thank you for signing up!

+ )} + +

{termsText}

+
+ + {/* Media */} + {(imageSrc || videoSrc) && ( +
+ {imageSrc && {imageAlt} + {videoSrc &&
+ )}
- ); - - const mediaContent = ( -
- -
- ); - - return ( -
-
-
- {mediaPosition === "left" && mediaContent} - {contactContent} - {mediaPosition === "right" && mediaContent} -
-
-
- ); +
+
+ ); }; -ContactSplit.displayName = "ContactSplit"; - export default ContactSplit; diff --git a/src/components/sections/contact/ContactSplitForm.tsx b/src/components/sections/contact/ContactSplitForm.tsx index 15ed065..227f557 100644 --- a/src/components/sections/contact/ContactSplitForm.tsx +++ b/src/components/sections/contact/ContactSplitForm.tsx @@ -1,214 +1,59 @@ "use client"; -import { useState } from "react"; -import TextAnimation from "@/components/text/TextAnimation"; -import Button from "@/components/button/Button"; -import Input from "@/components/form/Input"; -import Textarea from "@/components/form/Textarea"; -import MediaContent from "@/components/shared/MediaContent"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import { useButtonAnimation } from "@/components/hooks/useButtonAnimation"; -import { getButtonProps } from "@/lib/buttonUtils"; -import type { AnimationType } from "@/components/text/types"; -import type { ButtonAnimationType } from "@/types/button"; -import {sendContactEmail} from "@/utils/sendContactEmail"; - -export interface InputField { - name: string; - type: string; - placeholder: string; - required?: boolean; - className?: string; -} - -export interface TextareaField { - name: string; - placeholder: string; - rows?: number; - required?: boolean; - className?: string; -} +import React, { useState } from 'react'; +import Input from '@/components/form/Input'; interface ContactSplitFormProps { - title: string; - description: string; - inputs: InputField[]; - textarea?: TextareaField; - useInvertedBackground: boolean; - imageSrc?: string; - videoSrc?: string; - imageAlt?: string; - videoAriaLabel?: string; - mediaPosition?: "left" | "right"; - mediaAnimation: ButtonAnimationType; - buttonText?: string; - onSubmit?: (data: Record) => void; - ariaLabel?: string; - className?: string; - containerClassName?: string; - contentClassName?: string; - formCardClassName?: string; - titleClassName?: string; - descriptionClassName?: string; - buttonClassName?: string; - buttonTextClassName?: string; - mediaWrapperClassName?: string; - mediaClassName?: string; + title: string; + description?: string; + placeholder?: string; + buttonText?: string; } -const ContactSplitForm = ({ - title, - description, - inputs, - textarea, - useInvertedBackground, - imageSrc, - videoSrc, - imageAlt = "", - videoAriaLabel = "Contact section video", - mediaPosition = "right", - mediaAnimation, - buttonText = "Submit", - onSubmit, - ariaLabel = "Contact section", - className = "", - containerClassName = "", - contentClassName = "", - formCardClassName = "", - titleClassName = "", - descriptionClassName = "", - buttonClassName = "", - buttonTextClassName = "", - mediaWrapperClassName = "", - mediaClassName = "", -}: ContactSplitFormProps) => { - const theme = useTheme(); - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation }); +const ContactSplitForm: React.FC = ({ + title, + description, + placeholder = 'Enter your email', + buttonText = 'Submit', +}) => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); - // Validate minimum inputs requirement - if (inputs.length < 2) { - throw new Error("ContactSplitForm requires at least 2 inputs"); - } + const handleSubmitForm = (e: React.FormEvent) => { + e.preventDefault(); + setSubmitted(true); + setEmail(''); + setTimeout(() => setSubmitted(false), 3000); + }; - // Initialize form data dynamically - const initialFormData: Record = {}; - inputs.forEach(input => { - initialFormData[input.name] = ""; - }); - if (textarea) { - initialFormData[textarea.name] = ""; - } + return ( +
+
+

{title}

+ {description &&

{description}

} +
- const [formData, setFormData] = useState(initialFormData); +
+ + +
- const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - try { - await sendContactEmail({ formData }); - console.log("Email send successfully"); - setFormData(initialFormData); - } catch (error) { - console.error("Failed to send email:", error); - } - }; - - const getButtonConfigProps = () => { - if (theme.defaultButtonVariant === "hover-bubble") { - return { bgClassName: "w-full" }; - } - if (theme.defaultButtonVariant === "icon-arrow") { - return { className: "justify-between" }; - } - return {}; - }; - - const formContent = ( -
-
-
- - - -
- -
- {inputs.map((input) => ( - setFormData({ ...formData, [input.name]: value })} - required={input.required} - ariaLabel={input.placeholder} - className={input.className} - /> - ))} - - {textarea && ( -