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

@ -7,7 +7,7 @@
#### Backend: #### Backend:
- Node.js & Express - NestJS
- PostgreSQL - PostgreSQL
- Prisma - Prisma

58
backend/.gitignore vendored
View file

@ -1,2 +1,56 @@
node_modules # compiled output
scratch /dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

4
backend/.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View file

@ -21,6 +21,8 @@ RUN npm run build
FROM base FROM base
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/prisma ./prisma
RUN mkdir /app/logs
RUN touch /app/logs/app.log
EXPOSE 3000 EXPOSE 3000

98
backend/README.md Normal file
View file

@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

35
backend/eslint.config.mjs Normal file
View file

@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);

8
backend/nest-cli.json Normal file
View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

21043
backend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,87 @@
{ {
"name": "backend", "name": "backend",
"version": "1.0.0", "version": "0.0.1",
"main": "index.js",
"scripts": {
"dev": "nodemon ./src/index.ts",
"build": "tsc",
"prisma:generate": "prisma generate",
"prod": "node ./dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "", "description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"dev": "nest start --watch",
"prisma:generate": "prisma generate",
"prod": "node ./dist/src/main.js",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": { "dependencies": {
"@nestjs/common": "^11.1.12",
"@nestjs/core": "^11.1.12",
"@nestjs/mapped-types": "2.1.0",
"@nestjs/platform-express": "^11.1.12",
"@prisma/adapter-pg": "7.2.0", "@prisma/adapter-pg": "7.2.0",
"@prisma/client": "7.2.0", "@prisma/client": "7.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.0.1", "dotenv": "^17.2.3",
"express": "^5.1.0", "nestjs-pino": "^4.5.0",
"pg": "^8.17.2", "pg": "^8.17.1",
"prisma": "7.2.0" "pino-http": "^11.0.0",
"pino-pretty": "^13.1.3",
"prisma": "7.2.0",
"prisma-cli": "^1.0.9",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19", "@eslint/eslintrc": "^3.3.3",
"@types/express": "^5.0.3", "@eslint/js": "^9.39.2",
"@types/node": "^24.2.1", "@nestjs/cli": "^11.0.16",
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.12",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^22.19.7",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"nodemon": "^3.1.10", "@types/supertest": "^6.0.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^16.5.0",
"jest": "^30.2.0",
"prettier": "^3.8.0",
"source-map-support": "^0.5.21",
"supertest": "^7.2.2",
"ts-jest": "^29.4.6",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.2" "tsconfig-paths": "^4.2.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.1"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
} }
} }

View file

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View file

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

36
backend/src/app.module.ts Normal file
View file

@ -0,0 +1,36 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DatabaseModule } from './database/database.module';
import { RecipesModule } from './recipes/recipes.module';
import { LoggerModule } from 'nestjs-pino';
import { createWriteStream } from 'fs';
const logStream = createWriteStream('/app/logs/app.log', { flags: 'a' });
@Module({
imports: [
DatabaseModule,
RecipesModule,
LoggerModule.forRoot({
pinoHttp:
process.env.NODE_ENV === 'dev'
? {
transport: {
target: 'pino-pretty',
options: {
colorize: true,
},
},
autoLogging: false,
}
: {
stream: logStream,
autoLogging: false,
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View file

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View file

@ -1,125 +0,0 @@
import { Request, Response } from "express";
import RecipeModel from "../models/recipeModel";
import { PrismaClient } from "@prisma/client";
let model: RecipeModel;
export const initializeController = (prisma: PrismaClient) => {
model = new RecipeModel(prisma);
};
export const test = async (req: Request, res: Response): Promise<void> => {
console.log("test");
res.json({ env: process.env.NODE_ENV });
};
export const getAllRecipes = async (
req: Request,
res: Response,
): Promise<void> => {
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 instanceof Error ? error.message : "Unknown error",
});
}
};
export const getRecipeById = async (
req: Request,
res: Response,
): Promise<void> => {
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 instanceof Error ? error.message : "Unknown error",
});
}
};
export const addRecipe = async (req: Request, res: Response): Promise<void> => {
try {
console.log(req.body);
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 instanceof Error ? error.message : "Unknown error",
});
}
};
export const updateRecipe = async (
req: Request,
res: Response,
): Promise<void> => {
console.log(req.body);
const id = parseInt(req.params.id, 10);
try {
const updatedRecipe = await model.updateRecipe(req.body, id);
res.status(201).json(updatedRecipe);
} catch (error) {
res.status(500).json({
msg: "Failed to add recipe",
source: "recipeController",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
export const setStars = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.body.id, 10);
const stars = parseInt(req.body.stars, 10);
try {
const updatedRecipe = await model.setStars(id, stars);
res.status(202).json(updatedRecipe);
} catch (error) {
res.status(500).json({
msg: "Failed to set stars",
source: "recipeController",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
export const deleteRecipe = async (
req: Request,
res: Response,
): Promise<void> => {
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 instanceof Error ? error.message : "Unknown error",
});
}
};
export default {
test,
getAllRecipes,
getRecipeById,
addRecipe,
updateRecipe,
setStars,
deleteRecipe,
};

View file

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
@Global()
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DatabaseService } from './database.service';
describe('DatabaseService', () => {
let service: DatabaseService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DatabaseService],
}).compile();
service = module.get<DatabaseService>(DatabaseService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View file

@ -0,0 +1,17 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
@Injectable()
export class DatabaseService extends PrismaClient implements OnModuleInit {
constructor() {
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL as string,
});
super({ adapter });
}
async onModuleInit() {
await this.$connect();
}
}

View file

@ -1,51 +0,0 @@
import "dotenv/config";
import express, { Express } from "express";
import cors from "cors";
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import appRoutes from "./routes/appRoutes";
import { initializeController } from "./controllers/recipeController";
const app: Express = express();
const port = process.env.PORT || 3000;
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
const prisma = new PrismaClient({ adapter });
initializeController(prisma);
function setupMiddleware(app: Express) {
app.use(cors());
app.use(express.json());
app.use("/api", appRoutes);
}
setupMiddleware(app);
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 };

15
backend/src/main.ts Normal file
View file

@ -0,0 +1,15 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from 'nestjs-pino';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.useLogger(app.get(Logger));
if (process.env.NODE_ENV === 'dev') {
app.enableCors();
}
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View file

@ -1,239 +0,0 @@
import { PrismaClient } from "@prisma/client";
import Logger from "../utils/logger";
const logger = new Logger();
class RecipeModel {
private prisma: PrismaClient;
constructor(prisma: PrismaClient) {
this.prisma = prisma;
}
async getAllRecipes(): Promise<any[]> {
try {
logger.info("index page view");
return await this.prisma.recipes.findMany();
} catch (err) {
console.error("Error fetching all recipes:", err);
throw new Error(err instanceof Error ? err.message : "Unknown error");
}
}
async findById(id: number): Promise<any | null> {
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: { raw: string | null }) => ing.raw,
),
steps: recipe.recipeSteps.map(
(step: {
step_number: number | null;
instruction: string | null;
}) => ({
step_number: step.step_number ?? 0, // Default to 0 if null
instruction: step.instruction ?? "", // Default to empty string if null
}),
),
};
logger.info("recipe page view", {
recipe_id: data.details.id,
recipe_name: data.details.name,
});
return data;
} catch (err) {
console.log("Error finding recipe:", err);
logger.error("Error finding recipe", {
message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error(err instanceof Error ? err.message : "Unknown error");
}
}
async addRecipe(recipeData: {
details: {
name: string;
author: string;
cuisine: string;
stars: number;
prep_minutes: number;
cook_minutes: number;
};
ingredients: string[];
steps: { [key: string]: string };
}): Promise<any> {
const name = recipeData.details.name;
const author = recipeData.details.author;
const cuisine = recipeData.details.cuisine;
const stars = recipeData.details.stars;
const prep_minutes = recipeData.details.prep_minutes;
const cook_minutes = recipeData.details.cook_minutes;
const ingredients = recipeData.ingredients;
const steps = recipeData.steps;
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: steps,
},
},
});
logger.info("New recipe created", {
id: createdRecipe.id,
name: createdRecipe.name,
});
return createdRecipe;
} catch (err) {
console.log("Error creating recipe:", err);
logger.error("Error creating recipe", {
message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error("Failed to add recipe");
}
}
async updateRecipe(
recipeData: {
details: {
name: string;
author: string;
cuisine: string;
stars: number;
prep_minutes: number;
cook_minutes: number;
};
ingredients: string[];
steps: { [key: string]: string };
},
id: number,
): Promise<any> {
const name = recipeData.details.name;
const author = recipeData.details.author;
const cuisine = recipeData.details.cuisine;
const stars = recipeData.details.stars;
const prep_minutes = recipeData.details.prep_minutes;
const cook_minutes = recipeData.details.cook_minutes;
const ingredients = recipeData.ingredients;
const steps = recipeData.steps;
try {
await this.deleteRecipeData(id);
const updatedRecipe = await this.prisma.recipes.update({
where: { id },
data: {
name,
author,
cuisine,
prep_minutes,
cook_minutes,
stars,
recipeIngredients: {
create: ingredients.map((ing) => ({ raw: ing })),
},
recipeSteps: {
create: steps,
},
},
});
if (!updatedRecipe) {
logger.warn(`Recipe with id ${id} cannot be found`);
return null;
}
logger.info("Updated Recipe", {
id: updatedRecipe.id,
name: updatedRecipe.name,
});
return updatedRecipe;
} catch (err) {
console.log("Error updating recipe:", err);
logger.error("Error updating recipe", {
message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error("Failed to update recipe");
}
}
async setStars(id: number, stars: number): Promise<{ message: string }> {
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", {
message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error(err instanceof Error ? err.message : "Unknown error");
}
}
async deleteRecipe(id: number): Promise<{ message: string }> {
try {
await this.deleteRecipeData(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", {
message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error(err instanceof Error ? err.message : "Unknown error");
}
}
async deleteRecipeData(id: number): Promise<{ message: string }> {
try {
await this.prisma.recipe_ingredients.deleteMany({
where: { recipe_id: id },
});
await this.prisma.recipe_steps.deleteMany({
where: { recipe_id: id },
});
return { message: "Recipe data deleted successfully" };
} catch (err) {
console.error("Error deleting recipe:", err);
logger.error("Error deleting recipe", {
message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error(err instanceof Error ? err.message : "Unknown error");
}
}
}
export default RecipeModel;

View file

@ -0,0 +1,7 @@
import { Prisma } from '@prisma/client';
// also used for updateRecipe
export type CreateRecipeDto = Prisma.recipesCreateInput & {
recipeIngredients: Prisma.recipe_ingredientsCreateWithoutRecipesInput[];
recipeSteps: Prisma.recipe_stepsCreateWithoutRecipesInput[];
};

View file

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RecipesController } from './recipes.controller';
import { RecipesService } from './recipes.service';
describe('RecipesController', () => {
let controller: RecipesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RecipesController],
providers: [RecipesService],
}).compile();
controller = module.get<RecipesController>(RecipesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,45 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { RecipesService } from './recipes.service';
import type { CreateRecipeDto } from './create-recipe.dto';
@Controller('recipe')
export class RecipesController {
constructor(private readonly recipeService: RecipesService) {}
@Post()
create(@Body() createRecipeDto: CreateRecipeDto) {
return this.recipeService.create(createRecipeDto);
}
@Get()
findAll() {
return this.recipeService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.recipeService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateRecipeDto: CreateRecipeDto) {
return this.recipeService.update(+id, updateRecipeDto);
}
@Patch(':id/stars')
setStars(@Param('id') id: string, @Body('stars') stars: number) {
return this.recipeService.setStars(+id, stars);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.recipeService.remove(+id);
}
}

View file

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RecipesService } from './recipes.service';
import { RecipesController } from './recipes.controller';
@Module({
controllers: [RecipesController],
providers: [RecipesService],
})
export class RecipesModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RecipesService } from './recipes.service';
describe('RecipesService', () => {
let service: RecipesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RecipesService],
}).compile();
service = module.get<RecipesService>(RecipesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View file

@ -0,0 +1,222 @@
import { Injectable, Logger } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import type { CreateRecipeDto } from './create-recipe.dto';
@Injectable()
export class RecipesService {
constructor(private readonly databaseService: DatabaseService) {}
async create(createRecipeDto: CreateRecipeDto) {
try {
const createdRecipe = await this.databaseService.recipes.create({
data: {
name: createRecipeDto.name,
author: createRecipeDto.author,
cuisine: createRecipeDto.cuisine,
stars: createRecipeDto.stars,
prep_minutes: createRecipeDto.prep_minutes,
cook_minutes: createRecipeDto.cook_minutes,
recipeIngredients: {
create: createRecipeDto.recipeIngredients.map((ingredient) => ({
raw: ingredient.raw,
})),
},
recipeSteps: {
create: createRecipeDto.recipeSteps || [],
},
},
});
Logger.log('New recipe created', {
id: createdRecipe.id,
name: createdRecipe.name,
});
return createdRecipe;
} catch (err) {
Logger.error('Error creating recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error('Failed to add recipe');
}
}
async findAll() {
try {
Logger.log('index page view');
return await this.databaseService.recipes.findMany({});
} catch (err) {
Logger.error('Error fetching all recipes', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async findOne(id: number) {
try {
const recipe = await this.databaseService.recipes.findUnique({
where: { id },
include: { recipeSteps: true, recipeIngredients: true },
});
if (!recipe) {
Logger.warn(`Recipe with id ${id} cannot be found`);
return null;
}
const data = {
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,
recipeIngredients: recipe.recipeIngredients.map((ing) => ing.raw),
recipeSteps: recipe.recipeSteps.map((step) => ({
step_number: step.step_number ?? 0,
instruction: step.instruction ?? '',
})),
};
Logger.log('Recipe page view', {
recipe_id: data.id,
recipe_name: data.name,
});
return data;
} catch (err) {
Logger.error('Error finding recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async update(id: number, updateRecipeDto: CreateRecipeDto) {
try {
const updatedRecipe = await this.databaseService.$transaction(
async (prisma) => {
await prisma.recipe_ingredients.deleteMany({
where: { recipe_id: id },
});
await prisma.recipe_steps.deleteMany({
where: { recipe_id: id },
});
const recipeUpdate = await prisma.recipes.update({
where: { id },
data: {
name: updateRecipeDto.name,
author: updateRecipeDto.author,
cuisine: updateRecipeDto.cuisine,
stars: updateRecipeDto.stars,
prep_minutes: updateRecipeDto.prep_minutes,
cook_minutes: updateRecipeDto.cook_minutes,
updated_at: new Date(),
},
});
const ingredientsData = updateRecipeDto.recipeIngredients.map(
(ingredient) => ({
raw: ingredient.raw ?? '',
quantity: ingredient.quantity ?? null,
unit: ingredient.unit ?? null,
notes: ingredient.notes ?? null,
recipe_id: id,
}),
);
const stepsData = updateRecipeDto.recipeSteps.map((step) => ({
step_number: step.step_number ?? null,
instruction: step.instruction ?? '',
recipe_id: id,
}));
if (ingredientsData.length > 0) {
await prisma.recipe_ingredients.createMany({
data: ingredientsData,
skipDuplicates: true,
});
}
if (stepsData.length > 0) {
await prisma.recipe_steps.createMany({
data: stepsData,
skipDuplicates: true,
});
}
return recipeUpdate;
},
);
Logger.log('Updated Recipe', {
id: updatedRecipe.id,
name: updatedRecipe.name,
});
return updatedRecipe;
} catch (err) {
console.error('Update method caught error:', err);
Logger.error('Error updating recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
stack: err instanceof Error ? err.stack : 'No stack available',
id,
updateRecipeDto,
});
throw new Error('Failed to update recipe');
}
}
private async deleteRecipeData(id: number) {
try {
await this.databaseService.recipe_ingredients.deleteMany({
where: { recipe_id: id },
});
await this.databaseService.recipe_steps.deleteMany({
where: { recipe_id: id },
});
Logger.log(`Recipe data deleted successfully for recipe id: ${id}`);
} catch (err) {
Logger.error('Error deleting recipe data', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async setStars(id: number, stars: number) {
try {
await this.databaseService.recipes.update({
where: { id },
data: { stars },
});
return { message: 'Stars updated' };
} catch (err) {
Logger.error('Error setting stars', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async remove(id: number) {
try {
await this.deleteRecipeData(id);
const deletedRecipe = await this.databaseService.recipes.delete({
where: { id },
});
Logger.log('Recipe deleted', {
id: deletedRecipe.id,
name: deletedRecipe.name,
});
return { message: 'Recipe deleted successfully' };
} catch (err) {
Logger.error('Error deleting recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
}

View file

@ -1,20 +0,0 @@
import express, { Router } from "express";
import recipeController from "../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("/update-recipe/:id", recipeController.updateRecipe);
router.post("/set-stars", recipeController.setStars);
router.delete("/delete-recipe", recipeController.deleteRecipe);
export default router;

View file

@ -1,35 +0,0 @@
import fs from "fs";
class Logger {
private filePath: string;
constructor(filePath: string = "/logs/app.log") {
this.filePath = filePath;
}
private log(level: string, message: string, params?: object) {
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: string, params: object = {}) {
this.log("info", message, params);
}
warn(message: string, params: object = {}) {
this.log("warn", message, params);
}
error(message: string, params: object = {}) {
this.log("error", message, params);
}
}
export default Logger;

View file

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View file

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View file

@ -1,16 +1,25 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "module": "nodenext",
"module": "CommonJS", "moduleResolution": "nodenext",
"moduleResolution": "node", "resolvePackageJsonExports": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "isolatedModules": true,
"strict": true, "declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"outDir": "dist" "strictNullChecks": true,
}, "forceConsistentCasingInFileNames": true,
"include": [ "noImplicitAny": false,
"src", "strictBindCallApply": false,
"scripts" "noFallthroughCasesInSwitch": false
] }
} }

View file

@ -29,6 +29,7 @@ services:
- DB_PASSWORD=${DB_PASSWORD} - DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME} - DB_NAME=${DB_NAME}
- DATABASE_URL=${DATABASE_URL} - DATABASE_URL=${DATABASE_URL}
- NODE_ENV=dev
frontend: frontend:
container_name: recipes_frontend_dev container_name: recipes_frontend_dev
restart: unless-stopped restart: unless-stopped

View file

@ -11,6 +11,8 @@ services:
- POSTGRES_DB=${DB_NAME} - POSTGRES_DB=${DB_NAME}
volumes: volumes:
- ./db:/var/lib/postgresql/data - ./db:/var/lib/postgresql/data
ports:
- "5432:5432"
backend: backend:
container_name: recipes_backend container_name: recipes_backend
image: forgejo.fredzernia.com/fred/recipe_app_backend:latest image: forgejo.fredzernia.com/fred/recipe_app_backend:latest
@ -18,7 +20,7 @@ services:
build: build:
context: ./backend context: ./backend
volumes: volumes:
- ./logs:/logs - ./logs:/app/logs
environment: environment:
- DB_USER=${DB_USER} - DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD} - DB_PASSWORD=${DB_PASSWORD}

View file

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

View file

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

View file

@ -1,23 +1,58 @@
function About() { function About() {
return ( return (
<div className="about page-outer"> <div className="about page-outer">
<div> <div>
<h2 className="text-xl text-[var(--color-secondaryTextDark)]">This app uses the following components:</h2> <h2 className="text-xl text-[var(--color-secondaryTextDark)]">
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Frontend:</h2> This app uses the following components:
<ul><li>React</li><li>TypeScript</li></ul> </h2>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Backend:</h2> <h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">
<ul><li>Node.js & Express</li><li>PostgreSQL</li><li>Prisma</li></ul> Frontend:
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Containerization:</h2> </h2>
<ul><li>Docker</li></ul> <ul>
<h2 className="mt-4 font-bold text-xl text-[var(--color-secondaryTextDark)]">Styling/UI:</h2> <li>React</li>
<ul><li>Tailwind CSS</li></ul> <li>TypeScript</li>
<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> | </ul>
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="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> </div>
) );
} }
export default About export default About;

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ pkgs.mkShell {
buildInputs = [ buildInputs = [
pkgs.prisma-engines pkgs.prisma-engines
pkgs.prisma pkgs.prisma
pkgs.nodePackages."@nestjs/cli"
]; ];
shellHook = '' shellHook = ''
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig" export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig"