Merge pull request #1 from RMCampos/feat/add-appwrite
feat: add appwrite
This commit is contained in:
@@ -3,6 +3,8 @@ name: Deploy to GitHub Pages
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
# Allow only one concurrent deployment
|
# Allow only one concurrent deployment
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
.env
|
||||||
|
senha.txt
|
||||||
|
.vscode/
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/* Authentication Header Styles */
|
||||||
|
.auth-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 15px 0;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
min-width: 300px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form .form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch {
|
||||||
|
text-align: center;
|
||||||
|
margin: 15px 0 0 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Overlay */
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 4px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay p {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update main content spacing */
|
||||||
|
#app-content {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive for Auth */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.auth-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-section,
|
||||||
|
.user-info {
|
||||||
|
order: 2;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
min-width: 280px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
export const foodDatabase = {
|
|
||||||
// Fruits
|
|
||||||
'Frutas - Abacate': { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3 },
|
|
||||||
'Frutas - Abacaxi': { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1 },
|
|
||||||
'Frutas - Banana Nanica': { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9 },
|
|
||||||
'Frutas - Laranja': { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8 },
|
|
||||||
'Frutas - Maçã': { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3 },
|
|
||||||
'Frutas - Manga Palmer': { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6 },
|
|
||||||
'Frutas - Mamão': { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1 },
|
|
||||||
'Frutas - Melancia': { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1 },
|
|
||||||
'Frutas - Melão': { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3 },
|
|
||||||
'Frutas - Uva': { calories: 53, protein: 0.7, fat: 0.2, carbs: 13.6, fiber: 0.9 },
|
|
||||||
|
|
||||||
// Vegetables
|
|
||||||
'Vegetais - Agrião': { calories: 17, protein: 2.7, fat: 0.2, carbs: 2.3, fiber: 2.1 },
|
|
||||||
'Vegetais - Brócolis (cozido)': { calories: 25, protein: 2.1, fat: 0.5, carbs: 4.4, fiber: 3.4 },
|
|
||||||
'Vegetais - Cenoura (cozida)': { calories: 30, protein: 0.8, fat: 0.2, carbs: 6.7, fiber: 2.6 },
|
|
||||||
'Vegetais - Couve flor (cozida)': { calories: 19, protein: 1.2, fat: 0.3, carbs: 3.9, fiber: 2.1 },
|
|
||||||
'Vegetais - Ervilha (cozida)': { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7 },
|
|
||||||
'Vegetais - Espinafre': { calories: 16, protein: 2, fat: 0.2, carbs: 2.6, fiber: 2.1 },
|
|
||||||
'Vegetais - Feijão preto (cozido)': { calories: 77, protein: 4.5, fat: 0.5, carbs: 14, fiber: 8.4 },
|
|
||||||
'Vegetais - Lentilha (cozida)': { calories: 93, protein: 6.3, fat: 0.5, carbs: 16.3, fiber: 7.9 },
|
|
||||||
'Vegetais - Mandioca (cozida)': { calories: 125, protein: 0.6, fat: 0.3, carbs: 30.1, fiber: 1.6 },
|
|
||||||
'Vegetais - Pepino': { calories: 10, protein: 0.9, fat: 0, carbs: 2, fiber: 1.1 },
|
|
||||||
'Vegetais - Rúcula': { calories: 13, protein: 1.8, fat: 0.1, carbs: 2.2, fiber: 1.7 },
|
|
||||||
'Vegetais - Spirulina': { calories: 290, protein: 57.5, fat: 23.9, carbs: 7.7, fiber: 0 },
|
|
||||||
'Vegetais - Tomate': { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.1, fiber: 1.2 },
|
|
||||||
|
|
||||||
// Carbohydrates
|
|
||||||
'Carboidratos - Batata doce (cozida)': { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2 },
|
|
||||||
'Carboidratos - Batata doce (crua)': { calories: 118, protein: 1.3, fat: 0.1, carbs: 28.2, fiber: 2.6 },
|
|
||||||
'Carboidratos - Batata inglesa (cozida)': { calories: 52, protein: 1.2, fat: 0, carbs: 11.9, fiber: 1.3 },
|
|
||||||
'Carboidratos - Batata inglesa (crua)': { calories: 64, protein: 1.8, fat: 0, carbs: 14.7, fiber: 1.2 },
|
|
||||||
'Carboidratos - Pão Francês': { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3 },
|
|
||||||
'Carboidrados - Polenta (cozida)': { calories: 192, protein: 4.8, fat: 1.2, carbs: 40, fiber: 3.8 },
|
|
||||||
|
|
||||||
// Proteins
|
|
||||||
'Proteínas - Atum Sólido (natural)': { calories: 135, protein: 28.3, fat: 2.7, carbs: 0, fiber: 0 },
|
|
||||||
'Proteínas - Filé de Tilápia (cozido)': { calories: 128, protein: 26.1, fat: 2.6, carbs: 0, fiber: 0 },
|
|
||||||
'Proteínas - Ovo inteiro (cozido)': { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0 },
|
|
||||||
'Proteínas - Ovo clara (cozido)': { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0 },
|
|
||||||
'Proteínas - PTS (crua)': { calories: 288, protein: 52, fat: 0, carbs: 20, fiber: 14 },
|
|
||||||
'Proteínas - Patinho moído (grelhado)': { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0 },
|
|
||||||
'Proteínas - Soja em Pó Growth': { calories: 400, protein: 86.7, fat: 5, carbs: 3.3, fiber: 0 },
|
|
||||||
'Proteínas - Sobrecoxa s/pele (cozida)': { calories: 245, protein: 24.9, fat: 15.4, carbs: 0, fiber: 0 },
|
|
||||||
'Proteínas - Tofu': { calories: 65, protein: 6.5, fat: 4, carbs: 2, fiber: 0.75 },
|
|
||||||
|
|
||||||
// Grains
|
|
||||||
'Grãos - Arroz integral (cozido)': { calories: 130, protein: 2.7, fat: 0.3, carbs: 28, fiber: 0.4 },
|
|
||||||
'Grãos - Aveia': { calories: 394, protein: 13.9, fat: 8.5, carbs: 66.6, fiber: 9.1 },
|
|
||||||
'Grãos - Grão de Bico (cozido)': { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6 },
|
|
||||||
'Grãos - Pão integral de forma': { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9 },
|
|
||||||
'Grãos - Pipoca c/óleo': { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3 },
|
|
||||||
'Grãos - Pipoca s/óleo': { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3 },
|
|
||||||
'Grãos - Quinoa (cozida)': { calories: 158, protein: 5.55, fat: 2.5, carbs: 29, fiber: 2.5 },
|
|
||||||
|
|
||||||
// Nuts & Seeds
|
|
||||||
'Castanhas - Amêndoas': { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6 },
|
|
||||||
'Castanhas - Amendoin natural': { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5 },
|
|
||||||
'Castanhas - Castanha de Caju': { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7 },
|
|
||||||
'Castanhas - Castanha do Pará': { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9 },
|
|
||||||
'Sementes - Semente de Chia': { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7 },
|
|
||||||
'Sementes - Semente de Linhaça': { calories: 495, protein: 14.1, fat: 32.3, carbs: 43.3, fiber: 33.5 },
|
|
||||||
'Sementes - Semente de Gergelim': { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9 },
|
|
||||||
|
|
||||||
// Snacks
|
|
||||||
'Lanches - Barra de Proteína Vegana Growth': { calories: 400, protein: 25, fat: 22, carbs: 25, fiber: 8 },
|
|
||||||
'Lanches - Goiabada': { calories: 320, protein: 0, fat: 0, carbs: 80, fiber: 4.5 },
|
|
||||||
'Lanches - Mel Silvestre': { calories: 230, protein: 0, fat: 0, carbs: 65, fiber: 0 },
|
|
||||||
'Lanches - Melado': { calories: 297, protein: 0, fat: 0, carbs: 76.6, fiber: 0 },
|
|
||||||
'Lanches - Pasta de Amendoim Growth': { calories: 544, protein: 27, fat: 33, carbs: 20, fiber: 8 },
|
|
||||||
'Lanches - Goma de Tapioca': { calories: 226, protein: 0, fat: 0, carbs: 57, fiber: 0 },
|
|
||||||
'Lanches - 70% Nibs Garoto': { calories: 544, protein: 8.4, fat: 40, carbs: 34, fiber: 0 }
|
|
||||||
};
|
|
||||||
+167
-77
@@ -5,94 +5,184 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Daily Calorie Tracker</title>
|
<title>Daily Calorie Tracker</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="auth.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<!-- Authentication Header -->
|
||||||
<h1>🍎 Daily Calorie Tracker</h1>
|
<header class="auth-header">
|
||||||
|
<div class="auth-container">
|
||||||
<div class="date-display" id="currentDate"></div>
|
<div class="logo">
|
||||||
|
<h2>🍎 Food Tracker</h2>
|
||||||
<div class="calories-banner">
|
|
||||||
<div class="calories-number" id="caloriesCounter">0</div>
|
|
||||||
<div class="calories-text">Calories Today</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nutrition-summary" id="nutritionSummary">
|
|
||||||
<div class="nutrition-item">
|
|
||||||
<div class="nutrition-value" id="proteinValue">0</div>
|
|
||||||
<div class="nutrition-label">Protein (g)</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="nutrition-item">
|
|
||||||
<div class="nutrition-value" id="fatValue">0</div>
|
<!-- Authentication Forms -->
|
||||||
<div class="nutrition-label">Fat (g)</div>
|
<div id="auth-section" class="auth-section">
|
||||||
</div>
|
<!-- Login Form -->
|
||||||
<div class="nutrition-item">
|
<div id="login-form" class="auth-form">
|
||||||
<div class="nutrition-value" id="carboValue">0</div>
|
<h3>Login</h3>
|
||||||
<div class="nutrition-label">Carbs (g)</div>
|
<form id="loginForm">
|
||||||
</div>
|
<div class="form-group">
|
||||||
<div class="nutrition-item">
|
<input type="email" id="loginEmail" placeholder="Email" required>
|
||||||
<div class="nutrition-value" id="fiberValue">0</div>
|
</div>
|
||||||
<div class="nutrition-label">Fiber (g)</div>
|
<div class="form-group">
|
||||||
</div>
|
<input type="password" id="loginPassword" placeholder="Password" required>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="auth-btn">Login</button>
|
||||||
<div class="add-food-section">
|
<p class="auth-switch">
|
||||||
<h2 class="add-food-title">Add Food</h2>
|
Don't have an account?
|
||||||
<div class="add-food-form">
|
<a href="#" id="showRegister">Register here</a>
|
||||||
<div class="form-group">
|
</p>
|
||||||
<label for="foodSelect">Food Item</label>
|
</form>
|
||||||
<select id="foodSelect">
|
|
||||||
<option value="">Select a food item...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="gramAmount">Amount (grams)</label>
|
<!-- Register Form -->
|
||||||
<input type="number" id="gramAmount" min="1" step="1" value="100">
|
<div id="register-form" class="auth-form hidden">
|
||||||
|
<h3>Register</h3>
|
||||||
|
<form id="registerForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" id="registerName" placeholder="Name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" id="registerEmail" placeholder="Email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" id="registerPassword" placeholder="Password" required minlength="8">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="auth-btn">Register</button>
|
||||||
|
<p class="auth-switch">
|
||||||
|
Already have an account?
|
||||||
|
<a href="#" id="showLogin">Login here</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<button class="add-btn" id="add-food-btn">Add Food</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="food-preview" class="add-food-form-item display-none">
|
|
||||||
<p>Calories: 0 - Protein: 0 - Fat: 0 - Carbohydrates: 0</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="food-table-section">
|
<!-- User Info (shown when logged in) -->
|
||||||
<div class="table-title">Food Log</div>
|
<div id="user-info" class="user-info hidden">
|
||||||
<div class="table-wrapper">
|
<div class="user-details">
|
||||||
<table class="food-table">
|
<span id="userName">Welcome, User!</span>
|
||||||
<thead>
|
<button id="logoutBtn" class="logout-btn">Logout</button>
|
||||||
<tr>
|
</div>
|
||||||
<th class="hidden-column">ID</th>
|
|
||||||
<th>Time</th>
|
|
||||||
<th>Food</th>
|
|
||||||
<th>Grams</th>
|
|
||||||
<th>Calories</th>
|
|
||||||
<th>Protein (g)</th>
|
|
||||||
<th>Fat (g)</th>
|
|
||||||
<th>Carbs (g)</th>
|
|
||||||
<th>Fiber (g)</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="foodTableBody">
|
|
||||||
<!-- Food entries will be added here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="calendar-section">
|
<!-- Loading Overlay -->
|
||||||
<div class="calendar-header">
|
<div id="loading-overlay" class="loading-overlay hidden">
|
||||||
<button class="calendar-nav" id="previous-month-btn">‹</button>
|
<div class="loading-spinner"></div>
|
||||||
<div class="calendar-title" id="calendarTitle">Month Year</div>
|
<p>Loading...</p>
|
||||||
<button class="calendar-nav" id="next-month-btn">›</button>
|
|
||||||
</div>
|
|
||||||
<div class="calendar-grid" id="calendarGrid">
|
|
||||||
<!-- Calendar will be generated here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="index.js"></script>
|
<!-- Main App Content -->
|
||||||
|
<main id="app-content" class="hidden">
|
||||||
|
<div class="container">
|
||||||
|
<h1>🍎 Daily Calorie Tracker</h1>
|
||||||
|
|
||||||
|
<div class="date-display" id="currentDate"></div>
|
||||||
|
|
||||||
|
<div class="calories-banner">
|
||||||
|
<div class="calories-number" id="caloriesCounter">0</div>
|
||||||
|
<div class="calories-text">Calories Today</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nutrition-summary" id="nutritionSummary">
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="proteinValue">0</div>
|
||||||
|
<div class="nutrition-label">Protein (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="fatValue">0</div>
|
||||||
|
<div class="nutrition-label">Fat (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="carboValue">0</div>
|
||||||
|
<div class="nutrition-label">Carbs (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="fiberValue">0</div>
|
||||||
|
<div class="nutrition-label">Fiber (g)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-food-section">
|
||||||
|
<h2 class="add-food-title">Add Food</h2>
|
||||||
|
<div class="add-food-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="foodSelect">Food Item</label>
|
||||||
|
<select id="foodSelect">
|
||||||
|
<option value="">Select a food item...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="gramAmount">Amount (grams)</label>
|
||||||
|
<input type="number" id="gramAmount" min="1" step="1" value="100">
|
||||||
|
</div>
|
||||||
|
<button class="add-btn" id="add-food-btn">Add Food</button>
|
||||||
|
</div>
|
||||||
|
<div class="display-none" id="food-preview">
|
||||||
|
<div class="nutrition-summary">
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="calorieValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Calories</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="proteinValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Protein (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="fatValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Fat (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="carboValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Carbs (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="fiberValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Fiber (g)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="food-table-section">
|
||||||
|
<div class="table-title">Food Log</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="food-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="hidden-column">ID</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Food</th>
|
||||||
|
<th>Grams</th>
|
||||||
|
<th>Calories</th>
|
||||||
|
<th>Protein (g)</th>
|
||||||
|
<th>Fat (g)</th>
|
||||||
|
<th>Carbs (g)</th>
|
||||||
|
<th>Fiber (g)</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="foodTableBody">
|
||||||
|
<!-- Food entries will be added here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-section">
|
||||||
|
<div class="calendar-header">
|
||||||
|
<button class="calendar-nav" id="previous-month-btn">‹</button>
|
||||||
|
<div class="calendar-title" id="calendarTitle">Month Year</div>
|
||||||
|
<button class="calendar-nav" id="next-month-btn">›</button>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-grid" id="calendarGrid">
|
||||||
|
<!-- Calendar will be generated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="src/index.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,349 +0,0 @@
|
|||||||
import { foodDatabase } from './foodDatabase.js';
|
|
||||||
|
|
||||||
// App state
|
|
||||||
let selectedDate = new Date().toISOString().split('T')[0];
|
|
||||||
let currentViewDate = new Date();
|
|
||||||
|
|
||||||
// Initialize the app
|
|
||||||
function init() {
|
|
||||||
populateFoodSelect();
|
|
||||||
updateCurrentDate();
|
|
||||||
loadDayData(selectedDate);
|
|
||||||
renderCalendar();
|
|
||||||
addEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFoodProportion(grams, foodData) {
|
|
||||||
const multiplier = grams / 100; // Database values are per 100g
|
|
||||||
return {
|
|
||||||
calories: Math.round(foodData.calories * multiplier),
|
|
||||||
protein: Math.round(foodData.protein * multiplier * 10) / 10,
|
|
||||||
fat: Math.round(foodData.fat * multiplier * 10) / 10,
|
|
||||||
carbs: Math.round(foodData.carbs * multiplier * 10) / 10,
|
|
||||||
fiber: Math.round(foodData.fiber * multiplier * 10) / 10
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function previewCalories() {
|
|
||||||
const foodSelect = document.getElementById('foodSelect');
|
|
||||||
const gramAmount = document.getElementById('gramAmount');
|
|
||||||
|
|
||||||
if (!foodSelect.value) {
|
|
||||||
document.getElementById('food-preview').innerHTML = '';
|
|
||||||
document.getElementById('food-preview').classList = 'add-food-form-item display-none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let grams = 100;
|
|
||||||
if (gramAmount && gramAmount.value && parseFloat(gramAmount.value) > 0) {
|
|
||||||
grams = parseFloat(gramAmount.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const foodName = foodSelect.value;
|
|
||||||
const foodData = foodDatabase[foodName];
|
|
||||||
const proportion = getFoodProportion(grams, foodData);
|
|
||||||
|
|
||||||
const textArray = [];
|
|
||||||
textArray.push(`Calories in ${grams}g: ${proportion.calories}; `);
|
|
||||||
textArray.push(`Protein: ${proportion.protein}g; `);
|
|
||||||
textArray.push(`Fat: ${proportion.fat}g; `);
|
|
||||||
textArray.push(`Carbohydrates: ${proportion.carbs}g; `);
|
|
||||||
textArray.push(`Fiber: ${proportion.fiber}g;`);
|
|
||||||
|
|
||||||
document.getElementById('food-preview').innerHTML = textArray.join('');
|
|
||||||
document.getElementById('food-preview').classList = 'add-food-form-item display-block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEvents() {
|
|
||||||
document.getElementById('foodSelect').addEventListener('change', () => {
|
|
||||||
previewCalories();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('gramAmount').addEventListener('change', () => {
|
|
||||||
previewCalories();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('add-food-btn').addEventListener('click', () => {
|
|
||||||
addFood();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('next-month-btn').addEventListener('click', () => {
|
|
||||||
nextMonth();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('previous-month-btn').addEventListener('click', () => {
|
|
||||||
previousMonth();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDeleteEvents() {
|
|
||||||
const elements = document.getElementsByClassName('delete-food-entry');
|
|
||||||
Array.from(elements).forEach(el => {
|
|
||||||
el.addEventListener('click', (e) => {
|
|
||||||
const row = e.target.closest('tr');
|
|
||||||
const entryId = row.querySelector('.hidden-column').textContent;
|
|
||||||
deleteEntry(selectedDate, entryId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate food select dropdown
|
|
||||||
function populateFoodSelect() {
|
|
||||||
const select = document.getElementById('foodSelect');
|
|
||||||
select.innerHTML = '<option value="">Select a food item...</option>';
|
|
||||||
|
|
||||||
const sortedFoods = Object.keys(foodDatabase).sort();
|
|
||||||
sortedFoods.forEach(food => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = food;
|
|
||||||
option.textContent = food.charAt(0).toUpperCase() + food.slice(1);
|
|
||||||
select.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update current date display
|
|
||||||
function updateCurrentDate() {
|
|
||||||
const date = new Date(selectedDate);
|
|
||||||
const options = {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
};
|
|
||||||
document.getElementById('currentDate').textContent = date.toLocaleDateString('en-US', options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add food to the log
|
|
||||||
function addFood() {
|
|
||||||
const foodSelect = document.getElementById('foodSelect');
|
|
||||||
const gramAmount = document.getElementById('gramAmount');
|
|
||||||
|
|
||||||
if (!foodSelect.value || !gramAmount.value) {
|
|
||||||
alert('Please select a food item and enter amount');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const foodName = foodSelect.value;
|
|
||||||
const grams = parseFloat(gramAmount.value);
|
|
||||||
const foodData = foodDatabase[foodName];
|
|
||||||
|
|
||||||
// Calculate nutrition for the amount
|
|
||||||
const proportion = getFoodProportion(grams, foodData);
|
|
||||||
const entry = {
|
|
||||||
id: Date.now(),
|
|
||||||
name: foodName,
|
|
||||||
grams: grams,
|
|
||||||
calories: proportion.calories,
|
|
||||||
protein: proportion.protein,
|
|
||||||
fat: proportion.fat,
|
|
||||||
carbs: proportion.carbs,
|
|
||||||
fiber: proportion.fiber,
|
|
||||||
time: new Date().toLocaleTimeString('en-US', {
|
|
||||||
hour12: false,
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
const dayData = getDayData(selectedDate);
|
|
||||||
dayData.push(entry);
|
|
||||||
saveDayData(selectedDate, dayData);
|
|
||||||
|
|
||||||
// Update display
|
|
||||||
loadDayData(selectedDate);
|
|
||||||
renderCalendar();
|
|
||||||
|
|
||||||
// Reset form
|
|
||||||
foodSelect.value = '';
|
|
||||||
gramAmount.value = '100';
|
|
||||||
document.getElementById('food-preview').innerHTML = '';
|
|
||||||
document.getElementById('food-preview').classList = 'add-food-form-item display-none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get day data from localStorage
|
|
||||||
function getDayData(date) {
|
|
||||||
const key = `calories_${date}`;
|
|
||||||
return JSON.parse(localStorage.getItem(key)) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save day data to localStorage
|
|
||||||
function saveDayData(date, data) {
|
|
||||||
const key = `calories_${date}`;
|
|
||||||
localStorage.setItem(key, JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and display day data
|
|
||||||
function loadDayData(date) {
|
|
||||||
const dayData = getDayData(date);
|
|
||||||
|
|
||||||
// Update counters
|
|
||||||
let totalCalories = 0;
|
|
||||||
let totalProtein = 0;
|
|
||||||
let totalFat = 0;
|
|
||||||
let totalCarbs = 0;
|
|
||||||
let totalFiber = 0;
|
|
||||||
|
|
||||||
dayData.forEach(entry => {
|
|
||||||
totalCalories += entry.calories;
|
|
||||||
totalProtein += entry.protein;
|
|
||||||
totalFat += entry.fat;
|
|
||||||
totalCarbs += entry.carbs;
|
|
||||||
totalFiber += entry.fiber;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('caloriesCounter').textContent = totalCalories;
|
|
||||||
document.getElementById('proteinValue').textContent = Math.round(totalProtein * 10) / 10;
|
|
||||||
document.getElementById('fatValue').textContent = Math.round(totalFat * 10) / 10;
|
|
||||||
document.getElementById('carboValue').textContent = Math.round(totalCarbs * 10) / 10;
|
|
||||||
document.getElementById('fiberValue').textContent = Math.round(totalFiber * 10) / 10;
|
|
||||||
|
|
||||||
// Update table
|
|
||||||
const tbody = document.getElementById('foodTableBody');
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
|
|
||||||
dayData.forEach(entry => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.innerHTML = `
|
|
||||||
<td class="hidden-column">${entry.id}</td>
|
|
||||||
<td>${entry.time}</td>
|
|
||||||
<td>${entry.name}</td>
|
|
||||||
<td>${entry.grams}</td>
|
|
||||||
<td>${entry.calories}</td>
|
|
||||||
<td>${entry.protein}</td>
|
|
||||||
<td>${entry.fat}</td>
|
|
||||||
<td>${entry.carbs}</td>
|
|
||||||
<td>${entry.fiber}</td>
|
|
||||||
<td><button class="delete-btn delete-food-entry">Delete</button></td>
|
|
||||||
`;
|
|
||||||
tbody.appendChild(row);
|
|
||||||
});
|
|
||||||
addDeleteEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete an entry
|
|
||||||
function deleteEntry(date, entryId) {
|
|
||||||
if (confirm('Are you sure you want to delete this entry?')) {
|
|
||||||
let dayData = getDayData(date);
|
|
||||||
dayData = dayData.filter(entry => entry.id !== parseInt(entryId));
|
|
||||||
saveDayData(date, dayData);
|
|
||||||
loadDayData(selectedDate);
|
|
||||||
renderCalendar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calendar functions
|
|
||||||
function previousMonth() {
|
|
||||||
currentViewDate.setMonth(currentViewDate.getMonth() - 1);
|
|
||||||
renderCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextMonth() {
|
|
||||||
currentViewDate.setMonth(currentViewDate.getMonth() + 1);
|
|
||||||
renderCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectDate(date) {
|
|
||||||
selectedDate = date;
|
|
||||||
updateCurrentDate();
|
|
||||||
loadDayData(selectedDate);
|
|
||||||
renderCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCalendar() {
|
|
||||||
const year = currentViewDate.getFullYear();
|
|
||||||
const month = currentViewDate.getMonth();
|
|
||||||
|
|
||||||
// Update calendar title
|
|
||||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
||||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
||||||
document.getElementById('calendarTitle').textContent = `${monthNames[month]} ${year}`;
|
|
||||||
|
|
||||||
// Clear calendar
|
|
||||||
const grid = document.getElementById('calendarGrid');
|
|
||||||
grid.innerHTML = '';
|
|
||||||
|
|
||||||
// Add day headers
|
|
||||||
const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
||||||
dayHeaders.forEach(day => {
|
|
||||||
const header = document.createElement('div');
|
|
||||||
header.className = 'calendar-day-header';
|
|
||||||
header.textContent = day;
|
|
||||||
grid.appendChild(header);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get first day of month and number of days
|
|
||||||
const firstDay = new Date(year, month, 1);
|
|
||||||
const lastDay = new Date(year, month + 1, 0);
|
|
||||||
const daysInMonth = lastDay.getDate();
|
|
||||||
const startingDayOfWeek = firstDay.getDay();
|
|
||||||
|
|
||||||
// Add empty cells for previous month
|
|
||||||
const prevMonth = new Date(year, month, 0);
|
|
||||||
const daysInPrevMonth = prevMonth.getDate();
|
|
||||||
|
|
||||||
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
|
|
||||||
const day = daysInPrevMonth - i;
|
|
||||||
const dayElement = createDayElement(day, true, year, month - 1);
|
|
||||||
grid.appendChild(dayElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add days of current month
|
|
||||||
for (let day = 1; day <= daysInMonth; day++) {
|
|
||||||
const dayElement = createDayElement(day, false, year, month);
|
|
||||||
grid.appendChild(dayElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add days from next month to fill the grid
|
|
||||||
const totalCells = grid.children.length - 7;
|
|
||||||
const remainingCells = 42 - totalCells;
|
|
||||||
|
|
||||||
for (let day = 1; day <= remainingCells; day++) {
|
|
||||||
const dayElement = createDayElement(day, true, year, month + 1);
|
|
||||||
grid.appendChild(dayElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDayElement(day, isOtherMonth, year, month) {
|
|
||||||
const dayElement = document.createElement('div');
|
|
||||||
dayElement.className = 'calendar-day';
|
|
||||||
|
|
||||||
const dayDate = new Date(year, month, day);
|
|
||||||
const dateString = dayDate.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
if (isOtherMonth) {
|
|
||||||
dayElement.classList.add('other-month');
|
|
||||||
} else {
|
|
||||||
dayElement.onclick = () => selectDate(dateString);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is today
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
if (!isOtherMonth && dateString === today) {
|
|
||||||
dayElement.classList.add('today');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is selected date
|
|
||||||
if (!isOtherMonth && dateString === selectedDate) {
|
|
||||||
dayElement.classList.add('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this day has data
|
|
||||||
const dayData = getDayData(dateString);
|
|
||||||
if (dayData.length > 0) {
|
|
||||||
dayElement.classList.add('has-data');
|
|
||||||
const totalCalories = dayData.reduce((sum, entry) => sum + entry.calories, 0);
|
|
||||||
|
|
||||||
dayElement.innerHTML = `
|
|
||||||
<div>${day}</div>
|
|
||||||
<div class="day-calories">${totalCalories} cal</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
dayElement.textContent = day;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dayElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize when page loads
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
Generated
+47
@@ -8,7 +8,12 @@
|
|||||||
"name": "calories-tracker",
|
"name": "calories-tracker",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"appwrite": "^18.1.1",
|
||||||
|
"sweetalert": "^2.1.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -724,6 +729,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/appwrite": {
|
||||||
|
"version": "18.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/appwrite/-/appwrite-18.1.1.tgz",
|
||||||
|
"integrity": "sha512-krwHjuwJcF+9Ig2+nqOEKMA/5kPIFhwwZsaLc7Gb8y2oP6EnG4ZMRPeHTFscdevOtVQj2Ax92cYYWAEvzlrc7A==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/es6-object-assign": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.5",
|
"version": "0.25.5",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||||
@@ -863,6 +880,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/promise-polyfill": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.43.0",
|
"version": "4.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
|
||||||
@@ -913,6 +936,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sweetalert": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sweetalert/-/sweetalert-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-iWx7X4anRBNDa/a+AdTmvAzQtkN1+s4j/JJRWlHpYE8Qimkohs8/XnFcWeYHH2lMA8LRCa5tj2d244If3S/hzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es6-object-assign": "^1.1.0",
|
||||||
|
"promise-polyfill": "^6.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
@@ -930,6 +963,20 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"appwrite": "^18.1.1",
|
||||||
|
"sweetalert": "^2.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export const getInputById = (elementId: string): HTMLInputElement => {
|
||||||
|
const el = document.getElementById(elementId) as HTMLInputElement;
|
||||||
|
if (!el) {
|
||||||
|
throw new Error(`Input Element id ${elementId} not found!`);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDivById = (elementId: string): HTMLElement => {
|
||||||
|
const el = document.getElementById(elementId) as HTMLElement;
|
||||||
|
if (!el) {
|
||||||
|
throw new Error(`HTML Element id ${elementId} not found!`);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getButtonById = (elementId: string): HTMLButtonElement => {
|
||||||
|
const el = document.getElementById(elementId) as HTMLButtonElement;
|
||||||
|
if (!el) {
|
||||||
|
throw new Error(`HTML Button Element id ${elementId} not found!`);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getButtonListByClassName = (name: string): Array<HTMLButtonElement> => {
|
||||||
|
const el = document.getElementsByClassName(name);
|
||||||
|
if (!el) {
|
||||||
|
throw new Error(`HTML Button List Elements not found for class ${name}!`);
|
||||||
|
}
|
||||||
|
return Array.from(el) as Array<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showFoodPreview = (show: boolean) => {
|
||||||
|
getDivById('food-preview').classList = `add-food-form-item ${show ? 'display-block' : 'display-none'}`;
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
import { Client, Account, Databases, Query } from 'appwrite';
|
||||||
|
import { FoodStorage } from './types';
|
||||||
|
|
||||||
|
// Initialize Appwrite client
|
||||||
|
const client = new Client();
|
||||||
|
|
||||||
|
client
|
||||||
|
.setEndpoint('https://cloud.appwrite.io/v1') // Your Appwrite endpoint
|
||||||
|
.setProject("684ac3bf000ee8950f5a"); // Replace with your project ID
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
export const account = new Account(client);
|
||||||
|
export const databases = new Databases(client);
|
||||||
|
|
||||||
|
// Configuration constants
|
||||||
|
export const DATABASE_ID = '684b38db000fac8c781c'; // Replace with your database ID
|
||||||
|
export const FOOD_ENTRIES_COLLECTION_ID = '684b38e300336fc605dc'; // Replace with your collection ID
|
||||||
|
|
||||||
|
// Auth helper functions
|
||||||
|
export class AppwriteAuth {
|
||||||
|
// Register new user
|
||||||
|
static async register(email: string, password: string, name: string) {
|
||||||
|
try {
|
||||||
|
const response = await account.create('unique()', email, password, name);
|
||||||
|
console.debug('User registered:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login user
|
||||||
|
static async login(email: string, password: string) {
|
||||||
|
try {
|
||||||
|
const session = await account.createEmailPasswordSession(email, password);
|
||||||
|
console.debug('User logged in:', session);
|
||||||
|
return session;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout user
|
||||||
|
static async logout() {
|
||||||
|
try {
|
||||||
|
await account.deleteSession('current');
|
||||||
|
console.debug('User logged out');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
static async getCurrentUser() {
|
||||||
|
try {
|
||||||
|
const user = await account.get();
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get user error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is logged in
|
||||||
|
static async isLoggedIn() {
|
||||||
|
try {
|
||||||
|
await account.get();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database helper functions
|
||||||
|
export class AppwriteDB {
|
||||||
|
// Save food entry
|
||||||
|
static async saveFoodEntry(entryData: FoodStorage) {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
FOOD_ENTRIES_COLLECTION_ID,
|
||||||
|
'unique()',
|
||||||
|
{
|
||||||
|
...entryData,
|
||||||
|
userId: (await account.get()).$id,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('Food entry saved:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save food entry error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's food entries for a specific date
|
||||||
|
static async getFoodEntries(date: Date) {
|
||||||
|
try {
|
||||||
|
const user = await account.get();
|
||||||
|
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
FOOD_ENTRIES_COLLECTION_ID,
|
||||||
|
[
|
||||||
|
Query.equal('userId', user.$id),
|
||||||
|
Query.equal('date', date.toISOString().split('T')[0]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.debug('Food entries retrieved:', response);
|
||||||
|
return response.documents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get food entries error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete food entry
|
||||||
|
static async deleteFoodEntry(documentId: string) {
|
||||||
|
try {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
FOOD_ENTRIES_COLLECTION_ID,
|
||||||
|
documentId
|
||||||
|
);
|
||||||
|
console.debug('Food entry deleted:', documentId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete food entry error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all food entries for a user (for calendar view)
|
||||||
|
static async getAllUserEntries() {
|
||||||
|
try {
|
||||||
|
const user = await account.get();
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
FOOD_ENTRIES_COLLECTION_ID,
|
||||||
|
[Query.equal('userId', user.$id)]
|
||||||
|
);
|
||||||
|
return response.documents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get all entries error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { FoodItem } from "./types";
|
||||||
|
|
||||||
|
export const foodDatabase: FoodItem[] = [
|
||||||
|
{ name: 'Frutas - Abacate', info: { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3 } },
|
||||||
|
{ name: 'Frutas - Abacaxi', info: { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1 } },
|
||||||
|
{ name: 'Frutas - Banana Nanica', info: { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9 } },
|
||||||
|
{ name: 'Frutas - Laranja', info: { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8 } },
|
||||||
|
{ name: 'Frutas - Maçã', info: { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3 } },
|
||||||
|
{ name: 'Frutas - Manga Palmer', info: { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6 } },
|
||||||
|
{ name: 'Frutas - Mamão', info: { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1 } },
|
||||||
|
{ name: 'Frutas - Melancia', info: { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1 } },
|
||||||
|
{ name: 'Frutas - Melão', info: { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3 } },
|
||||||
|
{ name: 'Frutas - Uva', info: { calories: 53, protein: 0.7, fat: 0.2, carbs: 13.6, fiber: 0.9 } },
|
||||||
|
|
||||||
|
{ name: 'Vegetais - Agrião', info: { calories: 17, protein: 2.7, fat: 0.2, carbs: 2.3, fiber: 2.1 } },
|
||||||
|
{ name: 'Vegetais - Brócolis (cozido)', info: { calories: 25, protein: 2.1, fat: 0.5, carbs: 4.4, fiber: 3.4 } },
|
||||||
|
{ name: 'Vegetais - Cenoura (cozida)', info: { calories: 30, protein: 0.8, fat: 0.2, carbs: 6.7, fiber: 2.6 } },
|
||||||
|
{ name: 'Vegetais - Couve flor (cozida)', info: { calories: 19, protein: 1.2, fat: 0.3, carbs: 3.9, fiber: 2.1 } },
|
||||||
|
{ name: 'Vegetais - Ervilha (cozida)', info: { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7 } },
|
||||||
|
{ name: 'Vegetais - Espinafre', info: { calories: 16, protein: 2, fat: 0.2, carbs: 2.6, fiber: 2.1 } },
|
||||||
|
{ name: 'Vegetais - Feijão preto (cozido)', info: { calories: 77, protein: 4.5, fat: 0.5, carbs: 14, fiber: 8.4 } },
|
||||||
|
{ name: 'Vegetais - Lentilha (cozida)', info: { calories: 93, protein: 6.3, fat: 0.5, carbs: 16.3, fiber: 7.9 } },
|
||||||
|
{ name: 'Vegetais - Mandioca (cozida)', info: { calories: 125, protein: 0.6, fat: 0.3, carbs: 30.1, fiber: 1.6 } },
|
||||||
|
{ name: 'Vegetais - Pepino', info: { calories: 10, protein: 0.9, fat: 0, carbs: 2, fiber: 1.1 } },
|
||||||
|
{ name: 'Vegetais - Rúcula', info: { calories: 13, protein: 1.8, fat: 0.1, carbs: 2.2, fiber: 1.7 } },
|
||||||
|
{ name: 'Vegetais - Spirulina', info: { calories: 290, protein: 57.5, fat: 23.9, carbs: 7.7, fiber: 0 } },
|
||||||
|
{ name: 'Vegetais - Tomate', info: { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.1, fiber: 1.2 } },
|
||||||
|
|
||||||
|
{ name: 'Carboidratos - Batata doce (assada)', info: { calories: 90, protein: 2.0, fat: 0.1, carbs: 20.7, fiber: 3.3 } },
|
||||||
|
{ name: 'Carboidratos - Batata doce (cozida)', info: { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2 } },
|
||||||
|
{ name: 'Carboidratos - Batata doce (crua)', info: { calories: 118, protein: 1.3, fat: 0.1, carbs: 28.2, fiber: 2.6 } },
|
||||||
|
{ name: 'Carboidratos - Batata inglesa (assada)', info: { calories: 94, protein: 2.1, fat: 0.1, carbs: 21.8, fiber: 2.1 } },
|
||||||
|
{ name: 'Carboidratos - Batata inglesa (cozida)', info: { calories: 52, protein: 1.2, fat: 0, carbs: 11.9, fiber: 1.3 } },
|
||||||
|
{ name: 'Carboidratos - Batata inglesa (crua)', info: { calories: 64, protein: 1.8, fat: 0, carbs: 14.7, fiber: 1.2 } },
|
||||||
|
{ name: 'Carboidratos - Pão Francês', info: { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3 } },
|
||||||
|
{ name: 'Carboidrados - Polenta (cozida)', info: { calories: 192, protein: 4.8, fat: 1.2, carbs: 40, fiber: 3.8 } },
|
||||||
|
|
||||||
|
{ name: 'Proteínas - Atum Sólido (natural)', info: { calories: 135, protein: 28.3, fat: 2.7, carbs: 0, fiber: 0 } },
|
||||||
|
{ name: 'Proteínas - Filé de Tilápia (cozido)', info: { calories: 128, protein: 26.1, fat: 2.6, carbs: 0, fiber: 0 } },
|
||||||
|
{ name: 'Proteínas - Ovo inteiro (cozido)', info: { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0 } },
|
||||||
|
{ name: 'Proteínas - Ovo clara (cozido)', info: { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0 } },
|
||||||
|
{ name: 'Proteínas - PTS (crua)', info: { calories: 288, protein: 52, fat: 0, carbs: 20, fiber: 14 } },
|
||||||
|
{ name: 'Proteínas - Patinho moído (grelhado)', info: { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0 } },
|
||||||
|
{ name: 'Proteínas - Soja em Pó Growth', info: { calories: 400, protein: 86.7, fat: 5, carbs: 3.3, fiber: 0 } },
|
||||||
|
{ name: 'Proteínas - Sobrecoxa s/pele (cozida)', info: { calories: 245, protein: 24.9, fat: 15.4, carbs: 0, fiber: 0 } },
|
||||||
|
{ name: 'Proteínas - Tofu', info: { calories: 65, protein: 6.5, fat: 4, carbs: 2, fiber: 0.75 } },
|
||||||
|
|
||||||
|
{ name: 'Grãos - Arroz integral (cozido)', info: { calories: 130, protein: 2.7, fat: 0.3, carbs: 28, fiber: 0.4 } },
|
||||||
|
{ name: 'Grãos - Aveia', info: { calories: 394, protein: 13.9, fat: 8.5, carbs: 66.6, fiber: 9.1 } },
|
||||||
|
{ name: 'Grãos - Grão de Bico (cozido)', info: { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6 } },
|
||||||
|
{ name: 'Grãos - Pão integral de forma', info: { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9 } },
|
||||||
|
{ name: 'Grãos - Pipoca c/óleo', info: { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3 } },
|
||||||
|
{ name: 'Grãos - Pipoca s/óleo', info: { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3 } },
|
||||||
|
{ name: 'Grãos - Quinoa (cozida)', info: { calories: 158, protein: 5.55, fat: 2.5, carbs: 29, fiber: 2.5 } },
|
||||||
|
|
||||||
|
{ name: 'Castanhas - Amêndoas', info: { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6 } },
|
||||||
|
{ name: 'Castanhas - Amendoin natural', info: { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5 } },
|
||||||
|
{ name: 'Castanhas - Castanha de Caju', info: { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7 } },
|
||||||
|
{ name: 'Castanhas - Castanha do Pará', info: { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9 } },
|
||||||
|
{ name: 'Sementes - Semente de Chia', info: { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7 } },
|
||||||
|
{ name: 'Sementes - Semente de Linhaça', info: { calories: 495, protein: 14.1, fat: 32.3, carbs: 43.3, fiber: 33.5 } },
|
||||||
|
{ name: 'Sementes - Semente de Gergelim', info: { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9 } },
|
||||||
|
|
||||||
|
{ name: 'Lanches - Barra de Proteína Vegana Growth', info: { calories: 400, protein: 25, fat: 22, carbs: 25, fiber: 8 } },
|
||||||
|
{ name: 'Lanches - Goiabada', info: { calories: 320, protein: 0, fat: 0, carbs: 80, fiber: 4.5 } },
|
||||||
|
{ name: 'Lanches - Mel Silvestre', info: { calories: 230, protein: 0, fat: 0, carbs: 65, fiber: 0 } },
|
||||||
|
{ name: 'Lanches - Melado', info: { calories: 297, protein: 0, fat: 0, carbs: 76.6, fiber: 0 } },
|
||||||
|
{ name: 'Lanches - Pasta de Amendoim Growth', info: { calories: 544, protein: 27, fat: 33, carbs: 20, fiber: 8 } },
|
||||||
|
{ name: 'Lanches - Goma de Tapioca', info: { calories: 226, protein: 0, fat: 0, carbs: 57, fiber: 0 } },
|
||||||
|
{ name: 'Lanches - 70% Nibs Garoto', info: { calories: 544, protein: 8.4, fat: 40, carbs: 34, fiber: 0 } }
|
||||||
|
];
|
||||||
+596
@@ -0,0 +1,596 @@
|
|||||||
|
import { foodDatabase } from './foodDatabase.js';
|
||||||
|
import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './HtmlUtil.ts';
|
||||||
|
import { FoodItem, FoodStorage } from './types.js';
|
||||||
|
import { AppwriteAuth, AppwriteDB } from './appwrite.js';
|
||||||
|
import swal from 'sweetalert';
|
||||||
|
|
||||||
|
// App state
|
||||||
|
let selectedDate = new Date();
|
||||||
|
let currentViewDate = new Date();
|
||||||
|
|
||||||
|
// Authentication state
|
||||||
|
let currentUser: any | null = null;
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const authSection = document.getElementById('auth-section');
|
||||||
|
const userInfo = document.getElementById('user-info');
|
||||||
|
const appContent = document.getElementById('app-content');
|
||||||
|
const loadingOverlay = getDivById('loading-overlay');
|
||||||
|
const loginForm = document.getElementById('login-form');
|
||||||
|
const registerForm = document.getElementById('register-form');
|
||||||
|
|
||||||
|
// Initialize when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
await initializeAuth();
|
||||||
|
populateFoodSelect();
|
||||||
|
updateCurrentDate();
|
||||||
|
setupEventListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize application
|
||||||
|
async function initializeAuth() {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if user is already logged in
|
||||||
|
const isLoggedIn = await AppwriteAuth.isLoggedIn();
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
currentUser = await AppwriteAuth.getCurrentUser();
|
||||||
|
showMainApp();
|
||||||
|
selectDate(selectedDate);
|
||||||
|
} else {
|
||||||
|
showAuthForms();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Initialize app error:', error);
|
||||||
|
showAuthForms();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle between login and register forms
|
||||||
|
function toggleAuthForms() {
|
||||||
|
loginForm?.classList.toggle('hidden');
|
||||||
|
registerForm?.classList.toggle('hidden');
|
||||||
|
|
||||||
|
// Clear form data
|
||||||
|
const logForm = document.getElementById('loginForm') as HTMLFormElement;
|
||||||
|
logForm.reset();
|
||||||
|
|
||||||
|
const resetForm = document.getElementById('registerForm') as HTMLFormElement;
|
||||||
|
resetForm.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user registration
|
||||||
|
async function handleRegister(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
const name = getInputById('registerName').value;
|
||||||
|
const email = getInputById('registerEmail').value;
|
||||||
|
const password = getInputById('registerPassword').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AppwriteAuth.register(email, password, name);
|
||||||
|
|
||||||
|
// Auto login after registration
|
||||||
|
await AppwriteAuth.login(email, password);
|
||||||
|
currentUser = await AppwriteAuth.getCurrentUser();
|
||||||
|
|
||||||
|
showMainApp();
|
||||||
|
selectDate(selectedDate);
|
||||||
|
|
||||||
|
swal('Registration successful! Welcome to Food Tracker.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
swal('Oh no!', 'Registration failed: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user login
|
||||||
|
async function handleLogin(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
const email = getInputById('loginEmail').value;
|
||||||
|
const password = getInputById('loginPassword').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AppwriteAuth.login(email, password);
|
||||||
|
currentUser = await AppwriteAuth.getCurrentUser();
|
||||||
|
|
||||||
|
showMainApp();
|
||||||
|
selectDate(selectedDate);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
swal('Oh no!', 'Login failed: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AppwriteAuth.logout();
|
||||||
|
currentUser = null;
|
||||||
|
showAuthForms();
|
||||||
|
clearAppData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
swal('Oh no!', 'Logout failed: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show authentication forms
|
||||||
|
function showAuthForms() {
|
||||||
|
authSection?.classList.remove('hidden');
|
||||||
|
userInfo?.classList.add('hidden');
|
||||||
|
appContent?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMainApp() {
|
||||||
|
authSection?.classList.add('hidden');
|
||||||
|
userInfo?.classList.remove('hidden');
|
||||||
|
appContent?.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Update user info display
|
||||||
|
if (currentUser) {
|
||||||
|
getDivById('userName').textContent = `Welcome, ${currentUser.name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update date display
|
||||||
|
updateCurrentDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide loading overlay
|
||||||
|
function showLoading() {
|
||||||
|
loadingOverlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading() {
|
||||||
|
loadingOverlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear application data
|
||||||
|
function clearAppData() {
|
||||||
|
getDivById('foodTableBody').innerHTML = '';
|
||||||
|
getDivById('caloriesCounter').textContent = '0';
|
||||||
|
getInputById('foodSelect').value = '';
|
||||||
|
getInputById('gramAmount').value = '100';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFoodData = (grams: number, foodData: FoodItem): FoodItem => {
|
||||||
|
const multiplier = grams / 100; // Database values are per 100g
|
||||||
|
return {
|
||||||
|
...foodData,
|
||||||
|
info: {
|
||||||
|
calories: Math.round(foodData.info.calories * multiplier),
|
||||||
|
protein: Math.round(foodData.info.protein * multiplier * 10) / 10,
|
||||||
|
fat: Math.round(foodData.info.fat * multiplier * 10) / 10,
|
||||||
|
carbs: Math.round(foodData.info.carbs * multiplier * 10) / 10,
|
||||||
|
fiber: Math.round(foodData.info.fiber * multiplier * 10) / 10
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFoodItemByName = (foodName: string): FoodItem => {
|
||||||
|
const foodDataSearch: FoodItem[] = foodDatabase.filter(f => f.name === foodName);
|
||||||
|
if (foodDataSearch.length === 0) {
|
||||||
|
throw new Error(`Food not found for name ${name}`);
|
||||||
|
}
|
||||||
|
return foodDataSearch[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewCalories = () => {
|
||||||
|
const foodSelect = getInputById('foodSelect');
|
||||||
|
const gramAmount = getInputById('gramAmount');
|
||||||
|
|
||||||
|
if (!foodSelect.value) {
|
||||||
|
showFoodPreview(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let grams = 100;
|
||||||
|
if (gramAmount && gramAmount.value && parseFloat(gramAmount.value) > 0) {
|
||||||
|
grams = parseFloat(gramAmount.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foodData: FoodItem = getFoodItemByName(foodSelect.value);
|
||||||
|
const proportion = getFoodData(grams, foodData);
|
||||||
|
|
||||||
|
getDivById('calorieValuePreview').textContent = proportion.info.calories.toString();
|
||||||
|
getDivById('proteinValuePreview').textContent = (Math.round(proportion.info.protein * 10) / 10).toString ();
|
||||||
|
getDivById('fatValuePreview').textContent = (Math.round(proportion.info.fat * 10) / 10).toString ();
|
||||||
|
getDivById('carboValuePreview').textContent = (Math.round(proportion.info.carbs * 10) / 10).toString ();
|
||||||
|
getDivById('fiberValuePreview').textContent = (Math.round(proportion.info.fiber * 10) / 10).toString ();
|
||||||
|
|
||||||
|
showFoodPreview(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupEventListeners = () => {
|
||||||
|
getInputById('foodSelect').addEventListener('change', () => {
|
||||||
|
previewCalories();
|
||||||
|
});
|
||||||
|
|
||||||
|
getInputById('gramAmount').addEventListener('change', () => {
|
||||||
|
previewCalories();
|
||||||
|
});
|
||||||
|
|
||||||
|
getButtonById('add-food-btn').addEventListener('click', () => {
|
||||||
|
addFood();
|
||||||
|
});
|
||||||
|
|
||||||
|
getButtonById('next-month-btn').addEventListener('click', () => {
|
||||||
|
nextMonth();
|
||||||
|
});
|
||||||
|
|
||||||
|
getButtonById('previous-month-btn').addEventListener('click', () => {
|
||||||
|
previousMonth();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form switching
|
||||||
|
document.getElementById('showRegister')?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleAuthForms();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('showLogin')?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleAuthForms();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auth forms
|
||||||
|
document.getElementById('loginForm')?.addEventListener('submit', handleLogin);
|
||||||
|
document.getElementById('registerForm')?.addEventListener('submit', handleRegister);
|
||||||
|
document.getElementById('logoutBtn')?.addEventListener('click', handleLogout);
|
||||||
|
}
|
||||||
|
|
||||||
|
const addDeleteEvents = () => {
|
||||||
|
const elements = getButtonListByClassName('delete-food-entry');
|
||||||
|
Array.from(elements).forEach((el: HTMLButtonElement) => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const row = target.closest('tr');
|
||||||
|
const entryId = row?.querySelector('.hidden-column')?.textContent;
|
||||||
|
if (entryId && row) {
|
||||||
|
handleDeleteFood(entryId, row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate food select dropdown
|
||||||
|
const populateFoodSelect = () => {
|
||||||
|
const select = getInputById('foodSelect');
|
||||||
|
select.innerHTML = '<option value="">Select a food item...</option>';
|
||||||
|
|
||||||
|
const sortedFoods = foodDatabase.sort((f1, f2) => f1.name.localeCompare(f2.name));
|
||||||
|
sortedFoods.forEach((food: FoodItem) => {
|
||||||
|
const option = document.createElement('option') as HTMLOptionElement;
|
||||||
|
option.value = food.name;
|
||||||
|
option.textContent = food.name;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current date display
|
||||||
|
const updateCurrentDate = () => {
|
||||||
|
const date = new Date(selectedDate);
|
||||||
|
getDivById('currentDate').textContent = date.toLocaleDateString('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add food to the log
|
||||||
|
const addFood = async () => {
|
||||||
|
if (!currentUser) {
|
||||||
|
swal('Hey!', 'Please log in to add food entries.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foodSelect = getInputById('foodSelect');
|
||||||
|
const gramAmount = getInputById('gramAmount');
|
||||||
|
|
||||||
|
if (!foodSelect.value || !gramAmount.value) {
|
||||||
|
swal('Hey!', 'Please select a food item and enter amount', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const grams: number = parseFloat(gramAmount.value);
|
||||||
|
const foodData: FoodItem = getFoodItemByName(foodSelect.value);
|
||||||
|
|
||||||
|
// Calculate nutrition for the amount
|
||||||
|
const proportion: FoodItem = getFoodData(grams, foodData);
|
||||||
|
const entry: FoodStorage = {
|
||||||
|
name: foodSelect.value,
|
||||||
|
grams: grams,
|
||||||
|
calories: proportion.info.calories,
|
||||||
|
protein: proportion.info.protein,
|
||||||
|
fat: proportion.info.fat,
|
||||||
|
carbs: proportion.info.carbs,
|
||||||
|
fiber: proportion.info.fiber,
|
||||||
|
date: selectedDate.toISOString().split('T')[0],
|
||||||
|
time: new Date().toLocaleTimeString('en-US', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to Appwrite
|
||||||
|
const savedEntry = await AppwriteDB.saveFoodEntry(entry);
|
||||||
|
|
||||||
|
// Add to HTML table with Appwrite document ID
|
||||||
|
addFoodToTable(entry, savedEntry.$id);
|
||||||
|
|
||||||
|
// Update totals
|
||||||
|
updateTotalCalories();
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
foodSelect.value = '';
|
||||||
|
gramAmount.value = '100';
|
||||||
|
showFoodPreview(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Add food error:', error);
|
||||||
|
swal('Oh no!', 'Failed to add food entry: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load food entries from Appwrite
|
||||||
|
async function loadFoodEntries(date: Date) {
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await AppwriteDB.getFoodEntries(date);
|
||||||
|
|
||||||
|
// Clear current table
|
||||||
|
getDivById('foodTableBody').innerHTML = '';
|
||||||
|
|
||||||
|
// Add each entry to table
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const foodData: FoodStorage = {
|
||||||
|
id: entry.$id,
|
||||||
|
name: entry.name,
|
||||||
|
grams: entry.grams,
|
||||||
|
calories: entry.calories,
|
||||||
|
protein: entry.protein,
|
||||||
|
fat: entry.fat,
|
||||||
|
carbs: entry.carbs,
|
||||||
|
fiber: entry.fiber,
|
||||||
|
time: entry.time,
|
||||||
|
date: entry.date,
|
||||||
|
};
|
||||||
|
|
||||||
|
addFoodToTable(foodData, entry.$id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup Delete events
|
||||||
|
addDeleteEvents();
|
||||||
|
|
||||||
|
// Update total calories
|
||||||
|
updateTotalCalories();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load food entries error:', error);
|
||||||
|
swal('Oh no!', 'Failed to load food entries: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle food deletion
|
||||||
|
async function handleDeleteFood(documentId: string, row: HTMLTableRowElement) {
|
||||||
|
if (!documentId) return;
|
||||||
|
|
||||||
|
const willDelete = await swal({
|
||||||
|
title: "Are you sure?",
|
||||||
|
text: 'You want to delete this food entry?',
|
||||||
|
icon: 'warning',
|
||||||
|
dangerMode: true,
|
||||||
|
closeOnEsc: true,
|
||||||
|
buttons:["No, abort", "Yes, Do it!"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!willDelete) return;
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete from Appwrite
|
||||||
|
await AppwriteDB.deleteFoodEntry(documentId);
|
||||||
|
|
||||||
|
// Remove from table
|
||||||
|
row.remove();
|
||||||
|
|
||||||
|
// Update totals
|
||||||
|
updateTotalCalories();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete food error:', error);
|
||||||
|
swal('Oh no!', 'Failed to delete food entry: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add food to table (existing function - update to use document ID)
|
||||||
|
function addFoodToTable(foodData: FoodStorage, documentId: string) {
|
||||||
|
const tableBody = document.getElementById('foodTableBody');
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="hidden-column">${documentId}</td>
|
||||||
|
<td>${foodData.time || new Date().toLocaleTimeString()}</td>
|
||||||
|
<td>${foodData.name}</td>
|
||||||
|
<td>${foodData.grams}</td>
|
||||||
|
<td>${foodData.calories}</td>
|
||||||
|
<td>${foodData.protein}</td>
|
||||||
|
<td>${foodData.fat}</td>
|
||||||
|
<td>${foodData.carbs}</td>
|
||||||
|
<td>${foodData.fiber}</td>
|
||||||
|
<td><button class="delete-btn delete-food-entry">Delete</button></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody?.appendChild(row);
|
||||||
|
|
||||||
|
// Setup Delete events
|
||||||
|
addDeleteEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total calories
|
||||||
|
function updateTotalCalories() {
|
||||||
|
const caloriesCells = document.querySelectorAll('#foodTableBody td:nth-child(5)'); // 5th column is calories
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
caloriesCells.forEach((cell: Element) => {
|
||||||
|
total += parseInt(cell.innerHTML) || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
getDivById('caloriesCounter').textContent = total.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar functions
|
||||||
|
const previousMonth = () => {
|
||||||
|
currentViewDate.setMonth(currentViewDate.getMonth() - 1);
|
||||||
|
renderCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
currentViewDate.setMonth(currentViewDate.getMonth() + 1);
|
||||||
|
renderCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDate = async (date: Date) => {
|
||||||
|
selectedDate = date;
|
||||||
|
updateCurrentDate();
|
||||||
|
await loadFoodEntries(date);
|
||||||
|
renderCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderCalendar = () => {
|
||||||
|
const year = currentViewDate.getFullYear();
|
||||||
|
const month = currentViewDate.getMonth();
|
||||||
|
|
||||||
|
// Update calendar title
|
||||||
|
const monthNames = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December'
|
||||||
|
];
|
||||||
|
getDivById('calendarTitle').textContent = `${monthNames[month]} ${year}`;
|
||||||
|
|
||||||
|
// Clear calendar
|
||||||
|
const grid = getDivById('calendarGrid');
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
// Add day headers
|
||||||
|
const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
dayHeaders.forEach(day => {
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'calendar-day-header';
|
||||||
|
header.textContent = day;
|
||||||
|
grid.appendChild(header);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get first day of month and number of days
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const daysInMonth = lastDay.getDate();
|
||||||
|
const startingDayOfWeek = firstDay.getDay();
|
||||||
|
|
||||||
|
// Add empty cells for previous month
|
||||||
|
const prevMonth = new Date(year, month, 0);
|
||||||
|
const daysInPrevMonth = prevMonth.getDate();
|
||||||
|
|
||||||
|
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
|
||||||
|
const day = daysInPrevMonth - i;
|
||||||
|
const dayElement = createDayElement(day, true, year, month - 1);
|
||||||
|
grid.appendChild(dayElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add days of current month
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dayElement = createDayElement(day, false, year, month);
|
||||||
|
grid.appendChild(dayElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add days from next month to fill the grid
|
||||||
|
const totalCells = grid.children.length - 7;
|
||||||
|
const remainingCells = 42 - totalCells;
|
||||||
|
|
||||||
|
for (let day = 1; day <= remainingCells; day++) {
|
||||||
|
const dayElement = createDayElement(day, true, year, month + 1);
|
||||||
|
grid.appendChild(dayElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDayElement(day: number, isOtherMonth: boolean, year: number, month: number) {
|
||||||
|
const dayElement = document.createElement('div') as HTMLElement;
|
||||||
|
dayElement.className = 'calendar-day';
|
||||||
|
|
||||||
|
const dayDate = new Date(year, month, day);
|
||||||
|
const dateString = dayDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
if (isOtherMonth) {
|
||||||
|
dayElement.classList.add('other-month');
|
||||||
|
} else {
|
||||||
|
dayElement.onclick = () => selectDate(dayDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is today
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
if (!isOtherMonth && dateString === today) {
|
||||||
|
dayElement.classList.add('today');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is selected date
|
||||||
|
if (!isOtherMonth && dateString === selectedDate.toISOString().split('T')[0]) {
|
||||||
|
dayElement.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this day has data
|
||||||
|
const dayData = []; // FIXME get from AppWrite
|
||||||
|
if (dayData.length > 0) {
|
||||||
|
dayElement.classList.add('has-data');
|
||||||
|
const totalCalories = 0; // dayData.reduce((sum, entry) => sum + entry.calories, 0);
|
||||||
|
|
||||||
|
dayElement.innerHTML = `
|
||||||
|
<div>${day}</div>
|
||||||
|
<div class="day-calories">${totalCalories} cal</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
dayElement.textContent = day.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export type FoodItem = {
|
||||||
|
name: string;
|
||||||
|
info: { calories: number, protein: number, fat: number, carbs: number, fiber: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FoodStorage = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
grams: number;
|
||||||
|
calories: number;
|
||||||
|
protein: number;
|
||||||
|
fat: number;
|
||||||
|
carbs: number;
|
||||||
|
fiber: number;
|
||||||
|
time: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user