'add simple logging'
This commit is contained in:
parent
5dc89497c6
commit
6169274fe1
9 changed files with 140 additions and 70 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@ postgres/db
|
||||||
.env
|
.env
|
||||||
todo
|
todo
|
||||||
sqldumps/
|
sqldumps/
|
||||||
|
logs/
|
||||||
|
|
|
@ -78,7 +78,7 @@ exports.deleteRecipe = async (req, res) => {
|
||||||
const id = parseInt(req.body.id, 10);
|
const id = parseInt(req.body.id, 10);
|
||||||
try {
|
try {
|
||||||
await model.deleteRecipe(id);
|
await model.deleteRecipe(id);
|
||||||
res.status(204).send();
|
res.json({ success: "true" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
msg: "Failed to delete recipe",
|
msg: "Failed to delete recipe",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
const { PrismaClient } = require("@prisma/client");
|
const { PrismaClient } = require("@prisma/client");
|
||||||
|
const Logger = require("../utils/logger.js");
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
class recipeModel {
|
class recipeModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -21,6 +23,7 @@ class recipeModel {
|
||||||
include: { recipeSteps: true, recipeIngredients: true },
|
include: { recipeSteps: true, recipeIngredients: true },
|
||||||
});
|
});
|
||||||
if (!recipe) {
|
if (!recipe) {
|
||||||
|
logger.warn(`recipe with id ${id} cannot be found`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const data = {
|
const data = {
|
||||||
|
@ -41,7 +44,8 @@ class recipeModel {
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error finding recipe:", err);
|
console.log("Error finding recipe:", err);
|
||||||
|
logger.error("error finding recipe", err);
|
||||||
throw new Error(err.message);
|
throw new Error(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,9 +81,14 @@ class recipeModel {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.info("new recipe created", {
|
||||||
|
id: createdRecipe.id,
|
||||||
|
name: createdRecipe.name,
|
||||||
|
});
|
||||||
return createdRecipe;
|
return createdRecipe;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
logger.error("error creating recipe", err);
|
||||||
throw new Error("Failed to add recipe");
|
throw new Error("Failed to add recipe");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,6 +102,7 @@ class recipeModel {
|
||||||
return { message: "stars updated" };
|
return { message: "stars updated" };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error updating stars:", err);
|
console.error("Error updating stars:", err);
|
||||||
|
logger.error("error setting stars", err);
|
||||||
throw new Error(err.message);
|
throw new Error(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,18 +110,23 @@ class recipeModel {
|
||||||
async deleteRecipe(id) {
|
async deleteRecipe(id) {
|
||||||
try {
|
try {
|
||||||
await this.prisma.recipe_ingredients.deleteMany({
|
await this.prisma.recipe_ingredients.deleteMany({
|
||||||
where: { recipe_id: id }, // Ensure you have the right foreign key relation
|
where: { recipe_id: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.recipe_steps.deleteMany({
|
await this.prisma.recipe_steps.deleteMany({
|
||||||
where: { recipe_id: id }, // Ensure you have the right foreign key relation
|
where: { recipe_id: id },
|
||||||
});
|
});
|
||||||
const deletedRecipe = await this.prisma.recipes.delete({
|
const deletedRecipe = await this.prisma.recipes.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
logger.info("recipe deleted", {
|
||||||
|
id: deletedRecipe.id,
|
||||||
|
name: deletedRecipe.name,
|
||||||
|
});
|
||||||
return { message: "Recipe deleted successfully" };
|
return { message: "Recipe deleted successfully" };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error deleting recipe:", err);
|
console.error("Error deleting recipe:", err);
|
||||||
|
logger.error("error deleting recipe", err);
|
||||||
throw new Error(err.message);
|
throw new Error(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
backend/src/utils/logger.js
Normal file
33
backend/src/utils/logger.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
constructor(filePath) {
|
||||||
|
this.filePath = "/logs/app.log";
|
||||||
|
}
|
||||||
|
|
||||||
|
log(level, message, params) {
|
||||||
|
const logEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
params: params,
|
||||||
|
};
|
||||||
|
fs.appendFile(this.filePath, JSON.stringify(logEntry) + "\n", (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message, params = {}) {
|
||||||
|
this.log("info", message, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message, params = {}) {
|
||||||
|
this.log("warn", message, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message, params = {}) {
|
||||||
|
this.log("error", message, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Logger;
|
|
@ -1 +0,0 @@
|
||||||
// todo
|
|
|
@ -24,6 +24,7 @@ services:
|
||||||
- "${BACKEND_PORT}:3000"
|
- "${BACKEND_PORT}:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/usr/src/app
|
- ./backend:/usr/src/app
|
||||||
|
- ./logs:/logs
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV}
|
- NODE_ENV=${NODE_ENV}
|
||||||
- DB_USER=${DB_USER}
|
- DB_USER=${DB_USER}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { getRecipeIngredients } from "../services/frontendApi.js";
|
import { getRecipeIngredients } from "../services/frontendApi.js";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { type Ingredient } from "../types/Recipe.ts"
|
|
||||||
|
|
||||||
function RecipeIngredients() {
|
function RecipeIngredients() {
|
||||||
|
|
||||||
const [recipeIngredients, setRecipeIngredients] = useState<Ingredient[]>([]);
|
const [recipeIngredients, setRecipeIngredients] = useState<Ingredient[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
@ -13,36 +11,32 @@ function RecipeIngredients() {
|
||||||
try {
|
try {
|
||||||
const recipeIngredients = await getRecipeIngredients();
|
const recipeIngredients = await getRecipeIngredients();
|
||||||
setRecipeIngredients(recipeIngredients);
|
setRecipeIngredients(recipeIngredients);
|
||||||
console.log(recipeIngredients)
|
console.log(recipeIngredients);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
setError("Failed to load recipe ingredients...");
|
setError("Failed to load recipe ingredients...");
|
||||||
console.log(error)
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadRecipeIngredients();
|
loadRecipeIngredients();
|
||||||
}, []);
|
}, []);
|
||||||
console.log(recipeIngredients)
|
console.log(recipeIngredients);
|
||||||
return (
|
return (
|
||||||
// should this be a string[]? only if we are only returning raw. otherwise i will need to type and return the ingredient object. This template shoudl work for steps though, so maybe setting that up is a good first step
|
<div className="page-outer">
|
||||||
<div className='page-outer'>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">Loading...</div>
|
<div className="loading">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="recipe-outer bg-amber-100 p-4 md:p-8 lg:p-12">
|
<div className="recipe-outer bg-amber-100 p-4 md:p-8 lg:p-12">
|
||||||
<div className="recipes-grid grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
<div className="recipes-grid grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
||||||
{recipeIngredients.map(ing => (
|
{recipeIngredients.map((ing) => (
|
||||||
<li key={ing.id}>
|
<li key={ing.id}>{ing.raw}</li>
|
||||||
{ing.raw}
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
export default RecipeIngredients
|
export default RecipeIngredients;
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { getRecipeById, deleteRecipe, setDBStars } from "../services/frontendApi.js";
|
import {
|
||||||
import { type Recipe, type Ingredient } from "../types/Recipe"
|
getRecipeById,
|
||||||
import Modal from '../components/Modal.tsx'
|
deleteRecipe,
|
||||||
import DemoModal from '../components/DemoModal.tsx'
|
setDBStars,
|
||||||
import StarRating from "../components/StarRating.tsx"
|
} from "../services/frontendApi.js";
|
||||||
import TimeDisplay from '../components/TimeDisplay.tsx'
|
import { type Recipe } from "../types/Recipe";
|
||||||
|
import Modal from "../components/Modal.tsx";
|
||||||
|
import DemoModal from "../components/DemoModal.tsx";
|
||||||
|
import StarRating from "../components/StarRating.tsx";
|
||||||
|
import TimeDisplay from "../components/TimeDisplay.tsx";
|
||||||
|
|
||||||
function RecipePage() {
|
function RecipePage() {
|
||||||
const [recipe, setRecipe] = useState<Recipe>({
|
const [recipe, setRecipe] = useState<Recipe>({
|
||||||
details: {},
|
details: {},
|
||||||
ingredients: [],
|
ingredients: [],
|
||||||
steps: []
|
steps: [],
|
||||||
});
|
});
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
@ -20,16 +24,21 @@ function RecipePage() {
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||||
const [showDemoModal, setShowDemoModal] = useState(false);
|
const [showDemoModal, setShowDemoModal] = useState(false);
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const isWebSource = recipe && recipe.details && recipe.details.author
|
const isWebSource =
|
||||||
? /http|com/.test(recipe.details.author) //etc
|
recipe && recipe.details && recipe.details.author
|
||||||
: false;
|
? /http|com/.test(recipe.details.author) //etc
|
||||||
|
: false;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const openModal = () => { setShowConfirmModal(true) };
|
const openModal = () => {
|
||||||
const closeModal = () => { setShowConfirmModal(false) };
|
setShowConfirmModal(true);
|
||||||
|
};
|
||||||
|
const closeModal = () => {
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (process.env.NODE_ENV === 'demo') {
|
if (process.env.NODE_ENV === "demo") {
|
||||||
closeModal();
|
closeModal();
|
||||||
setShowDemoModal(true);
|
setShowDemoModal(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -43,13 +52,13 @@ function RecipePage() {
|
||||||
try {
|
try {
|
||||||
const recipe = await getRecipeById(id);
|
const recipe = await getRecipeById(id);
|
||||||
if (!recipe.details) {
|
if (!recipe.details) {
|
||||||
setError("Sorry, this recipe no longer exists")
|
setError("Sorry, this recipe no longer exists");
|
||||||
} else {
|
} else {
|
||||||
setRecipe(recipe);
|
setRecipe(recipe);
|
||||||
setStars(recipe.details?.stars ?? 0)
|
setStars(recipe.details?.stars ?? 0);
|
||||||
setInitialStars(recipe.details?.stars ?? 0);
|
setInitialStars(recipe.details?.stars ?? 0);
|
||||||
if (process.env.NODE_ENV === 'dev') {
|
if (process.env.NODE_ENV === "dev") {
|
||||||
console.log(recipe)
|
console.log(recipe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -77,38 +86,56 @@ function RecipePage() {
|
||||||
const handleDelete = async (id: number | void) => {
|
const handleDelete = async (id: number | void) => {
|
||||||
try {
|
try {
|
||||||
await deleteRecipe(id);
|
await deleteRecipe(id);
|
||||||
navigate('/')
|
navigate("/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting recipe:", error);
|
console.error("Error deleting recipe:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="recipe page-outer">
|
<div className="recipe page-outer">
|
||||||
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">Loading...</div>
|
<div className="loading">Loading...</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="error-message text-lg">{error}</div>
|
<div className="error-message text-lg">{error}</div>
|
||||||
<div className="m-2">
|
<div className="m-2">
|
||||||
<Link to="/" className="ar-button bg-amber-600 text-white py-2 px-4 rounded hover:bg-amber-700">
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="ar-button bg-amber-600 text-white py-2 px-4 rounded hover:bg-amber-700"
|
||||||
|
>
|
||||||
Return to Cookbook
|
Return to Cookbook
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
<div className="border-b-2 border-amber-300 pb-4">
|
<div className="border-b-2 border-amber-300 pb-4">
|
||||||
<div className="recipe-card">
|
<div className="recipe-card">
|
||||||
<div className="flex relative justify-between">
|
<div className="flex relative justify-between">
|
||||||
<button onClick={() => { }} className="invisible ar-button py-1 px-1 rounded self-start">🗑️</button>
|
<button
|
||||||
<h3 className="text-center max-w-lg px-4 text-2xl lg:text-3xl font-bold text-amber-900">{recipe.details.name}</h3>
|
onClick={() => {}}
|
||||||
<button onClick={openModal} className="ar-button bg-amber-500 text-white py-1 px-1 rounded hover:bg-amber-600 self-start">🗑️</button>
|
className="invisible ar-button py-1 px-1 rounded self-start"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
<h3 className="text-center max-w-lg px-4 text-2xl lg:text-3xl font-bold text-amber-900">
|
||||||
|
{recipe.details.name}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={openModal}
|
||||||
|
className="ar-button bg-amber-500 text-white py-1 px-1 rounded hover:bg-amber-600 self-start"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<p className="text-amber-700 italic text-lg">{recipe.details.cuisine}</p>
|
<p className="text-amber-700 italic text-lg">
|
||||||
<p>prep: <TimeDisplay minutes={recipe.details.prep_minutes ?? 0} /> | cook: <TimeDisplay minutes={recipe.details.cook_minutes ?? 0} /></p>
|
{recipe.details.cuisine}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
prep: <TimeDisplay minutes={recipe.details.prep_minutes ?? 0} />{" "}
|
||||||
|
| cook:{" "}
|
||||||
|
<TimeDisplay minutes={recipe.details.cook_minutes ?? 0} />
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -121,7 +148,7 @@ function RecipePage() {
|
||||||
{recipe.ingredients.map((ingredient: Ingredient, index) => (
|
{recipe.ingredients.map((ingredient: Ingredient, index) => (
|
||||||
<li key={index} className="text-gray-700 flex items-start">
|
<li key={index} className="text-gray-700 flex items-start">
|
||||||
<span className="w-1.5 h-1.5 bg-amber-400 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
<span className="w-1.5 h-1.5 bg-amber-400 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||||
<span className="font-medium text-left">{ingredient.raw}</span>
|
<span className="font-medium text-left">{ingredient}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -132,14 +159,20 @@ function RecipePage() {
|
||||||
Instructions:
|
Instructions:
|
||||||
</h4>
|
</h4>
|
||||||
<ol className="space-y-3">
|
<ol className="space-y-3">
|
||||||
{recipe.steps && Object.keys(recipe.steps || {}).map((stepNumber) => (
|
{recipe.steps &&
|
||||||
<li key={stepNumber} className="text-gray-700 flex items-start">
|
Object.keys(recipe.steps || {}).map((stepNumber) => (
|
||||||
<span className="bg-amber-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold mr-3 mt-0.5 flex-shrink-0">
|
<li
|
||||||
{recipe.steps[parseInt(stepNumber)].step_number}
|
key={stepNumber}
|
||||||
</span>
|
className="text-gray-700 flex items-start"
|
||||||
<span className="leading-relaxed text-left">{recipe.steps[parseInt(stepNumber)].instruction}</span>
|
>
|
||||||
</li>
|
<span className="bg-amber-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold mr-3 mt-0.5 flex-shrink-0">
|
||||||
))}
|
{recipe.steps[parseInt(stepNumber)].step_number}
|
||||||
|
</span>
|
||||||
|
<span className="leading-relaxed text-left">
|
||||||
|
{recipe.steps[parseInt(stepNumber)].instruction}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -152,13 +185,15 @@ function RecipePage() {
|
||||||
<span>From the kitchen of {recipe.details.author}</span>
|
<span>From the kitchen of {recipe.details.author}</span>
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
<StarRating rating={stars} onRatingChange={(newRating: number) => setStars(newRating)} />
|
<StarRating
|
||||||
|
rating={stars}
|
||||||
|
onRatingChange={(newRating: number) => setStars(newRating)}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showConfirmModal}
|
isOpen={showConfirmModal}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
|
@ -173,7 +208,7 @@ function RecipePage() {
|
||||||
closeModal={() => setShowDemoModal(false)}
|
closeModal={() => setShowDemoModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div >
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,6 @@ interface Step {
|
||||||
instruction: string;
|
instruction: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Ingredient {
|
|
||||||
id?: number;
|
|
||||||
name?: string;
|
|
||||||
quantity?: number;
|
|
||||||
unit?: string;
|
|
||||||
raw?: string;
|
|
||||||
}
|
|
||||||
interface Recipe {
|
interface Recipe {
|
||||||
details: {
|
details: {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
@ -20,8 +13,8 @@ interface Recipe {
|
||||||
cuisine?: string;
|
cuisine?: string;
|
||||||
prep_minutes?: number;
|
prep_minutes?: number;
|
||||||
cook_minutes?: number;
|
cook_minutes?: number;
|
||||||
},
|
};
|
||||||
ingredients: Ingredient[],
|
ingredients: string[];
|
||||||
steps: Step[];
|
steps: Step[];
|
||||||
}
|
}
|
||||||
// smaller Recipe type returned by backend at /recipes for all
|
// smaller Recipe type returned by backend at /recipes for all
|
||||||
|
@ -34,5 +27,4 @@ interface RecipeSmall {
|
||||||
prep_minutes: number;
|
prep_minutes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { Recipe, Step, RecipeSmall };
|
||||||
export type { Recipe, Ingredient, Step, RecipeSmall }
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue