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 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 (
+
+ );
+};
+
export const Card: FC<{
data: List;
}> = (props) => {
const { data } = props;
+ const modal = useModals();
const tags = useMemo(() => {
return data.items
@@ -187,6 +366,17 @@ export const Card: FC<{
});
}, [data]);
+ const requestService = useCallback(() => {
+ modal.openModal({
+ children: ,
+ classNames: {
+ modal: 'bg-transparent text-white',
+ },
+ withCloseButton: false,
+ size: '100%',
+ });
+ }, []);
+
const identifier = useMemo(() => {
return [
...new Set(
@@ -202,10 +392,12 @@ export const Card: FC<{
-

+ {data?.picture?.path && (
+

+ )}
-
22,6K
+
{data?.audience}
@@ -261,8 +453,7 @@ export const Card: FC<{
- Maecenas dignissim justo eget nulla rutrum molestie. Maecenas
- lobortis sem dui,
+ {data.bio || 'No bio'}
- Request Service
+ Request Service
);
@@ -304,13 +495,16 @@ export const Buyer = () => {
method: 'POST',
body: JSON.stringify({
items: services?.split(',').filter((f) => f) || [],
- page,
+ page: page === 0 ? 1 : page,
}),
})
).json();
}, [services, page]);
useEffect(() => {
+ if (!services) {
+ return;
+ }
const params = new URLSearchParams(window.location.search);
params.set('page', '1');
router.replace('?' + params.toString());
@@ -339,6 +533,7 @@ export const Buyer = () => {
{list?.list?.map((item, index) => (
))}
+
);
diff --git a/apps/frontend/src/components/marketplace/seller.tsx b/apps/frontend/src/components/marketplace/seller.tsx
index ead65265..b7c37120 100644
--- a/apps/frontend/src/components/marketplace/seller.tsx
+++ b/apps/frontend/src/components/marketplace/seller.tsx
@@ -4,9 +4,11 @@ import { Slider } from '@gitroom/react/form/slider';
import { Button } from '@gitroom/react/form/button';
import { tagsList } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list';
import { Options } from '@gitroom/frontend/components/marketplace/buyer';
-import { useCallback, useEffect, useState } from 'react';
+import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
+import { Input } from '@gitroom/react/form/input';
+import { useDebouncedCallback } from 'use-debounce';
export const Seller = () => {
const fetch = useFetch();
@@ -16,6 +18,7 @@ export const Seller = () => {
>([]);
const [connectedLoading, setConnectedLoading] = useState(false);
const [state, setState] = useState(true);
+ const [audience, setAudience] = useState(0);
const accountInformation = useCallback(async () => {
const account = await (
@@ -25,6 +28,7 @@ export const Seller = () => {
).json();
setState(account.marketplace);
+ setAudience(account.audience);
return account;
}, []);
@@ -60,15 +64,36 @@ export const Seller = () => {
setLoading(false);
}, []);
- const changeMarketplace = useCallback(async (value: string) => {
- await fetch('/marketplace/active', {
- method: 'POST',
- body: JSON.stringify({
- active: value === 'on',
- }),
- });
- setState(!state);
- }, [state]);
+ const changeAudienceBackend = useDebouncedCallback(
+ useCallback(async (aud: number) => {
+ fetch('/marketplace/audience', {
+ method: 'POST',
+ body: JSON.stringify({
+ audience: aud,
+ }),
+ });
+ }, []),
+ 500
+ );
+
+ const changeAudience = useCallback((e: ChangeEvent) => {
+ const num = String(+e.target.value.replace(/\D/g, '') || 0).slice(0, 8);
+ setAudience(+num);
+ changeAudienceBackend(+num);
+ }, []);
+
+ const changeMarketplace = useCallback(
+ async (value: string) => {
+ await fetch('/marketplace/active', {
+ method: 'POST',
+ body: JSON.stringify({
+ active: value === 'on',
+ }),
+ });
+ setState(!state);
+ },
+ [state]
+ );
const { data } = useSWR('/marketplace/account', accountInformation);
@@ -85,11 +110,23 @@ export const Seller = () => {
Seller Mode
-
-
John Smith
+
+ {!!data?.picture?.path && (
+

+ )}
+
+
{data?.fullname || ''}
{data?.connectedAccount && (
)}
@@ -119,6 +156,23 @@ export const Seller = () => {
title={tag.name}
/>
))}
+
+ Audience Size
+
+
diff --git a/apps/frontend/src/components/messages/layout.tsx b/apps/frontend/src/components/messages/layout.tsx
new file mode 100644
index 00000000..e1208de4
--- /dev/null
+++ b/apps/frontend/src/components/messages/layout.tsx
@@ -0,0 +1,96 @@
+'use client';
+
+import { FC, ReactNode, useCallback } from 'react';
+import clsx from 'clsx';
+import useSWR from 'swr';
+import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
+import { useParams, useRouter } from 'next/navigation';
+
+export interface Root2 {
+ id: string;
+ buyerId: string;
+ sellerId: string;
+ createdAt: string;
+ updatedAt: string;
+ seller: Seller;
+ messages: Message[];
+}
+
+export interface Seller {
+ name: any;
+ picture: Picture;
+}
+
+export interface Picture {
+ id: string;
+ path: string;
+}
+
+export interface Message {
+ id: string;
+ from: string;
+ content: string;
+ groupId: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: any;
+}
+
+const Card: FC<{ message: Root2 }> = (props) => {
+ const { message } = props;
+ const path = useParams();
+ const router = useRouter();
+
+ const changeConversation = useCallback(() => {
+ router.push(`/messages/${message.id}`);
+ }, []);
+
+ return (
+
+
+ {message?.seller?.picture?.path && (
+

+ )}
+
+
+
+
{message.seller.name || 'Noname'}
+
+ {message.messages[0]?.content}
+
+
+
+
Mar 28
+
+ );
+};
+export const Layout: FC<{ renderChildren: ReactNode }> = (props) => {
+ const { renderChildren } = props;
+ const fetch = useFetch();
+
+ const loadMessagesGroup = useCallback(async () => {
+ return await (await fetch('/messages')).json();
+ }, []);
+
+ const messagesGroup = useSWR('messagesGroup', loadMessagesGroup);
+
+ return (
+
+
+
All Messages
+
+ {messagesGroup.data?.map((message) => (
+
+ ))}
+
+
+
{renderChildren}
+
+ );
+};
diff --git a/apps/frontend/src/components/messages/messages.tsx b/apps/frontend/src/components/messages/messages.tsx
new file mode 100644
index 00000000..54a8f51e
--- /dev/null
+++ b/apps/frontend/src/components/messages/messages.tsx
@@ -0,0 +1,229 @@
+'use client';
+
+import dayjs from 'dayjs';
+
+export interface Root {
+ id: string;
+ buyerId: string;
+ sellerId: string;
+ createdAt: string;
+ updatedAt: string;
+ seller: SellerBuyer;
+ buyer: SellerBuyer;
+ messages: Message[];
+}
+
+export interface SellerBuyer {
+ name?: string;
+ picture: Picture;
+}
+
+export interface Picture {
+ id: string;
+ path: string;
+}
+
+export interface Message {
+ id: string;
+ from: string;
+ content: string;
+ groupId: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: any;
+}
+
+import { Textarea } from '@gitroom/react/form/textarea';
+import interClass from '@gitroom/react/helpers/inter.font';
+import clsx from 'clsx';
+import useSWR from 'swr';
+import { FC, UIEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useParams } from 'next/navigation';
+import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
+import { reverse } from 'lodash';
+import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
+import { classValidatorResolver } from '@hookform/resolvers/class-validator';
+import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
+import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
+
+export const Message: FC<{
+ message: Message;
+ seller: SellerBuyer;
+ buyer: SellerBuyer;
+ scrollDown: () => void;
+}> = (props) => {
+ const { message, seller, buyer, scrollDown } = props;
+ useEffect(() => {
+ scrollDown();
+ }, []);
+ const person = useMemo(() => {
+ return message.from === 'BUYER' ? buyer : seller;
+ }, [message]);
+
+ const isMe = useMemo(() => {
+ return message.from === 'BUYER';
+ }, []);
+
+ const time = useMemo(() => {
+ return dayjs(message.createdAt).format('h:mm A');
+ }, [message]);
+ return (
+
+
+
+ {!!person.picture?.path && (
+

+ )}
+
+
+
+
+
{isMe ? 'Me' : person.name}
+
+
{time}
+
+
+ {message.content}
+
+
+
+ );
+};
+
+const Page: FC<{ page: number; group: string; refChange: any }> = (props) => {
+ const { page, group, refChange } = props;
+ const fetch = useFetch();
+
+ const loadMessages = useCallback(async () => {
+ return await (await fetch(`/messages/${group}/${page}`)).json();
+ }, []);
+
+ const { data, mutate } = useSWR(`load-${page}-${group}`, loadMessages);
+
+ const scrollDown = useCallback(() => {
+ if (page > 1) {
+ return ;
+ }
+ // @ts-ignore
+ refChange.current?.scrollTo(0, refChange.current.scrollHeight);
+ }, [refChange]);
+
+ const messages = useMemo(() => {
+ return reverse([...(data?.messages || [])]);
+ }, [data]);
+
+ return (
+ <>
+ {messages.map((message) => (
+
+ ))}
+ >
+ );
+};
+
+export const Messages = () => {
+ const [pages, setPages] = useState([makeId(3)]);
+ const params = useParams();
+ const fetch = useFetch();
+ const ref = useRef(null);
+ const resolver = useMemo(() => {
+ return classValidatorResolver(AddMessageDto);
+ }, []);
+
+ const form = useForm({ resolver, values: { message: '' } });
+ useEffect(() => {
+ setPages([makeId(3)]);
+ }, [params.id]);
+
+ const loadMessages = useCallback(async () => {
+ return await (await fetch(`/messages/${params.id}/1`)).json();
+ }, []);
+
+ const { data, mutate, isLoading } = useSWR(`load-1-${params.id}`, loadMessages);
+
+ const submit: SubmitHandler = useCallback(async (values) => {
+ await fetch(`/messages/${params.id}`, {
+ method: 'POST',
+ body: JSON.stringify(values),
+ });
+ mutate();
+ form.reset();
+ }, []);
+
+ const changeScroll: UIEventHandler = useCallback((e) => {
+ // @ts-ignore
+ if (e.target.scrollTop === 0) {
+ // @ts-ignore
+ e.target.scrollTop = 1;
+ setPages((prev) => [...prev, makeId(3)]);
+ }
+ }, [pages, setPages]);
+
+ return (
+
+ );
+};
diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
index 402e72a0..43c20396 100644
--- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
@@ -22,6 +22,8 @@ import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/pris
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
import { ItemUserRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.repository';
import { ItemUserService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.service';
+import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
+import { MessagesRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.repository';
@Global()
@Module({
@@ -44,11 +46,13 @@ import { ItemUserService } from '@gitroom/nestjs-libraries/database/prisma/marke
IntegrationRepository,
PostsService,
PostsRepository,
+ MessagesRepository,
MediaService,
MediaRepository,
CommentsRepository,
ItemUserRepository,
ItemUserService,
+ MessagesService,
CommentsService,
IntegrationManager,
EmailService,
diff --git a/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts b/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts
new file mode 100644
index 00000000..3702310d
--- /dev/null
+++ b/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts
@@ -0,0 +1,155 @@
+import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
+import { Injectable } from '@nestjs/common';
+import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
+import { From } from '@prisma/client';
+import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
+
+@Injectable()
+export class MessagesRepository {
+ constructor(
+ private _messagesGroup: PrismaRepository<'messagesGroup'>,
+ private _messages: PrismaRepository<'messages'>
+ ) {}
+
+ async createConversation(userId: string, body: NewConversationDto) {
+ const { id } =
+ (await this._messagesGroup.model.messagesGroup.findFirst({
+ where: {
+ buyerId: userId,
+ sellerId: body.to,
+ },
+ })) ||
+ (await this._messagesGroup.model.messagesGroup.create({
+ data: {
+ buyerId: userId,
+ sellerId: body.to,
+ },
+ }));
+
+ await this._messagesGroup.model.messagesGroup.update({
+ where: {
+ id,
+ },
+ data: {
+ updatedAt: new Date(),
+ },
+ });
+
+ await this._messages.model.messages.create({
+ data: {
+ groupId: id,
+ from: From.BUYER,
+ content: body.message,
+ },
+ });
+
+ return { id };
+ }
+
+ async getMessagesGroup(userId: string) {
+ return this._messagesGroup.model.messagesGroup.findMany({
+ where: {
+ buyerId: userId,
+ },
+ orderBy: {
+ updatedAt: 'desc',
+ },
+ include: {
+ seller: {
+ select: {
+ name: true,
+ picture: {
+ select: {
+ id: true,
+ path: true,
+ },
+ },
+ },
+ },
+ messages: {
+ orderBy: {
+ createdAt: 'desc',
+ },
+ take: 1,
+ },
+ },
+ });
+ }
+
+ async createMessage(userId: string, groupId: string, body: AddMessageDto) {
+ const group = await this._messagesGroup.model.messagesGroup.findFirst({
+ where: {
+ id: groupId,
+ OR: [
+ {
+ buyerId: userId,
+ },
+ {
+ sellerId: userId,
+ },
+ ],
+ },
+ });
+
+ if (!group) {
+ throw new Error('Group not found');
+ }
+
+ await this._messages.model.messages.create({
+ data: {
+ groupId,
+ from: group.buyerId === userId ? From.BUYER : From.SELLER,
+ content: body.message,
+ },
+ });
+ }
+
+ async getMessages(userId: string, groupId: string, page: number) {
+ return this._messagesGroup.model.messagesGroup.findFirst({
+ where: {
+ id: groupId,
+ OR: [
+ {
+ buyerId: userId,
+ },
+ {
+ sellerId: userId,
+ },
+ ],
+ },
+ include: {
+ seller: {
+ select: {
+ id: true,
+ name: true,
+ picture: {
+ select: {
+ id: true,
+ path: true,
+ },
+ },
+ },
+ },
+ buyer: {
+ select: {
+ id: true,
+ name: true,
+ picture: {
+ select: {
+ id: true,
+ path: true,
+ },
+ },
+ },
+ },
+ messages: {
+ orderBy: {
+ createdAt: 'desc',
+ },
+ take: 10,
+ skip: (page - 1) * 10,
+ },
+ },
+ });
+ }
+}
diff --git a/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.service.ts b/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.service.ts
new file mode 100644
index 00000000..191d472a
--- /dev/null
+++ b/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.service.ts
@@ -0,0 +1,25 @@
+import { Injectable } from '@nestjs/common';
+import { MessagesRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.repository';
+import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
+import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
+
+@Injectable()
+export class MessagesService {
+ constructor(private _messagesRepository: MessagesRepository) {}
+
+ createConversation(userId: string, body: NewConversationDto) {
+ return this._messagesRepository.createConversation(userId, body);
+ }
+
+ getMessagesGroup(userId: string) {
+ return this._messagesRepository.getMessagesGroup(userId);
+ }
+
+ getMessages(userId: string, groupId: string, page: number) {
+ return this._messagesRepository.getMessages(userId, groupId, page);
+ }
+
+ createMessage(userId: string, groupId: string, body: AddMessageDto) {
+ return this._messagesRepository.createMessage(userId, groupId, body);
+ }
+}
diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
index 575d9fa3..0555ecb7 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -35,6 +35,7 @@ model User {
name String?
lastName String?
bio String?
+ audience Int @default(0)
pictureId String?
picture Media? @relation(fields: [pictureId], references: [id])
providerId String?
@@ -48,7 +49,9 @@ model User {
items ItemUser[]
marketplace Boolean @default(true)
account String?
- connectedAccount Boolean @default(false)
+ connectedAccount Boolean @default(false)
+ groupBuyer MessagesGroup[] @relation("groupBuyer")
+ groupSeller MessagesGroup[] @relation("groupSeller")
@@unique([email, providerName])
@@index([lastReadNotifications])
@@ -250,6 +253,41 @@ model Notifications {
@@index([deletedAt])
}
+model MessagesGroup {
+ id String @id @default(uuid())
+ buyerId String
+ buyer User @relation("groupBuyer", fields: [buyerId], references: [id])
+ sellerId String
+ seller User @relation("groupSeller", fields: [sellerId], references: [id])
+ messages Messages[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@unique([buyerId, sellerId])
+ @@index([createdAt])
+ @@index([updatedAt])
+}
+
+model Messages {
+ id String @id @default(uuid())
+ from From
+ content String?
+ groupId String
+ group MessagesGroup @relation(fields: [groupId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+
+ @@index([groupId])
+ @@index([createdAt])
+ @@index([deletedAt])
+}
+
+enum From {
+ BUYER
+ SELLER
+}
+
enum State {
QUEUE
PUBLISHED
diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts
index cc822e26..eb4238ee 100644
--- a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts
@@ -4,6 +4,8 @@ import { Provider } from '@prisma/client';
import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto';
import { allTagsOptions } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list';
+import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
+import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
@Injectable()
export class UsersRepository {
@@ -14,6 +16,14 @@ export class UsersRepository {
where: {
email,
},
+ include: {
+ picture: {
+ select: {
+ id: true,
+ path: true,
+ },
+ },
+ },
});
}
@@ -38,6 +48,17 @@ export class UsersRepository {
});
}
+ changeAudienceSize(userId: string, audience: number) {
+ return this._user.model.user.update({
+ where: {
+ id: userId,
+ },
+ data: {
+ audience,
+ },
+ });
+ }
+
changeMarketplaceActive(userId: string, active: boolean) {
return this._user.model.user.update({
where: {
@@ -70,6 +91,27 @@ export class UsersRepository {
return user;
}
+ async changePersonal(userId: string, body: UserDetailDto) {
+ await this._user.model.user.update({
+ where: {
+ id: userId,
+ },
+ data: {
+ name: body.fullname,
+ bio: body.bio,
+ picture: body.picture
+ ? {
+ connect: {
+ id: body.picture.id,
+ },
+ }
+ : {
+ disconnect: true,
+ },
+ },
+ });
+ }
+
async getMarketplacePeople(orgId: string, userId: string, items: ItemsDto) {
const info = {
id: {
@@ -100,7 +142,16 @@ export class UsersRepository {
...info,
},
select: {
+ id: true,
name: true,
+ bio: true,
+ audience: true,
+ picture: {
+ select: {
+ id: true,
+ path: true,
+ },
+ },
organizations: {
select: {
organization: {
@@ -124,8 +175,8 @@ export class UsersRepository {
},
},
},
- skip: (items.page - 1) * 10,
- take: 10,
+ skip: (items.page - 1) * 8,
+ take: 8,
});
const count = await this._user.model.user.count({
diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts
index 3f183ce4..cac770fc 100644
--- a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts
@@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository';
import { Provider } from '@prisma/client';
import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto';
+import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
+import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
@Injectable()
export class UsersService {
@@ -19,6 +21,10 @@ export class UsersService {
return this._usersRepository.updatePassword(id, password);
}
+ changeAudienceSize(userId: string, audience: number) {
+ return this._usersRepository.changeAudienceSize(userId, audience);
+ }
+
changeMarketplaceActive(userId: string, active: boolean) {
return this._usersRepository.changeMarketplaceActive(userId, active);
}
@@ -30,4 +36,8 @@ export class UsersService {
getPersonal(userId: string) {
return this._usersRepository.getPersonal(userId);
}
+
+ changePersonal(userId: string, body: UserDetailDto) {
+ return this._usersRepository.changePersonal(userId, body);
+ }
}
diff --git a/libraries/nestjs-libraries/src/dtos/marketplace/audience.dto.ts b/libraries/nestjs-libraries/src/dtos/marketplace/audience.dto.ts
new file mode 100644
index 00000000..a28fbaa8
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/marketplace/audience.dto.ts
@@ -0,0 +1,8 @@
+import { IsNumber, Max, Min } from 'class-validator';
+
+export class AudienceDto {
+ @IsNumber()
+ @Max(99999999)
+ @Min(1)
+ audience: number;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/marketplace/new.conversation.dto.ts b/libraries/nestjs-libraries/src/dtos/marketplace/new.conversation.dto.ts
new file mode 100644
index 00000000..924e7797
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/marketplace/new.conversation.dto.ts
@@ -0,0 +1,10 @@
+import { IsString, MinLength } from 'class-validator';
+
+export class NewConversationDto {
+ @IsString()
+ to: string;
+
+ @IsString()
+ @MinLength(50)
+ message: string;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/messages/add.message.ts b/libraries/nestjs-libraries/src/dtos/messages/add.message.ts
new file mode 100644
index 00000000..f3f4d381
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/messages/add.message.ts
@@ -0,0 +1,7 @@
+import { IsString, MinLength } from 'class-validator';
+
+export class AddMessageDto {
+ @IsString()
+ @MinLength(3)
+ message: string;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/users/user.details.dto.ts b/libraries/nestjs-libraries/src/dtos/users/user.details.dto.ts
new file mode 100644
index 00000000..c218ce3e
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/users/user.details.dto.ts
@@ -0,0 +1,16 @@
+import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto';
+import { IsOptional, IsString, MinLength, ValidateNested } from 'class-validator';
+
+export class UserDetailDto {
+ @IsString()
+ @MinLength(3)
+ fullname: string;
+
+ @IsString()
+ @IsOptional()
+ bio: string;
+
+ @IsOptional()
+ @ValidateNested()
+ picture: MediaDto;
+}
\ No newline at end of file