diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..153f9d9 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,7 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index da30ba9..99b43e3 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,3 +1,4 @@ node_modules build/ *.db +.env \ No newline at end of file diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..15713ff --- /dev/null +++ b/api/README.md @@ -0,0 +1,26 @@ +# Greenpeace Backend + +## Overview +The frontend of the application will serve the maps UI by accessing the Google Maps API and rendering the result using React, with additional components from Material UI. Our complete (frontend) tech stack is: +- Express.js +- Prisma + + +## Setup + +To create the `.env` file, copy the `.env.example` file, make sure you are in the `backend` folder and run the following: +``` +cp .env.example .env +``` + +Next, start the Docker Compose services, use the command: + +``` +docker-compose up +``` + + +--- + + +Feel free to add any issue in this file, or let the leads know! \ No newline at end of file diff --git a/api/dockerfile b/api/dockerfile new file mode 100644 index 0000000..589aa17 --- /dev/null +++ b/api/dockerfile @@ -0,0 +1,13 @@ +FROM node:lts + +RUN mkdir -p /app/ + +COPY . /app/ + +WORKDIR /app/ + +EXPOSE 3000 + +RUN npm install + +CMD ["npm", "run", "debug:watch"] \ No newline at end of file diff --git a/api/package.json b/api/package.json index 0f69763..f6c3c6d 100644 --- a/api/package.json +++ b/api/package.json @@ -5,7 +5,6 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "prepare": "husky install", "lint": "eslint --ignore-path .gitignore --ext .ts src", "lint:fix": "npm run lint -- --fix", "format": "prettier write src", diff --git a/api/src/pin/pin.controller.ts b/api/src/pin/pin.controller.ts index 53c62e6..2b59174 100644 --- a/api/src/pin/pin.controller.ts +++ b/api/src/pin/pin.controller.ts @@ -1,5 +1,14 @@ import { Injectable } from '@decorators/di'; -import { Controller, Get, Params, Response } from '@decorators/express'; +import { + Body, + Controller, + Delete, + Get, + Params, + Post, + Put, + Response, +} from '@decorators/express'; import { Response as ExpressResponse } from 'express'; import PinService from './pin.service'; import { ManyPinResponseDto, PinDto, PinResponseDto } from './pin.dto'; @@ -23,7 +32,7 @@ class PinController { } @Get('/:id') - async getByID( + async getOne( @Response() response: ExpressResponse, @Params('id') id: string ) { @@ -37,6 +46,55 @@ class PinController { ? response.status(200).send(dto) : response.status(404).send(dto); } + + @Post('/') + async create( + @Response() response: ExpressResponse, + @Body('pin') pin: PinDto + ) { + const result: PinDto | null = await this.service.create(pin); + + const dto: PinResponseDto = result + ? ({ status: 'Success', pin: result } as PinResponseDto) + : ({ status: 'Error' } as PinResponseDto); + + return result + ? response.status(200).send(dto) + : response.status(500).send(dto); + } + + @Put('/:id') + async update( + @Response() response: ExpressResponse, + @Params('id') id: number, + @Body('pin') pin: Partial + ) { + const result: PinDto | null = await this.service.update(id, pin); + + const dto: PinResponseDto = result + ? ({ status: 'Success', pin: result } as PinResponseDto) + : ({ status: 'Error' } as PinResponseDto); + + return result + ? response.status(200).send(dto) + : response.status(404).send(dto); + } + + @Delete('/:id') + async delete( + @Response() response: ExpressResponse, + @Params('id') id: string + ) { + const result: PinDto = await this.service.remove(Number(id)); + + const dto: PinResponseDto = result + ? ({ status: 'Success', pin: result } as PinResponseDto) + : ({ status: 'Error' } as PinResponseDto); + + return result + ? response.status(200).send(dto) + : response.status(404).send(dto); + } } export { PinController }; diff --git a/api/src/pin/pin.dto.ts b/api/src/pin/pin.dto.ts index ed6eec3..16cba46 100644 --- a/api/src/pin/pin.dto.ts +++ b/api/src/pin/pin.dto.ts @@ -16,4 +16,6 @@ export interface PinDto { coordinateY: number; isValid?: boolean; createdAt: Date; -} + category: string; + reactions?: string[]; +} \ No newline at end of file diff --git a/api/src/pin/pin.service.ts b/api/src/pin/pin.service.ts index ebdf81b..7429b17 100644 --- a/api/src/pin/pin.service.ts +++ b/api/src/pin/pin.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@decorators/di'; import { PrismaClient } from '@prisma/client'; +import { PinDto } from './pin.dto'; @Injectable() class PinService { @@ -16,6 +17,38 @@ class PinService { }, }); } + + async create(pin: PinDto) { + return await this.prisma.pin.create({ + data: { + name: pin.name, + description: String(pin.description), + coordinateX: pin.coordinateX, + coordinateY: pin.coordinateY, + isValid: pin.isValid, + createdAt: pin.createdAt, + }, + }); + } + + async remove(id: number) { + return await this.prisma.pin.delete({ + where: { + id: id, + }, + }); + } + + async update(id: number, pin: Partial) { + return await this.prisma.pin.update({ + where: { + id: id, + }, + data: { + ...pin, + }, + }); + } } export default PinService; diff --git a/api/src/prisma/app.db-journal b/api/src/prisma/app.db-journal new file mode 100644 index 0000000..f81be46 Binary files /dev/null and b/api/src/prisma/app.db-journal differ diff --git a/api/src/prisma/migrations/20231018232930_added_pin/migration.sql b/api/src/prisma/migrations/20231018232930_added_pin/migration.sql deleted file mode 100644 index f473e5f..0000000 --- a/api/src/prisma/migrations/20231018232930_added_pin/migration.sql +++ /dev/null @@ -1,10 +0,0 @@ --- CreateTable -CREATE TABLE "Pin" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "name" TEXT NOT NULL, - "description" TEXT NOT NULL, - "coordinateX" REAL NOT NULL, - "coordinateY" REAL NOT NULL, - "isVaid" BOOLEAN NOT NULL DEFAULT true, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); diff --git a/api/src/prisma/migrations/20240131234713_initial_pin/migration.sql b/api/src/prisma/migrations/20240131234713_initial_pin/migration.sql new file mode 100644 index 0000000..8c51bca --- /dev/null +++ b/api/src/prisma/migrations/20240131234713_initial_pin/migration.sql @@ -0,0 +1,17 @@ +-- CreateEnum +CREATE TYPE "Reaction" AS ENUM ('LIKE', 'DISLIKE', 'GOOD_VALUE', 'BAD_VALUE'); + +-- CreateTable +CREATE TABLE "Pin" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "coordinateX" DOUBLE PRECISION NOT NULL, + "coordinateY" DOUBLE PRECISION NOT NULL, + "isValid" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "category" TEXT NOT NULL DEFAULT '', + "reactions" "Reaction"[], + + CONSTRAINT "Pin_pkey" PRIMARY KEY ("id") +); diff --git a/api/src/prisma/migrations/20240131235715_convert_reactions_to_string/migration.sql b/api/src/prisma/migrations/20240131235715_convert_reactions_to_string/migration.sql new file mode 100644 index 0000000..37b9e5a --- /dev/null +++ b/api/src/prisma/migrations/20240131235715_convert_reactions_to_string/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - The `reactions` column on the `Pin` table would be dropped and recreated. This will lead to data loss if there is data in the column. + +*/ +-- AlterTable +ALTER TABLE "Pin" DROP COLUMN "reactions", +ADD COLUMN "reactions" TEXT[]; + +-- DropEnum +DROP TYPE "Reaction"; diff --git a/api/src/prisma/migrations/migration_lock.toml b/api/src/prisma/migrations/migration_lock.toml index e5e5c47..fbffa92 100644 --- a/api/src/prisma/migrations/migration_lock.toml +++ b/api/src/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file +provider = "postgresql" \ No newline at end of file diff --git a/api/src/prisma/schema.prisma b/api/src/prisma/schema.prisma index 77ec4ea..17ec992 100644 --- a/api/src/prisma/schema.prisma +++ b/api/src/prisma/schema.prisma @@ -6,8 +6,8 @@ generator client { } datasource db { - provider = "sqlite" - url = "file:app.db" + provider = "postgresql" + url = "postgresql://greenpeace@127.0.0.1:5432/greenpeace" } model Pin { @@ -16,6 +16,8 @@ model Pin { description String coordinateX Float coordinateY Float - isVaid Boolean @default(true) + isValid Boolean @default(true) createdAt DateTime @default(now()) + category String @default("") + reactions String[] } diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f1d9424 --- /dev/null +++ b/compose.yml @@ -0,0 +1,28 @@ +version: "3.8" + +services: + database: + image: postgres + container_name: greenpeace-postgres + ports: + - 5432:5432 + volumes: + - greenpeace-postgres-data:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=greenpeace + - POSTGRES_PASSWORD=greenpeace + - POSTGRES_DATABASE=greenpeace + restart: unless-stopped + api: + build: api/ + container_name: greenpeace-api + ports: + - 3000:3000 + volumes: + - ./api/:/app/ + environment: + - DATABASE_URL=postgresql://greenpeace:greenpeace@127.0.0.1:5432/greenpeace + restart: unless-stopped + +volumes: + greenpeace-postgres-data: \ No newline at end of file diff --git a/frontend/src/Main.js b/frontend/src/Main.js index 958ac82..5e04886 100644 --- a/frontend/src/Main.js +++ b/frontend/src/Main.js @@ -13,13 +13,30 @@ import mapStyles from './mapStyles'; function Map() { const [selectedPin, setSelectedPin] = useState(null); - const [pins] = useState([]) + const [pins, setPins] = useState([]); + const [isListening, setIsListening] = useState(false); + + const createPin = (coordinates, name, description) => { + setPins([...pins, { + id: pins.length, + coordinates: coordinates, + location: name, + description: description + }]) + } return ( { + if (isListening) { + createPin([event.latLng.lat(), event.latLng.lng()], "New Pin", "New Description"); + setIsListening(false); + } + } + } > {pins.map(pin => ( ))} diff --git a/frontend/src/components/LoginSignup/LoginSignup.jsx b/frontend/src/components/LoginSignup/LoginSignup.jsx index 5b1571f..444f8a0 100644 --- a/frontend/src/components/LoginSignup/LoginSignup.jsx +++ b/frontend/src/components/LoginSignup/LoginSignup.jsx @@ -14,10 +14,12 @@ const LoginSignup = () => { const handleEmailChange = (event) => { setEmail(event.target.value); + setErrorEmail(false); }; const handlePasswordChange = (event) => { setPassword(event.target.value); + setErrorPassword(false); }; const handleLoginSignup = () => {