Vibed it... :(

This commit is contained in:
2025-08-09 14:34:48 +01:00
commit 5cf478feab
41 changed files with 23512 additions and 0 deletions

64
backend/models/Recipe.js Normal file
View File

@@ -0,0 +1,64 @@
const mongoose = require('mongoose');
const ingredientSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
amount: {
type: Number,
required: true
},
unit: {
type: String,
required: true
}
});
const recipeSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
ingredients: [ingredientSchema],
instructions: [{
step: Number,
description: String
}],
servings: {
type: Number,
default: 4
},
prepTime: {
type: Number, // in minutes
required: true
},
cookTime: {
type: Number, // in minutes
required: true
},
category: {
type: String,
enum: ['breakfast', 'lunch', 'dinner', 'dessert', 'snack', 'appetizer'],
required: true
},
difficulty: {
type: String,
enum: ['easy', 'medium', 'hard'],
default: 'medium'
},
imageUrl: {
type: String,
default: ''
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Recipe', recipeSchema);

49
backend/models/User.js Normal file
View File

@@ -0,0 +1,49 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3,
maxlength: 30
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
},
createdAt: {
type: Date,
default: Date.now
}
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);

View File

@@ -0,0 +1,55 @@
const mongoose = require('mongoose');
const userSelectionSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
selectedRecipes: [{
recipeId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Recipe',
required: true
},
quantity: {
type: Number,
default: 1,
min: 1
},
addedAt: {
type: Date,
default: Date.now
}
}],
aggregatedIngredients: [{
name: String,
totalAmount: Number,
unit: String,
recipes: [{
recipeId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Recipe'
},
recipeTitle: String,
amount: Number,
quantity: Number // recipe quantity multiplier
}]
}],
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Update the updatedAt field before saving
userSelectionSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('UserSelection', userSelectionSchema);

1656
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
backend/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "recipe-backend",
"version": "1.0.0",
"description": "Backend for recipe management app",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"mongoose": "^7.5.0",
"dotenv": "^16.3.1",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": ["recipe", "management", "api"],
"author": "",
"license": "ISC"
}

81
backend/routes/recipes.js Normal file
View File

