From b21cf8995ef16e7006fc9a17f1297a5452164968 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 6 May 2024 00:02:50 +0700 Subject: [PATCH] feat: messages --- apps/backend/src/api/api.module.ts | 4 +- .../src/api/routes/marketplace.controller.ts | 33 ++- .../src/api/routes/messages.controller.ts | 34 +++ .../src/api/routes/users.controller.ts | 14 +- .../src/app/(site)/messages/[id]/page.tsx | 14 ++ .../src/app/(site)/messages/layout.tsx | 9 + .../frontend/src/app/(site)/messages/page.tsx | 14 ++ .../components/layout/settings.component.tsx | 59 ++++- .../src/components/layout/top.menu.tsx | 7 +- .../src/components/marketplace/buyer.tsx | 215 +++++++++++++++- .../src/components/marketplace/seller.tsx | 80 +++++- .../src/components/messages/layout.tsx | 96 ++++++++ .../src/components/messages/messages.tsx | 229 ++++++++++++++++++ .../src/database/prisma/database.module.ts | 4 + .../prisma/marketplace/messages.repository.ts | 155 ++++++++++++ .../prisma/marketplace/messages.service.ts | 25 ++ .../src/database/prisma/schema.prisma | 40 ++- .../database/prisma/users/users.repository.ts | 55 ++++- .../database/prisma/users/users.service.ts | 10 + .../src/dtos/marketplace/audience.dto.ts | 8 + .../dtos/marketplace/new.conversation.dto.ts | 10 + .../src/dtos/messages/add.message.ts | 7 + .../src/dtos/users/user.details.dto.ts | 16 ++ 23 files changed, 1090 insertions(+), 48 deletions(-) create mode 100644 apps/backend/src/api/routes/messages.controller.ts create mode 100644 apps/frontend/src/app/(site)/messages/[id]/page.tsx create mode 100644 apps/frontend/src/app/(site)/messages/layout.tsx create mode 100644 apps/frontend/src/app/(site)/messages/page.tsx create mode 100644 apps/frontend/src/components/messages/layout.tsx create mode 100644 apps/frontend/src/components/messages/messages.tsx create mode 100644 libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts create mode 100644 libraries/nestjs-libraries/src/database/prisma/marketplace/messages.service.ts create mode 100644 libraries/nestjs-libraries/src/dtos/marketplace/audience.dto.ts create mode 100644 libraries/nestjs-libraries/src/dtos/marketplace/new.conversation.dto.ts create mode 100644 libraries/nestjs-libraries/src/dtos/messages/add.message.ts create mode 100644 libraries/nestjs-libraries/src/dtos/users/user.details.dto.ts diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index 6104d9ee..36d45276 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -21,6 +21,7 @@ import { CommentsController } from '@gitroom/backend/api/routes/comments.control import { BillingController } from '@gitroom/backend/api/routes/billing.controller'; import { NotificationsController } from '@gitroom/backend/api/routes/notifications.controller'; import { MarketplaceController } from '@gitroom/backend/api/routes/marketplace.controller'; +import { MessagesController } from '@gitroom/backend/api/routes/messages.controller'; const authenticatedController = [ UsersController, @@ -32,7 +33,8 @@ const authenticatedController = [ CommentsController, BillingController, NotificationsController, - MarketplaceController + MarketplaceController, + MessagesController ]; @Module({ imports: [ diff --git a/apps/backend/src/api/routes/marketplace.controller.ts b/apps/backend/src/api/routes/marketplace.controller.ts index 62342ed4..b2d07b99 100644 --- a/apps/backend/src/api/routes/marketplace.controller.ts +++ b/apps/backend/src/api/routes/marketplace.controller.ts @@ -9,6 +9,9 @@ import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/us import { ChangeActiveDto } from '@gitroom/nestjs-libraries/dtos/marketplace/change.active.dto'; import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; +import { AudienceDto } from '@gitroom/nestjs-libraries/dtos/marketplace/audience.dto'; +import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto'; +import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service'; @ApiTags('Marketplace') @Controller('/marketplace') @@ -16,7 +19,8 @@ export class MarketplaceController { constructor( private _itemUserService: ItemUserService, private _stripeService: StripeService, - private _userService: UsersService + private _userService: UsersService, + private _messagesService: MessagesService ) {} @Post('/') @@ -25,7 +29,19 @@ export class MarketplaceController { @GetUserFromRequest() user: User, @Body() body: ItemsDto ) { - return this._userService.getMarketplacePeople(organization.id, user.id, body); + return this._userService.getMarketplacePeople( + organization.id, + user.id, + body + ); + } + + @Post('/conversation') + createConversation( + @GetUserFromRequest() user: User, + @Body() body: NewConversationDto + ) { + return this._messagesService.createConversation(user.id, body); } @Get('/bank') @@ -49,6 +65,14 @@ export class MarketplaceController { await this._userService.changeMarketplaceActive(user.id, body.active); } + @Post('/audience') + async changeAudience( + @GetUserFromRequest() user: User, + @Body() body: AudienceDto + ) { + await this._userService.changeAudienceSize(user.id, body.audience); + } + @Get('/item') async getItems(@GetUserFromRequest() user: User) { return this._itemUserService.getItems(user.id); @@ -56,12 +80,15 @@ export class MarketplaceController { @Get('/account') async getAccount(@GetUserFromRequest() user: User) { - const { account, marketplace, connectedAccount } = + const { account, marketplace, connectedAccount, name, picture, audience } = await this._userService.getUserByEmail(user.email); return { account, marketplace, connectedAccount, + fullname: name, + audience, + picture, }; } } diff --git a/apps/backend/src/api/routes/messages.controller.ts b/apps/backend/src/api/routes/messages.controller.ts new file mode 100644 index 00000000..c028b217 --- /dev/null +++ b/apps/backend/src/api/routes/messages.controller.ts @@ -0,0 +1,34 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service'; +import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; +import { User } from '@prisma/client'; +import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message'; + +@ApiTags('Messages') +@Controller('/messages') +export class MessagesController { + constructor(private _messagesService: MessagesService) {} + + @Get('/') + getMessagesGroup(@GetUserFromRequest() user: User) { + return this._messagesService.getMessagesGroup(user.id); + } + + @Get('/:groupId/:page') + getMessages( + @GetUserFromRequest() user: User, + @Param('groupId') groupId: string, + @Param('page') page: string + ) { + return this._messagesService.getMessages(user.id, groupId, +page); + } + @Post('/:groupId') + createMessage( + @GetUserFromRequest() user: User, + @Param('groupId') groupId: string, + @Body() message: AddMessageDto + ) { + return this._messagesService.createMessage(user.id, groupId, message); + } +} diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index fdce646b..457af7e4 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -24,6 +24,7 @@ import { removeSubdomain } from '@gitroom/helpers/subdomain/subdomain.management import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; import { ApiTags } from '@nestjs/swagger'; import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; +import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; @ApiTags('User') @Controller('/user') @@ -57,13 +58,18 @@ export class UsersController { } @Get('/personal') - async getPersonal( - @GetUserFromRequest() user: User, - ) { - + async getPersonal(@GetUserFromRequest() user: User) { return this._userService.getPersonal(user.id); } + @Post('/personal') + async changePersonal( + @GetUserFromRequest() user: User, + @Body() body: UserDetailDto + ) { + return this._userService.changePersonal(user.id, body); + } + @Get('/subscription') @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) async getSubscription(@GetOrgFromRequest() organization: Organization) { diff --git a/apps/frontend/src/app/(site)/messages/[id]/page.tsx b/apps/frontend/src/app/(site)/messages/[id]/page.tsx new file mode 100644 index 00000000..f4d65941 --- /dev/null +++ b/apps/frontend/src/app/(site)/messages/[id]/page.tsx @@ -0,0 +1,14 @@ +import { Messages } from '@gitroom/frontend/components/messages/messages'; + +export const dynamic = 'force-dynamic'; + +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Gitroom Messages', + description: '', +}; + +export default async function Index() { + return ; +} diff --git a/apps/frontend/src/app/(site)/messages/layout.tsx b/apps/frontend/src/app/(site)/messages/layout.tsx new file mode 100644 index 00000000..5328067e --- /dev/null +++ b/apps/frontend/src/app/(site)/messages/layout.tsx @@ -0,0 +1,9 @@ +import { Layout } from '@gitroom/frontend/components/messages/layout'; +export const dynamic = 'force-dynamic'; +import { ReactNode } from 'react'; + +export default async function LayoutWrapper({children}: {children: ReactNode}) { + return ( + + ); +} diff --git a/apps/frontend/src/app/(site)/messages/page.tsx b/apps/frontend/src/app/(site)/messages/page.tsx new file mode 100644 index 00000000..7e1673ba --- /dev/null +++ b/apps/frontend/src/app/(site)/messages/page.tsx @@ -0,0 +1,14 @@ +export const dynamic = 'force-dynamic'; + +import {Metadata} from "next"; + +export const metadata: Metadata = { + title: 'Gitroom Messages', + description: '', +} + +export default async function Index() { + return ( +
asd
+ ); +} diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index 98f84904..1e560cd7 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -1,21 +1,32 @@ import { useModals } from '@mantine/modals'; -import React, { FC, useCallback, useEffect } from 'react'; +import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { Input } from '@gitroom/react/form/input'; import { Button } from '@gitroom/react/form/button'; import { Textarea } from '@gitroom/react/form/textarea'; import { FormProvider, useForm } from 'react-hook-form'; import { showMediaBox } from '@gitroom/frontend/components/media/media.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { classValidatorResolver } from '@hookform/resolvers/class-validator'; +import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { useSWRConfig } from 'swr'; const SettingsPopup: FC = () => { const fetch = useFetch(); - const form = useForm({}); + const toast = useToaster(); + const swr = useSWRConfig(); + + const resolver = useMemo(() => { + return classValidatorResolver(UserDetailDto); + }, []); + const form = useForm({ resolver }); + const picture = form.watch('picture'); const modal = useModals(); const close = useCallback(() => { return modal.closeAll(); }, []); - const loadProfile = useCallback( async() => { + const loadProfile = useCallback(async () => { const personal = await (await fetch('/user/personal')).json(); form.setValue('fullname', personal.name || ''); form.setValue('bio', personal.bio || ''); @@ -24,12 +35,22 @@ const SettingsPopup: FC = () => { const openMedia = useCallback(() => { showMediaBox((values) => { - console.log(values); - }) + form.setValue('picture', values); + }); }, []); - const submit = useCallback((val: any) => { - console.log(val); + const remove = useCallback(() => { + form.setValue('picture', null); + }, []); + + const submit = useCallback(async (val: any) => { + await fetch('/user/personal', { + method: 'POST', + body: JSON.stringify(val), + }); + toast.show('Profile updated'); + swr.mutate('/marketplace/account'); + close(); }, []); useEffect(() => { @@ -73,11 +94,19 @@ const SettingsPopup: FC = () => {
-
+
+ {!!picture?.path && ( + profile + )} +
Profile Picture
- -
@@ -148,7 +181,7 @@ export const SettingsComponent = () => { viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" - className="cursor-pointer" + className="cursor-pointer relative z-[200]" onClick={openModal} > { @@ -64,7 +69,7 @@ export const TopMenu: FC = () => { } return true; }) - .map((p) => path.indexOf(p.path) > -1 ? index : -1) + .map((p) => (path.indexOf(p.path) > -1 ? index : -1)) .indexOf(index) === index ? 'text-primary showbox' : 'text-gray' diff --git a/apps/frontend/src/components/marketplace/buyer.tsx b/apps/frontend/src/components/marketplace/buyer.tsx index 1980f23d..3da792b2 100644 --- a/apps/frontend/src/components/marketplace/buyer.tsx +++ b/apps/frontend/src/components/marketplace/buyer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { +import React, { FC, Fragment, useCallback, @@ -21,6 +21,12 @@ import { import { chunk, fill } from 'lodash'; import useSWR from 'swr'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useModals } from '@mantine/modals'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { Textarea } from '@gitroom/react/form/textarea'; +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; +import { classValidatorResolver } from '@hookform/resolvers/class-validator'; +import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto'; export interface Root { list: List[]; @@ -28,7 +34,14 @@ export interface Root { } export interface List { + id: string; name: any; + bio: string; + audience: number; + picture: { + id: string; + path: string; + }; organizations: Organization[]; items: Item[]; } @@ -86,6 +99,100 @@ export const LabelCheckbox: FC<{ ); }; +const Pagination: FC<{ results: number }> = (props) => { + const { results } = props; + const router = useRouter(); + const search = useSearchParams(); + const page = +(parseInt(search.get('page')!) || 1) - 1; + const from = page * 8; + const to = (page + 1) * 8; + const pagesArray = useMemo(() => { + return Array.from({ length: Math.ceil(results / 8) }, (_, i) => i + 1); + }, [results]); + + const changePage = useCallback( + (newPage: number) => () => { + const params = new URLSearchParams(window.location.search); + params.set('page', String(newPage)); + router.replace('?' + params.toString(), { + scroll: true, + }); + }, + [page] + ); + + if (results < 8) { + return null; + } + return ( +
+
+ Showing {from + 1} to {to > results ? results : to} from {results}{' '} + Results +
+
+ {page > 0 && ( +
+ + + + + + + + + + +
+ )} + {pagesArray.map((p) => ( +
+ {p} +
+ ))} + {page + 1 < pagesArray[pagesArray.length - 1] && ( + + + + )} +
+
+ ); +}; + export const Options: FC<{ title: string; options: Array<{ key: string; value: string }>; @@ -174,10 +281,82 @@ export const Options: FC<{ ); }; +export const RequestService: FC<{ toId: string; name: string }> = (props) => { + const { toId, name } = props; + const router = useRouter(); + const fetch = useFetch(); + const modal = useModals(); + const resolver = useMemo(() => { + return classValidatorResolver(NewConversationDto); + }, []); + + const form = useForm({ resolver, values: { to: toId, message: '' } }); + const close = useCallback(() => { + return modal.closeAll(); + }, []); + + const createConversation: SubmitHandler = useCallback(async (data) => { + const {id} = await (await fetch('/marketplace/conversation', { + method: 'POST', + body: JSON.stringify(data), + })).json(); + close(); + router.push(`/messages/${id}`); + }, []); + + return ( +
+ +
+ +
+ +