refactor: migrate backend to NestJS
All checks were successful
/ build (push) Successful in 1m5s

This commit is contained in:
fred 2026-02-11 09:36:55 -08:00
parent 7258d283ed
commit 31f5bdc254
42 changed files with 21523 additions and 1040 deletions

View file

@ -2196,9 +2196,9 @@
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {

View file

@ -27,14 +27,14 @@ const RecipeForm: React.FC<RecipeFormProps> = ({ onSubmit, initialData }) => {
useEffect(() => {
if (initialData) {
setRecipeName(initialData.details.name || "");
setRecipeCuisine(initialData.details.cuisine || "");
setAuthor(initialData.details.author || "");
setStars(initialData.details.stars || 0);
setPrepMinutes(initialData.details.prep_minutes || 5);
setCookMinutes(initialData.details.cook_minutes || 5);
setIngredients(initialData.ingredients);
setSteps(initialData.steps);
setRecipeName(initialData.name || "");
setRecipeCuisine(initialData.cuisine || "");
setAuthor(initialData.author || "");
setStars(initialData.stars || 0);
setPrepMinutes(initialData.prep_minutes || 5);
setCookMinutes(initialData.cook_minutes || 5);
setIngredients(initialData.recipeIngredients);
setSteps(initialData.recipeSteps);
}
}, [initialData]);
@ -53,19 +53,17 @@ const RecipeForm: React.FC<RecipeFormProps> = ({ onSubmit, initialData }) => {
}
const recipeData = {
details: {
name: recipeName,
cuisine: recipeCuisine.toLowerCase(),
author: author,
prep_minutes: prepMinutes,
cook_minutes: cookMinutes,
stars: stars,
},
ingredients: ingredients,
steps: steps.map((step) => ({
name: recipeName,
cuisine: recipeCuisine.toLowerCase(),
author: author,
prep_minutes: prepMinutes,
cook_minutes: cookMinutes,
stars: stars,
recipeIngredients: ingredients.map((ing) => ing.trim()),
recipeSteps: steps.map((step) => ({
id: undefined,
step_number: step.step_number,
instruction: step.instruction,
instruction: step.instruction.trim(),
})),
};

View file

@ -1,23 +1,58 @@
function About() {
return (
<div className="about page-outer">
<div>
<h2 className="text-xl text-[var(--color-secondaryTextDark)]">This app uses the following components:</h2>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Frontend:</h2>
<ul><li>React</li><li>TypeScript</li></ul>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Backend:</h2>
<ul><li>Node.js & Express</li><li>PostgreSQL</li><li>Prisma</li></ul>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Containerization:</h2>
<ul><li>Docker</li></ul>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Styling/UI:</h2>
<ul><li>Tailwind CSS</li></ul>
<p className="mt-4 text-[var(--color-secondaryTextDark)]">More about me <a className="text-[var(--color-textLink)]" target="_blank" href="https://fredzernia.com">here</a> |
Code for this app <a className="text-[var(--color-textLink)]" target="_blank" href="https://forgejo.fredzernia.com/fred/recipe_app">here</a></p>
<h2 className="text-xl text-[var(--color-secondaryTextDark)]">
This app uses the following components:
</h2>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">
Frontend:
</h2>
<ul>
<li>React</li>
<li>TypeScript</li>
</ul>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">
Backend:
</h2>
<ul>
<li>NestJS</li>
<li>PostgreSQL</li>
<li>Prisma</li>
</ul>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">
Containerization:
</h2>
<ul>
<li>Docker</li>
</ul>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">
Styling/UI:
</h2>
<ul>
<li>Tailwind CSS</li>
</ul>
<p className="mt-4 text-[var(--color-secondaryTextDark)]">
More about me{" "}
<a
className="text-[var(--color-textLink)]"
target="_blank"
href="https://fredzernia.com"
>
here
</a>{" "}
| Code for this app{" "}
<a
className="text-[var(--color-textLink)]"
target="_blank"
href="https://forgejo.fredzernia.com/fred/recipe_app"
>
here
</a>
</p>
</div>
</div >
)
</div>
);
}
export default About
export default About;

View file

@ -12,9 +12,8 @@ import TimeDisplay from "../components/TimeDisplay.tsx";
function RecipePage() {
const [recipe, setRecipe] = useState<Recipe>({
details: {},
ingredients: [],
steps: [],
recipeIngredients: [],
recipeSteps: [],
});
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@ -23,8 +22,8 @@ function RecipePage() {
const [showConfirmModal, setShowConfirmModal] = useState(false);
const { id } = useParams();
const isWebSource =
recipe && recipe.details && recipe.details.author
? /http|com/.test(recipe.details.author) //etc
recipe && recipe.author
? /http|com/.test(recipe.author) //etc
: false;
const navigate = useNavigate();
@ -36,7 +35,7 @@ function RecipePage() {
};
const confirmDelete = () => {
handleDelete(recipe.details.id);
handleDelete(recipe.id);
closeModal();
};
@ -44,12 +43,12 @@ function RecipePage() {
const loadRecipe = async () => {
try {
const recipe = await getRecipeById(id);
if (!recipe.details) {
if (!recipe.name) {
setError("Sorry, this recipe no longer exists");
} else {
setRecipe(recipe);
setStars(recipe.details?.stars ?? 0);
setInitialStars(recipe.details?.stars ?? 0);
setStars(recipe.stars ?? 0);
setInitialStars(recipe.stars ?? 0);
if (process.env.NODE_ENV === "dev") {
console.log(recipe);
}
@ -119,11 +118,11 @@ function RecipePage() {
</button>
</div>
<h3 className="text-center max-w-lg px-4 text-2xl lg:text-3xl font-bold text-[var(--color-textDark)]">
{recipe.details.name}
{recipe.name}
</h3>
<div className="modify-buttons">
<button
onClick={() => navigate(`/edit-recipe/${recipe.details.id}`)}
onClick={() => navigate(`/edit-recipe/${recipe.id}`)}
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-1 px-1 rounded hover:bg-[var(--color-buttonBgHover)] mr-2 self-start"
>
🔧
@ -138,12 +137,12 @@ function RecipePage() {
</div>
<div className="mt-1">
<p className="text-[var(--color-textDark)] italic text-lg">
{recipe.details.cuisine}
{recipe.cuisine}
</p>
<p>
prep: <TimeDisplay minutes={recipe.details.prep_minutes ?? 0} />{" "}
prep: <TimeDisplay minutes={recipe.prep_minutes ?? 0} />{" "}
| cook:{" "}
<TimeDisplay minutes={recipe.details.cook_minutes ?? 0} />
<TimeDisplay minutes={recipe.cook_minutes ?? 0} />
</p>
</div>
</div>
@ -154,7 +153,7 @@ function RecipePage() {
Ingredients:
</h4>
<ul className="space-y-2">
{recipe.ingredients.map((ingredient: string, index) => (
{recipe.recipeIngredients.map((ingredient: string, index) => (
<li
key={index}
className="text-[var(--color-secondaryTextDark)] flex items-start"
@ -171,17 +170,17 @@ function RecipePage() {
Instructions:
</h4>
<ol className="space-y-3">
{recipe.steps &&
Object.keys(recipe.steps || {}).map((stepNumber) => (
{recipe.recipeSteps &&
Object.keys(recipe.recipeSteps || {}).map((stepNumber) => (
<li
key={stepNumber}
className="text-[var(--color-secondaryTextDark)] flex items-start"
>
<span className="bg-[var(--color-buttonBg)] text-[var(--color-textLight)] 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.recipeSteps[parseInt(stepNumber)].step_number}
</span>
<span className="leading-relaxed text-left">
{recipe.steps[parseInt(stepNumber)].instruction}
{recipe.recipeSteps[parseInt(stepNumber)].instruction}
</span>
</li>
))}
@ -192,9 +191,9 @@ function RecipePage() {
<div className="border-t-2 border-[var(--color-primaryBorder)] pt-4">
<div className="flex justify-between items-center text-sm text-[var(--color-textDark)]">
{isWebSource ? (
<span>Source: {recipe.details.author}</span>
<span>Source: {recipe.author}</span>
) : (
<span>From the kitchen of {recipe.details.author}</span>
<span>From the kitchen of {recipe.author}</span>
)}
<span>
<StarRating

View file

@ -24,7 +24,7 @@ function RecipeSteps() {
};
loadRecipeSteps();
}, []);
console.log(recipeSteps)
// console.log(recipeSteps)
return (
<div className='page-outer'>
{loading ? (

View file

@ -2,8 +2,8 @@ const baseUrl =
process.env.NODE_ENV !== "dev" ? "/api" : "http://localhost:3000/api";
export const getRecipes = async () => {
console.log(`${baseUrl}/recipes`);
const response = await fetch(`${baseUrl}/recipes`);
// console.log(`${baseUrl}/recipe`);
const response = await fetch(`${baseUrl}/recipe`);
const data = await response.json();
return data;
};
@ -23,30 +23,36 @@ export const getRecipeIngredients = async () => {
export const getRecipeById = async (id) => {
const response = await fetch(`${baseUrl}/recipe/${id}`);
const data = await response.json();
if (!data || !data.details) {
if (!data || !data.name) {
return { details: null };
}
return data;
};
export const addRecipe = async (recipeData) => {
console.log(JSON.stringify(recipeData));
recipeData.recipeIngredients = recipeData.recipeIngredients.map((str) => ({
raw: str,
}));
// console.log(JSON.stringify(recipeData));
// return
const response = await fetch(`${baseUrl}/add-recipe`, {
const response = await fetch(`${baseUrl}/recipe`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(recipeData),
});
const data = await response.json();
console.log(data);
// console.log(data);
return data;
};
export const updateRecipe = async (id, recipeData) => {
console.log("updateRecipe");
console.log(recipeData);
const response = await fetch(`${baseUrl}/update-recipe/${id}`, {
method: "POST",
// console.log("updateRecipe");
// console.log(recipeData);
recipeData.recipeIngredients = recipeData.recipeIngredients.map((str) => ({
raw: str,
}));
const response = await fetch(`${baseUrl}/recipe/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(recipeData),
});
@ -55,22 +61,22 @@ export const updateRecipe = async (id, recipeData) => {
};
export const setDBStars = async (id, stars) => {
console.log(JSON.stringify({ id: id, stars: stars }));
// console.log(JSON.stringify({ id: id, stars: stars }));
// return
const response = await fetch(`${baseUrl}/set-stars`, {
method: "POST",
const response = await fetch(`${baseUrl}/recipe/${id}/stars`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: id, stars: stars }),
});
const data = await response.json();
console.log(data);
// console.log(data);
return data;
};
export const deleteRecipe = async (id) => {
console.log(id);
// console.log(id);
// return
const response = await fetch(`${baseUrl}/delete-recipe`, {
const response = await fetch(`${baseUrl}/recipe/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),

View file

@ -5,17 +5,15 @@ interface Step {
}
interface Recipe {
details: {
id?: number;
name?: string;
author?: string;
stars?: number;
cuisine?: string;
prep_minutes?: number;
cook_minutes?: number;
};
ingredients: string[];
steps: Step[];
id?: number;
name?: string;
author?: string;
stars?: number;
cuisine?: string;
prep_minutes?: number;
cook_minutes?: number;
recipeIngredients: string[];
recipeSteps: Step[];
}
// smaller Recipe type returned by backend at /recipes for all