diff --git a/databases/dev.db b/databases/dev.db deleted file mode 100644 index 04166d9..0000000 Binary files a/databases/dev.db and /dev/null differ diff --git a/prisma/migrations/20230217230230_created_at_for_the_review_model/migration.sql b/prisma/migrations/20230217230230_created_at_for_the_review_model/migration.sql new file mode 100644 index 0000000..163c2da --- /dev/null +++ b/prisma/migrations/20230217230230_created_at_for_the_review_model/migration.sql @@ -0,0 +1,17 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_review" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "authorId" INTEGER NOT NULL, + "hotelId" INTEGER NOT NULL, + "rating" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "review_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "Users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "review_hotelId_fkey" FOREIGN KEY ("hotelId") REFERENCES "Hotels" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_review" ("authorId", "hotelId", "id", "rating", "text") SELECT "authorId", "hotelId", "id", "rating", "text" FROM "review"; +DROP TABLE "review"; +ALTER TABLE "new_review" RENAME TO "review"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20230218005318_make_review_unique_author_id_hotel_id/migration.sql b/prisma/migrations/20230218005318_make_review_unique_author_id_hotel_id/migration.sql new file mode 100644 index 0000000..517cc08 --- /dev/null +++ b/prisma/migrations/20230218005318_make_review_unique_author_id_hotel_id/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[authorId,hotelId]` on the table `review` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "review_authorId_hotelId_key" ON "review"("authorId", "hotelId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e23a289..79f8a1d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -78,4 +78,6 @@ model review { hotel hotel @relation(fields: [hotelId], references: [id]) rating Int text String + createdAt DateTime @default(now()) + // @@unique([authorId, hotelId]) it should be unique } diff --git a/public/auth/front.css b/public/auth/front.css new file mode 100644 index 0000000..92ab08a --- /dev/null +++ b/public/auth/front.css @@ -0,0 +1,106 @@ +/* Hero image */ +.hero-image { + background-image: url("https://source.unsplash.com/1600x900/?hotel"); + background-color: #000; + height: 50vh; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + position: relative; + } + + .hero-text { + text-align: center; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + } + + /* Hotel cards */ + .card { + border: none; + margin: 20px; + } + + .card .card-img-top { + object-fit: cover; + height: 200px; + } + + .card-title { + font-size: 24px; + font-weight: 500; + } + + .card-text { + margin-top: 10px; + margin-bottom: 10px; + } + + + .rating { + color:red + } + + /* Star rating */ +.stars { + display: inline-block; + font-size: 18px; + color: #ffd700; + } + + .stars i { + margin-right: 5px; + } + + .half { + position: relative; + } + + .half::before { + content: "\f089"; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + position: absolute; + left: 0; + color: #777; + } + + /* change color of the stars according to rating data */ + .stars-0, + .stars-1, + .stars-2, + .stars-3, + .stars-4, + .stars-5 { + color: #b3b3b3; + } + + .stars-1 i:nth-child(-n + 1), + .stars-2 i:nth-child(-n + 2), + .stars-3 i:nth-child(-n + 3), + .stars-4 i:nth-child(-n + 4), + .stars-5 i:nth-child(-n + 5) { + color: #ffd700; + } + + .stars-0.half, + .stars-1.half, + .stars-2.half, + .stars-3.half, + .stars-4.half, + .stars-5.half { + color: #ffd700; + } + + .stars-0.half::before, + .stars-1.half::before, + .stars-2.half::before, + .stars-3.half::before, + .stars-4.half::before, + .stars-5.half::before { + color: #b3b3b3; + } + diff --git a/public/auth/script/front.js b/public/auth/script/front.js new file mode 100644 index 0000000..bc13f99 --- /dev/null +++ b/public/auth/script/front.js @@ -0,0 +1,44 @@ +let hotelurl; +hotelurl = 'http://localhost:3000/upload/'; +JavaScript: + +$(document).ready(() => { + // fetch data from API and generate hotel cards + fetch("http://localhost:3000/review/a",{ + method:'Put' + }) + .then(response => response.json()) + .then(data => { + const hotelCards = data.map(hotel => { + // calculate star rating data + const rating = hotel.averageRating; + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 >= 0.5; + const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); + // generate star rating HTML + const starRatingHtml = ` +
+ ${Array(fullStars).fill().map(() => '').join("")} + ${hasHalfStar ? '' : ""} + ${Array(emptyStars).fill().map(() => '').join("")} +
+ `; + // generate hotel card HTML + + return ` +
+
+ ${hotel.hotel_name} +
+
${hotel.hotel_name}
+

${hotel.user_name}

+
${starRatingHtml}
+
+
+
+ `; + }).join(""); + // insert hotel cards into DOM + $("#hotel-cards").html(hotelCards); + }); +}); diff --git a/src/main.ts b/src/main.ts index 867bf90..a0f1591 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ async function bootstrap() { AppModule, {cors: true} ); + app.enableCors(); app.useGlobalPipes( new ValidationPipe({ diff --git a/src/review/review.controller.ts b/src/review/review.controller.ts index 87631b8..943758c 100644 --- a/src/review/review.controller.ts +++ b/src/review/review.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, ParseIntPipe, Patch, Post, Req, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Put, Req, UseGuards } from '@nestjs/common'; import { JwtGuard } from 'src/auth/jwt.guard'; import { reviewDto, updateReviewDto } from './review.dto'; import { ReviewService } from './review.service'; @@ -42,7 +42,20 @@ export class ReviewController { } + @UseGuards(JwtGuard) + @Delete('/:id') + async deleteReview(@Param('id', new ParseIntPipe()) reviewId:number, @Req() req: any){ + return await this.reviewService.deleteReview(reviewId,req.user.id) + } + @Get('average/:id') + async getAverageRating(@Param('id', new ParseIntPipe()) hotelId:number){ + return await this.reviewService.getAverageRating(hotelId) + } + @Put('a') + async getAllHotels(){ + return await this.reviewService.getAllHotels() + } diff --git a/src/review/review.dto.ts b/src/review/review.dto.ts index b75b69f..b8fe961 100644 --- a/src/review/review.dto.ts +++ b/src/review/review.dto.ts @@ -17,3 +17,5 @@ export class updateReviewDto{ @IsNotEmpty() hotelId:number } + + diff --git a/src/review/review.service.ts b/src/review/review.service.ts index cf66418..b8304ac 100644 --- a/src/review/review.service.ts +++ b/src/review/review.service.ts @@ -1,4 +1,4 @@ -import { All, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { All, ForbiddenException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from 'src/prisma/prisma.service'; import { reviewDto, updateReviewDto } from './review.dto' @@ -8,17 +8,34 @@ export class ReviewService { constructor(private prisma:PrismaService){} async createReview(dto: reviewDto, userId: number) { - let newReview = await this.prisma.review.create({ - data: { - authorId: userId, - hotelId:dto.hotelId, - rating:dto.rating, - text:dto.text + try { + let hotel = await this.prisma.hotel.findUnique({ + where:{ + id:dto.hotelId + } + }) + if (!hotel){ + throw new NotFoundException(`Hotel with ID ${dto.hotelId} not found.`); } - }); - return newReview - } - + let newReview = await this.prisma.review.create({ + data: { + authorId: userId, + hotelId:dto.hotelId, + rating:dto.rating, + text:dto.text + } + }); + return { + success:true, + data:newReview + } + } catch (error) { + if (error.code === "P2002") { + return { success: false, message: "You have already reviewed this hotel." }; + } + return { success: false, message: error.message }; + } +} async getAllReviews(){ let AllReviews = await this.prisma.review.findMany(); return { @@ -63,6 +80,56 @@ export class ReviewService { } - + async deleteReview(id: number, userId: number): Promise { + // Check if review with given id exists + const review = await this.prisma.review.findUnique({ where: { id } }); + if (!review) { + throw new NotFoundException(`Review with id ${id} not found`); + } + + // Check if the user is the author of the review + if (review.authorId !== userId) { + throw new ForbiddenException('You are not authorized to delete this review'); + } + + // Delete the review + return this.prisma.review.delete({ where: { id } }); + } + + + async getAverageRating(hotelId:number):Promise { + const reviews = await this.prisma.review.findMany({ + where: { + hotelId: hotelId, + }, + select: { + rating: true, + }, + }); + if (!reviews){ + return 0; + } + + const numReviews = reviews.length; + const sumRatings = reviews.reduce((acc, review) => acc + review.rating, 0); + + if (numReviews === 0) { + return 0; + } else { + const averageRating = sumRatings / numReviews; + await this.prisma.hotel.update({ + where: { id: hotelId }, + data: { averageRating: averageRating }, + }); + return averageRating; + } + } + + async getAllHotels(){ + return this.prisma.hotel.findMany() + } } + + + diff --git a/views/front.hbs b/views/front.hbs index 302065c..c9d812c 100644 --- a/views/front.hbs +++ b/views/front.hbs @@ -1,44 +1,83 @@ + - - - - - - - - - - - - Maderia - - -
-
- + + + +
+ + + + + + + "> + + + -
-

Find your next stay

-

- search low price on hotels, homes and much more... -

-
- -
- - \ No newline at end of file