Vibed it... :(
This commit is contained in:
64
backend/models/Recipe.js
Normal file
64
backend/models/Recipe.js
Normal 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
49
backend/models/User.js
Normal 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);
|
||||
55
backend/models/UserSelection.js
Normal file
55
backend/models/UserSelection.js
Normal 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
1656
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
backend/package.json
Normal file
25
backend/package.json
Normal 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
81
backend/routes/recipes.js
Normal 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;
|
||||
219
backend/routes/selections.js
Normal file
219
backend/routes/selections.js
Normal 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
115
backend/routes/users.js
Normal 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
177
backend/seedData.js
Normal 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
37
backend/server.js
Normal 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user