feat: add appwrite
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
node_modules/
|
||||
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;
|
||||
}
|
||||
}
|
||||
+165
-96
@@ -5,115 +5,184 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Daily Calorie Tracker</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="auth.css">
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
<!-- Authentication Header -->
|
||||
<header class="auth-header">
|
||||
<div class="auth-container">
|
||||
<div class="logo">
|
||||
<h2>🍎 Food Tracker</h2>
|
||||
</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>
|
||||
<!-- Authentication Forms -->
|
||||
<div id="auth-section" class="auth-section">
|
||||
<!-- Login Form -->
|
||||
<div id="login-form" class="auth-form">
|
||||
<h3>Login</h3>
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<input type="email" id="loginEmail" placeholder="Email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="loginPassword" placeholder="Password" required>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn">Login</button>
|
||||
<p class="auth-switch">
|
||||
Don't have an account?
|
||||
<a href="#" id="showRegister">Register here</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gramAmount">Amount (grams)</label>
|
||||
<input type="number" id="gramAmount" min="1" step="1" value="100">
|
||||
|
||||
<!-- Register Form -->
|
||||
<div id="register-form" class="auth-form hidden">
|
||||
<h3>Register</h3>
|
||||
<form id="registerForm">
|
||||
<div class="form-group">
|
||||
<input type="text" id="registerName" placeholder="Full 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>
|
||||
<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>
|
||||
|
||||
<!-- User Info (shown when logged in) -->
|
||||
<div id="user-info" class="user-info hidden">
|
||||
<div class="user-details">
|
||||
<span id="userName">Welcome, User!</span>
|
||||
<button id="logoutBtn" class="logout-btn">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loading-overlay" class="loading-overlay hidden">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</html>
|
||||
|
||||
+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.log('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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+336
-125
@@ -1,21 +1,173 @@
|
||||
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';
|
||||
|
||||
// App state
|
||||
let selectedDate = new Date().toISOString().split('T')[0];
|
||||
let selectedDate = new Date();
|
||||
let currentViewDate = new Date();
|
||||
|
||||
// Initialize the app
|
||||
const init = () => {
|
||||
// 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();
|
||||
loadDayData(selectedDate);
|
||||
renderCalendar();
|
||||
addEvents();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
const getFoodProportion = (grams: number, foodData: FoodItem): FoodItem => {
|
||||
// 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);
|
||||
|
||||
alert('Registration successful! Welcome to Food Tracker.');
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
alert('Registration failed: ' + error.message);
|
||||
} 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);
|
||||
alert('Login failed: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
await AppwriteAuth.logout();
|
||||
currentUser = null;
|
||||
showAuthForms();
|
||||
clearAppData();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
alert('Logout failed: ' + error.message);
|
||||
} 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,
|
||||
@@ -52,7 +204,7 @@ const previewCalories = () => {
|
||||
}
|
||||
|
||||
const foodData: FoodItem = getFoodItemByName(foodSelect.value);
|
||||
const proportion = getFoodProportion(grams, foodData);
|
||||
const proportion = getFoodData(grams, foodData);
|
||||
|
||||
getDivById('calorieValuePreview').textContent = proportion.info.calories.toString();
|
||||
getDivById('proteinValuePreview').textContent = (Math.round(proportion.info.protein * 10) / 10).toString ();
|
||||
@@ -63,7 +215,7 @@ const previewCalories = () => {
|
||||
showFoodPreview(true);
|
||||
}
|
||||
|
||||
const addEvents = () => {
|
||||
const setupEventListeners = () => {
|
||||
getInputById('foodSelect').addEventListener('change', () => {
|
||||
previewCalories();
|
||||
});
|
||||
@@ -83,6 +235,25 @@ const addEvents = () => {
|
||||
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);
|
||||
|
||||
// Food table delete buttons (event delegation)
|
||||
// document.getElementById('foodTableBody')?.addEventListener('click', handleDeleteFood);
|
||||
}
|
||||
|
||||
const addDeleteEvents = () => {
|
||||
@@ -92,9 +263,8 @@ const addDeleteEvents = () => {
|
||||
const target = e.target as HTMLElement;
|
||||
const row = target.closest('tr');
|
||||
const entryId = row?.querySelector('.hidden-column')?.textContent;
|
||||
const entryIdNumber = entryId ? parseInt(entryId) : 0;
|
||||
if (entryIdNumber) {
|
||||
deleteEntry(selectedDate, entryIdNumber);
|
||||
if (entryId && row) {
|
||||
handleDeleteFood(entryId, row);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -126,7 +296,12 @@ const updateCurrentDate = () => {
|
||||
}
|
||||
|
||||
// Add food to the log
|
||||
const addFood = () => {
|
||||
const addFood = async () => {
|
||||
if (!currentUser) {
|
||||
alert('Please log in to add food entries.');
|
||||
return;
|
||||
}
|
||||
|
||||
const foodSelect = getInputById('foodSelect');
|
||||
const gramAmount = getInputById('gramAmount');
|
||||
|
||||
@@ -135,120 +310,157 @@ const addFood = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const grams: number = parseFloat(gramAmount.value);
|
||||
const foodData: FoodItem = getFoodItemByName(foodSelect.value);
|
||||
showLoading();
|
||||
|
||||
// Calculate nutrition for the amount
|
||||
const proportion: FoodItem = getFoodProportion(grams, foodData);
|
||||
const entry: FoodStorage = {
|
||||
id: Date.now(),
|
||||
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,
|
||||
time: new Date().toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
};
|
||||
try {
|
||||
const grams: number = parseFloat(gramAmount.value);
|
||||
const foodData: FoodItem = getFoodItemByName(foodSelect.value);
|
||||
|
||||
// Save to localStorage
|
||||
const dayData = getDayData(selectedDate);
|
||||
dayData.push(entry);
|
||||
saveDayData(selectedDate, dayData);
|
||||
// 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'
|
||||
})
|
||||
};
|
||||
|
||||
// Update display
|
||||
loadDayData(selectedDate);
|
||||
renderCalendar();
|
||||
// Save to Appwrite
|
||||
const savedEntry = await AppwriteDB.saveFoodEntry(entry);
|
||||
|
||||
// Reset form
|
||||
foodSelect.value = '';
|
||||
gramAmount.value = '100';
|
||||
showFoodPreview(false);
|
||||
}
|
||||
// Add to HTML table with Appwrite document ID
|
||||
addFoodToTable(entry, savedEntry.$id);
|
||||
|
||||
// Get day data from localStorage
|
||||
const getDayData = (date: string): FoodStorage[] => {
|
||||
const key = `calories_${date}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
if (!saved) {
|
||||
return [];
|
||||
}
|
||||
return JSON.parse(saved);
|
||||
}
|
||||
// Update totals
|
||||
updateTotalCalories();
|
||||
|
||||
// Save day data to localStorage
|
||||
const saveDayData = (date: string, data: FoodStorage[]) => {
|
||||
const key = `calories_${date}`;
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// Load and display day data
|
||||
const loadDayData = (date: string) => {
|
||||
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;
|
||||
});
|
||||
|
||||
getDivById('caloriesCounter').textContent = totalCalories.toString();
|
||||
getDivById('proteinValue').textContent = (Math.round(totalProtein * 10) / 10).toString ();
|
||||
getDivById('fatValue').textContent = (Math.round(totalFat * 10) / 10).toString ();
|
||||
getDivById('carboValue').textContent = (Math.round(totalCarbs * 10) / 10).toString ();
|
||||
getDivById('fiberValue').textContent = (Math.round(totalFiber * 10) / 10).toString ();
|
||||
|
||||
// Update table
|
||||
const tbody = document.getElementById('foodTableBody');
|
||||
if (!tbody) {
|
||||
throw Error('Unable to create row tables!');
|
||||
}
|
||||
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
|
||||
const deleteEntry = (date: string, entryId: number) => {
|
||||
if (confirm('Are you sure you want to delete this entry?')) {
|
||||
let dayData: FoodStorage[] = getDayData(date);
|
||||
dayData = dayData.filter(entry => entry.id !== entryId);
|
||||
saveDayData(date, dayData);
|
||||
loadDayData(selectedDate);
|
||||
// Update display
|
||||
renderCalendar();
|
||||
|
||||
// Reset form
|
||||
foodSelect.value = '';
|
||||
gramAmount.value = '100';
|
||||
showFoodPreview(false);
|
||||
} catch (error) {
|
||||
console.error('Add food error:', error);
|
||||
alert('Failed to add food entry: ' + error.message);
|
||||
} 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);
|
||||
alert('Failed to load food entries: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle food deletion
|
||||
async function handleDeleteFood(documentId: string, row: HTMLTableRowElement) {
|
||||
if (!documentId) return;
|
||||
|
||||
if (!confirm('Are you sure you want to delete this food entry?')) 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);
|
||||
alert('Failed to delete food entry: ' + error.message);
|
||||
} 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -260,10 +472,10 @@ const nextMonth = () => {
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
const selectDate = (date: string) => {
|
||||
const selectDate = async (date: Date) => {
|
||||
selectedDate = date;
|
||||
updateCurrentDate();
|
||||
loadDayData(selectedDate);
|
||||
await loadFoodEntries(date);
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
@@ -343,7 +555,7 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
|
||||
if (isOtherMonth) {
|
||||
dayElement.classList.add('other-month');
|
||||
} else {
|
||||
dayElement.onclick = () => selectDate(dateString);
|
||||
dayElement.onclick = () => selectDate(dayDate);
|
||||
}
|
||||
|
||||
// Check if this is today
|
||||
@@ -353,15 +565,15 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
|
||||
}
|
||||
|
||||
// Check if this is selected date
|
||||
if (!isOtherMonth && dateString === selectedDate) {
|
||||
if (!isOtherMonth && dateString === selectedDate.toISOString().split('T')[0]) {
|
||||
dayElement.classList.add('selected');
|
||||
}
|
||||
|
||||
// Check if this day has data
|
||||
const dayData = getDayData(dateString);
|
||||
const dayData = []; // FIXME get from AppWrite
|
||||
if (dayData.length > 0) {
|
||||
dayElement.classList.add('has-data');
|
||||
const totalCalories = dayData.reduce((sum, entry) => sum + entry.calories, 0);
|
||||
const totalCalories = 0; // dayData.reduce((sum, entry) => sum + entry.calories, 0);
|
||||
|
||||
dayElement.innerHTML = `
|
||||
<div>${day}</div>
|
||||
@@ -374,5 +586,4 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
|
||||
return dayElement;
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ export type FoodItem = {
|
||||
}
|
||||
|
||||
export type FoodStorage = {
|
||||
id: number;
|
||||
id?: string;
|
||||
name: string;
|
||||
grams: number;
|
||||
calories: number;
|
||||
@@ -13,4 +13,5 @@ export type FoodStorage = {
|
||||
carbs: number;
|
||||
fiber: number;
|
||||
time: string;
|
||||
date: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user