'add simple logging'

This commit is contained in:
fred 2025-08-14 15:12:28 -07:00
parent 5dc89497c6
commit 6169274fe1
9 changed files with 140 additions and 70 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ postgres/db
.env .env
todo todo
sqldumps/ sqldumps/
logs/

View file

@ -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",

View file

@ -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);
} }
} }

View 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;

View file

@ -1 +0,0 @@
// todo

View file

@ -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}

View file

@ -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;

View file

@ -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 =
recipe && recipe.details && recipe.details.author
? /http|com/.test(recipe.details.author) //etc ? /http|com/.test(recipe.details.author) //etc
: false; : 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,12 +159,18 @@ 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) => (
<li
key={stepNumber}
className="text-gray-700 flex items-start"
>
<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"> <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} {recipe.steps[parseInt(stepNumber)].step_number}
</span> </span>
<span className="leading-relaxed text-left">{recipe.steps[parseInt(stepNumber)].instruction}</span> <span className="leading-relaxed text-left">
{recipe.steps[parseInt(stepNumber)].instruction}
</span>
</li> </li>
))} ))}
</ol> </ol>
@ -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>
); );
} }

View file

@ -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 }