├── .gitignore
├── src
├── index.css
├── Components
│ ├── ScrollToTop.jsx
│ ├── FitnessAdComponent.jsx
│ ├── AdvancedSliderFeature.jsx
│ ├── FeaturedCards.jsx
│ ├── NearbyGyms.jsx
│ ├── Muscles.jsx
│ ├── PaymentModal.jsx
│ ├── FeatureShowcase.jsx
│ └── NavBar.jsx
├── main.jsx
├── Loaders
│ ├── Loader.jsx
│ └── LoadingContext.jsx
├── services
│ └── gymService.js
├── Exercises
│ ├── ExerciseList.jsx
│ └── WorkoutPlan.jsx
├── App.jsx
├── App.css
├── Pages
│ ├── NearbyGymsPage.jsx
│ ├── BrowseExercise.jsx
│ ├── DefaultPage.jsx
│ ├── Plans.jsx
│ ├── AllExercises.jsx
│ ├── MyWorkoutPlans.jsx
│ ├── FoodAnalysis.jsx
│ └── WorkoutPlanDetail.jsx
├── assets
│ └── react.svg
└── Logins
│ ├── SignIn.jsx
│ └── Register.jsx
├── vercel.json
├── postcss.config.js
├── tailwind.config.js
├── vite.config.js
├── README.md
├── index.html
├── eslint.config.js
├── package.json
├── public
└── vite.svg
└── AI_TRAINER_README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .env/
2 | node_modules/
3 | .env
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {"source": "/(.*)", "destination": "/"}
4 | ]
5 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | base: '/', // This ensures assets are loaded correctly
8 | build: {
9 | outDir: 'dist'
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/src/Components/ScrollToTop.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 |
4 | const ScrollToTop = () => {
5 | const { pathname } = useLocation();
6 |
7 | useEffect(() => {
8 | window.scrollTo(0, 0);
9 | }, [pathname]);
10 |
11 | return null;
12 | };
13 |
14 | export default ScrollToTop;
15 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.jsx'
5 | import { BrowserRouter } from 'react-router-dom'
6 | import ScrollToTop from './Components/ScrollToTop'
7 |
8 | createRoot(document.getElementById('root')).render(
9 |
10 |
11 |
12 |
13 |
14 | ,
15 | )
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/src/Loaders/Loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { motion } from 'framer-motion'
3 |
4 | const Loader = () => {
5 | return (
6 |
7 |
22 |
23 | )
24 | }
25 |
26 | export default Loader
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Eleweight
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Loaders/LoadingContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import Loader from './Loader';
4 | const LoadingContext = createContext();
5 |
6 | export const LoadingProvider = ({ children }) => {
7 | const [loading, setLoading] = useState(false);
8 | const location = useLocation();
9 |
10 | useEffect(() => {
11 | setLoading(true);
12 | const timer = setTimeout(() => {
13 | setLoading(false);
14 | }, 800);
15 |
16 | return () => clearTimeout(timer);
17 | }, [location.pathname]);
18 |
19 | return (
20 |
21 | {loading && }
22 | {children}
23 |
24 | );
25 | };
26 |
27 | export const useLoading = () => useContext(LoadingContext);
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import react from 'eslint-plugin-react'
4 | import reactHooks from 'eslint-plugin-react-hooks'
5 | import reactRefresh from 'eslint-plugin-react-refresh'
6 |
7 | export default [
8 | { ignores: ['dist'] },
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: 'latest',
16 | ecmaFeatures: { jsx: true },
17 | sourceType: 'module',
18 | },
19 | },
20 | settings: { react: { version: '18.3' } },
21 | plugins: {
22 | react,
23 | 'react-hooks': reactHooks,
24 | 'react-refresh': reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs['jsx-runtime'].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | 'react/jsx-no-target-blank': 'off',
32 | 'react-refresh/only-export-components': [
33 | 'warn',
34 | { allowConstantExport: true },
35 | ],
36 | },
37 | },
38 | ]
39 |
--------------------------------------------------------------------------------
/src/services/gymService.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const MAPS_API_KEY = import.meta.env.VITE_MAPS_API_KEY
4 |
5 | /**
6 | * Fetch nearby gyms based on user location
7 | * @param {Object} params - Search parameters
8 | * @param {string} params.latitude - User's latitude
9 | * @param {string} params.longitude - User's longitude
10 | * @param {number} params.radius - Search radius in meters (default: 1500)
11 | * @returns {Promise} - Promise with the API response
12 | */
13 | export const getNearbyGyms = async ({ latitude, longitude, radius = 1500 }) => {
14 | try {
15 | const response = await axios.get(
16 | `https://maps.gomaps.pro/maps/api/place/nearbysearch/json`,
17 | {
18 | params: {
19 | keyword: 'gym',
20 | location: `${latitude},${longitude}`,
21 | name: 'gym',
22 | radius,
23 | key: MAPS_API_KEY
24 | },
25 | headers: {
26 | 'Accept': 'application/json'
27 | }
28 | }
29 | );
30 |
31 | return response.data;
32 | } catch (error) {
33 | console.error('Error fetching nearby gyms:', error);
34 | throw error;
35 | }
36 | };
37 |
38 | export default {
39 | getNearbyGyms
40 | };
--------------------------------------------------------------------------------
/src/Exercises/ExerciseList.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { useEffect, useState } from 'react';
3 | import { fetchExercises } from './ExerciseApi';
4 |
5 | const ExerciseList = () => {
6 | const [exercises, setExercises] = useState([]);
7 | const [loading, setLoading] = useState(true);
8 |
9 | useEffect(() => {
10 | const getExercises = async () => {
11 | try {
12 | const exerciseData = await fetchExercises();
13 | setExercises(exerciseData);
14 | } catch (error) {
15 | console.error('Failed to load exercises:', error);
16 | } finally {
17 | setLoading(false);
18 | }
19 | };
20 |
21 | getExercises();
22 | }, []);
23 |
24 | if (loading) return Loading exercises...
;
25 |
26 | return (
27 |
28 |
Exercise List
29 |
39 |
40 | );
41 | };
42 |
43 | export default ExerciseList;
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@cloudinary/react": "^1.14.1",
14 | "@cloudinary/url-gen": "^1.21.0",
15 | "@flaticon/flaticon-uicons": "^3.3.1",
16 | "@google/generative-ai": "^0.21.0",
17 | "axios": "^1.7.7",
18 | "framer-motion": "^11.11.11",
19 | "lucide-react": "^0.474.0",
20 | "react": "^18.3.1",
21 | "react-dom": "^18.3.1",
22 | "react-icons": "^5.3.0",
23 | "react-markdown": "^9.0.3",
24 | "react-router-dom": "^6.27.0",
25 | "react-webcam": "^7.2.0",
26 | "rehype-raw": "^7.0.0",
27 | "remark-gfm": "^4.0.1"
28 | },
29 | "devDependencies": {
30 | "@eslint/js": "^9.13.0",
31 | "@types/react": "^18.3.12",
32 | "@types/react-dom": "^18.3.1",
33 | "@vitejs/plugin-react": "^4.3.3",
34 | "autoprefixer": "^10.4.20",
35 | "eslint": "^9.13.0",
36 | "eslint-plugin-react": "^7.37.2",
37 | "eslint-plugin-react-hooks": "^5.0.0",
38 | "eslint-plugin-react-refresh": "^0.4.14",
39 | "globals": "^15.11.0",
40 | "postcss": "^8.4.47",
41 | "tailwindcss": "^3.4.14",
42 | "vite": "^5.4.10"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Register from './Logins/Register'
3 | import SignIn from './Logins/SignIn'
4 | import { Route , Routes } from 'react-router-dom'
5 | import Home from './Components/Home'
6 | import BrowseExercise from './Pages/BrowseExercise'
7 | import MuscleExercises from './Components/Muscles'
8 | import Plans from './Pages/Plans'
9 | import DietPlans from './Pages/DietPlans'
10 | import { LoadingProvider } from './Loaders/LoadingContext'
11 | import WorkoutPlan from './Exercises/WorkoutPlan'
12 | import AllExercises from './Pages/AllExercises'
13 | import CustomPlan from './Pages/CustomPlan'
14 | import MyWorkoutPlans from './Pages/MyWorkoutPlans'
15 | import WorkoutPlanDetail from './Pages/WorkoutPlanDetail'
16 | import EditPlan from './Pages/EditPlan'
17 | import NearbyGymsPage from './Pages/NearbyGymsPage'
18 | import FoodAnalysis from './Pages/FoodAnalysis'
19 | import AITrainerChatbot from './Components/AITrainerChatbot'
20 |
21 | const App = () => {
22 |
23 | return (
24 |
25 |
26 |
27 | } >
28 | } >
29 | } >
30 | } >
31 | } >
32 | } >
33 | } >
34 | } />
35 | } />
36 | } />
37 | } />
38 | } />
39 | } />
40 | } />
41 | } />
42 | } />
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
50 | export default App
51 |
--------------------------------------------------------------------------------
/AI_TRAINER_README.md:
--------------------------------------------------------------------------------
1 | # AI Gym Trainer Chatbot
2 |
3 | ## Overview
4 | The AI Gym Trainer Chatbot is a specialized assistant that helps users with fitness, gym workouts, and healthcare-related questions. It's powered by Google's Gemini AI and is designed to provide personalized workout guidance, exercise tutorials, and equipment usage instructions.
5 |
6 | ## Features
7 |
8 | ### 1. Fitness Knowledge Base
9 | - Answers questions about workouts, exercises, and fitness routines
10 | - Provides guidance on proper form and technique
11 | - Offers diet and nutrition advice related to fitness goals
12 | - Suggests workout plans based on user goals and equipment availability
13 |
14 | ### 2. Gym Equipment Recognition
15 | - Upload or capture images of gym machines
16 | - AI identifies the equipment and provides:
17 | - Usage instructions
18 | - Target muscle groups
19 | - Exercise benefits
20 | - Proper form guidance
21 |
22 | ### 3. Restricted to Fitness Topics
23 | - The AI is programmed to only discuss topics related to fitness, gym, and healthcare
24 | - It will politely decline to answer questions outside these domains
25 |
26 | ## How to Use
27 |
28 | ### Text Queries
29 | 1. Click the dumbbell icon in the bottom-right corner of any page
30 | 2. Type your fitness-related question in the text input
31 | 3. Press Enter or click the send button
32 | 4. Receive personalized fitness guidance
33 |
34 | ### Image Analysis
35 | 1. Open the chatbot by clicking the dumbbell icon
36 | 2. Click the "Camera" button to use your device's camera or "Upload" to select an image
37 | 3. If using the camera, click "Capture" when the gym equipment is in frame
38 | 4. The AI will analyze the image and provide detailed information about the equipment
39 |
40 | ## Technical Implementation
41 |
42 | The AI Trainer Chatbot is built using:
43 | - React for the frontend UI
44 | - Google Gemini API for AI processing
45 | - React Webcam for camera functionality
46 | - Tailwind CSS for styling
47 |
48 | The chatbot is designed to be non-intrusive, appearing as a floating button that expands into a chat window when clicked.
49 |
50 | ## Privacy Note
51 |
52 | Images uploaded or captured are only used for the immediate analysis and are not stored on our servers. All processing is done via the Google Gemini API with appropriate privacy measures in place.
53 |
54 | ## Feedback and Improvements
55 |
56 | We're constantly working to improve the AI Trainer's knowledge and capabilities. If you have suggestions or encounter any issues, please let us know through the feedback form in the app settings.
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .calculator-container {
2 | max-width: 800px;
3 | margin: 0 auto;
4 | padding: 20px;
5 | font-family: Arial, sans-serif;
6 | }
7 |
8 | .calculator-title {
9 | text-align: center;
10 | color: #333;
11 | margin-bottom: 30px;
12 | }
13 |
14 | .calculator-form {
15 | margin-bottom: 30px;
16 | }
17 |
18 | .form-grid {
19 | display: grid;
20 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
21 | gap: 20px;
22 | margin-bottom: 20px;
23 | }
24 |
25 | .form-group {
26 | display: flex;
27 | flex-direction: column;
28 | }
29 |
30 | .form-group label {
31 | margin-bottom: 8px;
32 | font-weight: 500;
33 | color: #444;
34 | }
35 |
36 | .form-group input,
37 | .form-group select {
38 | padding: 10px;
39 | border: 1px solid #ddd;
40 | border-radius: 4px;
41 | font-size: 14px;
42 | transition: border-color 0.3s ease;
43 | }
44 |
45 | .form-group input:focus,
46 | .form-group select:focus {
47 | outline: none;
48 | border-color: #2196F3;
49 | }
50 |
51 | .error-message {
52 | padding: 12px;
53 | margin-bottom: 15px;
54 | background-color: #ffebee;
55 | color: #c62828;
56 | border-radius: 4px;
57 | font-size: 14px;
58 | }
59 |
60 | .submit-button {
61 | width: 100%;
62 | padding: 12px;
63 | background-color: #4CAF50;
64 | color: white;
65 | border: none;
66 | border-radius: 4px;
67 | cursor: pointer;
68 | font-size: 16px;
69 | transition: background-color 0.3s ease;
70 | }
71 |
72 | .submit-button:hover {
73 | background-color: #43A047;
74 | }
75 |
76 | .submit-button.loading {
77 | background-color: #9E9E9E;
78 | cursor: not-allowed;
79 | }
80 |
81 | .results-container {
82 | margin-top: 30px;
83 | }
84 |
85 | .metrics-grid {
86 | display: grid;
87 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
88 | gap: 20px;
89 | margin-bottom: 20px;
90 | }
91 |
92 | .metric-card {
93 | padding: 20px;
94 | background-color: #f5f5f5;
95 | border-radius: 8px;
96 | text-align: center;
97 | transition: transform 0.3s ease;
98 | }
99 |
100 | .metric-card:hover {
101 | transform: translateY(-2px);
102 | }
103 |
104 | .metric-card h3 {
105 | margin: 0 0 10px 0;
106 | color: #2196F3;
107 | font-size: 24px;
108 | }
109 |
110 | .metric-card p {
111 | margin: 0;
112 | color: #666;
113 | font-size: 14px;
114 | }
115 |
116 | .diet-plan-card {
117 | padding: 20px;
118 | background-color: #f5f5f5;
119 | border-radius: 8px;
120 | }
121 |
122 | .diet-plan-card h3 {
123 | margin: 0 0 15px 0;
124 | color: #333;
125 | }
126 |
127 | .diet-plan-card p {
128 | margin: 0;
129 | line-height: 1.6;
130 | color: #444;
131 | }
132 |
133 | /* Responsive adjustments */
134 | @media (max-width: 600px) {
135 | .calculator-container {
136 | padding: 15px;
137 | }
138 |
139 | .form-grid {
140 | grid-template-columns: 1fr;
141 | }
142 |
143 | .metrics-grid {
144 | grid-template-columns: 1fr;
145 | }
146 | }
--------------------------------------------------------------------------------
/src/Pages/NearbyGymsPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import NavBar from '../Components/NavBar';
4 | import NearbyGyms from '../Components/NearbyGyms';
5 | import { MapPin } from 'lucide-react';
6 |
7 | const NearbyGymsPage = () => {
8 | const navigate = useNavigate();
9 | const token = localStorage.getItem('token');
10 |
11 | useEffect(() => {
12 | if (!token) {
13 | navigate("/login");
14 | }
15 | }, [token, navigate]);
16 |
17 | return (
18 |
19 |
20 |
21 | {/* Hero Section */}
22 |
23 |
24 |
25 |
26 |
27 |
Find Gyms Near You
28 |
29 | Discover fitness centers in your area to kickstart your fitness journey.
30 | We'll help you find the perfect gym based on your location.
31 |
32 |
33 |
34 |
35 | {/* Main Content */}
36 |
39 |
40 | {/* FAQ Section */}
41 |
42 |
43 |
Frequently Asked Questions
44 |
45 |
46 | {[
47 | {
48 | question: "How does the gym finder work?",
49 | answer: "Our gym finder uses your current location (with your permission) to search for fitness centers near you. We display information like ratings, opening hours, and directions to help you choose the right gym."
50 | },
51 | {
52 | question: "Why do I need to enable location services?",
53 | answer: "Location services help us find gyms that are closest to your current position. This ensures you get the most relevant results. If you prefer not to share your location, we'll use a default location instead."
54 | },
55 | {
56 | question: "Can I adjust the search radius?",
57 | answer: "Yes! You can use the slider to adjust how far you want to search for gyms, from 500 meters up to 5 kilometers."
58 | },
59 | {
60 | question: "Are the gym ratings accurate?",
61 | answer: "The ratings shown are based on user reviews from Google Maps. They reflect the experiences of people who have visited these gyms."
62 | }
63 | ].map((faq, index) => (
64 |
65 |
{faq.question}
66 |
{faq.answer}
67 |
68 | ))}
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default NearbyGymsPage;
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Components/FitnessAdComponent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CheckCircle, ArrowRight } from 'lucide-react';
3 |
4 | const FitnessAdComponent = () => {
5 | return (
6 |
7 |
8 | {/* Image Grid Section - Enhanced with overlays and better spacing */}
9 |
10 |
11 |
12 |
13 |
FITNESS PROGRAM
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
Join 2,500+ members
55 |
56 |
57 |
58 |
59 | {/* Content Section - Enhanced with better typography and spacing */}
60 |
61 |
62 |
63 |
64 | Transform your physique with our fitness plan
65 |
66 |
67 |
68 | Join our community and achieve your fitness goals with personalized workout plans and expert guidance.
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Increase Muscle and Strength
77 |
78 |
79 |
80 |
81 |
82 |
Be Healthier than before
83 |
84 |
85 |
86 |
87 |
88 |
Increase Stamina
89 |
90 |
91 |
92 |
93 |
94 | Join now
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default FitnessAdComponent;
--------------------------------------------------------------------------------
/src/Pages/BrowseExercise.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Link, Navigate, useLocation } from 'react-router-dom';
3 | import { motion } from 'framer-motion';
4 | import NavBar from '../Components/NavBar';
5 | import { Dumbbell, Trophy, ChevronRight, Target } from 'lucide-react';
6 |
7 | const MuscleCard = ({ to, imageSrc, title, exercises, difficulty, benefits }) => (
8 |
14 |
15 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 | Explore exercises
32 |
33 |
34 |
35 |
36 |
37 |
38 | {title}
39 |
40 |
41 | {exercises} exercises
42 |
43 |
44 |
45 |
46 |
47 |
48 | {difficulty}
49 |
50 |
51 |
52 | {benefits}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
65 |
66 |
67 | );
68 |
69 | const BrowseExercise = () => {
70 | const location = useLocation();
71 | const token = localStorage.getItem('token');
72 |
73 | useEffect(() => {
74 | if (!token) {
75 | navigate("/login");
76 | }
77 | }, [token, Navigate]);
78 |
79 |
80 | useEffect(() => {
81 | window.scrollTo(0, 0);
82 | }, [location]);
83 |
84 | const muscles = [
85 | {
86 | id: 'chest',
87 | title: 'Chest',
88 | image: 'https://i.pinimg.com/474x/52/86/62/5286624abd2910a4f11157be790fafd9.jpg',
89 | exercises: '12',
90 | difficulty: 'Intermediate',
91 | benefits: 'Upper Body Strength'
92 | },
93 | {
94 | id: 'back',
95 | title: 'Back',
96 | image: 'https://i.pinimg.com/474x/ad/1d/cd/ad1dcdec4554e11afd0961795b6984f1.jpg',
97 | exercises: '15',
98 | difficulty: 'Advanced',
99 | benefits: 'Posture & Power'
100 | },
101 | {
102 | id: 'shoulders',
103 | title: 'Shoulders',
104 | image: 'https://i.pinimg.com/474x/9a/f4/ed/9af4edf7dfea3b33aa498007aaf5e173.jpg',
105 | exercises: '10',
106 | difficulty: 'All Levels',
107 | benefits: '3D Definition'
108 | },
109 | {
110 | id: 'arms',
111 | title: 'Arms',
112 | image: 'https://i.pinimg.com/474x/94/fc/97/94fc978575c901e4e40dee99c762a923.jpg',
113 | exercises: '14',
114 | difficulty: 'Beginner',
115 | benefits: 'Muscle Growth'
116 | },
117 | {
118 | id: 'core',
119 | title: 'Core',
120 | image: 'https://i.pinimg.com/474x/1e/cd/a9/1ecda9a944ff50c39ef0de25b6813347.jpg',
121 | exercises: '16',
122 | difficulty: 'All Levels',
123 | benefits: 'Stability & Strength'
124 | },
125 | {
126 | id: 'legs',
127 | title: 'Legs',
128 | image: 'https://i.pinimg.com/474x/50/b7/f8/50b7f82b56a8796932911e5753328682.jpg',
129 | exercises: '18',
130 | difficulty: 'Advanced',
131 | benefits: 'Power & Size'
132 | }
133 | ];
134 |
135 | return (
136 |
137 |
138 |
139 |
140 |
146 |
147 |
148 | Target Your
149 |
150 |
151 | {" "}Muscle Groups
152 |
153 |
154 |
155 | Select a muscle group to discover targeted exercises and build your perfect workout
156 |
157 |
158 |
159 |
160 | {muscles.map((muscle, index) => (
161 |
170 | ))}
171 |
172 |
173 |
174 | );
175 | };
176 |
177 | export default BrowseExercise;
--------------------------------------------------------------------------------
/src/Components/AdvancedSliderFeature.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import { ChevronLeft, ChevronRight, Play, Pause } from 'lucide-react';
4 |
5 | const AdvancedFeatureSlider = ({ features, autoplay = true, interval = 5000 }) => {
6 | const [currentIndex, setCurrentIndex] = useState(0);
7 | const [isPlaying, setIsPlaying] = useState(autoplay);
8 | const [width, setWidth] = useState(0);
9 | const [isHovering, setIsHovering] = useState(false);
10 |
11 | const itemsToShow = width >= 1024 ? 3 : width >= 640 ? 2 : 1;
12 | const totalSlides = Math.ceil(features.length / itemsToShow);
13 |
14 | useEffect(() => {
15 | const handleResize = () => {
16 | setWidth(window.innerWidth);
17 | };
18 |
19 | handleResize();
20 | window.addEventListener('resize', handleResize);
21 | return () => window.removeEventListener('resize', handleResize);
22 | }, []);
23 |
24 | const nextSlide = useCallback(() => {
25 | setCurrentIndex((prevIndex) =>
26 | prevIndex === totalSlides - 1 ? 0 : prevIndex + 1
27 | );
28 | }, [totalSlides]);
29 |
30 | const prevSlide = () => {
31 | setCurrentIndex((prevIndex) =>
32 | prevIndex === 0 ? totalSlides - 1 : prevIndex - 1
33 | );
34 | };
35 |
36 | const goToSlide = (index) => {
37 | setCurrentIndex(index);
38 | };
39 |
40 | const toggleAutoplay = () => {
41 | setIsPlaying(!isPlaying);
42 | };
43 |
44 | useEffect(() => {
45 | let timer;
46 | if (isPlaying && !isHovering) {
47 | timer = setInterval(nextSlide, interval);
48 | }
49 | return () => {
50 | if (timer) clearInterval(timer);
51 | };
52 | }, [isPlaying, isHovering, nextSlide, interval]);
53 |
54 | return (
55 | setIsHovering(true)}
58 | onMouseLeave={() => setIsHovering(false)}
59 | >
60 | {/* Gradient Background */}
61 |
62 |
63 |
64 | {/* Navigation Controls */}
65 |
66 |
71 |
72 |
73 |
74 |
79 |
80 |
81 |
82 |
83 | {/* Slider Container */}
84 |
85 |
90 | {features.map((feature, index) => (
91 | = 2 ? 'sm:w-1/2' : ''} p-4`}
94 | >
95 |
101 |
102 | ))}
103 |
104 |
105 |
106 | {/* Controls and Indicators */}
107 |
108 | {/* Autoplay Toggle */}
109 |
113 | {isPlaying ? (
114 | <>
115 |
116 | Pause
117 | >
118 | ) : (
119 | <>
120 |
121 | Play
122 | >
123 | )}
124 |
125 |
126 | {/* Dots Indicator */}
127 |
128 | {Array.from({ length: totalSlides }).map((_, index) => (
129 | goToSlide(index)}
132 | className={`h-2 rounded-full transition-all duration-300 ${
133 | currentIndex === index ? 'bg-blue-600 w-6' : 'bg-gray-300 w-2'
134 | }`}
135 | aria-label={`Go to slide ${index + 1}`}
136 | />
137 | ))}
138 |
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | const FeatureCard = ({ title, description, icon: Icon, link }) => {
146 | return (
147 |
151 | {/* Decorative Elements */}
152 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | {title}
164 |
165 |
166 |
167 |
168 | {description}
169 |
170 |
171 |
172 | Explore
173 |
174 |
175 |
176 |
177 | );
178 | };
179 |
180 | export default AdvancedFeatureSlider;
--------------------------------------------------------------------------------
/src/Pages/DefaultPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { ArrowRight, Activity, Heart, Users, Target, Calendar, Star, ChevronLeft, ChevronRight } from 'lucide-react';
4 |
5 | const DefaultPage = () => {
6 | const navigate = useNavigate();
7 | const [isHovered, setIsHovered] = useState(false);
8 | const [currentTestimonial, setCurrentTestimonial] = useState(0);
9 |
10 | const features = [
11 | { icon: , title: "Workout Tracking", description: "Log and monitor your exercises with ease" },
12 | { icon: , title: "Health Metrics", description: "Track vital statistics and progress" },
13 | { icon: , title: "Community", description: "Connect with fitness enthusiasts" },
14 | { icon: , title: "Goal Setting", description: "Set and achieve your fitness targets" },
15 | { icon: , title: "Smart Scheduling", description: "Plan your workouts efficiently" }
16 | ];
17 |
18 | const testimonials = [
19 | { name: "Karan K.", role: "Fitness Enthusiast", quote: "Eleweight has transformed my fitness journey. I love how easy it is to track my workouts and stay motivated!", rating: 5 },
20 | { name: "Sarah M.", role: "Personal Trainer", quote: "The best fitness tracking app I've ever used. My clients love the intuitive interface!", rating: 5 },
21 | { name: "John D.", role: "Gym Owner", quote: "Eleweight has helped my gym members stay consistent and achieve their goals faster.", rating: 4 },
22 | { name: "Emily R.", role: "Yoga Instructor", quote: "The app's scheduling feature is a game-changer for my yoga classes.", rating: 5 }
23 | ];
24 |
25 | const nextTestimonial = () => {
26 | setCurrentTestimonial((prev) => (prev + 1) % testimonials.length);
27 | };
28 |
29 | const prevTestimonial = () => {
30 | setCurrentTestimonial((prev) => (prev - 1 + testimonials.length) % testimonials.length);
31 | };
32 |
33 | useEffect(() => {
34 | const interval = setInterval(nextTestimonial, 5000);
35 | return () => clearInterval(interval);
36 | }, []);
37 |
38 | return (
39 |
40 | {/* Hero Section */}
41 |
42 |
43 |
44 |
49 |
50 |
51 |
52 |
53 | Welcome to Eleweight
54 |
55 |
56 | Your ultimate fitness companion for tracking workouts, managing nutrition, and achieving your fitness goals.
57 |
58 |
59 |
60 |
navigate('/login')}
62 | onMouseEnter={() => setIsHovered(true)}
63 | onMouseLeave={() => setIsHovered(false)}
64 | className="group relative px-8 py-3 bg-purple-500 text-white rounded-full font-semibold text-lg transition-all duration-300 hover:bg-purple-600 hover:shadow-lg"
65 | >
66 | Get Started
67 |
68 |
69 |
navigate('/register')}
71 | className="px-8 py-3 bg-white text-gray-900 rounded-full font-semibold text-lg transition-all duration-300 hover:bg-gray-100 hover:shadow-lg"
72 | >
73 | Sign Up
74 |
75 |
76 |
77 |
78 |
79 | {/* Features Section */}
80 |
81 |
82 |
Why Choose Eleweight?
83 |
84 | {features.map((feature, index) => (
85 |
86 |
87 | {feature.icon}
88 |
89 |
{feature.title}
90 |
{feature.description}
91 |
92 | ))}
93 |
94 |
95 |
96 |
97 | {/* Testimonials Section */}
98 |
99 |
100 |
What Our Users Say
101 |
102 |
106 |
107 |
108 |
109 |
{testimonials[currentTestimonial].quote}
110 |
111 |
112 |
113 |
114 |
{testimonials[currentTestimonial].name}
115 |
{testimonials[currentTestimonial].role}
116 |
117 |
118 |
119 | {[...Array(testimonials[currentTestimonial].rating)].map((_, i) => (
120 |
121 | ))}
122 |
123 |
124 |
125 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | {/* CTA Section */}
136 |
137 |
138 |
Ready to Start Your Fitness Journey?
139 | navigate('/register')}
141 | className="px-8 py-3 bg-white text-indigo-500 rounded-full font-semibold text-lg transition-all duration-300 hover:bg-gray-100 hover:shadow-lg"
142 | >
143 | Join Eleweight Today
144 |
145 |
146 |
147 |
148 | );
149 | };
150 |
151 | export default DefaultPage;
--------------------------------------------------------------------------------
/src/Logins/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Link, useNavigate } from 'react-router-dom'
3 | import axios from 'axios'
4 | import { Mail, Lock, Loader2, ArrowRight } from 'lucide-react'
5 |
6 | const SignIn = () => {
7 | const [email, setEmail] = useState('')
8 | const [password, setPassword] = useState('')
9 | const [errorMessage, setErrorMessage] = useState('')
10 | const [isLoading, setIsLoading] = useState(false)
11 | const navigate = useNavigate()
12 | const token = localStorage.getItem('token');
13 |
14 | useEffect(() => {
15 | if (token) {
16 | navigate("/home");
17 | }
18 | }, [token, navigate]);
19 |
20 | const handleSubmit = async (e) => {
21 | e.preventDefault()
22 | setIsLoading(true)
23 | setErrorMessage('')
24 |
25 | try {
26 | const API = import.meta.env.VITE_API_URL
27 | if (!API) {
28 | throw new Error('API URL is not configured')
29 | }
30 |
31 | const response = await axios.post(`${API}/login`, { email, password })
32 |
33 | const { token, message, user } = response.data
34 |
35 | if (message === "Login Successful") {
36 | localStorage.setItem('token', token)
37 | localStorage.setItem('userName', user.name)
38 | localStorage.setItem('userEmail', user.email)
39 | navigate("/home")
40 | } else {
41 | setErrorMessage(message || "Login failed. Please try again.")
42 | }
43 | } catch (error) {
44 | console.error("Login Error:", error)
45 |
46 | if (error.message === 'API URL is not configured') {
47 | setErrorMessage('Server configuration error. Please contact support.')
48 | } else if (error.response?.status === 401) {
49 | setErrorMessage('Invalid email or password')
50 | } else if (error.response?.data?.message) {
51 | setErrorMessage(error.response.data.message)
52 | } else {
53 | setErrorMessage('Login failed. Please check your credentials')
54 | }
55 | } finally {
56 | setIsLoading(false)
57 | }
58 | }
59 |
60 | return (
61 |
62 | {/* Left Panel - Image */}
63 |
64 |
65 |
70 |
71 |
72 |
Welcome Back to Better Health
73 |
74 | Continue your fitness journey with Eleweight's personalized tracking and insights.
75 |
76 |
77 |
78 |
79 | {/* Right Panel - Form */}
80 |
81 |
82 |
83 |
84 |
89 |
90 |
Sign In to Eleweight
91 |
Enter your credentials to continue
92 |
93 |
94 |
169 |
170 |
171 |
172 | )
173 | }
174 |
175 | export default SignIn
--------------------------------------------------------------------------------
/src/Components/FeaturedCards.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { motion } from 'framer-motion';
3 | import {
4 | Dumbbell,
5 | List,
6 | Plus,
7 | Utensils,
8 | MapPin,
9 | MoveRight,
10 | Target,
11 | } from 'lucide-react';
12 | import { Link } from 'react-router-dom';
13 |
14 | const FeatureCard = ({ title, description, icon: Icon, link, isNew }) => {
15 | return (
16 |
20 | {/* Decorative top accent bar */}
21 |
22 |
23 | {isNew && (
24 |
25 | NEW
26 |
27 | )}
28 |
29 | {/* Hover background effect */}
30 |
31 |
32 |
33 | {/* Icon with improved styling */}
34 |
39 |
40 | {/* Title with hover effect */}
41 |
42 | {title}
43 |
44 |
45 | {/* Description */}
46 |
47 | {description}
48 |
49 |
50 | {/* Button with improved styling */}
51 |
55 |
Get Started
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | const FeaturesSection = () => {
64 | const features = [
65 | {
66 | icon: Dumbbell,
67 | title: "Browse Exercises",
68 | description: "Explore a comprehensive library of exercises categorized by muscle groups. Find detailed instructions, animations, and tips to perfect your form.",
69 | link: "/exercises"
70 | },
71 | {
72 | icon: List,
73 | title: "Workout Plans",
74 | description: "Access curated workout plans designed by fitness experts. From beginners to advanced athletes, find the perfect routine to achieve your goals.",
75 | link: "/plans"
76 | },
77 | {
78 | icon: Plus,
79 | title: "Create Custom Plans",
80 | description: "Design your own personalized workout routines with our easy-to-use plan builder. Mix and match exercises to create the perfect workout for your specific needs and goals.",
81 | link: "/create-plan",
82 | isNew: true
83 | },
84 | {
85 | icon: Utensils,
86 | title: "Diet Plans",
87 | description: "Get personalized diet plans that complement your workout routine. Our nutrition guidance helps you fuel your body properly and achieve optimal results.",
88 | link: "/diet"
89 | },
90 | {
91 | icon: MapPin,
92 | title: "Find Nearby Gyms",
93 | description: "Discover fitness centers in your area to kickstart your fitness journey. We'll help you find the perfect gym based on your location.",
94 | link: "/nearby-gyms",
95 | isNew: true
96 | },
97 | {
98 | icon: Target,
99 | title: "Muscle Targeting",
100 | description: "Find specific exercises that target individual muscle groups for balanced, effective workouts.",
101 | link: "/muscle"
102 | }
103 | ];
104 |
105 | // State for slider
106 | const [currentIndex, setCurrentIndex] = React.useState(0);
107 | const sliderRef = React.useRef(null);
108 |
109 | // Number of cards to show based on screen size
110 | const getVisibleCards = () => {
111 | if (typeof window !== 'undefined') {
112 | if (window.innerWidth >= 1280) return 3; // xl screens
113 | if (window.innerWidth >= 768) return 2; // md screens
114 | return 1; // small screens
115 | }
116 | return 3; // Default for SSR
117 | };
118 |
119 | const [visibleCards, setVisibleCards] = React.useState(3);
120 |
121 | React.useEffect(() => {
122 | const handleResize = () => {
123 | setVisibleCards(getVisibleCards());
124 | };
125 |
126 | handleResize(); // Set initial value
127 | window.addEventListener('resize', handleResize);
128 | return () => window.removeEventListener('resize', handleResize);
129 | }, []);
130 |
131 | const maxIndex = Math.max(0, features.length - visibleCards);
132 |
133 | const nextSlide = () => {
134 | setCurrentIndex(prev => Math.min(prev + 1, maxIndex));
135 | };
136 |
137 | const prevSlide = () => {
138 | setCurrentIndex(prev => Math.max(prev - 1, 0));
139 | };
140 |
141 | const goToSlide = (index) => {
142 | setCurrentIndex(Math.min(Math.max(0, index), maxIndex));
143 | };
144 |
145 | return (
146 |
147 |
148 | {/* Slider container */}
149 |
150 |
155 | {features.map((feature, index) => (
156 |
164 |
165 |
166 | ))}
167 |
168 |
169 |
170 | {/* Navigation buttons */}
171 |
172 | {/* Previous button */}
173 |
181 |
182 |
183 |
184 |
185 |
186 | {/* Dots navigation */}
187 |
188 | {Array.from({ length: maxIndex + 1 }).map((_, idx) => (
189 | goToSlide(idx)}
192 | className={`w-2.5 h-2.5 rounded-full transition-all ${
193 | currentIndex === idx
194 | ? 'bg-gradient-to-r from-indigo-500 to-purple-600 w-8'
195 | : 'bg-gray-300 hover:bg-gray-400'
196 | }`}
197 | aria-label={`Go to slide ${idx + 1}`}
198 | />
199 | ))}
200 |
201 |
202 | {/* Next button */}
203 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | );
219 | };
220 |
221 | export default FeaturesSection;
--------------------------------------------------------------------------------
/src/Components/NearbyGyms.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { getNearbyGyms } from '../services/gymService';
3 | import { MapPin, Navigation, Clock, Star, Phone, ExternalLink, Loader2 } from 'lucide-react';
4 |
5 | const NearbyGyms = () => {
6 | const [gyms, setGyms] = useState([]);
7 | const [loading, setLoading] = useState(false);
8 | const [error, setError] = useState(null);
9 | const [userLocation, setUserLocation] = useState(null);
10 | const [searchRadius, setSearchRadius] = useState(1500);
11 |
12 | // Get user's location
13 | useEffect(() => {
14 | const getUserLocation = () => {
15 | setLoading(true);
16 | if (navigator.geolocation) {
17 | navigator.geolocation.getCurrentPosition(
18 | (position) => {
19 | const { latitude, longitude } = position.coords;
20 | setUserLocation({ latitude, longitude });
21 | fetchNearbyGyms(latitude, longitude, searchRadius);
22 | },
23 | (error) => {
24 | console.error('Error getting user location:', error);
25 | setError('Unable to get your location. Please enable location services and try again.');
26 | setLoading(false);
27 | // Use default location if user denies permission
28 | const defaultLocation = { latitude: 18.399251, longitude: 76.556786 };
29 | setUserLocation(defaultLocation);
30 | fetchNearbyGyms(defaultLocation.latitude, defaultLocation.longitude, searchRadius);
31 | }
32 | );
33 | } else {
34 | setError('Geolocation is not supported by your browser.');
35 | setLoading(false);
36 | }
37 | };
38 |
39 | getUserLocation();
40 | }, []);
41 |
42 | // Fetch nearby gyms when radius changes
43 | useEffect(() => {
44 | if (userLocation) {
45 | fetchNearbyGyms(userLocation.latitude, userLocation.longitude, searchRadius);
46 | }
47 | }, [searchRadius, userLocation]);
48 |
49 | const fetchNearbyGyms = async (latitude, longitude, radius) => {
50 | try {
51 | setLoading(true);
52 | const data = await getNearbyGyms({ latitude, longitude, radius });
53 | setGyms(data.results || []);
54 | setError(null);
55 | } catch (err) {
56 | setError('Failed to fetch nearby gyms. Please try again later.');
57 | console.error('Error fetching gyms:', err);
58 | } finally {
59 | setLoading(false);
60 | }
61 | };
62 |
63 | const handleRadiusChange = (e) => {
64 | setSearchRadius(Number(e.target.value));
65 | };
66 |
67 | const getDirectionsUrl = (gym) => {
68 | if (!gym.geometry || !gym.geometry.location) return '#';
69 | const { lat, lng } = gym.geometry.location;
70 | return `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
71 | };
72 |
73 | return (
74 |
75 |
76 |
Find Nearby Gyms
77 |
78 | Discover fitness centers in your area to start your workout journey. We use your location to find the closest gyms.
79 |
80 |
81 |
82 |
83 | Search Radius: {searchRadius} meters
84 |
85 |
96 |
97 | 500m
98 | 5000m
99 |
100 |
101 |
102 |
103 | {loading && (
104 |
105 |
106 | Finding gyms near you...
107 |
108 | )}
109 |
110 | {error && (
111 |
123 | )}
124 |
125 | {!loading && !error && gyms.length === 0 && (
126 |
127 |
128 |
No gyms found
129 |
130 | Try increasing your search radius or check back later.
131 |
132 |
133 | )}
134 |
135 |
136 | {gyms.map((gym) => (
137 |
138 | {gym.photos && gym.photos[0] ? (
139 |
140 |
141 |
142 | ) : (
143 |
144 |
145 |
146 | )}
147 |
148 |
149 |
150 |
{gym.name}
151 | {gym.rating && (
152 |
153 |
154 | {gym.rating}
155 | {gym.user_ratings_total && (
156 | ({gym.user_ratings_total})
157 | )}
158 |
159 | )}
160 |
161 |
162 | {gym.vicinity && (
163 |
164 |
165 |
{gym.vicinity}
166 |
167 | )}
168 |
169 | {gym.opening_hours && (
170 |
171 |
172 |
173 | {gym.opening_hours.open_now ? (
174 | Open now
175 | ) : (
176 | Closed
177 | )}
178 |
179 |
180 | )}
181 |
182 |
203 |
204 |
205 | ))}
206 |
207 |
208 | );
209 | };
210 |
211 | export default NearbyGyms;
--------------------------------------------------------------------------------
/src/Components/Muscles.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import { motion, AnimatePresence } from 'framer-motion';
4 | import { X, ChevronRight, Target, PlayCircle, Info } from 'lucide-react';
5 | import exercisesData from '../exercises.json';
6 | import NavBar from './NavBar';
7 |
8 | const Card = ({ index, title, muscle, gif_url, onSelect }) => {
9 | return (
10 |
20 |
21 | {/* Background Pattern */}
22 |
25 |
26 | {/* Content */}
27 |
28 |
33 |
36 |
37 |
38 |
39 |
40 |
41 | {title}
42 |
43 |
44 |
45 | {muscle}
46 |
47 |
48 |
49 |
50 | View details
51 |
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | const FullScreenCard = ({ exercise, onClose }) => {
60 | return (
61 |
67 |
74 |
75 | {/* Header */}
76 |
77 |
81 |
82 |
83 |
84 |
85 | {/* Content */}
86 |
87 | {/* Image Section */}
88 |
89 |
90 |
95 |
96 |
97 |
98 |
99 | {/* Details Section */}
100 |
101 |
102 | {/* Title */}
103 |
104 |
105 |
106 |
107 |
108 |
{exercise.name}
109 |
110 |
111 |
112 | Target Muscle: {exercise.muscle}
113 |
114 |
115 |
116 |
117 |
118 | {/* Instructions */}
119 |
120 |
Instructions
121 |
122 |
123 |
124 | 1
125 |
126 |
{exercise.description1}
127 |
128 |
129 |
130 | 2
131 |
132 |
{exercise.description2}
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | const MuscleExercises = () => {
146 | const { muscle } = useParams();
147 | const [exercises, setExercises] = useState([]);
148 | const [selectedIndex, setSelectedIndex] = useState(null);
149 |
150 | useEffect(() => {
151 | if (exercisesData.exercises && exercisesData.exercises[muscle]) {
152 | setExercises(exercisesData.exercises[muscle]);
153 | }
154 | }, [muscle]);
155 |
156 | const selectedExercise = selectedIndex !== null ? exercises[selectedIndex] : null;
157 |
158 | return (
159 |
160 |
161 |
162 | {/* Header */}
163 |
164 |
165 |
171 | {muscle.charAt(0).toUpperCase() + muscle.slice(1)} Exercises
172 |
173 |
174 |
175 |
176 | {/* Content */}
177 |
178 |
179 | {exercises.length > 0 ? (
180 | exercises.map((exercise, index) => (
181 |
setSelectedIndex(index)}
189 | />
190 | ))
191 | ) : (
192 |
193 |
194 |
195 |
196 |
No exercises found for this muscle group.
197 |
198 | )}
199 |
200 |
201 |
202 | {/* Modal */}
203 |
204 | {selectedExercise && (
205 | setSelectedIndex(null)}
208 | />
209 | )}
210 |
211 |
212 | );
213 | };
214 |
215 | export default MuscleExercises;
216 |
217 |
218 |
219 |
220 |
--------------------------------------------------------------------------------
/src/Logins/Register.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link, useNavigate } from 'react-router-dom';
3 | import axios from 'axios';
4 | import { Loader2, UserPlus, Mail, Lock, ArrowRight } from 'lucide-react';
5 |
6 | const Register = () => {
7 | const [formData, setFormData] = useState({
8 | name: '',
9 | email: '',
10 | password: '',
11 | });
12 | const [errorMessage, setErrorMessage] = useState('');
13 | const [isLoading, setIsLoading] = useState(false);
14 | const navigate = useNavigate();
15 |
16 | const handleChange = (e) => {
17 | const { name, value } = e.target;
18 | setFormData((prev) => ({
19 | ...prev,
20 | [name]: value,
21 | }));
22 | if (errorMessage) setErrorMessage('');
23 | };
24 |
25 | const handleSubmit = async (e) => {
26 | e.preventDefault();
27 | setIsLoading(true);
28 |
29 | try {
30 | const API = import.meta.env.VITE_API_URL
31 | const response = await axios.post(`${API}/register`, formData);
32 |
33 | if (response.data.message === 'You are signed in') {
34 | localStorage.setItem('token', response.data.token);
35 | localStorage.setItem('userName', response.data.user.name);
36 | localStorage.setItem('userEmail', response.data.user.email);
37 | navigate('/home');
38 | } else {
39 | setErrorMessage(response.data.message);
40 | }
41 | } catch (error) {
42 | if (error.response?.status === 409) {
43 | setErrorMessage('Email already exists');
44 | } else if (error.response?.data?.message) {
45 | setErrorMessage(error.response.data.message);
46 | } else {
47 | setErrorMessage('Registration failed. Please try again.');
48 | }
49 | } finally {
50 | setIsLoading(false);
51 | }
52 | };
53 |
54 | return (
55 |
56 | {/* Left Panel - Image */}
57 |
58 |
59 |
64 |
65 |
66 |
Start Your Journey to Better Health
67 |
68 | Join thousands of users who have transformed their lives with Eleweight's personalized health tracking and insights.
69 |
70 |
71 |
72 |
73 | {/* Right Panel - Form */}
74 |
75 |
76 |
77 |
78 |
83 |
84 |
Create your account
85 |
Enter your information to get started
86 |
87 |
88 |
185 |
186 |
187 |
188 | );
189 | };
190 |
191 | export default Register;
--------------------------------------------------------------------------------
/src/Pages/Plans.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Navigate, useLocation, useNavigate } from 'react-router-dom';
3 | import { motion } from 'framer-motion';
4 | import NavBar from '../Components/NavBar';
5 | import { exercises } from '../exercises.json';
6 | import { Calendar, Users, Dumbbell, Timer, ChevronRight, Target, Trophy } from 'lucide-react';
7 |
8 | const Plans = () => {
9 | const location = useLocation();
10 | const navigate = useNavigate();
11 | const token = localStorage.getItem('token');
12 |
13 | useEffect(() => {
14 | if (!token) {
15 | navigate("/login");
16 | }
17 | }, [token, Navigate]);
18 |
19 |
20 | useEffect(() => {
21 | window.scrollTo(0, 0);
22 | }, [location]);
23 |
24 |
25 | const planDetails = {
26 | classic: {
27 | image:"https://i.pinimg.com/474x/53/ec/b5/53ecb5dae265e12b423ed8cdbdb1de03.jpg",
28 | title: 'Classic Body Part Split',
29 | days: '5 Days per Week',
30 | description: 'Traditional bodybuilding split targeting each muscle group once per week',
31 | intensity: 'High',
32 | experience: 'Intermediate'
33 | },
34 | ppl: {
35 | image:'https://i.pinimg.com/474x/81/3a/a7/813aa7255685f11be23fd13de9ac84b2.jpg',
36 | title: 'Push, Pull, Legs',
37 | days: '6 Days per Week',
38 | description: 'Efficient split for maximum muscle growth and strength gains',
39 | intensity: 'High',
40 | experience: 'Advanced'
41 | },
42 | 'upper-lower': {
43 | image:'https://i.pinimg.com/474x/45/40/c1/4540c1ae44d7ec684bac54292b99bdc5.jpg',
44 | title: 'Upper/Lower Split',
45 | days: '4 Days per Week',
46 | description: 'Balanced approach for building strength and muscle mass',
47 | intensity: 'Moderate',
48 | experience: 'Intermediate'
49 | },
50 | fullbody: {
51 | image:'https://i.pinimg.com/474x/d0/ca/72/d0ca72fac05f6b51c4e888ba0c154381.jpg',
52 | title: 'Full-Body Routine',
53 | days: '3 Days per Week',
54 | description: 'Perfect for beginners and those with limited time for exercise, this routine offers a simple yet effective way to stay active.',
55 | intensity: 'Moderate',
56 | experience: 'Beginner'
57 | },
58 | power: {
59 | image:'https://i.pinimg.com/736x/b2/15/63/b21563f95b6b34fce85bc843699038f5.jpg',
60 | title: 'Power & Hypertrophy Split',
61 | days: '5 Days per Week',
62 | description: 'Focus on building strength and increasing muscle size through a well-structured workout plan that incorporates progressive overload.',
63 | intensity: 'Very High',
64 | experience: 'Advanced'
65 | },
66 | endurance: {
67 | image:'https://i.pinimg.com/474x/cb/5d/ee/cb5deee7a729b262409de28df8392fa4.jpg',
68 | title: 'Endurance Training',
69 | days: '4 Days per Week',
70 | description: 'Improve stamina and functional fitness along with muscle tone and endurance. This plan is perfect for those looking to increase cardiovascular health.',
71 | intensity: 'Moderate',
72 | experience: 'All Levels'
73 | }
74 | };
75 |
76 | const handlePlanSelection = (plan) => {
77 | let selectedExercises = [];
78 |
79 | switch (plan) {
80 | case 'classic':
81 | selectedExercises = {
82 | day1: [...exercises.chest.slice(0, 4), ...exercises.arms.slice(5, 9)], // Chest & arms
83 | day2: [...exercises.back.slice(0, 4), ...exercises.arms.slice(0, 3)], // Back & arms
84 | day3: [...exercises.legs.slice(0, 6)], // Legs
85 | day4: [...exercises.shoulders.slice(0, 4), ...exercises.core.slice(0, 3)], // Shoulders & Core
86 | day5: [...exercises.arms.slice(0, 4), ...exercises.core.slice(0, 3)] // Arms & Core
87 | };
88 | break;
89 |
90 | case 'ppl':
91 | selectedExercises = {
92 | day1: [...exercises.chest.slice(0, 4), ...exercises.shoulders.slice(0, 3)], // Push
93 | day2: [...exercises.back.slice(0, 4), ...exercises.arms.slice(0, 3)], // Pull
94 | day3: [...exercises.legs.slice(0, 6)], // Legs
95 | day4: [...exercises.chest.slice(0, 4), ...exercises.shoulders.slice(0, 3)], // Push
96 | day5: [...exercises.back.slice(0, 4), ...exercises.arms.slice(0, 3)], // Pull
97 | day6: [...exercises.legs.slice(0, 6)], // Legs
98 | };
99 | break;
100 |
101 | case 'upper-lower':
102 | selectedExercises = {
103 | day1: [...exercises.chest.slice(0, 3), ...exercises.back.slice(0, 3), ...exercises.shoulders.slice(0, 3)],
104 | day2: [...exercises.legs.slice(0, 6)],
105 | day3: [...exercises.chest.slice(0, 3), ...exercises.back.slice(0, 3), ...exercises.shoulders.slice(0, 3)],
106 | day4: [...exercises.legs.slice(0, 6)]
107 | };
108 | break;
109 |
110 | case 'fullbody':
111 | selectedExercises = {
112 | day1: [...exercises.chest.slice(0, 2), ...exercises.back.slice(0, 2), ...exercises.legs.slice(0, 2), ...exercises.shoulders.slice(0, 2), ...exercises.arms.slice(0, 2)],
113 | day2: [...exercises.chest.slice(0, 2), ...exercises.back.slice(0, 2), ...exercises.legs.slice(0, 2), ...exercises.shoulders.slice(0, 2), ...exercises.core.slice(0, 2)],
114 | day3: [...exercises.chest.slice(0, 2), ...exercises.back.slice(0, 2), ...exercises.legs.slice(0, 2), ...exercises.shoulders.slice(0, 2), ...exercises.arms.slice(0, 2)]
115 | };
116 | break;
117 |
118 | case 'power':
119 | selectedExercises = {
120 | day1: [...exercises.chest.slice(0, 3), ...exercises.arms.slice(0, 3)],
121 | day2: [...exercises.back.slice(0, 3), ...exercises.arms.slice(0, 3)],
122 | day3: [...exercises.legs.slice(0, 6)],
123 | day4: [...exercises.shoulders.slice(0, 3), ...exercises.core.slice(0, 3)],
124 | day5: [...exercises.arms.slice(0, 4), ...exercises.core.slice(0, 3)]
125 | };
126 | break;
127 |
128 | case 'endurance':
129 | selectedExercises = {
130 | day1: [...exercises.chest.slice(0, 2), ...exercises.back.slice(0, 2), ...exercises.core.slice(0, 2)],
131 | day2: [...exercises.legs.slice(0, 4), ...exercises.core.slice(0, 2)],
132 | day3: [...exercises.shoulders.slice(0, 2), ...exercises.arms.slice(0, 2), ...exercises.core.slice(0, 2)],
133 | day4: [...exercises.chest.slice(0, 2), ...exercises.back.slice(0, 2), ...exercises.core.slice(0, 2)]
134 | };
135 | break;
136 |
137 | default:
138 | selectedExercises = {};
139 | }
140 |
141 |
142 | navigate(`/plans/${plan}`, { state: { exercises: selectedExercises } });
143 | };
144 |
145 | return (
146 |
147 |
148 |
149 |
150 |
156 |
157 |
158 | Choose Your
159 |
160 | {" "}
161 | Training Plan
162 |
163 |
164 |
165 |
166 | Select a workout plan that matches your goals and experience level
167 |
168 |
169 |
170 |
171 | {Object.keys(planDetails).map((plan, index) => (
172 |
handlePlanSelection(plan)}
178 | className="overflow-hidden relative bg-white rounded-2xl shadow-md transition-all duration-300 cursor-pointer group hover:shadow-xl"
179 | >
180 |
181 |
186 |
187 |
188 |
189 |
190 |
191 | {planDetails[plan].title}
192 |
193 |
194 |
195 | {planDetails[plan].description}
196 |
197 |
198 |
199 |
200 |
201 | {planDetails[plan].days}
202 |
203 |
204 |
205 | {planDetails[plan].intensity}
206 |
207 |
208 |
209 | {planDetails[plan].experience}
210 |
211 |
212 |
213 | Strength Focus
214 |
215 |
216 |
217 |
218 |
219 |
220 | ))}
221 |
222 |
223 |
224 | );
225 | };
226 |
227 | export default Plans;
--------------------------------------------------------------------------------
/src/Components/PaymentModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import { X, CreditCard, Check, Lock, AlertCircle } from 'lucide-react';
4 |
5 | const PaymentModal = ({ isOpen, onClose, plan, price, period }) => {
6 | const [step, setStep] = useState(1);
7 | const [cardNumber, setCardNumber] = useState('');
8 | const [cardName, setCardName] = useState('');
9 | const [expiryDate, setExpiryDate] = useState('');
10 | const [cvv, setCvv] = useState('');
11 | const [isProcessing, setIsProcessing] = useState(false);
12 | const [isSuccess, setIsSuccess] = useState(false);
13 | const [error, setError] = useState('');
14 |
15 | const handleSubmit = (e) => {
16 | e.preventDefault();
17 |
18 | // Basic validation
19 | if (!cardNumber || !cardName || !expiryDate || !cvv) {
20 | setError('Please fill in all fields');
21 | return;
22 | }
23 |
24 | if (cardNumber.replace(/\s/g, '').length !== 16) {
25 | setError('Card number must be 16 digits');
26 | return;
27 | }
28 |
29 | if (cvv.length < 3) {
30 | setError('CVV must be at least 3 digits');
31 | return;
32 | }
33 |
34 | // Clear any previous errors
35 | setError('');
36 |
37 | // Simulate payment processing
38 | setIsProcessing(true);
39 |
40 | setTimeout(() => {
41 | setIsProcessing(false);
42 | setIsSuccess(true);
43 |
44 | // Close modal after showing success message
45 | setTimeout(() => {
46 | onClose();
47 | setStep(1);
48 | setIsSuccess(false);
49 | // Reset form
50 | setCardNumber('');
51 | setCardName('');
52 | setExpiryDate('');
53 | setCvv('');
54 | }, 3000);
55 | }, 2000);
56 | };
57 |
58 | const formatCardNumber = (value) => {
59 | const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
60 | const matches = v.match(/\d{4,16}/g);
61 | const match = (matches && matches[0]) || '';
62 | const parts = [];
63 |
64 | for (let i = 0, len = match.length; i < len; i += 4) {
65 | parts.push(match.substring(i, i + 4));
66 | }
67 |
68 | if (parts.length) {
69 | return parts.join(' ');
70 | } else {
71 | return value;
72 | }
73 | };
74 |
75 | const handleCardNumberChange = (e) => {
76 | const formattedValue = formatCardNumber(e.target.value);
77 | setCardNumber(formattedValue);
78 | };
79 |
80 | const handleExpiryDateChange = (e) => {
81 | let value = e.target.value.replace(/\D/g, '');
82 |
83 | if (value.length > 2) {
84 | value = value.substring(0, 2) + '/' + value.substring(2, 4);
85 | }
86 |
87 | setExpiryDate(value);
88 | };
89 |
90 | return (
91 |
92 | {isOpen && (
93 |
99 |
105 |
109 |
110 |
111 |
112 | {/* Header */}
113 |
114 |
115 | {isSuccess ? 'Payment Successful!' : `Subscribe to ${plan}`}
116 |
117 |
118 | {isSuccess
119 | ? 'Thank you for your purchase!'
120 | : `${price} billed ${period === 'month' ? 'monthly' : 'annually'}`}
121 |
122 |
123 |
124 |
125 | {isSuccess ? (
126 |
127 |
128 |
129 |
130 |
Payment Successful!
131 |
132 | Your subscription has been activated successfully. Enjoy your premium features!
133 |
134 |
135 | ) : step === 1 ? (
136 |
137 |
138 |
139 |
140 | 1
141 |
142 |
Payment Details
143 |
144 |
145 |
146 | {error && (
147 |
148 |
149 |
{error}
150 |
151 | )}
152 |
153 |
240 |
241 | ) : null}
242 |
243 |
244 |
245 | )}
246 |
247 | );
248 | };
249 |
250 | export default PaymentModal;
--------------------------------------------------------------------------------
/src/Components/FeatureShowcase.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { useState, useEffect, useCallback, useRef } from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { ChevronLeft, ChevronRight, PlayCircle, PauseCircle } from 'lucide-react';
6 |
7 | const FeatureShowcase = ({ features = [] }) => {
8 | const [currentIndex, setCurrentIndex] = useState(0);
9 | const [isPlaying, setIsPlaying] = useState(true);
10 | const [isHovering, setIsHovering] = useState(false);
11 |
12 | // Early return if no features
13 | if (!features || features.length === 0) {
14 | return null;
15 | }
16 |
17 | const nextSlide = useRef(() => {});
18 |
19 | nextSlide.current = useCallback(() => {
20 | setCurrentIndex((prevIndex) =>
21 | prevIndex === features.length - 1 ? 0 : prevIndex + 1
22 | );
23 | }, [features.length]);
24 |
25 | const prevSlide = () => {
26 | setCurrentIndex((prevIndex) =>
27 | prevIndex === 0 ? features.length - 1 : prevIndex - 1
28 | );
29 | };
30 |
31 | const goToSlide = (index) => {
32 | setCurrentIndex(index);
33 | };
34 |
35 | const toggleAutoplay = () => {
36 | setIsPlaying(!isPlaying);
37 | };
38 |
39 | useEffect(() => {
40 | let timer;
41 | if (isPlaying && !isHovering) {
42 | timer = setInterval(() => nextSlide.current(), 5000);
43 | }
44 | return () => {
45 | if (timer) clearInterval(timer);
46 | };
47 | }, [isPlaying, isHovering]);
48 |
49 | return (
50 | setIsHovering(true)}
53 | onMouseLeave={() => setIsHovering(false)}
54 | >
55 | {/* Progress bar */}
56 |
57 |
69 |
70 |
71 | {/* Main Slider */}
72 |
73 |
74 | {features[currentIndex] && (
75 |
83 | {/* Image with overlay */}
84 |
95 |
96 | {/* Content overlay */}
97 |
98 |
104 |
105 | {features[currentIndex].icon && (
106 |
112 | {React.createElement(features[currentIndex].icon, {
113 | className: "w-6 h-6 text-white"
114 | })}
115 |
116 | )}
117 | {features[currentIndex].isNew && (
118 |
124 | New
125 |
126 | )}
127 |
128 |
129 |
135 | {features[currentIndex].title}
136 |
137 |
138 |
144 | {features[currentIndex].description}
145 |
146 |
147 | {features[currentIndex].link && (
148 |
153 |
157 | Learn More
158 |
163 | →
164 |
165 |
166 |
167 | )}
168 |
169 |
170 |
171 | )}
172 |
173 |
174 | {/* Navigation arrows - more subtle and positioned better */}
175 |
176 |
185 | Previous slide
186 |
187 |
188 |
189 |
198 | Next slide
199 |
200 |
201 |
202 |
203 | {/* Controls footer - redesigned */}
204 |
205 | {/* Play/Pause button */}
206 |
215 | {isPlaying ? (
216 |
217 | ) : (
218 |
219 | )}
220 |
221 | {isPlaying ? "Pause" : "Play"}
222 |
223 |
224 |
225 | {/* Dots navigation - enhanced */}
226 |
227 | {features.map((_, index) => (
228 | goToSlide(index)}
231 | className={`h-1.5 sm:h-2 rounded-full transition-all ${
232 | currentIndex === index
233 | ? 'w-6 sm:w-8 bg-white shadow-glow'
234 | : 'w-1.5 sm:w-2 bg-white/30 hover:bg-white/60'
235 | }`}
236 | whileHover={{ scale: 1.2 }}
237 | whileTap={{ scale: 0.9 }}
238 | initial={{ opacity: 0, y: 10 }}
239 | animate={{ opacity: 1, y: 0 }}
240 | transition={{ delay: 0.7 + index * 0.05 }}
241 | aria-label={`Go to slide ${index + 1}`}
242 | />
243 | ))}
244 |
245 |
246 | {/* Slide counter - redesigned */}
247 |
253 | {currentIndex + 1} / {features.length}
254 |
255 |
256 |
257 |
258 | {/* Add subtle particle effect */}
259 |
260 | {[...Array(20)].map((_, i) => (
261 |
278 | ))}
279 |
280 |
281 | {/* Add custom styles */}
282 |
287 |
288 | );
289 | };
290 |
291 | FeatureShowcase.defaultProps = {
292 | features: []
293 | };
294 |
295 | export default FeatureShowcase;
296 |
--------------------------------------------------------------------------------
/src/Pages/AllExercises.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { motion } from 'framer-motion';
4 | import NavBar from '../Components/NavBar';
5 | import { exercises } from '../exercises.json';
6 | import { Search, Filter, X, ChevronDown, ChevronUp, Dumbbell, Info, Plus } from 'lucide-react';
7 |
8 | const AllExercises = () => {
9 | const navigate = useNavigate();
10 | const token = localStorage.getItem('token');
11 | const [searchTerm, setSearchTerm] = useState('');
12 | const [selectedMuscle, setSelectedMuscle] = useState('');
13 | const [isFilterOpen, setIsFilterOpen] = useState(false);
14 | const [allExercises, setAllExercises] = useState([]);
15 | const [selectedExercise, setSelectedExercise] = useState(null);
16 |
17 | useEffect(() => {
18 | if (!token) {
19 | navigate("/login");
20 | }
21 | }, [token, navigate]);
22 |
23 | useEffect(() => {
24 | // Flatten the exercises object into an array
25 | const exerciseArray = [];
26 | Object.entries(exercises).forEach(([muscleGroup, exerciseList]) => {
27 | exerciseList.forEach(exercise => {
28 | exerciseArray.push({
29 | ...exercise,
30 | muscleGroup
31 | });
32 | });
33 | });
34 | setAllExercises(exerciseArray);
35 | }, []);
36 |
37 | const muscleGroups = Object.keys(exercises);
38 |
39 | const filteredExercises = allExercises.filter(exercise => {
40 | const matchesSearch = exercise.name.toLowerCase().includes(searchTerm.toLowerCase());
41 | const matchesMuscle = selectedMuscle ? exercise.muscleGroup === selectedMuscle : true;
42 | return matchesSearch && matchesMuscle;
43 | });
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
setSearchTerm(e.target.value)}
62 | />
63 |
64 |
65 |
66 |
setIsFilterOpen(!isFilterOpen)}
68 | className="flex justify-between items-center px-4 py-3 w-full bg-white rounded-lg border border-gray-300 shadow-sm md:w-48 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-purple-500"
69 | >
70 |
71 |
72 | {selectedMuscle || 'Filter Muscle'}
73 |
74 | {isFilterOpen ? (
75 |
76 | ) : (
77 |
78 | )}
79 |
80 |
81 | {isFilterOpen && (
82 |
83 |
84 | {
86 | setSelectedMuscle('');
87 | setIsFilterOpen(false);
88 | }}
89 | className="flex items-center px-3 py-2 w-full text-left rounded-md hover:bg-gray-100"
90 | >
91 | All muscles
92 |
93 | {muscleGroups.map((muscle) => (
94 | {
97 | setSelectedMuscle(muscle);
98 | setIsFilterOpen(false);
99 | }}
100 | className="flex items-center px-3 py-2 w-full text-left rounded-md hover:bg-gray-100"
101 | >
102 | {muscle.charAt(0).toUpperCase() + muscle.slice(1)}
103 |
104 | ))}
105 |
106 |
107 | )}
108 |
109 |
110 |
111 | {selectedMuscle && (
112 |
113 | Filtered by:
114 |
115 | {selectedMuscle.charAt(0).toUpperCase() + selectedMuscle.slice(1)}
116 | setSelectedMuscle('')}
118 | className="ml-1 text-purple-600 hover:text-purple-800"
119 | >
120 |
121 |
122 |
123 |
124 | )}
125 |
126 |
127 |
128 |
129 | {filteredExercises.length} {filteredExercises.length === 1 ? 'Exercise' : 'Exercises'} Found
130 |
131 |
132 | Click on any exercise to view details and add it to your custom workout plan
133 |
134 |
135 |
136 |
137 | {filteredExercises.map((exercise, index) => (
138 | setSelectedExercise(exercise)}
143 | />
144 | ))}
145 |
146 |
147 | {filteredExercises.length === 0 && (
148 |
149 |
150 |
No exercises found matching your criteria.
151 |
{
153 | setSearchTerm('');
154 | setSelectedMuscle('');
155 | }}
156 | className="px-6 py-3 text-white bg-purple-500 rounded-lg transition-colors hover:bg-purple-600"
157 | >
158 | Clear filters
159 |
160 |
161 | )}
162 |
163 |
164 | {/* Exercise Detail Modal */}
165 | {selectedExercise && (
166 |
167 |
173 |
174 |
setSelectedExercise(null)}
176 | className="absolute top-4 right-4 z-10 p-2 rounded-full shadow-lg backdrop-blur-sm transition-all duration-200 bg-white/90 hover:bg-gray-100"
177 | >
178 |
179 |
180 |
181 |
182 |
183 |
184 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
{selectedExercise.name}
200 |
201 |
202 |
203 | {selectedExercise.muscleGroup.charAt(0).toUpperCase() + selectedExercise.muscleGroup.slice(1)}
204 |
205 | •
206 | {selectedExercise.muscle}
207 |
208 |
209 |
210 |
211 |
Instructions
212 |
213 |
{selectedExercise.description1}
214 |
{selectedExercise.description2}
215 |
216 |
217 |
218 |
219 |
{
221 | navigate('/create-plan');
222 | setSelectedExercise(null);
223 | }}
224 | className="flex justify-center items-center py-3 w-full text-white bg-purple-500 rounded-lg transition-colors hover:bg-purple-600"
225 | >
226 |
227 | Add to Custom Plan
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 | )}
237 |
238 | );
239 | };
240 |
241 | const ExerciseCard = ({ exercise, index, onClick }) => {
242 | return (
243 |
247 |
248 |
249 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
{exercise.name}
263 |
264 | {exercise.muscleGroup.charAt(0).toUpperCase() + exercise.muscleGroup.slice(1)}
265 |
266 |
267 |
268 | {exercise.description1}
269 |
270 |
271 |
272 | {exercise.muscle}
273 |
274 | View details
275 |
276 |
277 |
278 |
279 | );
280 | };
281 |
282 | export default AllExercises;
--------------------------------------------------------------------------------
/src/Exercises/WorkoutPlan.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import { motion, AnimatePresence } from 'framer-motion';
4 | import { Calendar, Target, ChevronRight, Info, PlayCircle, X, ArrowLeft, Dumbbell, Clock } from 'lucide-react';
5 | import NavBar from '../Components/NavBar';
6 |
7 | const FullScreenCard = ({ exercise, onClose }) => {
8 | return (
9 |
15 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
{exercise.name}
49 |
50 |
51 |
52 |
53 | Target Muscle: {exercise.muscle}
54 |
55 | {exercise.sets && exercise.reps && (
56 |
57 |
58 | Sets × Reps: {exercise.sets} × {exercise.reps}
59 |
60 | )}
61 |
62 |
63 |
64 |
65 |
66 | {(exercise.description1 || exercise.description2) && (
67 |
68 |
Instructions
69 |
70 | {exercise.description1 && (
71 |
72 |
73 | 1
74 |
75 |
{exercise.description1}
76 |
77 | )}
78 | {exercise.description2 && (
79 |
80 |
81 | 2
82 |
83 |
{exercise.description2}
84 |
85 | )}
86 |
87 |
88 | )}
89 |
90 |
91 |
92 |
93 |
94 |
95 | );
96 | };
97 |
98 | const DayCard = ({ day, exerciseCount, onClick }) => {
99 | return (
100 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | {day.replace('day', 'Day ')}
116 |
117 |
118 | {exerciseCount} exercise{exerciseCount !== 1 ? 's' : ''}
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | );
127 | };
128 |
129 | const ExerciseList = ({ day, exercises, onBack, onSelectExercise }) => {
130 | return (
131 |
137 |
138 |
142 |
143 |
144 |
145 | {day.replace('day', 'Day ')} Exercises
146 |
147 |
148 |
149 |
150 | {exercises.map((exercise, index) => (
151 |
onSelectExercise(exercise)}
158 | >
159 |
160 |
161 |
162 |
167 |
170 |
171 |
172 |
173 |
174 | {exercise.name}
175 |
176 |
177 |
178 |
179 | {exercise.muscle}
180 |
181 | {exercise.sets && exercise.reps && (
182 |
183 |
184 |
185 | {exercise.sets} × {exercise.reps}
186 |
187 |
188 | )}
189 |
190 |
191 |
192 |
193 |
194 |
195 | ))}
196 |
197 |
198 | );
199 | };
200 |
201 | const WorkoutPlan = () => {
202 | const location = useLocation();
203 | const { exercises } = location.state || {};
204 | const [selectedDay, setSelectedDay] = useState(null);
205 | const [selectedExercise, setSelectedExercise] = useState(null);
206 |
207 | if (!exercises) {
208 | return (
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
No workout plan selected
217 |
218 |
219 |
220 | );
221 | }
222 |
223 | return (
224 |
225 |
226 |
227 |
228 |
229 |
234 |
235 | Your Workout Plan
236 |
237 | Stay consistent, stay strong
238 |
239 |
240 |
241 |
242 |
243 |
244 | {!selectedDay ? (
245 |
252 | {Object.entries(exercises).map(([day, dayExercises]) => (
253 | setSelectedDay(day)}
258 | />
259 | ))}
260 |
261 | ) : (
262 | setSelectedDay(null)}
267 | onSelectExercise={setSelectedExercise}
268 | />
269 | )}
270 |
271 |
272 |
273 |
274 | {selectedExercise && (
275 | setSelectedExercise(null)}
278 | />
279 | )}
280 |
281 |
282 | );
283 | };
284 |
285 | export default WorkoutPlan;
--------------------------------------------------------------------------------
/src/Pages/MyWorkoutPlans.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useNavigate, Link } from 'react-router-dom';
3 | import { motion, AnimatePresence } from 'framer-motion';
4 | import NavBar from '../Components/NavBar';
5 | import { Calendar, ChevronRight, Plus, Trash2, Edit, AlertCircle, Dumbbell, Clock, Users, Target } from 'lucide-react';
6 | import axios from 'axios';
7 |
8 | const MyWorkoutPlans = () => {
9 | const navigate = useNavigate();
10 | const token = localStorage.getItem('token');
11 | const [workoutPlans, setWorkoutPlans] = useState([]);
12 | const [loading, setLoading] = useState(true);
13 | const [error, setError] = useState('');
14 | const [deleteModalOpen, setDeleteModalOpen] = useState(false);
15 | const [planToDelete, setPlanToDelete] = useState(null);
16 |
17 | const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
18 |
19 | useEffect(() => {
20 | if (!token) {
21 | navigate("/login");
22 | } else {
23 | fetchWorkoutPlans();
24 | }
25 | }, [token, navigate]);
26 |
27 | const fetchWorkoutPlans = async () => {
28 | setLoading(true);
29 | try {
30 | // Ensure token is valid
31 | if (!token) {
32 | navigate("/login");
33 | return;
34 | }
35 |
36 | const response = await axios.get(`${API_URL}/api/workout-plans`, {
37 | headers: {
38 | Authorization: `Bearer ${token}`
39 | }
40 | });
41 | setWorkoutPlans(response.data);
42 | } catch (error) {
43 | console.error('Error fetching workout plans:', error);
44 | if (error.response?.status === 401 || error.response?.status === 403) {
45 | // Token is invalid or expired
46 | localStorage.removeItem('token');
47 | navigate("/login");
48 | return;
49 | }
50 | setError(error.response?.data?.message || 'Failed to load your workout plans. Please try again later.');
51 | } finally {
52 | setLoading(false);
53 | }
54 | };
55 |
56 | const handleDeletePlan = async () => {
57 | if (!planToDelete) return;
58 |
59 | try {
60 | // Ensure token is valid
61 | if (!token) {
62 | setError('You must be logged in to delete a workout plan');
63 | setDeleteModalOpen(false);
64 | return;
65 | }
66 |
67 | await axios.delete(`${API_URL}/api/workout-plans/${planToDelete}`, {
68 | headers: {
69 | Authorization: `Bearer ${token}`
70 | }
71 | });
72 |
73 | // Remove the deleted plan from state
74 | setWorkoutPlans(workoutPlans.filter(plan => plan._id !== planToDelete));
75 | setDeleteModalOpen(false);
76 | setPlanToDelete(null);
77 | } catch (error) {
78 | console.error('Error deleting workout plan:', error);
79 | if (error.response?.status === 401 || error.response?.status === 403) {
80 | // Token is invalid or expired
81 | localStorage.removeItem('token');
82 | navigate("/login");
83 | return;
84 | } else if (error.response?.status === 404) {
85 | // Plan not found, might have been deleted already
86 | setWorkoutPlans(workoutPlans.filter(plan => plan._id !== planToDelete));
87 | setDeleteModalOpen(false);
88 | setPlanToDelete(null);
89 | } else {
90 | setError(error.response?.data?.message || 'Failed to delete the workout plan. Please try again later.');
91 | }
92 | }
93 | };
94 |
95 | const openDeleteModal = (planId, e) => {
96 | e.stopPropagation();
97 | setPlanToDelete(planId);
98 | setDeleteModalOpen(true);
99 | };
100 |
101 | const navigateToCreatePlan = () => {
102 | navigate('/create-plan');
103 | };
104 |
105 | const getRandomGradient = () => {
106 | const gradients = [
107 | 'from-blue-500 to-indigo-600',
108 | 'from-green-500 to-teal-600',
109 | 'from-purple-500 to-pink-600',
110 | 'from-red-500 to-orange-600',
111 | 'from-yellow-500 to-amber-600',
112 | 'from-indigo-500 to-purple-600',
113 | 'from-teal-500 to-cyan-600',
114 | 'from-pink-500 to-rose-600'
115 | ];
116 | return gradients[Math.floor(Math.random() * gradients.length)];
117 | };
118 |
119 | return (
120 |
121 |
122 |
123 | {/* Hero Section */}
124 |
125 |
126 |
127 |
133 | My Workout Plans
134 |
135 | Manage your custom workout routines and track your fitness journey
136 |
137 |
141 |
142 | Create New Plan
143 |
144 |
145 |
146 |
147 |
148 |
149 | {error && (
150 |
151 |
152 |
{error}
153 |
154 | )}
155 |
156 | {loading ? (
157 |
160 | ) : workoutPlans.length === 0 ? (
161 |
167 |
172 | No Workout Plans Yet
173 |
174 | Create your first custom workout plan to start tracking your fitness journey and achieving your goals.
175 |
176 |
180 |
181 | Create Your First Plan
182 |
183 |
184 | ) : (
185 |
186 |
187 |
188 |
Your Workout Plans
189 |
You have {workoutPlans.length} custom workout {workoutPlans.length === 1 ? 'plan' : 'plans'}
190 |
191 |
195 |
196 | Create New Plan
197 |
198 |
199 |
200 |
201 |
202 | {workoutPlans.map((plan, index) => (
203 | navigate(`/workout-plan/${plan._id}`)}
211 | >
212 |
213 |
214 |
215 |
{plan.name}
216 |
217 | {
219 | e.stopPropagation();
220 | navigate(`/edit-plan/${plan._id}`);
221 | }}
222 | className="p-1 sm:p-1.5 text-purple-500 hover:text-purple-700 hover:bg-purple-50 rounded-full transition-colors"
223 | aria-label="Edit plan"
224 | >
225 |
226 |
227 | openDeleteModal(plan._id, e)}
229 | className="p-1 sm:p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-full transition-colors"
230 | aria-label="Delete plan"
231 | >
232 |
233 |
234 |
235 |
236 |
237 | {plan.description && (
238 |
{plan.description}
239 | )}
240 |
241 |
242 |
243 | {plan.days.length} {plan.days.length === 1 ? 'day' : 'days'} workout
244 | •
245 |
246 | {plan.days.reduce((total, day) => total + day.exercises.length, 0)} exercises
247 |
248 |
249 |
250 | {plan.days.map((day, index) => (
251 |
252 | {day.name}
253 |
254 | ))}
255 |
256 |
257 |
258 |
259 | Created: {new Date(plan.createdAt).toLocaleDateString()}
260 |
261 |
262 | View details
263 |
264 |
265 |
266 |
267 |
268 | ))}
269 |
270 |
271 |
272 | )}
273 |
274 |
275 | {/* Delete Confirmation Modal */}
276 |
277 | {deleteModalOpen && (
278 |
284 |
290 |
291 |
292 |
Delete Workout Plan
293 |
294 |
295 | Are you sure you want to delete this workout plan? This action cannot be undone.
296 |
297 |
298 | {
300 | setDeleteModalOpen(false);
301 | setPlanToDelete(null);
302 | }}
303 | className="px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 border border-gray-300 text-gray-700 text-xs sm:text-sm md:text-base rounded-lg hover:bg-gray-50 transition-colors"
304 | >
305 | Cancel
306 |
307 |
311 | Delete
312 |
313 |
314 |
315 |
316 | )}
317 |
318 |
319 | );
320 | };
321 |
322 | export default MyWorkoutPlans;
--------------------------------------------------------------------------------
/src/Components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react';
2 | import { Link, NavLink, useNavigate } from 'react-router-dom';
3 | import { motion, AnimatePresence } from 'framer-motion';
4 | import { Dumbbell, X, Menu, User, Search, Bell, ChevronDown, LogOut, Settings, Camera, Upload, Check, UserCircle, ScanLine } from 'lucide-react';
5 |
6 | const NavBar = () => {
7 | const navigate = useNavigate();
8 | const [profileImage, setProfileImage] = useState('https://i.pinimg.com/474x/a3/cc/fd/a3ccfd7885e6cff94ebbbe40fd9e1611.jpg');
9 | const [isOpen, setIsOpen] = useState(false);
10 | const [scrolled, setScrolled] = useState(false);
11 | const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
12 | const [userName, setUserName] = useState(localStorage.getItem('userName') || 'User');
13 | const [userEmail, setUserEmail] = useState(localStorage.getItem('userEmail') || 'user@example.com');
14 | const fileInputRef = useRef(null);
15 | const dropdownRef = useRef(null);
16 |
17 | useEffect(() => {
18 | const handleStorageChange = () => {
19 | const savedPicture = localStorage.getItem('profilePicture');
20 | if (savedPicture) {
21 | setProfileImage(savedPicture);
22 | }
23 | setUserName(localStorage.getItem('userName') || 'User');
24 | setUserEmail(localStorage.getItem('userEmail') || 'user@example.com');
25 | };
26 |
27 | const handleProfileUpdate = (e) => {
28 | setProfileImage(e.detail.picture);
29 | };
30 |
31 | const handleScroll = () => {
32 | const offset = window.scrollY;
33 | if (offset > 50) {
34 | setScrolled(true);
35 | } else {
36 | setScrolled(false);
37 | }
38 | };
39 |
40 | const handleClickOutside = (event) => {
41 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
42 | setProfileDropdownOpen(false);
43 | }
44 | };
45 |
46 | handleStorageChange();
47 | window.addEventListener('scroll', handleScroll);
48 | window.addEventListener('storage', handleStorageChange);
49 | window.addEventListener('profilePictureUpdate', handleProfileUpdate);
50 | document.addEventListener('mousedown', handleClickOutside);
51 |
52 | return () => {
53 | window.removeEventListener('scroll', handleScroll);
54 | window.removeEventListener('storage', handleStorageChange);
55 | window.removeEventListener('profilePictureUpdate', handleProfileUpdate);
56 | document.removeEventListener('mousedown', handleClickOutside);
57 | };
58 | }, []);
59 |
60 | const handleLogOut = () => {
61 | localStorage.removeItem('token');
62 | localStorage.removeItem('userName');
63 | localStorage.removeItem('userEmail');
64 | navigate("/login");
65 | };
66 |
67 | const handleFileUpload = async (e) => {
68 | const file = e.target.files[0];
69 | if (!file) return;
70 |
71 | try {
72 | // Create a local URL for the file for preview
73 | const imageUrl = URL.createObjectURL(file);
74 |
75 | // Update the UI and localStorage
76 | setProfileImage(imageUrl);
77 | localStorage.setItem('profilePicture', imageUrl);
78 |
79 | // Notify other components about the profile picture update
80 | window.dispatchEvent(new CustomEvent('profilePictureUpdate', {
81 | detail: { picture: imageUrl }
82 | }));
83 | } catch (error) {
84 | console.error('Error handling image:', error);
85 | }
86 | };
87 |
88 | const triggerFileInput = () => {
89 | fileInputRef.current.click();
90 | };
91 |
92 | // Enhanced navigation links with icons
93 | const navLinks = [
94 | { path: "/home", name: "Home", icon: "home" },
95 | { path: "/exercises", name: "Exercises", icon: "dumbbell" },
96 | { path: "/my-plans", name: "My Plans", icon: "calendar" },
97 | { path: "/diet", name: "Diet Plans", icon: "utensils" },
98 | { path: "/nearby-gyms", name: "Find Gyms", icon: "map-pin" },
99 | ];
100 |
101 | return (
102 |
105 |
106 | {/* Logo with enhanced animation */}
107 |
108 |
113 |
114 |
115 |
116 | Eleweight
117 |
118 |
119 |
120 | {/* Desktop Navigation with improved hover effects */}
121 |
122 |
123 | {navLinks.map((link) => (
124 |
128 | `relative text-base font-medium px-4 py-2 rounded-full transition-all flex items-center gap-1.5
129 | ${isActive
130 | ? 'text-purple-600 bg-white shadow-sm'
131 | : scrolled
132 | ? 'text-gray-700 hover:text-purple-500 hover:bg-white/80'
133 | : 'text-gray-800 hover:text-purple-500 hover:bg-white/80'}`
134 | }
135 | >
136 | {({ isActive }) => (
137 |
138 | {link.name}
139 | {link.isNew && (
140 |
141 | NEW
142 |
143 | )}
144 |
145 | )}
146 |
147 | ))}
148 |
149 |
150 | {/* Food Analysis Camera Icon */}
151 |
154 | `relative p-2 rounded-full transition-all
155 | ${isActive
156 | ? 'text-white bg-gradient-to-r from-purple-600 to-purple-500 shadow-md'
157 | : 'text-gray-700 hover:text-purple-500 hover:bg-gray-100'}`
158 | }
159 | title="Analyze Food"
160 | >
161 |
162 |
163 |
164 | {/* Enhanced Profile Section with Dropdown */}
165 |
166 |
setProfileDropdownOpen(!profileDropdownOpen)}
168 | className="flex gap-2 items-center p-1.5 rounded-full transition-colors group hover:bg-gray-50/80"
169 | >
170 |
174 |
179 |
180 |
181 |
182 | {userName}
183 |
184 |
185 |
186 |
187 | {/* Profile Dropdown */}
188 |
189 | {profileDropdownOpen && (
190 |
197 |
198 |
199 |
200 |
205 |
209 |
210 |
211 |
218 |
219 |
220 |
{userName}
221 |
{userEmail}
222 |
223 |
224 |
225 |
226 |
230 |
231 | Log Out
232 |
233 |
234 |
235 |
236 | )}
237 |
238 |
239 |
240 |
241 | {/* Improved Mobile Menu Button */}
242 |
243 | {/* Food Analysis Camera Icon for Mobile */}
244 |
247 | `relative p-2 rounded-full transition-all
248 | ${isActive
249 | ? 'text-white bg-gradient-to-r from-purple-600 to-purple-500 shadow-md'
250 | : 'text-gray-700 hover:text-purple-500 hover:bg-gray-100'}`
251 | }
252 | title="Analyze Food"
253 | >
254 |
255 |
256 |
257 | setIsOpen(!isOpen)}
259 | className="p-2 rounded-full backdrop-blur-sm transition-colors bg-gray-50/80 hover:bg-gray-100"
260 | aria-label="Toggle menu"
261 | >
262 |
266 | {isOpen ? (
267 |
268 | ) : (
269 |
270 | )}
271 |
272 |
273 |
274 |
275 | {/* Enhanced Mobile Menu */}
276 |
277 | {isOpen && (
278 |
285 |
286 | {navLinks.map((link) => (
287 |
setIsOpen(false)}
291 | className={({ isActive }) =>
292 | `px-4 py-3 my-1 rounded-lg text-base font-medium transition-all flex items-center justify-between
293 | ${isActive
294 | ? 'bg-purple-50 text-purple-600'
295 | : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}`
296 | }
297 | >
298 |
299 | {link.name}
300 | {link.isNew && (
301 |
302 | NEW
303 |
304 | )}
305 |
306 |
307 | ))}
308 |
309 | {/* Profile section in mobile menu */}
310 |
311 |
312 |
313 |
314 |
319 |
323 |
324 |
325 |
326 |
327 |
{userName}
328 |
{userEmail}
329 |
330 |
331 |
332 |
333 |
{
335 | handleLogOut();
336 | setIsOpen(false);
337 | }}
338 | className="flex gap-2 items-center px-3 py-2 w-full text-sm text-left text-red-600 rounded-md transition-colors hover:bg-red-50"
339 | >
340 |
341 | Log Out
342 |
343 |
344 |
345 |
346 |
347 | )}
348 |
349 |
350 |
351 | );
352 | };
353 |
354 | export default NavBar;
--------------------------------------------------------------------------------
/src/Pages/FoodAnalysis.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import { Camera, Upload, ArrowRight } from 'lucide-react';
4 | import axios from 'axios';
5 | import NavBar from '../Components/NavBar';
6 |
7 | const FoodAnalysis = () => {
8 | const [imageBase64, setImageBase64] = useState('');
9 | const [isAnalyzingImage, setIsAnalyzingImage] = useState(false);
10 | const [foodAnalysisResults, setFoodAnalysisResults] = useState(null);
11 | const [error, setError] = useState('');
12 | const [showCamera, setShowCamera] = useState(false);
13 | const videoRef = useRef(null);
14 | const canvasRef = useRef(null);
15 | const fileInputRef = useRef(null);
16 |
17 | const gradientText = "bg-gradient-to-r from-purple-500 via-purple-500 to-sky-500 bg-clip-text text-transparent";
18 |
19 | // Start camera stream
20 | const startCamera = async () => {
21 | setShowCamera(true);
22 | try {
23 | const stream = await navigator.mediaDevices.getUserMedia({
24 | video: { facingMode: 'environment' }
25 | });
26 |
27 | if (videoRef.current) {
28 | videoRef.current.srcObject = stream;
29 | }
30 | } catch (err) {
31 | console.error("Error accessing camera:", err);
32 | setError("Could not access camera. Please check permissions or try uploading an image instead.");
33 | }
34 | };
35 |
36 | // Stop camera stream
37 | const stopCamera = () => {
38 | if (videoRef.current && videoRef.current.srcObject) {
39 | const tracks = videoRef.current.srcObject.getTracks();
40 | tracks.forEach(track => track.stop());
41 | videoRef.current.srcObject = null;
42 | }
43 | setShowCamera(false);
44 | };
45 |
46 | // Capture image from camera
47 | const captureImage = () => {
48 | if (videoRef.current && canvasRef.current) {
49 | const video = videoRef.current;
50 | const canvas = canvasRef.current;
51 | const context = canvas.getContext('2d');
52 |
53 | // Set canvas dimensions to match video
54 | canvas.width = video.videoWidth;
55 | canvas.height = video.videoHeight;
56 |
57 | // Draw video frame to canvas
58 | context.drawImage(video, 0, 0, canvas.width, canvas.height);
59 |
60 | // Convert canvas to base64
61 | const base64Data = canvas.toDataURL('image/jpeg').split(",")[1];
62 | setImageBase64(base64Data);
63 |
64 | // Stop camera after capturing
65 | stopCamera();
66 | }
67 | };
68 |
69 | // Handle file upload
70 | const handleImageUpload = (event) => {
71 | const file = event.target.files[0];
72 | const reader = new FileReader();
73 |
74 | reader.onloadend = () => {
75 | // Remove the "data:image/jpeg;base64," part
76 | const base64Data = reader.result.split(",")[1];
77 | setImageBase64(base64Data);
78 | };
79 |
80 | if (file) {
81 | reader.readAsDataURL(file);
82 | }
83 | };
84 |
85 | // Trigger file input click
86 | const triggerFileInput = () => {
87 | fileInputRef.current.click();
88 | };
89 |
90 | // Analyze food using Google Gemini API
91 | const analyzeFood = async () => {
92 | if (!imageBase64) {
93 | setError('Please capture or upload an image first');
94 | return;
95 | }
96 |
97 | setIsAnalyzingImage(true);
98 | setFoodAnalysisResults(null);
99 | setError('');
100 |
101 | const API_URL = `https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent?key=${import.meta.env.VITE_GEMINI_API_KEY}`;
102 |
103 | const requestData = {
104 | contents: [
105 | {
106 | parts: [
107 | {
108 | inlineData: {
109 | mimeType: "image/jpeg",
110 | data: imageBase64
111 | }
112 | },
113 | {
114 | text: "Identify the food in this image and provide the estimated macronutrients (calories, protein, carbs, fats). Format the response as JSON with fields: foodName, calories, protein, carbs, fats."
115 | }
116 | ]
117 | }
118 | ]
119 | };
120 |
121 | try {
122 | const response = await axios.post(API_URL, requestData, {
123 | headers: { "Content-Type": "application/json" }
124 | });
125 |
126 | // Extract the text response from Gemini
127 | const responseText = response.data.candidates[0].content.parts[0].text;
128 |
129 | // Try to parse JSON from the response
130 | try {
131 | // Find JSON in the response text (it might be surrounded by markdown code blocks)
132 | const jsonMatch = responseText.match(/```json\n([\s\S]*?)\n```/) ||
133 | responseText.match(/```\n([\s\S]*?)\n```/) ||
134 | responseText.match(/{[\s\S]*?}/);
135 |
136 | let parsedData;
137 | if (jsonMatch) {
138 | parsedData = JSON.parse(jsonMatch[1] || jsonMatch[0]);
139 | } else {
140 | // If no JSON format is found, create a simple object from the text
141 | parsedData = {
142 | analysis: responseText,
143 | foodName: "Unknown",
144 | calories: 0,
145 | protein: 0,
146 | carbs: 0,
147 | fats: 0
148 | };
149 | }
150 |
151 | setFoodAnalysisResults(parsedData);
152 | } catch (parseError) {
153 | console.error("Error parsing response:", parseError);
154 | setFoodAnalysisResults({
155 | analysis: responseText,
156 | foodName: "Unknown",
157 | calories: 0,
158 | protein: 0,
159 | carbs: 0,
160 | fats: 0
161 | });
162 | }
163 | } catch (error) {
164 | console.error("Error analyzing food:", error);
165 | setError('Error analyzing food image. Please try again.');
166 | } finally {
167 | setIsAnalyzingImage(false);
168 | }
169 | };
170 |
171 | return (
172 |
173 |
174 |
175 |
180 |
181 | Food Analysis with AI
182 |
183 |
184 | Take a picture of your food or upload an image to get instant nutritional information
185 |
186 |
187 |
188 |
193 |
194 |
195 | {/* Camera View */}
196 |
197 | {showCamera ? (
198 |
204 |
210 |
211 |
212 |
213 |
219 | Capture
220 |
221 |
222 |
223 |
229 | Cancel
230 |
231 |
232 |
233 | ) : (
234 |
239 | {imageBase64 ? (
240 |
241 |
246 |
247 | setImageBase64('')}
249 | whileHover={{ scale: 1.05 }}
250 | whileTap={{ scale: 0.95 }}
251 | className="px-4 py-2 text-white bg-red-500 rounded-lg shadow-md opacity-80 hover:opacity-100"
252 | >
253 | Change Image
254 |
255 |
256 |
257 | ) : (
258 |
259 |
265 |
266 |
267 |
268 |
269 |
Take a Photo
270 |
Use your camera to capture food
271 |
272 |
273 |
274 |
280 |
281 |
282 |
283 |
284 |
Upload Image
285 |
Select a photo from your device
286 |
287 |
294 |
295 |
296 | )}
297 |
298 | {imageBase64 && (
299 |
306 | {isAnalyzingImage ? (
307 | <>
308 |
309 | Analyzing...
310 | >
311 | ) : (
312 | <>
313 | Analyze Food
314 |
315 | >
316 | )}
317 |
318 | )}
319 |
320 | )}
321 |
322 |
323 | {error && (
324 |
{error}
325 | )}
326 |
327 |
328 |
329 | {foodAnalysisResults && (
330 |
336 |
337 | {foodAnalysisResults.foodName || "Food Analysis"}
338 |
339 |
340 |
341 |
342 |
Calories
343 |
{foodAnalysisResults.calories || 0} kcal
344 |
345 |
346 |
Protein
347 |
{foodAnalysisResults.protein || 0} g
348 |
349 |
350 |
Carbs
351 |
{foodAnalysisResults.carbs || 0} g
352 |
353 |
354 |
Fats
355 |
{foodAnalysisResults.fats || 0} g
356 |
357 |
358 |
359 | {foodAnalysisResults.analysis && (
360 |
361 |
{foodAnalysisResults.analysis}
362 |
363 | )}
364 |
365 | )}
366 |
367 |
368 |
369 |
370 |
371 | );
372 | };
373 |
374 | export default FoodAnalysis;
--------------------------------------------------------------------------------
/src/Pages/WorkoutPlanDetail.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams, useNavigate } from 'react-router-dom';
3 | import { motion, AnimatePresence } from 'framer-motion';
4 | import NavBar from '../Components/NavBar';
5 | import { Calendar, ChevronLeft, ChevronRight, Clock, Dumbbell, ArrowLeft, Info, Edit, Share, Download, X } from 'lucide-react';
6 | import axios from 'axios';
7 |
8 | const WorkoutPlanDetail = () => {
9 | const { id } = useParams();
10 | const navigate = useNavigate();
11 | const token = localStorage.getItem('token');
12 | const [plan, setPlan] = useState(null);
13 | const [loading, setLoading] = useState(true);
14 | const [error, setError] = useState('');
15 | const [activeDay, setActiveDay] = useState(0);
16 | const [selectedExercise, setSelectedExercise] = useState(null);
17 |
18 | const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
19 |
20 | useEffect(() => {
21 | if (!token) {
22 | navigate("/login");
23 | } else {
24 | fetchWorkoutPlan();
25 | }
26 | }, [token, navigate, id]);
27 |
28 | const fetchWorkoutPlan = async () => {
29 | setLoading(true);
30 | try {
31 | // Ensure token is valid
32 | if (!token) {
33 | navigate("/login");
34 | return;
35 | }
36 |
37 | const response = await axios.get(`${API_URL}/api/workout-plans/${id}`, {
38 | headers: {
39 | Authorization: `Bearer ${token}`
40 | }
41 | });
42 | setPlan(response.data);
43 | } catch (error) {
44 | console.error('Error fetching workout plan:', error);
45 | if (error.response?.status === 401 || error.response?.status === 403) {
46 | // Token is invalid or expired
47 | localStorage.removeItem('token');
48 | navigate("/login");
49 | return;
50 | } else if (error.response?.status === 404) {
51 | setError('Workout plan not found.');
52 | } else {
53 | setError(error.response?.data?.message || 'Failed to load the workout plan. Please try again later.');
54 | }
55 | } finally {
56 | setLoading(false);
57 | }
58 | };
59 |
60 | const handlePreviousDay = () => {
61 | if (activeDay > 0) {
62 | setActiveDay(activeDay - 1);
63 | }
64 | };
65 |
66 | const handleNextDay = () => {
67 | if (plan && activeDay < plan.days.length - 1) {
68 | setActiveDay(activeDay + 1);
69 | }
70 | };
71 |
72 | const getTotalExercises = () => {
73 | if (!plan) return 0;
74 | return plan.days.reduce((total, day) => total + day.exercises.length, 0);
75 | };
76 |
77 | if (loading) {
78 | return (
79 |
87 | );
88 | }
89 |
90 | if (error) {
91 | return (
92 |
93 |
94 |
95 |
96 | {error}
97 |
98 |
navigate('/my-plans')}
100 | className="flex items-center px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
101 | >
102 |
103 | Back to My Plans
104 |
105 |
106 |
107 | );
108 | }
109 |
110 | if (!plan) {
111 | return (
112 |
113 |
114 |
115 |
116 | Workout plan not found.
117 |
118 |
navigate('/my-plans')}
120 | className="flex items-center px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
121 | >
122 |
123 | Back to My Plans
124 |
125 |
126 |
127 | );
128 | }
129 |
130 | return (
131 |
132 |
133 |
134 | {/* Hero Section */}
135 |
136 |
137 |
138 |
139 |
navigate('/my-plans')}
141 | className="flex items-center text-white/80 hover:text-white transition-colors"
142 | >
143 |
144 | Back to My Plans
145 |
146 |
147 |
148 |
153 | {plan.name}
154 | {plan.description && (
155 | {plan.description}
156 | )}
157 |
158 |
159 |
160 |
161 | {plan.days.length} {plan.days.length === 1 ? 'Day' : 'Days'}
162 |
163 |
164 |
165 | {getTotalExercises()} Exercises
166 |
167 |
168 |
169 | Created {new Date(plan.createdAt).toLocaleDateString()}
170 |
171 |
172 |
173 |
174 | navigate(`/edit-plan/${plan._id}`)}
176 | className="flex items-center px-3 sm:px-4 py-1.5 sm:py-2 bg-white text-blue-600 rounded-lg hover:bg-blue-50 transition-colors text-sm sm:text-base"
177 | >
178 |
179 | Edit Plan
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | {plan.days[activeDay].name}
196 |
197 |
198 |
199 |
208 |
209 |
210 |
211 | Day {activeDay + 1} of {plan.days.length}
212 |
213 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | {plan.days[activeDay].exercises.length === 0 ? (
230 |
231 |
232 |
No exercises for this day.
233 |
navigate(`/edit-plan/${plan._id}`)}
235 | className="mt-4 px-3 sm:px-4 py-1.5 sm:py-2 bg-blue-500 text-white text-sm sm:text-base rounded-lg hover:bg-blue-600 transition-colors"
236 | >
237 | Add exercises
238 |
239 |
240 | ) : (
241 |
242 | {plan.days[activeDay].exercises.map((exercise, index) => (
243 |
setSelectedExercise(exercise)}
250 | >
251 |
252 |
253 |
258 |
259 |
260 |
{exercise.name}
261 |
{exercise.muscle}
262 |
263 |
264 |
265 | {exercise.sets} sets
266 |
267 |
268 |
269 | {exercise.reps} reps
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 | ))}
279 |
280 | )}
281 |
282 |
283 |
284 |
285 |
All Days
286 |
287 |
288 |
289 | {plan.days.map((day, index) => (
290 |
setActiveDay(index)}
298 | >
299 |
300 |
{day.name}
301 |
302 | {day.exercises.length} {day.exercises.length === 1 ? 'exercise' : 'exercises'}
303 |
304 |
305 | {day.exercises.length > 0 && (
306 |
307 | {day.exercises.slice(0, 3).map((exercise, i) => (
308 |
309 |
310 | {exercise.name}
311 |
312 | ))}
313 | {day.exercises.length > 3 && (
314 |
315 | +{day.exercises.length - 3} more exercises
316 |
317 | )}
318 |
319 | )}
320 |
321 | ))}
322 |
323 |
324 |
325 | {/* Exercise Detail Modal */}
326 |
327 | {selectedExercise && (
328 |
334 |
340 |
341 |
setSelectedExercise(null)}
343 | className="absolute top-4 right-4 z-10 p-2 rounded-full bg-white/90 backdrop-blur-sm shadow-lg hover:bg-gray-100 transition-all duration-200"
344 | >
345 |
346 |
347 |
348 |
349 |
350 |
351 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
{selectedExercise.name}
367 |
368 |
{selectedExercise.muscle}
369 |
370 |
371 |
372 |
373 |
Sets
374 |
{selectedExercise.sets}
375 |
376 |
377 |
Reps
378 |
{selectedExercise.reps}
379 |
380 |
381 |
382 |
383 |
Instructions
384 |
385 |
{selectedExercise.description1}
386 |
{selectedExercise.description2}
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 | )}
396 |
397 |
398 | );
399 | };
400 |
401 | export default WorkoutPlanDetail;
--------------------------------------------------------------------------------