feat: messages
This commit is contained in:
parent
e90b8e8389
commit
b21cf8995e
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 <Messages />;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Layout renderChildren={children} />
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div>asd</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 = () => {
|
|||
<Input label="Full Name" name="fullname" />
|
||||
</div>
|
||||
<div className="flex gap-[8px] mb-[10px]">
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-[#D9D9D9]"></div>
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-[#D9D9D9]">
|
||||
{!!picture?.path && (
|
||||
<img
|
||||
src={picture?.path}
|
||||
alt="profile"
|
||||
className="w-full h-full rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-[2px]">
|
||||
<div className="text-[14px]">Profile Picture</div>
|
||||
<div className="flex gap-[8px]">
|
||||
<button className="h-[24px] w-[120px] bg-[#612AD5] rounded-[4px] flex justify-center gap-[4px] items-center cursor-pointer">
|
||||
<button className="h-[24px] w-[120px] bg-[#612AD5] rounded-[4px] flex justify-center gap-[4px] items-center cursor-pointer" type="button">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -92,9 +121,11 @@ const SettingsPopup: FC = () => {
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px]" onClick={openMedia}>Upload image</div>
|
||||
<div className="text-[12px]" onClick={openMedia}>
|
||||
Upload image
|
||||
</div>
|
||||
</button>
|
||||
<button className="h-[24px] w-[88px] rounded-[4px] border-2 border-[#506490] flex justify-center items-center gap-[4px]">
|
||||
<button className="h-[24px] w-[88px] rounded-[4px] border-2 border-[#506490] flex justify-center items-center gap-[4px]" type="button">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -109,7 +140,9 @@ const SettingsPopup: FC = () => {
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px]">Remove</div>
|
||||
<div className="text-[12px]" onClick={remove}>
|
||||
Remove
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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}
|
||||
>
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ export const menuItems = [
|
|||
icon: 'marketplace',
|
||||
path: '/marketplace',
|
||||
},
|
||||
{
|
||||
name: 'Messages',
|
||||
icon: 'messages',
|
||||
path: '/messages',
|
||||
},
|
||||
];
|
||||
|
||||
export const TopMenu: FC = () => {
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex items-center relative">
|
||||
<div className="absolute left-0">
|
||||
Showing {from + 1} to {to > results ? results : to} from {results}{' '}
|
||||
Results
|
||||
</div>
|
||||
<div className="flex mx-auto">
|
||||
{page > 0 && (
|
||||
<div>
|
||||
<svg
|
||||
width="41"
|
||||
height="40"
|
||||
viewBox="0 0 41 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onClick={changePage(page)}
|
||||
>
|
||||
<g clipPath="url(#clip0_703_22324)">
|
||||
<path
|
||||
d="M22.5 25L17.5 20L22.5 15"
|
||||
stroke="#64748B"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_703_22324">
|
||||
<rect x="0.5" width="40" height="40" rx="8" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{pagesArray.map((p) => (
|
||||
<div
|
||||
key={p}
|
||||
onClick={changePage(p)}
|
||||
className={clsx(
|
||||
'w-[40px] h-[40px] flex justify-center items-center rounded-[8px] cursor-pointer',
|
||||
p === page + 1 ? 'bg-[#8155DD]' : 'text-[#64748B]'
|
||||
)}
|
||||
>
|
||||
{p}
|
||||
</div>
|
||||
))}
|
||||
{page + 1 < pagesArray[pagesArray.length - 1] && (
|
||||
<svg
|
||||
onClick={changePage(page + 2)}
|
||||
width="41"
|
||||
height="40"
|
||||
viewBox="0 0 41 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.5 15L23.5 20L18.5 25"
|
||||
stroke="#64748B"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<NewConversationDto> = useCallback(async (data) => {
|
||||
const {id} = await (await fetch('/marketplace/conversation', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})).json();
|
||||
close();
|
||||
router.push(`/messages/${id}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(createConversation)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="w-full max-w-[920px] mx-auto bg-[#0B101B] px-[16px] rounded-[4px] border border-[#172034] gap-[24px] flex flex-col relative">
|
||||
<button
|
||||
onClick={close}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="text-[18px] font-[500] flex flex-col">
|
||||
<TopTitle title={`Send a message to ${name}`} />
|
||||
<Textarea
|
||||
placeholder="Add a message like: I'm intrested in 3 posts for Linkedin... (min 50 chars)"
|
||||
className="mt-[14px] resize-none h-[400px]"
|
||||
name="message"
|
||||
label=""
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
disabled={!form.formState.isValid}
|
||||
type="submit"
|
||||
className="w-[144px] mb-[16px] rounded-[4px] text-[14px]"
|
||||
>
|
||||
Send Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
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: <RequestService toId={data.id} name={data.name || 'Noname'} />,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-white',
|
||||
},
|
||||
withCloseButton: false,
|
||||
size: '100%',
|
||||
});
|
||||
}, []);
|
||||
|
||||
const identifier = useMemo(() => {
|
||||
return [
|
||||
...new Set(
|
||||
|
|
@ -202,10 +392,12 @@ export const Card: FC<{
|
|||
<div className="flex gap-[16px] flex-1">
|
||||
<div>
|
||||
<div className="h-[103px] w-[103px] bg-red-500/10 rounded-full relative">
|
||||
<img
|
||||
src="https://via.placeholder.com/103"
|
||||
className="rounded-full w-full h-full"
|
||||
/>
|
||||
{data?.picture?.path && (
|
||||
<img
|
||||
src={data?.picture?.path}
|
||||
className="rounded-full w-full h-full"
|
||||
/>
|
||||
)}
|
||||
<div className="w-[80px] h-[28px] bg-[#8155DD] absolute bottom-0 left-[50%] -translate-x-[50%] rounded-[30px] flex gap-[4px] justify-center items-center">
|
||||
<div>
|
||||
<svg
|
||||
|
|
@ -221,7 +413,7 @@ export const Card: FC<{
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[14px]">22,6K</div>
|
||||
<div className="text-[14px]">{data?.audience}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -261,8 +453,7 @@ export const Card: FC<{
|
|||
</div>
|
||||
</div>
|
||||
<div className="text-[18px] text-[#AAA] font-[400]">
|
||||
Maecenas dignissim justo eget nulla rutrum molestie. Maecenas
|
||||
lobortis sem dui,
|
||||
{data.bio || 'No bio'}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
|
|
@ -284,7 +475,7 @@ export const Card: FC<{
|
|||
</div>
|
||||
</div>
|
||||
<div className="ml-[100px] items-center flex">
|
||||
<Button>Request Service</Button>
|
||||
<Button onClick={requestService}>Request Service</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -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) => (
|
||||
<Card key={String(index)} data={item} />
|
||||
))}
|
||||
<Pagination results={list?.count || 0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<number>(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<HTMLInputElement>) => {
|
||||
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 = () => {
|
|||
<div className="w-[328px] flex flex-col gap-[16px]">
|
||||
<h2 className="text-[20px]">Seller Mode</h2>
|
||||
<div className="flex p-[24px] bg-sixth rounded-[4px] border border-[#172034] flex-col items-center gap-[16px]">
|
||||
<div className="w-[64px] h-[64px] bg-[#D9D9D9] rounded-full" />
|
||||
<div className="text-[24px]">John Smith</div>
|
||||
<div className="w-[64px] h-[64px] bg-[#D9D9D9] rounded-full">
|
||||
{!!data?.picture?.path && (
|
||||
<img
|
||||
className="w-full h-full rounded-full"
|
||||
src={data?.picture?.path || ''}
|
||||
alt="avatar"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[24px]">{data?.fullname || ''}</div>
|
||||
{data?.connectedAccount && (
|
||||
<div className="flex gap-[16px] items-center pb-[8px]">
|
||||
<Slider fill={true} value={state ? 'on' : 'off'} onChange={changeMarketplace} />
|
||||
<Slider
|
||||
fill={true}
|
||||
value={state ? 'on' : 'off'}
|
||||
onChange={changeMarketplace}
|
||||
/>
|
||||
<div className="text-[18px]">Active</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -119,6 +156,23 @@ export const Seller = () => {
|
|||
title={tag.name}
|
||||
/>
|
||||
))}
|
||||
<div className="h-[56px] text-[20px] font-[600] flex items-center px-[24px] bg-[#0F1524]">
|
||||
Audience Size
|
||||
</div>
|
||||
<div className="bg-[#0b0f1c] flex px-[32px] py-[24px]">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Audience size on all platforms"
|
||||
name="audience"
|
||||
type="text"
|
||||
pattern="\d*"
|
||||
max={8}
|
||||
disableForm={true}
|
||||
value={audience}
|
||||
onChange={changeAudience}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
onClick={changeConversation}
|
||||
className={clsx(
|
||||
'h-[89px] p-[24px] flex gap-[16px] rounded-[4px] cursor-pointer',
|
||||
path?.id === message.id && 'bg-sixth border border-[#172034]'
|
||||
)}
|
||||
>
|
||||
<div className="w-[40px] h-[40px] rounded-full bg-amber-200">
|
||||
{message?.seller?.picture?.path && (
|
||||
<img src={message.seller.picture.path} alt={message.seller.name || 'Noname'} className="w-full h-full rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute left-0 top-0 w-full h-full flex flex-col whitespace-nowrap">
|
||||
<div>{message.seller.name || 'Noname'}</div>
|
||||
<div className="text-[12px] w-full overflow-ellipsis overflow-hidden">
|
||||
{message.messages[0]?.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[12px]">Mar 28</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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<Root2[]>('messagesGroup', loadMessagesGroup);
|
||||
|
||||
return (
|
||||
<div className="flex gap-[20px]">
|
||||
<div className="pt-[7px] w-[330px] flex flex-col">
|
||||
<div className="text-[20px] mb-[18px]">All Messages</div>
|
||||
<div className="flex flex-col">
|
||||
{messagesGroup.data?.map((message) => (
|
||||
<Card key={message.id} message={message} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">{renderChildren}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<div className="flex gap-[10px]">
|
||||
<div>
|
||||
<div className="w-[24px] h-[24px] rounded-full bg-amber-200">
|
||||
{!!person.picture?.path && (
|
||||
<img
|
||||
src={person.picture.path}
|
||||
alt="person"
|
||||
className="w-[24px] h-[24px] rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col max-w-[534px] gap-[10px]">
|
||||
<div className="flex gap-[10px] items-center">
|
||||
<div>{isMe ? 'Me' : person.name}</div>
|
||||
<div className="w-[6px] h-[6px] bg-[#334155] rounded-full" />
|
||||
<div className="text-[14px] text-inputText">{time}</div>
|
||||
</div>
|
||||
<pre
|
||||
className={clsx(
|
||||
'whitespace-pre-line font-[400] text-[12px]',
|
||||
interClass
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<Root>(`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) => (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
seller={data?.seller!}
|
||||
buyer={data?.buyer!}
|
||||
scrollDown={scrollDown}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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<Root>(`load-1-${params.id}`, loadMessages);
|
||||
|
||||
const submit: SubmitHandler<AddMessageDto> = useCallback(async (values) => {
|
||||
await fetch(`/messages/${params.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
mutate();
|
||||
form.reset();
|
||||
}, []);
|
||||
|
||||
const changeScroll: UIEventHandler<HTMLDivElement> = useCallback((e) => {
|
||||
// @ts-ignore
|
||||
if (e.target.scrollTop === 0) {
|
||||
// @ts-ignore
|
||||
e.target.scrollTop = 1;
|
||||
setPages((prev) => [...prev, makeId(3)]);
|
||||
}
|
||||
}, [pages, setPages]);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(submit)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex-1 flex flex-col rounded-[4px] border border-[#172034] bg-[#0b0f1c] pb-[16px]">
|
||||
<div className="bg-[#0F1524] h-[64px] px-[24px] py-[16px] flex gap-[10px] items-center">
|
||||
<div className="w-[32px] h-[32px] rounded-full bg-amber-200">
|
||||
{!!data?.seller?.picture?.path && (
|
||||
<img
|
||||
src={data?.seller?.picture?.path}
|
||||
alt="seller"
|
||||
className="w-[32px] h-[32px] rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[20px]">{data?.seller?.name || 'Noname'}</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-[658px] max-h-[658px] relative">
|
||||
<div
|
||||
className="pt-[18px] pb-[18px] absolute top-0 left-0 w-full h-full px-[24px] flex flex-col gap-[24px] overflow-x-hidden overflow-y-auto"
|
||||
onScroll={changeScroll}
|
||||
ref={ref}
|
||||
>
|
||||
{pages.map((p, index) => (
|
||||
<Page key={'page_' + (pages.length - index)} refChange={ref} page={pages.length - index} group={params.id as string} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-t-[#658dac] p-[16px] flex flex-col">
|
||||
<div>
|
||||
<Textarea
|
||||
className="!min-h-[100px] resize-none"
|
||||
label=""
|
||||
name="message"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className={clsx(
|
||||
'rounded-[4px] border border-[#506490] h-[48px] px-[24px]',
|
||||
!form.formState.isValid && 'opacity-40'
|
||||
)}
|
||||
disabled={!form.formState.isValid}
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { IsNumber, Max, Min } from 'class-validator';
|
||||
|
||||
export class AudienceDto {
|
||||
@IsNumber()
|
||||
@Max(99999999)
|
||||
@Min(1)
|
||||
audience: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class NewConversationDto {
|
||||
@IsString()
|
||||
to: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(50)
|
||||
message: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class AddMessageDto {
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
message: string;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue