mirror of
https://github.com/escalante29/healthy-fit.git
synced 2026-03-21 10:48:46 +01:00
Initial project scaffolding for health tracker app
Set up backend and frontend structure for a health and fitness tracker using Python (FastAPI, SQLModel, DSPy) and React. Includes Docker and Compose configs, authentication, nutrition AI module, health/nutrition/user endpoints, database models, and basic frontend with routing and context. Enables tracking nutrition, health metrics, and user management, with architecture ready for future mobile and cloud deployment.
This commit is contained in:
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
cache=./.npm-cache
|
||||
10
frontend/Dockerfile
Normal file
10
frontend/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM node:20-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
16
frontend/README.md
Normal file
16
frontend/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
29
frontend/eslint.config.js
Normal file
29
frontend/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4658
frontend/package-lock.json
generated
Normal file
4658
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
frontend/package.json
Normal file
34
frontend/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.12.0",
|
||||
"recharts": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
42
frontend/src/App.css
Normal file
42
frontend/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
33
frontend/src/App.jsx
Normal file
33
frontend/src/App.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Nutrition from './pages/Nutrition';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<div className="min-h-screen bg-gray-900 text-white font-sans">
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/nutrition" element={
|
||||
<ProtectedRoute>
|
||||
<Nutrition />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
{/* Add Health route later */}
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
17
frontend/src/api/client.js
Normal file
17
frontend/src/api/client.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: `${API_URL}/api/v1`,
|
||||
});
|
||||
|
||||
client.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
export default client;
|
||||
1
frontend/src/assets/react.svg
Normal file
1
frontend/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
13
frontend/src/components/ProtectedRoute.jsx
Normal file
13
frontend/src/components/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useContext } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { AuthContext } from '../context/AuthContext';
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { token, loading } = useContext(AuthContext);
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (!token) return <Navigate to="/login" />;
|
||||
|
||||
return children;
|
||||
};
|
||||
export default ProtectedRoute;
|
||||
47
frontend/src/context/AuthContext.jsx
Normal file
47
frontend/src/context/AuthContext.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createContext, useState, useEffect } from 'react';
|
||||
import client from '../api/client';
|
||||
|
||||
export const AuthContext = createContext();
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
// Fetch user profile if needed, or just decode token
|
||||
// For now, we assume user is logged in if token exists
|
||||
// Ideally call /users/me
|
||||
setLoading(false);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', email);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await client.post('/login/access-token', formData);
|
||||
const { access_token } = response.data;
|
||||
|
||||
localStorage.setItem('token', access_token);
|
||||
setToken(access_token);
|
||||
// Ideally fetch user details here
|
||||
return true;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, token, login, logout, loading }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
3
frontend/src/index.css
Normal file
3
frontend/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
20
frontend/src/pages/Dashboard.jsx
Normal file
20
frontend/src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<div className="p-8 text-white">
|
||||
<h1 className="text-4xl font-bold mb-8">Dashboard</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Link to="/nutrition" className="p-6 bg-gray-800 rounded-lg shadow-lg hover:bg-gray-700 transition">
|
||||
<h2 className="text-2xl font-bold mb-2">Nutrition Tracker</h2>
|
||||
<p className="text-gray-400">Log meals and view macros.</p>
|
||||
</Link>
|
||||
<Link to="/health" className="p-6 bg-gray-800 rounded-lg shadow-lg hover:bg-gray-700 transition">
|
||||
<h2 className="text-2xl font-bold mb-2">Health Metrics</h2>
|
||||
<p className="text-gray-400">Track weight and blood indicators.</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Dashboard;
|
||||
57
frontend/src/pages/Login.jsx
Normal file
57
frontend/src/pages/Login.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useState, useContext } from 'react';
|
||||
import { AuthContext } from '../context/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Login = () => {
|
||||
const { login } = useContext(AuthContext);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await login(email, password);
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
setError('Invalid credentials');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-900 text-white">
|
||||
<div className="w-full max-w-md p-8 bg-gray-800 rounded-lg shadow-lg">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center text-purple-400">Healthy Fit Login</h2>
|
||||
{error && <p className="text-red-500 mb-4">{error}</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full p-2 rounded bg-gray-700 border border-gray-600 focus:outline-none focus:border-purple-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block mb-2">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full p-2 rounded bg-gray-700 border border-gray-600 focus:outline-none focus:border-purple-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="w-full bg-purple-600 hover:bg-purple-700 text-white p-2 rounded font-bold transition">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
88
frontend/src/pages/Nutrition.jsx
Normal file
88
frontend/src/pages/Nutrition.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useState } from 'react';
|
||||
import client from '../api/client';
|
||||
|
||||
const Nutrition = () => {
|
||||
const [description, setDescription] = useState('');
|
||||
const [analysis, setAnalysis] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await client.post('/nutrition/analyze', { description });
|
||||
setAnalysis(res.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Failed to analyze');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!analysis) return;
|
||||
try {
|
||||
// Backend extracts user from token via params/dependency
|
||||
await client.post('/nutrition/log', analysis);
|
||||
alert('Saved!');
|
||||
setAnalysis(null);
|
||||
setDescription('');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Failed to save');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-6">Nutrition Tracker</h1>
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-8">
|
||||
<textarea
|
||||
className="w-full p-4 bg-gray-700 rounded mb-4 text-white"
|
||||
rows="4"
|
||||
placeholder="Describe your meal (e.g. 'A chicken breast with a cup of rice')..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
disabled={loading}
|
||||
className="bg-blue-600 px-6 py-2 rounded text-white font-bold"
|
||||
>
|
||||
{loading ? 'Analyzing...' : 'Analyze'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{analysis && (
|
||||
<div className="bg-gray-800 p-6 rounded-lg">
|
||||
<h2 className="text-2xl font-bold mb-4">Analysis Result</h2>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="p-4 bg-gray-700 rounded">
|
||||
<span className="block text-gray-400">Calories</span>
|
||||
<span className="text-xl font-bold">{analysis.calories}</span>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-700 rounded">
|
||||
<span className="block text-gray-400">Protein</span>
|
||||
<span className="text-xl font-bold">{analysis.protein}g</span>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-700 rounded">
|
||||
<span className="block text-gray-400">Carbs</span>
|
||||
<span className="text-xl font-bold">{analysis.carbs}g</span>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-700 rounded">
|
||||
<span className="block text-gray-400">Fats</span>
|
||||
<span className="text-xl font-bold">{analysis.fats}g</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-green-600 px-6 py-2 rounded text-white font-bold"
|
||||
>
|
||||
Save to Log
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Nutrition;
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
7
frontend/vite.config.js
Normal file
7
frontend/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user