feat: more information in the marketplace, pricing change

This commit is contained in:
Nevo David 2024-05-14 00:03:46 +07:00
parent b21cf8995e
commit 46931be66d
48 changed files with 3892 additions and 568 deletions

View File

@ -1,10 +1,10 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto';
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { Organization } from '@prisma/client';
import { Organization, User } from '@prisma/client';
import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
import { IntegrationFunctionDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.function.dto';
import {
@ -14,6 +14,7 @@ import {
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import {pricing} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing";
import {ApiTags} from "@nestjs/swagger";
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
@ApiTags('Integrations')
@Controller('/integrations')
@ -44,6 +45,16 @@ export class IntegrationsController {
};
}
@Get('/:id')
getSingleIntegration(
@Param('id') id: string,
@Query('order') order: string,
@GetUserFromRequest() user: User,
@GetOrgFromRequest() org: Organization
) {
return this._integrationService.getIntegrationForOrder(id, order, user.id, org.id);
}
@Get('/social/:integration')
async getIntegrationUrl(@Param('integration') integration: string) {
if (
@ -135,7 +146,6 @@ export class IntegrationsController {
throw new Error('Invalid api key');
}
console.log('asd');
return this._integrationService.createOrUpdateIntegration(
org.id,
name,

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { Organization, User } from '@prisma/client';
import { ApiTags } from '@nestjs/swagger';
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
@ -12,6 +12,8 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque
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';
import { CreateOfferDto } from '@gitroom/nestjs-libraries/dtos/marketplace/create.offer.dto';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
@ApiTags('Marketplace')
@Controller('/marketplace')
@ -20,7 +22,8 @@ export class MarketplaceController {
private _itemUserService: ItemUserService,
private _stripeService: StripeService,
private _userService: UsersService,
private _messagesService: MessagesService
private _messagesService: MessagesService,
private _postsService: PostsService
) {}
@Post('/')
@ -39,9 +42,14 @@ export class MarketplaceController {
@Post('/conversation')
createConversation(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Body() body: NewConversationDto
) {
return this._messagesService.createConversation(user.id, body);
return this._messagesService.createConversation(
user.id,
organization.id,
body
);
}
@Get('/bank')
@ -78,6 +86,15 @@ export class MarketplaceController {
return this._itemUserService.getItems(user.id);
}
@Get('/orders')
async getOrders(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Query('type') type: 'seller' | 'buyer'
) {
return this._messagesService.getOrders(user.id, organization.id, type);
}
@Get('/account')
async getAccount(@GetUserFromRequest() user: User) {
const { account, marketplace, connectedAccount, name, picture, audience } =
@ -91,4 +108,121 @@ export class MarketplaceController {
picture,
};
}
@Post('/offer')
async createOffer(
@GetUserFromRequest() user: User,
@Body() body: CreateOfferDto
) {
return this._messagesService.createOffer(user.id, body);
}
@Get('/posts/:id')
async post(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string
) {
const getPost = await this._messagesService.getPost(user.id, organization.id, id);
if (!getPost) {
return ;
}
return {...await this._postsService.getPost(getPost.organizationId, id), providerId: getPost.integration.providerIdentifier};
}
@Post('/posts/:id/revision')
async revision(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string,
@Body('message') message: string
) {
return this._messagesService.requestRevision(
user.id,
organization.id,
id,
message
);
}
@Post('/posts/:id/approve')
async approve(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string,
@Body('message') message: string
) {
return this._messagesService.requestApproved(
user.id,
organization.id,
id,
message
);
}
@Post('/posts/:id/cancel')
async cancel(
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string
) {
return this._messagesService.requestCancel(organization.id, id);
}
@Post('/offer/:id/complete')
async completeOrder(
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string
) {
const order = await this._messagesService.completeOrderAndPay(
organization.id,
id
);
if (!order) {
return;
}
try {
await this._stripeService.payout(
id,
order.charge,
order.account,
order.price
);
} catch (e) {
await this._messagesService.payoutProblem(
id,
order.sellerId,
order.price
);
}
await this._messagesService.completeOrder(id);
}
@Post('/orders/:id/payment')
async payOrder(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string
) {
const orderDetails = await this._messagesService.getOrderDetails(
user.id,
organization.id,
id
);
const payment = await this._stripeService.payAccountStepOne(
user.id,
organization,
orderDetails.seller,
orderDetails.order.id,
orderDetails.order.ordersItems.map((p) => ({
quantity: p.quantity,
integrationType: p.integration.providerIdentifier,
price: p.price,
})),
orderDetails.order.messageGroupId
);
return payment;
}
}

View File

@ -2,8 +2,9 @@ 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 { Organization, User } from '@prisma/client';
import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
@ApiTags('Messages')
@Controller('/messages')
@ -11,24 +12,29 @@ export class MessagesController {
constructor(private _messagesService: MessagesService) {}
@Get('/')
getMessagesGroup(@GetUserFromRequest() user: User) {
return this._messagesService.getMessagesGroup(user.id);
getMessagesGroup(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization
) {
return this._messagesService.getMessagesGroup(user.id, organization.id);
}
@Get('/:groupId/:page')
getMessages(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Param('groupId') groupId: string,
@Param('page') page: string
) {
return this._messagesService.getMessages(user.id, groupId, +page);
return this._messagesService.getMessages(user.id, organization.id, groupId, +page);
}
@Post('/:groupId')
createMessage(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Param('groupId') groupId: string,
@Body() message: AddMessageDto
) {
return this._messagesService.createMessage(user.id, groupId, message);
return this._messagesService.createMessage(user.id, organization.id, groupId, message);
}
}

View File

@ -10,14 +10,18 @@ import {
} from '@nestjs/common';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { Organization } from '@prisma/client';
import { Organization, User } from '@prisma/client';
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service';
import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service";
import {CheckPolicies} from "@gitroom/backend/services/auth/permissions/permissions.ability";
import {AuthorizationActions, Sections} from "@gitroom/backend/services/auth/permissions/permissions.service";
import {ApiTags} from "@nestjs/swagger";
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import {
AuthorizationActions,
Sections,
} from '@gitroom/backend/services/auth/permissions/permissions.service';
import { ApiTags } from '@nestjs/swagger';
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
@ApiTags('Posts')
@Controller('/posts')
@ -25,9 +29,18 @@ export class PostsController {
constructor(
private _postsService: PostsService,
private _commentsService: CommentsService,
private _starsService: StarsService
private _starsService: StarsService,
private _messagesService: MessagesService
) {}
@Get('/marketplace/:id?')
async getMarketplacePosts(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string
) {
return this._messagesService.getMarketplaceAvailableOffers(org.id, id);
}
@Get('/')
async getPosts(
@GetOrgFromRequest() org: Organization,
@ -55,8 +68,8 @@ export class PostsController {
@Get('/old')
oldPosts(
@GetOrgFromRequest() org: Organization,
@Query('date') date: string
@GetOrgFromRequest() org: Organization,
@Query('date') date: string
) {
return this._postsService.getOldPosts(org.id, date);
}
@ -72,6 +85,7 @@ export class PostsController {
@GetOrgFromRequest() org: Organization,
@Body() body: CreatePostDto
) {
return this._postsService.createPost(org.id, body);
}

View File

@ -1,18 +1,13 @@
import {Controller, Post, RawBodyRequest, Req} from "@nestjs/common";
import {StripeService} from "@gitroom/nestjs-libraries/services/stripe.service";
import {ApiTags} from "@nestjs/swagger";
import { Controller, Post, RawBodyRequest, Req } from '@nestjs/common';
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('Stripe')
@Controller('/stripe')
export class StripeController {
constructor(
private readonly _stripeService: StripeService
) {
}
constructor(private readonly _stripeService: StripeService) {}
@Post('/')
stripe(
@Req() req: RawBodyRequest<Request>
) {
stripe(@Req() req: RawBodyRequest<Request>) {
const event = this._stripeService.validateRequest(
req.rawBody,
req.headers['stripe-signature'],
@ -23,10 +18,12 @@ export class StripeController {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (event?.data?.object?.metadata?.service !== 'gitroom') {
return {ok: true};
return { ok: true };
}
switch (event.type) {
case 'checkout.session.completed':
return this._stripeService.updateOrder(event);
case 'account.updated':
return this._stripeService.updateAccount(event);
case 'customer.subscription.created':
@ -36,7 +33,7 @@ export class StripeController {
case 'customer.subscription.deleted':
return this._stripeService.deleteSubscription(event);
default:
return {ok: true};
return { ok: true };
}
}
}

View File

@ -36,14 +36,18 @@ export class PermissionsService {
async getPackageOptions(orgId: string) {
const subscription =
await this._subscriptionService.getSubscriptionByOrganizationId(orgId);
const tier =
subscription?.subscriptionTier ||
(!process.env.STRIPE_PUBLISHABLE_KEY ? 'PRO' : 'FREE');
const { channel, ...all } = pricing[tier];
return {
subscription,
options:
pricing[
subscription?.subscriptionTier || !process.env.STRIPE_PUBLISHABLE_KEY
? 'PRO'
: 'FREE'
],
options: {
...all,
...{ channel: tier === 'FREE' ? { channel } : {} },
},
};
}
@ -73,6 +77,7 @@ export class PermissionsService {
).length;
if (
// @ts-ignore
(options.channel && options.channel > totalChannels) ||
(subscription?.totalChannels || 0) > totalChannels
) {

View File

@ -0,0 +1,68 @@
<svg width="493" height="369" viewBox="0 0 493 369" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_895_7185)">
<path d="M359.988 350.101H350.178L345.51 312.266H359.988V350.101Z" fill="#9E616A"/>
<path d="M365.164 367.097H358.129L356.873 360.456L353.657 367.097H335C334.106 367.097 333.235 366.811 332.514 366.281C331.794 365.752 331.262 365.005 330.996 364.151C330.73 363.298 330.743 362.381 331.035 361.536C331.327 360.69 331.881 359.96 332.617 359.452L347.516 349.162V342.448L363.188 343.383L365.164 367.097Z" fill="#2F2E41"/>
<path d="M418.264 337.722L410.392 343.575L384.069 315.999L395.687 307.36L418.264 337.722Z" fill="#9E616A"/>
<path d="M432.56 348.271L426.915 352.468L421.944 347.888L423.327 355.137L408.355 366.27C407.637 366.804 406.768 367.094 405.874 367.098C404.979 367.103 404.107 366.822 403.384 366.295C402.661 365.769 402.125 365.025 401.855 364.173C401.584 363.32 401.593 362.404 401.881 361.557L407.696 344.409L403.69 339.021L416.824 330.42L432.56 348.271Z" fill="#2F2E41"/>
<path d="M371.407 154.889C371.407 154.889 390.019 170.968 381.431 216.807L370.948 261.722L405.138 318.443L394.236 331.705L349.788 270.311L333.682 232.53L329.162 165.287L371.407 154.889Z" fill="#2F2E41"/>
<path d="M342.56 243.297L341.44 260.634L344.866 334.512L360.987 331.705L364.067 279.104L342.56 243.297Z" fill="#2F2E41"/>
<path d="M135.691 358.997L144.463 358.997L148.636 325.161L135.689 325.162L135.691 358.997Z" fill="#FFB6B6"/>
<path d="M133.452 356.133L135.921 356.133L145.561 352.213L150.728 356.132H150.729C153.648 356.133 156.448 357.292 158.513 359.357C160.578 361.422 161.738 364.222 161.738 367.142V367.499L133.453 367.5L133.452 356.133Z" fill="#2F2E41"/>
<path d="M169.685 353.47L178.241 355.401L189.765 323.316L177.136 320.464L169.685 353.47Z" fill="#FFB6B6"/>
<path d="M168.132 350.183L170.541 350.726L180.808 349.026L184.984 353.987L184.985 353.988C187.833 354.631 190.308 356.379 191.868 358.848C193.427 361.317 193.941 364.303 193.298 367.152L193.22 367.501L165.629 361.271L168.132 350.183Z" fill="#2F2E41"/>
<path d="M124.579 152.143C120.467 164.547 120.901 178.012 125.568 192.463L127.563 273.826C127.563 273.826 124.591 327.183 134.181 345.959H148.091L154.977 268.954L155.842 217.267L185.662 260.873L166.669 344.7L183.788 346.23L214.609 255.259L176.582 167.001L124.579 152.143Z" fill="#2F2E41"/>
<path d="M284.985 116.811C295.625 107.224 311.449 102.403 324.723 107.779C311.107 115.339 300.603 127.471 295.071 142.029C292.958 147.652 291.193 154.125 285.995 157.135C282.761 159.008 278.738 159.099 275.112 158.195C271.485 157.291 268.161 155.478 264.883 153.684L263.95 153.663C268.138 139.967 274.346 126.398 284.985 116.811Z" fill="#8155DD"/>
<path d="M324.648 108.07C312.892 109.589 301.913 114.774 293.271 122.888C291.38 124.602 289.771 126.603 288.502 128.818C287.337 131.016 286.781 133.485 286.892 135.97C286.95 138.29 287.277 140.655 286.77 142.948C286.493 144.118 285.972 145.216 285.24 146.17C284.509 147.124 283.585 147.913 282.527 148.485C279.929 149.978 276.95 150.449 274.02 150.808C270.768 151.206 267.383 151.569 264.567 153.393C264.226 153.614 263.903 153.072 264.243 152.851C269.142 149.677 275.341 150.832 280.608 148.714C283.066 147.725 285.241 145.99 286.011 143.366C286.684 141.072 286.349 138.633 286.27 136.285C286.126 133.862 286.577 131.441 287.584 129.233C288.735 126.961 290.265 124.901 292.107 123.142C296.192 119.115 300.885 115.756 306.013 113.187C311.85 110.223 318.15 108.278 324.642 107.439C325.044 107.387 325.047 108.019 324.648 108.07Z" fill="white"/>
<path d="M295.902 120.112C294.737 118.477 294.192 116.482 294.363 114.482C294.534 112.482 295.41 110.608 296.835 109.195C297.124 108.91 297.581 109.346 297.291 109.631C295.962 110.945 295.146 112.691 294.99 114.553C294.835 116.416 295.351 118.272 296.444 119.788C296.681 120.118 296.138 120.44 295.902 120.112Z" fill="white"/>
<path d="M286.709 134.798C290.749 135.514 294.91 134.681 298.363 132.465C298.705 132.245 299.028 132.787 298.687 133.006C295.089 135.306 290.757 136.164 286.555 135.41C286.154 135.338 286.311 134.726 286.709 134.798Z" fill="white"/>
<path d="M311.902 111.058C312.221 111.784 312.715 112.42 313.34 112.908C313.966 113.397 314.702 113.722 315.484 113.855C315.886 113.922 315.729 114.534 315.33 114.467C314.467 114.316 313.653 113.956 312.961 113.418C312.269 112.88 311.72 112.181 311.36 111.382C311.32 111.309 311.309 111.224 311.329 111.144C311.35 111.063 311.4 110.994 311.469 110.949C311.541 110.906 311.627 110.894 311.708 110.914C311.789 110.935 311.859 110.986 311.902 111.058Z" fill="white"/>
<path d="M339.34 154.013C339.088 153.978 338.837 153.943 338.581 153.912C335.195 153.463 331.777 153.289 328.362 153.393C328.098 153.397 327.831 153.405 327.567 153.416C319.343 153.755 311.253 155.617 303.709 158.908C300.708 160.221 297.811 161.76 295.044 163.512C291.222 165.931 287.271 168.859 282.998 169.846C282.554 169.954 282.103 170.034 281.649 170.086L263.997 154.803C263.974 154.748 263.948 154.697 263.925 154.642L263.192 154.064C263.329 153.963 263.472 153.862 263.609 153.762C263.688 153.703 263.771 153.648 263.849 153.59C263.903 153.552 263.957 153.514 264.004 153.477C264.022 153.464 264.04 153.451 264.054 153.443C264.1 153.405 264.148 153.376 264.19 153.342C264.992 152.775 265.799 152.212 266.611 151.654C266.614 151.65 266.614 151.65 266.622 151.649C272.769 147.337 279.407 143.769 286.395 141.02C286.604 140.941 286.817 140.858 287.035 140.786C290.182 139.615 293.42 138.704 296.715 138.062C298.521 137.716 300.343 137.461 302.174 137.299C306.907 136.89 311.675 137.228 316.302 138.3C325.528 140.439 333.966 145.505 338.964 153.405C339.092 153.608 339.216 153.806 339.34 154.013Z" fill="#8155DD"/>
<path d="M339.107 154.202C328.805 148.337 316.918 145.867 305.132 147.143C302.59 147.373 300.101 148.001 297.754 149.006C295.501 150.059 293.57 151.696 292.163 153.748C290.812 155.634 289.649 157.72 287.864 159.245C286.938 160.012 285.861 160.575 284.703 160.897C283.545 161.218 282.331 161.291 281.143 161.111C278.17 160.74 275.507 159.322 272.952 157.845C270.115 156.204 267.194 154.456 263.847 154.218C263.442 154.189 263.51 153.561 263.915 153.59C269.738 154.005 273.992 158.659 279.473 160.139C282.031 160.83 284.812 160.754 287.006 159.122C288.925 157.696 290.126 155.546 291.477 153.624C292.82 151.603 294.638 149.941 296.771 148.785C299.059 147.663 301.52 146.94 304.05 146.644C309.736 145.889 315.505 146.032 321.147 147.068C327.592 148.215 333.793 150.457 339.482 153.695C339.834 153.896 339.456 154.402 339.107 154.202Z" fill="white"/>
<path d="M308.903 146.509C308.958 144.502 309.724 142.581 311.064 141.087C312.405 139.593 314.232 138.624 316.221 138.354C316.624 138.3 316.726 138.923 316.322 138.977C314.47 139.225 312.767 140.128 311.522 141.521C310.277 142.915 309.571 144.708 309.531 146.577C309.522 146.983 308.894 146.912 308.903 146.509Z" fill="white"/>
<path d="M292.722 152.701C295.516 155.705 299.341 157.545 303.432 157.854C303.837 157.885 303.769 158.512 303.364 158.482C299.107 158.152 295.131 156.229 292.23 153.096C291.954 152.798 292.447 152.404 292.722 152.701Z" fill="white"/>
<path d="M327.13 148.913C326.947 149.685 326.959 150.49 327.164 151.257C327.37 152.023 327.762 152.726 328.306 153.303C328.586 153.598 328.093 153.992 327.815 153.699C327.216 153.058 326.784 152.281 326.555 151.435C326.326 150.589 326.308 149.7 326.503 148.845C326.514 148.763 326.557 148.688 326.621 148.637C326.686 148.585 326.768 148.559 326.85 148.565C326.933 148.575 327.009 148.617 327.062 148.682C327.114 148.747 327.139 148.83 327.13 148.913Z" fill="white"/>
<path d="M29.3006 358.383C28.4719 361.491 26.2947 364.08 23.666 365.966C23.2374 366.275 22.7974 366.56 22.3516 366.829C22.2145 366.909 22.0773 366.995 21.9345 367.069C21.7802 367.16 21.6259 367.246 21.4716 367.326H9.1109C8.88803 366.875 8.67088 366.417 8.45372 365.966C3.15629 354.834 -0.563907 342.822 0.0704131 330.587C0.322071 325.841 1.31908 321.165 3.02486 316.729C6.41933 307.889 12.6139 300.237 21.1344 296.385C21.3459 296.288 21.5687 296.191 21.7859 296.099C21.7173 296.345 21.6487 296.585 21.5801 296.831C20.666 300.124 20.0201 303.486 19.6486 306.883C19.6143 307.146 19.5858 307.409 19.5629 307.677C18.751 315.87 19.4719 324.143 21.6887 332.073C21.6944 332.09 21.7001 332.113 21.7059 332.13C22.1916 333.884 22.7555 335.612 23.3974 337.313C23.9003 338.656 24.4432 339.982 25.0375 341.285C27.5176 346.754 30.8435 352.582 29.3006 358.383Z" fill="#8155DD"/>
<path d="M21.9401 296.357C21.8201 296.517 21.7001 296.671 21.5801 296.831C19.0701 300.157 16.9388 303.753 15.2255 307.552C15.134 307.74 15.0483 307.929 14.974 308.123C12.6264 313.427 11.1188 319.065 10.5052 324.833C10.4986 324.872 10.4947 324.912 10.4938 324.953C10.3509 326.301 10.2652 327.661 10.2309 329.021C10.1038 331.571 10.3796 334.124 11.0481 336.587C11.6576 338.599 12.7257 340.442 14.1683 341.971C14.3111 342.125 14.454 342.279 14.6083 342.428C14.7226 342.548 14.8483 342.668 14.9683 342.782C16.1226 343.885 17.3856 344.908 18.4713 346.091C18.969 346.621 19.4184 347.195 19.8143 347.805C20.446 348.828 20.8541 349.973 21.0117 351.165C21.1693 352.356 21.0728 353.568 20.7286 354.72C19.9514 357.611 18.1742 360.052 16.3569 362.377C15.4483 363.543 14.5111 364.72 13.6997 365.966C13.4025 366.412 13.1282 366.863 12.8768 367.326H12.1567C12.3968 366.863 12.6596 366.412 12.9454 365.966C15.0312 362.68 17.9971 359.857 19.5343 356.24C20.5743 353.8 20.8887 351.034 19.5743 348.64C19.1891 347.943 18.723 347.295 18.1856 346.708C17.1399 345.525 15.8884 344.514 14.7512 343.445C14.4654 343.176 14.1854 342.902 13.9168 342.616C12.4929 341.17 11.3941 339.436 10.6938 337.53C9.90079 335.11 9.52588 332.573 9.58518 330.027C9.58518 328.404 9.6709 326.77 9.83091 325.147C9.84806 324.941 9.87091 324.73 9.89377 324.518C11.0624 314.305 14.9436 304.592 21.1344 296.385C21.2487 296.225 21.3687 296.071 21.4887 295.917C21.7344 295.597 22.1858 296.042 21.9401 296.357Z" fill="white"/>
<path d="M10.1245 325.201C8.14509 324.868 6.34835 323.843 5.05511 322.308C3.76187 320.773 3.05635 318.828 3.06452 316.821C3.06562 316.74 3.09843 316.661 3.15606 316.603C3.21369 316.545 3.29164 316.512 3.37347 316.51C3.45529 316.508 3.53462 316.538 3.59474 316.594C3.65485 316.649 3.69106 316.726 3.69573 316.808C3.68467 318.677 4.34204 320.488 5.54922 321.915C6.75641 323.342 8.43387 324.29 10.2789 324.589C10.6797 324.654 10.523 325.266 10.1245 325.201Z" fill="white"/>
<path d="M14.009 342.085C17.3718 339.735 19.7251 336.203 20.5997 332.194C20.686 331.797 21.298 331.952 21.2118 332.349C20.2939 336.519 17.8374 340.189 14.3326 342.627C13.9984 342.859 13.6766 342.316 14.009 342.085Z" fill="white"/>
<path d="M15.0363 307.485C15.7753 307.773 16.5744 307.873 17.3617 307.776C18.149 307.679 18.9 307.388 19.547 306.929C19.8781 306.693 20.1996 307.237 19.8706 307.471C19.1533 307.975 18.3236 308.295 17.454 308.404C16.5843 308.513 15.7012 308.408 14.8818 308.097C14.8021 308.074 14.7341 308.021 14.6916 307.95C14.6491 307.879 14.6353 307.794 14.653 307.713C14.6739 307.632 14.7258 307.563 14.7976 307.52C14.8694 307.477 14.9551 307.465 15.0363 307.485Z" fill="white"/>
<path d="M64.8117 318.455C64.6117 318.609 64.406 318.764 64.2059 318.924C61.4928 321 58.9529 323.293 56.6112 325.781C56.4284 325.97 56.2455 326.164 56.0683 326.358L56.0626 326.364C50.5103 332.391 46.1201 339.393 43.1134 347.017L43.0962 347.068C43.0905 347.085 43.0848 347.097 43.0791 347.114C41.8873 350.165 40.9264 353.302 40.2046 356.497C39.5075 359.617 38.936 362.96 37.8445 365.966C37.6788 366.429 37.4959 366.886 37.3016 367.326H11.8945C11.9803 366.875 12.0717 366.418 12.1631 365.966C13.4556 359.345 15.4755 352.886 18.1863 346.708C18.2777 346.502 18.3692 346.297 18.472 346.091C19.8714 343.038 21.517 340.105 23.3923 337.319L23.3927 337.317L23.3939 337.315L23.3958 337.314L23.398 337.313C24.4278 335.793 25.534 334.326 26.7125 332.919C29.7687 329.282 33.3789 326.15 37.4102 323.638C37.4331 323.621 37.4616 323.61 37.4845 323.592C45.5078 318.615 55.0169 316.243 64.1088 318.295H64.1145C64.3488 318.346 64.5774 318.398 64.8117 318.455Z" fill="#8155DD"/>
<path d="M64.7822 318.758C64.5879 318.809 64.3936 318.866 64.205 318.923C60.2 320.068 56.3343 321.653 52.6787 323.649C52.4958 323.747 52.3129 323.849 52.1301 323.952C47.0782 326.772 42.4924 330.355 38.5351 334.576C38.532 334.579 38.53 334.583 38.5293 334.587C38.4836 334.633 38.4322 334.684 38.3922 334.73C37.4779 335.713 36.5978 336.736 35.7635 337.787C34.1315 339.748 32.8164 341.952 31.8661 344.319C31.14 346.292 30.8837 348.407 31.1175 350.497C31.1347 350.708 31.1632 350.914 31.1918 351.125C31.2147 351.291 31.2375 351.457 31.2661 351.628C31.6433 353.92 32.2947 356.217 32.1118 358.554C32.0006 359.752 31.637 360.912 31.0451 361.959C30.4531 363.006 29.6462 363.916 28.6774 364.629C28.0188 365.138 27.3149 365.586 26.5744 365.966C25.5157 366.512 24.4127 366.967 23.2771 367.326H21.0312C21.3341 367.24 21.6313 367.155 21.9342 367.069C23.0089 366.766 24.0643 366.397 25.0943 365.966C25.6828 365.72 26.2553 365.437 26.8087 365.12C29.106 363.8 31.0204 361.777 31.4204 359.074C31.769 356.708 31.0947 354.343 30.6946 352.028C30.6261 351.628 30.5632 351.234 30.5289 350.839C30.2561 348.835 30.4218 346.796 31.0146 344.862C31.8369 342.45 33.0662 340.196 34.6491 338.199C35.6284 336.898 36.6736 335.649 37.7807 334.456C37.9179 334.301 38.0608 334.147 38.2093 333.993C42.2499 329.742 46.9152 326.133 52.0444 323.289H52.0501C55.8639 321.166 59.9099 319.49 64.1079 318.295H64.1136C64.3022 318.238 64.4965 318.181 64.685 318.129C65.0794 318.026 65.1708 318.649 64.7822 318.758Z" fill="white"/>
<path d="M37.9835 334.673C36.6032 333.215 35.7859 331.315 35.6774 329.311C35.5689 327.307 36.1764 325.329 37.3913 323.732C37.6383 323.409 38.1509 323.778 37.9036 324.101C36.7696 325.586 36.2039 327.428 36.3087 329.294C36.4135 331.161 37.1819 332.928 38.4752 334.277C38.7559 334.57 38.2626 334.964 37.9835 334.673Z" fill="white"/>
<path d="M30.9204 350.493C35.0206 350.641 39.0259 349.238 42.1377 346.564C42.4458 346.299 42.8411 346.791 42.5334 347.056C39.29 349.832 35.1189 351.284 30.8525 351.121C30.4458 351.105 30.516 350.477 30.9204 350.493Z" fill="white"/>
<path d="M52.5721 323.485C52.9886 324.16 53.5664 324.721 54.2533 325.117C54.9403 325.514 55.7151 325.734 56.508 325.757C56.9146 325.768 56.844 326.395 56.4401 326.385C55.5641 326.355 54.7088 326.111 53.9488 325.674C53.1888 325.238 52.5473 324.622 52.0803 323.88C52.0304 323.814 52.0076 323.731 52.0165 323.649C52.0255 323.566 52.0654 323.491 52.1283 323.437C52.1937 323.384 52.277 323.36 52.3601 323.369C52.4432 323.378 52.5193 323.42 52.5721 323.485Z" fill="white"/>
<path d="M260.804 98.9366C260.218 99.708 259.469 100.342 258.611 100.792C257.754 101.243 256.807 101.501 255.839 101.546C254.871 101.592 253.905 101.425 253.009 101.057C252.112 100.688 251.308 100.128 250.651 99.4152L229.05 108.81L231.154 96.7514L251.344 89.6786C252.615 88.6552 254.217 88.1334 255.847 88.2121C257.476 88.2909 259.02 88.9647 260.186 90.1058C261.352 91.247 262.059 92.7763 262.173 94.4038C262.287 96.0314 261.8 97.6442 260.804 98.9366Z" fill="#FFB8B8"/>
<path d="M246.389 102.702L196.216 125.051L196.109 125.012L138.925 104.634C134.665 102.408 131.418 98.6382 129.847 94.0959C128.277 89.5536 128.502 84.5828 130.477 80.2013C131.572 77.7805 133.167 75.619 135.158 73.859C137.148 72.0991 139.488 70.7807 142.025 69.9905C144.562 69.2004 147.237 68.9564 149.875 69.2747C152.512 69.5929 155.053 70.4662 157.329 71.8371L199.884 97.4723L241.15 92.1241L246.389 102.702Z" fill="#8155DD"/>
<path d="M493 367.789C493 367.878 492.982 367.967 492.948 368.049C492.914 368.132 492.864 368.207 492.801 368.27C492.738 368.334 492.663 368.384 492.58 368.418C492.497 368.452 492.409 368.469 492.319 368.469H0.698584C0.518227 368.469 0.345264 368.397 0.217732 368.27C0.0902003 368.142 0.0185547 367.969 0.0185547 367.789C0.0185547 367.609 0.0902003 367.436 0.217732 367.308C0.345264 367.181 0.518227 367.109 0.698584 367.109H492.319C492.409 367.109 492.497 367.126 492.58 367.16C492.663 367.194 492.738 367.244 492.801 367.307C492.864 367.371 492.914 367.446 492.948 367.528C492.982 367.611 493 367.7 493 367.789Z" fill="#172034"/>
<path d="M179.063 169.607L128.486 161.517C127.159 161.092 125.94 160.385 124.913 159.445C123.885 158.505 123.073 157.353 122.533 156.069C121.992 154.786 121.736 153.4 121.782 152.008C121.827 150.616 122.173 149.25 122.796 148.004L125.09 143.416L124.199 95.2157C124.092 88.946 125.637 82.7585 128.681 77.2758C131.724 71.7931 136.157 67.2085 141.535 63.9833L146.467 61.0342L148.96 54.7043L171.944 55.2649L172.011 63.5667L180.242 76.6922L180.242 76.7466L179.726 148.187L177.457 155.559L180.887 164.135L179.063 169.607Z" fill="#8155DD"/>
<path d="M162.557 46.8334C174.091 46.8334 183.441 37.4835 183.441 25.9499C183.441 14.4163 174.091 5.06641 162.557 5.06641C151.024 5.06641 141.674 14.4163 141.674 25.9499C141.674 37.4835 151.024 46.8334 162.557 46.8334Z" fill="#FFB8B8"/>
<path d="M140.811 30.5026C140.188 27.8786 140.474 24.186 140.826 21.5439C141.749 14.617 145.595 8.03185 151.44 4.13122C152.548 3.28956 153.865 2.76626 155.248 2.61774C156.631 2.56002 158.13 3.30016 158.554 4.61727C158.902 3.74797 159.425 2.95928 160.09 2.29987C160.755 1.64047 161.548 1.12434 162.42 0.783346C164.172 0.112085 166.057 -0.14177 167.924 0.0419035C171.543 0.340228 175.014 1.61282 177.968 3.72463C180.922 5.83644 183.25 8.70868 184.704 12.0365C185.261 13.3616 186.819 8.46708 187.526 9.71911C188.176 11.0205 189.309 12.016 190.683 12.4926C192.068 12.8791 192.63 18.5846 193.175 17.2542C193.598 18.2374 193.765 19.3116 193.661 20.3768C193.557 21.442 193.185 22.4636 192.579 23.3463C191.974 24.229 191.155 24.9442 190.199 25.4253C189.243 25.9064 188.181 26.1377 187.112 26.0976C185.702 26.0449 184.354 25.5265 182.962 25.3033C177.981 24.5053 172.698 28.1167 171.633 33.0469C171.252 31.6212 170.643 30.2663 169.83 29.0347C169.417 28.4222 168.864 27.917 168.217 27.5613C167.569 27.2056 166.846 27.0096 166.108 26.9897C164.691 27.0427 163.446 27.9691 162.526 29.0486C161.607 30.1281 160.916 31.3876 159.99 32.4613C157.273 35.6117 153.597 42.9588 149.724 42.423C146.668 42.0001 141.661 34.0854 140.811 30.5026Z" fill="#2F2E41"/>
<path d="M288.795 122.601C288.942 121.649 288.746 120.676 288.243 119.855C287.74 119.033 286.963 118.416 286.048 118.113L229.809 99.3824C229.27 99.202 228.699 99.1365 228.133 99.1899L214.037 100.523L205.949 101.29L191.023 102.701C190.122 102.787 189.275 103.17 188.614 103.789C187.954 104.409 187.518 105.23 187.375 106.124L179.797 153.462C179.653 154.385 179.829 155.33 180.295 156.14C180.761 156.95 181.489 157.577 182.359 157.917L239.186 179.852C239.452 179.956 239.729 180.03 240.012 180.074C240.424 180.141 240.843 180.144 241.255 180.083L277.975 174.641C278.841 174.516 279.643 174.115 280.263 173.497C280.883 172.879 281.288 172.078 281.417 171.213L288.795 122.601Z" fill="black"/>
<path d="M183.648 154.582L240.473 176.516C240.556 176.548 240.646 176.559 240.735 176.546L277.449 171.105C277.558 171.09 277.659 171.04 277.737 170.962C277.815 170.885 277.865 170.784 277.881 170.675L285.264 122.063C285.281 121.944 285.256 121.823 285.193 121.721C285.131 121.619 285.034 121.541 284.921 121.502L284.543 121.374L228.679 102.775C228.653 102.766 228.627 102.76 228.6 102.756C228.557 102.748 228.513 102.746 228.469 102.75L194.087 106.003L191.359 106.259C191.247 106.271 191.142 106.319 191.06 106.396C190.978 106.473 190.923 106.575 190.905 106.686L183.328 154.023C183.309 154.138 183.331 154.257 183.389 154.359C183.447 154.46 183.538 154.539 183.648 154.582Z" fill="#6C63FF"/>
<path d="M194.087 106.003L228.468 102.75C228.512 102.746 228.556 102.748 228.6 102.756C228.627 102.76 228.653 102.766 228.678 102.775L284.543 121.374L269.807 122.934L264.292 123.515L248.599 125.173C248.554 125.175 248.508 125.175 248.462 125.171C248.434 125.163 248.406 125.154 248.372 125.145L194.087 106.003Z" fill="#2F2E41"/>
<path d="M182.359 157.92L239.185 179.854C239.452 179.957 239.73 180.03 240.013 180.071C240.425 180.139 240.845 180.144 241.258 180.087L277.972 174.645C278.839 174.519 279.642 174.117 280.262 173.499C280.882 172.881 281.287 172.08 281.417 171.214L288.795 122.601C288.904 121.885 288.821 121.152 288.556 120.477C288.518 120.388 288.481 120.298 288.437 120.213C288.145 119.598 287.699 119.067 287.144 118.672C286.809 118.432 286.44 118.243 286.049 118.114L285.982 118.09L229.812 99.382C229.583 99.3088 229.349 99.2518 229.111 99.2116C228.787 99.1657 228.457 99.1593 228.131 99.1926L214.037 100.521L205.95 101.293L191.022 102.702C190.411 102.759 189.821 102.954 189.297 103.272C189.256 103.292 189.217 103.315 189.181 103.341C189.155 103.358 189.13 103.377 189.107 103.397C188.649 103.71 188.259 104.113 187.962 104.581C187.664 105.049 187.465 105.572 187.377 106.12L179.794 153.461C179.65 154.385 179.826 155.331 180.293 156.141C180.759 156.952 181.488 157.579 182.359 157.92ZM188.383 106.285C188.475 105.717 188.727 105.188 189.109 104.758C189.224 104.62 189.354 104.494 189.495 104.382C189.88 104.07 190.336 103.857 190.823 103.762C190.919 103.742 191.016 103.729 191.114 103.721L216.249 101.347L228.23 100.207C228.311 100.197 228.385 100.199 228.46 100.194C228.642 100.19 228.823 100.204 229.002 100.234C229.166 100.256 229.328 100.294 229.485 100.349L285.721 119.081C285.765 119.097 285.804 119.112 285.849 119.128C286.36 119.318 286.809 119.645 287.148 120.073C287.415 120.4 287.61 120.781 287.721 121.189C287.831 121.597 287.854 122.024 287.788 122.442L280.404 171.06C280.308 171.708 280.005 172.308 279.541 172.771C279.077 173.235 278.476 173.536 277.828 173.632L241.108 179.073C240.582 179.149 240.046 179.088 239.552 178.896L182.725 156.968C182.074 156.711 181.529 156.241 181.18 155.634C180.831 155.026 180.699 154.319 180.805 153.627L188.383 106.285Z" fill="#3F3D56"/>
<path d="M188.984 103.656C189.014 103.569 189.067 103.493 189.138 103.435C189.161 103.416 189.185 103.399 189.211 103.385C189.235 103.375 189.259 103.36 189.283 103.351C189.397 103.305 189.524 103.303 189.64 103.345L190.823 103.762L248.606 124.146L262.13 122.717L269.698 121.919L285.715 120.223L287.148 120.073L287.426 120.042C287.546 120.031 287.666 120.063 287.766 120.131C287.865 120.199 287.937 120.3 287.97 120.416C287.978 120.442 287.983 120.47 287.985 120.497C287.997 120.608 287.973 120.718 287.916 120.813C287.859 120.908 287.773 120.982 287.671 121.024C287.626 121.043 287.578 121.056 287.53 121.062L285.581 121.267L269.807 122.934L264.293 123.515L248.6 125.173C248.554 125.175 248.508 125.175 248.462 125.171C248.434 125.163 248.407 125.154 248.373 125.146L189.495 104.382L189.295 104.311C189.249 104.296 189.207 104.272 189.17 104.242C189.08 104.176 189.014 104.084 188.981 103.979C188.948 103.874 188.949 103.76 188.984 103.656Z" fill="#3F3D56"/>
<path d="M240.167 180.22C240.034 180.199 239.915 180.126 239.836 180.017C239.756 179.908 239.723 179.773 239.743 179.639L248.039 124.588C248.059 124.454 248.132 124.334 248.241 124.254C248.35 124.173 248.486 124.139 248.62 124.159L248.625 124.16C248.758 124.182 248.877 124.254 248.956 124.363C249.036 124.472 249.069 124.608 249.049 124.741L240.753 179.792C240.733 179.926 240.66 180.046 240.551 180.127C240.442 180.207 240.306 180.241 240.172 180.221L240.167 180.22Z" fill="#3F3D56"/>
<path d="M261.196 176.728L255.679 177.31L262.132 122.716C262.132 122.716 269.501 121.381 269.698 121.916C269.82 122.236 261.249 176.377 261.196 176.728Z" fill="#3F3D56"/>
<path d="M189.296 104.31L189.147 129.207L233.222 146.001L249.05 124.74L189.296 104.31Z" fill="#3F3D56"/>
<path d="M287.149 120.074C286.78 119.611 286.284 119.266 285.722 119.081L229.487 100.35C229.329 100.297 229.167 100.258 229.002 100.232C228.823 100.201 228.641 100.188 228.459 100.192L228.55 99.6216L229.113 99.2141L242.467 89.5457L287.064 103.213L287.142 118.672L287.149 120.074Z" fill="#3F3D56"/>
<path d="M191.542 144.278C191.605 144.284 191.667 144.297 191.728 144.317L208.982 149.984C209.198 150.055 209.377 150.209 209.48 150.412C209.583 150.614 209.6 150.85 209.53 151.066C209.459 151.282 209.305 151.461 209.102 151.563C208.899 151.666 208.664 151.684 208.448 151.613L191.193 145.946C190.993 145.88 190.824 145.743 190.718 145.56C190.613 145.378 190.579 145.162 190.622 144.956C190.665 144.75 190.782 144.566 190.952 144.441C191.122 144.316 191.332 144.258 191.542 144.278Z" fill="white"/>
<path d="M261.159 248.72C264.947 248.72 268.017 245.649 268.017 241.862C268.017 238.075 264.947 235.005 261.159 235.005C257.372 235.005 254.302 238.075 254.302 241.862C254.302 245.649 257.372 248.72 261.159 248.72Z" fill="#172034"/>
<path d="M86.2921 273.864C90.0794 273.864 93.1496 270.793 93.1496 267.006C93.1496 263.219 90.0794 260.149 86.2921 260.149C82.5048 260.149 79.4346 263.219 79.4346 267.006C79.4346 270.793 82.5048 273.864 86.2921 273.864Z" fill="#172034"/>
<path d="M229.157 46.9946C232.945 46.9946 236.015 43.9244 236.015 40.1371C236.015 36.3498 232.945 33.2795 229.157 33.2795C225.37 33.2795 222.3 36.3498 222.3 40.1371C222.3 43.9244 225.37 46.9946 229.157 46.9946Z" fill="#172034"/>
<path d="M240.408 154.363C239.519 154.749 238.556 154.936 237.588 154.91C236.619 154.884 235.668 154.645 234.801 154.212C233.934 153.778 233.173 153.16 232.572 152.401C231.97 151.641 231.542 150.759 231.318 149.816L207.881 147.448L215.613 137.958L236.683 141.662C238.292 141.391 239.945 141.719 241.328 142.584C242.711 143.45 243.728 144.793 244.187 146.358C244.646 147.924 244.515 149.604 243.819 151.079C243.122 152.554 241.908 153.723 240.408 154.363Z" fill="#FFB8B8"/>
<path d="M225.993 150.599L171.3 145.559L171.225 145.473L131.308 99.7355C128.681 95.7108 127.692 90.8342 128.543 86.1041C129.394 81.374 132.022 77.1483 135.887 74.2921C138.026 72.7158 140.474 71.6103 143.071 71.0484C145.667 70.4865 148.354 70.4809 150.953 71.0321C153.552 71.5832 156.005 72.6786 158.15 74.246C160.295 75.8135 162.084 77.8175 163.399 80.1261L187.985 123.297L226.596 138.81L225.993 150.599Z" fill="#8155DD"/>
<path d="M363.271 62.6414C361.913 52.6787 360.209 41.9822 353.183 34.7903C350.953 32.5083 348.284 30.7014 345.338 29.4787C342.391 28.2559 339.227 27.6426 336.037 27.6756C332.846 27.7086 329.696 28.3873 326.775 29.6708C323.854 30.9543 321.223 32.816 319.041 35.1436C313.926 40.6004 311.668 48.2625 311.461 55.739C311.254 63.2154 312.919 70.6088 314.766 77.8564C324.542 78.0517 334.245 76.1418 343.217 72.2565C345.461 71.2827 347.668 70.1818 350.038 69.5784C352.409 68.9751 354.098 70.4933 356.303 71.5519L357.514 69.2344C358.504 71.0782 361.572 70.5317 362.685 68.7588C363.798 66.9859 363.554 64.7154 363.271 62.6414Z" fill="#2F2E41"/>
<path d="M324.522 118.249L284.591 119.103C283.978 118.043 283.07 117.184 281.977 116.631C280.885 116.078 279.655 115.854 278.438 115.987C277.221 116.119 276.068 116.603 275.121 117.379C274.173 118.155 273.472 119.189 273.101 120.357C272.731 121.524 272.708 122.774 273.035 123.954C273.362 125.134 274.025 126.193 274.944 127.004C275.862 127.814 276.996 128.34 278.207 128.517C279.419 128.695 280.656 128.516 281.768 128.003C287.064 128.304 328.498 134.185 330.969 130.395C333.478 126.549 346.87 112.973 346.87 112.973L336.576 98.3191L324.522 118.249Z" fill="#9E616A"/>
<path d="M334.253 72.4985C344.615 72.4985 353.014 64.0989 353.014 53.7374C353.014 43.3759 344.615 34.9763 334.253 34.9763C323.892 34.9763 315.492 43.3759 315.492 53.7374C315.492 64.0989 323.892 72.4985 334.253 72.4985Z" fill="#9E616A"/>
<path d="M364.302 89.8354C362.091 85.4353 361.017 79.9995 356.304 78.573C353.121 77.6097 340.247 79.0533 337.346 80.6793C332.521 83.3841 336.894 88.4525 334.047 93.1955C330.953 98.3516 322.543 108.699 319.448 113.855C317.321 117.4 324.487 127.804 323.336 131.775C322.186 135.746 322.131 140.202 324.09 143.843C325.844 147.101 323.307 149.983 325.07 153.236C326.905 156.618 329.156 169.267 327.448 172.716L326.306 176.144C337.647 176.811 347.184 163.261 358.455 161.834C361.254 161.479 364.161 161.016 366.468 159.391C369.866 156.998 371.21 152.654 372.049 148.583C376.09 128.685 373.363 108.006 364.302 89.8354Z" fill="#3F3D56"/>
<path d="M381.306 136.296C379.727 131.984 376.867 113.131 376.867 113.131L358.966 112.606L367.718 134.191L343.836 167.623C343.836 167.623 343.881 167.697 343.961 167.828C342.705 167.626 341.418 167.809 340.268 168.351C339.118 168.893 338.158 169.77 337.514 170.866C336.87 171.963 336.572 173.228 336.659 174.497C336.747 175.766 337.215 176.978 338.003 177.977C338.79 178.975 339.861 179.712 341.075 180.092C342.289 180.471 343.589 180.476 344.805 180.104C346.021 179.733 347.097 179.003 347.891 178.01C348.686 177.017 349.162 175.808 349.258 174.54C349.346 174.59 349.449 174.608 349.549 174.591C349.649 174.574 349.74 174.522 349.806 174.446C352.463 171.111 382.886 140.608 381.306 136.296Z" fill="#9E616A"/>
<path d="M383.126 124.39C381.483 117.455 379.828 110.483 377.058 103.917C374.288 97.3508 370.322 91.1489 364.745 86.7129C362.962 85.2947 360.968 84.0429 358.733 83.602C356.497 83.161 353.978 83.6545 352.408 85.3057C349.896 87.9479 350.652 92.189 351.573 95.7165C354.29 106.117 357.007 116.518 359.724 126.919C367.57 126.087 375.416 125.256 383.261 124.424L383.126 124.39Z" fill="#3F3D56"/>
<path d="M357.242 56.3396C356.551 51.801 355.83 47.2245 354.206 42.9511C352.583 38.6776 349.967 34.6749 346.214 32.254C340.271 28.4212 332.577 29.1604 325.915 31.3652C320.763 33.0702 315.733 35.6618 312.1 39.8281C308.466 43.9944 306.425 49.9394 307.777 55.387C314.884 53.7079 321.991 52.0288 329.097 50.3497L328.319 50.8994C330.653 52.063 332.691 53.7429 334.278 55.8117C335.865 57.8804 336.96 60.2836 337.48 62.8388C337.989 65.425 337.916 68.0919 337.267 70.6465C336.618 73.2011 335.409 75.5795 333.728 77.6097C338.575 75.8903 343.422 74.1709 348.269 72.4515C351.261 71.3903 354.463 70.1674 356.237 67.4453C358.294 64.2892 357.816 60.106 357.242 56.3396Z" fill="#2F2E41"/>
</g>
<defs>
<clipPath id="clip0_895_7185">
<rect width="493" height="368.469" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -9,6 +9,11 @@ export const metadata: Metadata = {
export default async function Index() {
return (
<div>asd</div>
<div className="bg-[#0b0f1c] h-[951px] flex flex-col rounded-[4px] border border-[#172034]">
<div className="bg-[#0F1524] h-[64px]" />
<div className="flex-1 flex justify-center items-center text-[20px]">
Select a conversation and chat away.
</div>
</div>
);
}

View File

@ -3,7 +3,7 @@
import { Slider } from '@gitroom/react/form/slider';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@gitroom/react/form/button';
import { isEqual, sortBy } from 'lodash';
import { sortBy } from 'lodash';
import { Track } from '@gitroom/react/form/track';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { Subscription } from '@prisma/client';
@ -17,7 +17,6 @@ import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions
import { FAQComponent } from '@gitroom/frontend/components/billing/faq.component';
import { useSWRConfig } from 'swr';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { useRouter } from 'next/navigation';
import interClass from '@gitroom/react/helpers/inter.font';
export interface Tiers {
@ -34,11 +33,10 @@ export interface Tiers {
}
export const Prorate: FC<{
totalChannels: number;
period: 'MONTHLY' | 'YEARLY';
pack: 'STANDARD' | 'PRO';
}> = (props) => {
const { totalChannels, period, pack } = props;
const { period, pack } = props;
const fetch = useFetch();
const [price, setPrice] = useState<number | false>(0);
const [loading, setLoading] = useState(false);
@ -51,7 +49,6 @@ export const Prorate: FC<{
await fetch('/billing/prorate', {
method: 'POST',
body: JSON.stringify({
total: totalChannels,
period,
billing: pack,
}),
@ -65,7 +62,7 @@ export const Prorate: FC<{
useEffect(() => {
setPrice(false);
calculatePrice();
}, [totalChannels, period, pack]);
}, [period, pack]);
if (loading) {
return (
@ -88,12 +85,11 @@ export const Prorate: FC<{
export const Features: FC<{
pack: 'FREE' | 'STANDARD' | 'PRO';
channels: number;
}> = (props) => {
const { pack, channels } = props;
const { pack } = props;
const features = useMemo(() => {
const currentPricing = pricing[pack];
const channelsOr = currentPricing.channel || channels;
const channelsOr = currentPricing.channel;
const list = [];
list.push(`${channelsOr} ${channelsOr === 1 ? 'channel' : 'channels'}`);
list.push(
@ -124,7 +120,7 @@ export const Features: FC<{
}
return list;
}, [pack, channels]);
}, [pack]);
return (
<div className="flex flex-col gap-[10px] justify-center text-[16px] text-[#AAA]">
@ -152,10 +148,9 @@ export const Features: FC<{
};
export const MainBillingComponent: FC<{
tiers: Tiers;
sub?: Subscription;
}> = (props) => {
const { tiers, sub } = props;
const { sub } = props;
const { mutate } = useSWRConfig();
const fetch = useFetch();
const toast = useToaster();
@ -176,17 +171,17 @@ export const MainBillingComponent: FC<{
const [initialChannels, setInitialChannels] = useState(
sub?.totalChannels || 1
);
const [totalChannels, setTotalChannels] = useState<number>(initialChannels);
useEffect(() => {
if (initialChannels !== sub?.totalChannels) {
setTotalChannels(sub?.totalChannels || 1);
setInitialChannels(sub?.totalChannels || 1);
}
if (period !== sub?.period) {
setPeriod(sub?.period || 'MONTHLY');
setMonthlyOrYearly((sub?.period || 'MONTHLY') === 'MONTHLY' ? 'off' : 'on');
setMonthlyOrYearly(
(sub?.period || 'MONTHLY') === 'MONTHLY' ? 'off' : 'on'
);
}
setSubscription(sub);
@ -201,9 +196,6 @@ export const MainBillingComponent: FC<{
if (!subscription) {
return 'FREE';
}
if (initialChannels !== totalChannels) {
return '';
}
if (period === 'YEARLY' && monthlyOrYearly === 'off') {
return '';
@ -214,26 +206,11 @@ export const MainBillingComponent: FC<{
}
return subscription?.subscriptionTier;
}, [subscription, totalChannels, initialChannels, monthlyOrYearly, period]);
const currentDisplay = useMemo(() => {
return sortBy(
[
{ name: 'Free', price: 0 },
...(monthlyOrYearly === 'on' ? tiers.year : tiers.month),
],
(p) => ['Free', 'Standard', 'Pro'].indexOf(p.name)
);
}, [monthlyOrYearly]);
}, [subscription, initialChannels, monthlyOrYearly, period]);
const moveToCheckout = useCallback(
(billing: 'STANDARD' | 'PRO' | 'FREE') => async () => {
const messages = [];
const beforeTotalChannels = pricing[billing].channel || initialChannels;
if (totalChannels < beforeTotalChannels) {
messages.push(`Some of the channels will be disabled`);
}
if (
!pricing[billing].team_members &&
@ -284,7 +261,6 @@ export const MainBillingComponent: FC<{
await fetch('/billing/subscribe', {
method: 'POST',
body: JSON.stringify({
total: totalChannels,
period: monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY',
billing,
}),
@ -307,8 +283,6 @@ export const MainBillingComponent: FC<{
window.open(portal);
}
} else {
setTotalChannels(totalChannels);
setInitialChannels(totalChannels);
setPeriod(monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY');
setSubscription((subs) => ({
...subs!,
@ -319,7 +293,6 @@ export const MainBillingComponent: FC<{
'/user/self',
{
...user,
totalChannels,
tier: billing,
},
{
@ -331,7 +304,7 @@ export const MainBillingComponent: FC<{
setLoading(false);
},
[monthlyOrYearly, totalChannels, subscription, user]
[monthlyOrYearly, subscription, user]
);
return (
@ -346,34 +319,26 @@ export const MainBillingComponent: FC<{
<div>YEARLY</div>
</div>
</div>
<div className="flex flex-col items-center gap-[10px]">
<div>Total Channels</div>
<div className="w-[60%]">
<Track
min={1}
max={60}
value={totalChannels}
onChange={setTotalChannels}
/>
</div>
</div>
<div className="flex gap-[16px]">
{currentDisplay.map((p) => (
{Object.entries(pricing).map(([name, values]) => (
<div
key={p.name}
key={name}
className="flex-1 bg-sixth border border-[#172034] rounded-[4px] p-[24px] gap-[16px] flex flex-col"
>
<div className="text-[18px]">{p.name}</div>
<div className="text-[18px]">{name}</div>
<div className="text-[38px] flex gap-[2px] items-center">
<div>{p.price ? '$' + totalChannels * p.price : p.name}</div>
{!!p.price && (
<div className={`text-[14px] ${interClass} text-[#AAA]`}>
{monthlyOrYearly === 'on' ? '/year' : '/month'}
</div>
)}
<div>
$
{monthlyOrYearly === 'on'
? values.year_price
: values.month_price}
</div>
<div className={`text-[14px] ${interClass} text-[#AAA]`}>
{monthlyOrYearly === 'on' ? '/year' : '/month'}
</div>
</div>
<div className="text-[14px] flex gap-[10px]">
{currentPackage === p.name.toUpperCase() &&
{currentPackage === name.toUpperCase() &&
subscription?.cancelAt ? (
<div className="gap-[3px] flex flex-col">
<div>
@ -384,24 +349,24 @@ export const MainBillingComponent: FC<{
</div>
) : (
<Button
loading={loading && !!p.price}
loading={loading}
disabled={
(!!subscription?.cancelAt &&
p.name.toUpperCase() === 'FREE') ||
currentPackage === p.name.toUpperCase()
name.toUpperCase() === 'FREE') ||
currentPackage === name.toUpperCase()
}
className={clsx(
subscription &&
p.name.toUpperCase() === 'FREE' &&
name.toUpperCase() === 'FREE' &&
'!bg-red-500'
)}
onClick={moveToCheckout(
p.name.toUpperCase() as 'STANDARD' | 'PRO'
name.toUpperCase() as 'STANDARD' | 'PRO'
)}
>
{currentPackage === p.name.toUpperCase()
{currentPackage === name.toUpperCase()
? 'Current Plan'
: p.name.toUpperCase() === 'FREE'
: name.toUpperCase() === 'FREE'
? subscription?.cancelAt
? `Downgrade on ${dayjs
.utc(subscription?.cancelAt)
@ -412,18 +377,17 @@ export const MainBillingComponent: FC<{
</Button>
)}
{subscription &&
currentPackage !== p.name.toUpperCase() &&
!!p.price && (
currentPackage !== name.toUpperCase() &&
name !== 'FREE' &&
!!name && (
<Prorate
period={monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY'}
pack={p.name.toUpperCase() as 'STANDARD' | 'PRO'}
totalChannels={totalChannels}
pack={name.toUpperCase() as 'STANDARD' | 'PRO'}
/>
)}
</div>
<Features
pack={p.name.toUpperCase() as 'FREE' | 'STANDARD' | 'PRO'}
channels={totalChannels}
pack={name.toUpperCase() as 'FREE' | 'STANDARD' | 'PRO'}
/>
</div>
))}

View File

@ -6,6 +6,7 @@ import React, {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import dayjs from 'dayjs';
@ -33,23 +34,30 @@ import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.titl
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
import { ProvidersOptions } from '@gitroom/frontend/components/launches/providers.options';
import { v4 as uuidv4 } from 'uuid';
import { useSWRConfig } from 'swr';
import useSWR, { useSWRConfig } from 'swr';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
import { DatePicker } from '@gitroom/frontend/components/launches/helpers/date.picker';
import { arrayMoveImmutable } from 'array-move';
import { linkedinCompany } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
import {
Information,
PostToOrganization,
} from '@gitroom/frontend/components/launches/post.to.organization';
import { Submitted } from '@gitroom/frontend/components/launches/submitted';
export const AddEditModal: FC<{
date: dayjs.Dayjs;
integrations: Integrations[];
reopenModal: () => void;
}> = (props) => {
const { date, integrations } = props;
const { date, integrations, reopenModal } = props;
const [dateState, setDateState] = useState(date);
const { mutate } = useSWRConfig();
// hook to open a new modal
const modal = useModals();
// selected integrations to allow edit
const [selectedIntegrations, setSelectedIntegrations] = useState<
Integrations[]
@ -66,6 +74,11 @@ export const AddEditModal: FC<{
const fetch = useFetch();
const updateOrder = useCallback(() => {
modal.closeAll();
reopenModal();
}, [reopenModal, modal]);
// prevent the window exit by mistake
usePreventWindowUnload(true);
@ -77,12 +90,12 @@ export const AddEditModal: FC<{
const [showError, setShowError] = useState(false);
// hook to open a new modal
const modal = useModals();
// are we in edit mode?
const existingData = useExistingData();
// Post for
const [postFor, setPostFor] = useState<Information | undefined>();
const expend = useExpend();
const toaster = useToaster();
@ -253,6 +266,7 @@ export const AddEditModal: FC<{
await fetch('/posts', {
method: 'POST',
body: JSON.stringify({
...(postFor ? { order: postFor.id } : {}),
type,
date: dateState.utc().format('YYYY-MM-DDTHH:mm:ss'),
posts: allKeys,
@ -269,12 +283,41 @@ export const AddEditModal: FC<{
);
modal.closeAll();
},
[]
[postFor, dateState, value, integrations, existingData]
);
const getPostsMarketplace = useCallback(async () => {
return (
await fetch(`/posts/marketplace/${existingData?.posts?.[0]?.id}`)
).json();
}, []);
const { data } = useSWR(
`/posts/marketplace/${existingData?.posts?.[0]?.id}`,
getPostsMarketplace
);
const canSendForPublication = useMemo(() => {
if (!postFor) {
return true;
}
return selectedIntegrations.every((integration) => {
const find = postFor.missing.find(
(p) => p.integration.integration.id === integration.id
);
if (!find) {
return false;
}
return find.missing !== 0;
});
}, [data, postFor, selectedIntegrations]);
return (
<>
<div className="flex gap-[20px] bg-black">
<div className={clsx('flex gap-[20px] bg-black')}>
<div
className={clsx(
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
@ -284,13 +327,16 @@ export const AddEditModal: FC<{
)}
>
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-[#172034] bg-[#0B101B] p-[16px] pt-0">
<TopTitle
title={existingData?.group ? 'Edit Post' : 'Create Post'}
/>
<div className="absolute h-[57px] right-0 top-0 flex justify-center items-center">
<DatePicker onChange={setDateState} date={dateState} />
</div>
<TopTitle title={existingData?.group ? 'Edit Post' : 'Create Post'}>
<div className="flex items-center">
<PostToOrganization
selected={existingData?.posts?.[0]?.submittedForOrderId!}
information={data}
onChange={setPostFor}
/>
<DatePicker onChange={setDateState} date={dateState} />
</div>
</TopTitle>
{!existingData.integration && (
<PickPlatforms
@ -415,55 +461,72 @@ export const AddEditModal: FC<{
>
Cancel
</Button>
{!!existingData.integration && (
<Submitted
updateOrder={updateOrder}
postId={existingData?.posts?.[0]?.id}
status={existingData?.posts?.[0]?.approvedSubmitForOrder}
>
{!!existingData.integration && (
<Button
onClick={schedule('delete')}
className="rounded-[4px] border-2 border-red-400 text-red-400"
secondary={true}
>
Delete Post
</Button>
)}
<Button
onClick={schedule('delete')}
className="rounded-[4px] border-2 border-red-400 text-red-400"
onClick={schedule('draft')}
className="rounded-[4px] border-2 border-[#506490]"
secondary={true}
disabled={selectedIntegrations.length === 0}
>
Delete Post
Save as draft
</Button>
)}
<Button
onClick={schedule('draft')}
className="rounded-[4px] border-2 border-[#506490]"
secondary={true}
disabled={selectedIntegrations.length === 0}
>
Save as draft
</Button>
<Button
onClick={schedule('schedule')}
className="rounded-[4px] relative group"
disabled={selectedIntegrations.length === 0}
>
<div className="flex justify-center items-center gap-[5px] h-full">
<div className="h-full flex items-center">
{!existingData.integration ? 'Add to calendar' : 'Update'}
</div>
<div className="h-full flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M15.0233 7.14804L9.39828 12.773C9.34604 12.8253 9.284 12.8668 9.21572 12.8951C9.14743 12.9234 9.07423 12.938 9.00031 12.938C8.92639 12.938 8.8532 12.9234 8.78491 12.8951C8.71662 12.8668 8.65458 12.8253 8.60234 12.773L2.97734 7.14804C2.8718 7.04249 2.8125 6.89934 2.8125 6.75007C2.8125 6.6008 2.8718 6.45765 2.97734 6.3521C3.08289 6.24655 3.22605 6.18726 3.37531 6.18726C3.52458 6.18726 3.66773 6.24655 3.77328 6.3521L9.00031 11.5798L14.2273 6.3521C14.2796 6.29984 14.3417 6.25838 14.4099 6.2301C14.4782 6.20181 14.5514 6.18726 14.6253 6.18726C14.6992 6.18726 14.7724 6.20181 14.8407 6.2301C14.909 6.25838 14.971 6.29984 15.0233 6.3521C15.0755 6.40436 15.117 6.46641 15.1453 6.53469C15.1736 6.60297 15.1881 6.67616 15.1881 6.75007C15.1881 6.82398 15.1736 6.89716 15.1453 6.96545C15.117 7.03373 15.0755 7.09578 15.0233 7.14804Z"
fill="white"
/>
</svg>
<div
onClick={postNow}
className="hidden group-hover:flex hover:flex flex-col justify-center absolute left-0 top-[100%] w-full h-[40px] bg-[#B91C1C] border border-tableBorder"
>
Post now
<Button
onClick={schedule('schedule')}
className="rounded-[4px] relative group"
disabled={
selectedIntegrations.length === 0 ||
!canSendForPublication
}
>
<div className="flex justify-center items-center gap-[5px] h-full">
<div className="h-full flex items-center">
{!canSendForPublication
? 'Not matching order'
: postFor
? 'Submit for order'
: !existingData.integration
? 'Add to calendar'
: 'Update'}
</div>
{!postFor && (
<div className="h-full flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M15.0233 7.14804L9.39828 12.773C9.34604 12.8253 9.284 12.8668 9.21572 12.8951C9.14743 12.9234 9.07423 12.938 9.00031 12.938C8.92639 12.938 8.8532 12.9234 8.78491 12.8951C8.71662 12.8668 8.65458 12.8253 8.60234 12.773L2.97734 7.14804C2.8718 7.04249 2.8125 6.89934 2.8125 6.75007C2.8125 6.6008 2.8718 6.45765 2.97734 6.3521C3.08289 6.24655 3.22605 6.18726 3.37531 6.18726C3.52458 6.18726 3.66773 6.24655 3.77328 6.3521L9.00031 11.5798L14.2273 6.3521C14.2796 6.29984 14.3417 6.25838 14.4099 6.2301C14.4782 6.20181 14.5514 6.18726 14.6253 6.18726C14.6992 6.18726 14.7724 6.20181 14.8407 6.2301C14.909 6.25838 14.971 6.29984 15.0233 6.3521C15.0755 6.40436 15.117 6.46641 15.1453 6.53469C15.1736 6.60297 15.1881 6.67616 15.1881 6.75007C15.1881 6.82398 15.1736 6.89716 15.1453 6.96545C15.117 7.03373 15.0755 7.09578 15.0233 7.14804Z"
fill="white"
/>
</svg>
<div
onClick={postNow}
className="hidden group-hover:flex hover:flex flex-col justify-center absolute left-0 top-[100%] w-full h-[40px] bg-[#B91C1C] border border-tableBorder"
>
Post now
</div>
</div>
)}
</div>
</div>
</Button>
</Button>
</Submitted>
</div>
</div>
</div>

View File

@ -18,6 +18,7 @@ import { useAddProvider } from '@gitroom/frontend/components/launches/add.provid
import { CommentComponent } from '@gitroom/frontend/components/launches/comments/comment.component';
import { useSWRConfig } from 'swr';
import { useIntersectionObserver } from '@uidotdev/usehooks';
import { useToaster } from '@gitroom/react/toaster/toaster';
export const days = [
'',
@ -203,6 +204,7 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
changeDate,
} = useCalendar();
const toaster = useToaster();
const modal = useModals();
const fetch = useFetch();
@ -243,15 +245,21 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
const [{ canDrop }, drop] = useDrop(() => ({
accept: 'post',
drop: (item: any) => {
drop: async (item: any) => {
if (isBeforeNow) return;
fetch(`/posts/${item.id}/date`, {
const { status } = await fetch(`/posts/${item.id}/date`, {
method: 'PUT',
body: JSON.stringify({
date: getDate.utc().format('YYYY-MM-DDTHH:mm:ss'),
}),
});
changeDate(item.id, getDate);
if (status !== 500) {
changeDate(item.id, getDate);
return;
}
toaster.show('Can\'t change date, remove post from publication', 'warning');
},
collect: (monitor) => ({
canDrop: isBeforeNow ? false : !!monitor.canDrop() && !!monitor.isOver(),
@ -272,6 +280,7 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
children: (
<ExistingDataContextProvider value={data}>
<AddEditModal
reopenModal={editPost(id)}
integrations={integrations.filter(
(f) => f.id === data.integration
)}
@ -294,7 +303,13 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
classNames: {
modal: 'bg-transparent text-white',
},
children: <AddEditModal integrations={integrations} date={getDate} />,
children: (
<AddEditModal
reopenModal={() => ({})}
integrations={integrations}
date={getDate}
/>
),
size: '80%',
// title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
});

View File

@ -1,16 +1,18 @@
import {FC} from "react";
import { FC, ReactNode } from 'react';
export const TopTitle: FC<{
title: string;
shouldExpend?: boolean;
expend?: () => void;
collapse?: () => void;
children?: ReactNode;
}> = (props) => {
const { title, shouldExpend, expend, collapse } = props;
const { title, children, shouldExpend, expend, collapse } = props;
return (
<div className="h-[57px] border-b flex items-center border-[#172034] px-[16px] -mx-[16px]">
<div className="flex-1">{title}</div>
{children}
{shouldExpend !== undefined && (
<div className="cursor-pointer">
{!shouldExpend ? (

View File

@ -5,7 +5,7 @@ const ExistingDataContext = createContext({
integration: '',
group: undefined as undefined | string,
posts: [] as Post[],
settings: {} as any
settings: {} as any,
});

View File

@ -0,0 +1,90 @@
'use client';
import { FC, useEffect } from 'react';
import { CustomSelect } from '@gitroom/react/form/custom.select';
import { FormProvider, useForm } from 'react-hook-form';
export interface Information {
buyer: Buyer;
usedIds: Array<{ id: string; status: 'NO' | 'WAITING_CONFIRMATION' | 'YES' }>;
id: string;
missing: Missing[];
}
export interface Buyer {
id: string;
name: string;
picture: Picture;
}
export interface Picture {
id: string;
path: string;
}
export interface Missing {
integration: Integration;
missing: number;
}
export interface Integration {
quantity: number;
integration: Integration2;
}
export interface Integration2 {
id: string;
name: string;
providerIdentifier: string;
}
export const PostToOrganization: FC<{
information: Information[];
onChange: (order?: Information) => void;
selected?: string;
}> = (props) => {
const { information, onChange, selected } = props;
const form = useForm();
const postFor = form.watch('post_for');
useEffect(() => {
onChange(information?.find((p) => p.id === postFor?.value)!);
}, [postFor]);
useEffect(() => {
if (!selected || !information?.length) {
return;
}
const findIt = information?.find((p) => p.id === selected);
form.setValue('post_for', {
value: findIt?.id,
});
onChange(information?.find((p) => p.id === selected)!);
}, [selected, information]);
if (!information?.length) {
return null;
}
return (
<FormProvider {...form}>
<CustomSelect
className="w-[240px]"
removeError={true}
label=""
placeholder="Select order from marketplace"
name="post_for"
options={information?.map((p) => ({
label: 'For: ' + p?.buyer?.name,
value: p?.id,
icon: (
<img
src={p?.buyer?.picture?.path}
className="w-[24px] h-[24px] rounded-full"
/>
),
}))}
/>
</FormProvider>
);
};

View File

@ -76,6 +76,7 @@ export const withProvider = (
id?: string;
image?: Array<{ path: string; id: string }>;
}>;
hideMenu?: boolean;
show: boolean;
}) => {
const existingData = useExistingData();
@ -228,40 +229,42 @@ export const withProvider = (
<FormProvider {...form}>
<SetTab changeTab={() => setShowTab(0)} />
<div className="mt-[15px] w-full flex flex-col flex-1">
<div className="flex gap-[4px]">
<div className="flex-1 flex">
<Button
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
secondary={showTab !== 0}
onClick={() => setShowTab(0)}
>
Preview
</Button>
</div>
{!!SettingsComponent && (
{!props.hideMenu && (
<div className="flex gap-[4px]">
<div className="flex-1 flex">
<Button
className={clsx(
'flex-1 overflow-hidden whitespace-nowrap',
showTab === 2 && 'rounded-[4px]'
)}
secondary={showTab !== 2}
onClick={() => setShowTab(2)}
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
secondary={showTab !== 0}
onClick={() => setShowTab(0)}
>
Settings
Preview
</Button>
</div>
{!!SettingsComponent && (
<div className="flex-1 flex">
<Button
className={clsx(
'flex-1 overflow-hidden whitespace-nowrap',
showTab === 2 && 'rounded-[4px]'
)}
secondary={showTab !== 2}
onClick={() => setShowTab(2)}
>
Settings
</Button>
</div>
)}
<div className="flex-1 flex">
<Button
className="rounded-[4px] flex-1 !bg-red-700 overflow-hidden whitespace-nowrap"
secondary={showTab !== 1}
onClick={changeToEditor}
>
{editInPlace ? 'Edit globally' : 'Edit only this'}
</Button>
</div>
)}
<div className="flex-1 flex">
<Button
className="rounded-[4px] flex-1 !bg-red-700 overflow-hidden whitespace-nowrap"
secondary={showTab !== 1}
onClick={changeToEditor}
>
{editInPlace ? 'Edit globally' : 'Edit only this'}
</Button>
</div>
</div>
)}
{editInPlace &&
createPortal(
<EditorWrapper>
@ -285,7 +288,10 @@ export const withProvider = (
.filter((f) => f.name !== 'image'),
newImage,
postSelector(date),
...linkedinCompany(integration?.identifier!, integration?.id!),
...linkedinCompany(
integration?.identifier!,
integration?.id!
),
]}
preview="edit"
// @ts-ignore

View File

@ -7,7 +7,7 @@ import RedditProvider from "@gitroom/frontend/components/launches/providers/redd
import MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider";
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
const Providers = [
export const Providers = [
{identifier: 'devto', component: DevtoProvider},
{identifier: 'x', component: XProvider},
{identifier: 'linkedin', component: LinkedinProvider},

View File

@ -0,0 +1,39 @@
import React, { FC, ReactNode, useCallback } from 'react';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
export const Submitted: FC<{
children: ReactNode;
postId: string;
status: 'YES' | 'NO' | 'WAITING_CONFIRMATION';
updateOrder: () => void;
}> = (props) => {
const { postId, updateOrder, status, children } = props;
const fetch = useFetch();
const cancel = useCallback(async () => {
if (!await deleteDialog('Are you sure you want to cancel this publication?', 'Yes')) {
return ;
}
await fetch(`/marketplace/posts/${postId}/cancel`, {
method: 'POST'
});
updateOrder();
}, [postId]);
if (!status || status === 'NO') {
return <>{children}</>;
}
return (
<Button
className="rounded-[4px] border-2 border-red-400 text-red-400"
secondary={true}
onClick={cancel}
>
Cancel publication
</Button>
);
};

View File

@ -23,12 +23,6 @@ export const menuItems = [
path: '/settings',
role: ['ADMIN', 'SUPERADMIN'],
},
{
name: 'Billing',
icon: 'billing',
path: '/billing',
role: ['ADMIN', 'SUPERADMIN'],
},
{
name: 'Marketplace',
icon: 'marketplace',
@ -39,6 +33,12 @@ export const menuItems = [
icon: 'messages',
path: '/messages',
},
{
name: 'Billing',
icon: 'billing',
path: '/billing',
role: ['ADMIN', 'SUPERADMIN'],
},
];
export const TopMenu: FC = () => {

View File

@ -27,6 +27,7 @@ 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';
import { OrderList } from '@gitroom/frontend/components/marketplace/order.list';
export interface Root {
list: List[];
@ -295,14 +296,19 @@ export const RequestService: FC<{ toId: string; name: string }> = (props) => {
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}`);
}, []);
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)}>
@ -512,29 +518,36 @@ export const Buyer = () => {
const { data: list } = useSWR<Root>('search' + services + page, marketplace);
return (
<div className="flex mt-[29px] w-full gap-[43px]">
<div className="w-[330px]">
<div className="flex flex-col gap-[16px]">
<h2 className="text-[20px]">Filter</h2>
<div className="flex flex-col">
{tagsList.map((tag) => (
<Options
search={true}
key={tag.key}
options={tag.options}
title={tag.name}
/>
))}
<>
<div>
<OrderList type="buyer" />
</div>
<div className="flex mt-[29px] w-full gap-[43px]">
<div className="w-[330px]">
<div className="flex flex-col gap-[16px]">
<h2 className="text-[20px]">Filter</h2>
<div className="flex flex-col">
{tagsList.map((tag) => (
<Options
search={true}
key={tag.key}
options={tag.options}
title={tag.name}
/>
))}
</div>
</div>
</div>
<div className="flex-1 gap-[16px] flex-col flex">
<div className="text-[20px] text-right">
{list?.count || 0} Result
</div>
{list?.list?.map((item, index) => (
<Card key={String(index)} data={item} />
))}
<Pagination results={list?.count || 0} />
</div>
</div>
<div className="flex-1 gap-[16px] flex-col flex">
<div className="text-[20px] text-right">{list?.count || 0} Result</div>
{list?.list?.map((item, index) => (
<Card key={String(index)} data={item} />
))}
<Pagination results={list?.count || 0} />
</div>
</div>
</>
);
};

View File

@ -0,0 +1,40 @@
'use client';
import { createContext } from 'react';
import { Orders } from '@prisma/client';
export interface Root2 {
id: string;
buyerId: string;
sellerId: string;
createdAt: string;
updatedAt: string;
buyer: SellerBuyer;
seller: SellerBuyer;
messages: Message[];
orders: Orders[];
}
export interface SellerBuyer {
id: string;
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;
}
export const MarketplaceProvider = createContext<{message?: Root2}>({});

View File

@ -0,0 +1,78 @@
import React, { FC, useCallback, useMemo } from 'react';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
export const OrderList: FC<{ type: 'seller' | 'buyer' }> = (props) => {
const fetch = useFetch();
const { type } = props;
const getOrderDetails = useCallback(async () => {
return (await fetch(`/marketplace/orders?type=${type}`)).json();
}, [type]);
const { data, isLoading } = useSWR(
`/marketplace/orders/${type}`,
getOrderDetails
);
const biggerRow = useMemo(() => {
return data?.orders?.reduce((all: any, current: any) => {
if (current.details.length > all) return current.details.length;
return all;
}, 0);
}, [data]);
if (isLoading || !data?.orders?.length) return <></>;
return (
<div className="bg-sixth p-[24px] flex flex-col gap-[24px] border border-[#172034] rounded-[4px]">
<h3 className="text-[24px]">Orders</h3>
<div className="pt-[20px] px-[24px] border border-[#172034] flex">
<table className="w-full">
<tr>
<td colSpan={biggerRow + 1} className="pb-[20px]">
{type === 'seller' ? 'Buyer' : 'Seller'}
</td>
<td className="pb-[20px]">Price</td>
<td className="pb-[20px]">State</td>
</tr>
{data.orders.map((order: any) => (
<tr key={order.id}>
<td className="pb-[20px]">{order.name}</td>
{order.details.map((details: any, index: number) => (
<td
className="pb-[20px]"
key={details.id}
{...(index === order.details.length - 1
? { colSpan: biggerRow - order.details.length + 1 }
: {})}
>
<div className="flex gap-[20px] items-center">
<div className="relative">
<img
src={details.integration.picture}
alt="platform"
className="w-[24px] h-[24px] rounded-full"
/>
<img
className="absolute left-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
src={`/icons/platforms/${details.integration.providerIdentifier}.png`}
alt={details.integration.name}
/>
</div>
<div>
{details.integration.name} ({details.total}/
{details.submitted})
</div>
</div>
</td>
))}
<td className="pb-[20px]">{order.price}</td>
<td className="pb-[20px]">{order.status}</td>
</tr>
))}
</table>
</div>
</div>
);
};

View File

@ -0,0 +1,366 @@
import React, { FC, useCallback, useContext, useMemo, useState } from 'react';
import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/marketplace.provider';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { useModals } from '@mantine/modals';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { Input } from '@gitroom/react/form/input';
import { CustomSelect } from '@gitroom/react/form/custom.select';
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
import { Total } from '@gitroom/react/form/total';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { Button } from '@gitroom/react/form/button';
import { array, number, object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
const schema = object({
socialMedia: array()
.min(1)
.of(
object({
total: number().required(),
value: object({
value: string().required('Platform is required'),
}).required(),
price: string().matches(/^\d+$/, 'Price must be a number').required(),
})
)
.required(),
}).required();
export const NewOrder: FC<{ group: string }> = (props) => {
const { group } = props;
const modal = useModals();
const fetch = useFetch();
const [update, setUpdate] = useState(0);
const toast = useToaster();
const loadIntegrations = useCallback(async () => {
return (
await (await fetch('/integrations/list')).json()
).integrations.filter((f: any) => !f.disabled);
}, []);
const { data } = useSWR('integrations', loadIntegrations);
const options: Array<{ label: string; value: string; icon: string }> =
useMemo(() => {
if (!data) {
return [];
}
return data?.map((p: any) => ({
label: p.name,
value: p.identifier,
id: p.id,
icon: (
<div className="relative">
<img
className="w-[20px] h-[20px] rounded-full"
src={p.picture}
alt={p.name}
/>
<img
className="absolute left-[10px] top-[10px] w-[15px] h-[15px] rounded-full"
src={`/icons/platforms/${p.identifier}.png`}
alt={p.name}
/>
</div>
),
}));
}, [data]);
const change = useCallback(() => {
setUpdate((prev) => prev + 1);
}, [update]);
const form = useForm<{
price: string;
socialMedia: Array<{ value?: string; total: number; price: any }>;
}>({
values: {
price: '',
socialMedia: [{ value: undefined, total: 1, price: '' }],
},
criteriaMode: 'all',
// @ts-ignore
resolver: yupResolver(schema),
mode: 'onChange',
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'socialMedia',
});
const possibleOptions = useMemo(() => {
return fields.map((z, index) => {
const field = form.getValues(`socialMedia.${index}.value`) as {
value?: { value?: string; total?: number };
};
return options.filter((f) => {
const getAllValues = fields.reduce((all, p, innerIndex) => {
if (index === innerIndex) {
return all;
}
const newField = form.getValues(
`socialMedia.${innerIndex}.value`
) as { value?: { value?: string } };
all.push(newField);
return all;
}, [] as any[]);
return (
field?.value?.value === f.value ||
!getAllValues.some((v) => v?.value === f.value)
);
});
});
}, [update, fields, options]);
const canAddMoreOptions = useMemo(() => {
return fields.length < options.length;
}, [update, fields, options]);
const close = useCallback(() => {
return modal.closeAll();
}, []);
const submit = useCallback(async (data: any) => {
await (
await fetch('/marketplace/offer', {
method: 'POST',
body: JSON.stringify({
group,
socialMedia: data.socialMedia.map((z: any) => ({
total: z.total,
price: +z.price,
value: z.value.id,
})),
}),
})
).json();
toast.show('Offer sent successfully');
modal.closeAll();
}, []);
const totalPrice = useMemo(() => {
return fields.reduce((total, field, index) => {
return (
total +
(+(form.getValues(`socialMedia.${index}.price`) || 0) *
form.getValues(`socialMedia.${index}.total`) || 0)
);
}, 0);
}, [update, fields, options]);
return (
<form onSubmit={form.handleSubmit(submit)}>
<FormProvider {...form}>
<div className="w-full max-w-[647px] 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 new offer`} />
<div className="p-[16px] -mx-[16px]">
{fields.map((field, index) => (
<div className="relative flex gap-[10px]" key={field.id}>
{index !== 0 && (
<div
onClick={() => remove(index)}
className="cursor-pointer top-[3px] z-[99] w-[15px] h-[15px] bg-red-500 rounded-full text-white absolute left-[60px] text-[12px] flex justify-center items-center pb-[2px] select-none"
>
x
</div>
)}
<div className="flex-1">
<CustomSelect
{...form.register(`socialMedia.${index}.value`)}
onChange={change}
options={possibleOptions[index]}
placeholder="Select social media"
label="Platform"
disableForm={true}
/>
</div>
<div>
<Total
customOnChange={change}
{...form.register(`socialMedia.${index}.total`)}
/>
</div>
<div>
<Input
icon={<div className="text-[14px]">$</div>}
className="text-[14px]"
label="Price per post"
error={
form.formState.errors?.socialMedia?.[index]?.price
?.message
}
customUpdate={change}
name={`socialMedia.${index}.price`}
/>
</div>
</div>
))}
{canAddMoreOptions && (
<div>
<div
onClick={() =>
append({ value: undefined, total: 1, price: '' })
}
className="select-none rounded-[4px] border-2 border-[#506490] flex py-[9.5px] px-[24px] items-center gap-[4px] text-[14px] float-left cursor-pointer"
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M15.75 9C15.75 9.14918 15.6907 9.29226 15.5852 9.39775C15.4798 9.50324 15.3367 9.5625 15.1875 9.5625H9.5625V15.1875C9.5625 15.3367 9.50324 15.4798 9.39775 15.5852C9.29226 15.6907 9.14918 15.75 9 15.75C8.85082 15.75 8.70774 15.6907 8.60225 15.5852C8.49676 15.4798 8.4375 15.3367 8.4375 15.1875V9.5625H2.8125C2.66332 9.5625 2.52024 9.50324 2.41475 9.39775C2.30926 9.29226 2.25 9.14918 2.25 9C2.25 8.85082 2.30926 8.70774 2.41475 8.60225C2.52024 8.49676 2.66332 8.4375 2.8125 8.4375H8.4375V2.8125C8.4375 2.66332 8.49676 2.52024 8.60225 2.41475C8.70774 2.30926 8.85082 2.25 9 2.25C9.14918 2.25 9.29226 2.30926 9.39775 2.41475C9.50324 2.52024 9.5625 2.66332 9.5625 2.8125V8.4375H15.1875C15.3367 8.4375 15.4798 8.49676 15.5852 8.60225C15.6907 8.70774 15.75 8.85082 15.75 9Z"
fill="white"
/>
</svg>
</div>
<div>Add another platform</div>
</div>
</div>
)}
</div>
<div className="py-[16px] flex justify-end">
<Button type="submit" className="rounded-[4px]">
Send an offer for ${totalPrice}
</Button>
</div>
</div>
</div>
</FormProvider>
</form>
);
};
export const OrderInProgress: FC<{ group: string; buyer: boolean, order: string }> = (
props
) => {
const { group, buyer, order } = props;
const fetch = useFetch();
const completeOrder = useCallback(async () => {
if (await deleteDialog('Are you sure you want to pay the seller and end the order? this is irreversible action')) {
await (
await fetch(`/marketplace/offer/${order}/complete`, {
method: 'POST',
})
).json();
}
}, [order]);
return (
<div className="flex gap-[10px]">
{buyer && (
<div onClick={completeOrder} className="rounded-[34px] border-[1px] border-[#506490] !bg-[#0B101B] h-[28px] justify-center items-center text-[12px] px-[12px] flex font-[600] cursor-pointer">
Complete order and pay early
</div>
)}
<div className="h-[28px] justify-center items-center bg-[#32D583] text-[12px] px-[12px] flex rounded-[34px] font-[600]">
Order in progress
</div>
</div>
);
};
export const CreateNewOrder: FC<{ group: string }> = (props) => {
const { group } = props;
const modals = useModals();
const createOrder = useCallback(() => {
modals.openModal({
classNames: {
modal: 'bg-transparent text-white',
},
withCloseButton: false,
size: '100%',
children: <NewOrder group={group} />,
});
}, [group]);
return (
<div
className="h-[28px] justify-center items-center bg-[#32D583] text-[12px] px-[12px] flex rounded-[34px] font-[600] cursor-pointer"
onClick={createOrder}
>
Create a new offer
</div>
);
};
enum OrderOptions {
CREATE_A_NEW_ORDER = 'CREATE_A_NEW_ORDER',
IN_PROGRESS = 'IN_PROGRESS',
WAITING_PUBLICATION = 'WAITING_PUBLICATION',
}
export const OrderTopActions = () => {
const { message } = useContext(MarketplaceProvider);
const user = useUser();
const isBuyer = useMemo(() => {
return user?.id === message?.buyerId;
}, [user, message]);
const myOptions: OrderOptions | undefined = useMemo(() => {
if (
!isBuyer &&
(!message?.orders.length ||
message.orders[0].status === 'COMPLETED' ||
message.orders[0].status === 'CANCELED')
) {
return OrderOptions.CREATE_A_NEW_ORDER;
}
if (message?.orders?.[0]?.status === 'PENDING') {
return OrderOptions.IN_PROGRESS;
}
if (message?.orders?.[0]?.status === 'ACCEPTED') {
return OrderOptions.WAITING_PUBLICATION;
}
}, [isBuyer, user, message]);
if (!myOptions) {
return null;
}
switch (myOptions) {
case OrderOptions.CREATE_A_NEW_ORDER:
return <CreateNewOrder group={message?.id!} />;
case OrderOptions.WAITING_PUBLICATION:
return <OrderInProgress group={message?.id!} buyer={isBuyer} order={message?.orders[0]?.id!} />;
}
return <div />;
};

View File

@ -0,0 +1,33 @@
import 'reflect-metadata';
import { FC, useCallback } from 'react';
import { Post as PrismaPost } from '.prisma/client';
import { Providers } from '@gitroom/frontend/components/launches/providers/show.all.providers';
export const PreviewPopupDynamic: FC<{
postId: string;
providerId: string;
post: {
integration: string;
group: string;
posts: PrismaPost[];
settings: any;
};
}> = (props) => {
const { component: ProviderComponent } = Providers.find(
(p) => p.identifier === props.providerId
)!;
return (
<ProviderComponent
hideMenu={true}
show={true}
identifier={props.post.integration}
// @ts-ignore
value={props.post.posts.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))}
/>
);
};

View File

@ -9,6 +9,7 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { Input } from '@gitroom/react/form/input';
import { useDebouncedCallback } from 'use-debounce';
import { OrderList } from '@gitroom/frontend/components/marketplace/order.list';
export const Seller = () => {
const fetch = useFetch();
@ -106,75 +107,80 @@ export const Seller = () => {
}
return (
<div className="flex mt-[29px] w-full gap-[26px]">
<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">
{!!data?.picture?.path && (
<img
className="w-full h-full rounded-full"
src={data?.picture?.path || ''}
alt="avatar"
/>
<>
<OrderList type="seller" />
<div className="flex mt-[29px] w-full gap-[26px]">
<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">
{!!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}
/>
<div className="text-[18px]">Active</div>
</div>
)}
</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}
/>
<div className="text-[18px]">Active</div>
<div className="border-t border-t-[#425379] w-full" />
<div className="w-full">
<Button
className="w-full"
onClick={connectBankAccount}
loading={connectedLoading}
>
{!data?.account
? 'Connect Bank Account'
: 'Update Bank Account'}
</Button>
</div>
)}
<div className="border-t border-t-[#425379] w-full" />
<div className="w-full">
<Button
className="w-full"
onClick={connectBankAccount}
loading={connectedLoading}
>
{!data?.account ? 'Connect Bank Account' : 'Update Bank Account'}
</Button>
</div>
</div>
</div>
<div className="flex-1 flex gap-[16px] flex-col">
<h2 className="text-[20px]">Details</h2>
<div className="bg-sixth rounded-[4px] border border-[#172034]">
{tagsList.map((tag) => (
<Options
rows={3}
key={tag.key}
onChange={onChange}
preSelected={keys.map((key) => key.key)}
search={false}
options={tag.options}
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 className="flex-1 flex gap-[16px] flex-col">
<h2 className="text-[20px]">Details</h2>
<div className="bg-sixth rounded-[4px] border border-[#172034]">
{tagsList.map((tag) => (
<Options
rows={3}
key={tag.key}
onChange={onChange}
preSelected={keys.map((key) => key.key)}
search={false}
options={tag.options}
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>
</div>
</>
);
};

View File

@ -0,0 +1,438 @@
'use client';
import React, { FC, useCallback, useContext, useMemo } from 'react';
import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/marketplace.provider';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { capitalize } from 'lodash';
import removeMd from 'remove-markdown';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useModals } from '@mantine/modals';
import { Post as PrismaPost } from '@prisma/client';
import dynamic from 'next/dynamic';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import dayjs from 'dayjs';
const PreviewPopupDynamic = dynamic(() =>
import('@gitroom/frontend/components/marketplace/preview.popup.dynamic').then(
(mod) => mod.PreviewPopupDynamic
)
);
interface SpecialMessageInterface {
type: string;
data: {
id: string;
[key: string]: any;
};
}
export const OrderCompleted: FC = () => {
return (
<div className="border border-[#283450] flex flex-col rounded-[6px] overflow-hidden">
<div className="flex items-center bg-[#0F1524] px-[24px] py-[16px] text-[20px]">
<div className="flex-1">Order completed</div>
</div>
<div className="py-[16px] px-[24px] flex flex-col gap-[20px] text-[18px]">
The order has been completed
</div>
</div>
);
};
export const Published: FC<{
isCurrentOrder: boolean;
isSellerOrBuyer: 'BUYER' | 'SELLER';
orderStatus: string;
data: SpecialMessageInterface;
}> = (props) => {
const { data, isSellerOrBuyer } = props;
return (
<div className="border border-[#283450] flex flex-col rounded-[6px] overflow-hidden">
<div className="flex items-center bg-[#0F1524] px-[24px] py-[16px] text-[20px]">
<div className="flex-1">
{isSellerOrBuyer === 'BUYER' ? 'Your' : 'The'} post has been published
</div>
</div>
<div className="py-[16px] px-[24px] flex flex-col gap-[20px]">
<div className="flex gap-[20px]">
<div className="relative">
<img
src={data.data.picture}
alt="platform"
className="w-[24px] h-[24px] rounded-full"
/>
<img
className="absolute left-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
src={`/icons/platforms/${data.data.integration}.png`}
alt={data.data.name}
/>
</div>
<div className="flex-1 text-[18px]">{data.data.name}</div>
</div>
<div className="text-[14px]">
URL:{' '}
<a className="underline hover:font-bold" href={data.data.url}>
{data.data.url}
</a>
</div>
</div>
</div>
);
};
const PreviewPopup: FC<{
postId: string;
providerId: string;
post: {
integration: string;
group: string;
posts: PrismaPost[];
settings: any;
};
}> = (props) => {
const modal = useModals();
const close = useCallback(() => {
return modal.closeAll();
}, []);
return (
<div className="bg-black p-[20px] w-full 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>
<PreviewPopupDynamic {...props} />
</div>
);
};
export const Offer: FC<{
isCurrentOrder: boolean;
isSellerOrBuyer: 'BUYER' | 'SELLER';
orderStatus: string;
data: SpecialMessageInterface;
}> = (props) => {
const { data, isSellerOrBuyer, isCurrentOrder, orderStatus } = props;
const fetch = useFetch();
const acceptOrder = useCallback(async () => {
const { url } = await (
await fetch(`/marketplace/orders/${data.data.id}/payment`, {
method: 'POST',
})
).json();
window.location.href = url;
}, [data.data.id]);
const totalPrice = useMemo(() => {
return data?.data?.ordersItems?.reduce((all: any, current: any) => {
return all + current.price * current.quantity;
}, 0);
}, [data?.data?.ordersItems]);
return (
<div className="border border-[#283450] flex flex-col rounded-[6px] overflow-hidden">
<div className="flex items-center bg-[#0F1524] px-[24px] py-[16px] text-[20px]">
<div className="flex-1">New Offer</div>
<div className="text-[#32D583]">${totalPrice}</div>
</div>
<div className="py-[16px] px-[24px] flex flex-col gap-[20px]">
<div className="text-[#64748B] text-[12px]">Platform</div>
{data.data.ordersItems.map((item: any) => (
<div
key={item.integration.id}
className="flex gap-[10px] items-center"
>
<div className="relative">
<img
src={item.integration.picture}
alt="platform"
className="w-[24px] h-[24px] rounded-full"
/>
<img
className="absolute left-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
src={`/icons/platforms/${item.integration.providerIdentifier}.png`}
alt={item.integration.name}
/>
</div>
<div className="flex-1 text-[18px]">{item.integration.name}</div>
<div className="text-[18px]">{item.quantity} Posts</div>
</div>
))}
{orderStatus === 'PENDING' &&
isCurrentOrder &&
isSellerOrBuyer === 'BUYER' && (
<div className="flex justify-end">
<Button
className="rounded-[4px] text-[14px]"
onClick={acceptOrder}
>
Pay & Accept Offer
</Button>
</div>
)}
{orderStatus === 'ACCEPTED' && (
<div className="flex justify-end">
<Button className="rounded-[4px] text-[14px] border border-[#1F2941] !bg-[#0B101B] text-[#1F2941]">
Accepted
</Button>
</div>
)}
</div>
</div>
);
};
export const Post: FC<{
isCurrentOrder: boolean;
isSellerOrBuyer: 'BUYER' | 'SELLER';
orderStatus: string;
message: string;
data: SpecialMessageInterface;
}> = (props) => {
const { data, isSellerOrBuyer, message, isCurrentOrder, orderStatus } = props;
const fetch = useFetch();
const modal = useModals();
const getIntegration = useCallback(async () => {
return (
await fetch(
`/integrations/${data.data.integration}?order=${data.data.id}`,
{
method: 'GET',
}
)
).json();
}, []);
const requestRevision = useCallback(async () => {
if (
!(await deleteDialog(
'Are you sure you want to request a revision?',
'Yes'
))
) {
return;
}
await fetch(`/marketplace/posts/${data.data.postId}/revision`, {
method: 'POST',
body: JSON.stringify({
message,
}),
headers: {
'Content-Type': 'application/json',
},
});
}, [data]);
const requestApproved = useCallback(async () => {
if (
!(await deleteDialog(
'Are you sure you want to approve this post?',
'Yes'
))
) {
return;
}
await fetch(`/marketplace/posts/${data.data.postId}/approve`, {
method: 'POST',
body: JSON.stringify({
message,
}),
headers: {
'Content-Type': 'application/json',
},
});
}, [data]);
const preview = useCallback(async () => {
const post = await (
await fetch(`/marketplace/posts/${data.data.postId}`)
).json();
const integration = await getIntegration();
modal.openModal({
classNames: {
modal: 'bg-transparent text-white',
},
size: 'auto',
withCloseButton: false,
children: (
<IntegrationContext.Provider
value={{
date: dayjs(),
integration,
value: [],
}}
>
<PreviewPopup
providerId={post?.providerId!}
post={post}
postId={data?.data?.postId!}
/>
</IntegrationContext.Provider>
),
});
}, [data?.data]);
const { data: integrationData } = useSWR<{
id: string;
name: string;
picture: string;
providerIdentifier: string;
}>(`/integrations/${data.data.integration}`, getIntegration);
return (
<div className="border border-[#283450] flex flex-col rounded-[6px] overflow-hidden">
<div className="flex items-center bg-[#0F1524] px-[24px] py-[16px] text-[20px]">
<div className="flex-1">
Post Draft {capitalize(integrationData?.providerIdentifier || '')}
</div>
</div>
<div className="py-[16px] px-[24px] flex gap-[20px]">
<div>
<div className="relative">
<img
src={integrationData?.picture}
alt="platform"
className="w-[24px] h-[24px] rounded-full"
/>
<img
className="absolute left-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
src={`/icons/platforms/${integrationData?.providerIdentifier}.png`}
alt={integrationData?.name}
/>
</div>
</div>
<div className="flex flex-1 flex-col text-[16px] gap-[2px]">
<div className="text-[18px]">{integrationData?.name}</div>
<div>{removeMd(data.data.description)}</div>
{isSellerOrBuyer === 'BUYER' &&
isCurrentOrder &&
data.data.status === 'PENDING' &&
orderStatus === 'ACCEPTED' && (
<div className="mt-[18px] flex gap-[10px] justify-end">
<Button
onClick={requestRevision}
className="rounded-[4px] text-[14px] border-[2px] border-[#506490] !bg-[#0B101B]"
>
Revision Needed
</Button>
<Button
onClick={requestApproved}
className="rounded-[4px] text-[14px] border-[2px] border-[#506490] !bg-[#0B101B]"
>
Approve
</Button>
<Button className="rounded-[4px]" onClick={preview}>
Preview
</Button>
</div>
)}
{data.data.status === 'REVISION' && (
<div className="flex justify-end">
<Button className="rounded-[4px] text-[14px] border border-[#1F2941] !bg-[#0B101B] text-[#1F2941]">
Revision Requested
</Button>
</div>
)}
{data.data.status === 'APPROVED' && (
<div className="flex justify-end gap-[10px]">
<Button className="rounded-[4px] text-[14px] border border-[#1F2941] !bg-[#0B101B] text-[#1F2941]">
ACCEPTED
</Button>
</div>
)}
{data.data.status === 'CANCELED' && (
<div className="flex justify-end gap-[10px]">
<Button className="rounded-[4px] text-[14px] border border-[#1F2941] !bg-[#0B101B] text-[#1F2941]">
Cancelled by the seller
</Button>
</div>
)}
</div>
</div>
</div>
);
};
export const SpecialMessage: FC<{
data: SpecialMessageInterface;
id: string;
}> = (props) => {
const { data, id } = props;
const { message } = useContext(MarketplaceProvider);
const user = useUser();
const isCurrentOrder = useMemo(() => {
return message?.orders?.[0]?.id === data?.data?.id;
}, [message, data]);
const isSellerOrBuyer = useMemo(() => {
return user?.id === message?.buyerId ? 'BUYER' : 'SELLER';
}, [user, message]);
if (data.type === 'offer') {
return (
<Offer
data={data}
orderStatus={message?.orders?.[0]?.status!}
isCurrentOrder={isCurrentOrder}
isSellerOrBuyer={isSellerOrBuyer}
/>
);
}
if (data.type === 'post') {
return (
<Post
data={data}
orderStatus={message?.orders?.[0]?.status!}
isCurrentOrder={isCurrentOrder}
isSellerOrBuyer={isSellerOrBuyer}
message={id}
/>
);
}
if (data.type === 'published') {
return (
<Published
data={data}
orderStatus={message?.orders?.[0]?.status!}
isCurrentOrder={isCurrentOrder}
isSellerOrBuyer={isSellerOrBuyer}
/>
);
}
if (data.type === 'order-completed') {
return <OrderCompleted />;
}
return null;
};

View File

@ -1,50 +1,28 @@
'use client';
import { FC, ReactNode, useCallback } from 'react';
import { FC, ReactNode, useCallback, useMemo } 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;
}
import { MarketplaceProvider, Root2 } from '@gitroom/frontend/components/marketplace/marketplace.provider';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { Button } from '@gitroom/react/form/button';
const Card: FC<{ message: Root2 }> = (props) => {
const { message } = props;
const path = useParams();
const router = useRouter();
const user = useUser();
const changeConversation = useCallback(() => {
router.push(`/messages/${message.id}`);
}, []);
const showFrom = useMemo(() => {
return user?.id === message?.buyerId ? message?.seller : message?.buyer;
}, [message, user]);
return (
<div
onClick={changeConversation}
@ -54,13 +32,17 @@ const Card: FC<{ message: Root2 }> = (props) => {
)}
>
<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" />
{showFrom?.picture?.path && (
<img
src={showFrom.picture.path}
alt={showFrom.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>{showFrom?.name || 'Noname'}</div>
<div className="text-[12px] w-full overflow-ellipsis overflow-hidden">
{message.messages[0]?.content}
</div>
@ -73,12 +55,37 @@ const Card: FC<{ message: Root2 }> = (props) => {
export const Layout: FC<{ renderChildren: ReactNode }> = (props) => {
const { renderChildren } = props;
const fetch = useFetch();
const params = useParams();
const router = useRouter();
const loadMessagesGroup = useCallback(async () => {
return await (await fetch('/messages')).json();
}, []);
const messagesGroup = useSWR<Root2[]>('messagesGroup', loadMessagesGroup);
const messagesGroup = useSWR<Root2[]>('messagesGroup', loadMessagesGroup, {
refreshInterval: 5000
});
const marketplace = useCallback(() => {
router.push('/marketplace');
}, [router]);
const currentMessage = useMemo(() => {
return messagesGroup?.data?.find((message) => message.id === params.id);
}, [params.id, messagesGroup.data]);
if (messagesGroup.isLoading) {
return null;
}
if (!messagesGroup.isLoading && !messagesGroup?.data?.length) {
return (
<div className="flex flex-col justify-center items-center mt-[100px] gap-[27px] text-center">
<div><img src="/peoplemarketplace.svg" /></div>
<div className="text-[48px]">There are no messages yet.<br />Checkout the Marketplace</div>
<div><Button onClick={marketplace}>Go to marketplace</Button></div>
</div>
);
}
return (
<div className="flex gap-[20px]">
@ -90,7 +97,9 @@ export const Layout: FC<{ renderChildren: ReactNode }> = (props) => {
))}
</div>
</div>
<div className="flex-1 flex flex-col">{renderChildren}</div>
<MarketplaceProvider.Provider value={{ message: currentMessage }}>
<div className="flex-1 flex flex-col">{renderChildren}</div>
</MarketplaceProvider.Provider>
</div>
);
};

View File

@ -8,12 +8,11 @@ export interface Root {
sellerId: string;
createdAt: string;
updatedAt: string;
seller: SellerBuyer;
buyer: SellerBuyer;
messages: Message[];
}
export interface SellerBuyer {
id: string;
name?: string;
picture: Picture;
}
@ -27,6 +26,7 @@ export interface Message {
id: string;
from: string;
content: string;
special?: string;
groupId: string;
createdAt: string;
updatedAt: string;
@ -37,7 +37,16 @@ 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 {
FC,
UIEventHandler,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useParams } from 'next/navigation';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { reverse } from 'lodash';
@ -45,6 +54,11 @@ 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';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { OrderTopActions } from '@gitroom/frontend/components/marketplace/order.top.actions';
import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/marketplace.provider';
import { SpecialMessage } from '@gitroom/frontend/components/marketplace/special.message';
import { usePageVisibility } from '@gitroom/react/helpers/use.is.visible';
export const Message: FC<{
message: Message;
@ -53,16 +67,40 @@ export const Message: FC<{
scrollDown: () => void;
}> = (props) => {
const { message, seller, buyer, scrollDown } = props;
const user = useUser();
const amITheBuyerOrSeller = useMemo(() => {
return user?.id === buyer?.id ? 'BUYER' : 'SELLER';
}, [buyer, user]);
useEffect(() => {
scrollDown();
}, []);
const person = useMemo(() => {
return message.from === 'BUYER' ? buyer : seller;
if (message.from === 'BUYER') {
return buyer;
}
if (message.from === 'SELLER') {
return seller;
}
}, [amITheBuyerOrSeller, buyer, seller, message]);
const data = useMemo(() => {
if (!message.special) {
return false;
}
return JSON.parse(message.special);
}, [message]);
const isMe = useMemo(() => {
return message.from === 'BUYER';
}, []);
return (
(amITheBuyerOrSeller === 'BUYER' && message.from === 'BUYER') ||
(amITheBuyerOrSeller === 'SELLER' && message.from === 'SELLER')
);
}, [amITheBuyerOrSeller, message]);
const time = useMemo(() => {
return dayjs(message.createdAt).format('h:mm A');
@ -71,7 +109,7 @@ export const Message: FC<{
<div className="flex gap-[10px]">
<div>
<div className="w-[24px] h-[24px] rounded-full bg-amber-200">
{!!person.picture?.path && (
{!!person?.picture?.path && (
<img
src={person.picture.path}
alt="person"
@ -82,7 +120,7 @@ export const Message: FC<{
</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>{isMe ? 'Me' : person?.name}</div>
<div className="w-[6px] h-[6px] bg-[#334155] rounded-full" />
<div className="text-[14px] text-inputText">{time}</div>
</div>
@ -93,6 +131,7 @@ export const Message: FC<{
)}
>
{message.content}
{data && <SpecialMessage data={data} id={message.id} />}
</pre>
</div>
</div>
@ -102,16 +141,28 @@ export const Message: FC<{
const Page: FC<{ page: number; group: string; refChange: any }> = (props) => {
const { page, group, refChange } = props;
const fetch = useFetch();
const { message } = useContext(MarketplaceProvider);
const visible = usePageVisibility(page);
const loadMessages = useCallback(async () => {
return await (await fetch(`/messages/${group}/${page}`)).json();
}, []);
const { data, mutate } = useSWR<Root>(`load-${page}-${group}`, loadMessages);
const { data, mutate } = useSWR<Root>(`load-${page}-${group}`, loadMessages, {
...(page === 1
? {
refreshInterval: visible ? 5000 : 0,
refreshWhenHidden: false,
refreshWhenOffline: false,
revalidateOnFocus: false,
revalidateIfStale: false,
}
: {}),
});
const scrollDown = useCallback(() => {
if (page > 1) {
return ;
return;
}
// @ts-ignore
refChange.current?.scrollTo(0, refChange.current.scrollHeight);
@ -123,12 +174,12 @@ const Page: FC<{ page: number; group: string; refChange: any }> = (props) => {
return (
<>
{messages.map((message) => (
{messages.map((m) => (
<Message
key={message.id}
message={message}
seller={data?.seller!}
buyer={data?.buyer!}
key={m.id}
message={m}
seller={message?.seller!}
buyer={message?.buyer!}
scrollDown={scrollDown}
/>
))}
@ -138,9 +189,16 @@ const Page: FC<{ page: number; group: string; refChange: any }> = (props) => {
export const Messages = () => {
const [pages, setPages] = useState([makeId(3)]);
const user = useUser();
const params = useParams();
const fetch = useFetch();
const ref = useRef(null);
const { message } = useContext(MarketplaceProvider);
const showFrom = useMemo(() => {
return user?.id === message?.buyerId ? message?.seller : message?.buyer;
}, [message, user]);
const resolver = useMemo(() => {
return classValidatorResolver(AddMessageDto);
}, []);
@ -154,7 +212,10 @@ export const Messages = () => {
return await (await fetch(`/messages/${params.id}/1`)).json();
}, []);
const { data, mutate, isLoading } = useSWR<Root>(`load-1-${params.id}`, loadMessages);
const { data, mutate, isLoading } = useSWR<Root>(
`load-1-${params.id}`,
loadMessages
);
const submit: SubmitHandler<AddMessageDto> = useCallback(async (values) => {
await fetch(`/messages/${params.id}`, {
@ -165,14 +226,17 @@ export const Messages = () => {
form.reset();
}, []);
const changeScroll: UIEventHandler<HTMLDivElement> = useCallback((e) => {
// @ts-ignore
if (e.target.scrollTop === 0) {
const changeScroll: UIEventHandler<HTMLDivElement> = useCallback(
(e) => {
// @ts-ignore
e.target.scrollTop = 1;
setPages((prev) => [...prev, makeId(3)]);
}
}, [pages, setPages]);
if (e.target.scrollTop === 0) {
// @ts-ignore
e.target.scrollTop = 1;
setPages((prev) => [...prev, makeId(3)]);
}
},
[pages, setPages]
);
return (
<form onSubmit={form.handleSubmit(submit)}>
@ -180,15 +244,20 @@ export const Messages = () => {
<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 && (
{!!showFrom?.picture?.path && (
<img
src={data?.seller?.picture?.path}
src={showFrom?.picture?.path}
alt="seller"
className="w-[32px] h-[32px] rounded-full"
/>
)}
</div>
<div className="text-[20px]">{data?.seller?.name || 'Noname'}</div>
<div className="text-[20px] flex-1">
{showFrom?.name || 'Noname'}
</div>
<div>
<OrderTopActions />
</div>
</div>
<div className="flex-1 min-h-[658px] max-h-[658px] relative">
<div
@ -197,7 +266,12 @@ export const Messages = () => {
ref={ref}
>
{pages.map((p, index) => (
<Page key={'page_' + (pages.length - index)} refChange={ref} page={pages.length - index} group={params.id as string} />
<Page
key={'page_' + (pages.length - index)}
refChange={ref}
page={pages.length - index}
group={params.id as string}
/>
))}
</div>
</div>

View File

@ -1,15 +1,17 @@
import {Controller} from '@nestjs/common';
import {EventPattern, Transport} from '@nestjs/microservices';
import {PostsService} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.service";
import { Controller } from '@nestjs/common';
import { EventPattern, Transport } from '@nestjs/microservices';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
@Controller()
export class PostsController {
constructor(
private _postsService: PostsService
) {
}
constructor(private _postsService: PostsService) {}
@EventPattern('post', Transport.REDIS)
async checkStars(data: {id: string}) {
async checkStars(data: { id: string }) {
return this._postsService.post(data.id);
}
@EventPattern('submit', Transport.REDIS)
async submitOrderItemForPayout(data: { id: string, releaseURL: string }) {
return this._postsService.payout(data.id, data.releaseURL);
}
}

View File

@ -24,6 +24,7 @@ import { ItemUserRepository } from '@gitroom/nestjs-libraries/database/prisma/ma
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';
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
@Global()
@Module({
@ -46,6 +47,7 @@ import { MessagesRepository } from '@gitroom/nestjs-libraries/database/prisma/ma
IntegrationRepository,
PostsService,
PostsRepository,
StripeService,
MessagesRepository,
MediaService,
MediaRepository,

View File

@ -1,6 +1,7 @@
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import dayjs from 'dayjs';
import * as console from 'node:console';
@Injectable()
export class IntegrationRepository {
@ -77,6 +78,37 @@ export class IntegrationRepository {
});
}
async getIntegrationForOrder(id: string, order: string, user: string, org: string) {
console.log(id, order, user, org);
const integration = await this._posts.model.post.findFirst({
where: {
integrationId: id,
submittedForOrder: {
id: order,
messageGroup: {
OR: [
{sellerId: user},
{buyerId: user},
{buyerOrganizationId: org},
]
}
}
},
select: {
integration: {
select: {
id: true,
name: true,
picture: true,
providerIdentifier: true,
},
}
}
});
return integration?.integration;
}
getIntegrationsList(org: string) {
return this._integration.model.integration.findMany({
where: {

View File

@ -36,6 +36,10 @@ export class IntegrationService {
return this._integrationRepository.getIntegrationsList(org);
}
getIntegrationForOrder(id: string, order: string, user: string, org: string) {
return this._integrationRepository.getIntegrationForOrder(id, order, user, org);
}
getIntegrationById(org: string, id: string) {
return this._integrationRepository.getIntegrationById(org, id);
}

View File

@ -1,26 +1,38 @@
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 { From, OrderStatus } from '@prisma/client';
import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
import { CreateOfferDto } from '@gitroom/nestjs-libraries/dtos/marketplace/create.offer.dto';
@Injectable()
export class MessagesRepository {
constructor(
private _messagesGroup: PrismaRepository<'messagesGroup'>,
private _messages: PrismaRepository<'messages'>
private _messages: PrismaRepository<'messages'>,
private _orders: PrismaRepository<'orders'>,
private _organizations: PrismaRepository<'organization'>,
private _post: PrismaRepository<'post'>,
private _payoutProblems: PrismaRepository<'payoutProblems'>,
private _users: PrismaRepository<'user'>
) {}
async createConversation(userId: string, body: NewConversationDto) {
async createConversation(
userId: string,
organizationId: string,
body: NewConversationDto
) {
const { id } =
(await this._messagesGroup.model.messagesGroup.findFirst({
where: {
buyerOrganizationId: organizationId,
buyerId: userId,
sellerId: body.to,
},
})) ||
(await this._messagesGroup.model.messagesGroup.create({
data: {
buyerOrganizationId: organizationId,
buyerId: userId,
sellerId: body.to,
},
@ -46,77 +58,22 @@ export class MessagesRepository {
return { id };
}
async getMessagesGroup(userId: string) {
async getMessagesGroup(userId: string, organizationId: string) {
return this._messagesGroup.model.messagesGroup.findMany({
where: {
buyerId: userId,
OR: [
{
buyerOrganizationId: organizationId,
buyerId: userId,
},
{
sellerId: 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: {
@ -142,6 +99,100 @@ export class MessagesRepository {
},
},
},
orders: {
orderBy: {
createdAt: 'desc',
},
take: 1,
},
messages: {
orderBy: {
createdAt: 'desc',
},
take: 1,
},
},
});
}
async createMessage(
userId: string,
orgId: string,
groupId: string,
body: AddMessageDto
) {
const group = await this._messagesGroup.model.messagesGroup.findFirst({
where: {
id: groupId,
OR: [
{
buyerOrganizationId: orgId,
buyerId: userId,
},
{
sellerId: userId,
},
],
},
});
if (!group) {
throw new Error('Group not found');
}
const create = await this.createNewMessage(
groupId,
group.buyerId === userId ? From.BUYER : From.SELLER,
body.message
);
await this._messagesGroup.model.messagesGroup.update({
where: {
id: groupId,
},
data: {
updatedAt: new Date(),
},
});
if (userId === group.buyerId) {
return create.group.seller;
}
return create.group.buyer;
}
async updateOrderOnline(userId: string) {
await this._users.model.user.update({
where: {
id: userId,
},
data: {
lastOnline: new Date(),
},
});
}
async getMessages(
userId: string,
organizationId: string,
groupId: string,
page: number
) {
return this._messagesGroup.model.messagesGroup.findFirst({
where: {
id: groupId,
OR: [
{
buyerOrganizationId: organizationId,
buyerId: userId,
},
{
sellerId: userId,
},
],
},
include: {
messages: {
orderBy: {
createdAt: 'desc',
@ -152,4 +203,697 @@ export class MessagesRepository {
},
});
}
async createOffer(userId: string, body: CreateOfferDto) {
const messageGroup =
await this._messagesGroup.model.messagesGroup.findFirst({
where: {
id: body.group,
sellerId: userId,
},
select: {
id: true,
buyer: {
select: {
id: true,
},
},
orders: {
orderBy: {
createdAt: 'desc',
},
take: 1,
},
},
});
if (!messageGroup?.id) {
throw new Error('Group not found');
}
if (
messageGroup.orders.length &&
messageGroup.orders[0].status !== 'COMPLETED' &&
messageGroup.orders[0].status !== 'CANCELED'
) {
throw new Error('Order already exists');
}
const data = await this._orders.model.orders.create({
data: {
sellerId: userId,
buyerId: messageGroup.buyer.id,
messageGroupId: messageGroup.id,
ordersItems: {
createMany: {
data: body.socialMedia.map((item) => ({
quantity: item.total,
integrationId: item.value,
price: item.price,
})),
},
},
status: 'PENDING',
},
select: {
id: true,
ordersItems: {
select: {
quantity: true,
price: true,
integration: {
select: {
name: true,
providerIdentifier: true,
picture: true,
id: true,
},
},
},
},
},
});
await this._messages.model.messages.create({
data: {
groupId: body.group,
from: From.SELLER,
content: '',
special: JSON.stringify({ type: 'offer', data: data }),
},
});
return { success: true };
}
async createNewMessage(
group: string,
from: From,
content: string,
special?: object
) {
return this._messages.model.messages.create({
data: {
groupId: group,
from,
content,
special: JSON.stringify(special),
},
select: {
id: true,
group: {
select: {
buyer: {
select: {
lastOnline: true,
id: true,
organizations: true,
},
},
seller: {
select: {
lastOnline: true,
id: true,
organizations: true,
},
},
},
},
},
});
}
async getOrderDetails(
userId: string,
organizationId: string,
orderId: string
) {
const order = await this._messagesGroup.model.messagesGroup.findFirst({
where: {
buyerId: userId,
buyerOrganizationId: organizationId,
},
select: {
buyer: true,
seller: true,
orders: {
include: {
ordersItems: {
select: {
quantity: true,
integration: true,
price: true,
},
},
},
where: {
id: orderId,
status: 'PENDING',
},
},
},
});
if (!order?.orders[0]?.id) {
throw new Error('Order not found');
}
return {
buyer: order.buyer,
seller: order.seller,
order: order.orders[0]!,
};
}
async canAddPost(id: string, order: string, integrationId: string) {
const findOrder = await this._orders.model.orders.findFirst({
where: {
id: order,
status: 'ACCEPTED',
},
select: {
posts: true,
ordersItems: true,
},
});
if (!findOrder) {
return false;
}
if (
findOrder.posts.find(
(p) => p.id === id && p.approvedSubmitForOrder === 'YES'
)
) {
return false;
}
if (
findOrder.posts.find(
(p) =>
p.id === id && p.approvedSubmitForOrder === 'WAITING_CONFIRMATION'
)
) {
return true;
}
const postsForIntegration = findOrder.ordersItems.filter(
(p) => p.integrationId === integrationId
);
const totalPostsRequired = postsForIntegration.reduce(
(acc, item) => acc + item.quantity,
0
);
const usedPosts = findOrder.posts.filter(
(p) =>
p.integrationId === integrationId &&
['WAITING_CONFIRMATION', 'YES'].indexOf(p.approvedSubmitForOrder) > -1
).length;
return totalPostsRequired > usedPosts;
}
changeOrderStatus(
orderId: string,
status: OrderStatus,
paymentIntent?: string
) {
return this._orders.model.orders.update({
where: {
id: orderId,
},
data: {
status,
captureId: paymentIntent,
},
});
}
async getMarketplaceAvailableOffers(orgId: string, id: string) {
const offers = await this._organizations.model.organization.findFirst({
where: {
id: orgId,
},
select: {
users: {
select: {
user: {
select: {
orderSeller: {
where: {
status: 'ACCEPTED',
},
select: {
id: true,
posts: {
where: {
deletedAt: null,
},
select: {
id: true,
integrationId: true,
approvedSubmitForOrder: true,
},
},
messageGroup: {
select: {
buyerOrganizationId: true,
},
},
buyer: {
select: {
id: true,
name: true,
picture: {
select: {
id: true,
path: true,
},
},
},
},
ordersItems: {
select: {
quantity: true,
integration: {
select: {
id: true,
name: true,
providerIdentifier: true,
},
},
},
},
},
},
},
},
},
},
},
});
const allOrders =
offers?.users.flatMap((user) => user.user.orderSeller) || [];
const onlyValidItems = allOrders.filter(
(order) =>
(order.posts.find((p) => p.id === id)
? 0
: order.posts.filter((f) => f.approvedSubmitForOrder !== 'NO')
.length) <
order.ordersItems.reduce((acc, item) => acc + item.quantity, 0)
);
return onlyValidItems
.map((order) => {
const postsNumbers = order.posts
.filter(
(p) =>
['WAITING_CONFIRMATION', 'YES'].indexOf(
p.approvedSubmitForOrder
) > -1
)
.reduce((acc, post) => {
acc[post.integrationId] = acc[post.integrationId] + 1 || 1;
return acc;
}, {} as { [key: string]: number });
const missing = order.ordersItems.map((item) => {
return {
integration: item,
missing: item.quantity - (postsNumbers[item.integration.id] || 0),
};
});
return {
id: order.id,
usedIds: order.posts.map((p) => ({
id: p.id,
status: p.approvedSubmitForOrder,
})),
buyer: order.buyer,
missing,
};
})
.filter((f) => f.missing.length);
}
async requestRevision(
userId: string,
orgId: string,
postId: string,
message: string
) {
const loadMessage = await this._messages.model.messages.findFirst({
where: {
id: message,
group: {
buyerOrganizationId: orgId,
},
},
select: {
id: true,
special: true,
},
});
const post = await this._post.model.post.findFirst({
where: {
id: postId,
approvedSubmitForOrder: 'WAITING_CONFIRMATION',
deletedAt: null,
},
});
if (post && loadMessage) {
const special = JSON.parse(loadMessage.special!);
special.data.status = 'REVISION';
await this._messages.model.messages.update({
where: {
id: message,
},
data: {
special: JSON.stringify(special),
},
});
await this._post.model.post.update({
where: {
id: postId,
deletedAt: null,
},
data: {
approvedSubmitForOrder: 'NO',
},
});
}
}
async requestCancel(orgId: string, postId: string) {
const getPost = await this._post.model.post.findFirst({
where: {
id: postId,
organizationId: orgId,
approvedSubmitForOrder: {
in: ['WAITING_CONFIRMATION', 'YES'],
},
},
select: {
lastMessage: true,
},
});
if (!getPost) {
throw new Error('Post not found');
}
await this._post.model.post.update({
where: {
id: postId,
},
data: {
approvedSubmitForOrder: 'NO',
},
});
const special = JSON.parse(getPost.lastMessage!.special!);
special.data.status = 'CANCELED';
await this._messages.model.messages.update({
where: {
id: getPost.lastMessage!.id,
},
data: {
special: JSON.stringify(special),
},
});
}
async requestApproved(
userId: string,
orgId: string,
postId: string,
message: string
) {
const loadMessage = await this._messages.model.messages.findFirst({
where: {
id: message,
group: {
buyerOrganizationId: orgId,
},
},
select: {
id: true,
special: true,
},
});
const post = await this._post.model.post.findFirst({
where: {
id: postId,
approvedSubmitForOrder: 'WAITING_CONFIRMATION',
deletedAt: null,
},
});
if (post && loadMessage) {
const special = JSON.parse(loadMessage.special!);
special.data.status = 'APPROVED';
await this._messages.model.messages.update({
where: {
id: message,
},
data: {
special: JSON.stringify(special),
},
});
await this._post.model.post.update({
where: {
id: postId,
deletedAt: null,
},
data: {
approvedSubmitForOrder: 'YES',
},
});
return post;
}
return false;
}
completeOrder(orderId: string) {
return this._orders.model.orders.update({
where: {
id: orderId,
},
data: {
status: 'COMPLETED',
},
});
}
async completeOrderAndPay(orgId: string, order: string) {
const findOrder = await this._orders.model.orders.findFirst({
where: {
id: order,
messageGroup: {
buyerOrganizationId: orgId,
},
},
select: {
captureId: true,
seller: {
select: {
account: true,
id: true,
},
},
ordersItems: true,
posts: true,
},
});
if (!findOrder) {
return false;
}
const releasedPosts = findOrder.posts.filter((p) => p.releaseURL);
const nonReleasedPosts = findOrder.posts.filter((p) => !p.releaseURL);
const totalPosts = releasedPosts.reduce((acc, item) => {
acc[item.integrationId] = (acc[item.integrationId] || 0) + 1;
return acc;
}, {} as { [key: string]: number });
const totalOrderItems = findOrder.ordersItems.reduce((acc, item) => {
acc[item.integrationId] = (acc[item.integrationId] || 0) + item.quantity;
return acc;
}, {} as { [key: string]: number });
const calculate = Object.keys(totalOrderItems).reduce((acc, key) => {
acc.push({
price: findOrder.ordersItems.find((p) => p.integrationId === key)!
.price,
quantity: totalOrderItems[key] - (totalPosts[key] || 0),
});
return acc;
}, [] as { price: number; quantity: number }[]);
const price = calculate.reduce((acc, item) => {
acc += item.price * item.quantity;
return acc;
}, 0);
return {
price,
account: findOrder.seller.account,
charge: findOrder.captureId,
posts: nonReleasedPosts,
sellerId: findOrder.seller.id,
};
}
payoutProblem(
orderId: string,
sellerId: string,
amount: number,
postId?: string
) {
return this._payoutProblems.model.payoutProblems.create({
data: {
amount,
orderId,
...(postId ? { postId } : {}),
userId: sellerId,
status: 'PAYMENT_ERROR',
},
});
}
async getOrders(userId: string, orgId: string, type: 'seller' | 'buyer') {
const orders = await this._orders.model.orders.findMany({
where: {
status: {
in: ['ACCEPTED', 'PENDING', 'COMPLETED'],
},
...(type === 'seller'
? {
sellerId: userId,
}
: {
messageGroup: {
buyerOrganizationId: orgId,
},
}),
},
orderBy: {
updatedAt: 'desc',
},
select: {
id: true,
status: true,
...(type === 'seller'
? {
buyer: {
select: {
name: true,
},
},
}
: {
seller: {
select: {
name: true,
},
},
}),
ordersItems: {
select: {
id: true,
quantity: true,
price: true,
integration: {
select: {
id: true,
picture: true,
name: true,
providerIdentifier: true,
},
},
},
},
posts: {
select: {
id: true,
integrationId: true,
releaseURL: true,
approvedSubmitForOrder: true,
state: true,
},
},
},
});
return {
orders: await Promise.all(
orders.map(async (order) => {
return {
id: order.id,
status: order.status,
// @ts-ignore
name: type === 'seller' ? order?.buyer?.name : order?.seller?.name,
price: order.ordersItems.reduce(
(acc, item) => acc + item.price * item.quantity,
0
),
details: await Promise.all(
order.ordersItems.map((item) => {
return {
posted: order.posts.filter(
(p) =>
p.releaseURL && p.integrationId === item.integration.id
).length,
submitted: order.posts.filter(
(p) =>
!p.releaseURL &&
(p.approvedSubmitForOrder === 'WAITING_CONFIRMATION' ||
p.approvedSubmitForOrder === 'YES') &&
p.integrationId === item.integration.id
).length,
integration: item.integration,
total: item.quantity,
price: item.price,
};
})
),
};
})
),
};
}
getPost(userId: string, orgId: string, postId: string) {
return this._post.model.post.findFirst({
where: {
id: postId,
submittedForOrder: {
messageGroup: {
OR: [{ sellerId: userId }, {buyerOrganizationId: orgId}],
}
},
},
select: {
organizationId: true,
integration: {
select: {
providerIdentifier: true
}
}
}
});
}
}

View File

@ -2,24 +2,247 @@ 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';
import { CreateOfferDto } from '@gitroom/nestjs-libraries/dtos/marketplace/create.offer.dto';
import { From, OrderStatus, User } from '@prisma/client';
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client';
import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
import dayjs from 'dayjs';
@Injectable()
export class MessagesService {
constructor(private _messagesRepository: MessagesRepository) {}
constructor(
private _workerServiceProducer: BullMqClient,
private _messagesRepository: MessagesRepository,
private _organizationRepository: OrganizationRepository,
private _inAppNotificationService: NotificationService
) {}
createConversation(userId: string, body: NewConversationDto) {
return this._messagesRepository.createConversation(userId, body);
async createConversation(
userId: string,
organizationId: string,
body: NewConversationDto
) {
const conversation = await this._messagesRepository.createConversation(
userId,
organizationId,
body
);
const orgs = await this._organizationRepository.getOrgsByUserId(body.to);
await Promise.all(
orgs.map(async (org) => {
return this._inAppNotificationService.inAppNotification(
org.id,
'Request for service',
'A user has requested a service from you',
true
);
})
);
return conversation;
}
getMessagesGroup(userId: string) {
return this._messagesRepository.getMessagesGroup(userId);
getMessagesGroup(userId: string, organizationId: string) {
return this._messagesRepository.getMessagesGroup(userId, organizationId);
}
getMessages(userId: string, groupId: string, page: number) {
return this._messagesRepository.getMessages(userId, groupId, page);
async getMessages(
userId: string,
organizationId: string,
groupId: string,
page: number
) {
if (page === 1) {
this._messagesRepository.updateOrderOnline(userId);
}
return this._messagesRepository.getMessages(
userId,
organizationId,
groupId,
page
);
}
createMessage(userId: string, groupId: string, body: AddMessageDto) {
return this._messagesRepository.createMessage(userId, groupId, body);
async createNewMessage(
group: string,
from: From,
content: string,
special?: object
) {
const message = await this._messagesRepository.createNewMessage(
group,
from,
content,
special
);
const user = from === 'BUYER' ? message.group.seller : message.group.buyer;
await Promise.all(
user.organizations.map((p) => {
return this.sendMessageNotification({
id: p.organizationId,
lastOnline: user.lastOnline,
});
})
);
return message;
}
async sendMessageNotification(user: { id: string; lastOnline: Date }) {
if (dayjs(user.lastOnline).add(5, 'minute').isBefore(dayjs())) {
await this._inAppNotificationService.inAppNotification(
user.id,
'New message',
'You have a new message',
true
);
}
}
async createMessage(
userId: string,
orgId: string,
groupId: string,
body: AddMessageDto
) {
const message = await this._messagesRepository.createMessage(
userId,
orgId,
groupId,
body
);
await Promise.all(
message.organizations.map((p) => {
return this.sendMessageNotification({
id: p.organizationId,
lastOnline: message.lastOnline,
});
})
);
return message;
}
createOffer(userId: string, body: CreateOfferDto) {
return this._messagesRepository.createOffer(userId, body);
}
getOrderDetails(userId: string, organizationId: string, orderId: string) {
return this._messagesRepository.getOrderDetails(
userId,
organizationId,
orderId
);
}
canAddPost(id: string, order: string, integrationId: string) {
return this._messagesRepository.canAddPost(id, order, integrationId);
}
changeOrderStatus(
orderId: string,
status: OrderStatus,
paymentIntent?: string
) {
return this._messagesRepository.changeOrderStatus(
orderId,
status,
paymentIntent
);
}
getMarketplaceAvailableOffers(orgId: string, id: string) {
return this._messagesRepository.getMarketplaceAvailableOffers(orgId, id);
}
getPost(userId: string, orgId: string, postId: string) {
return this._messagesRepository.getPost(userId, orgId, postId);
}
requestRevision(
userId: string,
orgId: string,
postId: string,
message: string
) {
return this._messagesRepository.requestRevision(
userId,
orgId,
postId,
message
);
}
async requestApproved(
userId: string,
orgId: string,
postId: string,
message: string
) {
const post = await this._messagesRepository.requestApproved(
userId,
orgId,
postId,
message
);
if (post) {
this._workerServiceProducer.emit('post', {
id: post.id,
options: {
delay: 0, //dayjs(post.publishDate).diff(dayjs(), 'millisecond'),
},
payload: {
id: post.id,
},
});
}
}
async requestCancel(orgId: string, postId: string) {
const cancel = await this._messagesRepository.requestCancel(orgId, postId);
await this._workerServiceProducer.delete('post', postId);
return cancel;
}
async completeOrderAndPay(orgId: string, order: string) {
const orderList = await this._messagesRepository.completeOrderAndPay(
orgId,
order
);
if (!orderList) {
return false;
}
orderList.posts.forEach((post) => {
this._workerServiceProducer.delete('post', post.id);
});
return orderList;
}
completeOrder(orderId: string) {
return this._messagesRepository.completeOrder(orderId);
}
payoutProblem(
orderId: string,
sellerId: string,
amount: number,
postId?: string
) {
return this._messagesRepository.payoutProblem(
orderId,
sellerId,
amount,
postId
);
}
getOrders(userId: string, orgId: string, type: 'seller' | 'buyer') {
return this._messagesRepository.getOrders(userId, orgId, type);
}
}

View File

@ -1,7 +1,7 @@
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { Post as PostBody } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import { Post } from '@prisma/client';
import { APPROVED_SUBMIT_FOR_ORDER, Post } from '@prisma/client';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
@ -114,7 +114,12 @@ export class PostsRepository {
});
}
getPost(id: string, includeIntegration = false, orgId?: string) {
getPost(
id: string,
includeIntegration = false,
orgId?: string,
isFirst?: boolean
) {
return this._post.model.post.findUnique({
where: {
id,
@ -122,7 +127,11 @@ export class PostsRepository {
deletedAt: null,
},
include: {
...(includeIntegration ? { integration: true } : {}),
...(includeIntegration
? {
integration: true,
}
: {}),
childrenPost: true,
},
});
@ -210,6 +219,7 @@ export class PostsRepository {
: {}),
content: value.content,
group: uuid,
approvedSubmitForOrder: APPROVED_SUBMIT_FOR_ORDER.NO,
state: state === 'draft' ? ('DRAFT' as const) : ('QUEUE' as const),
image: JSON.stringify(value.image),
settings: JSON.stringify(body.settings),
@ -225,8 +235,16 @@ export class PostsRepository {
where: {
id: value.id || uuidv4(),
},
create: updateData('create'),
update: updateData('update'),
create: { ...updateData('create') },
update: {
...updateData('update'),
lastMessage: {
disconnect: true,
},
submittedForOrder: {
disconnect: true,
},
},
})
);
}
@ -261,4 +279,64 @@ export class PostsRepository {
return { previousPost, posts };
}
async submit(id: string, order: string) {
return this._post.model.post.update({
where: {
id,
},
data: {
submittedForOrderId: order,
approvedSubmitForOrder: 'WAITING_CONFIRMATION',
},
select: {
id: true,
description: true,
submittedForOrder: {
select: {
messageGroupId: true,
},
},
},
});
}
updateMessage(id: string, messageId: string) {
return this._post.model.post.update({
where: {
id,
},
data: {
lastMessageId: messageId,
},
});
}
getPostById(id: string, org?: string) {
return this._post.model.post.findUnique({
where: {
id,
...(org ? { organizationId: org } : {}),
},
include: {
integration: true,
submittedForOrder: {
include: {
posts: {
where: {
state: 'PUBLISHED',
},
},
ordersItems: true,
seller: {
select: {
id: true,
account: true
},
},
},
},
},
});
}
}

View File

@ -4,10 +4,12 @@ import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client';
import dayjs from 'dayjs';
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import { Integration, Post, Media } from '@prisma/client';
import { Integration, Post, Media, From } from '@prisma/client';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
import {capitalize} from "lodash";
import { capitalize } from 'lodash';
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
type PostWithConditionals = Post & {
integration?: Integration;
@ -20,23 +22,33 @@ export class PostsService {
private _postRepository: PostsRepository,
private _workerServiceProducer: BullMqClient,
private _integrationManager: IntegrationManager,
private _notificationService: NotificationService
private _notificationService: NotificationService,
private _messagesService: MessagesService,
private _stripeService: StripeService
) {}
async getPostsRecursively(
id: string,
includeIntegration = false,
orgId?: string
orgId?: string,
isFirst?: boolean
): Promise<PostWithConditionals[]> {
const post = await this._postRepository.getPost(
id,
includeIntegration,
orgId
orgId,
isFirst
);
return [
post!,
...(post?.childrenPost?.length
? await this.getPostsRecursively(post.childrenPost[0].id, false, orgId)
? await this.getPostsRecursively(
post.childrenPost[0].id,
false,
orgId,
false
)
: []),
];
}
@ -46,7 +58,7 @@ export class PostsService {
}
async getPost(orgId: string, id: string) {
const posts = await this.getPostsRecursively(id, false, orgId);
const posts = await this.getPostsRecursively(id, false, orgId, true);
return {
group: posts?.[0]?.group,
posts: posts.map((post) => ({
@ -79,16 +91,29 @@ export class PostsService {
}
try {
if (firstPost.integration?.type === 'article') {
await this.postArticle(firstPost.integration!, [
firstPost,
...morePosts,
]);
const finalPost =
firstPost.integration?.type === 'article'
? await this.postArticle(firstPost.integration!, [
firstPost,
...morePosts,
])
: await this.postSocial(firstPost.integration!, [
firstPost,
...morePosts,
]);
if (!finalPost?.postId || !finalPost?.releaseURL) {
return;
}
await this.postSocial(firstPost.integration!, [firstPost, ...morePosts]);
if (firstPost.submittedForOrderId) {
this._workerServiceProducer.emit('submit', {
payload: {
id: firstPost.id,
releaseURL: finalPost.releaseURL,
},
});
}
} catch (err: any) {
await this._notificationService.inAppNotification(
firstPost.organizationId,
@ -147,7 +172,10 @@ export class PostsService {
m.path
: m.path,
type: 'image',
path: m.path.indexOf('http') === -1 ? process.env.UPLOAD_DIRECTORY + m.path : m.path,
path:
m.path.indexOf('http') === -1
? process.env.UPLOAD_DIRECTORY + m.path
: m.path,
})),
}))
);
@ -162,10 +190,17 @@ export class PostsService {
await this._notificationService.inAppNotification(
integration.organizationId,
`Your post has been published on ${capitalize(integration.providerIdentifier)}`,
`Your post has been published on ${capitalize(
integration.providerIdentifier
)}`,
`Your post has been published at ${publishedPosts[0].releaseURL}`,
true
);
return {
postId: publishedPosts[0].postId,
releaseURL: publishedPosts[0].releaseURL,
};
}
private async postArticle(integration: Integration, posts: Post[]) {
@ -186,11 +221,18 @@ export class PostsService {
await this._notificationService.inAppNotification(
integration.organizationId,
`Your article has been published on ${capitalize(integration.providerIdentifier)}`,
`Your article has been published on ${capitalize(
integration.providerIdentifier
)}`,
`Your article has been published at ${releaseURL}`,
true
);
await this._postRepository.updatePost(newPosts[0].id, postId, releaseURL);
return {
postId,
releaseURL,
};
}
async deletePost(orgId: string, group: string) {
@ -204,6 +246,38 @@ export class PostsService {
return this._postRepository.countPostsFromDay(orgId, date);
}
async submit(
id: string,
order: string,
message: string,
integrationId: string
) {
if (!(await this._messagesService.canAddPost(id, order, integrationId))) {
console.log('hello');
throw new Error('You can not add a post to this publication');
}
const submit = await this._postRepository.submit(id, order);
const messageModel = await this._messagesService.createNewMessage(
submit?.submittedForOrder?.messageGroupId || '',
From.SELLER,
'',
{
type: 'post',
data: {
id: order,
postId: id,
status: 'PENDING',
integration: integrationId,
description: message.slice(0, 300) + '...',
},
}
);
await this._postRepository.updateMessage(id, messageModel.id);
return messageModel;
}
async createPost(orgId: string, body: CreatePostDto) {
for (const post of body.posts) {
const { previousPost, posts } =
@ -224,6 +298,17 @@ export class PostsService {
'post',
previousPost ? previousPost : posts?.[0]?.id
);
if (body.order && body.type !== 'draft') {
await this.submit(
posts[0].id,
body.order,
post.value[0].content,
post.integration.id
);
continue;
}
if (
(body.type === 'schedule' || body.type === 'now') &&
dayjs(body.date).isAfter(dayjs())
@ -245,16 +330,105 @@ export class PostsService {
}
async changeDate(orgId: string, id: string, date: string) {
const getPostById = await this._postRepository.getPostById(id, orgId);
if (
getPostById?.submittedForOrderId &&
getPostById.approvedSubmitForOrder !== 'NO'
) {
throw new Error(
'You can not change the date of a post that has been submitted'
);
}
await this._workerServiceProducer.delete('post', id);
this._workerServiceProducer.emit('post', {
id: id,
options: {
delay: dayjs(date).diff(dayjs(), 'millisecond'),
},
payload: {
if (getPostById?.state !== 'DRAFT' && !getPostById?.submittedForOrderId) {
this._workerServiceProducer.emit('post', {
id: id,
},
});
options: {
delay: dayjs(date).diff(dayjs(), 'millisecond'),
},
payload: {
id: id,
},
});
}
return this._postRepository.changeDate(orgId, id, date);
}
async payout(id: string, url: string) {
const getPost = await this._postRepository.getPostById(id);
if (!getPost || !getPost.submittedForOrder) {
return;
}
const findPrice = getPost.submittedForOrder.ordersItems.find(
(orderItem) => orderItem.integrationId === getPost.integrationId
)!;
await this._messagesService.createNewMessage(
getPost.submittedForOrder.messageGroupId,
From.SELLER,
'',
{
type: 'published',
data: {
id: getPost.submittedForOrder.id,
postId: id,
status: 'PUBLISHED',
integrationId: getPost.integrationId,
integration: getPost.integration.providerIdentifier,
picture: getPost.integration.picture,
name: getPost.integration.name,
url,
},
}
);
const totalItems = getPost.submittedForOrder.ordersItems.reduce(
(all, p) => all + p.quantity,
0
);
const totalPosts = getPost.submittedForOrder.posts.length;
if (totalItems === totalPosts) {
await this._messagesService.completeOrder(getPost.submittedForOrder.id);
await this._messagesService.createNewMessage(
getPost.submittedForOrder.messageGroupId,
From.SELLER,
'',
{
type: 'order-completed',
data: {
id: getPost.submittedForOrder.id,
postId: id,
status: 'PUBLISHED',
},
}
);
}
try {
await this._stripeService.payout(
getPost.submittedForOrder.id,
getPost.submittedForOrder.captureId!,
getPost.submittedForOrder.seller.account!,
findPrice.price
);
return this._notificationService.inAppNotification(
getPost.integration.organizationId,
'Payout completed',
`You have received a payout of $${findPrice.price}`,
true
);
} catch (err) {
await this._messagesService.payoutProblem(
getPost.submittedForOrder.id,
getPost.submittedForOrder.seller.id,
findPrice.price,
id
);
}
}
}

View File

@ -25,6 +25,7 @@ model Organization {
post Post[]
Comments Comments[]
notifications Notifications[]
buyerOrganization MessagesGroup[]
}
model User {
@ -51,12 +52,18 @@ model User {
account String?
connectedAccount Boolean @default(false)
groupBuyer MessagesGroup[] @relation("groupBuyer")
groupSeller MessagesGroup[] @relation("groupSeller")
groupSeller MessagesGroup[] @relation("groupSeller")
orderBuyer Orders[] @relation("orderBuyer")
orderSeller Orders[] @relation("orderSeller")
payoutProblems PayoutProblems[]
lastOnline DateTime @default(now())
@@unique([email, providerName])
@@index([lastReadNotifications])
@@index([inviteId])
@@index([account])
@@index([lastOnline])
@@index([pictureId])
}
model UserOrganization {
@ -71,6 +78,7 @@ model UserOrganization {
updatedAt DateTime @updatedAt
@@unique([userId, organizationId])
@@index([disabled])
}
model GitHub {
@ -179,6 +187,7 @@ model Integration {
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
orderItems OrderItems[]
@@index([updatedAt])
@@index([deletedAt])
@ -208,27 +217,33 @@ model Comments {
}
model Post {
id String @id @default(cuid())
state State @default(QUEUE)
publishDate DateTime
organizationId String
integrationId String
content String
group String
organization Organization @relation(fields: [organizationId], references: [id])
integration Integration @relation(fields: [integrationId], references: [id])
title String?
description String?
parentPostId String?
releaseId String?
releaseURL String?
settings String?
parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id])
childrenPost Post[] @relation("parentPostId")
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
id String @id @default(cuid())
state State @default(QUEUE)
publishDate DateTime
organizationId String
integrationId String
content String
group String
organization Organization @relation(fields: [organizationId], references: [id])
integration Integration @relation(fields: [integrationId], references: [id])
title String?
description String?
parentPostId String?
releaseId String?
releaseURL String?
settings String?
parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id])
childrenPost Post[] @relation("parentPostId")
image String?
submittedForOrderId String?
submittedForOrder Orders? @relation(fields: [submittedForOrderId], references: [id])
approvedSubmitForOrder APPROVED_SUBMIT_FOR_ORDER @default(NO)
lastMessageId String?
lastMessage Messages? @relation(fields: [lastMessageId], references: [id])
payoutProblems PayoutProblems[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([group])
@@index([deletedAt])
@ -236,6 +251,13 @@ model Post {
@@index([state])
@@index([organizationId])
@@index([parentPostId])
@@index([submittedForOrderId])
@@index([approvedSubmitForOrder])
@@index([lastMessageId])
@@index([createdAt])
@@index([updatedAt])
@@index([releaseURL])
@@index([integrationId])
}
model Notifications {
@ -254,18 +276,72 @@ model Notifications {
}
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
id String @id @default(uuid())
buyerOrganizationId String
buyerOrganization Organization @relation(fields: [buyerOrganizationId], references: [id])
buyerId String
buyer User @relation("groupBuyer", fields: [buyerId], references: [id])
sellerId String
seller User @relation("groupSeller", fields: [sellerId], references: [id])
messages Messages[]
orders Orders[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([buyerId, sellerId])
@@index([createdAt])
@@index([updatedAt])
@@index([buyerOrganizationId])
}
model PayoutProblems {
id String @id @default(uuid())
status String
orderId String
order Orders @relation(fields: [orderId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
postId String?
post Post? @relation(fields: [postId], references: [id])
amount Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Orders {
id String @id @default(uuid())
buyerId String
sellerId String
posts Post[]
buyer User @relation("orderBuyer", fields: [buyerId], references: [id])
seller User @relation("orderSeller", fields: [sellerId], references: [id])
status OrderStatus
ordersItems OrderItems[]
messageGroupId String
messageGroup MessagesGroup @relation(fields: [messageGroupId], references: [id])
captureId String?
payoutProblems PayoutProblems[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([buyerId])
@@index([sellerId])
@@index([updatedAt])
@@index([createdAt])
@@index([messageGroupId])
}
model OrderItems {
id String @id @default(uuid())
orderId String
order Orders @relation(fields: [orderId], references: [id])
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
quantity Int
price Int
@@index([orderId])
@@index([integrationId])
}
model Messages {
@ -274,6 +350,8 @@ model Messages {
content String?
groupId String
group MessagesGroup @relation(fields: [groupId], references: [id])
special String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@ -283,6 +361,13 @@ model Messages {
@@index([deletedAt])
}
enum OrderStatus {
PENDING
ACCEPTED
CANCELED
COMPLETED
}
enum From {
BUYER
SELLER
@ -315,3 +400,9 @@ enum Role {
ADMIN
USER
}
enum APPROVED_SUBMIT_FOR_ORDER {
NO
WAITING_CONFIRMATION
YES
}

View File

@ -1,4 +1,6 @@
export interface PricingInnerInterface {
month_price: number;
year_price: number;
channel?: number;
posts_per_month: number;
team_members: boolean;
@ -12,7 +14,9 @@ export interface PricingInterface {
}
export const pricing: PricingInterface = {
FREE: {
channel: 3,
month_price: 0,
year_price: 0,
channel: 2,
posts_per_month: 30,
team_members: false,
community_features: false,
@ -21,6 +25,9 @@ export const pricing: PricingInterface = {
import_from_channels: false,
},
STANDARD: {
month_price: 30,
year_price: 288,
channel: 5,
posts_per_month: 400,
team_members: false,
ai: true,
@ -29,6 +36,9 @@ export const pricing: PricingInterface = {
import_from_channels: true,
},
PRO: {
month_price: 40,
year_price: 384,
channel: 8,
posts_per_month: 1000000,
community_features: true,
team_members: true,

View File

@ -1,13 +1,9 @@
import {IsIn, Max, Min} from "class-validator";
import { IsIn } from 'class-validator';
export class BillingSubscribeDto {
@Min(1)
@Max(60)
total: number;
@IsIn(['MONTHLY', 'YEARLY'])
period: 'MONTHLY' | 'YEARLY';
@IsIn(['MONTHLY', 'YEARLY'])
period: 'MONTHLY' | 'YEARLY';
@IsIn(['STANDARD', 'PRO'])
billing: 'STANDARD' | 'PRO';
@IsIn(['STANDARD', 'PRO'])
billing: 'STANDARD' | 'PRO';
}

View File

@ -0,0 +1,27 @@
import {
ArrayMinSize,
IsNumber,
IsString,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
export class SocialMedia {
@IsNumber()
total: number;
@IsString()
value: string;
@IsNumber()
price: number;
}
export class CreateOfferDto {
@IsString()
group: string;
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => SocialMedia)
socialMedia: SocialMedia[];
}

View File

@ -71,6 +71,10 @@ export class CreatePostDto {
@IsIn(['draft', 'schedule', 'now'])
type: 'draft' | 'schedule' | 'now';
@IsOptional()
@IsString()
order: string;
@IsDefined()
@IsDateString()
date: string;

View File

@ -1,11 +1,13 @@
import Stripe from 'stripe';
import { Injectable } from '@nestjs/common';
import { Organization } from '@prisma/client';
import { OrderItems, Organization, User } from '@prisma/client';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto';
import { groupBy } from 'lodash';
import { capitalize, groupBy } from 'lodash';
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-04-10',
@ -15,7 +17,8 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
export class StripeService {
constructor(
private _subscriptionService: SubscriptionService,
private _organizationService: OrganizationService
private _organizationService: OrganizationService,
private _messagesService: MessagesService
) {}
validateRequest(rawBody: Buffer, signature: string, endpointSecret: string) {
return stripe.webhooks.constructEvent(rawBody, signature, endpointSecret);
@ -23,12 +26,17 @@ export class StripeService {
async updateAccount(event: Stripe.AccountUpdatedEvent) {
if (!event.account) {
return ;
return;
}
console.log(JSON.stringify(event.data.object, null, 2));
const accountCharges = event.data.object.payouts_enabled && event.data.object.payouts_enabled && !event?.data?.object?.requirements?.disabled_reason;
await this._subscriptionService.updateConnectedStatus(event.account!, accountCharges);
const accountCharges =
event.data.object.payouts_enabled &&
event.data.object.charges_enabled &&
!event?.data?.object?.requirements?.disabled_reason;
await this._subscriptionService.updateConnectedStatus(
event.account!,
accountCharges
);
}
createSubscription(event: Stripe.CustomerSubscriptionCreatedEvent) {
@ -46,7 +54,7 @@ export class StripeService {
return this._subscriptionService.createOrUpdateSubscription(
id,
event.data.object.customer as string,
event?.data?.object?.items?.data?.[0]?.quantity || 0,
pricing[billing].channel!,
billing,
period,
event.data.object.cancel_at
@ -67,7 +75,7 @@ export class StripeService {
return this._subscriptionService.createOrUpdateSubscription(
id,
event.data.object.customer as string,
event?.data?.object?.items?.data?.[0]?.quantity || 0,
pricing[billing].channel!,
billing,
period,
event.data.object.cancel_at
@ -121,24 +129,38 @@ export class StripeService {
async prorate(organizationId: string, body: BillingSubscribeDto) {
const org = await this._organizationService.getOrgById(organizationId);
const customer = await this.createOrGetCustomer(org!);
const priceData = pricing[body.billing];
const allProducts = await stripe.products.list({
active: true,
expand: ['data.prices'],
});
const findProduct = allProducts.data.find(
(product) => product.name.toLowerCase() === body.billing.toLowerCase()
);
(product) => product.name.toUpperCase() === body.billing.toUpperCase()
) || await stripe.products.create({
active: true,
name: body.billing,
});
const pricesList = await stripe.prices.list({
active: true,
product: findProduct?.id,
product: findProduct!.id,
});
const findPrice = pricesList.data.find(
(p) =>
p?.recurring?.interval?.toLowerCase() ===
body?.period?.toLowerCase().replace('ly', '')
);
p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') &&
p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100
) || await stripe.prices.create({
active: true,
product: findProduct!.id,
currency: 'usd',
nickname: body.billing + ' ' + body.period,
unit_amount: (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100,
recurring: {
interval: body.period === 'MONTHLY' ? 'month' : 'year',
},
});
const proration_date = Math.floor(Date.now() / 1000);
@ -157,7 +179,7 @@ export class StripeService {
{
id: currentUserSubscription?.data?.[0]?.items?.data?.[0]?.id,
price: findPrice?.id!,
quantity: body?.total,
quantity: 1,
},
],
subscription_proration_date: proration_date,
@ -221,9 +243,8 @@ export class StripeService {
private async createCheckoutSession(
uniqueId: string,
customer: string,
metaData: any,
price: string,
quantity: number
body: BillingSubscribeDto,
price: string
) {
const { url } = await stripe.checkout.sessions.create({
customer,
@ -232,15 +253,15 @@ export class StripeService {
subscription_data: {
metadata: {
service: 'gitroom',
...metaData,
...body,
uniqueId,
},
},
allow_promotion_codes: true,
line_items: [
{
price: price,
quantity: quantity,
price,
quantity: 1,
},
],
});
@ -271,7 +292,7 @@ export class StripeService {
metadata: {
service: 'gitroom',
},
email
email,
});
await this._subscriptionService.updateAccount(userId, account.id);
@ -290,18 +311,80 @@ export class StripeService {
return accountLink.url;
}
async payAccountStepOne(
userId: string,
organization: Organization,
seller: User,
orderId: string,
ordersItems: Array<{
integrationType: string;
quantity: number;
price: number;
}>,
groupId: string
) {
const customer = (await this.createOrGetCustomer(organization))!;
const price = ordersItems.reduce((all, current) => {
return all + current.price * current.quantity;
}, 0);
const { url } = await stripe.checkout.sessions.create({
customer,
mode: 'payment',
currency: 'usd',
success_url: process.env['FRONTEND_URL'] + `/messages/${groupId}`,
metadata: {
orderId,
service: 'gitroom',
type: 'marketplace',
},
line_items: [
...ordersItems,
{
integrationType: `Gitroom Fee (${+process.env.FEE_AMOUNT! * 100}%)`,
quantity: 1,
price: price * +process.env.FEE_AMOUNT!,
},
].map((item) => ({
price_data: {
currency: 'usd',
product_data: {
// @ts-ignore
name:
(!item.price ? 'Platform: ' : '') +
capitalize(item.integrationType),
},
// @ts-ignore
unit_amount: item.price * 100,
},
quantity: item.quantity,
})),
payment_intent_data: {
transfer_group: orderId,
},
});
return { url };
}
async subscribe(organizationId: string, body: BillingSubscribeDto) {
const id = makeId(10);
const priceData = pricing[body.billing];
const org = await this._organizationService.getOrgById(organizationId);
const customer = await this.createOrGetCustomer(org!);
const allProducts = await stripe.products.list({
active: true,
expand: ['data.prices'],
});
const findProduct = allProducts.data.find(
(product) => product.name.toLowerCase() === body.billing.toLowerCase()
);
(product) => product.name.toUpperCase() === body.billing.toUpperCase()
) || await stripe.products.create({
active: true,
name: body.billing,
});
const pricesList = await stripe.prices.list({
active: true,
product: findProduct!.id,
@ -309,22 +392,26 @@ export class StripeService {
const findPrice = pricesList.data.find(
(p) =>
p?.recurring?.interval?.toLowerCase() ===
body?.period?.toLowerCase().replace('ly', '')
);
p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') &&
p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100
) || await stripe.prices.create({
active: true,
product: findProduct!.id,
currency: 'usd',
nickname: body.billing + ' ' + body.period,
unit_amount: (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100,
recurring: {
interval: body.period === 'MONTHLY' ? 'month' : 'year',
},
});
const currentUserSubscription = await stripe.subscriptions.list({
customer,
status: 'active',
});
if (!currentUserSubscription.data.length) {
return this.createCheckoutSession(
id,
customer,
body,
findPrice!.id,
body.total
);
return this.createCheckoutSession(id, customer, body, findPrice!.id);
}
try {
@ -340,7 +427,7 @@ export class StripeService {
{
id: currentUserSubscription.data[0].items.data[0].id,
price: findPrice!.id,
quantity: body.total,
quantity: 1,
},
],
});
@ -353,4 +440,40 @@ export class StripeService {
};
}
}
async updateOrder(event: Stripe.CheckoutSessionCompletedEvent) {
if (event?.data?.object?.metadata?.type !== 'marketplace') {
return { ok: true };
}
const { orderId } = event?.data?.object?.metadata || { orderId: '' };
if (!orderId) {
return;
}
const charge = (
await stripe.paymentIntents.retrieve(
event.data.object.payment_intent as string
)
).latest_charge;
const id = typeof charge === 'string' ? charge : charge?.id;
await this._messagesService.changeOrderStatus(orderId, 'ACCEPTED', id);
return { ok: true };
}
async payout(
orderId: string,
charge: string,
account: string,
price: number
) {
return stripe.transfers.create({
amount: price * 100,
currency: 'usd',
destination: account,
source_transaction: charge,
transfer_group: orderId,
});
}
}

View File

@ -0,0 +1,146 @@
import {
FC,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import interClass from '../helpers/inter.font';
import { clsx } from 'clsx';
import { useFormContext } from 'react-hook-form';
export const CustomSelect: FC<{
error?: any;
disableForm?: boolean;
label: string;
name: string;
placeholder?: string;
removeError?: boolean;
onChange?: () => void;
className?: string;
options: Array<{ value: string; label: string; icon?: ReactNode }>;
}> = (props) => {
const { options, onChange, placeholder, className, removeError, label, ...rest } = props;
const form = useFormContext();
const value = form.watch(props.name);
const [isOpen, setIsOpen] = useState(false);
const err = useMemo(() => {
const split = (props.name + '.value').split('.');
let errIn = form?.formState?.errors;
for (let i = 0; i < split.length; i++) {
// @ts-ignore
errIn = errIn?.[split[i]];
}
return errIn?.message;
}, [props.name, form]);
const option = useMemo(() => {
if (value?.value && options.length) {
return (
options.find((option) => option.value === value.value) || {
label: placeholder,
icon: false,
}
);
}
return { label: placeholder };
}, [value, options]);
const changeOpen = useCallback(() => {
setIsOpen(!isOpen);
}, [isOpen]);
const setOption = useCallback(
(newOption: any) => (e: any) => {
form.setValue(props.name, newOption);
setIsOpen(false);
e.stopPropagation();
},
[]
);
useEffect(() => {
if (onChange) {
onChange();
}
}, [value]);
return (
<div className={clsx("flex flex-col gap-[6px] relative", className)}>
{!!label && (<div className={`${interClass} text-[14px]`}>{label}</div>)}
<div
className={clsx(
'bg-input h-[44px] border-fifth border rounded-[4px] text-inputText placeholder-inputText items-center justify-center flex'
)}
onClick={changeOpen}
>
<div className="flex-1 pl-[16px] text-[14px] select-none flex gap-[8px]">
{!!option.icon && (
<div className="flex justify-center items-center">
{option.icon}
</div>
)}
{option.label}
</div>
<div className="pr-[16px] flex gap-[8px]">
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M13.354 6.35378L8.35403 11.3538C8.30759 11.4003 8.25245 11.4372 8.19175 11.4623C8.13105 11.4875 8.06599 11.5004 8.00028 11.5004C7.93457 11.5004 7.86951 11.4875 7.80881 11.4623C7.74811 11.4372 7.69296 11.4003 7.64653 11.3538L2.64653 6.35378C2.55271 6.25996 2.5 6.13272 2.5 6.00003C2.5 5.86735 2.55271 5.7401 2.64653 5.64628C2.74035 5.55246 2.8676 5.49976 3.00028 5.49976C3.13296 5.49976 3.26021 5.55246 3.35403 5.64628L8.00028 10.2932L12.6465 5.64628C12.693 5.59983 12.7481 5.56298 12.8088 5.53784C12.8695 5.5127 12.9346 5.49976 13.0003 5.49976C13.066 5.49976 13.131 5.5127 13.1917 5.53784C13.2524 5.56298 13.3076 5.59983 13.354 5.64628C13.4005 5.69274 13.4373 5.74789 13.4625 5.80859C13.4876 5.86928 13.5006 5.93434 13.5006 6.00003C13.5006 6.06573 13.4876 6.13079 13.4625 6.19148C13.4373 6.25218 13.4005 6.30733 13.354 6.35378Z"
fill="#64748B"
/>
</svg>
</div>
{!!value && (
<div onClick={setOption(undefined)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M12.854 12.1463C12.9005 12.1927 12.9373 12.2479 12.9625 12.3086C12.9876 12.3693 13.0006 12.4343 13.0006 12.5C13.0006 12.5657 12.9876 12.6308 12.9625 12.6915C12.9373 12.7522 12.9005 12.8073 12.854 12.8538C12.8076 12.9002 12.7524 12.9371 12.6917 12.9622C12.631 12.9874 12.566 13.0003 12.5003 13.0003C12.4346 13.0003 12.3695 12.9874 12.3088 12.9622C12.2481 12.9371 12.193 12.9002 12.1465 12.8538L8.00028 8.70691L3.85403 12.8538C3.76021 12.9476 3.63296 13.0003 3.50028 13.0003C3.3676 13.0003 3.24035 12.9476 3.14653 12.8538C3.05271 12.76 3 12.6327 3 12.5C3 12.3674 3.05271 12.2401 3.14653 12.1463L7.2934 8.00003L3.14653 3.85378C3.05271 3.75996 3 3.63272 3 3.50003C3 3.36735 3.05271 3.2401 3.14653 3.14628C3.24035 3.05246 3.3676 2.99976 3.50028 2.99976C3.63296 2.99976 3.76021 3.05246 3.85403 3.14628L8.00028 7.29316L12.1465 3.14628C12.2403 3.05246 12.3676 2.99976 12.5003 2.99976C12.633 2.99976 12.7602 3.05246 12.854 3.14628C12.9478 3.2401 13.0006 3.36735 13.0006 3.50003C13.0006 3.63272 12.9478 3.75996 12.854 3.85378L8.70715 8.00003L12.854 12.1463Z"
fill="#64748B"
/>
</svg>
</div>
)}
</div>
</div>
{isOpen && (
<div className={clsx(label && !removeError && '-mt-[23px]', "z-[100] absolute w-full top-[100%] left-0 flex items-center rounded-bl-[4px] rounded-br-[4px] flex-col bg-fifth gap-[1px] border-l border-r border-b border-fifth overflow-hidden")}>
{options.map((option) => (
<div
onClick={setOption(option)}
className="px-[16px] py-[8px] bg-input w-full flex gap-[8px] hover:bg-[#0b0f1c] select-none cursor-pointer"
>
{!!option.icon && (
<div className="flex justify-center items-center">
{option.icon}
</div>
)}
<div className="flex-1 text-[14px]">{option.label}</div>
</div>
))}
</div>
)}
{!removeError && (
<div className="text-red-400 text-[12px]">
{(err as any) || <>&nbsp;</>}
</div>
)}
</div>
);
};

View File

@ -1,8 +1,10 @@
'use client';
import { DetailedHTMLProps, FC, InputHTMLAttributes, useMemo } from 'react';
import {
DetailedHTMLProps, FC, InputHTMLAttributes, ReactNode, useEffect, useMemo
} from 'react';
import { clsx } from 'clsx';
import { useFormContext } from 'react-hook-form';
import { useFormContext, useWatch } from 'react-hook-form';
import interClass from '../helpers/inter.font';
export const Input: FC<
@ -10,11 +12,22 @@ export const Input: FC<
removeError?: boolean;
error?: any;
disableForm?: boolean;
customUpdate?: () => void;
label: string;
name: string;
icon?: ReactNode;
}
> = (props) => {
const { label, removeError, className, disableForm, error, ...rest } = props;
const {
label,
icon,
removeError,
customUpdate,
className,
disableForm,
error,
...rest
} = props;
const form = useFormContext();
const err = useMemo(() => {
if (error) return error;
@ -22,18 +35,35 @@ export const Input: FC<
return form?.formState?.errors?.[props?.name!]?.message! as string;
}, [form?.formState?.errors?.[props?.name!]?.message, error]);
const watch = customUpdate ? form?.watch(props.name) : null;
useEffect(() => {
if (customUpdate) {
customUpdate();
}
}, [watch]);
return (
<div className="flex flex-col gap-[6px]">
<div className={`${interClass} text-[14px]`}>{label}</div>
<input
{...(disableForm ? {} : form.register(props.name))}
<div
className={clsx(
'bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText',
'bg-input h-[44px] border-fifth border rounded-[4px] text-inputText placeholder-inputText flex items-center justify-center',
className
)}
{...rest}
/>
{!removeError && <div className="text-red-400 text-[12px]">{err || <>&nbsp;</>}</div>}
>
{icon && <div className="pl-[16px]">{icon}</div>}
<input
className={clsx(
'h-full bg-transparent outline-none flex-1',
icon ? 'pl-[8px] pr-[16px]' : 'px-[16px]'
)}
{...(disableForm ? {} : form.register(props.name))}
{...rest}
/>
</div>
{!removeError && (
<div className="text-red-400 text-[12px]">{err || <>&nbsp;</>}</div>
)}
</div>
);
};

View File

@ -0,0 +1,73 @@
import { FC, useCallback, useEffect } from 'react';
import interClass from '../helpers/inter.font';
import { clsx } from 'clsx';
import { useFormContext } from 'react-hook-form';
export const Total: FC<{ name: string; customOnChange?: () => void }> = (
props
) => {
const { name, customOnChange } = props;
const form = useFormContext();
const value = form.watch(props.name);
const changeNumber = useCallback(
(value: number) => () => {
if (value === 0) {
return;
}
form.setValue(name, value);
},
[value]
);
useEffect(() => {
if (customOnChange) {
customOnChange();
}
}, [value, customOnChange]);
return (
<div className="flex flex-col gap-[6px] relative w-[158px]">
<div className={`${interClass} text-[14px]`}>Total</div>
<div
className={clsx(
'bg-input h-[44px] border-fifth border rounded-[4px] text-inputText placeholder-inputText items-center justify-center flex'
)}
>
<div className="flex-1 px-[16px] text-[14px] select-none flex gap-[8px] items-center">
<div onClick={changeNumber(value - 1)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M11 8C11 8.13261 10.9473 8.25979 10.8536 8.35355C10.7598 8.44732 10.6326 8.5 10.5 8.5H5.5C5.36739 8.5 5.24022 8.44732 5.14645 8.35355C5.05268 8.25979 5 8.13261 5 8C5 7.86739 5.05268 7.74021 5.14645 7.64645C5.24022 7.55268 5.36739 7.5 5.5 7.5H10.5C10.6326 7.5 10.7598 7.55268 10.8536 7.64645C10.9473 7.74021 11 7.86739 11 8ZM14.5 8C14.5 9.28558 14.1188 10.5423 13.4046 11.6112C12.6903 12.6801 11.6752 13.5132 10.4874 14.0052C9.29973 14.4972 7.99279 14.6259 6.73192 14.3751C5.47104 14.1243 4.31285 13.5052 3.40381 12.5962C2.49477 11.6872 1.8757 10.529 1.6249 9.26809C1.37409 8.00721 1.50282 6.70028 1.99479 5.51256C2.48676 4.32484 3.31988 3.30968 4.3888 2.59545C5.45772 1.88122 6.71442 1.5 8 1.5C9.72335 1.50182 11.3756 2.18722 12.5942 3.40582C13.8128 4.62441 14.4982 6.27665 14.5 8ZM13.5 8C13.5 6.9122 13.1774 5.84883 12.5731 4.94436C11.9687 4.03989 11.1098 3.33494 10.1048 2.91866C9.09977 2.50238 7.9939 2.39346 6.92701 2.60568C5.86011 2.8179 4.8801 3.34172 4.11092 4.11091C3.34173 4.8801 2.8179 5.86011 2.60568 6.927C2.39347 7.9939 2.50238 9.09977 2.91867 10.1048C3.33495 11.1098 4.0399 11.9687 4.94437 12.5731C5.84884 13.1774 6.91221 13.5 8 13.5C9.45819 13.4983 10.8562 12.9184 11.8873 11.8873C12.9184 10.8562 13.4983 9.45818 13.5 8Z"
fill={value === 1 ? '#64748B' : 'white'}
/>
</svg>
</div>
<div className="flex-1 text-white text-[14px] text-center">
{value}
</div>
<div onClick={changeNumber(value + 1)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M8 1.5C6.71442 1.5 5.45772 1.88122 4.3888 2.59545C3.31988 3.30968 2.48676 4.32484 1.99479 5.51256C1.50282 6.70028 1.37409 8.00721 1.6249 9.26809C1.8757 10.529 2.49477 11.6872 3.40381 12.5962C4.31285 13.5052 5.47104 14.1243 6.73192 14.3751C7.99279 14.6259 9.29973 14.4972 10.4874 14.0052C11.6752 13.5132 12.6903 12.6801 13.4046 11.6112C14.1188 10.5423 14.5 9.28558 14.5 8C14.4982 6.27665 13.8128 4.62441 12.5942 3.40582C11.3756 2.18722 9.72335 1.50182 8 1.5ZM8 13.5C6.91221 13.5 5.84884 13.1774 4.94437 12.5731C4.0399 11.9687 3.33495 11.1098 2.91867 10.1048C2.50238 9.09977 2.39347 7.9939 2.60568 6.927C2.8179 5.86011 3.34173 4.8801 4.11092 4.11091C4.8801 3.34172 5.86011 2.8179 6.92701 2.60568C7.9939 2.39346 9.09977 2.50238 10.1048 2.91866C11.1098 3.33494 11.9687 4.03989 12.5731 4.94436C13.1774 5.84883 13.5 6.9122 13.5 8C13.4983 9.45818 12.9184 10.8562 11.8873 11.8873C10.8562 12.9184 9.45819 13.4983 8 13.5ZM11 8C11 8.13261 10.9473 8.25979 10.8536 8.35355C10.7598 8.44732 10.6326 8.5 10.5 8.5H8.5V10.5C8.5 10.6326 8.44732 10.7598 8.35356 10.8536C8.25979 10.9473 8.13261 11 8 11C7.86739 11 7.74022 10.9473 7.64645 10.8536C7.55268 10.7598 7.5 10.6326 7.5 10.5V8.5H5.5C5.36739 8.5 5.24022 8.44732 5.14645 8.35355C5.05268 8.25979 5 8.13261 5 8C5 7.86739 5.05268 7.74021 5.14645 7.64645C5.24022 7.55268 5.36739 7.5 5.5 7.5H7.5V5.5C7.5 5.36739 7.55268 5.24021 7.64645 5.14645C7.74022 5.05268 7.86739 5 8 5C8.13261 5 8.25979 5.05268 8.35356 5.14645C8.44732 5.24021 8.5 5.36739 8.5 5.5V7.5H10.5C10.6326 7.5 10.7598 7.55268 10.8536 7.64645C10.9473 7.74021 11 7.86739 11 8Z"
fill="white"
/>
</svg>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,40 @@
'use client';
import { useEffect, useState } from 'react';
export function usePageVisibility(page: number) {
if (typeof document === 'undefined') {
return true;
}
const [isVisible, setIsVisible] = useState(!document.hidden);
useEffect(() => {
if (page > 1) {
return;
}
const handleVisibilityChange = () => {
setIsVisible(!document.hidden);
};
const onBlur = () => {
setIsVisible(false);
};
const onFocus = () => {
setIsVisible(true);
};
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
document.removeEventListener('blur', onBlur);
document.removeEventListener('focus', focus);
};
}, []);
return isVisible;
}

50
package-lock.json generated
View File

@ -40,6 +40,7 @@
"@types/multer": "^1.4.11",
"@types/remove-markdown": "^0.3.4",
"@types/stripe": "^8.0.417",
"@types/yup": "^0.32.0",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/react-md-editor": "^4.0.3",
"@virtual-grid/react": "^2.0.2",
@ -92,7 +93,8 @@
"twitter-api-v2": "^1.16.0",
"use-debounce": "^10.0.0",
"utf-8-validate": "^5.0.10",
"yargs": "^17.7.2"
"yargs": "^17.7.2",
"yup": "^1.4.0"
},
"devDependencies": {
"@mintlify/scraping": "^3.0.90",
@ -13449,6 +13451,15 @@
"@types/node": "*"
}
},
"node_modules/@types/yup": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.32.0.tgz",
"integrity": "sha512-Gr2lllWTDxGVYHgWfL8szjdedERpNgm44L9BDL2cmcHG7Bfd6taEpiW3ayMFLaYvlJr/6bFXDJdh6L406AGlFg==",
"deprecated": "This is a stub types definition. yup provides its own type definitions, so you do not need this installed.",
"dependencies": {
"yup": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
@ -32723,6 +32734,11 @@
"react-is": "^16.13.1"
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
},
"node_modules/property-information": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz",
@ -37800,6 +37816,11 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -37894,6 +37915,11 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@ -40307,6 +40333,28 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yup": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz",
"integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/yup/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",

View File

@ -44,6 +44,7 @@
"@types/multer": "^1.4.11",
"@types/remove-markdown": "^0.3.4",
"@types/stripe": "^8.0.417",
"@types/yup": "^0.32.0",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/react-md-editor": "^4.0.3",
"@virtual-grid/react": "^2.0.2",
@ -96,7 +97,8 @@
"twitter-api-v2": "^1.16.0",
"use-debounce": "^10.0.0",
"utf-8-validate": "^5.0.10",
"yargs": "^17.7.2"
"yargs": "^17.7.2",
"yup": "^1.4.0"
},
"devDependencies": {
"@mintlify/scraping": "^3.0.90",

0
videos.csv Normal file
View File