@@ -0,0 +1,81 @@
const express = require('express');
const router = express.Router();
const Recipe = require('../models/Recipe');
// Get all recipes
router.get('/', async (req, res) => {
try {
const { category, difficulty, search } = req.query;
let filter = {};
if (category) filter.category = category;
if (difficulty) filter.difficulty = difficulty;
if (search) {
filter.$or = [
{ title: { $regex: search, $options: 'i' } },
{ description: { $regex: search, $options: 'i' } }
];
}
const recipes = await Recipe.find(filter).sort({ createdAt: -1 });
res.json(recipes);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get recipe by ID
router.get('/:id', async (req, res) => {
try {
const recipe = await Recipe.findById(req.params.id);
if (!recipe) {
return res.status(404).json({ error: 'Recipe not found' });
}
res.json(recipe);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create new recipe
router.post('/', async (req, res) => {
try {
const recipe = new Recipe(req.body);
await recipe.save();
res.status(201).json(recipe);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Update recipe
router.put('/:id', async (req, res) => {
try {
const recipe = await Recipe.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!recipe) {
return res.status(404).json({ error: 'Recipe not found' });
}
res.json(recipe);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Delete recipe
router.delete('/:id', async (req, res) => {
try {
const recipe = await Recipe.findByIdAndDelete(req.params.id);
if (!recipe) {
return res.status(404).json({ error: 'Recipe not found' });
}
res.json({ message: 'Recipe deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,219 @@
const express = require('express');
const router = express.Router();
const UserSelection = require('../models/UserSelection');
const Recipe = require('../models/Recipe');
const jwt = require('jsonwebtoken');
// Middleware to authenticate JWT token
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.userId = decoded.userId;
next();
});
}
// Helper function to aggregate ingredients
async function aggregateIngredients(selectedRecipes) {
const aggregated = {};
for (const selection of selectedRecipes) {
const recipe = await Recipe.findById(selection.recipeId);
if (!recipe) continue;
for (const ingredient of recipe.ingredients) {
const key = `${ingredient.name}_${ingredient.unit}`;
if (!aggregated[key]) {
aggregated[key] = {
name: ingredient.name,
totalAmount: 0,
unit: ingredient.unit,
recipes: []
};
}
const totalAmount = ingredient.amount * selection.quantity;
aggregated[key].totalAmount += totalAmount;
aggregated[key].recipes.push({
recipeId: recipe._id,
recipeTitle: recipe.title,
amount: ingredient.amount,
quantity: selection.quantity
});
}
}
return Object.values(aggregated);
}
// Get user's selections
router.get('/', authenticateToken, async (req, res) => {
try {
let userSelection = await UserSelection.findOne({ userId: req.userId })
.populate('selectedRecipes.recipeId', 'title description imageUrl category');
if (!userSelection) {
userSelection = new UserSelection({
userId: req.userId,
selectedRecipes: [],
aggregatedIngredients: []
});
await userSelection.save();
}
res.json(userSelection);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Add recipe to user's selection
router.post('/add', authenticateToken, async (req, res) => {
try {
const { recipeId, quantity = 1 } = req.body;
// Verify recipe exists
const recipe = await Recipe.findById(recipeId);
if (!recipe) {
return res.status(404).json({ error: 'Recipe not found' });
}
let userSelection = await UserSelection.findOne({ userId: req.userId });
if (!userSelection) {
userSelection = new UserSelection({
userId: req.userId,
selectedRecipes: [],
aggregatedIngredients: []
});
}
// Check if recipe is already selected
const existingIndex = userSelection.selectedRecipes.findIndex(
item => item.recipeId.toString() === recipeId
);
if (existingIndex >= 0) {
// Update quantity if recipe already exists
userSelection.selectedRecipes[existingIndex].quantity += quantity;
} else {
// Add new recipe selection
userSelection.selectedRecipes.push({
recipeId,
quantity,
addedAt: new Date()
});
}
// Recalculate aggregated ingredients
userSelection.aggregatedIngredients = await aggregateIngredients(userSelection.selectedRecipes);
await userSelection.save();
// Populate recipe details for response
await userSelection.populate('selectedRecipes.recipeId', 'title description imageUrl category');
res.json(userSelection);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update recipe quantity in selection
router.put('/update', authenticateToken, async (req, res) => {
try {
const { recipeId, quantity } = req.body;
if (quantity < 0) {
return res.status(400).json({ error: 'Quantity must be positive' });
}
const userSelection = await UserSelection.findOne({ userId: req.userId });
if (!userSelection) {
return res.status(404).json({ error: 'No selections found' });
}
const recipeIndex = userSelection.selectedRecipes.findIndex(
item => item.recipeId.toString() === recipeId
);
if (recipeIndex === -1) {
return res.status(404).json({ error: 'Recipe not found in selections' });
}
if (quantity === 0) {
// Remove recipe if quantity is 0
userSelection.selectedRecipes.splice(recipeIndex, 1);
} else {
// Update quantity
userSelection.selectedRecipes[recipeIndex].quantity = quantity;
}
// Recalculate aggregated ingredients
userSelection.aggregatedIngredients = await aggregateIngredients(userSelection.selectedRecipes);
await userSelection.save();
await userSelection.populate('selectedRecipes.recipeId', 'title description imageUrl category');
res.json(userSelection);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Remove recipe from selection
router.delete('/remove/:recipeId', authenticateToken, async (req, res) => {
try {
const { recipeId } = req.params;
const userSelection = await UserSelection.findOne({ userId: req.userId });
if (!userSelection) {
return res.status(404).json({ error: 'No selections found' });
}
userSelection.selectedRecipes = userSelection.selectedRecipes.filter(
item => item.recipeId.toString() !== recipeId
);
// Recalculate aggregated ingredients
userSelection.aggregatedIngredients = await aggregateIngredients(userSelection.selectedRecipes);
await userSelection.save();
await userSelection.populate('selectedRecipes.recipeId', 'title description imageUrl category');
res.json(userSelection);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Clear all selections
router.delete('/clear', authenticateToken, async (req, res) => {
try {
const userSelection = await UserSelection.findOne({ userId: req.userId });
if (!userSelection) {
return res.status(404).json({ error: 'No selections found' });
}
userSelection.selectedRecipes = [];
userSelection.aggregatedIngredients = [];
await userSelection.save();
res.json(userSelection);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

115
backend/routes/users.js Normal file
View File

@@ -0,0 +1,115 @@
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// Register new user
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
error: 'User with this email or username already exists'
});
}
const user = new User({ username, email, password });
await user.save();
// Generate JWT token
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
message: 'User created successfully',
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Login user
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user by email
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT token
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
message: 'Login successful',
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get user profile (protected route)
router.get('/profile', authenticateToken, async (req, res) => {
try {
const user = await User.findById(req.userId).select('-password');
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Middleware to authenticate JWT token
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.userId = decoded.userId;
next();
});
}
module.exports = router;

177
backend/seedData.js Normal file
View File

@@ -0,0 +1,177 @@
const mongoose = require('mongoose');
const Recipe = require('./models/Recipe');
require('dotenv').config();
const sampleRecipes = [
{
title: "Classic Spaghetti Carbonara",
description: "A traditional Italian pasta dish with eggs, cheese, and pancetta",
ingredients: [
{ name: "spaghetti", amount: 400, unit: "g" },
{ name: "pancetta", amount: 150, unit: "g" },
{ name: "eggs", amount: 3, unit: "whole" },
{ name: "parmesan cheese", amount: 100, unit: "g" },
{ name: "black pepper", amount: 1, unit: "tsp" },
{ name: "salt", amount: 1, unit: "tsp" }
],
instructions: [
{ step: 1, description: "Cook spaghetti in salted boiling water until al dente" },
{ step: 2, description: "Fry pancetta until crispy" },
{ step: 3, description: "Beat eggs with grated parmesan and black pepper" },
{ step: 4, description: "Drain pasta and mix with pancetta" },
{ step: 5, description: "Remove from heat and quickly stir in egg mixture" },
{ step: 6, description: "Serve immediately with extra parmesan" }
],
servings: 4,
prepTime: 10,
cookTime: 15,
category: "dinner",
difficulty: "medium",
imageUrl: "https://images.unsplash.com/photo-1621996346565-e3dbc353d2e5?w=500"
},
{
title: "Chocolate Chip Cookies",
description: "Soft and chewy homemade chocolate chip cookies",
ingredients: [
{ name: "all-purpose flour", amount: 2.25, unit: "cups" },
{ name: "butter", amount: 1, unit: "cup" },
{ name: "brown sugar", amount: 0.75, unit: "cup" },
{ name: "white sugar", amount: 0.75, unit: "cup" },
{ name: "eggs", amount: 2, unit: "whole" },
{ name: "vanilla extract", amount: 2, unit: "tsp" },
{ name: "baking soda", amount: 1, unit: "tsp" },
{ name: "salt", amount: 1, unit: "tsp" },
{ name: "chocolate chips", amount: 2, unit: "cups" }
],
instructions: [
{ step: 1, description: "Preheat oven to 375°F (190°C)" },
{ step: 2, description: "Cream butter and sugars together" },
{ step: 3, description: "Beat in eggs and vanilla" },
{ step: 4, description: "Mix in flour, baking soda, and salt" },
{ step: 5, description: "Stir in chocolate chips" },
{ step: 6, description: "Drop spoonfuls on baking sheet" },
{ step: 7, description: "Bake for 9-11 minutes until golden brown" }
],
servings: 24,
prepTime: 15,
cookTime: 11,
category: "dessert",
difficulty: "easy",
imageUrl: "https://images.unsplash.com/photo-1499636136210-6f4ee915583e?w=500"
},
{
title: "Caesar Salad",
description: "Fresh romaine lettuce with classic Caesar dressing and croutons",
ingredients: [
{ name: "romaine lettuce", amount: 2, unit: "heads" },
{ name: "parmesan cheese", amount: 0.5, unit: "cup" },
{ name: "croutons", amount: 1, unit: "cup" },
{ name: "mayonnaise", amount: 0.5, unit: "cup" },
{ name: "lemon juice", amount: 2, unit: "tbsp" },
{ name: "garlic", amount: 2, unit: "cloves" },
{ name: "anchovy paste", amount: 1, unit: "tsp" },
{ name: "worcestershire sauce", amount: 1, unit: "tsp" },
{ name: "black pepper", amount: 0.5, unit: "tsp" }
],
instructions: [
{ step: 1, description: "Wash and chop romaine lettuce" },
{ step: 2, description: "Make dressing by mixing mayo, lemon juice, minced garlic, anchovy paste, and worcestershire" },
{ step: 3, description: "Toss lettuce with dressing" },
{ step: 4, description: "Top with parmesan cheese and croutons" },
{ step: 5, description: "Season with black pepper and serve" }
],
servings: 4,
prepTime: 15,
cookTime: 0,
category: "lunch",
difficulty: "easy",
imageUrl: "https://images.unsplash.com/photo-1546793665-c74683f339c1?w=500"
},
{
title: "Pancakes",
description: "Fluffy breakfast pancakes perfect for weekend mornings",
ingredients: [
{ name: "all-purpose flour", amount: 1.5, unit: "cups" },
{ name: "sugar", amount: 3, unit: "tbsp" },
{ name: "baking powder", amount: 1, unit: "tbsp" },
{ name: "salt", amount: 0.5, unit: "tsp" },
{ name: "milk", amount: 1.25, unit: "cups" },
{ name: "egg", amount: 1, unit: "whole" },
{ name: "butter", amount: 3, unit: "tbsp" },
{ name: "vanilla extract", amount: 1, unit: "tsp" }
],
instructions: [
{ step: 1, description: "Mix dry ingredients in a large bowl" },
{ step: 2, description: "Whisk together milk, egg, melted butter, and vanilla" },
{ step: 3, description: "Pour wet ingredients into dry ingredients and stir until just combined" },
{ step: 4, description: "Heat griddle or large skillet over medium heat" },
{ step: 5, description: "Pour 1/4 cup batter for each pancake" },
{ step: 6, description: "Cook until bubbles form on surface, then flip" },
{ step: 7, description: "Cook until golden brown on both sides" }
],
servings: 4,
prepTime: 10,
cookTime: 15,
category: "breakfast",
difficulty: "easy",
imageUrl: "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7445?w=500"
},
{
title: "Beef Stir Fry",
description: "Quick and healthy beef stir fry with vegetables",
ingredients: [
{ name: "beef sirloin", amount: 1, unit: "lb" },
{ name: "broccoli", amount: 2, unit: "cups" },
{ name: "bell peppers", amount: 2, unit: "whole" },
{ name: "carrots", amount: 2, unit: "whole" },
{ name: "soy sauce", amount: 3, unit: "tbsp" },
{ name: "garlic", amount: 3, unit: "cloves" },
{ name: "ginger", amount: 1, unit: "tbsp" },
{ name: "vegetable oil", amount: 2, unit: "tbsp" },
{ name: "cornstarch", amount: 1, unit: "tbsp" },
{ name: "rice", amount: 2, unit: "cups" }
],
instructions: [
{ step: 1, description: "Cut beef into thin strips and marinate with soy sauce and cornstarch" },
{ step: 2, description: "Prepare vegetables by cutting into bite-sized pieces" },
{ step: 3, description: "Heat oil in wok or large skillet over high heat" },
{ step: 4, description: "Stir-fry beef until browned, remove from pan" },
{ step: 5, description: "Stir-fry vegetables until crisp-tender" },
{ step: 6, description: "Return beef to pan, add garlic and ginger" },
{ step: 7, description: "Stir-fry for 2 more minutes and serve over rice" }
],
servings: 4,
prepTime: 20,
cookTime: 15,
category: "dinner",
difficulty: "medium",
imageUrl: "https://images.unsplash.com/photo-1603133872878-684f208fb84b?w=500"
}
];
async function seedDatabase() {
try {
// Connect to MongoDB
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/recipe-management');
console.log('Connected to MongoDB');
// Clear existing recipes
await Recipe.deleteMany({});
console.log('Cleared existing recipes');
// Insert sample recipes
await Recipe.insertMany(sampleRecipes);
console.log('Sample recipes inserted successfully');
console.log(`Inserted ${sampleRecipes.length} recipes`);
// Close connection
await mongoose.connection.close();
console.log('Database connection closed');
} catch (error) {
console.error('Error seeding database:', error);
process.exit(1);
}
}
seedDatabase();

37
backend/server.js Normal file
View File

@@ -0,0 +1,37 @@
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// MongoDB connection
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/recipe-management';
mongoose.connect(MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', () => {
console.log('Connected to MongoDB');
});
// Routes
app.use('/api/recipes', require('./routes/recipes'));
app.use('/api/users', require('./routes/users'));
app.use('/api/selections', require('./routes/selections'));
app.get('/', (req, res) => {
res.json({ message: 'Recipe Management API is running!' });
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});