Files
calorie_tracker_2/server.js
2026-01-30 23:32:43 +08:00

735 lines
20 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const expressLayouts = require('express-ejs-layouts');
const flash = require('express-flash');
const methodOverride = require('method-override');
const path = require('path');
const SQLiteStore = require('connect-sqlite3')(session);
const db = require('./models');
const utils = require('./utils');
const { NutritionAPI, searchAllSources } = require('./utils/api_client');
const app = express();
const PORT = process.env.PORT || 5001;
// Initialize API Client
const apiClient = new NutritionAPI(process.env.API_NINJAS_KEY);
// Passport Config
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
const user = await db.User.findOne({ where: { username } });
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
} catch (err) {
return done(err);
}
}
));
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await db.User.findByPk(id);
done(null, user);
} catch (err) {
done(err);
}
});
// Middleware
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.set('layout', 'layout');
app.use(expressLayouts);
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(methodOverride('_method'));
// Session
app.use(session({
store: new SQLiteStore({ db: 'sessions.db', dir: './data' }),
secret: process.env.SECRET_KEY || 'secret',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } // 30 days
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());
// Global Variables
app.use((req, res, next) => {
res.locals.current_user = req.user;
res.locals.success_msg = req.flash('success_msg');
res.locals.error_msg = req.flash('error_msg');
res.locals.error = req.flash('error');
res.locals.path = req.path;
next();
});
// Helper functions for templates
app.locals.round = Math.round;
// Auth Middleware
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('error_msg', 'Please log in to view that resource');
res.redirect('/login');
}
// Routes
app.get('/', (req, res) => {
if (req.isAuthenticated()) {
res.redirect('/dashboard');
} else {
res.redirect('/login');
}
});
app.get('/login', (req, res) => {
res.render('login');
});
app.post('/login', passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
failureFlash: true
}));
app.get('/register', (req, res) => {
res.render('register');
});
app.post('/register', async (req, res) => {
const { username, password, name } = req.body;
try {
const existingUser = await db.User.findOne({ where: { username } });
if (existingUser) {
req.flash('error', 'Username already exists');
return res.redirect('/register');
}
await db.User.create({ username, password, name });
req.flash('success_msg', 'You are now registered and can log in');
res.redirect('/login');
} catch (err) {
console.error(err);
req.flash('error', 'Error registering user');
res.redirect('/register');
}
});
app.get('/logout', (req, res, next) => {
req.logout((err) => {
if (err) { return next(err); }
req.flash('success_msg', 'You are logged out');
res.redirect('/login');
});
});
app.get('/dashboard', ensureAuthenticated, async (req, res) => {
try {
const today = new Date();
const dateStr = today.toISOString().split('T')[0];
// Get daily totals
const nutrition = await utils.calculateDailyTotals(req.user.id, dateStr);
const water = await utils.calculateWaterTotal(req.user.id, dateStr);
// Get user goals
let goals = await db.UserGoal.findOne({ where: { user_id: req.user.id } });
if (!goals) {
goals = await db.UserGoal.create({
user_id: req.user.id,
target_protein_g: 150,
target_carbs_g: 200,
target_fat_g: 60,
target_water_ml: 2000
});
}
// Get weight info
const weightLogToday = await db.WeightLog.findOne({
where: { user_id: req.user.id, date: dateStr }
});
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
const weightLogYesterday = await db.WeightLog.findOne({
where: { user_id: req.user.id, date: yesterdayStr }
});
let weightChange = null;
if (weightLogToday && weightLogYesterday) {
weightChange = weightLogToday.weight_kg - weightLogYesterday.weight_kg;
}
// Calculate remaining
const remaining = {
calories: req.user.target_daily_calories - nutrition.calories,
protein: goals.target_protein_g - nutrition.protein,
carbs: goals.target_carbs_g - nutrition.carbs,
fat: goals.target_fat_g - nutrition.fat,
water: goals.target_water_ml - water.total_ml
};
// Get macro percentages
const macroPercentages = utils.getMacroPercentages(
nutrition.protein,
nutrition.carbs,
nutrition.fat
);
// Get trends
const weightTrend = await utils.getWeightTrend(req.user.id, 7);
const calorieTrend = await utils.getCalorieTrend(req.user.id, 7);
// Suggestions
const suggestions = utils.suggestFoodsForMacros(
remaining.protein,
remaining.carbs,
remaining.fat
);
res.render('dashboard', {
nutrition,
water,
goals,
remaining,
macro_percentages: macroPercentages,
weight_today: weightLogToday,
weight_change: weightChange,
weight_trend: weightTrend,
calorie_trend: calorieTrend,
suggestions,
today: dateStr
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading dashboard');
res.render('dashboard', {
nutrition: { calories: 0, protein: 0, carbs: 0, fat: 0 },
water: { total_ml: 0 },
goals: { target_water_ml: 2000 },
remaining: { calories: 2000, protein: 150, carbs: 200, fat: 60, water: 2000 },
macro_percentages: { protein: 0, carbs: 0, fat: 0 },
weight_trend: [],
calorie_trend: [],
suggestions: [],
today: new Date().toISOString().split('T')[0]
});
}
});
app.get('/add-meal', ensureAuthenticated, (req, res) => {
res.render('add_meal', { today: new Date().toISOString().split('T')[0] });
});
app.post('/add-meal', ensureAuthenticated, async (req, res) => {
try {
let { date, meal_type, time, 'food_id[]': foodIds, 'quantity[]': quantities } = req.body;
if (!foodIds) {
req.flash('error_msg', 'Please add at least one food item');
return res.redirect('/add-meal');
}
// Normalize input (handle single item vs array)
if (!Array.isArray(foodIds)) {
foodIds = [foodIds];
quantities = [quantities];
}
const meal = await db.Meal.create({
user_id: req.user.id,
date: date || new Date(),
meal_type,
time: time || null
});
for (let i = 0; i < foodIds.length; i++) {
const foodId = foodIds[i];
const quantity = quantities[i];
if (foodId && quantity) {
const food = await db.FoodItem.findByPk(foodId);
if (food) {
await db.MealFood.create({
MealId: meal.id,
FoodItemId: foodId,
quantity: quantity,
calories_consumed: food.calories * quantity,
protein_consumed: food.protein_g * quantity,
carbs_consumed: food.carbs_g * quantity,
fat_consumed: food.fat_g * quantity
});
}
}
}
await utils.updateDailySummary(req.user.id, date);
req.flash('success_msg', 'Meal added successfully!');
res.redirect('/dashboard');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding meal');
res.redirect('/add-meal');
}
});
app.get('/api/search-food', ensureAuthenticated, async (req, res) => {
const query = req.query.q || '';
if (query.length < 2) {
return res.json([]);
}
try {
const results = await searchAllSources(query, apiClient);
res.json(results);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
app.post('/api/add-food', ensureAuthenticated, async (req, res) => {
try {
const data = req.body;
const food = await apiClient.saveFoodToDb(data);
if (food) {
res.json({
success: true,
food_id: food.id,
name: food.name
});
} else {
res.status(500).json({ success: false });
}
} catch (err) {
console.error(err);
res.status(500).json({ success: false });
}
});
app.post('/add-water', ensureAuthenticated, async (req, res) => {
try {
const amountMl = parseInt(req.body.amount_ml) || 250;
const date = req.body.date || new Date().toISOString().split('T')[0];
await db.WaterLog.create({
user_id: req.user.id,
date: date,
amount_ml: amountMl,
time: new Date()
});
await utils.updateDailySummary(req.user.id, date);
req.flash('success_msg', `Added ${amountMl}ml of water!`);
res.redirect('/dashboard');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding water');
res.redirect('/dashboard');
}
});
app.post('/add-weight', ensureAuthenticated, async (req, res) => {
try {
const weightKg = parseFloat(req.body.weight_kg);
const date = req.body.date || new Date().toISOString().split('T')[0];
let weightLog = await db.WeightLog.findOne({
where: { user_id: req.user.id, date: date }
});
if (weightLog) {
weightLog.weight_kg = weightKg;
weightLog.time = new Date();
await weightLog.save();
} else {
await db.WeightLog.create({
user_id: req.user.id,
date: date,
weight_kg: weightKg,
time: new Date()
});
}
await utils.updateDailySummary(req.user.id, date);
res.redirect('/dashboard');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding weight');
res.redirect('/dashboard');
}
});
app.get('/goals', ensureAuthenticated, async (req, res) => {
try {
const userGoals = await db.UserGoal.findOne({ where: { user_id: req.user.id } });
let bmr = null;
let tdee = null;
if (req.user.weight_kg && req.user.height_cm && req.user.age) {
bmr = utils.calculateBMR(
req.user.weight_kg,
req.user.height_cm,
req.user.age,
req.user.gender || 'male'
);
tdee = utils.calculateTDEE(bmr, req.user.activity_level || 'moderate');
}
res.render('goals', {
goals: userGoals,
bmr,
tdee
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading goals');
res.redirect('/dashboard');
}
});
app.post('/goals', ensureAuthenticated, async (req, res) => {
try {
// Update user info
const user = await db.User.findByPk(req.user.id);
user.age = parseInt(req.body.age) || 25;
user.gender = req.body.gender || 'male';
user.height_cm = parseFloat(req.body.height_cm) || 170;
user.weight_kg = parseFloat(req.body.weight_kg) || 70;
user.activity_level = req.body.activity_level || 'moderate';
// Calculate targets
const bmr = utils.calculateBMR(user.weight_kg, user.height_cm, user.age, user.gender);
const tdee = utils.calculateTDEE(bmr, user.activity_level);
const goalType = req.body.goal_type || 'recomp';
let targetCalories;
if (goalType === 'weight_loss') {
targetCalories = tdee - 500;
} else if (goalType === 'muscle_gain') {
targetCalories = tdee + 300;
} else {
targetCalories = tdee;
}
user.target_daily_calories = Math.round(targetCalories);
await user.save();
// Update or create goals
let userGoals = await db.UserGoal.findOne({ where: { user_id: req.user.id } });
if (!userGoals) {
userGoals = await db.UserGoal.create({ user_id: req.user.id });
}
userGoals.goal_type = goalType;
userGoals.target_weight_kg = parseFloat(req.body.target_weight_kg) || 70;
// Calculate macros
const macros = utils.calculateMacroTargets(user.weight_kg, goalType);
userGoals.target_protein_g = macros.protein_g;
userGoals.target_carbs_g = macros.carbs_g;
userGoals.target_fat_g = macros.fat_g;
userGoals.target_water_ml = parseInt(req.body.target_water_ml) || 2000;
await userGoals.save();
req.flash('success_msg', 'Goals updated successfully!');
res.redirect('/goals');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error updating goals');
res.redirect('/goals');
}
});
app.get('/foods', ensureAuthenticated, async (req, res) => {
try {
const category = req.query.category || 'all';
const searchQuery = req.query.q || '';
const whereClause = {};
if (category !== 'all') {
whereClause.category = category;
}
if (searchQuery) {
whereClause[db.Sequelize.Op.or] = [
{ name: { [db.Sequelize.Op.like]: `%${searchQuery}%` } },
{ name_tagalog: { [db.Sequelize.Op.like]: `%${searchQuery}%` } }
];
}
const filipinoWhere = { ...whereClause, is_filipino: true };
const otherWhere = { ...whereClause, is_filipino: false };
const filipinoFoods = await db.FoodItem.findAll({
where: filipinoWhere,
order: [['name', 'ASC']]
});
const otherFoods = await db.FoodItem.findAll({
where: otherWhere,
limit: 20,
order: [['name', 'ASC']]
});
const categories = ['all', 'kanin', 'ulam', 'sabaw', 'gulay', 'meryenda', 'almusal'];
res.render('foods', {
filipino_foods: filipinoFoods,
other_foods: otherFoods,
categories,
current_category: category,
search_query: searchQuery
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading foods');
res.redirect('/dashboard');
}
});
app.get('/progress', ensureAuthenticated, async (req, res) => {
try {
const days = parseInt(req.query.days) || 30;
const weightTrend = await utils.getWeightTrend(req.user.id, days);
const calorieTrend = await utils.getCalorieTrend(req.user.id, days);
let avgCalories = 0, avgProtein = 0, avgCarbs = 0, avgFat = 0;
if (calorieTrend.length > 0) {
avgCalories = calorieTrend.reduce((sum, d) => sum + d.calories, 0) / calorieTrend.length;
avgProtein = calorieTrend.reduce((sum, d) => sum + d.protein, 0) / calorieTrend.length;
avgCarbs = calorieTrend.reduce((sum, d) => sum + d.carbs, 0) / calorieTrend.length;
avgFat = calorieTrend.reduce((sum, d) => sum + d.fat, 0) / calorieTrend.length;
}
let weightChange = null;
if (weightTrend.length >= 2) {
weightChange = weightTrend[weightTrend.length - 1].weight_kg - weightTrend[0].weight_kg;
}
res.render('progress', {
weight_trend: weightTrend,
calorie_trend: calorieTrend,
avg_calories: Math.round(avgCalories),
avg_protein: Math.round(avgProtein),
avg_carbs: Math.round(avgCarbs),
avg_fat: Math.round(avgFat),
weight_change: weightChange,
days
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading progress');
res.redirect('/dashboard');
}
});
app.get('/meal-planner', ensureAuthenticated, async (req, res) => {
try {
const today = new Date();
// Calculate Monday of current week
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
const startDate = new Date(today);
startDate.setDate(diff);
const dates = [];
for (let i = 0; i < 7; i++) {
const d = new Date(startDate);
d.setDate(startDate.getDate() + i);
dates.push(d);
}
const mealPlans = {};
for (const d of dates) {
const dateStr = d.toISOString().split('T')[0];
const plans = await db.MealPlan.findAll({
where: { user_id: req.user.id, date: dateStr },
include: [{
model: db.PlannedFood,
include: [db.FoodItem]
}]
});
mealPlans[dateStr] = plans.map(p => {
let calories = 0, protein = 0, carbs = 0, fat = 0;
const foods = [];
if (p.PlannedFoods) {
p.PlannedFoods.forEach(pf => {
if (pf.FoodItem) {
const q = pf.quantity || 1;
calories += pf.FoodItem.calories * q;
protein += pf.FoodItem.protein_g * q;
carbs += pf.FoodItem.carbs_g * q;
fat += pf.FoodItem.fat_g * q;
foods.push({
name: pf.FoodItem.name,
quantity: q
});
}
});
}
return {
id: p.id,
meal_type: p.meal_type,
is_completed: p.is_completed,
foods: foods,
totals: {
calories, protein, carbs, fat
}
};
});
}
res.render('meal_planner', {
dates: dates.map(d => d.toISOString().split('T')[0]),
meal_plans: mealPlans,
today: new Date().toISOString().split('T')[0]
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading meal planner');
res.redirect('/dashboard');
}
});
app.post('/meal-planner/add', ensureAuthenticated, async (req, res) => {
try {
let { date, meal_type, 'food_id[]': foodIds, 'quantity[]': quantities } = req.body;
if (!foodIds) {
req.flash('error_msg', 'Please add at least one food item');
return res.redirect('/meal-planner');
}
// Normalize input
if (!Array.isArray(foodIds)) {
foodIds = [foodIds];
quantities = [quantities];
}
// Find or create meal plan
let mealPlan = await db.MealPlan.findOne({
where: {
user_id: req.user.id,
date: date,
meal_type: meal_type
}
});
if (!mealPlan) {
mealPlan = await db.MealPlan.create({
user_id: req.user.id,
date: date,
meal_type: meal_type
});
}
// Add foods
for (let i = 0; i < foodIds.length; i++) {
const foodId = foodIds[i];
const quantity = quantities[i];
if (foodId && quantity) {
await db.PlannedFood.create({
MealPlanId: mealPlan.id,
FoodItemId: foodId,
quantity: quantity
});
}
}
req.flash('success_msg', 'Meal plan updated!');
res.redirect('/meal-planner');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding meal plan');
res.redirect('/meal-planner');
}
});
app.post('/meal-planner/complete/:id', ensureAuthenticated, async (req, res) => {
try {
const plan = await db.MealPlan.findOne({
where: { id: req.params.id, user_id: req.user.id }
});
if (plan) {
plan.is_completed = !plan.is_completed;
await plan.save();
// If marked complete, optionally create actual meal log?
// For now just toggle status as per request simplicity.
}
res.redirect('/meal-planner');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error updating plan status');
res.redirect('/meal-planner');
}
});
app.post('/meal-planner/delete/:id', ensureAuthenticated, async (req, res) => {
try {
await db.MealPlan.destroy({
where: { id: req.params.id, user_id: req.user.id }
});
req.flash('success_msg', 'Meal plan deleted');
res.redirect('/meal-planner');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error deleting meal plan');
res.redirect('/meal-planner');
}
});
// Database Sync and Server Start
const startServer = async () => {
try {
await db.sequelize.sync();
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
} catch (err) {
console.error('Database connection error:', err);
}
};
startServer();