feat: public api
This commit is contained in:
commit
6fc237f44f
|
|
@ -0,0 +1,19 @@
|
|||
name: "🙏🏻 Installation Problem"
|
||||
description: "Report an issue with installation"
|
||||
title: "Installation Problem"
|
||||
labels: ["type: installation"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: For installation issues, please visit our https://discord.postiz.com for assistance.
|
||||
description: For installation issues, please visit our [Discord Support](https://discord.postiz.com) for assistance.
|
||||
placeholder: |
|
||||
For installation issues, please visit our https://discord.postiz.com for assistance.
|
||||
Please do not save this issue - do not submit installation issues on GitHub.
|
||||
|
||||
24
README.md
24
README.md
|
|
@ -1,8 +1,14 @@
|
|||
<p align="center">
|
||||
<a href="https://affiliate.postiz.com">
|
||||
<img src="https://github.com/user-attachments/assets/af9f47b3-e20c-402b-bd11-02f39248d738" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://postiz.com" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/765e9d72-3ee7-4a56-9d59-a2c9befe2311">
|
||||
<img alt="Novu Logo" src="https://github.com/user-attachments/assets/f0d30d70-dddb-4142-8876-e9aa6ed1cb99" width="280"/>
|
||||
<img alt="Postiz Logo" src="https://github.com/user-attachments/assets/f0d30d70-dddb-4142-8876-e9aa6ed1cb99" width="280"/>
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
|
@ -58,22 +64,6 @@
|
|||
|
||||
<br />
|
||||
|
||||
|
||||
<p align="center">
|
||||
<br /><br /><br />
|
||||
<h1>We participate in Hacktoberfest 2024! 🎉🎊</h1>
|
||||
<p align="left">We are sending a t-shirt for every merged PR! (max 1 per person)</p>
|
||||
<p align="left"><strong>Rules:</strong></p>
|
||||
<ul align="left">
|
||||
<li>You must create an issue before making a pull request.</li>
|
||||
<li>You can also ask to be assigned to an issue. During Hacktoberfest, each issue can have multiple assignees.</li>
|
||||
<li>We have to approve the issue and add a "hacktoberfest" tag.</li>
|
||||
<li>We encourage everybody to contribute to all types of issues. We will only send swag for issues with features and bug fixes (no typos, sorry).</li>
|
||||
</ul>
|
||||
<p align="center"><img align="center" width="400" src="https://github.com/user-attachments/assets/3ceffccc-e4b3-4098-b9ba-44a94cf01294" /></p>
|
||||
<br /><br /><br />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<video src="https://github.com/user-attachments/assets/05436a01-19c8-4827-b57f-05a5e7637a67" width="100%" />
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Post, Req, Res } from '@nestjs/common';
|
||||
import { Logger, Controller, Get, Post, Req, Res } from '@nestjs/common';
|
||||
import {
|
||||
CopilotRuntime,
|
||||
OpenAIAdapter,
|
||||
|
|
@ -13,6 +13,11 @@ export class CopilotController {
|
|||
constructor(private _subscriptionService: SubscriptionService) {}
|
||||
@Post('/chat')
|
||||
chat(@Req() req: Request, @Res() res: Response) {
|
||||
if (process.env.OPENAI_API_KEY === undefined || process.env.OPENAI_API_KEY === '') {
|
||||
Logger.warn('OpenAI API key not set, chat functionality will not work');
|
||||
return
|
||||
}
|
||||
|
||||
const copilotRuntimeHandler = copilotRuntimeNestEndpoint({
|
||||
endpoint: '/copilot/chat',
|
||||
runtime: new CopilotRuntime(),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseFilters,
|
||||
Body, Controller, Delete, Get, Param, Post, Put, Query, UseFilters
|
||||
} from '@nestjs/common';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto';
|
||||
|
|
@ -29,6 +22,12 @@ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/po
|
|||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import {
|
||||
NotEnoughScopes,
|
||||
RefreshToken,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -43,6 +42,37 @@ export class IntegrationsController {
|
|||
return this._integrationManager.getAllIntegrations();
|
||||
}
|
||||
|
||||
@Get('/customers')
|
||||
getCustomers(@GetOrgFromRequest() org: Organization) {
|
||||
return this._integrationService.customers(org.id);
|
||||
}
|
||||
|
||||
@Put('/:id/group')
|
||||
async updateIntegrationGroup(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { group: string }
|
||||
) {
|
||||
return this._integrationService.updateIntegrationGroup(
|
||||
org.id,
|
||||
id,
|
||||
body.group
|
||||
);
|
||||
}
|
||||
|
||||
@Put('/:id/customer-name')
|
||||
async updateOnCustomerName(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { name: string }
|
||||
) {
|
||||
return this._integrationService.updateOnCustomerName(
|
||||
org.id,
|
||||
id,
|
||||
body.name
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
async getIntegrationList(@GetOrgFromRequest() org: Organization) {
|
||||
return {
|
||||
|
|
@ -57,7 +87,7 @@ export class IntegrationsController {
|
|||
id: p.id,
|
||||
internalId: p.internalId,
|
||||
disabled: p.disabled,
|
||||
picture: p.picture,
|
||||
picture: p.picture || '/no-picture.jpg',
|
||||
identifier: p.providerIdentifier,
|
||||
inBetweenSteps: p.inBetweenSteps,
|
||||
refreshNeeded: p.refreshNeeded,
|
||||
|
|
@ -66,6 +96,7 @@ export class IntegrationsController {
|
|||
time: JSON.parse(p.postingTimes),
|
||||
changeProfilePicture: !!findIntegration?.changeProfilePicture,
|
||||
changeNickName: !!findIntegration?.changeNickname,
|
||||
customer: p.customer,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -207,11 +238,51 @@ export class IntegrationsController {
|
|||
}
|
||||
|
||||
if (integrationProvider[body.name]) {
|
||||
return integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
getIntegration.internalId
|
||||
);
|
||||
try {
|
||||
const load = await integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
getIntegration.internalId
|
||||
);
|
||||
|
||||
return load;
|
||||
} catch (err) {
|
||||
if (err instanceof RefreshToken) {
|
||||
const { accessToken, refreshToken, expiresIn } =
|
||||
await integrationProvider.refreshToken(
|
||||
getIntegration.refreshToken
|
||||
);
|
||||
|
||||
if (accessToken) {
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
getIntegration.organizationId,
|
||||
getIntegration.name,
|
||||
getIntegration.picture!,
|
||||
'social',
|
||||
getIntegration.internalId,
|
||||
getIntegration.providerIdentifier,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn
|
||||
);
|
||||
|
||||
getIntegration.token = accessToken;
|
||||
|
||||
if (integrationProvider.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
return this.functionIntegration(org, body);
|
||||
} else {
|
||||
await this._integrationService.disconnectChannel(
|
||||
org.id,
|
||||
getIntegration
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
throw new Error('Function not found');
|
||||
}
|
||||
|
|
@ -323,6 +394,7 @@ export class IntegrationsController {
|
|||
}
|
||||
|
||||
const {
|
||||
error,
|
||||
accessToken,
|
||||
expiresIn,
|
||||
refreshToken,
|
||||
|
|
@ -341,6 +413,17 @@ export class IntegrationsController {
|
|||
details ? JSON.parse(details) : undefined
|
||||
);
|
||||
|
||||
if (typeof auth === 'string') {
|
||||
return res({
|
||||
error: auth,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
});
|
||||
}
|
||||
|
||||
if (refresh && integrationProvider.reConnect) {
|
||||
const newAuth = await integrationProvider.reConnect(
|
||||
auth.id,
|
||||
|
|
@ -353,13 +436,31 @@ export class IntegrationsController {
|
|||
return res(auth);
|
||||
});
|
||||
|
||||
if (!id) {
|
||||
throw new Error('Invalid api key');
|
||||
if (error) {
|
||||
throw new NotEnoughScopes(error);
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw new NotEnoughScopes('Invalid API key');
|
||||
}
|
||||
|
||||
if (refresh && id !== refresh) {
|
||||
throw new NotEnoughScopes(
|
||||
'Please refresh the channel that needs to be refreshed'
|
||||
);
|
||||
}
|
||||
|
||||
let validName = name;
|
||||
if (!validName) {
|
||||
if (username) {
|
||||
validName = username.split('.')[0] ?? username;
|
||||
} else {
|
||||
validName = `Channel_${String(id).slice(0, 8)}`;
|
||||
}
|
||||
}
|
||||
return this._integrationService.createOrUpdateIntegration(
|
||||
org.id,
|
||||
name,
|
||||
validName.trim(),
|
||||
picture,
|
||||
'social',
|
||||
String(id),
|
||||
|
|
@ -446,4 +547,35 @@ export class IntegrationsController {
|
|||
|
||||
return this._integrationService.deleteChannel(org.id, id);
|
||||
}
|
||||
|
||||
@Get('/plug/list')
|
||||
async getPlugList() {
|
||||
return { plugs: this._integrationManager.getAllPlugs() };
|
||||
}
|
||||
|
||||
@Get('/:id/plugs')
|
||||
async getPlugsByIntegrationId(
|
||||
@Param('id') id: string,
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.getPlugsByIntegrationId(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/:id/plugs')
|
||||
async postPlugsByIntegrationId(
|
||||
@Param('id') id: string,
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: PlugDto
|
||||
) {
|
||||
return this._integrationService.createOrUpdatePlug(org.id, id, body);
|
||||
}
|
||||
|
||||
@Put('/plugs/:id/activate')
|
||||
async changePlugActivation(
|
||||
@Param('id') id: string,
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body('status') status: boolean
|
||||
) {
|
||||
return this._integrationService.changePlugActivation(org.id, id, status);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import handleR2Upload from '@gitroom/nestjs-libraries/upload/r2.uploader';
|
|||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { basename } from 'path';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
|
||||
@ApiTags('Media')
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export class UsersController {
|
|||
if (!organization) {
|
||||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return {
|
||||
...user,
|
||||
orgId: organization.id,
|
||||
|
|
@ -61,6 +61,8 @@ export class UsersController {
|
|||
isLifetime: !!organization?.subscription?.isLifetime,
|
||||
admin: !!user.isSuperAdmin,
|
||||
impersonate: !!req.cookies.impersonate,
|
||||
// @ts-ignore
|
||||
publicApi: (organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN') ? organization?.apiKey : '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,31 @@ import { APP_GUARD } from '@nestjs/core';
|
|||
import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { PluginModule } from '@gitroom/plugins/plugin.module';
|
||||
import { PublicApiModule } from '@gitroom/backend/public-api/public.api.module';
|
||||
import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [BullMqModule, DatabaseModule, ApiModule, PluginModule],
|
||||
imports: [
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PluginModule,
|
||||
PublicApiModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: 20,
|
||||
},
|
||||
]),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerBehindProxyGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: PoliciesGuard,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { AuthService } from '@gitroom/backend/services/auth/auth.service';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
|
||||
import { PermissionsService } from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { UploadModule } from '@gitroom/nestjs-libraries/upload/upload.module';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
|
||||
import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service';
|
||||
import { PublicIntegrationsController } from '@gitroom/backend/public-api/routes/v1/public.integrations.controller';
|
||||
import { PublicAuthMiddleware } from '@gitroom/backend/services/auth/public.auth.middleware';
|
||||
|
||||
const authenticatedController = [
|
||||
PublicIntegrationsController
|
||||
];
|
||||
@Module({
|
||||
imports: [
|
||||
UploadModule,
|
||||
],
|
||||
controllers: [
|
||||
...authenticatedController,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
StripeService,
|
||||
OpenaiService,
|
||||
ExtractContentService,
|
||||
PoliciesGuard,
|
||||
PermissionsService,
|
||||
CodesService,
|
||||
IntegrationManager,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
},
|
||||
})
|
||||
export class PublicApiModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(PublicAuthMiddleware).forRoutes(...authenticatedController);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
Body, Controller, Get, HttpException, Post, UploadedFile, UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import {
|
||||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permissions.service';
|
||||
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
|
||||
@ApiTags('Public API')
|
||||
@Controller('/public/v1')
|
||||
export class PublicIntegrationsController {
|
||||
private storage = UploadFactory.createStorage();
|
||||
|
||||
constructor(
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService,
|
||||
private _mediaService: MediaService
|
||||
) {}
|
||||
|
||||
@Post('/upload')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadSimple(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile('file') file: Express.Multer.File
|
||||
) {
|
||||
if (!file) {
|
||||
throw new HttpException({msg: 'No file provided'}, 400);
|
||||
}
|
||||
|
||||
const getFile = await this.storage.uploadFile(file);
|
||||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
getFile.originalname,
|
||||
getFile.path
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/posts')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH])
|
||||
createPost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: CreatePostDto
|
||||
) {
|
||||
console.log(JSON.stringify(body, null, 2));
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
|
||||
@Get('/integrations')
|
||||
async listIntegration(@GetOrgFromRequest() org: Organization) {
|
||||
return (await this._integrationService.getIntegrationsList(org.id)).map(
|
||||
(org) => ({
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
identifier: org.providerIdentifier,
|
||||
picture: org.picture,
|
||||
disabled: org.disabled,
|
||||
profile: org.profile,
|
||||
customer: org.customer
|
||||
? {
|
||||
id: org.customer.id,
|
||||
name: org.customer.name,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
|
||||
@Injectable()
|
||||
export class PublicAuthMiddleware implements NestMiddleware {
|
||||
constructor(private _organizationService: OrganizationService) {}
|
||||
async use(req: Request, res: Response, next: NextFunction) {
|
||||
const auth = (req.headers.authorization ||
|
||||
req.headers.Authorization) as string;
|
||||
if (!auth) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'No API Key found' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const org = await this._organizationService.getOrgByApiKey(auth);
|
||||
if (!org) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'Invalid API key' });
|
||||
return ;
|
||||
}
|
||||
|
||||
if (!!process.env.STRIPE_SECRET_KEY && !org.subscription) {
|
||||
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'No subscription found' });
|
||||
return ;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
req.org = {...org, users: [{users: {role: 'SUPERADMIN'}}]};
|
||||
} catch (err) {
|
||||
throw new HttpForbiddenException();
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
|
|
@ -28,7 +28,8 @@ export default async function Page({
|
|||
});
|
||||
|
||||
if (data.status === HttpStatusCode.NotAcceptable) {
|
||||
return redirect(`/launches?scope=missing`);
|
||||
const { msg } = await data.json();
|
||||
return redirect(`/launches?msg=${msg}`);
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -53,5 +54,5 @@ export default async function Page({
|
|||
return redirect(`/launches?added=${provider}&continue=${id}`);
|
||||
}
|
||||
|
||||
return redirect(`/launches?added=${provider}`);
|
||||
return redirect(`/launches?added=${provider}&msg=Channel Updated`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { Plugs } from '@gitroom/frontend/components/plugs/plugs';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { Metadata } from 'next';
|
||||
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Plugs`,
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default async function Index() {
|
||||
return (
|
||||
<>
|
||||
<Plugs />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ html {
|
|||
}
|
||||
.box {
|
||||
position: relative;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
.box span {
|
||||
position: relative;
|
||||
|
|
@ -385,3 +384,14 @@ div div .set-font-family {
|
|||
font-style: normal !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.col-calendar:hover:before {
|
||||
content: "Date passed";
|
||||
color: white;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
opacity: 30%;
|
||||
}
|
||||
|
|
@ -10,22 +10,32 @@ import { Chakra_Petch } from 'next/font/google';
|
|||
import PlausibleProvider from 'next-plausible';
|
||||
import clsx from 'clsx';
|
||||
import { VariableContextComponent } from '@gitroom/react/helpers/variable.context';
|
||||
import { Fragment } from 'react';
|
||||
import { PHProvider } from '@gitroom/react/helpers/posthog';
|
||||
import UtmSaver from '@gitroom/helpers/utils/utm.saver';
|
||||
import { ToltScript } from '@gitroom/frontend/components/layout/tolt.script';
|
||||
|
||||
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
|
||||
|
||||
export default async function AppLayout({ children }: { children: ReactNode }) {
|
||||
const Plausible = !!process.env.STRIPE_PUBLISHABLE_KEY
|
||||
? PlausibleProvider
|
||||
: Fragment;
|
||||
|
||||
return (
|
||||
<html className={interClass}>
|
||||
<head>
|
||||
<link
|
||||
rel="icon"
|
||||
href={!!process.env.IS_GENERAL ? '/favicon.png' : '/postiz-fav.png'}
|
||||
href="/favicon.ico"
|
||||
sizes="any"
|
||||
/>
|
||||
</head>
|
||||
<body className={clsx(chakra.className, 'text-primary dark')}>
|
||||
<VariableContextComponent
|
||||
storageProvider={process.env.STORAGE_PROVIDER! as 'local' | 'cloudflare'}
|
||||
storageProvider={
|
||||
process.env.STORAGE_PROVIDER! as 'local' | 'cloudflare'
|
||||
}
|
||||
backendUrl={process.env.NEXT_PUBLIC_BACKEND_URL!}
|
||||
plontoKey={process.env.NEXT_PUBLIC_POLOTNO!}
|
||||
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY}
|
||||
|
|
@ -33,12 +43,20 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
frontEndUrl={process.env.FRONTEND_URL!}
|
||||
isGeneral={!!process.env.IS_GENERAL}
|
||||
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
|
||||
tolt={process.env.NEXT_PUBLIC_TOLT!}
|
||||
>
|
||||
<PlausibleProvider
|
||||
<ToltScript />
|
||||
<Plausible
|
||||
domain={!!process.env.IS_GENERAL ? 'postiz.com' : 'gitroom.com'}
|
||||
>
|
||||
<LayoutContext>{children}</LayoutContext>
|
||||
</PlausibleProvider>
|
||||
<PHProvider
|
||||
phkey={process.env.NEXT_PUBLIC_POSTHOG_KEY}
|
||||
host={process.env.NEXT_PUBLIC_POSTHOG_HOST}
|
||||
>
|
||||
<UtmSaver />
|
||||
<LayoutContext>{children}</LayoutContext>
|
||||
</PHProvider>
|
||||
</Plausible>
|
||||
</VariableContextComponent>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { FC, useEffect, useMemo, useRef } from 'react';
|
||||
import DrawChart from 'chart.js/auto';
|
||||
import { TotalList } from '@gitroom/frontend/components/analytics/stars.and.forks.interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { chunk } from 'lodash';
|
||||
|
||||
function mergeDataPoints(data: TotalList[], numPoints: number): TotalList[] {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client';
|
||||
import { FC, useEffect, useMemo, useRef } from 'react';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import DrawChart from 'chart.js/auto';
|
||||
import {
|
||||
ForksList,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
import { Slider } from '@gitroom/react/form/slider';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { sortBy } from 'lodash';
|
||||
import { Track } from '@gitroom/react/form/track';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Subscription } from '@prisma/client';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
|
@ -21,9 +19,11 @@ import interClass from '@gitroom/react/helpers/inter.font';
|
|||
import { useRouter } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { AddProviderComponent } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
import { useUtmUrl } from '@gitroom/helpers/utils/utm.saver';
|
||||
import { useTolt } from '@gitroom/frontend/components/layout/tolt.script';
|
||||
|
||||
export interface Tiers {
|
||||
month: Array<{
|
||||
|
|
@ -156,9 +156,11 @@ export const Features: FC<{
|
|||
const Info: FC<{ proceed: (feedback: string) => void }> = (props) => {
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const modal = useModals();
|
||||
const events = useFireEvents();
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
props.proceed(feedback);
|
||||
events('cancel_subscription');
|
||||
modal.closeAll();
|
||||
}, [modal, feedback]);
|
||||
|
||||
|
|
@ -219,6 +221,8 @@ export const MainBillingComponent: FC<{
|
|||
const user = useUser();
|
||||
const modal = useModals();
|
||||
const router = useRouter();
|
||||
const utm = useUtmUrl();
|
||||
const tolt = useTolt();
|
||||
|
||||
const [subscription, setSubscription] = useState<Subscription | undefined>(
|
||||
sub
|
||||
|
|
@ -344,7 +348,9 @@ export const MainBillingComponent: FC<{
|
|||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
period: monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY',
|
||||
utm,
|
||||
billing,
|
||||
tolt: tolt()
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
|
@ -386,7 +392,7 @@ export const MainBillingComponent: FC<{
|
|||
|
||||
setLoading(false);
|
||||
},
|
||||
[monthlyOrYearly, subscription, user]
|
||||
[monthlyOrYearly, subscription, user, utm]
|
||||
);
|
||||
|
||||
if (user?.isLifetime) {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,17 @@ import { CopilotPopup } from '@copilotkit/react-ui';
|
|||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import Image from 'next/image';
|
||||
import { weightedLength } from '@gitroom/helpers/utils/count.length';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
|
||||
function countCharacters(text: string, type: string): number {
|
||||
if (type !== 'x') {
|
||||
return text.length;
|
||||
}
|
||||
|
||||
return weightedLength(text);
|
||||
}
|
||||
|
||||
export const AddEditModal: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
|
|
@ -56,17 +67,36 @@ export const AddEditModal: FC<{
|
|||
reopenModal: () => void;
|
||||
mutate: () => void;
|
||||
}> = (props) => {
|
||||
const { date, integrations, reopenModal, mutate } = props;
|
||||
const [dateState, setDateState] = useState(date);
|
||||
|
||||
// hook to open a new modal
|
||||
const modal = useModals();
|
||||
const { date, integrations: ints, reopenModal, mutate } = props;
|
||||
const [customer, setCustomer] = useState('');
|
||||
|
||||
// selected integrations to allow edit
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback<
|
||||
Integrations[]
|
||||
>([]);
|
||||
|
||||
const integrations = useMemo(() => {
|
||||
if (!customer) {
|
||||
return ints;
|
||||
}
|
||||
|
||||
const list = ints.filter((f) => f?.customer?.id === customer);
|
||||
if (list.length === 1) {
|
||||
setSelectedIntegrations([list[0]]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [customer, ints]);
|
||||
|
||||
const totalCustomers = useMemo(() => {
|
||||
return uniqBy(ints, (i) => i?.customer?.id).length;
|
||||
}, [ints]);
|
||||
|
||||
const [dateState, setDateState] = useState(date);
|
||||
|
||||
// hook to open a new modal
|
||||
const modal = useModals();
|
||||
|
||||
// value of each editor
|
||||
const [value, setValue] = useState<
|
||||
Array<{
|
||||
|
|
@ -267,7 +297,8 @@ export const AddEditModal: FC<{
|
|||
for (const key of allKeys) {
|
||||
if (key.checkValidity) {
|
||||
const check = await key.checkValidity(
|
||||
key?.value.map((p: any) => p.image || [])
|
||||
key?.value.map((p: any) => p.image || []),
|
||||
key.settings
|
||||
);
|
||||
if (typeof check === 'string') {
|
||||
toaster.show(check, 'warning');
|
||||
|
|
@ -276,9 +307,12 @@ export const AddEditModal: FC<{
|
|||
}
|
||||
|
||||
if (
|
||||
key.value.some(
|
||||
(p) => p.content.length > (key.maximumCharacters || 1000000)
|
||||
)
|
||||
key.value.some((p) => {
|
||||
return (
|
||||
countCharacters(p.content, key?.integration?.identifier || '') >
|
||||
(key.maximumCharacters || 1000000)
|
||||
);
|
||||
})
|
||||
) {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
|
|
@ -386,14 +420,15 @@ export const AddEditModal: FC<{
|
|||
/>
|
||||
)}
|
||||
<div
|
||||
className={clsx('flex p-[10px] rounded-[4px] bg-primary gap-[20px]')}
|
||||
id="add-edit-modal"
|
||||
className={clsx(
|
||||
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
|
||||
!expend.expend
|
||||
? 'flex-1 w-1 animate-overflow'
|
||||
: 'w-0 overflow-hidden'
|
||||
!expend.expend ? 'flex-1 animate-overflow' : 'w-0 overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0">
|
||||
|
|
@ -404,6 +439,26 @@ export const AddEditModal: FC<{
|
|||
information={data}
|
||||
onChange={setPostFor}
|
||||
/>
|
||||
{totalCustomers > 1 && (
|
||||
<Select
|
||||
hideErrors={true}
|
||||
label=""
|
||||
name="customer"
|
||||
value={customer}
|
||||
onChange={(e) => {
|
||||
setCustomer(e.target.value);
|
||||
setSelectedIntegrations([]);
|
||||
}}
|
||||
disableForm={true}
|
||||
>
|
||||
<option value="">Selected Customer</option>
|
||||
{uniqBy(ints, (u) => u?.customer?.name).map((p) => (
|
||||
<option key={p.customer?.id} value={p.customer?.id}>
|
||||
Customer: {p.customer?.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<DatePicker onChange={setDateState} date={dateState} />
|
||||
</div>
|
||||
</TopTitle>
|
||||
|
|
@ -419,7 +474,7 @@ export const AddEditModal: FC<{
|
|||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500'
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
|
|
@ -539,13 +594,13 @@ export const AddEditModal: FC<{
|
|||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative h-[68px] flex flex-col rounded-[4px] border border-customColor6 bg-sixth">
|
||||
<div className="flex flex-1 gap-[10px] relative">
|
||||
<div className="absolute w-full h-full flex gap-[10px] justify-end items-center right-[16px]">
|
||||
<Button
|
||||
className="bg-transparent text-inputText"
|
||||
onClick={askClose}
|
||||
>
|
||||
<div className="relative min-h-[68px] flex flex-col rounded-[4px] border border-customColor6 bg-sixth">
|
||||
<div className="gap-[10px] relative flex flex-col justify-center items-center min-h-full pr-[16px]">
|
||||
<div
|
||||
id="add-edit-post-dialog-buttons"
|
||||
className="flex flex-row flex-wrap w-full h-full gap-[10px] justify-end items-center"
|
||||
>
|
||||
<Button className="rounded-[4px]" onClick={askClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Submitted
|
||||
|
|
|
|||
|
|
@ -191,10 +191,11 @@ export const CustomVariables: FC<{
|
|||
validation: string;
|
||||
type: 'text' | 'password';
|
||||
}>;
|
||||
close?: () => void;
|
||||
identifier: string;
|
||||
gotoUrl(url: string): void;
|
||||
}> = (props) => {
|
||||
const { gotoUrl, identifier, variables } = props;
|
||||
const { close, gotoUrl, identifier, variables } = props;
|
||||
const modals = useModals();
|
||||
const schema = useMemo(() => {
|
||||
return object({
|
||||
|
|
@ -241,7 +242,7 @@ export const CustomVariables: FC<{
|
|||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative">
|
||||
<TopTitle title={`Custom URL`} />
|
||||
<button
|
||||
onClick={modals.closeAll}
|
||||
onClick={close || modals.closeAll}
|
||||
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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import { Input } from '@gitroom/react/form/input';
|
|||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import {
|
||||
MediaComponent,
|
||||
showMediaBox,
|
||||
} from '@gitroom/frontend/components/media/media.component';
|
||||
import { showMediaBox } from '@gitroom/frontend/components/media/media.component';
|
||||
|
||||
export const BotPicture: FC<{
|
||||
integration: Integrations;
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ import { useSearchParams } from 'next/navigation';
|
|||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(weekOfYear);
|
||||
import { extend } from 'dayjs';
|
||||
extend(isoWeek);
|
||||
extend(weekOfYear);
|
||||
|
||||
export const CalendarContext = createContext({
|
||||
currentDay: dayjs().day() as 0 | 1 | 2 | 3 | 4 | 5 | 6,
|
||||
|
|
@ -61,6 +62,10 @@ export interface Integrations {
|
|||
changeProfilePicture: boolean;
|
||||
changeNickName: boolean;
|
||||
time: { time: number }[];
|
||||
customer?: {
|
||||
name?: string;
|
||||
id?: string;
|
||||
}
|
||||
}
|
||||
|
||||
function getWeekNumber(date: Date) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import clsx from 'clsx';
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
|
||||
import { Integration, Post, State } from '@prisma/client';
|
||||
import { useAddProvider } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { CommentComponent } from '@gitroom/frontend/components/launches/comments/comment.component';
|
||||
|
|
@ -26,8 +25,19 @@ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
|||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import { groupBy, sortBy } from 'lodash';
|
||||
import Image from 'next/image';
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
import { extend } from 'dayjs';
|
||||
import { isUSCitizen } from './helpers/isuscitizen.utils';
|
||||
import removeMd from 'remove-markdown';
|
||||
extend(isSameOrAfter);
|
||||
extend(isSameOrBefore);
|
||||
|
||||
const convertTimeFormatBasedOnLocality = (time: number) => {
|
||||
if (isUSCitizen()) {
|
||||
return `${time === 12 ? 12 : time % 12}:00 ${time >= 12 ? 'PM' : 'AM'}`;
|
||||
} else {
|
||||
return `${time}:00`;
|
||||
}
|
||||
};
|
||||
|
||||
export const days = [
|
||||
'Monday',
|
||||
|
|
@ -90,7 +100,7 @@ export const DayView = () => {
|
|||
.startOf('day')
|
||||
.add(option[0].time, 'minute')
|
||||
.local()
|
||||
.format('HH:mm')}
|
||||
.format(isUSCitizen() ? 'hh:mm A' : 'HH:mm')}
|
||||
</div>
|
||||
<div
|
||||
key={option[0].time}
|
||||
|
|
@ -139,7 +149,8 @@ export const WeekView = () => {
|
|||
{hours.map((hour) => (
|
||||
<Fragment key={hour}>
|
||||
<div className="p-2 pr-4 bg-secondary text-center items-center justify-center flex">
|
||||
{hour.toString().padStart(2, '0')}:00
|
||||
{/* {hour.toString().padStart(2, '0')}:00 */}
|
||||
{convertTimeFormatBasedOnLocality(hour)}
|
||||
</div>
|
||||
{days.map((day, indexDay) => (
|
||||
<Fragment key={`${day}-${hour}`}>
|
||||
|
|
@ -230,7 +241,7 @@ export const Calendar = () => {
|
|||
const { display } = useCalendar();
|
||||
|
||||
return (
|
||||
<DNDProvider>
|
||||
<>
|
||||
{display === 'day' ? (
|
||||
<DayView />
|
||||
) : display === 'week' ? (
|
||||
|
|
@ -238,7 +249,7 @@ export const Calendar = () => {
|
|||
) : (
|
||||
<MonthView />
|
||||
)}
|
||||
</DNDProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -432,8 +443,9 @@ export const CalendarColumn: FC<{
|
|||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex flex-col flex-1',
|
||||
canDrop && 'bg-white/80'
|
||||
'relative flex flex-col flex-1 text-white',
|
||||
canDrop && 'bg-white/80',
|
||||
isBeforeNow && postList.length === 0 && 'cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -444,8 +456,9 @@ export const CalendarColumn: FC<{
|
|||
}
|
||||
: {})}
|
||||
className={clsx(
|
||||
'flex-col text-[12px] pointer w-full cursor-pointer overflow-hidden overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
isBeforeNow && 'bg-customColor23 flex-1',
|
||||
'flex-col text-[12px] pointer w-full overflow-hidden overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
isBeforeNow ? 'bg-customColor23 flex-1' : 'cursor-pointer',
|
||||
isBeforeNow && postList.length === 0 && 'col-calendar',
|
||||
canBeTrending && 'bg-customColor24'
|
||||
)}
|
||||
>
|
||||
|
|
@ -499,7 +512,10 @@ export const CalendarColumn: FC<{
|
|||
className={`w-full h-full rounded-[10px] hover:border hover:border-seventh flex justify-center items-center gap-[20px] opacity-30 grayscale hover:grayscale-0 hover:opacity-100`}
|
||||
>
|
||||
{integrations.map((selectedIntegrations) => (
|
||||
<div className="relative" key={selectedIntegrations.identifier}>
|
||||
<div
|
||||
className="relative"
|
||||
key={selectedIntegrations.identifier}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500'
|
||||
|
|
@ -589,7 +605,7 @@ const CalendarItem: FC<{
|
|||
</div>
|
||||
<div className="whitespace-pre-wrap line-clamp-3">
|
||||
{state === 'DRAFT' ? 'Draft: ' : ''}
|
||||
{post.content}
|
||||
{removeMd(post.content).replace(/\n/g, ' ')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Autocomplete } from '@mantine/core';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
|
||||
export const CustomerModal: FC<{
|
||||
integration: Integration & { customer?: { id: string; name: string } };
|
||||
onClose: () => void;
|
||||
}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const { onClose, integration } = props;
|
||||
const [customer, setCustomer] = useState(
|
||||
integration.customer?.name || undefined
|
||||
);
|
||||
const modal = useModals();
|
||||
|
||||
const loadCustomers = useCallback(async () => {
|
||||
return (await fetch('/integrations/customers')).json();
|
||||
}, []);
|
||||
|
||||
const removeFromCustomer = useCallback(async () => {
|
||||
saveCustomer(true);
|
||||
}, []);
|
||||
|
||||
const saveCustomer = useCallback(async (removeCustomer?: boolean) => {
|
||||
if (!customer) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(`/integrations/${integration.id}/customer-name`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: removeCustomer ? '' : customer }),
|
||||
});
|
||||
|
||||
modal.closeAll();
|
||||
onClose();
|
||||
}, [customer]);
|
||||
|
||||
const { data } = useSWR('/customers', loadCustomers);
|
||||
|
||||
return (
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
|
||||
<TopTitle title={`Move / Add to customer`} />
|
||||
<button
|
||||
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"
|
||||
onClick={() => modal.closeAll()}
|
||||
>
|
||||
<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="mt-[16px]">
|
||||
<Autocomplete
|
||||
value={customer}
|
||||
onChange={setCustomer}
|
||||
classNames={{
|
||||
label: 'text-white',
|
||||
}}
|
||||
label="Select Customer"
|
||||
placeholder="Start typing..."
|
||||
data={data?.map((p: any) => p.name) || []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="my-[16px] flex gap-[10px]">
|
||||
<Button onClick={() => saveCustomer()}>Save</Button>
|
||||
{!!integration?.customer?.name && <Button className="bg-red-700" onClick={removeFromCustomer}>Remove from customer</Button>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import { forwardRef, useCallback, useRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { MDEditorProps } from '@uiw/react-md-editor/src/Types';
|
||||
import { RefMDEditor } from '@uiw/react-md-editor/src/Editor';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
|
||||
import dayjs from 'dayjs';
|
||||
import { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import clsx from 'clsx';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useCalendar } from '@gitroom/frontend/components/launches/calendar.cont
|
|||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
import { isUSCitizen } from './helpers/isuscitizen.utils';
|
||||
|
||||
export const Filters = () => {
|
||||
const week = useCalendar();
|
||||
|
|
@ -12,30 +13,30 @@ export const Filters = () => {
|
|||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
: week.display === 'week'
|
||||
? dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.startOf('isoWeek')
|
||||
.format('DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.endOf('isoWeek')
|
||||
.format('DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
: dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.startOf('month')
|
||||
.format('DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.endOf('month')
|
||||
.format('DD/MM/YYYY');
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY');
|
||||
|
||||
const setDay = useCallback(() => {
|
||||
week.setFilters({
|
||||
|
|
@ -145,74 +146,78 @@ export const Filters = () => {
|
|||
week.currentDay,
|
||||
]);
|
||||
return (
|
||||
<div className="text-textColor flex gap-[8px] items-center select-none">
|
||||
<div onClick={previous} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.1644 15.5866C13.3405 15.7628 13.4395 16.0016 13.4395 16.2507C13.4395 16.4998 13.3405 16.7387 13.1644 16.9148C12.9883 17.0909 12.7494 17.1898 12.5003 17.1898C12.2513 17.1898 12.0124 17.0909 11.8363 16.9148L5.58629 10.6648C5.49889 10.5777 5.42954 10.4742 5.38222 10.3602C5.3349 10.2463 5.31055 10.1241 5.31055 10.0007C5.31055 9.87732 5.3349 9.75515 5.38222 9.64119C5.42954 9.52724 5.49889 9.42375 5.58629 9.33665L11.8363 3.08665C12.0124 2.91053 12.2513 2.81158 12.5003 2.81158C12.7494 2.81158 12.9883 2.91053 13.1644 3.08665C13.3405 3.26277 13.4395 3.50164 13.4395 3.75071C13.4395 3.99978 13.3405 4.23865 13.1644 4.41477L7.57925 9.99993L13.1644 15.5866Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="w-[80px] text-center">
|
||||
{week.display === 'day'
|
||||
? `${dayjs()
|
||||
.month(week.currentMonth)
|
||||
.week(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('dddd')}`
|
||||
: week.display === 'week'
|
||||
? `Week ${week.currentWeek}`
|
||||
: `${dayjs().month(week.currentMonth).format('MMMM')}`}
|
||||
</div>
|
||||
<div onClick={next} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M14.4137 10.6633L8.16374 16.9133C7.98761 17.0894 7.74874 17.1884 7.49967 17.1884C7.2506 17.1884 7.01173 17.0894 6.83561 16.9133C6.65949 16.7372 6.56055 16.4983 6.56055 16.2492C6.56055 16.0002 6.65949 15.7613 6.83561 15.5852L12.4223 10L6.83717 4.41331C6.74997 4.3261 6.68079 4.22257 6.6336 4.10863C6.5864 3.99469 6.56211 3.87257 6.56211 3.74925C6.56211 3.62592 6.5864 3.5038 6.6336 3.38986C6.68079 3.27592 6.74997 3.17239 6.83717 3.08518C6.92438 2.99798 7.02791 2.9288 7.14185 2.88161C7.25579 2.83441 7.37791 2.81012 7.50124 2.81012C7.62456 2.81012 7.74668 2.83441 7.86062 2.88161C7.97456 2.9288 8.07809 2.99798 8.1653 3.08518L14.4153 9.33518C14.5026 9.42238 14.5718 9.52596 14.619 9.63997C14.6662 9.75398 14.6904 9.87618 14.6903 9.99957C14.6901 10.123 14.6656 10.2451 14.6182 10.359C14.5707 10.4729 14.5012 10.5763 14.4137 10.6633Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">{betweenDates}</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'day' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setDay}
|
||||
>
|
||||
Day
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'week' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setWeek}
|
||||
>
|
||||
Week
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'month' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setMonth}
|
||||
>
|
||||
Month
|
||||
</div>
|
||||
<div className="text-textColor flex flex-col md:flex-row gap-[8px] items-center select-none">
|
||||
<div className = "flex flex-grow flex-row">
|
||||
<div onClick={previous} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.1644 15.5866C13.3405 15.7628 13.4395 16.0016 13.4395 16.2507C13.4395 16.4998 13.3405 16.7387 13.1644 16.9148C12.9883 17.0909 12.7494 17.1898 12.5003 17.1898C12.2513 17.1898 12.0124 17.0909 11.8363 16.9148L5.58629 10.6648C5.49889 10.5777 5.42954 10.4742 5.38222 10.3602C5.3349 10.2463 5.31055 10.1241 5.31055 10.0007C5.31055 9.87732 5.3349 9.75515 5.38222 9.64119C5.42954 9.52724 5.49889 9.42375 5.58629 9.33665L11.8363 3.08665C12.0124 2.91053 12.2513 2.81158 12.5003 2.81158C12.7494 2.81158 12.9883 2.91053 13.1644 3.08665C13.3405 3.26277 13.4395 3.50164 13.4395 3.75071C13.4395 3.99978 13.3405 4.23865 13.1644 4.41477L7.57925 9.99993L13.1644 15.5866Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="w-[80px] text-center">
|
||||
{week.display === 'day'
|
||||
? `${dayjs()
|
||||
.month(week.currentMonth)
|
||||
.week(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('dddd')}`
|
||||
: week.display === 'week'
|
||||
? `Week ${week.currentWeek}`
|
||||
: `${dayjs().month(week.currentMonth).format('MMMM')}`}
|
||||
</div>
|
||||
<div onClick={next} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M14.4137 10.6633L8.16374 16.9133C7.98761 17.0894 7.74874 17.1884 7.49967 17.1884C7.2506 17.1884 7.01173 17.0894 6.83561 16.9133C6.65949 16.7372 6.56055 16.4983 6.56055 16.2492C6.56055 16.0002 6.65949 15.7613 6.83561 15.5852L12.4223 10L6.83717 4.41331C6.74997 4.3261 6.68079 4.22257 6.6336 4.10863C6.5864 3.99469 6.56211 3.87257 6.56211 3.74925C6.56211 3.62592 6.5864 3.5038 6.6336 3.38986C6.68079 3.27592 6.74997 3.17239 6.83717 3.08518C6.92438 2.99798 7.02791 2.9288 7.14185 2.88161C7.25579 2.83441 7.37791 2.81012 7.50124 2.81012C7.62456 2.81012 7.74668 2.83441 7.86062 2.88161C7.97456 2.9288 8.07809 2.99798 8.1653 3.08518L14.4153 9.33518C14.5026 9.42238 14.5718 9.52596 14.619 9.63997C14.6662 9.75398 14.6904 9.87618 14.6903 9.99957C14.6901 10.123 14.6656 10.2451 14.6182 10.359C14.5707 10.4729 14.5012 10.5763 14.4137 10.6633Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">{betweenDates}</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'day' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setDay}
|
||||
>
|
||||
Day
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'week' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setWeek}
|
||||
>
|
||||
Week
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'month' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setMonth}
|
||||
>
|
||||
Month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
|||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import clsx from 'clsx';
|
||||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
import { Chakra_Petch } from 'next/font/google';
|
||||
import { FC } from 'react';
|
||||
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
|
||||
import { textSlicer } from '@gitroom/helpers/utils/count.length';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
|
||||
export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
|
|
@ -14,12 +14,13 @@ export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props)
|
|||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
return text.slice(0, props.maximumCharacters || 10000) + '<mark class="bg-red-500" data-tooltip-id="tooltip" data-tooltip-content="This text will be cropped">' + text?.slice(props.maximumCharacters || 10000) + '</mark>';
|
||||
const {start, end} = textSlicer(integration?.identifier || '', props.maximumCharacters || 10000, text);
|
||||
return text.slice(start, end) + '<mark class="bg-red-500" data-tooltip-id="tooltip" data-tooltip-content="This text will be cropped">' + text?.slice(end) + '</mark>';
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={clsx('w-[555px] px-[16px]')}>
|
||||
<div className={clsx('w-full md:w-[555px] px-[16px]')}>
|
||||
<div className="w-full h-full relative flex flex-col">
|
||||
{newValues.map((value, index) => (
|
||||
<div
|
||||
|
|
@ -62,7 +63,7 @@ export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props)
|
|||
{integration?.display || '@username'}
|
||||
</div>
|
||||
</div>
|
||||
<pre className={clsx('text-wrap', chakra.className)} dangerouslySetInnerHTML={{__html: value.text}} />
|
||||
<pre className={clsx('text-wrap', interClass)} dangerouslySetInnerHTML={{__html: value.text}} />
|
||||
{!!value?.images?.length && (
|
||||
<div className={clsx("w-full rounded-[16px] overflow-hidden mt-[12px]", value?.images?.length > 3 ? 'grid grid-cols-2 gap-[4px]' : 'flex gap-[4px]')}>
|
||||
{value.images.map((image, index) => (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import dayjs from 'dayjs';
|
|||
import { Calendar, TimeInput } from '@mantine/dates';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { isUSCitizen } from './isuscitizen.utils';
|
||||
|
||||
export const DatePicker: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
|
|
@ -39,7 +40,7 @@ export const DatePicker: FC<{
|
|||
onClick={changeShow}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="cursor-pointer">{date.format('DD/MM/YYYY HH:mm')}</div>
|
||||
<div className="cursor-pointer">{date.format(isUSCitizen() ? 'MM/DD/YYYY hh:mm A' : 'DD/MM/YYYY HH:mm')}</div>
|
||||
<div className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
export const isUSCitizen = () => {
|
||||
const userLanguage = navigator.language || navigator.languages[0];
|
||||
return userLanguage.startsWith('en-US')
|
||||
}
|
||||
|
|
@ -13,8 +13,7 @@ import {
|
|||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import dayjs from 'dayjs';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
const postUrlEmitter = new EventEmitter();
|
||||
|
||||
|
|
@ -78,26 +77,32 @@ export const LinkedinCompany: FC<{
|
|||
const { onClose, onSelect, id } = props;
|
||||
const fetch = useFetch();
|
||||
const [company, setCompany] = useState<any>(null);
|
||||
const toast = useToaster();
|
||||
|
||||
const getCompany = async () => {
|
||||
if (!company) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
const {options} = await (
|
||||
await fetch('/integrations/function', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: 'company',
|
||||
data: {
|
||||
url: company,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
onSelect(options.value);
|
||||
onClose();
|
||||
try {
|
||||
const { options } = await (
|
||||
await fetch('/integrations/function', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: 'company',
|
||||
data: {
|
||||
url: company,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
onSelect(options.value);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast.show('Failed to load profile', 'warning');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ export const useFormatting = (
|
|||
if (params.removeMarkdown) {
|
||||
newText = removeMd(newText);
|
||||
}
|
||||
newText = newText.replace(/@\w{1,15}/g, function(match) {
|
||||
return `<strong>${match}</strong>`;
|
||||
});
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ const finalInformation = {} as {
|
|||
settings: () => object;
|
||||
trigger: () => Promise<boolean>;
|
||||
isValid: boolean;
|
||||
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>;
|
||||
checkValidity?: (
|
||||
value: Array<Array<{ path: string }>>,
|
||||
settings: any
|
||||
) => Promise<string | true>;
|
||||
maximumCharacters?: number;
|
||||
};
|
||||
};
|
||||
|
|
@ -18,8 +21,11 @@ export const useValues = (
|
|||
identifier: string,
|
||||
value: Array<{ id?: string; content: string; media?: Array<string> }>,
|
||||
dto: any,
|
||||
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>,
|
||||
maximumCharacters?: number,
|
||||
checkValidity?: (
|
||||
value: Array<Array<{ path: string }>>,
|
||||
settings: any
|
||||
) => Promise<string | true>,
|
||||
maximumCharacters?: number
|
||||
) => {
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(dto);
|
||||
|
|
@ -43,8 +49,7 @@ export const useValues = (
|
|||
finalInformation[integration].trigger = form.trigger;
|
||||
|
||||
if (checkValidity) {
|
||||
finalInformation[integration].checkValidity =
|
||||
checkValidity;
|
||||
finalInformation[integration].checkValidity = checkValidity;
|
||||
}
|
||||
|
||||
if (maximumCharacters) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { AddProviderButton } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { orderBy } from 'lodash';
|
||||
// import { Calendar } from '@gitroom/frontend/components/launches/calendar';
|
||||
import { groupBy, orderBy } from 'lodash';
|
||||
import { CalendarWeekProvider } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { Filters } from '@gitroom/frontend/components/launches/filters';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
@ -19,7 +18,201 @@ import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
|||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
import { Calendar } from './calendar';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
|
||||
|
||||
interface MenuComponentInterface {
|
||||
refreshChannel: (
|
||||
integration: Integration & { identifier: string }
|
||||
) => () => void;
|
||||
continueIntegration: (integration: Integration) => () => void;
|
||||
totalNonDisabledChannels: number;
|
||||
mutate: (shouldReload?: boolean) => void;
|
||||
update: (shouldReload: boolean) => void;
|
||||
}
|
||||
|
||||
export const MenuGroupComponent: FC<
|
||||
MenuComponentInterface & {
|
||||
changeItemGroup: (id: string, group: string) => void;
|
||||
group: {
|
||||
id: string;
|
||||
name: string;
|
||||
values: Array<
|
||||
Integration & {
|
||||
identifier: string;
|
||||
changeProfilePicture: boolean;
|
||||
changeNickName: boolean;
|
||||
}
|
||||
>;
|
||||
};
|
||||
}
|
||||
> = (props) => {
|
||||
const {
|
||||
group,
|
||||
mutate,
|
||||
update,
|
||||
continueIntegration,
|
||||
totalNonDisabledChannels,
|
||||
refreshChannel,
|
||||
changeItemGroup,
|
||||
} = props;
|
||||
|
||||
const [collectedProps, drop] = useDrop(() => ({
|
||||
accept: 'menu',
|
||||
drop: (item: { id: string }, monitor) => {
|
||||
changeItemGroup(item.id, group.id);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: !!monitor.isOver(),
|
||||
}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="gap-[16px] flex flex-col relative"
|
||||
// @ts-ignore
|
||||
ref={drop}
|
||||
>
|
||||
{collectedProps.isOver && (
|
||||
<div className="absolute left-0 top-0 w-full h-full pointer-events-none">
|
||||
<div className="w-full h-full left-0 top-0 relative">
|
||||
<div className="bg-white/30 w-full h-full p-[8px] box-content rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!group.name && <div>{group.name}</div>}
|
||||
{group.values.map((integration) => (
|
||||
<MenuComponent
|
||||
key={integration.id}
|
||||
integration={integration}
|
||||
mutate={mutate}
|
||||
continueIntegration={continueIntegration}
|
||||
update={update}
|
||||
refreshChannel={refreshChannel}
|
||||
totalNonDisabledChannels={totalNonDisabledChannels}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const MenuComponent: FC<
|
||||
MenuComponentInterface & {
|
||||
integration: Integration & {
|
||||
identifier: string;
|
||||
changeProfilePicture: boolean;
|
||||
changeNickName: boolean;
|
||||
};
|
||||
}
|
||||
> = (props) => {
|
||||
const {
|
||||
totalNonDisabledChannels,
|
||||
continueIntegration,
|
||||
refreshChannel,
|
||||
mutate,
|
||||
update,
|
||||
integration,
|
||||
} = props;
|
||||
|
||||
const user = useUser();
|
||||
const [collected, drag, dragPreview] = useDrag(() => ({
|
||||
type: 'menu',
|
||||
item: { id: integration.id },
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
// @ts-ignore
|
||||
ref={dragPreview}
|
||||
{...(integration.refreshNeeded && {
|
||||
onClick: refreshChannel(integration),
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content': 'Channel disconnected, click to reconnect.',
|
||||
})}
|
||||
key={integration.id}
|
||||
className={clsx(
|
||||
'flex gap-[8px] items-center',
|
||||
integration.refreshNeeded && 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps || integration.refreshNeeded) && (
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer"
|
||||
onClick={
|
||||
integration.refreshNeeded
|
||||
? refreshChannel(integration)
|
||||
: continueIntegration(integration)
|
||||
}
|
||||
>
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture!}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{integration.identifier === 'youtube' ? (
|
||||
<img
|
||||
src="/icons/platforms/youtube.svg"
|
||||
className="absolute z-10 -bottom-[5px] -right-[5px]"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
// @ts-ignore
|
||||
ref={drag}
|
||||
{...(integration.disabled &&
|
||||
totalNonDisabledChannels === user?.totalChannels
|
||||
? {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'This channel is disabled, please upgrade your plan to enable it.',
|
||||
}
|
||||
: {})}
|
||||
role="Handle"
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden cursor-move',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
<Menu
|
||||
canChangeProfilePicture={integration.changeProfilePicture}
|
||||
canChangeNickName={integration.changeNickName}
|
||||
mutate={mutate}
|
||||
onChange={update}
|
||||
id={integration.id}
|
||||
canEnable={
|
||||
user?.totalChannels! > totalNonDisabledChannels &&
|
||||
integration.disabled
|
||||
}
|
||||
canDisable={!integration.disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const LaunchesComponent = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
|
|
@ -31,7 +224,6 @@ export const LaunchesComponent = () => {
|
|||
const load = useCallback(async (path: string) => {
|
||||
return (await (await fetch(path)).json()).integrations;
|
||||
}, []);
|
||||
const user = useUser();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
|
|
@ -48,6 +240,28 @@ export const LaunchesComponent = () => {
|
|||
);
|
||||
}, [integrations]);
|
||||
|
||||
const changeItemGroup = useCallback(
|
||||
async (id: string, group: string) => {
|
||||
mutate(
|
||||
integrations.map((integration: any) => {
|
||||
if (integration.id === id) {
|
||||
return { ...integration, customer: { id: group } };
|
||||
}
|
||||
return integration;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
await fetch(`/integrations/${id}/group`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ group }),
|
||||
});
|
||||
|
||||
mutate();
|
||||
},
|
||||
[integrations]
|
||||
);
|
||||
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
integrations,
|
||||
|
|
@ -56,6 +270,25 @@ export const LaunchesComponent = () => {
|
|||
);
|
||||
}, [integrations]);
|
||||
|
||||
const menuIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
Object.values(
|
||||
groupBy(sortedIntegrations, (o) => o?.customer?.id || '')
|
||||
).map((p) => ({
|
||||
name: (p[0].customer?.name || '') as string,
|
||||
id: (p[0].customer?.id || '') as string,
|
||||
isEmpty: p.length === 0,
|
||||
values: orderBy(
|
||||
p,
|
||||
['type', 'disabled', 'identifier'],
|
||||
['desc', 'asc', 'asc']
|
||||
),
|
||||
})),
|
||||
['isEmpty', 'name'],
|
||||
['desc', 'asc']
|
||||
);
|
||||
}, [sortedIntegrations]);
|
||||
|
||||
const update = useCallback(async (shouldReload: boolean) => {
|
||||
if (shouldReload) {
|
||||
setReload(true);
|
||||
|
|
@ -96,11 +329,13 @@ export const LaunchesComponent = () => {
|
|||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (search.get('scope') === 'missing') {
|
||||
toast.show('You have to approve all the channel permissions', 'warning');
|
||||
if (search.get('msg')) {
|
||||
toast.show(search.get('msg')!, 'warning');
|
||||
window?.opener?.postMessage({msg: search.get('msg')!, success: false}, '*');
|
||||
}
|
||||
if (search.get('added')) {
|
||||
fireEvents('channel_added');
|
||||
window?.opener?.postMessage({msg: 'Channel added', success: true}, '*');
|
||||
}
|
||||
if (window.opener) {
|
||||
window.close();
|
||||
|
|
@ -113,114 +348,41 @@ export const LaunchesComponent = () => {
|
|||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<CalendarWeekProvider integrations={sortedIntegrations}>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="outline-none w-full h-full grid grid-cols-[220px_minmax(0,1fr)] gap-[30px] scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
|
||||
<div className="w-[220px] bg-third p-[16px] flex flex-col gap-[24px] min-h-[100%]">
|
||||
<h2 className="text-[20px]">Channels</h2>
|
||||
<div className="gap-[16px] flex flex-col">
|
||||
{sortedIntegrations.length === 0 && (
|
||||
<div className="text-[12px]">No channels</div>
|
||||
)}
|
||||
{sortedIntegrations.map((integration) => (
|
||||
<div
|
||||
{...(integration.refreshNeeded && {
|
||||
onClick: refreshChannel(integration),
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'Channel disconnected, click to reconnect.',
|
||||
})}
|
||||
key={integration.id}
|
||||
className={clsx("flex gap-[8px] items-center", integration.refreshNeeded && 'cursor-pointer')}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps ||
|
||||
integration.refreshNeeded) && (
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer"
|
||||
onClick={
|
||||
integration.refreshNeeded
|
||||
? refreshChannel(integration)
|
||||
: continueIntegration(integration)
|
||||
}
|
||||
>
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full -left-[5px] -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{integration.identifier === 'youtube' ? (
|
||||
<img
|
||||
src="/icons/platforms/youtube.svg"
|
||||
className="absolute z-10 -bottom-[5px] -right-[5px]"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
{...(integration.disabled &&
|
||||
totalNonDisabledChannels === user?.totalChannels
|
||||
? {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'This channel is disabled, please upgrade your plan to enable it.',
|
||||
}
|
||||
: {})}
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
<Menu
|
||||
canChangeProfilePicture={integration.changeProfilePicture}
|
||||
canChangeNickName={integration.changeNickName}
|
||||
<DNDProvider>
|
||||
<CalendarWeekProvider integrations={sortedIntegrations}>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="outline-none w-full h-full grid grid-cols[1fr] md:grid-cols-[220px_minmax(0,1fr)] gap-[30px] scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
|
||||
<div className="bg-third p-[16px] flex flex-col gap-[24px] min-h-[100%]">
|
||||
<h2 className="text-[20px]">Channels</h2>
|
||||
<div className="gap-[16px] flex flex-col select-none">
|
||||
{sortedIntegrations.length === 0 && (
|
||||
<div className="text-[12px]">No channels</div>
|
||||
)}
|
||||
{menuIntegrations.map((menu) => (
|
||||
<MenuGroupComponent
|
||||
changeItemGroup={changeItemGroup}
|
||||
key={menu.name}
|
||||
group={menu}
|
||||
mutate={mutate}
|
||||
onChange={update}
|
||||
id={integration.id}
|
||||
canEnable={
|
||||
user?.totalChannels! > totalNonDisabledChannels &&
|
||||
integration.disabled
|
||||
}
|
||||
canDisable={!integration.disabled}
|
||||
continueIntegration={continueIntegration}
|
||||
update={update}
|
||||
refreshChannel={refreshChannel}
|
||||
totalNonDisabledChannels={totalNonDisabledChannels}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
<AddProviderButton update={() => update(true)} />
|
||||
{/*{sortedIntegrations?.length > 0 && user?.tier?.ai && <GeneratorComponent />}*/}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Filters />
|
||||
<Calendar />
|
||||
</div>
|
||||
<AddProviderButton update={() => update(true)} />
|
||||
{/*{sortedIntegrations?.length > 0 && user?.tier?.ai && <GeneratorComponent />}*/}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Filters />
|
||||
<Calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CalendarWeekProvider>
|
||||
</CalendarWeekProvider>
|
||||
</DNDProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, useCallback, useState } from 'react';
|
||||
import { FC, MouseEventHandler, useCallback, useState } from 'react';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
|
|
@ -8,6 +8,7 @@ import { useModals } from '@mantine/modals';
|
|||
import { TimeTable } from '@gitroom/frontend/components/launches/time.table';
|
||||
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { BotPicture } from '@gitroom/frontend/components/launches/bot.picture';
|
||||
import { CustomerModal } from '@gitroom/frontend/components/launches/customer.modal';
|
||||
|
||||
export const Menu: FC<{
|
||||
canEnable: boolean;
|
||||
|
|
@ -36,9 +37,13 @@ export const Menu: FC<{
|
|||
setShow(false);
|
||||
});
|
||||
|
||||
const changeShow = useCallback(() => {
|
||||
setShow(!show);
|
||||
}, [show]);
|
||||
const changeShow: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
setShow(!show);
|
||||
},
|
||||
[show]
|
||||
);
|
||||
|
||||
const disableChannel = useCallback(async () => {
|
||||
if (
|
||||
|
|
@ -138,6 +143,34 @@ export const Menu: FC<{
|
|||
setShow(false);
|
||||
}, [integrations]);
|
||||
|
||||
const addToCustomer = useCallback(() => {
|
||||
const findIntegration = integrations.find(
|
||||
(integration) => integration.id === id
|
||||
);
|
||||
|
||||
modal.openModal({
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[600px] bg-transparent text-textColor',
|
||||
},
|
||||
size: '100%',
|
||||
withCloseButton: false,
|
||||
closeOnEscape: true,
|
||||
closeOnClickOutside: true,
|
||||
children: (
|
||||
<CustomerModal
|
||||
// @ts-ignore
|
||||
integration={findIntegration}
|
||||
onClose={() => {
|
||||
mutate();
|
||||
toast.show('Customer Updated', 'success');
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
setShow(false);
|
||||
}, [integrations]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer relative select-none"
|
||||
|
|
@ -191,6 +224,23 @@ export const Menu: FC<{
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-[12px] items-center" onClick={addToCustomer}>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={18}
|
||||
height={18}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M31.9997 17C31.9997 17.2652 31.8943 17.5196 31.7068 17.7071C31.5192 17.8946 31.2649 18 30.9997 18H28.9997V20C28.9997 20.2652 28.8943 20.5196 28.7068 20.7071C28.5192 20.8946 28.2649 21 27.9997 21C27.7345 21 27.4801 20.8946 27.2926 20.7071C27.105 20.5196 26.9997 20.2652 26.9997 20V18H24.9997C24.7345 18 24.4801 17.8946 24.2926 17.7071C24.105 17.5196 23.9997 17.2652 23.9997 17C23.9997 16.7348 24.105 16.4804 24.2926 16.2929C24.4801 16.1054 24.7345 16 24.9997 16H26.9997V14C26.9997 13.7348 27.105 13.4804 27.2926 13.2929C27.4801 13.1054 27.7345 13 27.9997 13C28.2649 13 28.5192 13.1054 28.7068 13.2929C28.8943 13.4804 28.9997 13.7348 28.9997 14V16H30.9997C31.2649 16 31.5192 16.1054 31.7068 16.2929C31.8943 16.4804 31.9997 16.7348 31.9997 17ZM24.7659 24.3562C24.9367 24.5595 25.0197 24.8222 24.9967 25.0866C24.9737 25.351 24.8466 25.5955 24.6434 25.7662C24.4402 25.937 24.1775 26.02 23.9131 25.997C23.6486 25.974 23.4042 25.847 23.2334 25.6437C20.7184 22.6487 17.2609 21 13.4997 21C9.73843 21 6.28093 22.6487 3.76593 25.6437C3.59519 25.8468 3.35079 25.9737 3.08648 25.9966C2.82217 26.0194 2.55961 25.9364 2.35655 25.7656C2.15349 25.5949 2.02658 25.3505 2.00372 25.0862C1.98087 24.8219 2.06394 24.5593 2.23468 24.3562C4.10218 22.1337 6.42468 20.555 9.00593 19.71C7.43831 18.7336 6.23133 17.2733 5.56759 15.5498C4.90386 13.8264 4.81949 11.9337 5.32724 10.1581C5.83499 8.38242 6.90724 6.82045 8.38176 5.70847C9.85629 4.59649 11.6529 3.995 13.4997 3.995C15.3465 3.995 17.1431 4.59649 18.6176 5.70847C20.0921 6.82045 21.1644 8.38242 21.6721 10.1581C22.1799 11.9337 22.0955 13.8264 21.4318 15.5498C20.768 17.2733 19.561 18.7336 17.9934 19.71C20.5747 20.555 22.8972 22.1337 24.7659 24.3562ZM13.4997 19C14.7853 19 16.042 18.6188 17.1109 17.9045C18.1798 17.1903 19.0129 16.1752 19.5049 14.9874C19.9969 13.7997 20.1256 12.4928 19.8748 11.2319C19.624 9.97103 19.0049 8.81284 18.0959 7.9038C17.1868 6.99476 16.0286 6.37569 14.7678 6.12489C13.5069 5.87409 12.2 6.00281 11.0122 6.49478C9.82451 6.98675 8.80935 7.81987 8.09512 8.88879C7.38089 9.95771 6.99968 11.2144 6.99968 12.5C7.00166 14.2233 7.68712 15.8754 8.90567 17.094C10.1242 18.3126 11.7764 18.998 13.4997 19Z"
|
||||
fill="green"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px]">Move / add to customer</div>
|
||||
</div>
|
||||
<div className="flex gap-[12px] items-center" onClick={editTimeTable}>
|
||||
<div>
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ export default withProvider(null, undefined, undefined, async (posts) => {
|
|||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, 300);
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ export default withProvider(
|
|||
undefined,
|
||||
DiscordDto,
|
||||
undefined,
|
||||
280
|
||||
1980
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,12 +28,16 @@ import { newImage } from '@gitroom/frontend/components/launches/helpers/new.imag
|
|||
import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
|
||||
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { linkedinCompany } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
|
||||
import {
|
||||
LinkedinCompany,
|
||||
linkedinCompany,
|
||||
} from '@gitroom/frontend/components/launches/helpers/linkedin.component';
|
||||
import { Editor } from '@gitroom/frontend/components/launches/editor';
|
||||
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
|
||||
import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.button';
|
||||
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
|
||||
import { capitalize } from 'lodash';
|
||||
import { useModals } from '@mantine/modals';
|
||||
|
||||
// Simple component to change back to settings on after changing tab
|
||||
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
|
||||
|
|
@ -68,15 +72,16 @@ export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => {
|
|||
return children;
|
||||
};
|
||||
|
||||
export const withProvider = (
|
||||
SettingsComponent: FC<{values?: any}> | null,
|
||||
CustomPreviewComponent?: FC<{maximumCharacters?: number}>,
|
||||
export const withProvider = function <T extends object>(
|
||||
SettingsComponent: FC<{ values?: any }> | null,
|
||||
CustomPreviewComponent?: FC<{ maximumCharacters?: number }>,
|
||||
dto?: any,
|
||||
checkValidity?: (
|
||||
value: Array<Array<{ path: string }>>
|
||||
value: Array<Array<{ path: string }>>,
|
||||
settings: T
|
||||
) => Promise<string | true>,
|
||||
maximumCharacters?: number
|
||||
) => {
|
||||
) {
|
||||
return (props: {
|
||||
identifier: string;
|
||||
id: string;
|
||||
|
|
@ -90,6 +95,8 @@ export const withProvider = (
|
|||
}) => {
|
||||
const existingData = useExistingData();
|
||||
const { integration, date } = useIntegration();
|
||||
const [showLinkedinPopUp, setShowLinkedinPopUp] = useState<any>(false);
|
||||
|
||||
useCopilotReadable({
|
||||
description:
|
||||
integration?.type === 'social'
|
||||
|
|
@ -254,6 +261,21 @@ export const withProvider = (
|
|||
},
|
||||
});
|
||||
|
||||
const tagPersonOrCompany = useCallback(
|
||||
(integration: string, editor: (value: string) => void) => () => {
|
||||
setShowLinkedinPopUp(
|
||||
<LinkedinCompany
|
||||
onSelect={(tag) => {
|
||||
editor(tag);
|
||||
}}
|
||||
id={integration}
|
||||
onClose={() => setShowLinkedinPopUp(false)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// this is a trick to prevent the data from being deleted, yet we don't render the elements
|
||||
if (!props.show) {
|
||||
return null;
|
||||
|
|
@ -262,6 +284,7 @@ export const withProvider = (
|
|||
return (
|
||||
<FormProvider {...form}>
|
||||
<SetTab changeTab={() => setShowTab(0)} />
|
||||
{showLinkedinPopUp ? showLinkedinPopUp : null}
|
||||
<div className="mt-[15px] w-full flex flex-col flex-1">
|
||||
{!props.hideMenu && (
|
||||
<div className="flex gap-[4px]">
|
||||
|
|
@ -318,6 +341,20 @@ export const withProvider = (
|
|||
<div>
|
||||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 text-textColor editor">
|
||||
{integration?.identifier === 'linkedin' && (
|
||||
<Button
|
||||
className="mb-[5px]"
|
||||
onClick={tagPersonOrCompany(
|
||||
integration.id,
|
||||
(newValue: string) =>
|
||||
changeValue(index)(
|
||||
val.content + newValue
|
||||
)
|
||||
)}
|
||||
>
|
||||
Tag a company
|
||||
</Button>
|
||||
)}
|
||||
<Editor
|
||||
order={index}
|
||||
height={InPlaceValue.length > 1 ? 200 : 250}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import { FC } from 'react';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { InstagramCollaboratorsTags } from '@gitroom/frontend/components/launches/providers/instagram/instagram.tags';
|
||||
|
||||
const postType = [
|
||||
{
|
||||
value: 'post',
|
||||
label: 'Post / Reel',
|
||||
},
|
||||
{
|
||||
value: 'story',
|
||||
label: 'Story',
|
||||
},
|
||||
];
|
||||
const InstagramCollaborators: FC<{ values?: any }> = (props) => {
|
||||
const { watch, register, formState, control } = useSettings();
|
||||
const postCurrentType = watch('post_type');
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
label="Post Type"
|
||||
{...register('post_type', {
|
||||
value: 'post',
|
||||
})}
|
||||
>
|
||||
<option value="">Select Post Type...</option>
|
||||
{postType.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{postCurrentType !== 'story' && (
|
||||
<InstagramCollaboratorsTags
|
||||
label="Collaborators (max 3) - accounts can't be private"
|
||||
{...register('collaborators', {
|
||||
value: []
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider<InstagramDto>(
|
||||
InstagramCollaborators,
|
||||
undefined,
|
||||
InstagramDto,
|
||||
async ([firstPost, ...otherPosts], settings) => {
|
||||
if (!firstPost.length) {
|
||||
return 'Instagram should have at least one media';
|
||||
}
|
||||
|
||||
if (firstPost.length > 1 && settings.post_type === 'story') {
|
||||
return 'Instagram stories can only have one media';
|
||||
}
|
||||
|
||||
const checkVideosLength = await Promise.all(
|
||||
firstPost
|
||||
.filter((f) => f.path.indexOf('mp4') > -1)
|
||||
.flatMap((p) => p.path)
|
||||
.map((p) => {
|
||||
return new Promise<number>((res) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.src = p;
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
res(video.duration);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
for (const video of checkVideosLength) {
|
||||
if (video > 60 && settings.post_type === 'story') {
|
||||
return 'Instagram stories should be maximum 60 seconds';
|
||||
}
|
||||
|
||||
if (video > 90 && settings.post_type === 'post') {
|
||||
return 'Instagram reel should be maximum 90 seconds';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
2200
|
||||
);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async ([firstPost, ...otherPosts]) => {
|
||||
if (!firstPost.length) {
|
||||
return 'Instagram should have at least one media';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
2200
|
||||
);
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { ReactTags } from 'react-tag-autocomplete';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
|
||||
export const InstagramCollaboratorsTags: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
onChange: (event: { target: { value: any[]; name: string } }) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const { getValues } = useSettings();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
const [suggestions, setSuggestions] = useState<string>('');
|
||||
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({ target: { value: modify, name } });
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
if (tagValue.length >= 3) {
|
||||
return;
|
||||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({ target: { value: modify, name } });
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const suggestionsArray = useMemo(() => {
|
||||
return [...tagValue, { label: suggestions, value: suggestions }].filter(f => f.label);
|
||||
}, [suggestions, tagValue]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
|
||||
<ReactTags
|
||||
placeholderText="Add a tag"
|
||||
suggestions={suggestionsArray}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onInput={setSuggestions}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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";
|
||||
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider';
|
||||
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.provider';
|
||||
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.collaborators';
|
||||
import YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider';
|
||||
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
|
||||
import PinterestProvider from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.provider';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,52 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
export default withProvider(null, undefined, undefined, async (posts) => {
|
||||
if (posts.some(p => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async (posts) => {
|
||||
if (posts.some((p) => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
|
||||
return true;
|
||||
}, 280);
|
||||
if (
|
||||
posts.some(
|
||||
(p) => p.some((m) => m.path.indexOf('mp4') > -1) && p.length > 1
|
||||
)
|
||||
) {
|
||||
return 'There can be maximum 1 video in a post.';
|
||||
}
|
||||
|
||||
for (const load of posts.flatMap((p) => p.flatMap((a) => a.path))) {
|
||||
if (load.indexOf('mp4') > -1) {
|
||||
const isValid = await checkVideoDuration(load);
|
||||
if (!isValid) {
|
||||
return 'Video duration must be less than or equal to 140 seconds.';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
280
|
||||
);
|
||||
|
||||
const checkVideoDuration = async (url: string): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.src = url;
|
||||
video.preload = 'metadata';
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
// Check if the duration is less than or equal to 140 seconds
|
||||
const duration = video.duration;
|
||||
if (duration <= 140) {
|
||||
resolve(true); // Video duration is acceptable
|
||||
} else {
|
||||
resolve(false); // Video duration exceeds 140 seconds
|
||||
}
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
reject(new Error('Failed to load video metadata.'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const YoutubeSettings: FC = () => {
|
|||
const { register, control } = useSettings();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Input label="Title" {...register('title')} />
|
||||
<Input label="Title" {...register('title')} maxLength={100} />
|
||||
<Select label="Type" {...register('type', { value: 'public' })}>
|
||||
{type.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export const Impersonate = () => {
|
|||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="px-[23px]">
|
||||
<div className="md:px-[23px]">
|
||||
<div className="bg-forth h-[52px] flex justify-center items-center border-input border rounded-[8px]">
|
||||
<div className="relative flex flex-col w-[600px]">
|
||||
<div className="relative z-[999]">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { ReactNode, useCallback } from 'react';
|
||||
import { FetchWrapperComponent } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { isGeneral } from '@gitroom/react/helpers/is.general';
|
||||
import { useReturnUrl } from '@gitroom/frontend/app/auth/return.url.component';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import NotificationComponent from '@gitroom/frontend/components/notifications/no
|
|||
import Link from 'next/link';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
|
|
@ -37,10 +36,12 @@ const ModeComponent = dynamic(
|
|||
{ ssr: false }
|
||||
);
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(isBetween);
|
||||
import { extend } from 'dayjs';
|
||||
|
||||
extend(utc);
|
||||
extend(weekOfYear);
|
||||
extend(isoWeek);
|
||||
extend(isBetween);
|
||||
|
||||
export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
||||
const fetch = useFetch();
|
||||
|
|
@ -77,12 +78,12 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
{user.tier !== 'FREE' && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-textColor flex flex-col">
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary sm:px-6 px-0 text-textColor flex flex-col">
|
||||
{user?.admin && <Impersonate />}
|
||||
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<nav className="px-0 md:px-[23px] gap-2 grid grid-rows-[repeat(2,_auto)] grid-cols-2 md:grid-rows-1 md:grid-cols-[repeat(3,_auto)] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-2xl flex items-center gap-[10px] text-textColor"
|
||||
className="text-2xl flex items-center gap-[10px] text-textColor order-1"
|
||||
>
|
||||
<div className="min-w-[55px]">
|
||||
<Image
|
||||
|
|
@ -93,9 +94,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
!isGeneral ? 'mt-[12px]' : 'min-w-[80px]'
|
||||
)}
|
||||
className={clsx(!isGeneral ? 'mt-[12px]' : 'min-w-[80px]')}
|
||||
>
|
||||
{isGeneral ? (
|
||||
<svg
|
||||
|
|
@ -127,21 +126,22 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{user?.orgId && (user.tier !== 'FREE' || !isGeneral || !billingEnabled) ? (
|
||||
{user?.orgId &&
|
||||
(user.tier !== 'FREE' || !isGeneral || !billingEnabled) ? (
|
||||
<TopMenu />
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<div id = "systray-buttons" className="flex items-center justify-self-end gap-[8px] order-2 md:order-3">
|
||||
<ModeComponent />
|
||||
<SettingsComponent />
|
||||
<NotificationComponent />
|
||||
<OrganizationSelector />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1 rounded-3xl px-[23px] py-[17px] flex flex-col">
|
||||
{(user.tier === 'FREE' && isGeneral) && billingEnabled ? (
|
||||
<div className="flex-1 rounded-3xl px-0 md:px-[23px] py-[17px] flex flex-col">
|
||||
{user.tier === 'FREE' && isGeneral && billingEnabled ? (
|
||||
<>
|
||||
<div className="text-center mb-[20px] text-xl">
|
||||
<h1 className="text-3xl">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
|||
import { LogoutComponent } from '@gitroom/frontend/components/layout/logout.component';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { PublicComponent } from '@gitroom/frontend/components/public-api/public.component';
|
||||
|
||||
export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
|
||||
const {isGeneral} = useVariables();
|
||||
|
|
@ -38,7 +39,7 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
|
|||
}, []);
|
||||
|
||||
const url = useSearchParams();
|
||||
const showLogout = !url.get('onboarding');
|
||||
const showLogout = !url.get('onboarding') || user?.tier?.current === "FREE";
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
const personal = await (await fetch('/user/personal')).json();
|
||||
|
|
@ -195,6 +196,7 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
|
|||
</div>
|
||||
)}
|
||||
{!!user?.tier?.team_members && isGeneral && <TeamsComponent />}
|
||||
{!!user?.tier?.public_api && isGeneral && <PublicComponent />}
|
||||
{showLogout && <LogoutComponent />}
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
'use client';
|
||||
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import Script from 'next/script';
|
||||
|
||||
export const useTolt = () => {
|
||||
return () => {
|
||||
// @ts-ignore
|
||||
return window?.tolt_referral || '';
|
||||
};
|
||||
};
|
||||
|
||||
export const ToltScript = () => {
|
||||
const { tolt } = useVariables();
|
||||
if (!tolt) return null;
|
||||
return (
|
||||
<Script
|
||||
async={true}
|
||||
src="https://cdn.tolt.io/tolt.js"
|
||||
data-tolt={tolt}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,16 +8,16 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
|||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
export const useMenuItems = () => {
|
||||
const {isGeneral} = useVariables();
|
||||
const { isGeneral } = useVariables();
|
||||
return [
|
||||
...(!isGeneral
|
||||
? [
|
||||
{
|
||||
name: 'Analytics',
|
||||
icon: 'analytics',
|
||||
path: '/analytics',
|
||||
},
|
||||
]
|
||||
{
|
||||
name: 'Analytics',
|
||||
icon: 'analytics',
|
||||
path: '/analytics',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: isGeneral ? 'Calendar' : 'Launches',
|
||||
|
|
@ -26,32 +26,27 @@ export const useMenuItems = () => {
|
|||
},
|
||||
...(isGeneral
|
||||
? [
|
||||
{
|
||||
name: 'Analytics',
|
||||
icon: 'analytics',
|
||||
path: '/analytics',
|
||||
},
|
||||
]
|
||||
{
|
||||
name: 'Analytics',
|
||||
icon: 'analytics',
|
||||
path: '/analytics',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isGeneral
|
||||
? [
|
||||
{
|
||||
name: 'Settings',
|
||||
icon: 'settings',
|
||||
path: '/settings',
|
||||
role: ['ADMIN', 'SUPERADMIN'],
|
||||
},
|
||||
]
|
||||
{
|
||||
name: 'Settings',
|
||||
icon: 'settings',
|
||||
path: '/settings',
|
||||
role: ['ADMIN', 'SUPERADMIN'],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Marketplace',
|
||||
icon: 'marketplace',
|
||||
path: '/marketplace',
|
||||
},
|
||||
{
|
||||
name: 'Messages',
|
||||
icon: 'messages',
|
||||
path: '/messages',
|
||||
name: 'Plugs',
|
||||
icon: 'plugs',
|
||||
path: '/plugs',
|
||||
},
|
||||
{
|
||||
name: 'Billing',
|
||||
|
|
@ -60,18 +55,25 @@ export const useMenuItems = () => {
|
|||
role: ['ADMIN', 'SUPERADMIN'],
|
||||
requireBilling: true,
|
||||
},
|
||||
{
|
||||
name: 'Affiliate',
|
||||
icon: 'affiliate',
|
||||
path: 'https://affiliate.postiz.com',
|
||||
role: ['ADMIN', 'SUPERADMIN', 'USER'],
|
||||
requireBilling: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const TopMenu: FC = () => {
|
||||
const path = usePathname();
|
||||
const user = useUser();
|
||||
const {billingEnabled} = useVariables();
|
||||
const { billingEnabled } = useVariables();
|
||||
const menuItems = useMenuItems();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full animate-normalFadeDown">
|
||||
<ul className="gap-5 flex flex-1 items-center text-[18px]">
|
||||
<div className="flex flex-col h-full animate-normalFadeDown order-3 md:order-2 col-span-2 md:col-span-1">
|
||||
<ul className="gap-0 md:gap-5 flex flex-1 items-center text-[18px]">
|
||||
{menuItems
|
||||
.filter((f) => {
|
||||
if (f.requireBilling && !billingEnabled) {
|
||||
|
|
@ -86,9 +88,10 @@ export const TopMenu: FC = () => {
|
|||
<li key={item.name}>
|
||||
<Link
|
||||
prefetch={true}
|
||||
target={item.path.indexOf('http') > -1 ? '_blank' : '_self'}
|
||||
href={item.path}
|
||||
className={clsx(
|
||||
'flex gap-2 items-center box',
|
||||
'flex gap-2 items-center box px-[6px] md:px-[24px] py-[8px]',
|
||||
menuItems
|
||||
.filter((f) => {
|
||||
if (f.role) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const UserContext = createContext<
|
|||
| (User & {
|
||||
orgId: string;
|
||||
tier: PricingInnerInterface;
|
||||
publicApi: string;
|
||||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
totalChannels: number;
|
||||
isLifetime?: boolean;
|
||||
|
|
@ -24,6 +25,7 @@ export const ContextWrapper: FC<{
|
|||
orgId: string;
|
||||
tier: 'FREE' | 'STANDARD' | 'PRO' | 'ULTIMATE' | 'TEAM';
|
||||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
publicApi: string;
|
||||
totalChannels: number;
|
||||
};
|
||||
children: ReactNode;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'reflect-metadata';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { Post as PrismaPost } from '.prisma/client';
|
||||
import { Providers } from '@gitroom/frontend/components/launches/providers/show.all.providers';
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const NotificationOpenComponent = () => {
|
|||
const { data, isLoading } = useSWR('notifications', loadNotifications);
|
||||
|
||||
return (
|
||||
<div className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] min-h-[200px] top-[100%] right-0 bg-third text-textColor rounded-[16px] flex flex-col border border-tableBorder">
|
||||
<div id="notification-popup" className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] min-h-[200px] top-[100%] right-0 bg-third text-textColor rounded-[16px] flex flex-col border border-tableBorder z-[2]">
|
||||
<div className={`p-[16px] border-b border-tableBorder ${interClass} font-bold`}>
|
||||
Notifications
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { orderBy } from 'lodash';
|
||||
|
|
@ -15,9 +8,17 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
|||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { Menu } from '@gitroom/frontend/components/launches/menu/menu';
|
||||
import { ApiModal } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import {
|
||||
AddProviderComponent,
|
||||
ApiModal,
|
||||
CustomVariables,
|
||||
UrlModal,
|
||||
} from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { string } from 'yup';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useModals } from '@mantine/modals';
|
||||
|
||||
export const ConnectChannels: FC = () => {
|
||||
const fetch = useFetch();
|
||||
|
|
@ -25,6 +26,9 @@ export const ConnectChannels: FC = () => {
|
|||
const router = useRouter();
|
||||
const [identifier, setIdentifier] = useState<any>(undefined);
|
||||
const [popup, setPopups] = useState<undefined | string[]>(undefined);
|
||||
const toaster = useToaster();
|
||||
const modal = useModals();
|
||||
const [showCustom, setShowCustom] = useState<any>(undefined);
|
||||
|
||||
const getIntegrations = useCallback(async () => {
|
||||
return (await fetch('/integrations')).json();
|
||||
|
|
@ -32,17 +36,101 @@ export const ConnectChannels: FC = () => {
|
|||
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const getSocialLink = useCallback(
|
||||
(identifier: string) => async () => {
|
||||
const { url } = await (
|
||||
await fetch('/integrations/social/' + identifier)
|
||||
).json();
|
||||
// const getSocialLink = useCallback(
|
||||
// (identifier: string) => async () => {
|
||||
// const { url } = await (
|
||||
// await fetch('/integrations/social/' + identifier)
|
||||
// ).json();
|
||||
//
|
||||
// window.open(url, 'Social Connect', 'width=700,height=700');
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
window.open(url, 'Social Connect', 'width=700,height=700');
|
||||
const addMessage = useCallback(
|
||||
(event: MessageEvent<{ msg: string; success: boolean }>) => {
|
||||
if (!event.data.msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
toaster.show(event.data.msg, event.data.success ? 'success' : 'warning');
|
||||
setShowCustom(undefined);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', addMessage);
|
||||
return () => {
|
||||
window.removeEventListener('message', addMessage);
|
||||
};
|
||||
});
|
||||
|
||||
const getSocialLink = useCallback(
|
||||
(
|
||||
identifier: string,
|
||||
isExternal: boolean,
|
||||
customFields?: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
validation: string;
|
||||
defaultValue?: string;
|
||||
type: 'text' | 'password';
|
||||
}>
|
||||
) =>
|
||||
async () => {
|
||||
const gotoIntegration = async (externalUrl?: string) => {
|
||||
const { url, err } = await (
|
||||
await fetch(
|
||||
`/integrations/social/${identifier}${
|
||||
externalUrl ? `?externalUrl=${externalUrl}` : ``
|
||||
}`
|
||||
)
|
||||
).json();
|
||||
|
||||
if (err) {
|
||||
toaster.show('Could not connect to the platform', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
setShowCustom(undefined);
|
||||
window.open(url, 'Social Connect', 'width=700,height=700');
|
||||
};
|
||||
|
||||
// if (isExternal) {
|
||||
// modal.closeAll();
|
||||
//
|
||||
// modal.openModal({
|
||||
// title: '',
|
||||
// withCloseButton: false,
|
||||
// classNames: {
|
||||
// modal: 'bg-transparent text-textColor',
|
||||
// },
|
||||
// children: <UrlModal gotoUrl={gotoIntegration} />,
|
||||
// });
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (customFields) {
|
||||
setShowCustom(
|
||||
<CustomVariables
|
||||
identifier={identifier}
|
||||
gotoUrl={(url: string) =>
|
||||
window.open(url, 'Social Connect', 'width=700,height=700')
|
||||
}
|
||||
variables={customFields}
|
||||
close={() => setShowCustom(undefined)}
|
||||
/>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await gotoIntegration();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const load = useCallback(async (path: string) => {
|
||||
const list = (await (await fetch(path)).json()).integrations;
|
||||
setPopups(list.map((p: any) => p.id));
|
||||
|
|
@ -115,6 +203,13 @@ export const ConnectChannels: FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{!!showCustom && (
|
||||
<div className="absolute w-full h-full top-0 left-0 bg-black/40 z-[400]">
|
||||
<div className="absolute w-full h-full bg-primary/80 left-0 top-0 z-[200] p-[50px] flex justify-center">
|
||||
<div className="w-[400px]">{showCustom}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!identifier && (
|
||||
<div className="absolute w-full h-full bg-primary/80 left-0 top-0 z-[200] p-[30px] flex items-center justify-center">
|
||||
<div className="w-[400px]">
|
||||
|
|
@ -142,7 +237,11 @@ export const ConnectChannels: FC = () => {
|
|||
{data?.social.map((social: any) => (
|
||||
<div
|
||||
key={social.identifier}
|
||||
onClick={getSocialLink(social.identifier)}
|
||||
onClick={getSocialLink(
|
||||
social.identifier,
|
||||
social.isExternal,
|
||||
social.customFields
|
||||
)}
|
||||
className="h-[96px] bg-input flex flex-col justify-center items-center gap-[10px] cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { Integration } from '@prisma/client';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,316 @@
|
|||
'use client';
|
||||
import {
|
||||
PlugSettings,
|
||||
PlugsInterface,
|
||||
usePlugs,
|
||||
} from '@gitroom/frontend/components/plugs/plugs.context';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import {
|
||||
FormProvider,
|
||||
SubmitHandler,
|
||||
useForm,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import clsx from 'clsx';
|
||||
import { string, object } from 'yup';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { Slider } from '@gitroom/react/form/slider';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export function convertBackRegex(s: string) {
|
||||
const matches = s.match(/\/(.*)\/([a-z]*)/);
|
||||
const pattern = matches?.[1] || '';
|
||||
const flags = matches?.[2] || '';
|
||||
|
||||
return new RegExp(pattern, flags);
|
||||
}
|
||||
|
||||
export const TextArea: FC<{ name: string; placeHolder: string }> = (props) => {
|
||||
const form = useFormContext();
|
||||
const { onChange, onBlur, ...all } = form.register(props.name);
|
||||
const value = form.watch(props.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<textarea className="hidden" {...all}></textarea>
|
||||
<CopilotTextarea
|
||||
disableBranding={true}
|
||||
placeholder={props.placeHolder}
|
||||
value={value}
|
||||
className={clsx(
|
||||
'!min-h-40 !max-h-80 p-[24px] overflow-hidden bg-customColor2 outline-none rounded-[4px] border-fifth border'
|
||||
)}
|
||||
onChange={(e) => {
|
||||
onChange({ target: { name: props.name, value: e.target.value } });
|
||||
}}
|
||||
autosuggestionsConfig={{
|
||||
textareaPurpose: `Assist me in writing social media posts.`,
|
||||
chatApiConfigs: {},
|
||||
}}
|
||||
/>
|
||||
<div className="text-red-400 text-[12px]">
|
||||
{form?.formState?.errors?.[props.name]?.message as string}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlugPop: FC<{
|
||||
plug: PlugsInterface;
|
||||
settings: PlugSettings;
|
||||
data?: {
|
||||
activated: boolean;
|
||||
data: string;
|
||||
id: string;
|
||||
integrationId: string;
|
||||
organizationId: string;
|
||||
plugFunction: string;
|
||||
};
|
||||
}> = (props) => {
|
||||
const { plug, settings, data } = props;
|
||||
const { closeAll } = useModals();
|
||||
const fetch = useFetch();
|
||||
const toaster = useToaster();
|
||||
|
||||
const values = useMemo(() => {
|
||||
if (!data?.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(data.data).reduce((acc: any, current: any) => {
|
||||
return {
|
||||
...acc,
|
||||
[current.name]: current.value,
|
||||
};
|
||||
}, {} as any);
|
||||
}, []);
|
||||
|
||||
const yupSchema = useMemo(() => {
|
||||
return object(
|
||||
plug.fields.reduce((acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
[field.name]: field.validation
|
||||
? string().matches(convertBackRegex(field.validation), {
|
||||
message: 'Invalid value',
|
||||
})
|
||||
: null,
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const form = useForm({
|
||||
resolver: yupResolver(yupSchema),
|
||||
values,
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const submit: SubmitHandler<any> = useCallback(async (data) => {
|
||||
await fetch(`/integrations/${settings.providerId}/plugs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
func: plug.methodName,
|
||||
fields: Object.keys(data).map((key) => ({
|
||||
name: key,
|
||||
value: data[key],
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
toaster.show('Plug updated', 'success');
|
||||
closeAll();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(submit)}>
|
||||
<div className="fixed left-0 top-0 bg-primary/80 z-[300] w-full min-h-full p-4 md:p-[60px] animate-fade">
|
||||
<div className="max-w-[1000px] w-full h-full bg-sixth border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative mx-auto">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex-1">
|
||||
<TopTitle title={`Auto Plug: ${plug.title}`} />
|
||||
</div>
|
||||
<button
|
||||
onClick={closeAll}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary 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>
|
||||
<div className="my-[20px]">{plug.description}</div>
|
||||
<div>
|
||||
{plug.fields.map((field) => (
|
||||
<div key={field.name}>
|
||||
{field.type === 'richtext' ? (
|
||||
<TextArea
|
||||
name={field.name}
|
||||
placeHolder={field.placeholder}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
name={field.name}
|
||||
label={field.description}
|
||||
className="w-full mt-[8px] p-[8px] border border-tableBorder rounded-md text-black"
|
||||
placeholder={field.placeholder}
|
||||
type={field.type}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-[20px]">
|
||||
<Button type="submit">Activate</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlugItem: FC<{
|
||||
plug: PlugsInterface;
|
||||
addPlug: (data: any) => void;
|
||||
data?: {
|
||||
activated: boolean;
|
||||
data: string;
|
||||
id: string;
|
||||
integrationId: string;
|
||||
organizationId: string;
|
||||
plugFunction: string;
|
||||
};
|
||||
}> = (props) => {
|
||||
const { plug, addPlug, data } = props;
|
||||
const [activated, setActivated] = useState(!!data?.activated);
|
||||
useEffect(() => {
|
||||
setActivated(!!data?.activated);
|
||||
}, [data?.activated]);
|
||||
const fetch = useFetch();
|
||||
|
||||
const changeActivated = useCallback(
|
||||
async (status: 'on' | 'off') => {
|
||||
await fetch(`/integrations/plugs/${data?.id}/activate`, {
|
||||
body: JSON.stringify({
|
||||
status: status === 'on',
|
||||
}),
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
setActivated(status === 'on');
|
||||
},
|
||||
[activated]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => addPlug(data)}
|
||||
key={plug.title}
|
||||
className="w-full h-[300px] bg-customColor48 hover:bg-customColor2 hover:border-customColor48 hover:border"
|
||||
>
|
||||
<div key={plug.title} className="p-[16px] h-full flex flex-col flex-1">
|
||||
<div className="flex">
|
||||
<div className="text-[20px] mb-[8px] flex-1">{plug.title}</div>
|
||||
{!!data && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Slider
|
||||
value={activated ? 'on' : 'off'}
|
||||
onChange={changeActivated}
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">{plug.description}</div>
|
||||
<Button>{!data ? 'Set Plug' : 'Edit Plug'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Plug = () => {
|
||||
const plug = usePlugs();
|
||||
const modals = useModals();
|
||||
const fetch = useFetch();
|
||||
const load = useCallback(async () => {
|
||||
return (await fetch(`/integrations/${plug.providerId}/plugs`)).json();
|
||||
}, [plug.providerId]);
|
||||
|
||||
const { data, isLoading, mutate } = useSWR(`plugs-${plug.providerId}`, load);
|
||||
|
||||
const addEditPlug = useCallback(
|
||||
(p: PlugsInterface) =>
|
||||
(data?: {
|
||||
activated: boolean;
|
||||
data: string;
|
||||
id: string;
|
||||
integrationId: string;
|
||||
organizationId: string;
|
||||
plugFunction: string;
|
||||
}) => {
|
||||
modals.openModal({
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
withCloseButton: false,
|
||||
onClose() {
|
||||
mutate();
|
||||
},
|
||||
size: '100%',
|
||||
children: (
|
||||
<PlugPop
|
||||
plug={p}
|
||||
data={data}
|
||||
settings={{
|
||||
identifier: plug.identifier,
|
||||
providerId: plug.providerId,
|
||||
name: plug.name,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-[30px]">
|
||||
{plug.plugs.map((p) => (
|
||||
<PlugItem
|
||||
key={p.title + '-' + plug.providerId}
|
||||
addPlug={addEditPlug(p)}
|
||||
plug={p}
|
||||
data={data?.find((a: any) => a.plugFunction === p.methodName)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface PlugSettings {
|
||||
providerId: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
export interface PlugInterface extends PlugSettings {
|
||||
plugs: PlugsInterface[];
|
||||
}
|
||||
|
||||
export interface FieldsInterface {
|
||||
name: string;
|
||||
type: string;
|
||||
validation: string;
|
||||
placeholder: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface PlugsInterface {
|
||||
title: string;
|
||||
description: string;
|
||||
runEveryMilliseconds: number;
|
||||
methodName: string;
|
||||
fields: FieldsInterface[];
|
||||
}
|
||||
|
||||
export const PlugsContext = createContext<PlugInterface>({
|
||||
providerId: '',
|
||||
name: '',
|
||||
identifier: '',
|
||||
plugs: [
|
||||
{
|
||||
title: '',
|
||||
description: '',
|
||||
runEveryMilliseconds: 0,
|
||||
methodName: '',
|
||||
fields: [{ name: '', type: '', placeholder: '', description: '', validation: '' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const usePlugs = () => useContext(PlugsContext);
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { capitalize, orderBy } from 'lodash';
|
||||
import clsx from 'clsx';
|
||||
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
|
||||
import Image from 'next/image';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { PlugsContext } from '@gitroom/frontend/components/plugs/plugs.context';
|
||||
import { Plug } from '@gitroom/frontend/components/plugs/plug';
|
||||
|
||||
export const Plugs = () => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const toaster = useToaster();
|
||||
const load = useCallback(async () => {
|
||||
return (await (await fetch('/integrations/list')).json()).integrations;
|
||||
}, []);
|
||||
|
||||
const load2 = useCallback(async (path: string) => {
|
||||
return await (await fetch(path)).json();
|
||||
}, []);
|
||||
|
||||
const { data: plugList, isLoading: plugLoading } = useSWR(
|
||||
'/integrations/plug/list',
|
||||
load2,
|
||||
{
|
||||
fallbackData: [],
|
||||
}
|
||||
);
|
||||
|
||||
const { data, isLoading } = useSWR('analytics-list', load, {
|
||||
fallbackData: [],
|
||||
});
|
||||
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(
|
||||
data.filter((integration: any) =>
|
||||
plugList?.plugs?.some(
|
||||
(f: any) => f.identifier === integration.identifier
|
||||
)
|
||||
),
|
||||
// data.filter((integration) => !integration.disabled),
|
||||
['type', 'disabled', 'identifier'],
|
||||
['desc', 'asc', 'asc']
|
||||
);
|
||||
}, [data, plugList]);
|
||||
|
||||
const currentIntegration = useMemo(() => {
|
||||
return sortedIntegrations[current];
|
||||
}, [current, sortedIntegrations]);
|
||||
|
||||
const currentIntegrationPlug = useMemo(() => {
|
||||
const plug = plugList?.plugs?.find(
|
||||
(f: any) => f?.identifier === currentIntegration?.identifier
|
||||
);
|
||||
|
||||
if (!plug) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
providerId: currentIntegration.id,
|
||||
...plug,
|
||||
};
|
||||
}, [currentIntegration, plugList]);
|
||||
|
||||
if (isLoading || plugLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!sortedIntegrations.length && !isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center mt-[100px] gap-[27px] text-center">
|
||||
<div>
|
||||
<img src="/peoplemarketplace.svg" />
|
||||
</div>
|
||||
<div className="text-[48px]">
|
||||
There are not plugs matching your channels
|
||||
<br />
|
||||
You have to add: X or LinkedIn or Threads
|
||||
</div>
|
||||
<Button onClick={() => router.push('/launches')}>
|
||||
Go to the calendar to add channels
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-[30px] flex-1">
|
||||
<div className="p-[16px] bg-customColor48 overflow-hidden flex w-[220px]">
|
||||
<div className="flex gap-[16px] flex-col overflow-hidden">
|
||||
<div className="text-[20px] mb-[8px]">Channels</div>
|
||||
{sortedIntegrations.map((integration, index) => (
|
||||
<div
|
||||
key={integration.id}
|
||||
onClick={() => {
|
||||
if (integration.refreshNeeded) {
|
||||
toaster.show(
|
||||
'Please refresh the integration from the calendar',
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
setRefresh(true);
|
||||
setTimeout(() => {
|
||||
setRefresh(false);
|
||||
}, 10);
|
||||
setCurrent(index);
|
||||
}}
|
||||
className={clsx(
|
||||
'flex gap-[8px] items-center',
|
||||
currentIntegration.id !== integration.id &&
|
||||
'opacity-20 hover:opacity-100 cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{(integration.inBetweenSteps || integration.refreshNeeded) && (
|
||||
<div className="absolute left-0 top-0 w-[39px] h-[46px] cursor-pointer">
|
||||
<div className="bg-red-500 w-[15px] h-[15px] rounded-full left-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center">
|
||||
!
|
||||
</div>
|
||||
<div className="bg-primary/60 w-[39px] h-[46px] left-0 top-0 absolute rounded-full z-[199]" />
|
||||
</div>
|
||||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 whitespace-nowrap text-ellipsis overflow-hidden',
|
||||
integration.disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{integration.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<PlugsContext.Provider value={currentIntegrationPlug}>
|
||||
<Plug />
|
||||
</PlugsContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useUser } from '../layout/user.context';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export const PublicComponent = () => {
|
||||
const user = useUser();
|
||||
const toaster = useToaster();
|
||||
const [reveal, setReveal] = useState(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
toaster.show('API Key copied to clipboard', 'success');
|
||||
copy(user?.publicApi!);
|
||||
}, [user]);
|
||||
|
||||
if (!user || !user.publicApi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-[24px]">Public API</h2>
|
||||
<div className="text-customColor18 mt-[4px]">
|
||||
Use Postiz API to integrate with your tools.
|
||||
</div>
|
||||
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
|
||||
<div className="flex items-center">
|
||||
{reveal ? (
|
||||
user.publicApi
|
||||
) : (
|
||||
<>
|
||||
<div className="blur-sm">{user.publicApi.slice(0, -5)}</div>
|
||||
<div>{user.publicApi.slice(-5)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!reveal ? (
|
||||
<Button onClick={() => setReveal(true)}>Reveal</Button>
|
||||
) : (
|
||||
<Button onClick={copyToClipboard}>Copy Key</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -210,6 +210,7 @@ export const TeamsComponent = () => {
|
|||
<Button
|
||||
className={`!bg-customColor3 !h-[24px] border border-customColor21 rounded-[4px] text-[12px] ${interClass}`}
|
||||
onClick={remove(p)}
|
||||
secondary={true}
|
||||
>
|
||||
<div className="flex justify-center items-center gap-[4px]">
|
||||
<div>
|
||||
|
|
@ -222,7 +223,7 @@ export const TeamsComponent = () => {
|
|||
>
|
||||
<path
|
||||
d="M11.8125 3.125H9.625V2.6875C9.625 2.3394 9.48672 2.00556 9.24058 1.75942C8.99444 1.51328 8.6606 1.375 8.3125 1.375H5.6875C5.3394 1.375 5.00556 1.51328 4.75942 1.75942C4.51328 2.00556 4.375 2.3394 4.375 2.6875V3.125H2.1875C2.07147 3.125 1.96019 3.17109 1.87814 3.25314C1.79609 3.33519 1.75 3.44647 1.75 3.5625C1.75 3.67853 1.79609 3.78981 1.87814 3.87186C1.96019 3.95391 2.07147 4 2.1875 4H2.625V11.875C2.625 12.1071 2.71719 12.3296 2.88128 12.4937C3.04538 12.6578 3.26794 12.75 3.5 12.75H10.5C10.7321 12.75 10.9546 12.6578 11.1187 12.4937C11.2828 12.3296 11.375 12.1071 11.375 11.875V4H11.8125C11.9285 4 12.0398 3.95391 12.1219 3.87186C12.2039 3.78981 12.25 3.67853 12.25 3.5625C12.25 3.44647 12.2039 3.33519 12.1219 3.25314C12.0398 3.17109 11.9285 3.125 11.8125 3.125ZM5.25 2.6875C5.25 2.57147 5.29609 2.46019 5.37814 2.37814C5.46019 2.29609 5.57147 2.25 5.6875 2.25H8.3125C8.42853 2.25 8.53981 2.29609 8.62186 2.37814C8.70391 2.46019 8.75 2.57147 8.75 2.6875V3.125H5.25V2.6875ZM10.5 11.875H3.5V4H10.5V11.875ZM6.125 6.1875V9.6875C6.125 9.80353 6.07891 9.91481 5.99686 9.99686C5.91481 10.0789 5.80353 10.125 5.6875 10.125C5.57147 10.125 5.46019 10.0789 5.37814 9.99686C5.29609 9.91481 5.25 9.80353 5.25 9.6875V6.1875C5.25 6.07147 5.29609 5.96019 5.37814 5.87814C5.46019 5.79609 5.57147 5.75 5.6875 5.75C5.80353 5.75 5.91481 5.79609 5.99686 5.87814C6.07891 5.96019 6.125 6.07147 6.125 6.1875ZM8.75 6.1875V9.6875C8.75 9.80353 8.70391 9.91481 8.62186 9.99686C8.53981 10.0789 8.42853 10.125 8.3125 10.125C8.19647 10.125 8.08519 10.0789 8.00314 9.99686C7.92109 9.91481 7.875 9.80353 7.875 9.6875V6.1875C7.875 6.07147 7.92109 5.96019 8.00314 5.87814C8.08519 5.79609 8.19647 5.75 8.3125 5.75C8.42853 5.75 8.53981 5.79609 8.62186 5.87814C8.70391 5.96019 8.75 6.07147 8.75 6.1875Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { fetchBackend } from '@gitroom/helpers/utils/custom.fetch.func';
|
||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
|
||||
|
||||
// This function can be marked `async` if using `await` inside
|
||||
export async function middleware(request: NextRequest) {
|
||||
|
|
@ -68,13 +68,10 @@ export async function middleware(request: NextRequest) {
|
|||
try {
|
||||
if (org) {
|
||||
const { id } = await (
|
||||
await fetchBackend('/user/join-org', {
|
||||
await internalFetch('/user/join-org', {
|
||||
body: JSON.stringify({
|
||||
org,
|
||||
}),
|
||||
headers: {
|
||||
auth: authCookie?.value!,
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
).json();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { StarsController } from './stars.controller';
|
||||
import {DatabaseModule} from "@gitroom/nestjs-libraries/database/prisma/database.module";
|
||||
import {TrendingService} from "@gitroom/nestjs-libraries/services/trending.service";
|
||||
import {PostsController} from "@gitroom/workers/app/posts.controller";
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { TrendingService } from '@gitroom/nestjs-libraries/services/trending.service';
|
||||
import { PostsController } from '@gitroom/workers/app/posts.controller';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { PlugsController } from '@gitroom/workers/app/plugs.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, BullMqModule],
|
||||
controllers: [...!process.env.IS_GENERAL ? [StarsController] : [], PostsController],
|
||||
controllers: [
|
||||
...(!process.env.IS_GENERAL ? [StarsController] : []),
|
||||
PostsController,
|
||||
PlugsController,
|
||||
],
|
||||
providers: [TrendingService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { EventPattern, Transport } from '@nestjs/microservices';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
|
||||
@Controller()
|
||||
export class PlugsController {
|
||||
constructor(
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
|
||||
@EventPattern('plugs', Transport.REDIS)
|
||||
async plug(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
return this._integrationService.processPlugs(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ export class PostsController {
|
|||
}
|
||||
|
||||
@EventPattern('submit', Transport.REDIS)
|
||||
async payout(data: { id: string, releaseURL: string }) {
|
||||
async payout(data: { id: string; releaseURL: string }) {
|
||||
return this._postsService.payout(data.id, data.releaseURL);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,7 @@ export class ConfigurationChecker {
|
|||
this.checkIsValidUrl('FRONTEND_URL')
|
||||
this.checkIsValidUrl('NEXT_PUBLIC_BACKEND_URL')
|
||||
this.checkIsValidUrl('BACKEND_INTERNAL_URL')
|
||||
this.checkNonEmpty('CLOUDFLARE_ACCOUNT_ID', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_ACCESS_KEY', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_SECRET_ACCESS_KEY', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_BUCKETNAME', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_BUCKET_URL', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('CLOUDFLARE_REGION', 'Needed to setup providers.')
|
||||
this.checkNonEmpty('STORAGE_PROVIDER', 'Needed to setup storage.')
|
||||
}
|
||||
|
||||
checkNonEmpty (key: string, description?: string): boolean {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
export function Plug(params: {
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string;
|
||||
runEveryMilliseconds: number;
|
||||
totalRuns: number;
|
||||
fields: {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
validation?: RegExp;
|
||||
}[];
|
||||
}) {
|
||||
return function (target: Object, propertyKey: string | symbol, descriptor: any) {
|
||||
// Retrieve existing metadata or initialize an empty array
|
||||
const existingMetadata = Reflect.getMetadata('custom:plug', target) || [];
|
||||
|
||||
// Add the metadata information for this method
|
||||
existingMetadata.push({ methodName: propertyKey, ...params });
|
||||
|
||||
// Define metadata on the class prototype (so it can be retrieved from the class)
|
||||
Reflect.defineMetadata('custom:plug', existingMetadata, target);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// @ts-ignore
|
||||
import twitter from 'twitter-text';
|
||||
|
||||
export const textSlicer = (
|
||||
integrationType: string,
|
||||
end: number,
|
||||
text: string
|
||||
): {start: number, end: number} => {
|
||||
if (integrationType !== 'x') {
|
||||
return {
|
||||
start: 0,
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
const {validRangeEnd, valid} = twitter.parseTweet(text);
|
||||
return {
|
||||
start: 0,
|
||||
end: valid ? end : validRangeEnd
|
||||
}
|
||||
};
|
||||
|
||||
export const weightedLength = (text: string): number => {
|
||||
return twitter.parseTweet(text).weightedLength;
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { loadVars } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
export interface Params {
|
||||
baseUrl: string;
|
||||
|
|
@ -48,6 +47,6 @@ export const customFetch = (
|
|||
|
||||
export const fetchBackend = customFetch({
|
||||
get baseUrl() {
|
||||
return loadVars().backendUrl;
|
||||
return process.env.BACKEND_URL!;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,28 @@
|
|||
import {usePlausible} from 'next-plausible'
|
||||
import {useCallback} from "react";
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useCallback } from 'react';
|
||||
import { usePostHog } from 'posthog-js/react';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
|
||||
export const useFireEvents = () => {
|
||||
const plausible = usePlausible();
|
||||
return useCallback((name: string, props?: any) => {
|
||||
plausible(name, {props});
|
||||
}, []);
|
||||
}
|
||||
const { billingEnabled } = useVariables();
|
||||
const plausible = usePlausible();
|
||||
const posthog = usePostHog();
|
||||
const user = useUser();
|
||||
|
||||
return useCallback(
|
||||
(name: string, props?: any) => {
|
||||
if (!billingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
posthog.identify(user.id, { email: user.email, name: user.name });
|
||||
}
|
||||
|
||||
posthog.capture(name, props);
|
||||
plausible(name, { props });
|
||||
},
|
||||
[user]
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,47 +1,35 @@
|
|||
import {FC, useCallback, useEffect} from "react";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
|
||||
const UtmSaver: FC = () => {
|
||||
const query = useSearchParams();
|
||||
useEffect(() => {
|
||||
const landingUrl = localStorage.getItem('landingUrl');
|
||||
if (landingUrl) {
|
||||
return ;
|
||||
}
|
||||
const query = useSearchParams();
|
||||
const [value, setValue] = useLocalStorage({ key: 'utm', defaultValue: '' });
|
||||
|
||||
localStorage.setItem('landingUrl', window.location.href);
|
||||
localStorage.setItem('referrer', document.referrer);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const landingUrl = localStorage.getItem('landingUrl');
|
||||
if (landingUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const utm = query.get('utm_source') || query.get('utm');
|
||||
const utmMedium = query.get('utm_medium');
|
||||
const utmCampaign = query.get('utm_campaign');
|
||||
localStorage.setItem('landingUrl', window.location.href);
|
||||
localStorage.setItem('referrer', document.referrer);
|
||||
}, []);
|
||||
|
||||
if (utm) {
|
||||
localStorage.setItem('utm', utm);
|
||||
}
|
||||
if (utmMedium) {
|
||||
localStorage.setItem('utm_medium', utmMedium);
|
||||
}
|
||||
if (utmCampaign) {
|
||||
localStorage.setItem('utm_campaign', utmCampaign);
|
||||
}
|
||||
}, [query]);
|
||||
useEffect(() => {
|
||||
const utm = query.get('utm_source') || query.get('utm') || query.get('ref');
|
||||
if (utm && !value) {
|
||||
setValue(utm);
|
||||
}
|
||||
}, [query, value]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const useUtmSaver = () => {
|
||||
return useCallback(() => {
|
||||
return {
|
||||
utm: localStorage.getItem('utm'),
|
||||
utmMedium: localStorage.getItem('utm_medium'),
|
||||
utmCampaign: localStorage.getItem('utm_campaign'),
|
||||
landingUrl: localStorage.getItem('landingUrl'),
|
||||
referrer: localStorage.getItem('referrer'),
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default UtmSaver;
|
||||
export const useUtmUrl = () => {
|
||||
const [value] = useLocalStorage({ key: 'utm', defaultValue: '' });
|
||||
return value || '';
|
||||
};
|
||||
export default UtmSaver;
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export class BullMqClient extends ClientProxy {
|
|||
const job = await queue.add(packet.pattern, packet.data, {
|
||||
jobId: packet.data.id ?? v4(),
|
||||
...packet.data.options,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnComplete: !packet.data.options.attempts,
|
||||
removeOnFail: !packet.data.options.attempts,
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import { Injectable } from '@nestjs/common';
|
|||
import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.repository';
|
||||
import { User } from '@prisma/client';
|
||||
import { CreateAgencyDto } from '@gitroom/nestjs-libraries/dtos/agencies/create.agency.dto';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgenciesService {
|
||||
constructor(
|
||||
private _agenciesRepository: AgenciesRepository,
|
||||
private _emailService: EmailService
|
||||
private _notificationService: NotificationService
|
||||
) {}
|
||||
getAgencyByUser(user: User) {
|
||||
return this._agenciesRepository.getAgencyByUser(user);
|
||||
|
|
@ -35,7 +35,7 @@ export class AgenciesService {
|
|||
const agency = await this._agenciesRepository.getAgencyById(id);
|
||||
|
||||
if (action === 'approve') {
|
||||
await this._emailService.sendEmail(
|
||||
await this._notificationService.sendEmail(
|
||||
agency?.user?.email!,
|
||||
'Your Agency has been approved and added to Postiz 🚀',
|
||||
`
|
||||
|
|
@ -59,7 +59,7 @@ export class AgenciesService {
|
|||
return;
|
||||
}
|
||||
|
||||
await this._emailService.sendEmail(
|
||||
await this._notificationService.sendEmail(
|
||||
agency?.user?.email!,
|
||||
'Your Agency has been declined 😔',
|
||||
`
|
||||
|
|
@ -84,7 +84,7 @@ export class AgenciesService {
|
|||
|
||||
async createAgency(user: User, body: CreateAgencyDto) {
|
||||
const agency = await this._agenciesRepository.createAgency(user, body);
|
||||
await this._emailService.sendEmail(
|
||||
await this._notificationService.sendEmail(
|
||||
'nevo@postiz.com',
|
||||
'New agency created',
|
||||
`
|
||||
|
|
|
|||
|
|
@ -5,13 +5,17 @@ import { Integration } from '@prisma/client';
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationRepository {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _integration: PrismaRepository<'integration'>,
|
||||
private _posts: PrismaRepository<'post'>
|
||||
private _posts: PrismaRepository<'post'>,
|
||||
private _plugs: PrismaRepository<'plugs'>,
|
||||
private _exisingPlugData: PrismaRepository<'exisingPlugData'>,
|
||||
private _customers: PrismaRepository<'customer'>
|
||||
) {}
|
||||
|
||||
async setTimes(org: string, id: string, times: IntegrationTimeDto) {
|
||||
|
|
@ -29,6 +33,35 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getPlug(plugId: string) {
|
||||
return this._plugs.model.plugs.findFirst({
|
||||
where: {
|
||||
id: plugId,
|
||||
},
|
||||
include: {
|
||||
integration: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getPlugs(orgId: string, integrationId: string) {
|
||||
return this._plugs.model.plugs.findMany({
|
||||
where: {
|
||||
integrationId,
|
||||
organizationId: orgId,
|
||||
activated: true,
|
||||
},
|
||||
include: {
|
||||
integration: {
|
||||
select: {
|
||||
id: true,
|
||||
providerIdentifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateIntegration(id: string, params: Partial<Integration>) {
|
||||
if (
|
||||
params.picture &&
|
||||
|
|
@ -63,7 +96,7 @@ export class IntegrationRepository {
|
|||
createOrUpdateIntegration(
|
||||
org: string,
|
||||
name: string,
|
||||
picture: string,
|
||||
picture: string | undefined,
|
||||
type: 'article' | 'social',
|
||||
internalId: string,
|
||||
provider: string,
|
||||
|
|
@ -98,7 +131,7 @@ export class IntegrationRepository {
|
|||
providerIdentifier: provider,
|
||||
token,
|
||||
profile: username,
|
||||
picture,
|
||||
...(picture ? { picture } : {}),
|
||||
inBetweenSteps: isBetweenSteps,
|
||||
refreshToken,
|
||||
...(expiresIn
|
||||
|
|
@ -117,7 +150,7 @@ export class IntegrationRepository {
|
|||
inBetweenSteps: isBetweenSteps,
|
||||
}
|
||||
: {}),
|
||||
picture,
|
||||
...(picture ? { picture } : {}),
|
||||
profile: username,
|
||||
providerIdentifier: provider,
|
||||
token,
|
||||
|
|
@ -215,12 +248,79 @@ export class IntegrationRepository {
|
|||
return integration?.integration;
|
||||
}
|
||||
|
||||
async updateOnCustomerName(org: string, id: string, name: string) {
|
||||
const customer = !name
|
||||
? undefined
|
||||
: (await this._customers.model.customer.findFirst({
|
||||
where: {
|
||||
orgId: org,
|
||||
name,
|
||||
},
|
||||
})) ||
|
||||
(await this._customers.model.customer.create({
|
||||
data: {
|
||||
name,
|
||||
orgId: org,
|
||||
},
|
||||
}));
|
||||
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
id,
|
||||
organizationId: org,
|
||||
},
|
||||
data: {
|
||||
customer: !customer
|
||||
? { disconnect: true }
|
||||
: {
|
||||
connect: {
|
||||
id: customer.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateIntegrationGroup(org: string, id: string, group: string) {
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
id,
|
||||
organizationId: org,
|
||||
},
|
||||
data: !group
|
||||
? {
|
||||
customer: {
|
||||
disconnect: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
customer: {
|
||||
connect: {
|
||||
id: group,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
customers(orgId: string) {
|
||||
return this._customers.model.customer.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getIntegrationsList(org: string) {
|
||||
return this._integration.model.integration.findMany({
|
||||
where: {
|
||||
organizationId: org,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -310,4 +410,80 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
getPlugsByIntegrationId(org: string, id: string) {
|
||||
return this._plugs.model.plugs.findMany({
|
||||
where: {
|
||||
organizationId: org,
|
||||
integrationId: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createOrUpdatePlug(org: string, integrationId: string, body: PlugDto) {
|
||||
return this._plugs.model.plugs.upsert({
|
||||
where: {
|
||||
organizationId: org,
|
||||
plugFunction_integrationId: {
|
||||
integrationId,
|
||||
plugFunction: body.func,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
integrationId,
|
||||
organizationId: org,
|
||||
plugFunction: body.func,
|
||||
data: JSON.stringify(body.fields),
|
||||
activated: true,
|
||||
},
|
||||
update: {
|
||||
data: JSON.stringify(body.fields),
|
||||
},
|
||||
select: {
|
||||
activated: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
changePlugActivation(orgId: string, plugId: string, status: boolean) {
|
||||
return this._plugs.model.plugs.update({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
id: plugId,
|
||||
},
|
||||
data: {
|
||||
activated: !!status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async loadExisingData(
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
id: string[]
|
||||
) {
|
||||
return this._exisingPlugData.model.exisingPlugData.findMany({
|
||||
where: {
|
||||
integrationId,
|
||||
methodName,
|
||||
value: {
|
||||
in: id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async saveExisingData(
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
value: string[]
|
||||
) {
|
||||
return this._exisingPlugData.model.exisingPlugData.createMany({
|
||||
data: value.map((p) => ({
|
||||
integrationId,
|
||||
methodName,
|
||||
value: p,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,24 @@
|
|||
import {
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
Param,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
|
||||
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
|
||||
import { AnalyticsData, SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import {
|
||||
AnalyticsData,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { Integration, Organization } from '@prisma/client';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider';
|
||||
import { simpleUpload } from '@gitroom/nestjs-libraries/upload/r2.uploader';
|
||||
import axios from 'axios';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import dayjs from 'dayjs';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationService {
|
||||
|
|
@ -29,17 +26,22 @@ export class IntegrationService {
|
|||
constructor(
|
||||
private _integrationRepository: IntegrationRepository,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _notificationService: NotificationService
|
||||
private _notificationService: NotificationService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
|
||||
async setTimes(orgId: string, integrationId: string, times: IntegrationTimeDto) {
|
||||
async setTimes(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
times: IntegrationTimeDto
|
||||
) {
|
||||
return this._integrationRepository.setTimes(orgId, integrationId, times);
|
||||
}
|
||||
|
||||
async createOrUpdateIntegration(
|
||||
org: string,
|
||||
name: string,
|
||||
picture: string,
|
||||
picture: string | undefined,
|
||||
type: 'article' | 'social',
|
||||
internalId: string,
|
||||
provider: string,
|
||||
|
|
@ -52,7 +54,9 @@ export class IntegrationService {
|
|||
timezone?: number,
|
||||
customInstanceDetails?: string
|
||||
) {
|
||||
const uploadedPicture = await this.storage.uploadSimple(picture);
|
||||
const uploadedPicture = picture
|
||||
? await this.storage.uploadSimple(picture)
|
||||
: undefined;
|
||||
return this._integrationRepository.createOrUpdateIntegration(
|
||||
org,
|
||||
name,
|
||||
|
|
@ -71,6 +75,14 @@ export class IntegrationService {
|
|||
);
|
||||
}
|
||||
|
||||
updateIntegrationGroup(org: string, id: string, group: string) {
|
||||
return this._integrationRepository.updateIntegrationGroup(org, id, group);
|
||||
}
|
||||
|
||||
updateOnCustomerName(org: string, id: string, name: string) {
|
||||
return this._integrationRepository.updateOnCustomerName(org, id, name);
|
||||
}
|
||||
|
||||
getIntegrationsList(org: string) {
|
||||
return this._integrationRepository.getIntegrationsList(org);
|
||||
}
|
||||
|
|
@ -151,7 +163,7 @@ export class IntegrationService {
|
|||
await this.createOrUpdateIntegration(
|
||||
integration.organizationId,
|
||||
integration.name,
|
||||
integration.picture!,
|
||||
undefined,
|
||||
'social',
|
||||
integration.internalId,
|
||||
integration.providerIdentifier,
|
||||
|
|
@ -292,7 +304,12 @@ export class IntegrationService {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
async checkAnalytics(org: Organization, integration: string, date: string, forceRefresh = false): Promise<AnalyticsData[]> {
|
||||
async checkAnalytics(
|
||||
org: Organization,
|
||||
integration: string,
|
||||
date: string,
|
||||
forceRefresh = false
|
||||
): Promise<AnalyticsData[]> {
|
||||
const getIntegration = await this.getIntegrationById(org.id, integration);
|
||||
|
||||
if (!getIntegration) {
|
||||
|
|
@ -307,7 +324,10 @@ export class IntegrationService {
|
|||
getIntegration.providerIdentifier
|
||||
);
|
||||
|
||||
if (dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) {
|
||||
if (
|
||||
dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
|
||||
forceRefresh
|
||||
) {
|
||||
const { accessToken, expiresIn, refreshToken } =
|
||||
await integrationProvider.refreshToken(getIntegration.refreshToken!);
|
||||
|
||||
|
|
@ -367,4 +387,117 @@ export class IntegrationService {
|
|||
|
||||
return [];
|
||||
}
|
||||
|
||||
customers(orgId: string) {
|
||||
return this._integrationRepository.customers(orgId);
|
||||
}
|
||||
|
||||
getPlugsByIntegrationId(org: string, integrationId: string) {
|
||||
return this._integrationRepository.getPlugsByIntegrationId(
|
||||
org,
|
||||
integrationId
|
||||
);
|
||||
}
|
||||
|
||||
async processPlugs(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
const getPlugById = await this._integrationRepository.getPlug(data.plugId);
|
||||
if (!getPlugById) {
|
||||
return ;
|
||||
}
|
||||
|
||||
const integration = this._integrationManager.getSocialIntegration(
|
||||
getPlugById.integration.providerIdentifier
|
||||
);
|
||||
|
||||
const findPlug = this._integrationManager
|
||||
.getAllPlugs()
|
||||
.find(
|
||||
(p) => p.identifier === getPlugById.integration.providerIdentifier
|
||||
)!;
|
||||
|
||||
console.log(data.postId);
|
||||
|
||||
// @ts-ignore
|
||||
const process = await integration[getPlugById.plugFunction](
|
||||
getPlugById.integration,
|
||||
data.postId,
|
||||
JSON.parse(getPlugById.data).reduce((all: any, current: any) => {
|
||||
all[current.name] = current.value;
|
||||
return all;
|
||||
}, {})
|
||||
);
|
||||
|
||||
if (process) {
|
||||
return ;
|
||||
}
|
||||
|
||||
if (data.totalRuns === data.currentRun) {
|
||||
return ;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + data.postId + '_' + findPlug.identifier,
|
||||
options: {
|
||||
delay: 0, // runPlug.runEveryMilliseconds,
|
||||
},
|
||||
payload: {
|
||||
plugId: data.plugId,
|
||||
postId: data.postId,
|
||||
delay: data.delay,
|
||||
totalRuns: data.totalRuns,
|
||||
currentRun: data.currentRun + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdatePlug(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
body: PlugDto
|
||||
) {
|
||||
const { activated } = await this._integrationRepository.createOrUpdatePlug(
|
||||
orgId,
|
||||
integrationId,
|
||||
body
|
||||
);
|
||||
|
||||
return {
|
||||
activated,
|
||||
};
|
||||
}
|
||||
|
||||
async changePlugActivation(orgId: string, plugId: string, status: boolean) {
|
||||
const { id, integrationId, plugFunction } =
|
||||
await this._integrationRepository.changePlugActivation(
|
||||
orgId,
|
||||
plugId,
|
||||
status
|
||||
);
|
||||
|
||||
return { id };
|
||||
}
|
||||
|
||||
async getPlugs(orgId: string, integrationId: string) {
|
||||
return this._integrationRepository.getPlugs(orgId, integrationId);
|
||||
}
|
||||
|
||||
async loadExisingData(
|
||||
methodName: string,
|
||||
integrationId: string,
|
||||
id: string[]
|
||||
) {
|
||||
const exisingData = await this._integrationRepository.loadExisingData(
|
||||
methodName,
|
||||
integrationId,
|
||||
id
|
||||
);
|
||||
const loadOnlyIds = exisingData.map((p) => p.value);
|
||||
return difference(id, loadOnlyIds);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,23 @@ export class OrganizationRepository {
|
|||
private _user: PrismaRepository<'user'>
|
||||
) {}
|
||||
|
||||
getOrgByApiKey(api: string) {
|
||||
return this._organization.model.organization.findFirst({
|
||||
where: {
|
||||
apiKey: api,
|
||||
},
|
||||
include: {
|
||||
subscription: {
|
||||
select: {
|
||||
subscriptionTier: true,
|
||||
totalChannels: true,
|
||||
isLifetime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getUserOrg(id: string) {
|
||||
return this._userOrg.model.userOrganization.findFirst({
|
||||
where: {
|
||||
|
|
@ -161,9 +178,9 @@ export class OrganizationRepository {
|
|||
});
|
||||
|
||||
if (
|
||||
!process.env.STRIPE_PUBLISHABLE_KEY ||
|
||||
checkForSubscription?.subscription?.subscriptionTier !==
|
||||
SubscriptionTier.PRO
|
||||
process.env.STRIPE_PUBLISHABLE_KEY &&
|
||||
checkForSubscription?.subscription?.subscriptionTier ===
|
||||
SubscriptionTier.STANDARD
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ export class OrganizationService {
|
|||
async createOrgAndUser(
|
||||
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string }
|
||||
) {
|
||||
return this._organizationRepository.createOrgAndUser(body, this._notificationsService.hasEmailProvider());
|
||||
return this._organizationRepository.createOrgAndUser(
|
||||
body,
|
||||
this._notificationsService.hasEmailProvider()
|
||||
);
|
||||
}
|
||||
|
||||
addUserToOrg(
|
||||
|
|
@ -33,6 +36,10 @@ export class OrganizationService {
|
|||
return this._organizationRepository.getOrgById(id);
|
||||
}
|
||||
|
||||
getOrgByApiKey(api: string) {
|
||||
return this._organizationRepository.getOrgByApiKey(api);
|
||||
}
|
||||
|
||||
getUserOrg(id: string) {
|
||||
return this._organizationRepository.getUserOrg(id);
|
||||
}
|
||||
|
|
@ -50,7 +57,7 @@ export class OrganizationService {
|
|||
}
|
||||
|
||||
async inviteTeamMember(orgId: string, body: AddTeamMemberDto) {
|
||||
const timeLimit = dayjs().add(15, 'minutes').format('YYYY-MM-DD HH:mm:ss');
|
||||
const timeLimit = dayjs().add(1, 'hour').format('YYYY-MM-DD HH:mm:ss');
|
||||
const id = makeId(5);
|
||||
const url =
|
||||
process.env.FRONTEND_URL +
|
||||
|
|
@ -59,7 +66,7 @@ export class OrganizationService {
|
|||
await this._notificationsService.sendEmail(
|
||||
body.email,
|
||||
'You have been invited to join an organization',
|
||||
`You have been invited to join an organization. Click <a href="${url}">here</a> to join.<br />The link will expire in 15 minutes.`
|
||||
`You have been invited to join an organization. Click <a href="${url}">here</a> to join.<br />The link will expire in 1 hour.`
|
||||
);
|
||||
}
|
||||
return { url };
|
||||
|
|
@ -86,6 +93,9 @@ export class OrganizationService {
|
|||
}
|
||||
|
||||
disableOrEnableNonSuperAdminUsers(orgId: string, disable: boolean) {
|
||||
return this._organizationRepository.disableOrEnableNonSuperAdminUsers(orgId, disable);
|
||||
return this._organizationRepository.disableOrEnableNonSuperAdminUsers(
|
||||
orgId,
|
||||
disable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,6 +299,13 @@ export class PostsService {
|
|||
true
|
||||
);
|
||||
|
||||
await this.checkPlugs(
|
||||
integration.organizationId,
|
||||
getIntegration.identifier,
|
||||
integration.id,
|
||||
publishedPosts[0].postId
|
||||
);
|
||||
|
||||
return {
|
||||
postId: publishedPosts[0].postId,
|
||||
releaseURL: publishedPosts[0].releaseURL,
|
||||
|
|
@ -312,6 +319,42 @@ export class PostsService {
|
|||
}
|
||||
}
|
||||
|
||||
private async checkPlugs(
|
||||
orgId: string,
|
||||
providerName: string,
|
||||
integrationId: string,
|
||||
postId: string
|
||||
) {
|
||||
const loadAllPlugs = this._integrationManager.getAllPlugs();
|
||||
const getPlugs = await this._integrationService.getPlugs(
|
||||
orgId,
|
||||
integrationId
|
||||
);
|
||||
|
||||
const currentPlug = loadAllPlugs.find((p) => p.identifier === providerName);
|
||||
|
||||
for (const plug of getPlugs) {
|
||||
const runPlug = currentPlug?.plugs?.find((p: any) => p.methodName === plug.plugFunction)!;
|
||||
if (!runPlug) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + postId + '_' + runPlug.identifier,
|
||||
options: {
|
||||
delay: runPlug.runEveryMilliseconds,
|
||||
},
|
||||
payload: {
|
||||
plugId: plug.id,
|
||||
postId,
|
||||
delay: runPlug.runEveryMilliseconds,
|
||||
totalRuns: runPlug.totalRuns,
|
||||
currentRun: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async postArticle(integration: Integration, posts: Post[]) {
|
||||
const getIntegration = this._integrationManager.getArticlesIntegration(
|
||||
integration.providerIdentifier
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ model Organization {
|
|||
buyerOrganization MessagesGroup[]
|
||||
usedCodes UsedCodes[]
|
||||
credits Credits[]
|
||||
plugs Plugs[]
|
||||
customers Customer[]
|
||||
}
|
||||
|
||||
model User {
|
||||
|
|
@ -242,6 +244,19 @@ model Subscription {
|
|||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
orgId String
|
||||
organization Organization @relation(fields: [orgId], references: [id])
|
||||
integrations Integration[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@unique([orgId, name, deletedAt])
|
||||
}
|
||||
|
||||
model Integration {
|
||||
id String @id @default(cuid())
|
||||
internalId String
|
||||
|
|
@ -265,6 +280,10 @@ model Integration {
|
|||
refreshNeeded Boolean @default(false)
|
||||
postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]")
|
||||
customInstanceDetails String?
|
||||
customerId String?
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
plugs Plugs[]
|
||||
exisingPlugData ExisingPlugData[]
|
||||
|
||||
@@index([updatedAt])
|
||||
@@index([deletedAt])
|
||||
|
|
@ -440,6 +459,30 @@ model Messages {
|
|||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model Plugs {
|
||||
id String @id @default(uuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
plugFunction String
|
||||
data String
|
||||
integrationId String
|
||||
integration Integration @relation(fields: [integrationId], references: [id])
|
||||
activated Boolean @default(true)
|
||||
|
||||
@@unique([plugFunction, integrationId])
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model ExisingPlugData {
|
||||
id String @id @default(uuid())
|
||||
integrationId String
|
||||
integration Integration @relation(fields: [integrationId], references: [id])
|
||||
methodName String
|
||||
value String
|
||||
|
||||
@@unique([integrationId, methodName, value])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface PricingInnerInterface {
|
|||
import_from_channels: boolean;
|
||||
image_generator?: boolean;
|
||||
image_generation_count: number;
|
||||
public_api: boolean;
|
||||
}
|
||||
export interface PricingInterface {
|
||||
[key: string]: PricingInnerInterface;
|
||||
|
|
@ -29,6 +30,7 @@ export const pricing: PricingInterface = {
|
|||
ai: false,
|
||||
import_from_channels: false,
|
||||
image_generator: false,
|
||||
public_api: false,
|
||||
},
|
||||
STANDARD: {
|
||||
current: 'STANDARD',
|
||||
|
|
@ -43,6 +45,7 @@ export const pricing: PricingInterface = {
|
|||
featured_by_gitroom: false,
|
||||
import_from_channels: true,
|
||||
image_generator: false,
|
||||
public_api: true,
|
||||
},
|
||||
TEAM: {
|
||||
current: 'TEAM',
|
||||
|
|
@ -57,6 +60,7 @@ export const pricing: PricingInterface = {
|
|||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
public_api: true,
|
||||
},
|
||||
PRO: {
|
||||
current: 'PRO',
|
||||
|
|
@ -71,6 +75,7 @@ export const pricing: PricingInterface = {
|
|||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
public_api: true,
|
||||
},
|
||||
ULTIMATE: {
|
||||
current: 'ULTIMATE',
|
||||
|
|
@ -85,5 +90,6 @@ export const pricing: PricingInterface = {
|
|||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
public_api: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ export class BillingSubscribeDto {
|
|||
|
||||
@IsIn(['STANDARD', 'PRO', 'TEAM', 'ULTIMATE'])
|
||||
billing: 'STANDARD' | 'PRO' | 'TEAM' | 'ULTIMATE';
|
||||
|
||||
utm: string;
|
||||
|
||||
tolt: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { IsDefined, IsString, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class FieldsDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class PlugDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
func: string;
|
||||
|
||||
@Type(() => FieldsDto)
|
||||
@ValidateNested({ each: true })
|
||||
@IsDefined()
|
||||
fields: FieldsDto[];
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsDefined, IsIn, IsString, ValidateNested, IsOptional } from 'class-validator';
|
||||
|
||||
export class Collaborators {
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
label: string;
|
||||
}
|
||||
export class InstagramDto {
|
||||
@IsIn(['post', 'story'])
|
||||
@IsDefined()
|
||||
post_type: 'post' | 'story';
|
||||
|
||||
@Type(() => Collaborators)
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
collaborators: Collaborators[];
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { XProvider } from '@gitroom/nestjs-libraries/integrations/social/x.provider';
|
||||
import { SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
|
|
@ -64,6 +66,27 @@ export class IntegrationManager {
|
|||
})),
|
||||
};
|
||||
}
|
||||
|
||||
getAllPlugs() {
|
||||
return socialIntegrationList
|
||||
.map((p) => {
|
||||
return {
|
||||
name: p.name,
|
||||
identifier: p.identifier,
|
||||
plugs: (
|
||||
Reflect.getMetadata('custom:plug', p.constructor.prototype) || []
|
||||
).map((p: any) => ({
|
||||
...p,
|
||||
fields: p.fields.map((c: any) => ({
|
||||
...c,
|
||||
validation: c?.validation?.toString(),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
})
|
||||
.filter((f) => f.plugs.length);
|
||||
}
|
||||
|
||||
getAllowedSocialsIntegrations() {
|
||||
return socialIntegrationList.map((p) => p.identifier);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ export class NotEnoughScopesFilter implements ExceptionFilter {
|
|||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
response.status(HttpStatusCode.NotAcceptable).json({ invalid: true });
|
||||
response.status(HttpStatusCode.NotAcceptable).json({ msg: exception.message });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class RefreshToken {
|
||||
constructor(
|
||||
public identifier: string,
|
||||
|
|
@ -13,10 +15,16 @@ export class BadBody {
|
|||
) {}
|
||||
}
|
||||
|
||||
export class NotEnoughScopes {}
|
||||
export class NotEnoughScopes {
|
||||
constructor(public message = 'Not enough scopes') {}
|
||||
}
|
||||
|
||||
export abstract class SocialAbstract {
|
||||
async fetch(url: string, options: RequestInit = {}, identifier = '') {
|
||||
async fetch(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
identifier = ''
|
||||
): Promise<Response> {
|
||||
const request = await fetch(url, options);
|
||||
|
||||
if (request.status === 200 || request.status === 201) {
|
||||
|
|
@ -31,7 +39,15 @@ export abstract class SocialAbstract {
|
|||
json = '{}';
|
||||
}
|
||||
|
||||
if (request.status === 401 || json.includes('OAuthException')) {
|
||||
if (json.includes('rate_limit_exceeded') || json.includes('Rate limit')) {
|
||||
await timer(2000);
|
||||
return this.fetch(url, options, identifier);
|
||||
}
|
||||
|
||||
if (
|
||||
request.status === 401 ||
|
||||
(json.includes('OAuthException') && !json.includes("Unsupported format") && !json.includes('2207018'))
|
||||
) {
|
||||
throw new RefreshToken(identifier, json, options.body!);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import {
|
|||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { BskyAgent } from '@atproto/api';
|
||||
import { NotEnoughScopes, SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { BskyAgent, RichText } from '@atproto/api';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import sharp from 'sharp';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'bluesky';
|
||||
|
|
@ -70,30 +72,34 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
}) {
|
||||
const body = JSON.parse(Buffer.from(params.code, 'base64').toString());
|
||||
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
try {
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { accessJwt, refreshJwt, handle, did },
|
||||
} = await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
const {
|
||||
data: { accessJwt, refreshJwt, handle, did },
|
||||
} = await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const profile = await agent.getProfile({
|
||||
actor: did,
|
||||
});
|
||||
const profile = await agent.getProfile({
|
||||
actor: did,
|
||||
});
|
||||
|
||||
return {
|
||||
refreshToken: refreshJwt,
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
accessToken: accessJwt,
|
||||
id: did,
|
||||
name: profile.data.displayName!,
|
||||
picture: profile.data.avatar!,
|
||||
username: profile.data.handle!,
|
||||
};
|
||||
return {
|
||||
refreshToken: refreshJwt,
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
accessToken: accessJwt,
|
||||
id: did,
|
||||
name: profile.data.displayName!,
|
||||
picture: profile.data.avatar!,
|
||||
username: profile.data.handle!,
|
||||
};
|
||||
} catch (e) {
|
||||
return 'Invalid credentials';
|
||||
}
|
||||
}
|
||||
|
||||
async post(
|
||||
|
|
@ -116,7 +122,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
let loadCid = '';
|
||||
let loadUri = '';
|
||||
const cidUrl = [] as { cid: string; url: string, rev: string }[];
|
||||
const cidUrl = [] as { cid: string; url: string; rev: string }[];
|
||||
for (const post of postDetails) {
|
||||
const images = await Promise.all(
|
||||
post.media?.map(async (p) => {
|
||||
|
|
@ -132,9 +138,16 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
}) || []
|
||||
);
|
||||
|
||||
const rt = new RichText({
|
||||
text: post.message,
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent);
|
||||
|
||||
// @ts-ignore
|
||||
const { cid, uri, commit } = await agent.post({
|
||||
text: post.message,
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
...(images.length
|
||||
? {
|
||||
|
|
@ -172,9 +185,143 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
return postDetails.map((p, index) => ({
|
||||
id: p.id,
|
||||
postId: cidUrl[index].cid,
|
||||
postId: cidUrl[index].url,
|
||||
status: 'completed',
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${cidUrl[index].url.split('/').pop()}`,
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${cidUrl[index].url
|
||||
.split('/')
|
||||
.pop()}`,
|
||||
}));
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'bluesky-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const getThread = await agent.getPostThread({
|
||||
uri: id,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (getThread.data.thread.post?.likeCount >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await agent.repost(
|
||||
// @ts-ignore
|
||||
getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
getThread.data.thread.post?.cid
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'bluesky-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
const agent = new BskyAgent({
|
||||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
const getThread = await agent.getPostThread({
|
||||
uri: id,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
if (getThread.data.thread.post?.likeCount >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
const rt = new RichText({
|
||||
text: fields.post,
|
||||
});
|
||||
|
||||
await agent.post({
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
reply: {
|
||||
root: {
|
||||
// @ts-ignore
|
||||
uri: getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
cid: getThread.data.thread.post?.cid,
|
||||
},
|
||||
parent: {
|
||||
// @ts-ignore
|
||||
uri: getThread.data.thread.post?.uri,
|
||||
// @ts-ignore
|
||||
cid: getThread.data.thread.post?.cid,
|
||||
},
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,8 +298,8 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
const until = dayjs().endOf('day').unix()
|
||||
const since = dayjs().subtract(date, 'day').unix();
|
||||
|
||||
const { data } = await (
|
||||
await this.fetch(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { string } from 'yup';
|
||||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
|
||||
export class InstagramProvider
|
||||
extends SocialAbstract
|
||||
|
|
@ -204,10 +204,11 @@ export class InstagramProvider
|
|||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
postDetails: PostDetails<InstagramDto>[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...theRest] = postDetails;
|
||||
|
||||
console.log('in progress');
|
||||
const isStory = firstPost.settings.post_type === 'story';
|
||||
const medias = await Promise.all(
|
||||
firstPost?.media?.map(async (m) => {
|
||||
const caption =
|
||||
|
|
@ -219,18 +220,34 @@ export class InstagramProvider
|
|||
const mediaType =
|
||||
m.url.indexOf('.mp4') > -1
|
||||
? firstPost?.media?.length === 1
|
||||
? `video_url=${m.url}&media_type=REELS`
|
||||
? isStory
|
||||
? `video_url=${m.url}&media_type=STORIES`
|
||||
: `video_url=${m.url}&media_type=REELS`
|
||||
: isStory
|
||||
? `video_url=${m.url}&media_type=STORIES`
|
||||
: `video_url=${m.url}&media_type=VIDEO`
|
||||
: isStory
|
||||
? `image_url=${m.url}&media_type=STORIES`
|
||||
: `image_url=${m.url}`;
|
||||
console.log('in progress1');
|
||||
|
||||
const collaborators =
|
||||
firstPost?.settings?.collaborators?.length && !isStory
|
||||
? `&collaborators=${JSON.stringify(
|
||||
firstPost?.settings?.collaborators.map((p) => p.label)
|
||||
)}`
|
||||
: ``;
|
||||
|
||||
console.log(collaborators);
|
||||
const { id: photoId } = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}&access_token=${accessToken}${caption}`,
|
||||
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}${collaborators}&access_token=${accessToken}${caption}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
console.log('in progress2');
|
||||
|
||||
let status = 'IN_PROGRESS';
|
||||
while (status === 'IN_PROGRESS') {
|
||||
|
|
@ -242,6 +259,7 @@ export class InstagramProvider
|
|||
await timer(3000);
|
||||
status = status_code;
|
||||
}
|
||||
console.log('in progress3');
|
||||
|
||||
return photoId;
|
||||
}) || []
|
||||
|
|
@ -357,8 +375,8 @@ export class InstagramProvider
|
|||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
const until = dayjs().endOf('day').unix();
|
||||
const since = dayjs().subtract(date, 'day').unix();
|
||||
|
||||
const { data, ...all } = await (
|
||||
await fetch(
|
||||
|
|
@ -377,4 +395,12 @@ export class InstagramProvider
|
|||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
music(accessToken: string, data: { q: string }) {
|
||||
return this.fetch(
|
||||
`https://graph.facebook.com/v20.0/music/search?q=${encodeURIComponent(
|
||||
data.q
|
||||
)}&access_token=${accessToken}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|||
import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class LinkedinPageProvider
|
||||
extends LinkedinProvider
|
||||
|
|
@ -97,12 +99,14 @@ export class LinkedinPageProvider
|
|||
}
|
||||
|
||||
async companies(accessToken: string) {
|
||||
const { elements } = await (
|
||||
const { elements, ...all } = await (
|
||||
await fetch(
|
||||
'https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organizationalTarget~(localizedName,vanityName,logoV2(original~:playableStreams))))',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'LinkedIn-Version': '202402',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
@ -124,7 +128,10 @@ export class LinkedinPageProvider
|
|||
requiredId: string,
|
||||
accessToken: string
|
||||
): Promise<AuthTokenDetails> {
|
||||
const information = await this.fetchPageInformation(accessToken, requiredId);
|
||||
const information = await this.fetchPageInformation(
|
||||
accessToken,
|
||||
requiredId
|
||||
);
|
||||
|
||||
return {
|
||||
id: information.id,
|
||||
|
|
@ -355,6 +362,150 @@ export class LinkedinPageProvider
|
|||
percentageChange: 5,
|
||||
}));
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'linkedin-page-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
const {
|
||||
likesSummary: { totalLikes },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
if (totalLikes >= +fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await this.fetch(`https://api.linkedin.com/rest/posts`, {
|
||||
body: JSON.stringify({
|
||||
author: `urn:li:organization:${integration.internalId}`,
|
||||
commentary: '',
|
||||
visibility: 'PUBLIC',
|
||||
distribution: {
|
||||
feedDistribution: 'MAIN_FEED',
|
||||
targetEntities: [],
|
||||
thirdPartyDistributionChannels: [],
|
||||
},
|
||||
lifecycleState: 'PUBLISHED',
|
||||
isReshareDisabledByAuthor: false,
|
||||
reshareContext: {
|
||||
parent: id,
|
||||
},
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'linkedin-page-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
const {
|
||||
likesSummary: { totalLikes },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202402',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
if (totalLikes >= fields.likesAmount) {
|
||||
await timer(2000);
|
||||
await this.fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${decodeURIComponent(
|
||||
id
|
||||
)}/comments`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
actor: `urn:li:organization:${integration.internalId}`,
|
||||
object: id,
|
||||
message: {
|
||||
text: this.fixText(fields.post)
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Root {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
SocialAbstract,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'linkedin';
|
||||
name = 'LinkedIn';
|
||||
|
|
@ -156,7 +155,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
async company(token: string, data: { url: string }) {
|
||||
const { url } = data;
|
||||
const getCompanyVanity = url.match(
|
||||
/^https?:\/\/?www\.?linkedin\.com\/company\/([^/]+)\/$/
|
||||
/^https?:\/\/(?:www\.)?linkedin\.com\/company\/([^/]+)\/?$/
|
||||
);
|
||||
if (!getCompanyVanity || !getCompanyVanity?.length) {
|
||||
throw new Error('Invalid LinkedIn company URL');
|
||||
|
|
@ -282,6 +281,32 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
}
|
||||
|
||||
protected fixText(text: string) {
|
||||
const pattern = /@\[.+?]\(urn:li:organization.+?\)/g;
|
||||
const matches = text.match(pattern) || [];
|
||||
const splitAll = text.split(pattern);
|
||||
const splitTextReformat = splitAll.map((p) => {
|
||||
return p
|
||||
.replace(/\*/g, '\\*')
|
||||
.replace(/\(/g, '\\(')
|
||||
.replace(/\)/g, '\\)')
|
||||
.replace(/\{/g, '\\{')
|
||||
.replace(/}/g, '\\}')
|
||||
.replace(/@/g, '\\@');
|
||||
});
|
||||
|
||||
const connectAll = splitTextReformat.reduce((all, current) => {
|
||||
const match = matches.shift();
|
||||
all.push(current);
|
||||
if (match) {
|
||||
all.push(match);
|
||||
}
|
||||
return all;
|
||||
}, [] as string[]);
|
||||
|
||||
return connectAll.join('');
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
|
|
@ -305,6 +330,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
: await sharp(await readOrFetch(m.url), {
|
||||
animated: lookup(m.url) === 'image/gif',
|
||||
})
|
||||
.toFormat('jpeg')
|
||||
.resize({
|
||||
width: 1000,
|
||||
})
|
||||
|
|
@ -340,10 +366,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
type === 'personal'
|
||||
? `urn:li:person:${id}`
|
||||
: `urn:li:organization:${id}`,
|
||||
commentary: removeMarkdown({
|
||||
text: firstPost.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'),
|
||||
except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g],
|
||||
}).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'),
|
||||
commentary: this.fixText(firstPost.message),
|
||||
visibility: 'PUBLIC',
|
||||
distribution: {
|
||||
feedDistribution: 'MAIN_FEED',
|
||||
|
|
@ -409,10 +432,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
: `urn:li:organization:${id}`,
|
||||
object: topPostId,
|
||||
message: {
|
||||
text: removeMarkdown({
|
||||
text: post.message.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢'),
|
||||
except: [/@\[(.*?)]\(urn:li:organization:(\d+)\)/g],
|
||||
}).replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n'),
|
||||
text: this.fixText(post.message),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
expires_in: expiresIn,
|
||||
scope
|
||||
scope,
|
||||
} = await (
|
||||
await this.fetch('https://www.reddit.com/api/v1/access_token', {
|
||||
method: 'POST',
|
||||
|
|
@ -300,18 +300,28 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
)
|
||||
).json();
|
||||
|
||||
const newData = await (
|
||||
await this.fetch(
|
||||
`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
const newData = await new Promise<{id: string, name: string}[]>(async (res) => {
|
||||
try {
|
||||
const flair = await (
|
||||
await this.fetch(
|
||||
`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
res(flair);
|
||||
}
|
||||
catch (err) {
|
||||
return res([]);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
subreddit: data.subreddit,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export interface IAuthenticator {
|
|||
refresh?: string;
|
||||
},
|
||||
clientInformation?: ClientInformation
|
||||
): Promise<AuthTokenDetails>;
|
||||
): Promise<AuthTokenDetails|string>;
|
||||
refreshToken(refreshToken: string): Promise<AuthTokenDetails>;
|
||||
reConnect?(id: string, requiredId: string, accessToken: string): Promise<AuthTokenDetails>;
|
||||
generateAuthUrl(
|
||||
|
|
@ -51,6 +51,7 @@ export type GenerateAuthUrlResponse = {
|
|||
export type AuthTokenDetails = {
|
||||
id: string;
|
||||
name: string;
|
||||
error?: string;
|
||||
accessToken: string; // The obtained access token
|
||||
refreshToken?: string; // The refresh token, if applicable
|
||||
expiresIn?: number; // The duration in seconds for which the access token is valid
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { timer } from '@gitroom/helpers/utils/timer';
|
|||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { capitalize, chunk } from 'lodash';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'threads';
|
||||
|
|
@ -152,7 +154,6 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
let globalThread = '';
|
||||
let link = '';
|
||||
|
||||
if (firstPost?.media?.length! <= 1) {
|
||||
const type = !firstPost?.media?.[0]?.url
|
||||
? undefined
|
||||
|
|
@ -323,8 +324,8 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: string,
|
||||
date: number
|
||||
): Promise<AnalyticsData[]> {
|
||||
const until = dayjs().format('YYYY-MM-DD');
|
||||
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
|
||||
const until = dayjs().endOf('day').unix();
|
||||
const since = dayjs().subtract(date, 'day').unix();
|
||||
|
||||
const { data, ...all } = await (
|
||||
await fetch(
|
||||
|
|
@ -332,7 +333,6 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
)
|
||||
).json();
|
||||
|
||||
console.log(data);
|
||||
return (
|
||||
data?.map((d: any) => ({
|
||||
label: capitalize(d.name),
|
||||
|
|
@ -346,4 +346,73 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'threads-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
const { data } = await (
|
||||
await fetch(
|
||||
`https://graph.threads.net/v1.0/${id}/insights?metric=likes&access_token=${integration.token}`
|
||||
)
|
||||
).json();
|
||||
|
||||
const {
|
||||
values: [value],
|
||||
} = data.find((p: any) => p.name === 'likes');
|
||||
|
||||
if (value.value >= fields.likesAmount) {
|
||||
await timer(2000);
|
||||
|
||||
const form = new FormData();
|
||||
form.append('media_type', 'TEXT');
|
||||
form.append('text', fields.post);
|
||||
form.append('reply_to_id', id);
|
||||
form.append('access_token', integration.token);
|
||||
|
||||
const { id: replyId } = await (
|
||||
await this.fetch('https://graph.threads.net/v1.0/me/threads', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
|
||||
await (
|
||||
await this.fetch(
|
||||
`https://graph.threads.net/v1.0/${integration.internalId}/threads_publish?creation_id=${replyId}&access_token=${integration.token}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
SocialAbstract,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'tiktok';
|
||||
|
|
@ -46,7 +48,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
},
|
||||
} = await (
|
||||
await fetch(
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name,username',
|
||||
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name,union_id,username',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -76,10 +78,10 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
`?client_key=${process.env.TIKTOK_CLIENT_ID}` +
|
||||
`&redirect_uri=${encodeURIComponent(
|
||||
`${
|
||||
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
|
||||
? `https://integration.git.sn/integrations/social/tiktok`
|
||||
: `${process.env.FRONTEND_URL}/integrations/social/tiktok`
|
||||
}`
|
||||
process?.env?.FRONTEND_URL?.indexOf('https') === -1
|
||||
? 'https://redirectmeto.com/'
|
||||
: ''
|
||||
}${process?.env?.FRONTEND_URL}/integrations/social/tiktok`
|
||||
)}` +
|
||||
`&state=${state}` +
|
||||
`&response_type=code` +
|
||||
|
|
@ -100,10 +102,11 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
code: params.code,
|
||||
grant_type: 'authorization_code',
|
||||
code_verifier: params.codeVerifier,
|
||||
redirect_uri:
|
||||
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
|
||||
? `https://integration.git.sn/integrations/social/tiktok`
|
||||
: `${process.env.FRONTEND_URL}/integrations/social/tiktok`,
|
||||
redirect_uri: `${
|
||||
process?.env?.FRONTEND_URL?.indexOf('https') === -1
|
||||
? 'https://redirectmeto.com/'
|
||||
: ''
|
||||
}${process?.env?.FRONTEND_URL}/integrations/social/tiktok`
|
||||
};
|
||||
|
||||
const { access_token, refresh_token, scope } = await (
|
||||
|
|
@ -116,6 +119,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
})
|
||||
).json();
|
||||
|
||||
console.log(this.scopes, scope);
|
||||
this.checkScopes(this.scopes, scope);
|
||||
|
||||
const {
|
||||
|
|
@ -166,18 +170,16 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
private async uploadedVideoSuccess(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails<TikTokDto>[]
|
||||
): Promise<PostResponse[]> {
|
||||
try {
|
||||
const [firstPost, ...comments] = postDetails;
|
||||
const {
|
||||
data: { publish_id },
|
||||
} = await (
|
||||
publishId: string,
|
||||
accessToken: string
|
||||
): Promise<{ url: string; id: number }> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const post = await (
|
||||
await this.fetch(
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/',
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -185,37 +187,88 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
|||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
post_info: {
|
||||
title: firstPost.message,
|
||||
privacy_level: firstPost.settings.privacy_level,
|
||||
disable_duet: !firstPost.settings.duet,
|
||||
disable_comment: !firstPost.settings.comment,
|
||||
disable_stitch: !firstPost.settings.stitch,
|
||||
brand_content_toggle: firstPost.settings.brand_content_toggle,
|
||||
brand_organic_toggle: firstPost.settings.brand_organic_toggle,
|
||||
},
|
||||
source_info: {
|
||||
source: 'PULL_FROM_URL',
|
||||
video_url: firstPost?.media?.[0]?.url!,
|
||||
},
|
||||
publish_id: publishId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
releaseURL: `https://www.tiktok.com`,
|
||||
postId: publish_id,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
throw new BadBody('titok-error', JSON.stringify(err), {
|
||||
// @ts-ignore
|
||||
postDetails,
|
||||
});
|
||||
const { status, publicaly_available_post_id } = post.data;
|
||||
|
||||
if (status === 'PUBLISH_COMPLETE') {
|
||||
return {
|
||||
url: !publicaly_available_post_id
|
||||
? `https://www.tiktok.com/@${id}`
|
||||
: `https://www.tiktok.com/@${id}/video/` +
|
||||
publicaly_available_post_id,
|
||||
id: !publicaly_available_post_id
|
||||
? publishId
|
||||
: publicaly_available_post_id?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'FAILED') {
|
||||
throw new BadBody('titok-error-upload', JSON.stringify(post), {
|
||||
// @ts-ignore
|
||||
postDetails,
|
||||
});
|
||||
}
|
||||
|
||||
await timer(3000);
|
||||
}
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails<TikTokDto>[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...comments] = postDetails;
|
||||
|
||||
const {
|
||||
data: { publish_id },
|
||||
} = await (
|
||||
await this.fetch(
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
post_info: {
|
||||
title: firstPost.message,
|
||||
privacy_level: firstPost.settings.privacy_level,
|
||||
disable_duet: !firstPost.settings.duet,
|
||||
disable_comment: !firstPost.settings.comment,
|
||||
disable_stitch: !firstPost.settings.stitch,
|
||||
brand_content_toggle: firstPost.settings.brand_content_toggle,
|
||||
brand_organic_toggle: firstPost.settings.brand_organic_toggle,
|
||||
},
|
||||
source_info: {
|
||||
source: 'PULL_FROM_URL',
|
||||
video_url: firstPost?.media?.[0]?.url!,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
const { url, id: videoId } = await this.uploadedVideoSuccess(
|
||||
integration.profile!,
|
||||
publish_id,
|
||||
accessToken
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
releaseURL: url,
|
||||
postId: String(videoId),
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import sharp from 'sharp';
|
|||
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
|
||||
import removeMd from 'remove-markdown';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
export class XProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'x';
|
||||
|
|
@ -17,10 +20,112 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
isBetweenSteps = false;
|
||||
scopes = [];
|
||||
|
||||
@Plug({
|
||||
identifier: 'x-autoRepostPost',
|
||||
title: 'Auto Repost Posts',
|
||||
description:
|
||||
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoRepostPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string }
|
||||
) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
accessToken: accessTokenSplit,
|
||||
accessSecret: accessSecretSplit,
|
||||
});
|
||||
|
||||
if (
|
||||
(await client.v2.tweetLikedBy(id)).meta.result_count >=
|
||||
+fields.likesAmount
|
||||
) {
|
||||
await timer(2000);
|
||||
await client.v2.retweet(integration.internalId, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Plug({
|
||||
identifier: 'x-autoPlugPost',
|
||||
title: 'Auto plug post',
|
||||
description:
|
||||
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
||||
runEveryMilliseconds: 21600000,
|
||||
totalRuns: 3,
|
||||
fields: [
|
||||
{
|
||||
name: 'likesAmount',
|
||||
type: 'number',
|
||||
placeholder: 'Amount of likes',
|
||||
description: 'The amount of likes to trigger the repost',
|
||||
validation: /^\d+$/,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'richtext',
|
||||
placeholder: 'Post to plug',
|
||||
description: 'Message content to plug',
|
||||
validation: /^[\s\S]{3,}$/g,
|
||||
},
|
||||
],
|
||||
})
|
||||
async autoPlugPost(
|
||||
integration: Integration,
|
||||
id: string,
|
||||
fields: { likesAmount: string; post: string }
|
||||
) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
accessToken: accessTokenSplit,
|
||||
accessSecret: accessSecretSplit,
|
||||
});
|
||||
|
||||
if (
|
||||
(await client.v2.tweetLikedBy(id)).meta.result_count >=
|
||||
+fields.likesAmount
|
||||
) {
|
||||
await timer(2000);
|
||||
|
||||
await client.v2.tweet({
|
||||
text: removeMd(fields.post.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢')).replace(
|
||||
'𝔫𝔢𝔴𝔩𝔦𝔫𝔢',
|
||||
'\n'
|
||||
),
|
||||
reply: { in_reply_to_tweet_id: id },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const startingClient = new TwitterApi({
|
||||
clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
appKey: process.env.X_API_KEY!,
|
||||
appSecret: process.env.X_API_SECRET!,
|
||||
});
|
||||
const {
|
||||
accessToken,
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ export class StripeService {
|
|||
customer,
|
||||
status: 'all',
|
||||
})
|
||||
).data.filter((f) => f.status === 'active' || f.status === 'trialing'),
|
||||
).data,
|
||||
};
|
||||
|
||||
const { cancel_at } = await stripe.subscriptions.update(
|
||||
|
|
@ -270,12 +270,13 @@ export class StripeService {
|
|||
body: BillingSubscribeDto,
|
||||
price: string
|
||||
) {
|
||||
const isUtm = body.utm ? `&utm_source=${body.utm}` : '';
|
||||
const { url } = await stripe.checkout.sessions.create({
|
||||
customer,
|
||||
cancel_url: process.env['FRONTEND_URL'] + `/billing`,
|
||||
cancel_url: process.env['FRONTEND_URL'] + `/billing?cancel=true${isUtm}`,
|
||||
success_url:
|
||||
process.env['FRONTEND_URL'] +
|
||||
`/launches?onboarding=true&check=${uniqueId}`,
|
||||
`/launches?onboarding=true&check=${uniqueId}${isUtm}`,
|
||||
mode: 'subscription',
|
||||
subscription_data: {
|
||||
trial_period_days: 7,
|
||||
|
|
@ -285,6 +286,11 @@ export class StripeService {
|
|||
uniqueId,
|
||||
},
|
||||
},
|
||||
...body.tolt ? {
|
||||
metadata: {
|
||||
tolt_referral: body.tolt,
|
||||
}
|
||||
} : {},
|
||||
allow_promotion_codes: true,
|
||||
line_items: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
|
||||
public override async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
if (context.switchToHttp().getRequest().url.includes('/public/v1')) {
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override async getTracker(req: Record<string, any>): Promise<string> {
|
||||
return req.org.id;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue