From 93be66d003184658dc31e6e448f4426fd9d824da Mon Sep 17 00:00:00 2001 From: Radek <104318242+radekm2000@users.noreply.github.com> Date: Fri, 24 May 2024 19:54:24 +0200 Subject: [PATCH] add review command and make product photo larger using inventory command --- .../src/components/DisplayUserProducts.tsx | 22 ++-- .../ProductPage/DisplayUserInfo.tsx | 14 +-- .../components/ratingSystem/ReviewForm.tsx | 15 +-- .../src/discord-bot/discord-bot.service.ts | 2 + .../src/discord-bot/src/commands/inventory.ts | 2 +- .../src/discord-bot/src/commands/reviews.ts | 105 ++++++++++++++++++ server/ecommerce/src/users/users.service.ts | 19 ++++ .../ecommerce/src/utils/dtos/product.dto.ts | 5 +- 8 files changed, 145 insertions(+), 39 deletions(-) create mode 100644 server/ecommerce/src/discord-bot/src/commands/reviews.ts diff --git a/client/ecommerce/src/components/DisplayUserProducts.tsx b/client/ecommerce/src/components/DisplayUserProducts.tsx index 8dfcba3..e45a624 100644 --- a/client/ecommerce/src/components/DisplayUserProducts.tsx +++ b/client/ecommerce/src/components/DisplayUserProducts.tsx @@ -13,6 +13,7 @@ import { ProductWithImageAndUser } from "../types/types"; import { useMediaQuery } from "../hooks/useMediaQuery"; import { Link, useLocation } from "wouter"; import { calculateGridWidth } from "../utils/calculateGridWidth"; +import { RenderAvatar } from "./RenderAvatar"; export const DisplayUserProducts = ({ products, @@ -92,21 +93,12 @@ export const DisplayUserProducts = ({ cursor: "pointer", }} > - {product.user.avatar ? ( - - ) : ( - - )} + + - {user?.avatar ? ( - - ) : ( - - )} + + {user?.username} diff --git a/client/ecommerce/src/components/ratingSystem/ReviewForm.tsx b/client/ecommerce/src/components/ratingSystem/ReviewForm.tsx index 339dfe2..5b5db9a 100644 --- a/client/ecommerce/src/components/ratingSystem/ReviewForm.tsx +++ b/client/ecommerce/src/components/ratingSystem/ReviewForm.tsx @@ -11,9 +11,10 @@ import { import { UserWithAvatar } from "../../types/types"; import { AccountCircle } from "@mui/icons-material"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; -import { useState } from "react"; +import { useState } from "react"; import { useAddReview } from "../../hooks/useAddReview"; import { useMediaQuery } from "../../hooks/useMediaQuery"; +import { RenderAvatar } from "../RenderAvatar"; type FormFields = { rating: number; @@ -74,16 +75,8 @@ export const ReviewForm = ({ user }: { user: UserWithAvatar }) => { alignItems: "center", }} > - {user.avatar ? ( - - ) : ( - - )} + + {user.username} diff --git a/server/ecommerce/src/discord-bot/discord-bot.service.ts b/server/ecommerce/src/discord-bot/discord-bot.service.ts index 6b5c2b9..b2cb8db 100644 --- a/server/ecommerce/src/discord-bot/discord-bot.service.ts +++ b/server/ecommerce/src/discord-bot/discord-bot.service.ts @@ -7,6 +7,7 @@ import { ProfileCommand } from './src/commands/profile'; import { UsersService } from 'src/users/users.service'; import { InventoryCommand } from './src/commands/inventory'; import { ProductsService } from 'src/products/products.service'; +import { ReviewsCommand } from './src/commands/reviews'; @Injectable() export class DiscordBotService implements OnModuleInit { @@ -42,6 +43,7 @@ export class DiscordBotService implements OnModuleInit { productsService: this.productsService, usersService: this.userService, }), + new ReviewsCommand({ usersService: this.userService }), ], }); } diff --git a/server/ecommerce/src/discord-bot/src/commands/inventory.ts b/server/ecommerce/src/discord-bot/src/commands/inventory.ts index e1474c2..d43e6f2 100644 --- a/server/ecommerce/src/discord-bot/src/commands/inventory.ts +++ b/server/ecommerce/src/discord-bot/src/commands/inventory.ts @@ -60,7 +60,7 @@ export class InventoryCommand implements SlashCommand { { name: 'Brand', value: product.brand, inline: true }, { name: 'Category', value: product.category, inline: true }, ]) - .setThumbnail(productImage); + .setImage(productImage); }); return [headerEmbed, ...productEmbeds]; diff --git a/server/ecommerce/src/discord-bot/src/commands/reviews.ts b/server/ecommerce/src/discord-bot/src/commands/reviews.ts new file mode 100644 index 0000000..74ed46e --- /dev/null +++ b/server/ecommerce/src/discord-bot/src/commands/reviews.ts @@ -0,0 +1,105 @@ +import { + ChatInputCommandInteraction, + EmbedBuilder, + SlashCommandBuilder, +} from 'discord.js'; +import { SlashCommand } from './slash-command'; +import { UsersService } from 'src/users/users.service'; +import { Review } from 'src/utils/entities/review.entity'; +import { DiscordEmbedColors } from '../discord-embeds/colors'; + +type Config = { + usersService: UsersService; +}; + +export class ReviewsCommand implements SlashCommand { + private readonly usersService: UsersService; + + public readonly config = new SlashCommandBuilder() + .setName('reviews') + .setDescription('Displays reviews that are listed on your profile'); + + constructor(config: Config) { + this.usersService = config.usersService; + } + + public execute = async (interaction: ChatInputCommandInteraction) => { + const userId = interaction.user.id; + const userAvatar = interaction.user.avatarURL(); + + await interaction.deferReply({ ephemeral: true }); + + const user = await this.usersService.getUserWithReviews(userId); + + const totalReviews = this.calculateTotalReviews(user.reviews); + const averageRating = this.calculateAverageReviewsRating(user.reviews); + + if (totalReviews === 0) { + await interaction.editReply('You have no reviews yet'); + } + + const embed = await this.createEmbed( + totalReviews, + averageRating, + user.reviews, + userAvatar, + ); + + await interaction.editReply({ embeds: [embed] }); + }; + + private createEmbed = async ( + totalReviews: number, + averageRating: number, + reviews: Review[], + userAvatar: string, + ) => { + const embed = new EmbedBuilder() + .setTitle( + `You have a total of ${totalReviews} reviews with an average rating of ⭐ ${averageRating.toFixed( + 2, + )}`, + ) + .setColor(DiscordEmbedColors.default) + .setThumbnail(userAvatar); + + reviews.forEach((review) => { + embed.addFields([ + { + name: 'Reviewer', + value: review.reviewCreator.username, + inline: true, + }, + { + name: 'Rating', + value: `⭐ ${review.rating.toString()}`, + inline: true, + }, + { name: 'Comment', value: review.comment }, + ]); + }); + + return embed; + }; + + private calculateTotalReviews = (reviews: Review[]) => { + return reviews.length; + }; + + private calculateAverageReviewsRating = (reviews: Review[]): number => { + if (reviews.length === 0) { + return 0; + } + + const ratings = reviews.map((review) => review.rating); + const sortedRatings = ratings.sort((a, b) => a - b); + + const middleIndex = Math.floor(sortedRatings.length / 2); + + if (sortedRatings.length % 2 === 0) { + return (sortedRatings[middleIndex - 1] + sortedRatings[middleIndex]) / 2; + } else { + return sortedRatings[middleIndex]; + } + }; +} diff --git a/server/ecommerce/src/users/users.service.ts b/server/ecommerce/src/users/users.service.ts index 2fa3b58..ab5cddf 100644 --- a/server/ecommerce/src/users/users.service.ts +++ b/server/ecommerce/src/users/users.service.ts @@ -137,6 +137,25 @@ export class UsersService { return user; } + public getUserWithReviews = async (discordId: string) => { + const user = await this.usersRepository.findOne({ + where: { + discordId, + }, + relations: { + reviews: { + reviewCreator: true, + }, + avatarEntity: true, + }, + }); + if (!user) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return user; + }; + public findUserByDiscordId = async (discordId: string) => { const user = await this.usersRepository.findOne({ where: { diff --git a/server/ecommerce/src/utils/dtos/product.dto.ts b/server/ecommerce/src/utils/dtos/product.dto.ts index 1bc06cf..3a4437f 100644 --- a/server/ecommerce/src/utils/dtos/product.dto.ts +++ b/server/ecommerce/src/utils/dtos/product.dto.ts @@ -41,9 +41,10 @@ const User = z.object({ id: z.number(), username: z.string(), googleId: z.string().or(z.null()), - email: z.string(), - role: z.enum(['user', 'admin']), + email: z.string().or(z.null()), + role: z.enum(['user', 'admin', 'discordUser']), avatar: z.string().or(z.null()), + discordId: z.string().or(z.null()), }); export const ProductWithImageAndUserSchema = z.object({