This commit is contained in:
parent
7258d283ed
commit
31f5bdc254
42 changed files with 21523 additions and 1040 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
#### Backend:
|
#### Backend:
|
||||||
|
|
||||||
- Node.js & Express
|
- NestJS
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
- Prisma
|
- Prisma
|
||||||
|
|
||||||
|
|
|
||||||
58
backend/.gitignore
vendored
58
backend/.gitignore
vendored
|
|
@ -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
4
backend/.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
|
|
@ -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
98
backend/README.md
Normal 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>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](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
35
backend/eslint.config.mjs
Normal 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
8
backend/nest-cli.json
Normal 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
21043
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
backend/src/app.controller.spec.ts
Normal file
22
backend/src/app.controller.spec.ts
Normal 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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
backend/src/app.controller.ts
Normal file
12
backend/src/app.controller.ts
Normal 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
36
backend/src/app.module.ts
Normal 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 {}
|
||||||
8
backend/src/app.service.ts
Normal file
8
backend/src/app.service.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
9
backend/src/database/database.module.ts
Normal file
9
backend/src/database/database.module.ts
Normal 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 {}
|
||||||
18
backend/src/database/database.service.spec.ts
Normal file
18
backend/src/database/database.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
backend/src/database/database.service.ts
Normal file
17
backend/src/database/database.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
15
backend/src/main.ts
Normal 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();
|
||||||
|
|
@ -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;
|
|
||||||
7
backend/src/recipes/create-recipe.dto.ts
Normal file
7
backend/src/recipes/create-recipe.dto.ts
Normal 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[];
|
||||||
|
};
|
||||||
20
backend/src/recipes/recipes.controller.spec.ts
Normal file
20
backend/src/recipes/recipes.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
45
backend/src/recipes/recipes.controller.ts
Normal file
45
backend/src/recipes/recipes.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/recipes/recipes.module.ts
Normal file
9
backend/src/recipes/recipes.module.ts
Normal 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 {}
|
||||||
18
backend/src/recipes/recipes.service.spec.ts
Normal file
18
backend/src/recipes/recipes.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
222
backend/src/recipes/recipes.service.ts
Normal file
222
backend/src/recipes/recipes.service.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
25
backend/test/app.e2e-spec.ts
Normal file
25
backend/test/app.e2e-spec.ts
Normal 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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
backend/test/jest-e2e.json
Normal file
9
backend/test/jest-e2e.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
backend/tsconfig.build.json
Normal file
4
backend/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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 }),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue