Compare commits
No commits in common. "866922e0f284a5fc780111cff6de85b3493bb168" and "24281a6322666c1162ae9d9ec47fa514f7abf81f" have entirely different histories.
866922e0f2
...
24281a6322
22 changed files with 102 additions and 1576 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,7 +1,6 @@
|
||||||
db/
|
examp_frontend/
|
||||||
|
postgres/db
|
||||||
*/.env
|
*/.env
|
||||||
.env*
|
.env
|
||||||
todo
|
todo
|
||||||
sqldumps/
|
sqldumps/
|
||||||
logs/
|
|
||||||
dist/
|
|
||||||
|
|
938
backend/package-lock.json
generated
938
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,26 +3,19 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon ./src/index.js",
|
"dev": "node backendServer.js",
|
||||||
"production": "node ./src/index.js"
|
"demo": "node backendServer.js",
|
||||||
|
"production": "node backendServer.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.14.0",
|
|
||||||
"@types/node": "^24.2.1",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3"
|
||||||
"prisma": "^6.14.0",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.9.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^3.1.10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
-- CreateSchema
|
|
||||||
CREATE SCHEMA IF NOT EXISTS "public";
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."ingredients" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"name" VARCHAR(255) NOT NULL,
|
|
||||||
"type" VARCHAR(255),
|
|
||||||
"notes" VARCHAR(255),
|
|
||||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "ingredients_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."knex_migrations" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"name" VARCHAR(255),
|
|
||||||
"batch" INTEGER,
|
|
||||||
"migration_time" TIMESTAMPTZ(6),
|
|
||||||
|
|
||||||
CONSTRAINT "knex_migrations_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."knex_migrations_lock" (
|
|
||||||
"index" SERIAL NOT NULL,
|
|
||||||
"is_locked" INTEGER,
|
|
||||||
|
|
||||||
CONSTRAINT "knex_migrations_lock_pkey" PRIMARY KEY ("index")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."recipe_ingredients" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"recipe_id" INTEGER NOT NULL,
|
|
||||||
"ingredient_id" INTEGER,
|
|
||||||
"quantity" VARCHAR(255),
|
|
||||||
"unit" VARCHAR(255),
|
|
||||||
"notes" VARCHAR(255),
|
|
||||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"raw" VARCHAR(255) DEFAULT '',
|
|
||||||
|
|
||||||
CONSTRAINT "recipe_ingredients_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."recipe_steps" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"recipe_id" INTEGER,
|
|
||||||
"step_number" INTEGER,
|
|
||||||
"instruction" VARCHAR(510),
|
|
||||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "recipe_steps_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."recipes" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"name" VARCHAR(255) NOT NULL,
|
|
||||||
"cuisine" VARCHAR(255) NOT NULL,
|
|
||||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"author" VARCHAR(255),
|
|
||||||
"stars" INTEGER,
|
|
||||||
"prep_minutes" INTEGER,
|
|
||||||
"cook_minutes" INTEGER,
|
|
||||||
|
|
||||||
CONSTRAINT "recipes_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "ingredients_name_unique" ON "public"."ingredients"("name");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "recipe_ingredients_ingredient_id_index" ON "public"."recipe_ingredients"("ingredient_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "recipe_ingredients_recipe_id_index" ON "public"."recipe_ingredients"("recipe_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "recipe_steps_recipe_id_index" ON "public"."recipe_steps"("recipe_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "recipe_steps_recipe_id_step_number_unique" ON "public"."recipe_steps"("recipe_id", "step_number");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "recipes_name_unique" ON "public"."recipes"("name");
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the `knex_migrations` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
- You are about to drop the `knex_migrations_lock` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "public"."knex_migrations";
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "public"."knex_migrations_lock";
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the `ingredients` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "public"."recipe_ingredients" ADD COLUMN "recipesId" INTEGER;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "public"."recipe_steps" ADD COLUMN "recipesId" INTEGER;
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "public"."ingredients";
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_recipesId_fkey" FOREIGN KEY ("recipesId") REFERENCES "public"."recipes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."recipe_steps" ADD CONSTRAINT "recipe_steps_recipesId_fkey" FOREIGN KEY ("recipesId") REFERENCES "public"."recipes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
@ -1,11 +0,0 @@
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "public"."recipe_ingredients" DROP CONSTRAINT "recipe_ingredients_recipesId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "public"."recipe_steps" DROP CONSTRAINT "recipe_steps_recipesId_fkey";
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."recipe_steps" ADD CONSTRAINT "recipe_steps_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Please do not edit this file manually
|
|
||||||
# It should be added in your version-control system (e.g., Git)
|
|
||||||
provider = "postgresql"
|
|
|
@ -1,53 +0,0 @@
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
model recipes {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
name String @unique(map: "recipes_name_unique") @db.VarChar(255)
|
|
||||||
cuisine String @db.VarChar(255)
|
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
author String? @db.VarChar(255)
|
|
||||||
stars Int?
|
|
||||||
prep_minutes Int?
|
|
||||||
cook_minutes Int?
|
|
||||||
recipeIngredients recipe_ingredients[]
|
|
||||||
recipeSteps recipe_steps[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model recipe_ingredients {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
recipe_id Int
|
|
||||||
ingredient_id Int?
|
|
||||||
quantity String? @db.VarChar(255)
|
|
||||||
unit String? @db.VarChar(255)
|
|
||||||
notes String? @db.VarChar(255)
|
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
raw String? @default("") @db.VarChar(255)
|
|
||||||
recipes recipes? @relation(fields: [recipe_id], references: [id])
|
|
||||||
recipesId Int?
|
|
||||||
|
|
||||||
@@index([ingredient_id], map: "recipe_ingredients_ingredient_id_index")
|
|
||||||
@@index([recipe_id], map: "recipe_ingredients_recipe_id_index")
|
|
||||||
}
|
|
||||||
|
|
||||||
model recipe_steps {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
recipe_id Int?
|
|
||||||
step_number Int?
|
|
||||||
instruction String? @db.VarChar(510)
|
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
recipes recipes? @relation(fields: [recipe_id], references: [id])
|
|
||||||
recipesId Int?
|
|
||||||
|
|
||||||
@@unique([recipe_id, step_number], map: "recipe_steps_recipe_id_step_number_unique")
|
|
||||||
@@index([recipe_id], map: "recipe_steps_recipe_id_index")
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
const RecipeModel = require("../models/recipeModel");
|
|
||||||
const model = new RecipeModel();
|
|
||||||
|
|
||||||
exports.test = async (req, res) => {
|
|
||||||
console.log("test");
|
|
||||||
res.json({ env: process.env.NODE_ENV });
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getAllRecipes = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const recipes = await model.getAllRecipes();
|
|
||||||
res.json(recipes);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
msg: "Failed to fetch all recipes",
|
|
||||||
source: "recipeController",
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getRecipeById = async (req, res) => {
|
|
||||||
const id = parseInt(req.params.id, 10);
|
|
||||||
try {
|
|
||||||
const recipe = await model.findById(id);
|
|
||||||
if (recipe) {
|
|
||||||
res.json(recipe);
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ msg: "Recipe not found" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
msg: "Failed to fetch recipe",
|
|
||||||
source: "recipeController",
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.addRecipe = async (req, res) => {
|
|
||||||
if (process.env.NODE_ENV === "demo") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const createdRecipe = await model.addRecipe(req.body);
|
|
||||||
res.status(201).json(createdRecipe);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
msg: "Failed to add recipe",
|
|
||||||
source: "recipeController",
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.setStars = async (req, res) => {
|
|
||||||
if (process.env.NODE_ENV === "demo") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = parseInt(req.body.id, 10);
|
|
||||||
const stars = parseInt(req.body.stars, 10);
|
|
||||||
try {
|
|
||||||
const createdRecipe = await model.setStars(id, stars);
|
|
||||||
res.status(202).json(createdRecipe);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
msg: "Failed to set stars",
|
|
||||||
source: "recipeController",
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.deleteRecipe = async (req, res) => {
|
|
||||||
if (process.env.NODE_ENV === "demo") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = parseInt(req.body.id, 10);
|
|
||||||
try {
|
|
||||||
await model.deleteRecipe(id);
|
|
||||||
res.json({ success: "true" });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
msg: "Failed to delete recipe",
|
|
||||||
source: "recipeController",
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
const express = require("express");
|
|
||||||
const cors = require("cors");
|
|
||||||
const { PrismaClient } = require("@prisma/client");
|
|
||||||
const appRoutes = require("./routes/appRoutes");
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
function setupMiddleware(app) {
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
|
||||||
app.use("/api", appRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMiddleware(app);
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
async function startServer() {
|
|
||||||
try {
|
|
||||||
app.listen(port);
|
|
||||||
console.log(`Server is running on http://localhost:${port}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error starting the server:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("SIGINT", async () => {
|
|
||||||
try {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
console.log("Prisma client disconnected");
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error disconnecting Prisma client:", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
startServer();
|
|
||||||
|
|
||||||
module.exports = { app, prisma };
|
|
|
@ -1,134 +0,0 @@
|
||||||
const { PrismaClient } = require("@prisma/client");
|
|
||||||
const Logger = require("../utils/logger.js");
|
|
||||||
const logger = new Logger();
|
|
||||||
|
|
||||||
class recipeModel {
|
|
||||||
constructor() {
|
|
||||||
this.prisma = new PrismaClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllRecipes() {
|
|
||||||
try {
|
|
||||||
return await this.prisma.recipes.findMany();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching all recipies:", err);
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(id) {
|
|
||||||
try {
|
|
||||||
const recipe = await this.prisma.recipes.findUnique({
|
|
||||||
where: { id },
|
|
||||||
include: { recipeSteps: true, recipeIngredients: true },
|
|
||||||
});
|
|
||||||
if (!recipe) {
|
|
||||||
logger.warn(`recipe with id ${id} cannot be found`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const data = {
|
|
||||||
details: {
|
|
||||||
id: recipe.id,
|
|
||||||
name: recipe.name,
|
|
||||||
author: recipe.author,
|
|
||||||
cuisine: recipe.cuisine,
|
|
||||||
stars: recipe.stars,
|
|
||||||
prep_minutes: recipe.prep_minutes,
|
|
||||||
cook_minutes: recipe.cook_minutes,
|
|
||||||
},
|
|
||||||
ingredients: recipe.recipeIngredients.map((ing) => ing.raw),
|
|
||||||
steps: recipe.recipeSteps.map((step, idx) => ({
|
|
||||||
step_idx: step.step_number,
|
|
||||||
instruction: step.instruction,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Error finding recipe:", err);
|
|
||||||
logger.error("error finding recipe", err);
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async addRecipe(recipeData) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
author,
|
|
||||||
cuisine,
|
|
||||||
stars,
|
|
||||||
ingredients,
|
|
||||||
steps,
|
|
||||||
prep_minutes,
|
|
||||||
cook_minutes,
|
|
||||||
} = recipeData;
|
|
||||||
try {
|
|
||||||
const createdRecipe = await this.prisma.recipes.create({
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
author,
|
|
||||||
cuisine,
|
|
||||||
prep_minutes,
|
|
||||||
cook_minutes,
|
|
||||||
stars,
|
|
||||||
recipeIngredients: {
|
|
||||||
create: ingredients.map((ing) => ({ raw: ing })),
|
|
||||||
},
|
|
||||||
recipeSteps: {
|
|
||||||
create: Object.keys(steps).map((stepNumber) => ({
|
|
||||||
step_number: parseInt(stepNumber),
|
|
||||||
instruction: steps[stepNumber],
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("new recipe created", {
|
|
||||||
id: createdRecipe.id,
|
|
||||||
name: createdRecipe.name,
|
|
||||||
});
|
|
||||||
return createdRecipe;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
logger.error("error creating recipe", err);
|
|
||||||
throw new Error("Failed to add recipe");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setStars(id, stars) {
|
|
||||||
try {
|
|
||||||
await this.prisma.recipes.update({
|
|
||||||
where: { id },
|
|
||||||
data: { stars },
|
|
||||||
});
|
|
||||||
return { message: "stars updated" };
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error updating stars:", err);
|
|
||||||
logger.error("error setting stars", err);
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteRecipe(id) {
|
|
||||||
try {
|
|
||||||
await this.prisma.recipe_ingredients.deleteMany({
|
|
||||||
where: { recipe_id: id },
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.prisma.recipe_steps.deleteMany({
|
|
||||||
where: { recipe_id: id },
|
|
||||||
});
|
|
||||||
const deletedRecipe = await this.prisma.recipes.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
logger.info("recipe deleted", {
|
|
||||||
id: deletedRecipe.id,
|
|
||||||
name: deletedRecipe.name,
|
|
||||||
});
|
|
||||||
return { message: "Recipe deleted successfully" };
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error deleting recipe:", err);
|
|
||||||
logger.error("error deleting recipe", err);
|
|
||||||
throw new Error(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = recipeModel;
|
|
|
@ -1,18 +0,0 @@
|
||||||
const express = require("express");
|
|
||||||
const recipeController = require("../controllers/recipeController");
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/test", recipeController.test);
|
|
||||||
|
|
||||||
router.get("/recipes", recipeController.getAllRecipes);
|
|
||||||
|
|
||||||
router.get("/recipe/:id", recipeController.getRecipeById);
|
|
||||||
|
|
||||||
router.post("/add-recipe", recipeController.addRecipe);
|
|
||||||
|
|
||||||
router.post("/set-stars", recipeController.setStars);
|
|
||||||
|
|
||||||
router.delete("/delete-recipe", recipeController.deleteRecipe);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,33 +0,0 @@
|
||||||
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,16 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "CommonJS",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src",
|
|
||||||
"scripts"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "${DB_PORT}:5432"
|
- "${DB_PORT}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/var/lib/postgresql/data
|
- ./postgres/db:/var/lib/postgresql/data
|
||||||
backend:
|
backend:
|
||||||
image: recipes_backend
|
image: recipes_backend
|
||||||
container_name: recipes_backend_${ID}
|
container_name: recipes_backend_${ID}
|
||||||
|
@ -24,7 +24,6 @@ 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}
|
||||||
|
@ -41,6 +40,5 @@ services:
|
||||||
- "${FRONTEND_PORT}:80"
|
- "${FRONTEND_PORT}:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/usr/src/app
|
- ./frontend:/usr/src/app
|
||||||
- "$FRONTEND_BUILD_DIR:/usr/src/app/dist"
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV}
|
- NODE_ENV=${NODE_ENV}
|
||||||
|
|
|
@ -10,4 +10,5 @@ COPY . .
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
# CMD ["npm", "run", "$NODE_ENV"]
|
||||||
CMD npm run $NODE_ENV
|
CMD npm run $NODE_ENV
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
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<string[]>([]);
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
@ -11,32 +13,36 @@ 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 (
|
||||||
<div className="page-outer">
|
// 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'>
|
||||||
{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, idx) => (
|
{recipeIngredients.map(ing => (
|
||||||
<li key={idx}>{ing}</li>
|
<li key={ing.id}>
|
||||||
|
{ing.raw}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
export default RecipeIngredients;
|
export default RecipeIngredients
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
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 {
|
import { getRecipeById, deleteRecipe, setDBStars } from "../services/frontendApi.js";
|
||||||
getRecipeById,
|
import { type Recipe, type Ingredient } from "../types/Recipe"
|
||||||
deleteRecipe,
|
import Modal from '../components/Modal.tsx'
|
||||||
setDBStars,
|
import DemoModal from '../components/DemoModal.tsx'
|
||||||
} from "../services/frontendApi.js";
|
import StarRating from "../components/StarRating.tsx"
|
||||||
import { type Recipe } from "../types/Recipe";
|
import TimeDisplay from '../components/TimeDisplay.tsx'
|
||||||
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);
|
||||||
|
@ -24,21 +20,16 @@ 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 =
|
const isWebSource = recipe && recipe.details && recipe.details.author
|
||||||
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 = () => {
|
const openModal = () => { setShowConfirmModal(true) };
|
||||||
setShowConfirmModal(true);
|
const closeModal = () => { setShowConfirmModal(false) };
|
||||||
};
|
|
||||||
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 {
|
||||||
|
@ -52,13 +43,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) {
|
||||||
|
@ -86,56 +77,38 @@ 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
|
<Link to="/" className="ar-button bg-amber-600 text-white py-2 px-4 rounded hover:bg-amber-700">
|
||||||
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
|
<button onClick={() => { }} className="invisible ar-button py-1 px-1 rounded self-start">🗑️</button>
|
||||||
onClick={() => {}}
|
<h3 className="text-center max-w-lg px-4 text-2xl lg:text-3xl font-bold text-amber-900">{recipe.details.name}</h3>
|
||||||
className="invisible ar-button py-1 px-1 rounded self-start"
|
<button onClick={openModal} className="ar-button bg-amber-500 text-white py-1 px-1 rounded hover:bg-amber-600 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>
|
|
||||||
<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">
|
<p className="text-amber-700 italic text-lg">{recipe.details.cuisine}</p>
|
||||||
{recipe.details.cuisine}
|
<p>prep: <TimeDisplay minutes={recipe.details.prep_minutes ?? 0} /> | cook: <TimeDisplay minutes={recipe.details.cook_minutes ?? 0} /></p>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
prep: <TimeDisplay minutes={recipe.details.prep_minutes ?? 0} />{" "}
|
|
||||||
| cook:{" "}
|
|
||||||
<TimeDisplay minutes={recipe.details.cook_minutes ?? 0} />
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -145,10 +118,10 @@ function RecipePage() {
|
||||||
Ingredients:
|
Ingredients:
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{recipe.ingredients.map((ingredient: string, 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}</span>
|
<span className="font-medium text-left">{ingredient.raw}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -159,18 +132,12 @@ function RecipePage() {
|
||||||
Instructions:
|
Instructions:
|
||||||
</h4>
|
</h4>
|
||||||
<ol className="space-y-3">
|
<ol className="space-y-3">
|
||||||
{recipe.steps &&
|
{recipe.steps && Object.keys(recipe.steps || {}).map((stepNumber) => (
|
||||||
Object.keys(recipe.steps || {}).map((stepNumber) => (
|
<li key={stepNumber} className="text-gray-700 flex items-start">
|
||||||
<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">
|
<span className="leading-relaxed text-left">{recipe.steps[parseInt(stepNumber)].instruction}</span>
|
||||||
{recipe.steps[parseInt(stepNumber)].instruction}
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -185,15 +152,13 @@ function RecipePage() {
|
||||||
<span>From the kitchen of {recipe.details.author}</span>
|
<span>From the kitchen of {recipe.details.author}</span>
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
<StarRating
|
<StarRating rating={stars} onRatingChange={(newRating: number) => setStars(newRating)} />
|
||||||
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}
|
||||||
|
@ -208,7 +173,7 @@ function RecipePage() {
|
||||||
closeModal={() => setShowDemoModal(false)}
|
closeModal={() => setShowDemoModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
const baseUrl =
|
const baseUrl = process.env.NODE_ENV !== 'dev'
|
||||||
process.env.NODE_ENV !== "dev" ? "/api" : "http://localhost:3000/api";
|
? '/'
|
||||||
|
: 'http://localhost:3000/';
|
||||||
|
|
||||||
export const getRecipes = async () => {
|
export const getRecipes = async () => {
|
||||||
console.log(`${baseUrl}/recipes`);
|
const response = await fetch(`${baseUrl}backend/recipes`);
|
||||||
const response = await fetch(`${baseUrl}/recipes`);
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRecipeSteps = async () => {
|
export const getRecipeSteps = async () => {
|
||||||
const response = await fetch(`${baseUrl}/recipe-steps`);
|
const response = await fetch(`${baseUrl}backend/recipe-steps`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRecipeIngredients = async () => {
|
export const getRecipeIngredients = async () => {
|
||||||
const response = await fetch(`${baseUrl}/recipe-ingredients`);
|
const response = await fetch(`${baseUrl}backend/recipe-ingredients`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRecipeById = async (id) => {
|
export const getRecipeById = async (id) => {
|
||||||
const response = await fetch(`${baseUrl}/recipe/${id}`);
|
const response = await fetch(`${baseUrl}backend/recipe/${id}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!data || !data.details) {
|
if (!data || !data.details) {
|
||||||
return { details: null };
|
return { details: null };
|
||||||
|
@ -32,7 +32,7 @@ export const getRecipeById = async (id) => {
|
||||||
export const addRecipe = async (recipeData) => {
|
export const addRecipe = async (recipeData) => {
|
||||||
console.log(JSON.stringify(recipeData));
|
console.log(JSON.stringify(recipeData));
|
||||||
// return
|
// return
|
||||||
const response = await fetch(`${baseUrl}/add-recipe`, {
|
const response = await fetch(`${baseUrl}backend/add-recipe`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(recipeData),
|
body: JSON.stringify(recipeData),
|
||||||
|
@ -45,7 +45,7 @@ export const addRecipe = async (recipeData) => {
|
||||||
export const setDBStars = async (id, stars) => {
|
export const setDBStars = async (id, stars) => {
|
||||||
console.log(JSON.stringify({ id: id, stars: stars }));
|
console.log(JSON.stringify({ id: id, stars: stars }));
|
||||||
// return
|
// return
|
||||||
const response = await fetch(`${baseUrl}/set-stars`, {
|
const response = await fetch(`${baseUrl}backend/set-stars`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ id: id, stars: stars }),
|
body: JSON.stringify({ id: id, stars: stars }),
|
||||||
|
@ -58,7 +58,7 @@ export const setDBStars = async (id, stars) => {
|
||||||
export const deleteRecipe = async (id) => {
|
export const deleteRecipe = async (id) => {
|
||||||
console.log(id);
|
console.log(id);
|
||||||
// return
|
// return
|
||||||
const response = await fetch(`${baseUrl}/delete-recipe`, {
|
const response = await fetch(`${baseUrl}backend/delete-recipe`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ id }),
|
body: JSON.stringify({ id }),
|
||||||
|
|
|
@ -4,6 +4,13 @@ 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;
|
||||||
|
@ -13,8 +20,8 @@ interface Recipe {
|
||||||
cuisine?: string;
|
cuisine?: string;
|
||||||
prep_minutes?: number;
|
prep_minutes?: number;
|
||||||
cook_minutes?: number;
|
cook_minutes?: number;
|
||||||
};
|
},
|
||||||
ingredients: string[];
|
ingredients: Ingredient[],
|
||||||
steps: Step[];
|
steps: Step[];
|
||||||
}
|
}
|
||||||
// smaller Recipe type returned by backend at /recipes for all
|
// smaller Recipe type returned by backend at /recipes for all
|
||||||
|
@ -27,4 +34,5 @@ interface RecipeSmall {
|
||||||
prep_minutes: number;
|
prep_minutes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { Recipe, Step, RecipeSmall };
|
|
||||||
|
export type { Recipe, Ingredient, Step, RecipeSmall }
|
||||||
|
|
15
postgres/docker-compose.yaml
Normal file
15
postgres/docker-compose.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: recipe_postgres
|
||||||
|
image: docker.io/library/postgres:17
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${DB_USER}
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
- POSTGRES_DB=${DB_NAME}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- ./db:/var/lib/postgresql/data
|
Loading…
Add table
Add a link
Reference in a new issue