feat: lemmy
This commit is contained in:
commit
a2b6341cfd
|
|
@ -14,7 +14,6 @@ import { SettingsController } from '@gitroom/backend/api/routes/settings.control
|
|||
import { PostsController } from '@gitroom/backend/api/routes/posts.controller';
|
||||
import { MediaController } from '@gitroom/backend/api/routes/media.controller';
|
||||
import { UploadModule } from '@gitroom/nestjs-libraries/upload/upload.module';
|
||||
import { CommentsController } from '@gitroom/backend/api/routes/comments.controller';
|
||||
import { BillingController } from '@gitroom/backend/api/routes/billing.controller';
|
||||
import { NotificationsController } from '@gitroom/backend/api/routes/notifications.controller';
|
||||
import { MarketplaceController } from '@gitroom/backend/api/routes/marketplace.controller';
|
||||
|
|
@ -26,6 +25,7 @@ import { CopilotController } from '@gitroom/backend/api/routes/copilot.controlle
|
|||
import { AgenciesController } from '@gitroom/backend/api/routes/agencies.controller';
|
||||
import { PublicController } from '@gitroom/backend/api/routes/public.controller';
|
||||
import { RootController } from '@gitroom/backend/api/routes/root.controller';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
|
||||
const authenticatedController = [
|
||||
UsersController,
|
||||
|
|
@ -34,7 +34,6 @@ const authenticatedController = [
|
|||
SettingsController,
|
||||
PostsController,
|
||||
MediaController,
|
||||
CommentsController,
|
||||
BillingController,
|
||||
NotificationsController,
|
||||
MarketplaceController,
|
||||
|
|
@ -63,6 +62,7 @@ const authenticatedController = [
|
|||
PermissionsService,
|
||||
CodesService,
|
||||
IntegrationManager,
|
||||
TrackService,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ import { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.lis
|
|||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
@ApiTags('Analytics')
|
||||
@Controller('/analytics')
|
||||
|
|
@ -25,7 +22,6 @@ export class AnalyticsController {
|
|||
constructor(
|
||||
private _starsService: StarsService,
|
||||
private _integrationService: IntegrationService,
|
||||
private _integrationManager: IntegrationManager
|
||||
) {}
|
||||
@Get('/')
|
||||
async getStars(@GetOrgFromRequest() org: Organization) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Ip,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Response, Request } from 'express';
|
||||
|
||||
import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
|
||||
|
|
@ -9,6 +18,8 @@ import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.pa
|
|||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
import { RealIP } from 'nestjs-real-ip';
|
||||
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('/auth')
|
||||
|
|
@ -21,7 +32,9 @@ export class AuthController {
|
|||
async register(
|
||||
@Req() req: Request,
|
||||
@Body() body: CreateOrgUserDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string
|
||||
) {
|
||||
try {
|
||||
const getOrgFromCookie = this._authService.getOrgFromCookie(
|
||||
|
|
@ -31,10 +44,13 @@ export class AuthController {
|
|||
const { jwt, addedOrg } = await this._authService.routeAuth(
|
||||
body.provider,
|
||||
body,
|
||||
ip,
|
||||
userAgent,
|
||||
getOrgFromCookie
|
||||
);
|
||||
|
||||
const activationRequired = body.provider === 'LOCAL' && this._emailService.hasProvider();
|
||||
const activationRequired =
|
||||
body.provider === 'LOCAL' && this._emailService.hasProvider();
|
||||
|
||||
if (activationRequired) {
|
||||
response.header('activate', 'true');
|
||||
|
|
@ -73,7 +89,9 @@ export class AuthController {
|
|||
async login(
|
||||
@Req() req: Request,
|
||||
@Body() body: LoginUserDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string
|
||||
) {
|
||||
try {
|
||||
const getOrgFromCookie = this._authService.getOrgFromCookie(
|
||||
|
|
@ -83,6 +101,8 @@ export class AuthController {
|
|||
const { jwt, addedOrg } = await this._authService.routeAuth(
|
||||
body.provider,
|
||||
body,
|
||||
ip,
|
||||
userAgent,
|
||||
getOrgFromCookie
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Req } from '@nestjs/common';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
|
@ -7,6 +7,7 @@ import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/bill
|
|||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { Request } from 'express';
|
||||
|
||||
@ApiTags('Billing')
|
||||
@Controller('/billing')
|
||||
|
|
@ -23,19 +24,22 @@ export class BillingController {
|
|||
@Param('id') body: string
|
||||
) {
|
||||
return {
|
||||
exists: !!(await this._subscriptionService.checkSubscription(
|
||||
status: await this._stripeService.checkSubscription(
|
||||
org.id,
|
||||
body
|
||||
)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/subscribe')
|
||||
subscribe(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: BillingSubscribeDto
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: BillingSubscribeDto,
|
||||
@Req() req: Request
|
||||
) {
|
||||
return this._stripeService.subscribe(org.id, body);
|
||||
const uniqueId = req?.cookies?.track;
|
||||
return this._stripeService.subscribe(uniqueId, org.id, user.id, body, org.allowTrial);
|
||||
}
|
||||
|
||||
@Get('/portal')
|
||||
|
|
@ -57,12 +61,14 @@ export class BillingController {
|
|||
@Post('/cancel')
|
||||
async cancel(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: { feedback: string }
|
||||
) {
|
||||
await this._notificationService.sendEmail(
|
||||
process.env.EMAIL_FROM_ADDRESS,
|
||||
'Subscription Cancelled',
|
||||
`Organization ${org.name} has cancelled their subscription because: ${body.feedback}`
|
||||
`Organization ${org.name} has cancelled their subscription because: ${body.feedback}`,
|
||||
user.email
|
||||
);
|
||||
|
||||
return this._stripeService.setToCancel(org.id);
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import {Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common';
|
||||
import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { AddCommentDto } from '@gitroom/nestjs-libraries/dtos/comments/add.comment.dto';
|
||||
import {ApiTags} from "@nestjs/swagger";
|
||||
|
||||
@ApiTags('Comments')
|
||||
@Controller('/comments')
|
||||
export class CommentsController {
|
||||
constructor(private _commentsService: CommentsService) {}
|
||||
|
||||
@Post('/')
|
||||
addComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() addCommentDto: AddCommentDto
|
||||
) {
|
||||
return this._commentsService.addAComment(
|
||||
org.id,
|
||||
user.id,
|
||||
addCommentDto.content,
|
||||
addCommentDto.date
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/:id')
|
||||
addCommentTocComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() addCommentDto: AddCommentDto,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._commentsService.addACommentToComment(
|
||||
org.id,
|
||||
user.id,
|
||||
id,
|
||||
addCommentDto.content,
|
||||
addCommentDto.date
|
||||
);
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
editComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() addCommentDto: AddCommentDto,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._commentsService.updateAComment(
|
||||
org.id,
|
||||
user.id,
|
||||
id,
|
||||
addCommentDto.content
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
deleteComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._commentsService.deleteAComment(
|
||||
org.id,
|
||||
user.id,
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/:date')
|
||||
loadAllCommentsAndSubCommentsForADate(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('date') date: string
|
||||
) {
|
||||
return this._commentsService.loadAllCommentsAndSubCommentsForADate(
|
||||
org.id,
|
||||
date
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
import {
|
||||
Body, Controller, Delete, Get, Param, Post, Put, 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';
|
||||
|
|
@ -42,6 +50,11 @@ export class IntegrationsController {
|
|||
return this._integrationManager.getAllIntegrations();
|
||||
}
|
||||
|
||||
@Get('/:identifier/internal-plugs')
|
||||
getInternalPlugs(@Param('identifier') identifier: string) {
|
||||
return this._integrationManager.getInternalPlugs(identifier);
|
||||
}
|
||||
|
||||
@Get('/customers')
|
||||
getCustomers(@GetOrgFromRequest() org: Organization) {
|
||||
return this._integrationService.customers(org.id);
|
||||
|
|
@ -66,11 +79,7 @@ export class IntegrationsController {
|
|||
@Param('id') id: string,
|
||||
@Body() body: { name: string }
|
||||
) {
|
||||
return this._integrationService.updateOnCustomerName(
|
||||
org.id,
|
||||
id,
|
||||
body.name
|
||||
);
|
||||
return this._integrationService.updateOnCustomerName(org.id, id, body.name);
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
|
|
@ -97,11 +106,24 @@ export class IntegrationsController {
|
|||
changeProfilePicture: !!findIntegration?.changeProfilePicture,
|
||||
changeNickName: !!findIntegration?.changeNickname,
|
||||
customer: p.customer,
|
||||
additionalSettings: p.additionalSettings || '[]',
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/:id/settings')
|
||||
async updateProviderSettings(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('additionalSettings') body: string
|
||||
) {
|
||||
if (typeof body !== 'string') {
|
||||
throw new Error('Invalid body');
|
||||
}
|
||||
|
||||
await this._integrationService.updateProviderSettings(org.id, id, body);
|
||||
}
|
||||
@Post('/:id/nickname')
|
||||
async setNickname(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -242,19 +264,22 @@ export class IntegrationsController {
|
|||
const load = await integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
getIntegration.internalId
|
||||
getIntegration.internalId,
|
||||
getIntegration
|
||||
);
|
||||
|
||||
return load;
|
||||
} catch (err) {
|
||||
if (err instanceof RefreshToken) {
|
||||
const { accessToken, refreshToken, expiresIn } =
|
||||
const { accessToken, refreshToken, expiresIn, additionalSettings } =
|
||||
await integrationProvider.refreshToken(
|
||||
getIntegration.refreshToken
|
||||
);
|
||||
|
||||
if (accessToken) {
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
getIntegration.organizationId,
|
||||
getIntegration.name,
|
||||
getIntegration.picture!,
|
||||
|
|
@ -336,6 +361,8 @@ export class IntegrationsController {
|
|||
}
|
||||
|
||||
return this._integrationService.createOrUpdateIntegration(
|
||||
undefined,
|
||||
true,
|
||||
org.id,
|
||||
name,
|
||||
picture,
|
||||
|
|
@ -402,6 +429,7 @@ export class IntegrationsController {
|
|||
name,
|
||||
picture,
|
||||
username,
|
||||
additionalSettings,
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
} = await new Promise<AuthTokenDetails>(async (res) => {
|
||||
const auth = await integrationProvider.authenticate(
|
||||
|
|
@ -421,6 +449,7 @@ export class IntegrationsController {
|
|||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
additionalSettings: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +473,7 @@ export class IntegrationsController {
|
|||
throw new NotEnoughScopes('Invalid API key');
|
||||
}
|
||||
|
||||
if (refresh && id !== refresh) {
|
||||
if (refresh && String(id) !== String(refresh)) {
|
||||
throw new NotEnoughScopes(
|
||||
'Please refresh the channel that needs to be refreshed'
|
||||
);
|
||||
|
|
@ -459,6 +488,8 @@ export class IntegrationsController {
|
|||
}
|
||||
}
|
||||
return this._integrationService.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
org.id,
|
||||
validName.trim(),
|
||||
picture,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
import {
|
||||
Body, Controller, Get, Param, Post, Query, Req, Res, UploadedFile, UseInterceptors, UsePipes
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
Res,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
|
@ -10,7 +20,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')
|
||||
|
|
@ -26,14 +35,35 @@ export class MediaController {
|
|||
async generateImage(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Req() req: Request,
|
||||
@Body('prompt') prompt: string
|
||||
@Body('prompt') prompt: string,
|
||||
isPicturePrompt = false
|
||||
) {
|
||||
const total = await this._subscriptionService.checkCredits(org);
|
||||
if (total.credits <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {output: 'data:image/png;base64,' + await this._mediaService.generateImage(prompt, org)};
|
||||
return {
|
||||
output:
|
||||
(isPicturePrompt ? '' : 'data:image/png;base64,') +
|
||||
(await this._mediaService.generateImage(prompt, org, isPicturePrompt)),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/generate-image-with-prompt')
|
||||
async generateImageFromText(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Req() req: Request,
|
||||
@Body('prompt') prompt: string
|
||||
) {
|
||||
const image = await this.generateImage(org, req, prompt, true);
|
||||
if (!image) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const file = await this.storage.uploadSimple(image.output);
|
||||
|
||||
return this._mediaService.saveFile(org.id, file.split('/').pop(), file);
|
||||
}
|
||||
|
||||
@Post('/upload-server')
|
||||
|
|
@ -44,7 +74,11 @@ export class MediaController {
|
|||
@UploadedFile() file: Express.Multer.File
|
||||
) {
|
||||
const uploadedFile = await this.storage.uploadFile(file);
|
||||
return this._mediaService.saveFile(org.id, uploadedFile.originalname, uploadedFile.path);
|
||||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
uploadedFile.originalname,
|
||||
uploadedFile.path
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/upload-simple')
|
||||
|
|
@ -54,7 +88,11 @@ export class MediaController {
|
|||
@UploadedFile('file') file: Express.Multer.File
|
||||
) {
|
||||
const getFile = await this.storage.uploadFile(file);
|
||||
return this._mediaService.saveFile(org.id, getFile.originalname, getFile.path);
|
||||
return this._mediaService.saveFile(
|
||||
org.id,
|
||||
getFile.originalname,
|
||||
getFile.path
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/:endpoint')
|
||||
|
|
@ -76,10 +114,14 @@ export class MediaController {
|
|||
// @ts-ignore
|
||||
const name = upload.Location.split('/').pop();
|
||||
|
||||
// @ts-ignore
|
||||
await this._mediaService.saveFile(org.id, name, upload.Location);
|
||||
const saveFile = await this._mediaService.saveFile(
|
||||
org.id,
|
||||
name,
|
||||
// @ts-ignore
|
||||
upload.Location
|
||||
);
|
||||
|
||||
res.status(200).json(upload);
|
||||
res.status(200).json({ ...upload, saved: saveFile });
|
||||
// const filePath =
|
||||
// file.path.indexOf('http') === 0
|
||||
// ? file.path
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
|
@ -23,6 +24,9 @@ import { ApiTags } from '@nestjs/swagger';
|
|||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
import { Response } from 'express';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
|
||||
@ApiTags('Posts')
|
||||
@Controller('/posts')
|
||||
|
|
@ -30,7 +34,8 @@ export class PostsController {
|
|||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _starsService: StarsService,
|
||||
private _messagesService: MessagesService
|
||||
private _messagesService: MessagesService,
|
||||
private _agentGraphService: AgentGraphService
|
||||
) {}
|
||||
|
||||
@Get('/marketplace/:id?')
|
||||
|
|
@ -41,6 +46,16 @@ export class PostsController {
|
|||
return this._messagesService.getMarketplaceAvailableOffers(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/:id/comments')
|
||||
async createComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { comment: string }
|
||||
) {
|
||||
return this._postsService.createComment(org.id, user.id, id, body.comment);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
async getPosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -61,6 +76,13 @@ export class PostsController {
|
|||
};
|
||||
}
|
||||
|
||||
@Get('/find-slot')
|
||||
async findSlot(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
) {
|
||||
return {date: await this._postsService.findFreeDateTime(org.id)}
|
||||
}
|
||||
|
||||
@Get('/predict-trending')
|
||||
predictTrending() {
|
||||
return this._starsService.predictTrending();
|
||||
|
|
@ -100,11 +122,20 @@ export class PostsController {
|
|||
|
||||
@Post('/generator')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH])
|
||||
generatePosts(
|
||||
async generatePosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: GeneratorDto
|
||||
@Body() body: GeneratorDto,
|
||||
@Res({ passthrough: false }) res: Response
|
||||
) {
|
||||
return this._postsService.generatePosts(org.id, body);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
for await (const event of this._agentGraphService.start(
|
||||
org.id,
|
||||
body,
|
||||
)) {
|
||||
res.write(JSON.stringify(event) + '\n');
|
||||
}
|
||||
|
||||
res.end();
|
||||
}
|
||||
|
||||
@Delete('/:group')
|
||||
|
|
|
|||
|
|
@ -1,11 +1,37 @@
|
|||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
import { RealIP } from 'nestjs-real-ip';
|
||||
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
|
||||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
import { Request, Response } from 'express';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service';
|
||||
|
||||
@ApiTags('Public')
|
||||
@Controller('/public')
|
||||
export class PublicController {
|
||||
constructor(private _agenciesService: AgenciesService) {}
|
||||
constructor(
|
||||
private _agenciesService: AgenciesService,
|
||||
private _trackService: TrackService,
|
||||
private _agentGraphInsertService: AgentGraphInsertService,
|
||||
private _postsService: PostsService
|
||||
) {}
|
||||
@Post('/agent')
|
||||
async createAgent(@Body() body: { text: string; apiKey: string }) {
|
||||
if (
|
||||
!body.apiKey ||
|
||||
!process.env.AGENT_API_KEY ||
|
||||
body.apiKey !== process.env.AGENT_API_KEY
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return this._agentGraphInsertService.newPost(body.text);
|
||||
}
|
||||
|
||||
@Get('/agencies-list')
|
||||
async getAgencyByUser() {
|
||||
return this._agenciesService.getAllAgencies();
|
||||
|
|
@ -17,9 +43,7 @@ export class PublicController {
|
|||
}
|
||||
|
||||
@Get('/agencies-information/:agency')
|
||||
async getAgencyInformation(
|
||||
@Param('agency') agency: string,
|
||||
) {
|
||||
async getAgencyInformation(@Param('agency') agency: string) {
|
||||
return this._agenciesService.getAgencyInformation(agency);
|
||||
}
|
||||
|
||||
|
|
@ -27,4 +51,73 @@ export class PublicController {
|
|||
async getAgenciesCount() {
|
||||
return this._agenciesService.getCount();
|
||||
}
|
||||
|
||||
@Get(`/posts/:id`)
|
||||
async getPreview(@Param('id') id: string) {
|
||||
return (await this._postsService.getPostsRecursively(id, true)).map(
|
||||
({ childrenPost, ...p }) => ({
|
||||
...p,
|
||||
...(p.integration
|
||||
? {
|
||||
integration: {
|
||||
id: p.integration.id,
|
||||
name: p.integration.name,
|
||||
picture: p.integration.picture,
|
||||
providerIdentifier: p.integration.providerIdentifier,
|
||||
profile: p.integration.profile,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Get(`/posts/:id/comments`)
|
||||
async getComments(@Param('id') postId: string) {
|
||||
return { comments: await this._postsService.getComments(postId) };
|
||||
}
|
||||
|
||||
@Post('/t')
|
||||
async trackEvent(
|
||||
@Res() res: Response,
|
||||
@Req() req: Request,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string,
|
||||
@Body()
|
||||
body: { fbclid?: string; tt: TrackEnum; additional: Record<string, any> }
|
||||
) {
|
||||
const uniqueId = req?.cookies?.track || makeId(10);
|
||||
const fbclid = req?.cookies?.fbclid || body.fbclid;
|
||||
await this._trackService.track(
|
||||
uniqueId,
|
||||
ip,
|
||||
userAgent,
|
||||
body.tt,
|
||||
body.additional,
|
||||
fbclid
|
||||
);
|
||||
if (!req.cookies.track) {
|
||||
res.cookie('track', uniqueId, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
}
|
||||
|
||||
if (body.fbclid && !req.cookies.fbclid) {
|
||||
res.cookie('fbclid', body.fbclid, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
track: uniqueId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,11 +54,13 @@ export class StripeController {
|
|||
// Maybe it comes from another stripe webhook
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (event?.data?.object?.metadata?.service !== 'gitroom') {
|
||||
if (event?.data?.object?.metadata?.service !== 'gitroom' && event.type !== 'invoice.payment_succeeded') {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'invoice.payment_succeeded':
|
||||
return this._stripeService.paymentSucceeded(event);
|
||||
case 'checkout.session.completed':
|
||||
return this._stripeService.updateOrder(event);
|
||||
case 'account.updated':
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ import { ApiTags } from '@nestjs/swagger';
|
|||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
|
||||
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||
import { RealIP } from 'nestjs-real-ip';
|
||||
import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent';
|
||||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller('/user')
|
||||
|
|
@ -36,7 +41,8 @@ export class UsersController {
|
|||
private _stripeService: StripeService,
|
||||
private _authService: AuthService,
|
||||
private _orgService: OrganizationService,
|
||||
private _userService: UsersService
|
||||
private _userService: UsersService,
|
||||
private _trackService: TrackService
|
||||
) {}
|
||||
@Get('/self')
|
||||
async getSelf(
|
||||
|
|
@ -47,20 +53,26 @@ export class UsersController {
|
|||
if (!organization) {
|
||||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return {
|
||||
...user,
|
||||
orgId: organization.id,
|
||||
// @ts-ignore
|
||||
totalChannels: organization?.subscription?.totalChannels || pricing.FREE.channel,
|
||||
// @ts-ignore
|
||||
tier: organization?.subscription?.subscriptionTier || (!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE'),
|
||||
tier: organization?.subscription?.subscriptionTier ||
|
||||
(!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE'),
|
||||
// @ts-ignore
|
||||
role: organization?.users[0]?.role,
|
||||
// @ts-ignore
|
||||
isLifetime: !!organization?.subscription?.isLifetime,
|
||||
admin: !!user.isSuperAdmin,
|
||||
impersonate: !!req.cookies.impersonate,
|
||||
allowTrial: organization?.allowTrial,
|
||||
// @ts-ignore
|
||||
publicApi: organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN'
|
||||
? organization?.apiKey
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -203,4 +215,32 @@ export class UsersController {
|
|||
|
||||
response.status(200).send();
|
||||
}
|
||||
|
||||
@Post('/t')
|
||||
async trackEvent(
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Req() req: Request,
|
||||
@GetUserFromRequest() user: User,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string,
|
||||
@Body() body: { tt: TrackEnum; fbclid: string, additional: Record<string, any> }
|
||||
) {
|
||||
const uniqueId = req?.cookies?.track || makeId(10);
|
||||
const fbclid = req?.cookies?.fbclid || body.fbclid;
|
||||
await this._trackService.track(uniqueId, ip, userAgent, body.tt, body.additional, fbclid, user);
|
||||
if (!req.cookies.track) {
|
||||
res.cookie('track', uniqueId, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
}
|
||||
|
||||
console.log('hello');
|
||||
res.status(200).json({
|
||||
track: uniqueId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,33 @@ 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';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [BullMqModule, DatabaseModule, ApiModule, PluginModule],
|
||||
imports: [
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PluginModule,
|
||||
PublicApiModule,
|
||||
AgentModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
limit: 30,
|
||||
},
|
||||
]),
|
||||
],
|
||||
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,98 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Post,
|
||||
Query,
|
||||
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';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
|
||||
@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
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/posts')
|
||||
async getPosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsDto
|
||||
) {
|
||||
const posts = await this._postsService.getPosts(org.id, query);
|
||||
|
||||
return {
|
||||
posts,
|
||||
// comments,
|
||||
};
|
||||
}
|
||||
|
||||
@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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +81,10 @@ export class AuthMiddleware implements NestMiddleware {
|
|||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
||||
if (!setOrg.apiKey) {
|
||||
await this._organizationService.updateApiKey(setOrg.id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
req.user = user;
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ export class AuthService {
|
|||
private _userService: UsersService,
|
||||
private _organizationService: OrganizationService,
|
||||
private _notificationService: NotificationService,
|
||||
private _emailService: EmailService,
|
||||
private _emailService: EmailService
|
||||
) {}
|
||||
async routeAuth(
|
||||
provider: Provider,
|
||||
body: CreateOrgUserDto | LoginUserDto,
|
||||
ip: string,
|
||||
userAgent: string,
|
||||
addToOrg?: boolean | { orgId: string; role: 'USER' | 'ADMIN'; id: string }
|
||||
) {
|
||||
if (provider === Provider.LOCAL) {
|
||||
|
|
@ -32,7 +34,11 @@ export class AuthService {
|
|||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const create = await this._organizationService.createOrgAndUser(body);
|
||||
const create = await this._organizationService.createOrgAndUser(
|
||||
body,
|
||||
ip,
|
||||
userAgent
|
||||
);
|
||||
|
||||
const addedOrg =
|
||||
addToOrg && typeof addToOrg !== 'boolean'
|
||||
|
|
@ -45,7 +51,11 @@ export class AuthService {
|
|||
: false;
|
||||
|
||||
const obj = { addedOrg, jwt: await this.jwt(create.users[0].user) };
|
||||
await this._emailService.sendEmail(body.email, 'Activate your account', `Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`);
|
||||
await this._emailService.sendEmail(
|
||||
body.email,
|
||||
'Activate your account',
|
||||
`Click <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> to activate your account`
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +72,9 @@ export class AuthService {
|
|||
|
||||
const user = await this.loginOrRegisterProvider(
|
||||
provider,
|
||||
body as CreateOrgUserDto
|
||||
body as CreateOrgUserDto,
|
||||
ip,
|
||||
userAgent
|
||||
);
|
||||
|
||||
const addedOrg =
|
||||
|
|
@ -101,7 +113,9 @@ export class AuthService {
|
|||
|
||||
private async loginOrRegisterProvider(
|
||||
provider: Provider,
|
||||
body: CreateOrgUserDto
|
||||
body: CreateOrgUserDto,
|
||||
ip: string,
|
||||
userAgent: string
|
||||
) {
|
||||
const providerInstance = ProvidersFactory.loadProvider(provider);
|
||||
const providerUser = await providerInstance.getUser(body.providerToken);
|
||||
|
|
@ -118,15 +132,19 @@ export class AuthService {
|
|||
return user;
|
||||
}
|
||||
|
||||
const create = await this._organizationService.createOrgAndUser({
|
||||
company: body.company,
|
||||
email: providerUser.email,
|
||||
password: '',
|
||||
provider,
|
||||
providerId: providerUser.id,
|
||||
});
|
||||
const create = await this._organizationService.createOrgAndUser(
|
||||
{
|
||||
company: body.company,
|
||||
email: providerUser.email,
|
||||
password: '',
|
||||
provider,
|
||||
providerId: providerUser.id,
|
||||
},
|
||||
ip,
|
||||
userAgent
|
||||
);
|
||||
|
||||
NewsletterService.register(providerUser.email);
|
||||
await NewsletterService.register(providerUser.email);
|
||||
|
||||
return create.users[0].user;
|
||||
}
|
||||
|
|
@ -162,7 +180,11 @@ export class AuthService {
|
|||
}
|
||||
|
||||
async activate(code: string) {
|
||||
const user = AuthChecker.verifyJWT(code) as { id: string, activated: boolean, email: string };
|
||||
const user = AuthChecker.verifyJWT(code) as {
|
||||
id: string;
|
||||
activated: boolean;
|
||||
email: string;
|
||||
};
|
||||
if (user.id && !user.activated) {
|
||||
const getUserAgain = await this._userService.getUserByEmail(user.email);
|
||||
if (getUserAgain.activated) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -25,17 +25,7 @@
|
|||
"executor": "nx:run-commands",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "commands:build",
|
||||
"inspect": false,
|
||||
"command": "cd dist/apps/commands && node main.js"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "commands:build:development"
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "commands:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,18 @@ import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/databa
|
|||
import { RefreshTokens } from './tasks/refresh.tokens';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { ConfigurationTask } from './tasks/configuration';
|
||||
import { AgentRun } from './tasks/agent.run';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
|
||||
@Module({
|
||||
imports: [ExternalCommandModule, DatabaseModule, BullMqModule],
|
||||
imports: [
|
||||
ExternalCommandModule,
|
||||
DatabaseModule,
|
||||
BullMqModule,
|
||||
AgentModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [CheckStars, RefreshTokens, ConfigurationTask],
|
||||
providers: [CheckStars, RefreshTokens, ConfigurationTask, AgentRun],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { Command } from 'nestjs-command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentRun {
|
||||
constructor(private _agentGraphService: AgentGraphService) {}
|
||||
@Command({
|
||||
command: 'run:agent',
|
||||
describe: 'Run the agent',
|
||||
})
|
||||
async agentRun() {
|
||||
console.log(await this._agentGraphService.createGraph('hello', true));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,6 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { PreviewWrapper } from '@gitroom/frontend/components/preview/preview.wrapper';
|
||||
|
||||
export default async function AppLayout({ children }: { children: ReactNode }) {
|
||||
return <PreviewWrapper>{children}</PreviewWrapper>;
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { Metadata } from 'next';
|
||||
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { CommentsComponents } from '@gitroom/frontend/components/preview/comments.components';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
|
||||
import { CopyClient } from '@gitroom/frontend/components/preview/copy.client';
|
||||
|
||||
dayjs.extend(utc);
|
||||
export const metadata: Metadata = {
|
||||
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Preview`,
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default async function Auth({
|
||||
params: { id },
|
||||
searchParams,
|
||||
}: {
|
||||
params: { id: string };
|
||||
searchParams?: { share?: string };
|
||||
}) {
|
||||
const post = await (await internalFetch(`/public/posts/${id}`)).json();
|
||||
|
||||
if (!post.length) {
|
||||
return (
|
||||
<div className="text-white fixed left-0 top-0 w-full h-full flex justify-center items-center text-[20px]">
|
||||
Post not found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto w-full max-w-[1346px] py-3 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="min-w-[55px]">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-2xl flex items-center justify-center gap-[10px] text-textColor order-1"
|
||||
>
|
||||
<div className="max-w-[55px]">
|
||||
<Image
|
||||
src={'/postiz.svg'}
|
||||
width={55}
|
||||
height={55}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<svg
|
||||
width="80"
|
||||
height="75"
|
||||
viewBox="0 0 366 167"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24.9659 30.4263V43.3825C26.9237 41.3095 29.3998 39.582 32.3941 38.2C35.3885 36.7028 39.0162 35.9543 43.2774 35.9543C47.1931 35.9543 50.8784 36.7028 54.3334 38.2C57.9036 39.6972 61.0131 42.1157 63.6619 45.4555C66.4259 48.6802 68.6141 52.9989 70.2264 58.4118C71.8387 63.8246 72.6449 70.3891 72.6449 78.1053C72.6449 83.6333 72.1266 89.1613 71.0902 94.6893C70.1688 100.217 68.4989 105.169 66.0804 109.546C63.6619 113.922 60.3796 117.492 56.2336 120.256C52.2028 122.905 47.1355 124.23 41.0316 124.23C36.6553 124.23 33.2003 123.654 30.6666 122.502C28.1329 121.235 26.2327 119.796 24.9659 118.183V160.162L0.0898438 166.381V30.4263H24.9659ZM32.7396 109.2C35.734 109.2 38.2676 108.221 40.3406 106.264C42.4136 104.191 44.026 101.542 45.1776 98.3171C46.4445 95.0924 47.3082 91.5222 47.7689 87.6066C48.3447 83.5757 48.6326 79.6025 48.6326 75.6868C48.6326 69.3526 48.0568 64.3429 46.9051 60.6575C45.8686 56.9722 44.6018 54.2658 43.1046 52.5383C41.6075 50.6956 40.1103 49.5439 38.6131 49.0833C37.2311 48.6226 36.137 48.3923 35.3309 48.3923C33.2579 48.3923 31.2425 49.1409 29.2846 50.638C27.3268 52.02 25.8872 54.1506 24.9659 57.0298V105.227C25.5417 106.148 26.463 107.07 27.7299 107.991C28.9967 108.797 30.6666 109.2 32.7396 109.2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M188.176 31.4627C191.055 42.5188 193.588 51.5593 195.777 58.5845C197.965 65.4945 199.807 71.3105 201.305 76.0323C202.917 80.7541 204.126 84.9001 204.932 88.4703C205.854 92.0405 206.314 96.0137 206.314 100.39C208.272 99.1232 210.172 97.7988 212.015 96.4168C213.858 94.9196 215.413 93.5376 216.679 92.2708H223.935C220.825 96.9926 217.543 100.908 214.088 104.018C210.633 107.012 207.293 109.661 204.069 111.964C201.996 116.456 198.829 119.623 194.567 121.466C190.306 123.308 185.872 124.23 181.266 124.23C176.083 124.23 171.649 123.539 167.964 122.157C164.279 120.659 161.227 118.702 158.808 116.283C156.505 113.749 154.777 110.87 153.626 107.646C152.474 104.421 151.898 101.023 151.898 97.4533C151.898 93.5376 152.819 90.4857 154.662 88.2975C156.62 85.9942 158.866 84.8426 161.399 84.8426C168.424 84.8426 171.937 87.6641 171.937 93.3073C171.937 95.15 171.304 96.7047 170.037 97.9716C168.77 99.2384 167.158 99.8718 165.2 99.8718C164.278 99.8718 163.3 99.7566 162.263 99.5263C161.342 99.1808 160.593 98.5474 160.017 97.6261C160.939 101.657 162.436 104.824 164.509 107.127C166.697 109.431 169.461 110.582 172.801 110.582C175.68 110.582 177.811 109.891 179.193 108.509C180.575 107.012 181.266 104.478 181.266 100.908C181.266 97.1078 180.92 93.7104 180.229 90.7161C179.653 87.6066 178.732 84.2091 177.465 80.5238C176.198 76.8385 174.644 72.4621 172.801 67.3948C170.958 62.2123 168.885 55.5326 166.582 47.3558C160.823 59.6786 153.222 67.5675 143.779 71.0225C143.779 71.9439 143.779 72.8652 143.779 73.7865C143.894 74.5927 143.952 75.4565 143.952 76.3778C143.952 83.0575 143.376 89.334 142.224 95.2076C141.072 100.966 139.115 106.033 136.351 110.41C133.702 114.671 130.247 118.068 125.986 120.602C121.724 123.02 116.484 124.23 110.265 124.23C106.004 124.23 101.916 123.596 98 122.329C94.1995 120.947 90.8021 118.759 87.8078 115.765C84.8134 112.655 82.3949 108.624 80.5523 103.672C78.8248 98.605 77.961 92.4436 77.961 85.188C77.961 80.2359 78.4793 74.9382 79.5158 69.295C80.5523 63.5367 82.4525 58.1814 85.2165 53.2293C87.9805 48.2771 91.7234 44.1887 96.4453 40.964C101.282 37.6242 107.444 35.9543 114.93 35.9543C122.646 35.9543 128.807 38.0273 133.414 42.1733C138.136 46.3193 141.303 52.9989 142.915 62.2123C146.946 61.2909 150.574 58.5269 153.798 53.9203C157.138 49.1984 160.305 42.8643 163.3 34.9177L188.176 31.4627ZM115.102 107.991C117.521 107.991 119.594 107.185 121.321 105.573C123.164 103.845 124.661 101.542 125.813 98.6626C126.964 95.6682 127.771 92.1556 128.231 88.1248C128.807 84.094 129.095 79.7176 129.095 74.9958V72.75C124.488 71.7135 122.185 68.3161 122.185 62.5578C122.185 58.8724 123.682 56.4539 126.677 55.3023C125.41 51.6169 123.855 49.1984 122.012 48.0468C120.285 46.8951 118.788 46.3193 117.521 46.3193C114.987 46.3193 112.799 47.5285 110.956 49.947C109.229 52.2504 107.789 55.2447 106.638 58.93C105.486 62.5002 104.622 66.4734 104.046 70.8498C103.586 75.2261 103.355 79.4297 103.355 83.4605C103.355 88.6431 103.701 92.8466 104.392 96.0713C105.198 99.296 106.177 101.772 107.329 103.5C108.48 105.227 109.747 106.436 111.129 107.127C112.511 107.703 113.835 107.991 115.102 107.991Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M239.554 9.52348V36.818H250.092V43.728H239.554V95.5531C239.554 100.39 240.187 103.615 241.454 105.227C242.836 106.724 245.197 107.473 248.537 107.473C251.877 107.473 254.641 106.033 256.829 103.154C259.132 100.275 260.457 96.6471 260.802 92.2708H268.058C267.136 99.296 265.524 104.939 263.221 109.2C260.917 113.346 258.326 116.571 255.447 118.874C252.568 121.062 249.631 122.502 246.637 123.193C243.642 123.884 240.993 124.23 238.69 124.23C229.822 124.23 223.603 121.811 220.033 116.974C216.463 112.022 214.678 105.515 214.678 97.4533V43.728H209.15V36.818H214.678V12.9785L239.554 9.52348Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M258.833 13.8422C258.833 10.0417 260.158 6.81706 262.806 4.16823C265.455 1.40422 268.68 0.0222168 272.48 0.0222168C276.281 0.0222168 279.506 1.40422 282.154 4.16823C284.918 6.81706 286.3 10.0417 286.3 13.8422C286.3 17.6427 284.918 20.8674 282.154 23.5162C279.506 26.1651 276.281 27.4895 272.48 27.4895C268.68 27.4895 265.455 26.1651 262.806 23.5162C260.158 20.8674 258.833 17.6427 258.833 13.8422ZM285.609 36.818V95.5531C285.609 100.39 286.243 103.615 287.51 105.227C288.892 106.724 291.253 107.473 294.592 107.473C296.09 107.473 297.184 107.358 297.875 107.127C298.681 106.897 299.372 106.667 299.948 106.436C300.063 107.012 300.12 107.588 300.12 108.164C300.12 108.74 300.12 109.315 300.12 109.891C300.12 112.77 299.602 115.131 298.566 116.974C297.644 118.817 296.377 120.314 294.765 121.466C293.268 122.502 291.598 123.193 289.755 123.539C288.028 123.999 286.358 124.23 284.746 124.23C275.878 124.23 269.659 121.811 266.089 116.974C262.518 112.022 260.733 105.515 260.733 97.4533V36.818H285.609ZM351.773 107.473C350.391 107.358 349.354 106.897 348.663 106.091C347.972 105.169 347.627 104.133 347.627 102.981C347.627 101.484 348.26 100.045 349.527 98.6626C350.794 97.1654 352.867 96.4168 355.746 96.4168C358.971 96.4168 361.389 97.5109 363.001 99.6991C364.614 101.772 365.42 104.248 365.42 107.127C365.42 108.97 365.074 110.87 364.383 112.828C363.692 114.671 362.598 116.398 361.101 118.011C359.604 119.508 357.761 120.775 355.573 121.811C353.385 122.732 350.851 123.193 347.972 123.193H300.293L334.152 46.1465H321.369C318.835 46.1465 316.704 46.3193 314.977 46.6648C313.365 46.8951 312.558 47.5285 312.558 48.565C312.558 49.0257 312.674 49.256 312.904 49.256C313.249 49.256 313.595 49.3712 313.94 49.6015C314.401 49.8318 314.747 50.2925 314.977 50.9835C315.322 51.6745 315.495 52.8838 315.495 54.6113C315.495 57.1449 314.689 58.9876 313.077 60.1393C311.579 61.2909 309.852 61.8668 307.894 61.8668C305.591 61.8668 303.345 61.1182 301.157 59.621C299.084 58.0087 298.047 55.5902 298.047 52.3655C298.047 50.638 298.393 48.9105 299.084 47.183C299.775 45.3403 300.811 43.6704 302.193 42.1733C303.575 40.5609 305.303 39.2941 307.376 38.3728C309.449 37.3363 311.867 36.818 314.631 36.818H362.138L329.142 109.891C329.833 109.891 330.812 109.949 332.079 110.064C333.346 110.179 334.67 110.294 336.052 110.41C337.55 110.525 338.989 110.64 340.371 110.755C341.868 110.87 343.193 110.928 344.344 110.928C346.417 110.928 348.145 110.697 349.527 110.237C351.024 109.776 351.773 108.855 351.773 107.473Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 flex items-center gap-[20px]">
|
||||
{!!searchParams?.share && (
|
||||
<div>
|
||||
<CopyClient />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
Publication Date:{' '}
|
||||
{dayjs
|
||||
.utc(post[0].createdAt)
|
||||
.local()
|
||||
.format('MMMM D, YYYY h:mm A')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row text-white w-full max-w-[1346px] mx-auto">
|
||||
<div className="flex-1">
|
||||
<div className="gap-[20px] flex flex-col">
|
||||
{post.map((p: any, index: number) => (
|
||||
<div
|
||||
key={String(p.id)}
|
||||
className="relative px-4 py-4 bg-third border border-tableBorder"
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div>
|
||||
<div className="flex shrink-0 rounded-full h-30 w-30 relative">
|
||||
<div className="w-[50px] h-[50px] z-[20]">
|
||||
<img
|
||||
className="w-full h-full relative z-[20] bg-black aspect-square rounded-full border-tableBorder"
|
||||
alt={post[0].integration.name}
|
||||
src={post[0].integration.picture}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -right-[5px] -bottom-[5px] w-[30px] h-[30px] z-[20]">
|
||||
<img
|
||||
className="w-full h-full bg-black aspect-square rounded-full border-tableBorder"
|
||||
alt={post[0].integration.providerIdentifier}
|
||||
src={`/icons/platforms/${post[0].integration.providerIdentifier}.png`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h2 className="text-sm font-semibold">
|
||||
{post[0].integration.name}
|
||||
</h2>
|
||||
<span className="text-sm text-gray-500">
|
||||
@{post[0].integration.profile}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div
|
||||
className="text-sm whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: p.content }}
|
||||
/>
|
||||
<div className="flex w-full gap-[10px]">
|
||||
{JSON.parse(p?.image || '[]').map((p: any) => (
|
||||
<div
|
||||
key={p.name}
|
||||
className="flex-1 rounded-[10px] max-h-[500px] overflow-hidden"
|
||||
>
|
||||
<VideoOrImage isContain={true} src={p.path} autoplay={true} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-96 lg:flex-shrink-0">
|
||||
<div className="p-4 pt-0">
|
||||
<CommentsComponents postId={id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -394,4 +394,25 @@ div div .set-font-family {
|
|||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
opacity: 30%;
|
||||
}
|
||||
|
||||
.loading-shimmer {
|
||||
position: relative;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
.loading-shimmer:before {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
animation: loading 4s linear 0s infinite;
|
||||
filter: blur(0.4px);
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
max-width: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ 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';
|
||||
import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.component';
|
||||
|
||||
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
|
||||
|
||||
|
|
@ -25,11 +26,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
return (
|
||||
<html className={interClass}>
|
||||
<head>
|
||||
<link
|
||||
rel="icon"
|
||||
href="/favicon.ico"
|
||||
sizes="any"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</head>
|
||||
<body className={clsx(chakra.className, 'text-primary dark')}>
|
||||
<VariableContextComponent
|
||||
|
|
@ -44,8 +41,10 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
isGeneral={!!process.env.IS_GENERAL}
|
||||
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
|
||||
tolt={process.env.NEXT_PUBLIC_TOLT!}
|
||||
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
|
||||
>
|
||||
<ToltScript />
|
||||
<FacebookComponent />
|
||||
<Plausible
|
||||
domain={!!process.env.IS_GENERAL ? 'postiz.com' : 'gitroom.com'}
|
||||
>
|
||||
|
|
@ -53,8 +52,10 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
phkey={process.env.NEXT_PUBLIC_POSTHOG_KEY}
|
||||
host={process.env.NEXT_PUBLIC_POSTHOG_HOST}
|
||||
>
|
||||
<UtmSaver />
|
||||
<LayoutContext>{children}</LayoutContext>
|
||||
<LayoutContext>
|
||||
<UtmSaver />
|
||||
{children}
|
||||
</LayoutContext>
|
||||
</PHProvider>
|
||||
</Plausible>
|
||||
</VariableContextComponent>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
|
|
@ -16,6 +16,8 @@ import clsx from 'clsx';
|
|||
import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useTrack } from '@gitroom/react/helpers/use.track';
|
||||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
|
||||
type Inputs = {
|
||||
email: string;
|
||||
|
|
@ -83,10 +85,11 @@ export function RegisterAfter({
|
|||
token: string;
|
||||
provider: string;
|
||||
}) {
|
||||
const {isGeneral} = useVariables();
|
||||
const { isGeneral } = useVariables();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const fireEvents = useFireEvents();
|
||||
const track = useTrack();
|
||||
|
||||
const isAfterProvider = useMemo(() => {
|
||||
return !!token && !!provider;
|
||||
|
|
@ -112,27 +115,33 @@ export function RegisterAfter({
|
|||
await fetchData('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...data }),
|
||||
}).then((response) => {
|
||||
setLoading(false);
|
||||
|
||||
if (response.status === 200) {
|
||||
fireEvents('register')
|
||||
|
||||
if (response.headers.get('activate') === "true") {
|
||||
router.push('/auth/activate');
|
||||
} else {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
} else {
|
||||
form.setError('email', {
|
||||
message: getHelpfulReasonForRegistrationFailure(response.status),
|
||||
});
|
||||
}
|
||||
}).catch(e => {
|
||||
form.setError("email", {
|
||||
message: 'General error: ' + e.toString() + '. Please check your browser console.',
|
||||
});
|
||||
})
|
||||
.then((response) => {
|
||||
setLoading(false);
|
||||
|
||||
if (response.status === 200) {
|
||||
fireEvents('register');
|
||||
return track(TrackEnum.CompleteRegistration).then(() => {
|
||||
if (response.headers.get('activate') === 'true') {
|
||||
router.push('/auth/activate');
|
||||
} else {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
form.setError('email', {
|
||||
message: getHelpfulReasonForRegistrationFailure(response.status),
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
form.setError('email', {
|
||||
message:
|
||||
'General error: ' +
|
||||
e.toString() +
|
||||
'. Please check your browser console.',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -143,7 +152,8 @@ export function RegisterAfter({
|
|||
Sign Up
|
||||
</h1>
|
||||
</div>
|
||||
{!isAfterProvider && (!isGeneral ? <GithubProvider /> : <GoogleProvider />)}
|
||||
{!isAfterProvider &&
|
||||
(!isGeneral ? <GithubProvider /> : <GoogleProvider />)}
|
||||
{!isAfterProvider && (
|
||||
<div className="h-[20px] mb-[24px] mt-[24px] relative">
|
||||
<div className="absolute w-full h-[1px] bg-fifth top-[50%] -translate-y-[50%]" />
|
||||
|
|
@ -198,7 +208,11 @@ export function RegisterAfter({
|
|||
</div>
|
||||
<div className="text-center mt-6">
|
||||
<div className="w-full flex">
|
||||
<Button type="submit" className="flex-1 rounded-[4px]" loading={loading}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1 rounded-[4px]"
|
||||
loading={loading}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,9 @@ import useSWR from 'swr';
|
|||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { MainBillingComponent } from './main.billing.component';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
|
||||
export const BillingComponent = () => {
|
||||
const fetch = useFetch();
|
||||
const searchParams = useSearchParams();
|
||||
const fireEvents = useFireEvents();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('check')) {
|
||||
fireEvents('purchase');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const load = useCallback(async (path: string) => {
|
||||
return await (await fetch(path)).json();
|
||||
|
|
@ -36,7 +26,5 @@ export const BillingComponent = () => {
|
|||
return <LoadingComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MainBillingComponent sub={subscription?.subscription} />
|
||||
);
|
||||
return <MainBillingComponent sub={subscription?.subscription} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,13 +4,28 @@ import { FC, useCallback, useState } from 'react';
|
|||
import clsx from 'clsx';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
|
||||
const useFaqList = () => {
|
||||
const {isGeneral} = useVariables();
|
||||
const { isGeneral } = useVariables();
|
||||
const user = useUser();
|
||||
return [
|
||||
...(user?.allowTrial
|
||||
? [
|
||||
{
|
||||
title: 'Am I going to be charged by Postiz?',
|
||||
description:
|
||||
'To confirm credit card information Postiz will hold $2 and release it immediately',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: `Can I trust ${isGeneral ? 'Postiz' : 'Gitroom'}?`,
|
||||
description: `${isGeneral ? 'Postiz' : 'Gitroom'} is proudly open-source! We believe in an ethical and transparent culture, meaning that ${isGeneral ? 'Postiz' : 'Gitroom'} will live forever. You can check out the entire code or use it for personal projects. To view the open-source repository, <a href="https://github.com/gitroomhq/postiz-app" target="_blank" style="text-decoration: underline;">click here</a>.`,
|
||||
description: `${
|
||||
isGeneral ? 'Postiz' : 'Gitroom'
|
||||
} is proudly open-source! We believe in an ethical and transparent culture, meaning that ${
|
||||
isGeneral ? 'Postiz' : 'Gitroom'
|
||||
} will live forever. You can check out the entire code or use it for personal projects. To view the open-source repository, <a href="https://github.com/gitroomhq/postiz-app" target="_blank" style="text-decoration: underline;">click here</a>.`,
|
||||
},
|
||||
{
|
||||
title: 'What are channels?',
|
||||
|
|
@ -29,7 +44,7 @@ For example, you can schedule your posts on X, Facebook, Instagram, TikTok, YouT
|
|||
description: `We automate ChatGPT to help you write your social posts and articles`,
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const FAQSection: FC<{ title: string; description: string }> = (
|
||||
props
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ 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';
|
||||
import { useTrack } from '@gitroom/react/helpers/use.track';
|
||||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
|
||||
export interface Tiers {
|
||||
month: Array<{
|
||||
|
|
@ -223,6 +225,7 @@ export const MainBillingComponent: FC<{
|
|||
const router = useRouter();
|
||||
const utm = useUtmUrl();
|
||||
const tolt = useTolt();
|
||||
const track = useTrack();
|
||||
|
||||
const [subscription, setSubscription] = useState<Subscription | undefined>(
|
||||
sub
|
||||
|
|
@ -350,12 +353,18 @@ export const MainBillingComponent: FC<{
|
|||
period: monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY',
|
||||
utm,
|
||||
billing,
|
||||
tolt: tolt()
|
||||
tolt: tolt(),
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
if (url) {
|
||||
await track(TrackEnum.InitiateCheckout, {
|
||||
value:
|
||||
pricing[billing][
|
||||
monthlyOrYearly === 'on' ? 'year_price' : 'month_price'
|
||||
],
|
||||
});
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
|
@ -412,13 +421,13 @@ export const MainBillingComponent: FC<{
|
|||
<div>YEARLY</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[16px]">
|
||||
<div className="flex gap-[16px] [@media(max-width:1024px)]:flex-col [@media(max-width:1024px)]:text-center">
|
||||
{Object.entries(pricing)
|
||||
.filter((f) => !isGeneral || f[0] !== 'FREE')
|
||||
.map(([name, values]) => (
|
||||
<div
|
||||
key={name}
|
||||
className="flex-1 bg-sixth border border-customColor6 rounded-[4px] p-[24px] gap-[16px] flex flex-col"
|
||||
className="flex-1 bg-sixth border border-customColor6 rounded-[4px] p-[24px] gap-[16px] flex flex-col [@media(max-width:1024px)]:items-center"
|
||||
>
|
||||
<div className="text-[18px]">{name}</div>
|
||||
<div className="text-[38px] flex gap-[2px] items-center">
|
||||
|
|
@ -472,7 +481,7 @@ export const MainBillingComponent: FC<{
|
|||
.format('D MMM, YYYY')}`
|
||||
: 'Cancel subscription'
|
||||
: // @ts-ignore
|
||||
user?.tier === 'FREE' || user?.tier?.current === 'FREE'
|
||||
(user?.tier === 'FREE' || user?.tier?.current === 'FREE') && user.allowTrial
|
||||
? 'Start 7 days free trial'
|
||||
: 'Purchase'}
|
||||
</Button>
|
||||
|
|
@ -509,7 +518,7 @@ export const MainBillingComponent: FC<{
|
|||
)}
|
||||
{subscription?.cancelAt && isGeneral && (
|
||||
<div className="text-center">
|
||||
Your subscription will be cancel at{' '}
|
||||
Your subscription will be canceled at{' '}
|
||||
{dayjs(subscription.cancelAt).local().format('D MMM, YYYY')}
|
||||
<br />
|
||||
You will never be charged again
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
ClipboardEventHandler,
|
||||
FC,
|
||||
Fragment,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
ClipboardEvent,
|
||||
useState,
|
||||
memo,
|
||||
} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
|
|
@ -52,6 +56,11 @@ import Image from 'next/image';
|
|||
import { weightedLength } from '@gitroom/helpers/utils/count.length';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useClickOutside } from '@gitroom/frontend/components/layout/click.outside';
|
||||
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
|
||||
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
|
||||
|
||||
function countCharacters(text: string, type: string): number {
|
||||
if (type !== 'x') {
|
||||
|
|
@ -64,11 +73,19 @@ function countCharacters(text: string, type: string): number {
|
|||
export const AddEditModal: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
integrations: Integrations[];
|
||||
allIntegrations?: Integrations[];
|
||||
reopenModal: () => void;
|
||||
mutate: () => void;
|
||||
}> = (props) => {
|
||||
const { date, integrations: ints, reopenModal, mutate } = props;
|
||||
onlyValues?: Array<{
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{ id: string; path: string }>;
|
||||
}>;
|
||||
}> = memo((props) => {
|
||||
const { date, integrations: ints, reopenModal, mutate, onlyValues } = props;
|
||||
const [customer, setCustomer] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
// selected integrations to allow edit
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback<
|
||||
|
|
@ -88,10 +105,6 @@ export const AddEditModal: FC<{
|
|||
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
|
||||
|
|
@ -104,7 +117,7 @@ export const AddEditModal: FC<{
|
|||
id?: string;
|
||||
image?: Array<{ id: string; path: string }>;
|
||||
}>
|
||||
>([{ content: '' }]);
|
||||
>(onlyValues ? onlyValues : [{ content: '' }]);
|
||||
|
||||
const fetch = useFetch();
|
||||
|
||||
|
|
@ -265,12 +278,14 @@ export const AddEditModal: FC<{
|
|||
const schedule = useCallback(
|
||||
(type: 'draft' | 'now' | 'schedule' | 'delete') => async () => {
|
||||
if (type === 'delete') {
|
||||
setLoading(true);
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to delete this post?',
|
||||
'Yes, delete it!'
|
||||
))
|
||||
) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await fetch(`/posts/${existingData.group}`, {
|
||||
|
|
@ -294,53 +309,56 @@ export const AddEditModal: FC<{
|
|||
maximumCharacters: values[v].maximumCharacters,
|
||||
}));
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (key.checkValidity) {
|
||||
const check = await key.checkValidity(
|
||||
key?.value.map((p: any) => p.image || []),
|
||||
key.settings
|
||||
);
|
||||
if (typeof check === 'string') {
|
||||
toaster.show(check, 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
key.value.some((p) => {
|
||||
return (
|
||||
countCharacters(p.content, key?.integration?.identifier || '') >
|
||||
(key.maximumCharacters || 1000000)
|
||||
if (type !== 'draft') {
|
||||
for (const key of allKeys) {
|
||||
if (key.checkValidity) {
|
||||
const check = await key.checkValidity(
|
||||
key?.value.map((p: any) => p.image || []),
|
||||
key.settings
|
||||
);
|
||||
})
|
||||
) {
|
||||
if (typeof check === 'string') {
|
||||
toaster.show(check, 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
`${key?.integration?.name} post is too long, it will be cropped, do you want to continue?`,
|
||||
'Yes, continue'
|
||||
))
|
||||
key.value.some((p) => {
|
||||
return (
|
||||
countCharacters(p.content, key?.integration?.identifier || '') >
|
||||
(key.maximumCharacters || 1000000)
|
||||
);
|
||||
})
|
||||
) {
|
||||
await key.trigger();
|
||||
moveToIntegration({
|
||||
identifier: key?.integration?.id!,
|
||||
toPreview: true,
|
||||
});
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
`${key?.integration?.name} post is too long, it will be cropped, do you want to continue?`,
|
||||
'Yes, continue'
|
||||
))
|
||||
) {
|
||||
await key.trigger();
|
||||
moveToIntegration({
|
||||
identifier: key?.integration?.id!,
|
||||
toPreview: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.value.some((p) => !p.content || p.content.length < 6)) {
|
||||
setShowError(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.value.some((p) => !p.content || p.content.length < 6)) {
|
||||
setShowError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.valid) {
|
||||
await key.trigger();
|
||||
moveToIntegration({ identifier: key?.integration?.id! });
|
||||
return;
|
||||
if (!key.valid) {
|
||||
await key.trigger();
|
||||
moveToIntegration({ identifier: key?.integration?.id! });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
await fetch('/posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
|
|
@ -377,6 +395,68 @@ export const AddEditModal: FC<{
|
|||
]
|
||||
);
|
||||
|
||||
const uppy = useUppyUploader({
|
||||
onUploadSuccess: () => {
|
||||
/**empty**/
|
||||
},
|
||||
allowedFileTypes: 'image/*,video/mp4',
|
||||
});
|
||||
|
||||
const pasteImages = useCallback(
|
||||
(index: number, currentValue: any[], isFile?: boolean) => {
|
||||
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
|
||||
// @ts-ignore
|
||||
const clipboardItems = isFile
|
||||
? // @ts-ignore
|
||||
event.map((p) => ({ kind: 'file', getAsFile: () => p }))
|
||||
: // @ts-ignore
|
||||
event.clipboardData?.items; // Ensure clipboardData is available
|
||||
if (!clipboardItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files: File[] = [];
|
||||
|
||||
// @ts-ignore
|
||||
for (const item of clipboardItems) {
|
||||
console.log(item);
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
if (isImage || isVideo) {
|
||||
files.push(file); // Collect images or videos
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
const lastValues = [...currentValue];
|
||||
for (const file of files) {
|
||||
uppy.addFile(file);
|
||||
const upload = await uppy.upload();
|
||||
uppy.clear();
|
||||
if (upload?.successful?.length) {
|
||||
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
|
||||
changeImage(index)({
|
||||
target: {
|
||||
name: 'image',
|
||||
value: [...lastValues],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
setUploading(false);
|
||||
};
|
||||
},
|
||||
[changeImage]
|
||||
);
|
||||
|
||||
const getPostsMarketplace = useCallback(async () => {
|
||||
return (
|
||||
await fetch(`/posts/marketplace/${existingData?.posts?.[0]?.id}`)
|
||||
|
|
@ -406,6 +486,8 @@ export const AddEditModal: FC<{
|
|||
});
|
||||
}, [data, postFor, selectedIntegrations]);
|
||||
|
||||
useClickOutside(askClose);
|
||||
|
||||
return (
|
||||
<>
|
||||
{user?.tier?.ai && (
|
||||
|
|
@ -425,6 +507,11 @@ export const AddEditModal: FC<{
|
|||
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
|
||||
)}
|
||||
>
|
||||
{uploading && (
|
||||
<div className="absolute left-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
|
||||
<LoadingComponent width={100} height={100} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
|
||||
|
|
@ -439,38 +526,44 @@ 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>
|
||||
)}
|
||||
<SelectCustomer
|
||||
integrations={ints}
|
||||
onChange={(val) => {
|
||||
setCustomer(val);
|
||||
setSelectedIntegrations([]);
|
||||
}}
|
||||
/>
|
||||
<DatePicker onChange={setDateState} date={dateState} />
|
||||
{!selectedIntegrations.length && (
|
||||
<svg
|
||||
width="10"
|
||||
height="11"
|
||||
viewBox="0 0 10 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="cursor-pointer"
|
||||
onClick={askClose}
|
||||
>
|
||||
<path
|
||||
d="M9.85403 9.64628C9.90048 9.69274 9.93733 9.74789 9.96247 9.80859C9.98762 9.86928 10.0006 9.93434 10.0006 10C10.0006 10.0657 9.98762 10.1308 9.96247 10.1915C9.93733 10.2522 9.90048 10.3073 9.85403 10.3538C9.80757 10.4002 9.75242 10.4371 9.69173 10.4622C9.63103 10.4874 9.56598 10.5003 9.50028 10.5003C9.43458 10.5003 9.36953 10.4874 9.30883 10.4622C9.24813 10.4371 9.19298 10.4002 9.14653 10.3538L5.00028 6.20691L0.854028 10.3538C0.760208 10.4476 0.63296 10.5003 0.500278 10.5003C0.367596 10.5003 0.240348 10.4476 0.146528 10.3538C0.0527077 10.26 2.61548e-09 10.1327 0 10C-2.61548e-09 9.86735 0.0527077 9.7401 0.146528 9.64628L4.2934 5.50003L0.146528 1.35378C0.0527077 1.25996 0 1.13272 0 1.00003C0 0.867352 0.0527077 0.740104 0.146528 0.646284C0.240348 0.552464 0.367596 0.499756 0.500278 0.499756C0.63296 0.499756 0.760208 0.552464 0.854028 0.646284L5.00028 4.79316L9.14653 0.646284C9.24035 0.552464 9.3676 0.499756 9.50028 0.499756C9.63296 0.499756 9.76021 0.552464 9.85403 0.646284C9.94785 0.740104 10.0006 0.867352 10.0006 1.00003C10.0006 1.13272 9.94785 1.25996 9.85403 1.35378L5.70715 5.50003L9.85403 9.64628Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</TopTitle>
|
||||
|
||||
{!existingData.integration && integrations.length > 1 ? (
|
||||
<PickPlatforms
|
||||
integrations={integrations.filter((f) => !f.disabled)}
|
||||
selectedIntegrations={[]}
|
||||
singleSelect={false}
|
||||
onChange={setSelectedIntegrations}
|
||||
isMain={true}
|
||||
/>
|
||||
<div className="w-full max-w-[600px] overflow-y-auto pb-[10px]">
|
||||
<PickPlatforms
|
||||
toolTip={true}
|
||||
integrations={integrations.filter((f) => !f.disabled)}
|
||||
selectedIntegrations={[]}
|
||||
singleSelect={false}
|
||||
onChange={setSelectedIntegrations}
|
||||
isMain={true}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
|
|
@ -513,23 +606,28 @@ export const AddEditModal: FC<{
|
|||
<div>
|
||||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 editor text-textColor">
|
||||
<Editor
|
||||
order={index}
|
||||
height={value.length > 1 ? 150 : 250}
|
||||
commands={
|
||||
[
|
||||
// ...commands
|
||||
// .getCommands()
|
||||
// .filter((f) => f.name === 'image'),
|
||||
// newImage,
|
||||
// postSelector(dateState),
|
||||
]
|
||||
}
|
||||
value={p.content}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
<DropFiles
|
||||
onDrop={pasteImages(index, p.image || [], true)}
|
||||
>
|
||||
<Editor
|
||||
order={index}
|
||||
height={value.length > 1 ? 150 : 250}
|
||||
commands={
|
||||
[
|
||||
// ...commands
|
||||
// .getCommands()
|
||||
// .filter((f) => f.name === 'image'),
|
||||
// newImage,
|
||||
// postSelector(dateState),
|
||||
]
|
||||
}
|
||||
value={p.content}
|
||||
preview="edit"
|
||||
onPaste={pasteImages(index, p.image || [])}
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
</DropFiles>
|
||||
|
||||
{showError &&
|
||||
(!p.content || p.content.length < 6) && (
|
||||
|
|
@ -540,6 +638,7 @@ export const AddEditModal: FC<{
|
|||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<MultiMediaComponent
|
||||
text={p.content}
|
||||
label="Attachments"
|
||||
description=""
|
||||
value={p.image}
|
||||
|
|
@ -600,9 +699,6 @@ export const AddEditModal: FC<{
|
|||
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
|
||||
updateOrder={updateOrder}
|
||||
postId={existingData?.posts?.[0]?.id}
|
||||
|
|
@ -631,6 +727,7 @@ export const AddEditModal: FC<{
|
|||
className="rounded-[4px] relative group"
|
||||
disabled={
|
||||
selectedIntegrations.length === 0 ||
|
||||
loading ||
|
||||
!canSendForPublication
|
||||
}
|
||||
>
|
||||
|
|
@ -642,6 +739,9 @@ export const AddEditModal: FC<{
|
|||
? 'Submit for order'
|
||||
: !existingData.integration
|
||||
? 'Add to calendar'
|
||||
: // @ts-ignore
|
||||
existingData?.posts?.[0]?.state === 'DRAFT'
|
||||
? 'Schedule'
|
||||
: 'Update'}
|
||||
</div>
|
||||
{!postFor && (
|
||||
|
|
@ -660,7 +760,11 @@ export const AddEditModal: FC<{
|
|||
</svg>
|
||||
<div
|
||||
onClick={postNow}
|
||||
className="hidden group-hover:flex hover:flex flex-col justify-center absolute left-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder"
|
||||
className={clsx(
|
||||
'hidden group-hover:flex hover:flex flex-col justify-center absolute left-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder',
|
||||
loading &&
|
||||
'cursor-not-allowed pointer-events-none opacity-50'
|
||||
)}
|
||||
>
|
||||
Post now
|
||||
</div>
|
||||
|
|
@ -682,16 +786,27 @@ export const AddEditModal: FC<{
|
|||
)}
|
||||
>
|
||||
<div className="mx-[16px]">
|
||||
<TopTitle
|
||||
title=""
|
||||
expend={expend.show}
|
||||
collapse={expend.hide}
|
||||
shouldExpend={expend.expend}
|
||||
/>
|
||||
<TopTitle title="">
|
||||
<svg
|
||||
width="10"
|
||||
height="11"
|
||||
viewBox="0 0 10 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="cursor-pointer"
|
||||
onClick={askClose}
|
||||
>
|
||||
<path
|
||||
d="M9.85403 9.64628C9.90048 9.69274 9.93733 9.74789 9.96247 9.80859C9.98762 9.86928 10.0006 9.93434 10.0006 10C10.0006 10.0657 9.98762 10.1308 9.96247 10.1915C9.93733 10.2522 9.90048 10.3073 9.85403 10.3538C9.80757 10.4002 9.75242 10.4371 9.69173 10.4622C9.63103 10.4874 9.56598 10.5003 9.50028 10.5003C9.43458 10.5003 9.36953 10.4874 9.30883 10.4622C9.24813 10.4371 9.19298 10.4002 9.14653 10.3538L5.00028 6.20691L0.854028 10.3538C0.760208 10.4476 0.63296 10.5003 0.500278 10.5003C0.367596 10.5003 0.240348 10.4476 0.146528 10.3538C0.0527077 10.26 2.61548e-09 10.1327 0 10C-2.61548e-09 9.86735 0.0527077 9.7401 0.146528 9.64628L4.2934 5.50003L0.146528 1.35378C0.0527077 1.25996 0 1.13272 0 1.00003C0 0.867352 0.0527077 0.740104 0.146528 0.646284C0.240348 0.552464 0.367596 0.499756 0.500278 0.499756C0.63296 0.499756 0.760208 0.552464 0.854028 0.646284L5.00028 4.79316L9.14653 0.646284C9.24035 0.552464 9.3676 0.499756 9.50028 0.499756C9.63296 0.499756 9.76021 0.552464 9.85403 0.646284C9.94785 0.740104 10.0006 0.867352 10.0006 1.00003C10.0006 1.13272 9.94785 1.25996 9.85403 1.35378L5.70715 5.50003L9.85403 9.64628Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</TopTitle>
|
||||
</div>
|
||||
{!!selectedIntegrations.length && (
|
||||
<div className="flex-1 flex flex-col p-[16px] pt-0">
|
||||
<ProvidersOptions
|
||||
allIntegrations={props.allIntegrations || []}
|
||||
integrations={selectedIntegrations}
|
||||
editorValue={value}
|
||||
date={dateState}
|
||||
|
|
@ -702,4 +817,4 @@ export const AddEditModal: FC<{
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,8 +38,25 @@ export const AddProviderButton: FC<{ update?: () => void }> = (props) => {
|
|||
const { update } = props;
|
||||
const add = useAddProvider(update);
|
||||
return (
|
||||
<button className="text-white p-[8px] rounded-md bg-forth" onClick={add}>
|
||||
Add Channel
|
||||
<button
|
||||
className="text-white p-[8px] rounded-md bg-forth flex gap-[5px]"
|
||||
onClick={add}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 26C7 26.2652 6.89464 26.5196 6.70711 26.7071C6.51957 26.8946 6.26522 27 6 27C5.73478 27 5.48043 26.8946 5.29289 26.7071C5.10536 26.5196 5 26.2652 5 26C5 25.7348 4.89464 25.4804 4.70711 25.2929C4.51957 25.1054 4.26522 25 4 25C3.73478 25 3.48043 24.8946 3.29289 24.7071C3.10536 24.5196 3 24.2652 3 24C3 23.7348 3.10536 23.4804 3.29289 23.2929C3.48043 23.1054 3.73478 23 4 23C4.79565 23 5.55871 23.3161 6.12132 23.8787C6.68393 24.4413 7 25.2044 7 26ZM4 19C3.73478 19 3.48043 19.1054 3.29289 19.2929C3.10536 19.4804 3 19.7348 3 20C3 20.2652 3.10536 20.5196 3.29289 20.7071C3.48043 20.8946 3.73478 21 4 21C5.32608 21 6.59785 21.5268 7.53553 22.4645C8.47322 23.4021 9 24.6739 9 26C9 26.2652 9.10536 26.5196 9.29289 26.7071C9.48043 26.8946 9.73478 27 10 27C10.2652 27 10.5196 26.8946 10.7071 26.7071C10.8946 26.5196 11 26.2652 11 26C10.998 24.1441 10.2599 22.3648 8.94755 21.0524C7.63523 19.7401 5.85591 19.002 4 19ZM4 15C3.73478 15 3.48043 15.1054 3.29289 15.2929C3.10536 15.4804 3 15.7348 3 16C3 16.2652 3.10536 16.5196 3.29289 16.7071C3.48043 16.8946 3.73478 17 4 17C6.38614 17.0026 8.67378 17.9517 10.361 19.639C12.0483 21.3262 12.9974 23.6139 13 26C13 26.2652 13.1054 26.5196 13.2929 26.7071C13.4804 26.8946 13.7348 27 14 27C14.2652 27 14.5196 26.8946 14.7071 26.7071C14.8946 26.5196 15 26.2652 15 26C14.9967 23.0836 13.8367 20.2877 11.7745 18.2255C9.71234 16.1633 6.91637 15.0033 4 15ZM27 5H5C4.46957 5 3.96086 5.21071 3.58579 5.58579C3.21071 5.96086 3 6.46957 3 7V12C3 12.2652 3.10536 12.5196 3.29289 12.7071C3.48043 12.8946 3.73478 13 4 13C7.4467 13.0036 10.7512 14.3744 13.1884 16.8116C15.6256 19.2488 16.9964 22.5533 17 26C17 26.2652 17.1054 26.5196 17.2929 26.7071C17.4804 26.8946 17.7348 27 18 27H27C27.5304 27 28.0391 26.7893 28.4142 26.4142C28.7893 26.0391 29 25.5304 29 25V7C29 6.46957 28.7893 5.96086 28.4142 5.58579C28.0391 5.21071 27.5304 5 27 5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 text-left">Add Channel</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -191,10 +208,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 +259,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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
import { Button } from '@gitroom/react/form/button';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Loading from 'react-loading';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
||||
const list = [
|
||||
'Realistic',
|
||||
'Cartoon',
|
||||
'Anime',
|
||||
'Fantasy',
|
||||
'Abstract',
|
||||
'Pixel Art',
|
||||
'Sketch',
|
||||
'Watercolor',
|
||||
'Minimalist',
|
||||
'Cyberpunk',
|
||||
'Monochromatic',
|
||||
'Surreal',
|
||||
'Pop Art',
|
||||
'Fantasy Realism',
|
||||
];
|
||||
|
||||
export const AiImage: FC<{
|
||||
value: string;
|
||||
onChange: (params: { id: string; path: string }) => void;
|
||||
}> = (props) => {
|
||||
const { value, onChange } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fetch = useFetch();
|
||||
|
||||
const generateImage = useCallback(
|
||||
(type: string) => async () => {
|
||||
setLoading(true);
|
||||
const image = await (
|
||||
await fetch('/media/generate-image-with-prompt', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: `
|
||||
<!-- description -->
|
||||
${value}
|
||||
<!-- /description -->
|
||||
|
||||
<!-- style -->
|
||||
${type}
|
||||
<!-- /style -->
|
||||
|
||||
`,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
setLoading(false);
|
||||
onChange(image);
|
||||
},
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<Button
|
||||
{...(value.length < 30
|
||||
? {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'Please add at least 30 characters to generate AI image',
|
||||
}
|
||||
: {})}
|
||||
className={clsx(
|
||||
'relative ml-[10px] rounded-[4px] mb-[10px] gap-[8px] !text-primary justify-center items-center flex border border-dashed border-customColor21 bg-input',
|
||||
value.length < 30 && 'opacity-25'
|
||||
)}
|
||||
>
|
||||
{loading && (
|
||||
<div className="absolute left-[50%] -translate-x-[50%]">
|
||||
<Loading height={30} width={30} type="spin" color="#fff" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex gap-[5px] items-center',
|
||||
loading && 'invisible'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] !text-current">AI</div>
|
||||
</div>
|
||||
</Button>
|
||||
{value.length >= 30 && !loading && (
|
||||
<div className="text-[12px] ml-[10px] -mt-[10px] w-[200px] absolute top-[100%] z-[500] left-0 hidden group-hover:block">
|
||||
<ul className="cursor-pointer rounded-[4px] border border-dashed border-customColor21 mt-[3px] p-[5px] bg-customColor2">
|
||||
{list.map((p) => (
|
||||
<li onClick={generateImage(p)} key={p} className="hover:bg-sixth">
|
||||
{p}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { FC, useCallback } from 'react';
|
||||
import { Editor, Transforms } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
const originalMap = {
|
||||
a: '𝗮',
|
||||
b: '𝗯',
|
||||
c: '𝗰',
|
||||
d: '𝗱',
|
||||
e: '𝗲',
|
||||
f: '𝗳',
|
||||
g: '𝗴',
|
||||
h: '𝗵',
|
||||
i: '𝗶',
|
||||
j: '𝗷',
|
||||
k: '𝗸',
|
||||
l: '𝗹',
|
||||
m: '𝗺',
|
||||
n: '𝗻',
|
||||
o: '𝗼',
|
||||
p: '𝗽',
|
||||
q: '𝗾',
|
||||
r: '𝗿',
|
||||
s: '𝘀',
|
||||
t: '𝘁',
|
||||
u: '𝘂',
|
||||
v: '𝘃',
|
||||
w: '𝘄',
|
||||
x: '𝘅',
|
||||
y: '𝘆',
|
||||
z: '𝘇',
|
||||
A: '𝗔',
|
||||
B: '𝗕',
|
||||
C: '𝗖',
|
||||
D: '𝗗',
|
||||
E: '𝗘',
|
||||
F: '𝗙',
|
||||
G: '𝗚',
|
||||
H: '𝗛',
|
||||
I: '𝗜',
|
||||
J: '𝗝',
|
||||
K: '𝗞',
|
||||
L: '𝗟',
|
||||
M: '𝗠',
|
||||
N: '𝗡',
|
||||
O: '𝗢',
|
||||
P: '𝗣',
|
||||
Q: '𝗤',
|
||||
R: '𝗥',
|
||||
S: '𝗦',
|
||||
T: '𝗧',
|
||||
U: '𝗨',
|
||||
V: '𝗩',
|
||||
W: '𝗪',
|
||||
X: '𝗫',
|
||||
Y: '𝗬',
|
||||
Z: '𝗭',
|
||||
'1': '𝟭',
|
||||
'2': '𝟮',
|
||||
'3': '𝟯',
|
||||
'4': '𝟰',
|
||||
'5': '𝟱',
|
||||
'6': '𝟲',
|
||||
'7': '𝟳',
|
||||
'8': '𝟴',
|
||||
'9': '𝟵',
|
||||
'0': '𝟬',
|
||||
};
|
||||
|
||||
export const BoldText: FC<{ editor: any; currentValue: string }> = ({
|
||||
editor,
|
||||
}) => {
|
||||
const mark = () => {
|
||||
const selectedText = Editor.string(editor, editor.selection);
|
||||
|
||||
const newText = (
|
||||
!selectedText ? prompt('What do you want to write?') || '' : selectedText
|
||||
)
|
||||
.split('')
|
||||
// @ts-ignore
|
||||
.map((char) => originalMap?.[char] || char)
|
||||
.join('');
|
||||
|
||||
|
||||
Transforms.insertText(editor, newText);
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={mark}
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_31_12616)">
|
||||
<path
|
||||
d="M14.7686 12.24C15.4192 12.3787 15.9419 12.704 16.3366 13.216C16.7312 13.7173 16.9286 14.2933 16.9286 14.944C16.9286 15.8827 16.5979 16.6293 15.9366 17.184C15.2859 17.728 14.3739 18 13.2006 18H7.96856V6.768H13.0246C14.1659 6.768 15.0566 7.02933 15.6966 7.552C16.3472 8.07467 16.6726 8.784 16.6726 9.68C16.6726 10.3413 16.4966 10.8907 16.1446 11.328C15.8032 11.7653 15.3446 12.0693 14.7686 12.24ZM10.7046 11.312H12.4966C12.9446 11.312 13.2859 11.216 13.5206 11.024C13.7659 10.8213 13.8886 10.528 13.8886 10.144C13.8886 9.76 13.7659 9.46667 13.5206 9.264C13.2859 9.06133 12.9446 8.96 12.4966 8.96H10.7046V11.312ZM12.7206 15.792C13.1792 15.792 13.5312 15.6907 13.7766 15.488C14.0326 15.2747 14.1606 14.9707 14.1606 14.576C14.1606 14.1813 14.0272 13.872 13.7606 13.648C13.5046 13.424 13.1472 13.312 12.6886 13.312H10.7046V15.792H12.7206Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_31_12616">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.25)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -28,8 +28,9 @@ export const CalendarContext = createContext({
|
|||
currentWeek: dayjs().week(),
|
||||
currentYear: dayjs().year(),
|
||||
currentMonth: dayjs().month(),
|
||||
customer: null as string | null,
|
||||
comments: [] as Array<{ date: string; total: number }>,
|
||||
integrations: [] as Integrations[],
|
||||
integrations: [] as (Integrations & { refreshNeeded?: boolean })[],
|
||||
trendings: [] as string[],
|
||||
posts: [] as Array<Post & { integration: Integration }>,
|
||||
reloadCalendarView: () => {
|
||||
|
|
@ -42,6 +43,7 @@ export const CalendarContext = createContext({
|
|||
currentDay: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
currentMonth: number;
|
||||
display: 'week' | 'month' | 'day';
|
||||
customer: string | null;
|
||||
}) => {
|
||||
/** empty **/
|
||||
},
|
||||
|
|
@ -60,12 +62,13 @@ export interface Integrations {
|
|||
type: string;
|
||||
picture: string;
|
||||
changeProfilePicture: boolean;
|
||||
additionalSettings: string;
|
||||
changeNickName: boolean;
|
||||
time: { time: number }[];
|
||||
customer?: {
|
||||
name?: string;
|
||||
id?: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getWeekNumber(date: Date) {
|
||||
|
|
@ -93,6 +96,7 @@ export const CalendarWeekProvider: FC<{
|
|||
const fetch = useFetch();
|
||||
const [internalData, setInternalData] = useState([] as any[]);
|
||||
const [trendings] = useState<string[]>([]);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const display = searchParams.get('display') || 'week';
|
||||
|
|
@ -109,6 +113,7 @@ export const CalendarWeekProvider: FC<{
|
|||
currentWeek: +(searchParams.get('week') || getWeekNumber(new Date())),
|
||||
currentMonth: +(searchParams.get('month') || dayjs().month()),
|
||||
currentYear: +(searchParams.get('year') || dayjs().year()),
|
||||
customer: (searchParams.get('customer') as string) || null,
|
||||
display,
|
||||
});
|
||||
|
||||
|
|
@ -119,6 +124,7 @@ export const CalendarWeekProvider: FC<{
|
|||
week: filters.currentWeek.toString(),
|
||||
month: (filters.currentMonth + 1).toString(),
|
||||
year: filters.currentYear.toString(),
|
||||
customer: filters?.customer?.toString() || '',
|
||||
}).toString();
|
||||
}, [filters, display]);
|
||||
|
||||
|
|
@ -141,6 +147,7 @@ export const CalendarWeekProvider: FC<{
|
|||
currentYear: number;
|
||||
currentMonth: number;
|
||||
display: 'week' | 'month' | 'day';
|
||||
customer: string | null;
|
||||
}) => {
|
||||
setFilters(filters);
|
||||
setInternalData([]);
|
||||
|
|
@ -151,6 +158,7 @@ export const CalendarWeekProvider: FC<{
|
|||
`month=${filters.currentMonth}`,
|
||||
`year=${filters.currentYear}`,
|
||||
`display=${filters.display}`,
|
||||
filters.customer ? `customer=${filters.customer}` : ``,
|
||||
].filter((f) => f);
|
||||
window.history.replaceState(null, '', `/launches?${path.join('&')}`);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, Fragment, useCallback, useMemo } from 'react';
|
||||
import React, {
|
||||
FC, Fragment, memo, useCallback, useEffect, useMemo, useState
|
||||
} from 'react';
|
||||
import {
|
||||
CalendarContext,
|
||||
Integrations,
|
||||
|
|
@ -23,11 +25,12 @@ import { IntegrationContext } from '@gitroom/frontend/components/launches/helper
|
|||
import { PreviewPopup } from '@gitroom/frontend/components/marketplace/special.message';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import { groupBy, sortBy } from 'lodash';
|
||||
import { groupBy, random, sortBy } from 'lodash';
|
||||
import Image from 'next/image';
|
||||
import { extend } from 'dayjs';
|
||||
import { isUSCitizen } from './helpers/isuscitizen.utils';
|
||||
import removeMd from 'remove-markdown';
|
||||
import { useInterval } from '@mantine/hooks';
|
||||
extend(isSameOrAfter);
|
||||
extend(isSameOrBefore);
|
||||
|
||||
|
|
@ -153,7 +156,7 @@ export const WeekView = () => {
|
|||
{convertTimeFormatBasedOnLocality(hour)}
|
||||
</div>
|
||||
{days.map((day, indexDay) => (
|
||||
<Fragment key={`${day}-${hour}`}>
|
||||
<Fragment key={`${currentYear}-${currentWeek}-${day}-${hour}`}>
|
||||
<div className="relative bg-secondary">
|
||||
<CalendarColumn
|
||||
getDate={dayjs()
|
||||
|
|
@ -256,8 +259,10 @@ export const Calendar = () => {
|
|||
export const CalendarColumn: FC<{
|
||||
getDate: dayjs.Dayjs;
|
||||
randomHour?: boolean;
|
||||
}> = (props) => {
|
||||
}> = memo((props) => {
|
||||
const { getDate, randomHour } = props;
|
||||
const [num, setNum] = useState(0);
|
||||
|
||||
const user = useUser();
|
||||
const {
|
||||
integrations,
|
||||
|
|
@ -288,6 +293,22 @@ export const CalendarColumn: FC<{
|
|||
});
|
||||
}, [posts, display, getDate]);
|
||||
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const showAllFunc = useCallback(() => {
|
||||
setShowAll(true);
|
||||
}, []);
|
||||
|
||||
const showLessFunc = useCallback(() => {
|
||||
setShowAll(false);
|
||||
}, []);
|
||||
|
||||
const list = useMemo(() => {
|
||||
if (showAll) {
|
||||
return postList;
|
||||
}
|
||||
return postList.slice(0, 3);
|
||||
}, [postList, showAll]);
|
||||
|
||||
const canBeTrending = useMemo(() => {
|
||||
return !!trendings.find((trend) => {
|
||||
return dayjs
|
||||
|
|
@ -299,7 +320,25 @@ export const CalendarColumn: FC<{
|
|||
|
||||
const isBeforeNow = useMemo(() => {
|
||||
return getDate.startOf('hour').isBefore(dayjs().startOf('hour'));
|
||||
}, [getDate]);
|
||||
}, [getDate, num]);
|
||||
|
||||
const { start, stop } = useInterval(
|
||||
useCallback(() => {
|
||||
if (isBeforeNow) {
|
||||
return;
|
||||
}
|
||||
setNum(num + 1);
|
||||
}, [isBeforeNow]),
|
||||
random(120000, 150000)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
start();
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [{ canDrop }, drop] = useDrop(() => ({
|
||||
accept: 'post',
|
||||
|
|
@ -357,6 +396,7 @@ export const CalendarColumn: FC<{
|
|||
children: (
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
allIntegrations: [],
|
||||
date: dayjs(),
|
||||
integration,
|
||||
value: [],
|
||||
|
|
@ -375,37 +415,56 @@ export const CalendarColumn: FC<{
|
|||
);
|
||||
|
||||
const editPost = useCallback(
|
||||
(post: Post & { integration: Integration }) => async () => {
|
||||
if (user?.orgId === post.submittedForOrganizationId) {
|
||||
return previewPublication(post);
|
||||
}
|
||||
const data = await (await fetch(`/posts/${post.id}`)).json();
|
||||
const publishDate = dayjs.utc(data.posts[0].publishDate).local();
|
||||
(post: Post & { integration: Integration }, isDuplicate?: boolean) =>
|
||||
async () => {
|
||||
if (user?.orgId === post.submittedForOrganizationId) {
|
||||
return previewPublication(post);
|
||||
}
|
||||
const data = await (await fetch(`/posts/${post.id}`)).json();
|
||||
const date = !isDuplicate
|
||||
? null
|
||||
: (await (await fetch('/posts/find-slot')).json()).date;
|
||||
const publishDate = dayjs
|
||||
.utc(date || data.posts[0].publishDate)
|
||||
.local();
|
||||
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
children: (
|
||||
<ExistingDataContextProvider value={data}>
|
||||
<AddEditModal
|
||||
reopenModal={editPost(post)}
|
||||
mutate={reloadCalendarView}
|
||||
integrations={integrations
|
||||
.slice(0)
|
||||
.filter((f) => f.id === data.integration)
|
||||
.map((p) => ({ ...p, picture: data.integrationPicture }))}
|
||||
date={publishDate}
|
||||
/>
|
||||
</ExistingDataContextProvider>
|
||||
),
|
||||
size: '80%',
|
||||
title: ``,
|
||||
});
|
||||
},
|
||||
const ExistingData = !isDuplicate
|
||||
? ExistingDataContextProvider
|
||||
: Fragment;
|
||||
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
children: (
|
||||
<ExistingData value={data}>
|
||||
<AddEditModal
|
||||
{...(isDuplicate ? { onlyValues: data.posts } : {})}
|
||||
allIntegrations={integrations.map((p) => ({ ...p }))}
|
||||
reopenModal={editPost(post)}
|
||||
mutate={reloadCalendarView}
|
||||
integrations={
|
||||
isDuplicate
|
||||
? integrations
|
||||
: integrations
|
||||
.slice(0)
|
||||
.filter((f) => f.id === data.integration)
|
||||
.map((p) => ({
|
||||
...p,
|
||||
picture: data.integrationPicture,
|
||||
}))
|
||||
}
|
||||
date={publishDate}
|
||||
/>
|
||||
</ExistingData>
|
||||
),
|
||||
size: '80%',
|
||||
title: ``,
|
||||
});
|
||||
},
|
||||
[integrations]
|
||||
);
|
||||
|
||||
|
|
@ -419,6 +478,7 @@ export const CalendarColumn: FC<{
|
|||
},
|
||||
children: (
|
||||
<AddEditModal
|
||||
allIntegrations={integrations.map((p) => ({ ...p }))}
|
||||
integrations={integrations.slice(0).map((p) => ({ ...p }))}
|
||||
mutate={reloadCalendarView}
|
||||
date={
|
||||
|
|
@ -456,32 +516,49 @@ export const CalendarColumn: FC<{
|
|||
}
|
||||
: {})}
|
||||
className={clsx(
|
||||
'flex-col text-[12px] pointer w-full overflow-hidden overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
'flex-col text-[12px] pointer w-full flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
isBeforeNow ? 'bg-customColor23 flex-1' : 'cursor-pointer',
|
||||
isBeforeNow && postList.length === 0 && 'col-calendar',
|
||||
canBeTrending && 'bg-customColor24'
|
||||
)}
|
||||
>
|
||||
{postList.map((post) => (
|
||||
{list.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className={clsx(
|
||||
'text-textColor p-[2.5px] relative flex flex-col justify-center items-center'
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full flex flex-col items-center p-[2.5px]">
|
||||
<div className="relative w-full flex flex-col items-center p-[2.5px] h-[66px]">
|
||||
<CalendarItem
|
||||
display={display as 'day' | 'week' | 'month'}
|
||||
isBeforeNow={isBeforeNow}
|
||||
date={getDate}
|
||||
state={post.state}
|
||||
editPost={editPost(post)}
|
||||
editPost={editPost(post, false)}
|
||||
duplicatePost={editPost(post, true)}
|
||||
post={post}
|
||||
integrations={integrations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!showAll && postList.length > 3 && (
|
||||
<div
|
||||
className="text-center hover:underline py-[5px]"
|
||||
onClick={showAllFunc}
|
||||
>
|
||||
+ Show more ({postList.length - 3})
|
||||
</div>
|
||||
)}
|
||||
{showAll && postList.length > 3 && (
|
||||
<div
|
||||
className="text-center hover:underline py-[5px]"
|
||||
onClick={showLessFunc}
|
||||
>
|
||||
- Show less
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(display === 'day'
|
||||
? !isBeforeNow && postList.length === 0
|
||||
|
|
@ -554,18 +631,25 @@ export const CalendarColumn: FC<{
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const CalendarItem: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
isBeforeNow: boolean;
|
||||
editPost: () => void;
|
||||
duplicatePost: () => void;
|
||||
integrations: Integrations[];
|
||||
state: State;
|
||||
display: 'day' | 'week' | 'month';
|
||||
post: Post & { integration: Integration };
|
||||
}> = (props) => {
|
||||
const { editPost, post, date, isBeforeNow, state, display } = props;
|
||||
}> = memo((props) => {
|
||||
const { editPost, duplicatePost, post, date, isBeforeNow, state, display } =
|
||||
props;
|
||||
|
||||
const preview = useCallback(() => {
|
||||
window.open(`/p/` + post.id + '?share=true', '_blank');
|
||||
}, [post]);
|
||||
|
||||
const [{ opacity }, dragRef] = useDrag(
|
||||
() => ({
|
||||
type: 'post',
|
||||
|
|
@ -580,98 +664,53 @@ const CalendarItem: FC<{
|
|||
<div
|
||||
// @ts-ignore
|
||||
ref={dragRef}
|
||||
onClick={editPost}
|
||||
className={clsx(
|
||||
'gap-[5px] w-full flex h-full flex-1 rounded-[10px] border border-seventh px-[5px] p-[2.5px]',
|
||||
'relative',
|
||||
(state === 'DRAFT' || isBeforeNow) && '!grayscale'
|
||||
)}
|
||||
className={clsx('w-full flex h-full flex-1 flex-col group', 'relative')}
|
||||
style={{ opacity }}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative min-w-[20px] h-[20px]',
|
||||
display === 'day' ? 'h-[40px]' : 'h-[20px]'
|
||||
)}
|
||||
>
|
||||
<img
|
||||
className="w-[20px] h-[20px] rounded-full"
|
||||
src={post.integration.picture!}
|
||||
/>
|
||||
<img
|
||||
className="w-[12px] h-[12px] rounded-full absolute z-10 top-[10px] right-0 border border-fifth"
|
||||
src={`/icons/platforms/${post.integration?.providerIdentifier}.png`}
|
||||
/>
|
||||
<div className="bg-forth text-[11px] h-[15px] w-full rounded-tr-[10px] rounded-tl-[10px] flex justify-center gap-[10px] px-[5px]">
|
||||
<div
|
||||
className="hidden group-hover:block hover:underline cursor-pointer"
|
||||
onClick={duplicatePost}
|
||||
>
|
||||
Duplicate
|
||||
</div>
|
||||
<div
|
||||
className="hidden group-hover:block hover:underline cursor-pointer"
|
||||
onClick={preview}
|
||||
>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap line-clamp-3">
|
||||
{state === 'DRAFT' ? 'Draft: ' : ''}
|
||||
{removeMd(post.content).replace(/\n/g, ' ')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommentBox: FC<{ totalComments: number; date: dayjs.Dayjs }> = (
|
||||
props
|
||||
) => {
|
||||
const { totalComments, date } = props;
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const openCommentsModal = useCallback(() => {
|
||||
openModal({
|
||||
children: <CommentComponent date={date} />,
|
||||
withCloseButton: false,
|
||||
onClose() {
|
||||
mutate(`/posts`);
|
||||
},
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
size: '80%',
|
||||
});
|
||||
}, [date]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
totalComments === 0
|
||||
? 'transition-opacity opacity-0 group-hover:opacity-100'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={openCommentsModal}
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content="Add / View comments"
|
||||
onClick={editPost}
|
||||
className={clsx(
|
||||
'group absolute right-0 bottom-0 w-[20px] h-[20px] z-[10] hover:opacity-95 cursor-pointer hover:right-[3px] hover:bottom-[3px] transition-all duration-300 ease-in-out',
|
||||
totalComments === 0 ? 'opacity-50' : 'opacity-95'
|
||||
'gap-[5px] w-full flex h-full flex-1 rounded-br-[10px] rounded-bl-[10px] border border-seventh px-[5px] p-[2.5px]',
|
||||
'relative',
|
||||
isBeforeNow && '!grayscale'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-full h-full group-hover:opacity-100',
|
||||
totalComments === 0 && 'opacity-0'
|
||||
'relative min-w-[20px] h-[20px]',
|
||||
display === 'day' ? 'h-[40px]' : 'h-[20px]'
|
||||
)}
|
||||
>
|
||||
{totalComments > 0 && (
|
||||
<div className="absolute right-0 bottom-[10px] w-[10px] h-[10px] text-[8px] bg-red-500 z-[20] rounded-full flex justify-center items-center text-textColor">
|
||||
{totalComments}
|
||||
</div>
|
||||
)}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
id="comment"
|
||||
>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M25.784 21.017A10.992 10.992 0 0 0 27 16c0-6.065-4.935-11-11-11S5 9.935 5 16s4.935 11 11 11c1.742 0 3.468-.419 5.018-1.215l4.74 1.185a.996.996 0 0 0 .949-.263 1 1 0 0 0 .263-.95l-1.186-4.74zm-2.033.11.874 3.498-3.498-.875a1.006 1.006 0 0 0-.731.098A8.99 8.99 0 0 1 16 25c-4.963 0-9-4.038-9-9s4.037-9 9-9 9 4.038 9 9a8.997 8.997 0 0 1-1.151 4.395.995.995 0 0 0-.098.732z"
|
||||
></path>
|
||||
</svg>
|
||||
<img
|
||||
className="w-[20px] h-[20px] rounded-full"
|
||||
src={post.integration.picture!}
|
||||
/>
|
||||
<img
|
||||
className="w-[12px] h-[12px] rounded-full absolute z-10 top-[10px] right-0 border border-fifth"
|
||||
src={`/icons/platforms/${post.integration?.providerIdentifier}.png`}
|
||||
/>
|
||||
</div>
|
||||
<div className="whitespace-nowrap line-clamp-2">
|
||||
<div className="text-left">{state === 'DRAFT' ? 'Draft: ' : ''}</div>
|
||||
<div className="w-full overflow-hidden overflow-ellipsis text-left">
|
||||
{removeMd(post.content).replace(/\n/g, ' ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-0 w-[0] h-[0] shadow-yellow bg-[rgba(0,0,0,0)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { forwardRef, useCallback, useRef, useState } 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 { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import clsx from 'clsx';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { Transforms } from 'slate';
|
||||
import EmojiPicker from 'emoji-picker-react';
|
||||
import { Theme } from 'emoji-picker-react';
|
||||
import { BoldText } from '@gitroom/frontend/components/launches/bold.text';
|
||||
import { UText } from '@gitroom/frontend/components/launches/u.text';
|
||||
|
||||
export const Editor = forwardRef<
|
||||
RefMDEditor,
|
||||
|
|
@ -20,9 +25,13 @@ export const Editor = forwardRef<
|
|||
ref: React.ForwardedRef<RefMDEditor>
|
||||
) => {
|
||||
const user = useUser();
|
||||
const [id] = useState(makeId(10));
|
||||
const newRef = useRef<any>(null);
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
|
||||
useCopilotReadable({
|
||||
description: 'Content of the post number ' + (props.order + 1),
|
||||
value: props.content,
|
||||
value: props.value,
|
||||
});
|
||||
|
||||
useCopilotAction({
|
||||
|
|
@ -39,26 +48,63 @@ export const Editor = forwardRef<
|
|||
},
|
||||
});
|
||||
|
||||
const addText = useCallback(
|
||||
(emoji: string) => {
|
||||
// @ts-ignore
|
||||
Transforms.insertText(newRef?.current?.editor!, emoji);
|
||||
},
|
||||
[props.value, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative bg-customColor2">
|
||||
{user?.tier?.ai ? (
|
||||
<>
|
||||
<div className="flex gap-[5px] justify-end -mt-[30px]">
|
||||
<UText
|
||||
editor={newRef?.current?.editor!}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<BoldText
|
||||
editor={newRef?.current?.editor!}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<div
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
|
||||
onClick={() => setEmojiPickerOpen(!emojiPickerOpen)}
|
||||
>
|
||||
😀
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute z-[200] right-0">
|
||||
<EmojiPicker
|
||||
theme={(localStorage.getItem('mode') as Theme) || Theme.DARK}
|
||||
onEmojiClick={(e) => {
|
||||
addText(e.emoji);
|
||||
setEmojiPickerOpen(false);
|
||||
}}
|
||||
open={emojiPickerOpen}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative bg-customColor2" id={id}>
|
||||
<CopilotTextarea
|
||||
disableBranding={true}
|
||||
ref={newRef}
|
||||
className={clsx(
|
||||
'!min-h-40 !max-h-80 p-2 overflow-hidden bg-customColor2 outline-none'
|
||||
'!min-h-40 !max-h-80 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none'
|
||||
)}
|
||||
value={props.value}
|
||||
onChange={(e) => props?.onChange?.(e.target.value)}
|
||||
onChange={(e) => {
|
||||
props?.onChange?.(e.target.value);
|
||||
}}
|
||||
onPaste={props.onPaste}
|
||||
placeholder="Write your reply..."
|
||||
autosuggestionsConfig={{
|
||||
textareaPurpose: `Assist me in writing social media posts.`,
|
||||
chatApiConfigs: {},
|
||||
disabled: !user?.tier?.ai,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MDEditor {...props} ref={ref} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
|||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
import { isUSCitizen } from './helpers/isuscitizen.utils';
|
||||
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
|
||||
|
||||
export const Filters = () => {
|
||||
const week = useCalendar();
|
||||
|
|
@ -13,30 +14,30 @@ export const Filters = () => {
|
|||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
: week.display === 'week'
|
||||
? dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.startOf('isoWeek')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.endOf('isoWeek')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
: dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.startOf('month')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.endOf('month')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY');
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY');
|
||||
|
||||
const setDay = useCallback(() => {
|
||||
week.setFilters({
|
||||
|
|
@ -45,6 +46,7 @@ export const Filters = () => {
|
|||
currentYear: dayjs().year(),
|
||||
currentMonth: dayjs().month(),
|
||||
display: 'day',
|
||||
customer: week.customer,
|
||||
});
|
||||
}, [week]);
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ export const Filters = () => {
|
|||
currentYear: dayjs().year(),
|
||||
currentMonth: dayjs().month(),
|
||||
display: 'week',
|
||||
customer: week.customer,
|
||||
});
|
||||
}, [week]);
|
||||
|
||||
|
|
@ -65,9 +68,24 @@ export const Filters = () => {
|
|||
currentWeek: dayjs().isoWeek(),
|
||||
currentYear: dayjs().year(),
|
||||
display: 'month',
|
||||
customer: week.customer,
|
||||
});
|
||||
}, [week]);
|
||||
|
||||
const setCustomer = useCallback(
|
||||
(customer: string) => {
|
||||
week.setFilters({
|
||||
currentDay: week.currentDay,
|
||||
currentMonth: week.currentMonth,
|
||||
currentWeek: week.currentWeek,
|
||||
currentYear: week.currentYear,
|
||||
display: week.display as any,
|
||||
customer: customer,
|
||||
});
|
||||
},
|
||||
[week]
|
||||
);
|
||||
|
||||
const next = useCallback(() => {
|
||||
const increaseDay = week.display === 'day';
|
||||
const increaseWeek =
|
||||
|
|
@ -77,6 +95,7 @@ export const Filters = () => {
|
|||
week.display === 'month' || (increaseWeek && week.currentWeek === 52);
|
||||
|
||||
week.setFilters({
|
||||
customer: week.customer,
|
||||
currentDay: (!increaseDay
|
||||
? 0
|
||||
: week.currentDay === 6
|
||||
|
|
@ -116,6 +135,7 @@ export const Filters = () => {
|
|||
week.display === 'month' || (decreaseWeek && week.currentWeek === 1);
|
||||
|
||||
week.setFilters({
|
||||
customer: week.customer,
|
||||
currentDay: (!decreaseDay
|
||||
? 0
|
||||
: week.currentDay === 0
|
||||
|
|
@ -147,77 +167,82 @@ export const Filters = () => {
|
|||
]);
|
||||
return (
|
||||
<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 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>
|
||||
<SelectCustomer
|
||||
customer={week.customer as string}
|
||||
onChange={(customer: string) => setCustomer(customer)}
|
||||
integrations={week.integrations}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props)
|
|||
{value.images.map((image, index) => (
|
||||
<a
|
||||
key={`image_${index}`}
|
||||
className="flex-1 h-[270px]"
|
||||
className="flex-1"
|
||||
href={mediaDir.set(image.path)}
|
||||
target="_blank"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,193 +1,30 @@
|
|||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import {
|
||||
Step,
|
||||
StepSpace,
|
||||
} from '@gitroom/frontend/components/onboarding/onboarding';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import dayjs from 'dayjs';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { PostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { Checkbox } from '@gitroom/react/form/checkbox';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
CalendarWeekProvider,
|
||||
useCalendar,
|
||||
} from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
|
||||
import dayjs from 'dayjs';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
|
||||
const ThirdStep: FC<{ week: number; year: number }> = (props) => {
|
||||
const { week, year } = props;
|
||||
|
||||
const gotToPosts = useCallback(() => {
|
||||
window.location.href = `/launches?week=${week}&year=${year}`;
|
||||
}, [week, year]);
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[20px] mb-[20px] flex flex-col items-center justify-center text-center mt-[20px] gap-[20px]">
|
||||
<img src="/success.svg" alt="success" />
|
||||
Your posts have been scheduled as drafts.
|
||||
<br />
|
||||
<Button onClick={gotToPosts}>Click here to see them</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SecondStep: FC<{
|
||||
posts: Array<Array<{ post: string }>>;
|
||||
url: string;
|
||||
postId?: string;
|
||||
nextStep: (params: { week: number; year: number }) => void;
|
||||
}> = (props) => {
|
||||
const { posts, nextStep, url, postId } = props;
|
||||
const fetch = useFetch();
|
||||
const [selected, setSelected] = useState<Array<string>>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
values: {
|
||||
date: dayjs().week() + '_' + dayjs().year(),
|
||||
},
|
||||
});
|
||||
|
||||
const addPost = useCallback(
|
||||
(index: string) => () => {
|
||||
if (selected.includes(index)) {
|
||||
setSelected(selected.filter((i) => i !== index));
|
||||
return;
|
||||
}
|
||||
setSelected([...selected, index]);
|
||||
},
|
||||
[selected]
|
||||
);
|
||||
|
||||
const list = useMemo(() => {
|
||||
const currentDate = dayjs();
|
||||
return [...new Array(52)].map((_, i) => {
|
||||
const week = currentDate.add(i, 'week');
|
||||
return {
|
||||
value: week.week() + '_' + week.year(),
|
||||
label: `Week #${week.week()} (${week
|
||||
.startOf('isoWeek')
|
||||
.format('YYYY-MM-DD')} - ${week
|
||||
.endOf('isoWeek')
|
||||
.format('YYYY-MM-DD')})`,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createPosts: SubmitHandler<{
|
||||
date: any;
|
||||
}> = useCallback(
|
||||
async (values) => {
|
||||
setLoading(true);
|
||||
await fetch('/posts/generator/draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
posts: posts
|
||||
.filter((_, index) => selected.includes(String(index)))
|
||||
.map((po) => ({ list: po })),
|
||||
url,
|
||||
postId: postId ? `(post:${postId})` : undefined,
|
||||
year: values.date.year,
|
||||
week: values.date.week,
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
nextStep({
|
||||
week: values.date.week,
|
||||
year: values.date.year,
|
||||
});
|
||||
},
|
||||
[selected, postId, url]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(createPosts)}>
|
||||
<FormProvider {...form}>
|
||||
<div className={loading ? 'opacity-75' : ''}>
|
||||
<Select
|
||||
label="Select a week"
|
||||
name="date"
|
||||
extraForm={{
|
||||
setValueAs: (value) => {
|
||||
const [week, year] = value.split('_');
|
||||
return { week: +week, year: +year };
|
||||
},
|
||||
}}
|
||||
>
|
||||
{list.map((item) => (
|
||||
<option value={item.value} key={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="text-[20px] mb-[20px]">
|
||||
Click on the posts you would like to schedule.
|
||||
<br />
|
||||
They will be saved as drafts and you can edit them later.
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-[25px] select-none cursor-pointer">
|
||||
{posts.map((post, index) => (
|
||||
<div
|
||||
onClick={addPost(String(index))}
|
||||
className={clsx(
|
||||
'flex flex-col h-[200px] border rounded-[4px] group hover:border-white relative',
|
||||
selected.includes(String(index))
|
||||
? 'border-white'
|
||||
: 'border-fifth'
|
||||
)}
|
||||
key={post[0].post}
|
||||
>
|
||||
{post.length > 1 && (
|
||||
<div className="bg-forth absolute -left-[15px] -top-[15px] z-[100] p-[3px] rounded-[10px]">
|
||||
a thread
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 relative h-full w-full group-hover:bg-black',
|
||||
selected.includes(String(index)) && 'bg-black'
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-0 top-0 w-full h-full p-[16px]">
|
||||
<div className="w-full h-full overflow-hidden text-ellipsis group-hover:bg-black resize-none outline-none">
|
||||
{post[0].post.split('\n\n')[0]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-[20px] flex justify-end">
|
||||
<Button type="submit" disabled={!selected.length} loading={loading}>
|
||||
Create posts
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const FirstStep: FC<{
|
||||
nextStep: (
|
||||
posts: Array<Array<{ post: string }>>,
|
||||
url: string,
|
||||
postId?: string
|
||||
) => void;
|
||||
}> = (props) => {
|
||||
const { nextStep } = props;
|
||||
const FirstStep: FC = (props) => {
|
||||
const { integrations, reloadCalendarView } = useCalendar();
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showStep, setShowStep] = useState('');
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(GeneratorDto);
|
||||
}, []);
|
||||
|
|
@ -196,52 +33,131 @@ const FirstStep: FC<{
|
|||
mode: 'all',
|
||||
resolver,
|
||||
values: {
|
||||
url: '',
|
||||
post: undefined as undefined | string,
|
||||
research: '',
|
||||
isPicture: false,
|
||||
format: 'one_short',
|
||||
tone: 'personal',
|
||||
},
|
||||
});
|
||||
|
||||
const [url, post] = form.watch(['url', 'post']);
|
||||
const [research] = form.watch(['research']);
|
||||
|
||||
const makeSelect = useCallback(
|
||||
(post?: string) => {
|
||||
form.setValue('post', post?.split?.(':')[1]?.split(')')?.[0]);
|
||||
const generateStep = useCallback(
|
||||
async (reader: ReadableStreamDefaultReader) => {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
if (!post && !url) {
|
||||
form.setError('url', {
|
||||
message: 'You need to select a post or a URL',
|
||||
});
|
||||
return;
|
||||
let lastResponse = {} as any;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) return lastResponse.data.output;
|
||||
|
||||
// Convert chunked binary data to string
|
||||
const chunkStr = decoder.decode(value, { stream: true });
|
||||
for (const chunk of chunkStr
|
||||
.split('\n')
|
||||
.filter((f) => f && f.indexOf('{') > -1)) {
|
||||
try {
|
||||
const data = JSON.parse(chunk);
|
||||
switch (data.name) {
|
||||
case 'agent':
|
||||
setShowStep('Agent starting');
|
||||
break;
|
||||
case 'research':
|
||||
setShowStep('Researching your content...');
|
||||
break;
|
||||
case 'find-category':
|
||||
setShowStep('Understanding the category...');
|
||||
break;
|
||||
case 'find-topic':
|
||||
setShowStep('Finding the topic...');
|
||||
break;
|
||||
case 'find-popular-posts':
|
||||
setShowStep('Finding popular posts to match with...');
|
||||
break;
|
||||
case 'generate-hook':
|
||||
setShowStep('Generating hook...');
|
||||
break;
|
||||
case 'generate-content':
|
||||
setShowStep('Generating content...');
|
||||
break;
|
||||
case 'generate-picture':
|
||||
setShowStep('Generating pictures...');
|
||||
break;
|
||||
case 'upload-pictures':
|
||||
setShowStep('Uploading pictures...');
|
||||
break;
|
||||
case 'post-time':
|
||||
setShowStep('Finding time to post...');
|
||||
break;
|
||||
}
|
||||
lastResponse = data;
|
||||
} catch (e) {
|
||||
/** don't do anything **/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (post && url) {
|
||||
form.setError('url', {
|
||||
message: 'You can only have a URL or a post',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
form.setError('url', {
|
||||
message: '',
|
||||
});
|
||||
},
|
||||
[post, url]
|
||||
[]
|
||||
);
|
||||
|
||||
const onSubmit: SubmitHandler<{
|
||||
url: string;
|
||||
post: string | undefined;
|
||||
}> = useCallback(async (value) => {
|
||||
setLoading(true);
|
||||
const data = await (
|
||||
await fetch('/posts/generator', {
|
||||
research: string;
|
||||
}> = useCallback(
|
||||
async (value) => {
|
||||
setLoading(true);
|
||||
const response = await fetch('/posts/generator', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(value),
|
||||
})
|
||||
).json();
|
||||
nextStep(data.list, value.url, value.post);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
});
|
||||
|
||||
if (!response.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const load = await generateStep(reader);
|
||||
|
||||
const messages = load.content.map((p: any, index: number) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
content: load.hook + '\n' + p.content,
|
||||
...(p?.image?.path ? { image: [p.image] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: p.content,
|
||||
...(p?.image?.path ? { image: [p.image] } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
setShowStep('');
|
||||
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
children: (
|
||||
<AddEditModal
|
||||
allIntegrations={integrations.map((p) => ({ ...p }))}
|
||||
integrations={integrations.slice(0).map((p) => ({ ...p }))}
|
||||
mutate={reloadCalendarView}
|
||||
date={dayjs.utc(load.date).local()}
|
||||
reopenModal={() => ({})}
|
||||
onlyValues={messages}
|
||||
/>
|
||||
),
|
||||
size: '80%',
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
},
|
||||
[integrations, reloadCalendarView]
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
|
|
@ -250,31 +166,61 @@ const FirstStep: FC<{
|
|||
>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex flex-col">
|
||||
<div className="p-[20px] border border-fifth rounded-[4px]">
|
||||
<div className="pb-[10px] rounded-[4px]">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<Input label="URL" {...form.register('url')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col-reverse">
|
||||
<div className="p-[16px] bg-input border-fifth border rounded-[4px] min-h-[500px] empty:hidden">
|
||||
<PostSelector
|
||||
noModal={true}
|
||||
onClose={() => null}
|
||||
onSelect={makeSelect}
|
||||
date={dayjs().add(1, 'year')}
|
||||
only="article"
|
||||
{!showStep ? (
|
||||
<div className="loading-shimmer pb-[10px]"> </div>
|
||||
) : (
|
||||
<div
|
||||
className="loading-shimmer pb-[10px]"
|
||||
data-text={showStep}
|
||||
>
|
||||
{showStep}
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
label="Write anything"
|
||||
disabled={loading}
|
||||
placeholder="You can write anything you want, and also add links, we will do the research for you..."
|
||||
{...form.register('research')}
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-[10px] existing-empty">
|
||||
Or select from exising posts
|
||||
<Select label="Output format" {...form.register('format')}>
|
||||
<option value="one_short">Short post</option>
|
||||
<option value="one_long">Long post</option>
|
||||
<option value="thread_short">
|
||||
A thread with short posts
|
||||
</option>
|
||||
<option value="thread_long">A thread with long posts</option>
|
||||
</Select>
|
||||
<Select label="Output format" {...form.register('tone')}>
|
||||
<option value="personal">
|
||||
Personal voice ({'"'}I am happy to announce{'"'})
|
||||
</option>
|
||||
<option value="company">
|
||||
Company voice ({'"'}We are happy to announce{'"'})
|
||||
</option>
|
||||
</Select>
|
||||
<div
|
||||
className={clsx('flex items-center', loading && 'opacity-50')}
|
||||
>
|
||||
<Checkbox
|
||||
disabled={loading}
|
||||
{...form.register('isPicture')}
|
||||
label="Add pictures?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[20px] flex justify-end">
|
||||
<Button type="submit" disabled={!!(url && post)} loading={loading}>
|
||||
{url && post ? "You can't have both URL and a POST" : 'Next'}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={research.length < 10}
|
||||
loading={loading}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</FormProvider>
|
||||
|
|
@ -282,28 +228,14 @@ const FirstStep: FC<{
|
|||
);
|
||||
};
|
||||
export const GeneratorPopup = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const modals = useModals();
|
||||
const [posts, setPosts] = useState<
|
||||
| {
|
||||
posts: Array<Array<{ post: string }>>;
|
||||
url: string;
|
||||
postId?: string;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
const [yearAndWeek, setYearAndWeek] = useState<{
|
||||
year: number;
|
||||
week: number;
|
||||
} | null>(null);
|
||||
|
||||
const closeAll = useCallback(() => {
|
||||
modals.closeAll();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-sixth p-[32px] w-full max-w-[920px] mx-auto flex flex-col gap-[24px] rounded-[4px] border border-customColor6 relative">
|
||||
<div className="bg-sixth p-[32px] w-full max-w-[920px] mx-auto flex flex-col rounded-[4px] border border-customColor6 relative">
|
||||
<button
|
||||
onClick={closeAll}
|
||||
className="outline-none absolute right-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
|
|
@ -325,37 +257,7 @@ export const GeneratorPopup = () => {
|
|||
</svg>
|
||||
</button>
|
||||
<h1 className="text-[24px]">Generate Posts</h1>
|
||||
<div className="flex">
|
||||
<Step title="Generate posts" step={1} currentStep={step} lastStep={3} />
|
||||
<StepSpace />
|
||||
<Step title="Confirm posts" step={2} currentStep={step} lastStep={3} />
|
||||
<StepSpace />
|
||||
<Step title="Done" step={3} currentStep={step} lastStep={3} />
|
||||
</div>
|
||||
{step === 1 && (
|
||||
<FirstStep
|
||||
nextStep={(posts, url: string, postId?: string) => {
|
||||
setPosts({
|
||||
posts,
|
||||
url,
|
||||
postId,
|
||||
});
|
||||
setStep(2);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<SecondStep
|
||||
{...posts!}
|
||||
nextStep={(e) => {
|
||||
setYearAndWeek(e);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<ThirdStep week={yearAndWeek?.week!} year={yearAndWeek?.year!} />
|
||||
)}
|
||||
<FirstStep />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -363,6 +265,7 @@ export const GeneratorComponent = () => {
|
|||
const user = useUser();
|
||||
const router = useRouter();
|
||||
const modal = useModals();
|
||||
const all = useCalendar();
|
||||
|
||||
const generate = useCallback(async () => {
|
||||
if (!user?.tier?.ai) {
|
||||
|
|
@ -385,13 +288,17 @@ export const GeneratorComponent = () => {
|
|||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
size: '100%',
|
||||
children: <GeneratorPopup />,
|
||||
children: (
|
||||
<CalendarWeekProvider {...all}>
|
||||
<GeneratorPopup />
|
||||
</CalendarWeekProvider>
|
||||
),
|
||||
});
|
||||
}, [user]);
|
||||
}, [user, all]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="text-textColor p-[8px] rounded-md bg-red-700 flex justify-center items-center gap-[5px] outline-none"
|
||||
className="p-[8px] rounded-md bg-red-700 flex justify-center items-center gap-[5px] outline-none text-white"
|
||||
onClick={generate}
|
||||
>
|
||||
<svg
|
||||
|
|
@ -413,7 +320,7 @@ export const GeneratorComponent = () => {
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Generate Posts
|
||||
<div className="flex-1 text-left">Generate Posts</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export const DatePicker: FC<{
|
|||
}
|
||||
|
||||
if (modifiers.selected) {
|
||||
return '!text-textColor !bg-seventh !outline-none';
|
||||
return '!text-white !bg-seventh !outline-none';
|
||||
}
|
||||
|
||||
return '!text-textColor';
|
||||
|
|
@ -95,7 +95,7 @@ export const DatePicker: FC<{
|
|||
defaultValue={date.toDate()}
|
||||
/>
|
||||
<Button className="mt-[12px]" onClick={changeShow}>
|
||||
Close
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import Image from 'next/image';
|
|||
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
|
||||
import { useStateCallback } from '@gitroom/react/helpers/use.state.callback';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const PickPlatforms: FC<{
|
||||
integrations: Integrations[];
|
||||
|
|
@ -16,6 +15,7 @@ export const PickPlatforms: FC<{
|
|||
singleSelect: boolean;
|
||||
hide?: boolean;
|
||||
isMain: boolean;
|
||||
toolTip?: boolean;
|
||||
}> = (props) => {
|
||||
const { hide, isMain, integrations, selectedIntegrations, onChange } = props;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -65,7 +65,7 @@ export const PickPlatforms: FC<{
|
|||
useMoveToIntegrationListener(
|
||||
[integrations],
|
||||
props.singleSelect,
|
||||
({identifier, toPreview}: {identifier: string, toPreview: boolean}) => {
|
||||
({ identifier, toPreview }: { identifier: string; toPreview: boolean }) => {
|
||||
const findIntegration = integrations.find((p) => p.id === identifier);
|
||||
|
||||
if (findIntegration) {
|
||||
|
|
@ -146,13 +146,6 @@ export const PickPlatforms: FC<{
|
|||
);
|
||||
|
||||
const handler = async ({ integrationsId }: { integrationsId: string[] }) => {
|
||||
console.log(
|
||||
'setSelectedIntegration',
|
||||
integrations,
|
||||
integrationsId,
|
||||
dayjs().unix()
|
||||
);
|
||||
|
||||
const selected = selectedIntegrations.map((p) => p.id);
|
||||
const notToRemove = selected.filter((p) => integrationsId.includes(p));
|
||||
const toAdd = integrationsId.filter((p) => !selected.includes(p));
|
||||
|
|
@ -162,11 +155,11 @@ export const PickPlatforms: FC<{
|
|||
.filter((p) => p);
|
||||
|
||||
setSelectedAccounts(newIntegrations, () => {
|
||||
console.log('changed')
|
||||
console.log('changed');
|
||||
});
|
||||
|
||||
onChange(newIntegrations, () => {
|
||||
console.log('changed')
|
||||
console.log('changed');
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -224,7 +217,7 @@ export const PickPlatforms: FC<{
|
|||
>
|
||||
<path
|
||||
d="M10.3541 12.6463C10.4006 12.6927 10.4374 12.7479 10.4626 12.8086C10.4877 12.8693 10.5007 12.9343 10.5007 13C10.5007 13.0657 10.4877 13.1308 10.4626 13.1915C10.4374 13.2522 10.4006 13.3073 10.3541 13.3538C10.3077 13.4002 10.2525 13.4371 10.1918 13.4622C10.1311 13.4874 10.0661 13.5003 10.0004 13.5003C9.9347 13.5003 9.86964 13.4874 9.80894 13.4622C9.74825 13.4371 9.6931 13.4002 9.64664 13.3538L4.64664 8.35378C4.60015 8.30735 4.56328 8.2522 4.53811 8.1915C4.51295 8.13081 4.5 8.06574 4.5 8.00003C4.5 7.93433 4.51295 7.86926 4.53811 7.80856C4.56328 7.74786 4.60015 7.69272 4.64664 7.64628L9.64664 2.64628C9.74046 2.55246 9.86771 2.49976 10.0004 2.49976C10.1331 2.49976 10.2603 2.55246 10.3541 2.64628C10.448 2.7401 10.5007 2.86735 10.5007 3.00003C10.5007 3.13272 10.448 3.25996 10.3541 3.35378L5.70727 8.00003L10.3541 12.6463Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
|
|
@ -253,6 +246,10 @@ export const PickPlatforms: FC<{
|
|||
<div
|
||||
key={integration.id}
|
||||
className="flex gap-[8px] items-center mr-[10px]"
|
||||
{...(props.toolTip && {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content': integration.name,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onClick={addPlatform(integration)}
|
||||
|
|
@ -342,7 +339,7 @@ export const PickPlatforms: FC<{
|
|||
>
|
||||
<path
|
||||
d="M5.64586 12.6463C5.5994 12.6927 5.56255 12.7479 5.53741 12.8086C5.51227 12.8693 5.49933 12.9343 5.49933 13C5.49933 13.0657 5.51227 13.1308 5.53741 13.1915C5.56255 13.2522 5.5994 13.3073 5.64586 13.3538C5.69231 13.4002 5.74746 13.4371 5.80816 13.4622C5.86886 13.4874 5.93391 13.5003 5.99961 13.5003C6.0653 13.5003 6.13036 13.4874 6.19106 13.4622C6.25175 13.4371 6.3069 13.4002 6.35336 13.3538L11.3534 8.35378C11.3998 8.30735 11.4367 8.2522 11.4619 8.1915C11.487 8.13081 11.5 8.06574 11.5 8.00003C11.5 7.93433 11.487 7.86926 11.4619 7.80856C11.4367 7.74786 11.3998 7.69272 11.3534 7.64628L6.35336 2.64628C6.25954 2.55246 6.13229 2.49976 5.99961 2.49976C5.86692 2.49976 5.73968 2.55246 5.64586 2.64628C5.55204 2.7401 5.49933 2.86735 5.49933 3.00003C5.49933 3.13272 5.55204 3.25996 5.64586 3.35378L10.2927 8.00003L5.64586 12.6463Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import dayjs from 'dayjs';
|
|||
export const IntegrationContext = createContext<{
|
||||
date: dayjs.Dayjs;
|
||||
integration: Integrations | undefined;
|
||||
allIntegrations: Integrations[];
|
||||
value: Array<{
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{ path: string; id: string }>;
|
||||
}>;
|
||||
}>({ integration: undefined, value: [], date: dayjs() });
|
||||
}>({ integration: undefined, value: [], date: dayjs(), allIntegrations: [] });
|
||||
|
||||
export const useIntegration = () => useContext(IntegrationContext);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import {
|
||||
Integrations,
|
||||
useCalendar,
|
||||
} from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { Slider } from '@gitroom/react/form/slider';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const delayOptions = [
|
||||
{
|
||||
name: 'Immediately',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: '1 hour',
|
||||
value: 3600000,
|
||||
},
|
||||
{
|
||||
name: '2 hours',
|
||||
value: 7200000,
|
||||
},
|
||||
{
|
||||
name: '3 hours',
|
||||
value: 10800000,
|
||||
},
|
||||
{
|
||||
name: '8 hours',
|
||||
value: 28800000,
|
||||
},
|
||||
{
|
||||
name: '12 hours',
|
||||
value: 43200000,
|
||||
},
|
||||
{
|
||||
name: '15 hours',
|
||||
value: 54000000,
|
||||
},
|
||||
{
|
||||
name: '24 hours',
|
||||
value: 86400000,
|
||||
},
|
||||
];
|
||||
export const InternalChannels: FC<{
|
||||
plugs: {
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string;
|
||||
pickIntegration: string[];
|
||||
fields: {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
validation?: RegExp;
|
||||
}[];
|
||||
}[];
|
||||
}> = (props) => {
|
||||
const { plugs } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{plugs.map((plug, index) => (
|
||||
<Plug plug={plug} key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Plug: FC<{
|
||||
plug: {
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string;
|
||||
pickIntegration: string[];
|
||||
fields: {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
validation?: RegExp;
|
||||
}[];
|
||||
};
|
||||
}> = ({ plug }) => {
|
||||
const { allIntegrations, integration } = useIntegration();
|
||||
const { watch, setValue, control, register } = useSettings();
|
||||
const val = watch(`plug--${plug.identifier}--integrations`);
|
||||
const active = watch(`plug--${plug.identifier}--active`);
|
||||
|
||||
// const selectedIntegrationsValue = watch(
|
||||
// `plug.${plug.identifier}.integrations`
|
||||
// );
|
||||
//
|
||||
// console.log(selectedIntegrationsValue);
|
||||
const [localValue, setLocalValue] = useState<Integrations[]>(
|
||||
(val || []).map((p: any) => ({ ...p }))
|
||||
);
|
||||
useEffect(() => {
|
||||
setValue(`plug--${plug.identifier}--integrations`, [...localValue]);
|
||||
}, [localValue, plug, setValue]);
|
||||
|
||||
const [allowedIntegrations] = useState(
|
||||
allIntegrations.filter(
|
||||
(i) =>
|
||||
plug.pickIntegration.includes(i.identifier) && integration?.id !== i.id
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plug.title}
|
||||
className="flex flex-col gap-[10px] border-tableBorder border p-[15px] rounded-lg"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1">{plug.title}</div>
|
||||
<div>
|
||||
<Slider
|
||||
value={active ? 'on' : 'off'}
|
||||
onChange={(p) =>
|
||||
setValue(`plug--${plug.identifier}--active`, p === 'on')
|
||||
}
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-[600px] overflow-y-auto pb-[10px] text-[12px] flex flex-col gap-[10px]">
|
||||
{!allowedIntegrations.length ? (
|
||||
'No available accounts'
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col gap-[10px]',
|
||||
!active && 'opacity-25 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<div>{plug.description}</div>
|
||||
<Select
|
||||
label="Delay"
|
||||
hideErrors={true}
|
||||
{...register(`plug--${plug.identifier}--delay`)}
|
||||
>
|
||||
{delayOptions.map((p) => (
|
||||
<option key={p.name} value={p.value}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<div>Accounts that will engage:</div>
|
||||
<PickPlatforms
|
||||
hide={false}
|
||||
integrations={allowedIntegrations}
|
||||
selectedIntegrations={localValue}
|
||||
singleSelect={false}
|
||||
isMain={true}
|
||||
onChange={setLocalValue}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -20,6 +20,9 @@ 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';
|
||||
import { GeneratorComponent } from './generator/generator';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { NewPost } from '@gitroom/frontend/components/launches/new.post';
|
||||
|
||||
interface MenuComponentInterface {
|
||||
refreshChannel: (
|
||||
|
|
@ -31,6 +34,30 @@ interface MenuComponentInterface {
|
|||
update: (shouldReload: boolean) => void;
|
||||
}
|
||||
|
||||
export const OpenClose: FC<{
|
||||
isOpen: boolean;
|
||||
}> = (props) => {
|
||||
const { isOpen } = props;
|
||||
return (
|
||||
<svg
|
||||
width="11"
|
||||
height="6"
|
||||
viewBox="0 0 22 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={clsx(
|
||||
'rotate-180 transition-all',
|
||||
isOpen ? 'rotate-180' : 'rotate-90'
|
||||
)}
|
||||
>
|
||||
<path
|
||||
d="M21.9245 11.3823C21.8489 11.5651 21.7207 11.7213 21.5563 11.8312C21.3919 11.9411 21.1986 11.9998 21.0008 11.9998H1.00079C0.802892 12 0.609399 11.9414 0.444805 11.8315C0.280212 11.7217 0.151917 11.5654 0.076165 11.3826C0.000412494 11.1998 -0.0193921 10.9986 0.0192583 10.8045C0.0579087 10.6104 0.153276 10.4322 0.293288 10.2923L10.2933 0.29231C10.3862 0.199333 10.4964 0.125575 10.6178 0.0752506C10.7392 0.0249263 10.8694 -0.000976562 11.0008 -0.000976562C11.1322 -0.000976562 11.2623 0.0249263 11.3837 0.0752506C11.5051 0.125575 11.6154 0.199333 11.7083 0.29231L21.7083 10.2923C21.8481 10.4322 21.9433 10.6105 21.9818 10.8045C22.0202 10.9985 22.0003 11.1996 21.9245 11.3823Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MenuGroupComponent: FC<
|
||||
MenuComponentInterface & {
|
||||
changeItemGroup: (id: string, group: string) => void;
|
||||
|
|
@ -57,6 +84,19 @@ export const MenuGroupComponent: FC<
|
|||
changeItemGroup,
|
||||
} = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(
|
||||
!!+(localStorage.getItem(group.name + '_isOpen') || '1')
|
||||
);
|
||||
|
||||
const changeOpenClose = useCallback(
|
||||
(e: any) => {
|
||||
setIsOpen(!isOpen);
|
||||
localStorage.setItem(group.name + '_isOpen', isOpen ? '0' : '1');
|
||||
e.stopPropagation();
|
||||
},
|
||||
[isOpen]
|
||||
);
|
||||
|
||||
const [collectedProps, drop] = useDrop(() => ({
|
||||
accept: 'menu',
|
||||
drop: (item: { id: string }, monitor) => {
|
||||
|
|
@ -80,18 +120,35 @@ export const MenuGroupComponent: FC<
|
|||
</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}
|
||||
/>
|
||||
))}
|
||||
{!!group.name && (
|
||||
<div
|
||||
className="flex items-center gap-[5px] cursor-pointer"
|
||||
onClick={changeOpenClose}
|
||||
>
|
||||
<div>
|
||||
<OpenClose isOpen={isOpen} />
|
||||
</div>
|
||||
<div>{group.name}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'gap-[16px] flex flex-col relative',
|
||||
!isOpen && 'hidden'
|
||||
)}
|
||||
>
|
||||
{group.values.map((integration) => (
|
||||
<MenuComponent
|
||||
key={integration.id}
|
||||
integration={integration}
|
||||
mutate={mutate}
|
||||
continueIntegration={continueIntegration}
|
||||
update={update}
|
||||
refreshChannel={refreshChannel}
|
||||
totalNonDisabledChannels={totalNonDisabledChannels}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -101,6 +158,7 @@ export const MenuComponent: FC<
|
|||
identifier: string;
|
||||
changeProfilePicture: boolean;
|
||||
changeNickName: boolean;
|
||||
refreshNeeded?: boolean;
|
||||
};
|
||||
}
|
||||
> = (props) => {
|
||||
|
|
@ -201,6 +259,7 @@ export const MenuComponent: FC<
|
|||
<Menu
|
||||
canChangeProfilePicture={integration.changeProfilePicture}
|
||||
canChangeNickName={integration.changeNickName}
|
||||
refreshChannel={refreshChannel}
|
||||
mutate={mutate}
|
||||
onChange={update}
|
||||
id={integration.id}
|
||||
|
|
@ -215,6 +274,8 @@ export const MenuComponent: FC<
|
|||
};
|
||||
export const LaunchesComponent = () => {
|
||||
const fetch = useFetch();
|
||||
const user = useUser();
|
||||
const { billingEnabled } = useVariables();
|
||||
const router = useRouter();
|
||||
const search = useSearchParams();
|
||||
const toast = useToaster();
|
||||
|
|
@ -331,9 +392,14 @@ export const LaunchesComponent = () => {
|
|||
}
|
||||
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();
|
||||
|
|
@ -370,8 +436,13 @@ export const LaunchesComponent = () => {
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
<AddProviderButton update={() => update(true)} />
|
||||
{/*{sortedIntegrations?.length > 0 && user?.tier?.ai && <GeneratorComponent />}*/}
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<AddProviderButton update={() => update(true)} />
|
||||
{sortedIntegrations?.length > 0 && <NewPost />}
|
||||
{sortedIntegrations?.length > 0 &&
|
||||
user?.tier?.ai &&
|
||||
billingEnabled && <GeneratorComponent />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Filters />
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,56 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
||||
export const NewPost = () => {
|
||||
const fetch = useFetch();
|
||||
const modal = useModals();
|
||||
const { integrations, reloadCalendarView } = useCalendar();
|
||||
|
||||
const createAPost = useCallback(async () => {
|
||||
const date = (await (await fetch('/posts/find-slot')).json()).date;
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
children: (
|
||||
<AddEditModal
|
||||
allIntegrations={integrations.map((p) => ({ ...p }))}
|
||||
reopenModal={createAPost}
|
||||
mutate={reloadCalendarView}
|
||||
integrations={integrations}
|
||||
date={dayjs.utc(date).local()}
|
||||
/>
|
||||
),
|
||||
size: '80%',
|
||||
title: ``,
|
||||
});
|
||||
}, [integrations]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={createAPost}
|
||||
className="p-[8px] rounded-md bg-green-900 flex justify-center items-center gap-[5px] outline-none text-white"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M21 4H11C9.14409 4.00199 7.36477 4.74012 6.05245 6.05245C4.74012 7.36477 4.00199 9.14409 4 11V21C4.00199 22.8559 4.74012 24.6352 6.05245 25.9476C7.36477 27.2599 9.14409 27.998 11 28H17C17.1075 27.9999 17.2142 27.9826 17.3162 27.9487C20.595 26.855 26.855 20.595 27.9487 17.3162C27.9826 17.2142 27.9999 17.1075 28 17V11C27.998 9.14409 27.2599 7.36477 25.9476 6.05245C24.6352 4.74012 22.8559 4.00199 21 4ZM17 25.9275V22C17 20.6739 17.5268 19.4021 18.4645 18.4645C19.4021 17.5268 20.6739 17 22 17H25.9275C24.77 19.6938 19.6938 24.77 17 25.9275Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex-1 text-left">Create New Post</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ import { useStateCallback } from '@gitroom/react/helpers/use.state.callback';
|
|||
|
||||
export const ProvidersOptions: FC<{
|
||||
integrations: Integrations[];
|
||||
allIntegrations: Integrations[];
|
||||
editorValue: Array<{ id?: string; content: string }>;
|
||||
date: dayjs.Dayjs;
|
||||
}> = (props) => {
|
||||
|
|
@ -32,7 +33,7 @@ export const ProvidersOptions: FC<{
|
|||
isMain={false}
|
||||
/>
|
||||
<IntegrationContext.Provider
|
||||
value={{ value: editorValue, integration: selectedIntegrations?.[0], date }}
|
||||
value={{ value: editorValue, integration: selectedIntegrations?.[0], date, allIntegrations: props.allIntegrations }}
|
||||
>
|
||||
<ShowAllProviders
|
||||
value={editorValue}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
Fragment,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
FC, Fragment, ReactNode, useCallback, useEffect, useMemo, useState, ClipboardEvent, memo
|
||||
} from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
|
|
@ -38,6 +32,12 @@ import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.bu
|
|||
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
|
||||
import { capitalize } from 'lodash';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { InternalChannels } from '@gitroom/frontend/components/launches/internal.channels';
|
||||
|
||||
// Simple component to change back to settings on after changing tab
|
||||
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
|
||||
|
|
@ -80,9 +80,9 @@ export const withProvider = function <T extends object>(
|
|||
value: Array<Array<{ path: string }>>,
|
||||
settings: T
|
||||
) => Promise<string | true>,
|
||||
maximumCharacters?: number
|
||||
maximumCharacters?: number | ((settings: any) => number)
|
||||
) {
|
||||
return (props: {
|
||||
return memo((props: {
|
||||
identifier: string;
|
||||
id: string;
|
||||
value: Array<{
|
||||
|
|
@ -94,8 +94,10 @@ export const withProvider = function <T extends object>(
|
|||
show: boolean;
|
||||
}) => {
|
||||
const existingData = useExistingData();
|
||||
const { integration, date } = useIntegration();
|
||||
const { allIntegrations, integration, date } = useIntegration();
|
||||
const [showLinkedinPopUp, setShowLinkedinPopUp] = useState<any>(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fetch = useFetch();
|
||||
|
||||
useCopilotReadable({
|
||||
description:
|
||||
|
|
@ -146,7 +148,11 @@ export const withProvider = function <T extends object>(
|
|||
editInPlace ? InPlaceValue : props.value,
|
||||
dto,
|
||||
checkValidity,
|
||||
maximumCharacters
|
||||
!maximumCharacters
|
||||
? undefined
|
||||
: typeof maximumCharacters === 'number'
|
||||
? maximumCharacters
|
||||
: maximumCharacters(JSON.parse(integration?.additionalSettings || '[]'))
|
||||
);
|
||||
|
||||
// change editor value
|
||||
|
|
@ -276,6 +282,76 @@ export const withProvider = function <T extends object>(
|
|||
[]
|
||||
);
|
||||
|
||||
const uppy = useUppyUploader({
|
||||
onUploadSuccess: () => {
|
||||
/**empty**/
|
||||
},
|
||||
allowedFileTypes: 'image/*,video/mp4',
|
||||
});
|
||||
|
||||
const pasteImages = useCallback(
|
||||
(index: number, currentValue: any[], isFile?: boolean) => {
|
||||
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
|
||||
// @ts-ignore
|
||||
const clipboardItems = isFile
|
||||
? // @ts-ignore
|
||||
event.map((p) => ({ kind: 'file', getAsFile: () => p }))
|
||||
: // @ts-ignore
|
||||
event.clipboardData?.items; // Ensure clipboardData is available
|
||||
if (!clipboardItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files: File[] = [];
|
||||
|
||||
// @ts-ignore
|
||||
for (const item of clipboardItems) {
|
||||
console.log(item);
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
if (isImage || isVideo) {
|
||||
files.push(file); // Collect images or videos
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
const lastValues = [...currentValue];
|
||||
for (const file of files) {
|
||||
uppy.addFile(file);
|
||||
const upload = await uppy.upload();
|
||||
uppy.clear();
|
||||
if (upload?.successful?.length) {
|
||||
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
|
||||
changeImage(index)({
|
||||
target: {
|
||||
name: 'image',
|
||||
value: [...lastValues],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
setUploading(false);
|
||||
};
|
||||
},
|
||||
[changeImage]
|
||||
);
|
||||
|
||||
const getInternalPlugs = useCallback(async () => {
|
||||
return (
|
||||
await fetch(`/integrations/${props.identifier}/internal-plugs`)
|
||||
).json();
|
||||
}, [props.identifier]);
|
||||
|
||||
const { data } = useSWR(`internal-${props.identifier}`, getInternalPlugs);
|
||||
|
||||
// this is a trick to prevent the data from being deleted, yet we don't render the elements
|
||||
if (!props.show) {
|
||||
return null;
|
||||
|
|
@ -297,7 +373,7 @@ export const withProvider = function <T extends object>(
|
|||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
{!!SettingsComponent && (
|
||||
{(!!SettingsComponent || !!data?.internalPlugs?.length) && (
|
||||
<div className="flex-1 flex">
|
||||
<Button
|
||||
className={clsx(
|
||||
|
|
@ -329,9 +405,14 @@ export const withProvider = function <T extends object>(
|
|||
{editInPlace &&
|
||||
createPortal(
|
||||
<EditorWrapper>
|
||||
{uploading && (
|
||||
<div className="absolute left-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
|
||||
<LoadingComponent width={100} height={100} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
{!existingData?.integration && (
|
||||
<div className="bg-red-800">
|
||||
<div className="bg-red-800 text-white">
|
||||
You are now editing only {integration?.name} (
|
||||
{capitalize(integration?.identifier.replace('-', ' '))})
|
||||
</div>
|
||||
|
|
@ -341,39 +422,43 @@ export const withProvider = function <T extends object>(
|
|||
<div>
|
||||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 text-textColor editor">
|
||||
{integration?.identifier === 'linkedin' && (
|
||||
{(integration?.identifier === 'linkedin' ||
|
||||
integration?.identifier === 'linkedin-page') && (
|
||||
<Button
|
||||
className="mb-[5px]"
|
||||
onClick={tagPersonOrCompany(
|
||||
integration.id,
|
||||
(newValue: string) =>
|
||||
changeValue(index)(
|
||||
val.content + newValue
|
||||
)
|
||||
changeValue(index)(val.content + newValue)
|
||||
)}
|
||||
>
|
||||
Tag a company
|
||||
</Button>
|
||||
)}
|
||||
<Editor
|
||||
order={index}
|
||||
height={InPlaceValue.length > 1 ? 200 : 250}
|
||||
value={val.content}
|
||||
commands={[
|
||||
// ...commands
|
||||
// .getCommands()
|
||||
// .filter((f) => f.name !== 'image'),
|
||||
// newImage,
|
||||
postSelector(date),
|
||||
...linkedinCompany(
|
||||
integration?.identifier!,
|
||||
integration?.id!
|
||||
),
|
||||
]}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
<DropFiles
|
||||
onDrop={pasteImages(index, val.image || [], true)}
|
||||
>
|
||||
<Editor
|
||||
order={index}
|
||||
height={InPlaceValue.length > 1 ? 200 : 250}
|
||||
value={val.content}
|
||||
commands={[
|
||||
// ...commands
|
||||
// .getCommands()
|
||||
// .filter((f) => f.name !== 'image'),
|
||||
// newImage,
|
||||
postSelector(date),
|
||||
...linkedinCompany(
|
||||
integration?.identifier!,
|
||||
integration?.id!
|
||||
),
|
||||
]}
|
||||
preview="edit"
|
||||
onPaste={pasteImages(index, val.image || [])}
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
</DropFiles>
|
||||
{(!val.content || val.content.length < 6) && (
|
||||
<div className="my-[5px] text-customColor19 text-[12px] font-[500]">
|
||||
The post should be at least 6 characters long
|
||||
|
|
@ -382,6 +467,7 @@ export const withProvider = function <T extends object>(
|
|||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<MultiMediaComponent
|
||||
text={val.content}
|
||||
label="Attachments"
|
||||
description=""
|
||||
name="image"
|
||||
|
|
@ -441,12 +527,16 @@ export const withProvider = function <T extends object>(
|
|||
{(showTab === 0 || showTab === 2) && (
|
||||
<div className={clsx('mt-[20px]', showTab !== 2 && 'hidden')}>
|
||||
<Component values={editInPlace ? InPlaceValue : props.value} />
|
||||
{!!data?.internalPlugs?.length && (
|
||||
<InternalChannels plugs={data?.internalPlugs} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showTab === 0 && (
|
||||
<div className="mt-[20px] flex flex-col items-center">
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
allIntegrations,
|
||||
date,
|
||||
value: editInPlace ? InPlaceValue : props.value,
|
||||
integration,
|
||||
|
|
@ -457,11 +547,31 @@ export const withProvider = function <T extends object>(
|
|||
.join('').length ? (
|
||||
CustomPreviewComponent ? (
|
||||
<CustomPreviewComponent
|
||||
maximumCharacters={maximumCharacters}
|
||||
maximumCharacters={
|
||||
!maximumCharacters
|
||||
? undefined
|
||||
: typeof maximumCharacters === 'number'
|
||||
? maximumCharacters
|
||||
: maximumCharacters(
|
||||
JSON.parse(
|
||||
integration?.additionalSettings || '[]'
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<GeneralPreviewComponent
|
||||
maximumCharacters={maximumCharacters}
|
||||
maximumCharacters={
|
||||
!maximumCharacters
|
||||
? undefined
|
||||
: typeof maximumCharacters === 'number'
|
||||
? maximumCharacters
|
||||
: maximumCharacters(
|
||||
JSON.parse(
|
||||
integration?.additionalSettings || '[]'
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
|
@ -473,5 +583,5 @@ export const withProvider = function <T extends object>(
|
|||
</div>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { FC, useCallback } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { Subreddit } from './subreddit';
|
||||
import { LemmySettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/lemmy.dto';
|
||||
|
||||
const LemmySettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control, // control props comes from useForm (optional: if you are using FormContext)
|
||||
name: 'subreddit', // unique name for your Field Array
|
||||
});
|
||||
|
||||
const addField = useCallback(() => {
|
||||
append({});
|
||||
}, [fields, append]);
|
||||
|
||||
const deleteField = useCallback(
|
||||
(index: number) => async () => {
|
||||
if (
|
||||
!(await deleteDialog('Are you sure you want to delete this Subreddit?'))
|
||||
)
|
||||
return;
|
||||
remove(index);
|
||||
},
|
||||
[fields, remove]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[20px] mb-[20px]">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col relative">
|
||||
<div
|
||||
onClick={deleteField(index)}
|
||||
className="absolute -left-[10px] justify-center items-center flex -top-[10px] w-[20px] h-[20px] bg-red-600 rounded-full text-textColor"
|
||||
>
|
||||
x
|
||||
</div>
|
||||
<Subreddit {...register(`subreddit.${index}.value`)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={addField}>Add Subreddit</Button>
|
||||
{fields.length === 0 && (
|
||||
<div className="text-red-500 text-[12px] mt-[10px]">
|
||||
Please add at least one Subreddit
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider(
|
||||
LemmySettings,
|
||||
undefined,
|
||||
LemmySettingsDto,
|
||||
async (items) => {
|
||||
const [firstItems] = items;
|
||||
|
||||
if (
|
||||
firstItems.length &&
|
||||
firstItems[0].path.indexOf('png') === -1 &&
|
||||
firstItems[0].path.indexOf('jpg') === -1 &&
|
||||
firstItems[0].path.indexOf('jpef') === -1 &&
|
||||
firstItems[0].path.indexOf('gif') === -1
|
||||
) {
|
||||
return 'You can set only one picture for a cover';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
10000
|
||||
);
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
import { FC, FormEvent, useCallback, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
|
||||
export const Subreddit: FC<{
|
||||
onChange: (event: {
|
||||
target: {
|
||||
name: string;
|
||||
value: {
|
||||
id: string;
|
||||
subreddit: string;
|
||||
title: string;
|
||||
name: string;
|
||||
url: string;
|
||||
body: string;
|
||||
media: any[];
|
||||
};
|
||||
};
|
||||
}) => void;
|
||||
name: string;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
|
||||
const state = useSettings();
|
||||
const split = name.split('.');
|
||||
const [loading, setLoading] = useState(false);
|
||||
// @ts-ignore
|
||||
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
|
||||
|
||||
const [results, setResults] = useState([]);
|
||||
const func = useCustomProviderFunction();
|
||||
const value = useWatch({ name });
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const setResult = (result: { id: string; name: string }) => async () => {
|
||||
setLoading(true);
|
||||
setSearchValue('');
|
||||
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
id: String(result.id),
|
||||
subreddit: result.name,
|
||||
title: '',
|
||||
name: '',
|
||||
url: '',
|
||||
body: '',
|
||||
media: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const setTitle = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
title: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const setURL = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
url: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const search = useDebouncedCallback(
|
||||
useCallback(async (e: FormEvent<HTMLInputElement>) => {
|
||||
// @ts-ignore
|
||||
setResults([]);
|
||||
// @ts-ignore
|
||||
if (!e.target.value) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const results = await func.get('subreddits', { word: e.target.value });
|
||||
// @ts-ignore
|
||||
setResults(results);
|
||||
}, []),
|
||||
500
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-primary p-[20px]">
|
||||
{value?.subreddit ? (
|
||||
<>
|
||||
<Input
|
||||
error={errors?.subreddit?.message}
|
||||
disableForm={true}
|
||||
value={value.subreddit}
|
||||
readOnly={true}
|
||||
label="Community"
|
||||
name="subreddit"
|
||||
/>
|
||||
<Input
|
||||
error={errors?.title?.message}
|
||||
value={value.title}
|
||||
disableForm={true}
|
||||
label="Title"
|
||||
name="title"
|
||||
onChange={setTitle}
|
||||
/>
|
||||
<Input
|
||||
error={errors?.url?.message}
|
||||
value={value.url}
|
||||
label="URL"
|
||||
name="url"
|
||||
disableForm={true}
|
||||
onChange={setURL}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Community"
|
||||
name="search"
|
||||
label="Search Community"
|
||||
readOnly={loading}
|
||||
value={searchValue}
|
||||
error={errors?.message}
|
||||
disableForm={true}
|
||||
onInput={async (e) => {
|
||||
// @ts-ignore
|
||||
setSearchValue(e.target.value);
|
||||
await search(e);
|
||||
}}
|
||||
/>
|
||||
{!!results.length && !loading && (
|
||||
<div className="z-[400] w-full absolute bg-input -mt-[20px] outline-none border-fifth border cursor-pointer">
|
||||
{results.map((r: { id: string; name: string }) => (
|
||||
<div
|
||||
onClick={setResult(r)}
|
||||
key={r.id}
|
||||
className="px-[16px] py-[5px] hover:bg-secondary"
|
||||
>
|
||||
{r.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
|
||||
export default withProvider(null, undefined, undefined, undefined, 1300);
|
||||
export default withProvider(null, undefined, undefined, undefined, 3000);
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ export const Subreddit: FC<{
|
|||
<div className="w-full h-[10px] bg-input rounded-tr-[8px] rounded-tl-[8px]" />
|
||||
<div className="flex flex-col text-nowrap">
|
||||
<MultiMediaComponent
|
||||
text=""
|
||||
description=""
|
||||
name="media"
|
||||
label="Media"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import DiscordProvider from '@gitroom/frontend/components/launches/providers/dis
|
|||
import SlackProvider from '@gitroom/frontend/components/launches/providers/slack/slack.provider';
|
||||
import MastodonProvider from '@gitroom/frontend/components/launches/providers/mastodon/mastodon.provider';
|
||||
import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider';
|
||||
import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy/lemmy.provider';
|
||||
|
||||
export const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
|
|
@ -37,6 +38,7 @@ export const Providers = [
|
|||
{identifier: 'slack', component: SlackProvider},
|
||||
{identifier: 'mastodon', component: MastodonProvider},
|
||||
{identifier: 'bluesky', component: BlueskyProvider},
|
||||
{identifier: 'lemmy', component: LemmyProvider},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ export default withProvider(
|
|||
}
|
||||
return true;
|
||||
},
|
||||
280
|
||||
(settings) => {
|
||||
if (settings?.[0]?.value) {
|
||||
console.log(4000);
|
||||
return 4000;
|
||||
}
|
||||
return 280;
|
||||
}
|
||||
);
|
||||
|
||||
const checkVideoDuration = async (url: string): Promise<boolean> => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { Select } from '@gitroom/react/form/select';
|
||||
import { uniqBy } from 'lodash';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
|
||||
export const SelectCustomer: FC<{
|
||||
onChange: (value: string) => void;
|
||||
integrations: Integrations[];
|
||||
customer?: string;
|
||||
}> = (props) => {
|
||||
const { onChange, integrations, customer: currentCustomer } = props;
|
||||
const [customer, setCustomer] = useState(currentCustomer || '');
|
||||
|
||||
const totalCustomers = useMemo(() => {
|
||||
return uniqBy(integrations, (i) => i?.customer?.id).length;
|
||||
}, [integrations]);
|
||||
|
||||
if (totalCustomers <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
hideErrors={true}
|
||||
label=""
|
||||
name="customer"
|
||||
value={customer}
|
||||
onChange={(e) => {
|
||||
setCustomer(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
disableForm={true}
|
||||
>
|
||||
<option value="">Selected Customer</option>
|
||||
{uniqBy(integrations, (u) => u?.customer?.name)
|
||||
.filter((f) => f.customer?.name)
|
||||
.map((p) => (
|
||||
<option key={p.customer?.id} value={p.customer?.id}>
|
||||
Customer: {p.customer?.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { Slider } from '@gitroom/react/form/slider';
|
||||
|
||||
export const Element: FC<{ setting: any; onChange: (value: any) => void }> = (
|
||||
props
|
||||
) => {
|
||||
const { setting, onChange } = props;
|
||||
const [value, setValue] = useState(setting.value);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div>{setting.title}</div>
|
||||
<div className="text-[14px]">{setting.description}</div>
|
||||
<Slider
|
||||
value={value === true ? 'on' : 'off'}
|
||||
onChange={() => {
|
||||
setValue(!value);
|
||||
onChange(!value);
|
||||
}}
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsModal: FC<{
|
||||
integration: Integration & { customer?: { id: string; name: string } };
|
||||
onClose: () => void;
|
||||
}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const { onClose, integration } = props;
|
||||
const modal = useModals();
|
||||
const [values, setValues] = useState(
|
||||
JSON.parse(integration?.additionalSettings || '[]')
|
||||
);
|
||||
|
||||
const changeValue = useCallback(
|
||||
(index: number) => (value: any) => {
|
||||
const newValues = [...values];
|
||||
newValues[index].value = value;
|
||||
setValues(newValues);
|
||||
},
|
||||
[values]
|
||||
);
|
||||
|
||||
const save = useCallback(async () => {
|
||||
await fetch(`/integrations/${integration.id}/settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ additionalSettings: JSON.stringify(values) }),
|
||||
});
|
||||
|
||||
modal.closeAll();
|
||||
onClose();
|
||||
}, [values, integration]);
|
||||
|
||||
return (
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
|
||||
<TopTitle title={`Additional Settings`} />
|
||||
<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]">
|
||||
{values.map((setting: any, index: number) => (
|
||||
<Element key={setting.title} setting={setting} onChange={changeValue(index)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="my-[16px] flex gap-[10px]">
|
||||
<Button onClick={save}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import { FC, useCallback } from 'react';
|
||||
import { Editor, Transforms } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
const underlineMap = {
|
||||
a: 'a̲',
|
||||
b: 'b̲',
|
||||
c: 'c̲',
|
||||
d: 'd̲',
|
||||
e: 'e̲',
|
||||
f: 'f̲',
|
||||
g: 'g̲',
|
||||
h: 'h̲',
|
||||
i: 'i̲',
|
||||
j: 'j̲',
|
||||
k: 'k̲',
|
||||
l: 'l̲',
|
||||
m: 'm̲',
|
||||
n: 'n̲',
|
||||
o: 'o̲',
|
||||
p: 'p̲',
|
||||
q: 'q̲',
|
||||
r: 'r̲',
|
||||
s: 's̲',
|
||||
t: 't̲',
|
||||
u: 'u̲',
|
||||
v: 'v̲',
|
||||
w: 'w̲',
|
||||
x: 'x̲',
|
||||
y: 'y̲',
|
||||
z: 'z̲',
|
||||
A: 'A̲',
|
||||
B: 'B̲',
|
||||
C: 'C̲',
|
||||
D: 'D̲',
|
||||
E: 'E̲',
|
||||
F: 'F̲',
|
||||
G: 'G̲',
|
||||
H: 'H̲',
|
||||
I: 'I̲',
|
||||
J: 'J̲',
|
||||
K: 'K̲',
|
||||
L: 'L̲',
|
||||
M: 'M̲',
|
||||
N: 'N̲',
|
||||
O: 'O̲',
|
||||
P: 'P̲',
|
||||
Q: 'Q̲',
|
||||
R: 'R̲',
|
||||
S: 'S̲',
|
||||
T: 'T̲',
|
||||
U: 'U̲',
|
||||
V: 'V̲',
|
||||
W: 'W̲',
|
||||
X: 'X̲',
|
||||
Y: 'Y̲',
|
||||
Z: 'Z̲',
|
||||
'1': '1̲',
|
||||
'2': '2̲',
|
||||
'3': '3̲',
|
||||
'4': '4̲',
|
||||
'5': '5̲',
|
||||
'6': '6̲',
|
||||
'7': '7̲',
|
||||
'8': '8̲',
|
||||
'9': '9̲',
|
||||
'0': '0̲',
|
||||
};
|
||||
export const UText: FC<{ editor: any; currentValue: string }> = ({
|
||||
editor,
|
||||
}) => {
|
||||
const mark = () => {
|
||||
const selectedText = Editor.string(editor, editor.selection);
|
||||
|
||||
const newText = (
|
||||
!selectedText ? prompt('What do you want to write?') || '' : selectedText
|
||||
)
|
||||
.split('')
|
||||
// @ts-ignore
|
||||
.map((char) => underlineMap?.[char] || char)
|
||||
.join('');
|
||||
|
||||
Transforms.insertText(editor, newText);
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={mark}
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_31_12620)">
|
||||
<path
|
||||
d="M10.4119 6.47V12.77C10.4119 13.4 10.5669 13.885 10.8769 14.225C11.1869 14.565 11.6419 14.735 12.2419 14.735C12.8419 14.735 13.3019 14.565 13.6219 14.225C13.9419 13.885 14.1019 13.4 14.1019 12.77V6.47H16.6669V12.755C16.6669 13.695 16.4669 14.49 16.0669 15.14C15.6669 15.79 15.1269 16.28 14.4469 16.61C13.7769 16.94 13.0269 17.105 12.1969 17.105C11.3669 17.105 10.6219 16.945 9.96191 16.625C9.31191 16.295 8.79691 15.805 8.41691 15.155C8.03691 14.495 7.84691 13.695 7.84691 12.755V6.47H10.4119Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M6.96191 18.5H17.5369V19.25H6.96191V18.5Z" fill="currentColor" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_31_12620">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.25)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import Loading from 'react-loading';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export const CheckPayment: FC<{ check: string; mutate: () => void }> = (props) => {
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
const fetch = useFetch();
|
||||
const toaster = useToaster();
|
||||
|
||||
const checkSubscription = useCallback(async () => {
|
||||
const {status} = await (await fetch('/billing/check/' + props.check)).json();
|
||||
if (status === 0) {
|
||||
await timer(1000);
|
||||
return checkSubscription();
|
||||
}
|
||||
|
||||
if (status === 1) {
|
||||
toaster.show(
|
||||
'We could not validate your payment method, please try again',
|
||||
'warning'
|
||||
);
|
||||
setShowLoader(false);
|
||||
}
|
||||
|
||||
if (status === 2) {
|
||||
setShowLoader(false);
|
||||
props.mutate();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkSubscription();
|
||||
}, []);
|
||||
|
||||
if (showLoader) {
|
||||
return (
|
||||
<div className="fixed bg-black/40 w-full h-full flex justify-center items-center z-[400]">
|
||||
<div>
|
||||
<Loading type="spin" color="#612AD5" height={250} width={250} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export const useClickOutside = (callback: () => Promise<void>) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const selector = document.querySelector('#add-edit-modal');
|
||||
const copilotkit = document.querySelector('.copilotKitPopup');
|
||||
const emoji = document.querySelector('.EmojiPickerReact');
|
||||
if (
|
||||
selector &&
|
||||
!selector.contains(event.target as HTMLElement) &&
|
||||
copilotkit &&
|
||||
!copilotkit.contains(event.target as HTMLElement) &&
|
||||
emoji &&
|
||||
!emoji.contains(event.target as HTMLElement)
|
||||
) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
document
|
||||
.querySelector('.mantine-Modal-root')
|
||||
// @ts-ignore
|
||||
?.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
document
|
||||
.querySelector('.mantine-Modal-root')
|
||||
// @ts-ignore
|
||||
?.removeEventListener('click', handleClick);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
@ -76,12 +76,14 @@ export const ContinueProvider: FC = () => {
|
|||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="pt-[16px]">
|
||||
<div className="pt-[16px] max-h-[600px] overflow-hidden overflow-y-auto">
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
date: dayjs(),
|
||||
value: [],
|
||||
allIntegrations: [],
|
||||
integration: {
|
||||
additionalSettings: '',
|
||||
display: '',
|
||||
time: [{time: 0}],
|
||||
id: continueId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { useDropzone } from 'react-dropzone';
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
export const DropFiles: FC<{ children: ReactNode, onDrop: (files: File[]) => void }> = (props) => {
|
||||
const { getRootProps, isDragActive } = useDropzone({
|
||||
onDrop: props.onDrop
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...getRootProps()} className="relative">
|
||||
{isDragActive && (
|
||||
<div className="absolute left-0 top-0 w-full h-full bg-black/90 flex items-center justify-center z-[200] animate-normalFadeIn">
|
||||
Drag n drop some files here
|
||||
</div>
|
||||
)}
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
'use client';
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
export const FacebookComponent = () => {
|
||||
if (!process.env.NEXT_PUBLIC_FACEBOOK_PIXEL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Script strategy="afterInteractive" id="fb-pixel">
|
||||
{`!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'/f.js');
|
||||
fbq('init', '${process.env.NEXT_PUBLIC_FACEBOOK_PIXEL}');
|
||||
`}
|
||||
</Script>
|
||||
);
|
||||
};
|
||||
|
|
@ -20,6 +20,10 @@ function LayoutContextInner(params: { children: ReactNode }) {
|
|||
|
||||
const afterRequest = useCallback(
|
||||
async (url: string, options: RequestInit, response: Response) => {
|
||||
if (typeof window !== 'undefined' && window.location.href.includes('/p/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const reloadOrOnboarding =
|
||||
response?.headers?.get('reload') ||
|
||||
response?.headers?.get('onboarding');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { ReactNode, useCallback, useEffect } from 'react';
|
||||
import { Title } from '@gitroom/frontend/components/layout/title';
|
||||
import { ContextWrapper } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { TopMenu } from '@gitroom/frontend/components/layout/top.menu';
|
||||
|
|
@ -8,7 +8,7 @@ import { MantineWrapper } from '@gitroom/react/helpers/mantine.wrapper';
|
|||
import { ToolTip } from '@gitroom/frontend/components/layout/top.tip';
|
||||
import { ShowMediaBoxModal } from '@gitroom/frontend/components/media/media.component';
|
||||
import Image from 'next/image';
|
||||
import { Toaster } from '@gitroom/react/toaster/toaster';
|
||||
import { Toaster, useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { ShowPostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
|
||||
import { OrganizationSelector } from '@gitroom/frontend/components/layout/organization.selector';
|
||||
import NotificationComponent from '@gitroom/frontend/components/notifications/notification.component';
|
||||
|
|
@ -37,6 +37,8 @@ const ModeComponent = dynamic(
|
|||
);
|
||||
|
||||
import { extend } from 'dayjs';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { CheckPayment } from '@gitroom/frontend/components/layout/check.payment';
|
||||
|
||||
extend(utc);
|
||||
extend(weekOfYear);
|
||||
|
|
@ -47,11 +49,12 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
const fetch = useFetch();
|
||||
const { isGeneral } = useVariables();
|
||||
const { backendUrl, billingEnabled } = useVariables();
|
||||
const searchParams = useSearchParams();
|
||||
const load = useCallback(async (path: string) => {
|
||||
return await (await fetch(path)).json();
|
||||
}, []);
|
||||
|
||||
const { data: user } = useSWR('/user/self', load, {
|
||||
const { data: user, mutate } = useSWR('/user/self', load, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
|
|
@ -67,89 +70,95 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
credentials="include"
|
||||
runtimeUrl={backendUrl + '/copilot/chat'}
|
||||
>
|
||||
<ContextWrapper user={user}>
|
||||
<MantineWrapper>
|
||||
<ToolTip />
|
||||
<ShowMediaBoxModal />
|
||||
<ShowLinkedinCompany />
|
||||
<Toaster />
|
||||
<ShowPostSelector />
|
||||
<NewSubscription />
|
||||
{user.tier !== 'FREE' && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<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 />}
|
||||
<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 order-1"
|
||||
>
|
||||
<div className="min-w-[55px]">
|
||||
<Image
|
||||
src={isGeneral ? '/postiz.svg' : '/logo.svg'}
|
||||
width={55}
|
||||
height={53}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(!isGeneral ? 'mt-[12px]' : 'min-w-[80px]')}
|
||||
>
|
||||
{isGeneral ? (
|
||||
<svg
|
||||
width="80"
|
||||
height="75"
|
||||
viewBox="0 0 366 167"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24.9659 30.4263V43.3825C26.9237 41.3095 29.3998 39.582 32.3941 38.2C35.3885 36.7028 39.0162 35.9543 43.2774 35.9543C47.1931 35.9543 50.8784 36.7028 54.3334 38.2C57.9036 39.6972 61.0131 42.1157 63.6619 45.4555C66.4259 48.6802 68.6141 52.9989 70.2264 58.4118C71.8387 63.8246 72.6449 70.3891 72.6449 78.1053C72.6449 83.6333 72.1266 89.1613 71.0902 94.6893C70.1688 100.217 68.4989 105.169 66.0804 109.546C63.6619 113.922 60.3796 117.492 56.2336 120.256C52.2028 122.905 47.1355 124.23 41.0316 124.23C36.6553 124.23 33.2003 123.654 30.6666 122.502C28.1329 121.235 26.2327 119.796 24.9659 118.183V160.162L0.0898438 166.381V30.4263H24.9659ZM32.7396 109.2C35.734 109.2 38.2676 108.221 40.3406 106.264C42.4136 104.191 44.026 101.542 45.1776 98.3171C46.4445 95.0924 47.3082 91.5222 47.7689 87.6066C48.3447 83.5757 48.6326 79.6025 48.6326 75.6868C48.6326 69.3526 48.0568 64.3429 46.9051 60.6575C45.8686 56.9722 44.6018 54.2658 43.1046 52.5383C41.6075 50.6956 40.1103 49.5439 38.6131 49.0833C37.2311 48.6226 36.137 48.3923 35.3309 48.3923C33.2579 48.3923 31.2425 49.1409 29.2846 50.638C27.3268 52.02 25.8872 54.1506 24.9659 57.0298V105.227C25.5417 106.148 26.463 107.07 27.7299 107.991C28.9967 108.797 30.6666 109.2 32.7396 109.2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M188.176 31.4627C191.055 42.5188 193.588 51.5593 195.777 58.5845C197.965 65.4945 199.807 71.3105 201.305 76.0323C202.917 80.7541 204.126 84.9001 204.932 88.4703C205.854 92.0405 206.314 96.0137 206.314 100.39C208.272 99.1232 210.172 97.7988 212.015 96.4168C213.858 94.9196 215.413 93.5376 216.679 92.2708H223.935C220.825 96.9926 217.543 100.908 214.088 104.018C210.633 107.012 207.293 109.661 204.069 111.964C201.996 116.456 198.829 119.623 194.567 121.466C190.306 123.308 185.872 124.23 181.266 124.23C176.083 124.23 171.649 123.539 167.964 122.157C164.279 120.659 161.227 118.702 158.808 116.283C156.505 113.749 154.777 110.87 153.626 107.646C152.474 104.421 151.898 101.023 151.898 97.4533C151.898 93.5376 152.819 90.4857 154.662 88.2975C156.62 85.9942 158.866 84.8426 161.399 84.8426C168.424 84.8426 171.937 87.6641 171.937 93.3073C171.937 95.15 171.304 96.7047 170.037 97.9716C168.77 99.2384 167.158 99.8718 165.2 99.8718C164.278 99.8718 163.3 99.7566 162.263 99.5263C161.342 99.1808 160.593 98.5474 160.017 97.6261C160.939 101.657 162.436 104.824 164.509 107.127C166.697 109.431 169.461 110.582 172.801 110.582C175.68 110.582 177.811 109.891 179.193 108.509C180.575 107.012 181.266 104.478 181.266 100.908C181.266 97.1078 180.92 93.7104 180.229 90.7161C179.653 87.6066 178.732 84.2091 177.465 80.5238C176.198 76.8385 174.644 72.4621 172.801 67.3948C170.958 62.2123 168.885 55.5326 166.582 47.3558C160.823 59.6786 153.222 67.5675 143.779 71.0225C143.779 71.9439 143.779 72.8652 143.779 73.7865C143.894 74.5927 143.952 75.4565 143.952 76.3778C143.952 83.0575 143.376 89.334 142.224 95.2076C141.072 100.966 139.115 106.033 136.351 110.41C133.702 114.671 130.247 118.068 125.986 120.602C121.724 123.02 116.484 124.23 110.265 124.23C106.004 124.23 101.916 123.596 98 122.329C94.1995 120.947 90.8021 118.759 87.8078 115.765C84.8134 112.655 82.3949 108.624 80.5523 103.672C78.8248 98.605 77.961 92.4436 77.961 85.188C77.961 80.2359 78.4793 74.9382 79.5158 69.295C80.5523 63.5367 82.4525 58.1814 85.2165 53.2293C87.9805 48.2771 91.7234 44.1887 96.4453 40.964C101.282 37.6242 107.444 35.9543 114.93 35.9543C122.646 35.9543 128.807 38.0273 133.414 42.1733C138.136 46.3193 141.303 52.9989 142.915 62.2123C146.946 61.2909 150.574 58.5269 153.798 53.9203C157.138 49.1984 160.305 42.8643 163.3 34.9177L188.176 31.4627ZM115.102 107.991C117.521 107.991 119.594 107.185 121.321 105.573C123.164 103.845 124.661 101.542 125.813 98.6626C126.964 95.6682 127.771 92.1556 128.231 88.1248C128.807 84.094 129.095 79.7176 129.095 74.9958V72.75C124.488 71.7135 122.185 68.3161 122.185 62.5578C122.185 58.8724 123.682 56.4539 126.677 55.3023C125.41 51.6169 123.855 49.1984 122.012 48.0468C120.285 46.8951 118.788 46.3193 117.521 46.3193C114.987 46.3193 112.799 47.5285 110.956 49.947C109.229 52.2504 107.789 55.2447 106.638 58.93C105.486 62.5002 104.622 66.4734 104.046 70.8498C103.586 75.2261 103.355 79.4297 103.355 83.4605C103.355 88.6431 103.701 92.8466 104.392 96.0713C105.198 99.296 106.177 101.772 107.329 103.5C108.48 105.227 109.747 106.436 111.129 107.127C112.511 107.703 113.835 107.991 115.102 107.991Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M239.554 9.52348V36.818H250.092V43.728H239.554V95.5531C239.554 100.39 240.187 103.615 241.454 105.227C242.836 106.724 245.197 107.473 248.537 107.473C251.877 107.473 254.641 106.033 256.829 103.154C259.132 100.275 260.457 96.6471 260.802 92.2708H268.058C267.136 99.296 265.524 104.939 263.221 109.2C260.917 113.346 258.326 116.571 255.447 118.874C252.568 121.062 249.631 122.502 246.637 123.193C243.642 123.884 240.993 124.23 238.69 124.23C229.822 124.23 223.603 121.811 220.033 116.974C216.463 112.022 214.678 105.515 214.678 97.4533V43.728H209.15V36.818H214.678V12.9785L239.554 9.52348Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M258.833 13.8422C258.833 10.0417 260.158 6.81706 262.806 4.16823C265.455 1.40422 268.68 0.0222168 272.48 0.0222168C276.281 0.0222168 279.506 1.40422 282.154 4.16823C284.918 6.81706 286.3 10.0417 286.3 13.8422C286.3 17.6427 284.918 20.8674 282.154 23.5162C279.506 26.1651 276.281 27.4895 272.48 27.4895C268.68 27.4895 265.455 26.1651 262.806 23.5162C260.158 20.8674 258.833 17.6427 258.833 13.8422ZM285.609 36.818V95.5531C285.609 100.39 286.243 103.615 287.51 105.227C288.892 106.724 291.253 107.473 294.592 107.473C296.09 107.473 297.184 107.358 297.875 107.127C298.681 106.897 299.372 106.667 299.948 106.436C300.063 107.012 300.12 107.588 300.12 108.164C300.12 108.74 300.12 109.315 300.12 109.891C300.12 112.77 299.602 115.131 298.566 116.974C297.644 118.817 296.377 120.314 294.765 121.466C293.268 122.502 291.598 123.193 289.755 123.539C288.028 123.999 286.358 124.23 284.746 124.23C275.878 124.23 269.659 121.811 266.089 116.974C262.518 112.022 260.733 105.515 260.733 97.4533V36.818H285.609ZM351.773 107.473C350.391 107.358 349.354 106.897 348.663 106.091C347.972 105.169 347.627 104.133 347.627 102.981C347.627 101.484 348.26 100.045 349.527 98.6626C350.794 97.1654 352.867 96.4168 355.746 96.4168C358.971 96.4168 361.389 97.5109 363.001 99.6991C364.614 101.772 365.42 104.248 365.42 107.127C365.42 108.97 365.074 110.87 364.383 112.828C363.692 114.671 362.598 116.398 361.101 118.011C359.604 119.508 357.761 120.775 355.573 121.811C353.385 122.732 350.851 123.193 347.972 123.193H300.293L334.152 46.1465H321.369C318.835 46.1465 316.704 46.3193 314.977 46.6648C313.365 46.8951 312.558 47.5285 312.558 48.565C312.558 49.0257 312.674 49.256 312.904 49.256C313.249 49.256 313.595 49.3712 313.94 49.6015C314.401 49.8318 314.747 50.2925 314.977 50.9835C315.322 51.6745 315.495 52.8838 315.495 54.6113C315.495 57.1449 314.689 58.9876 313.077 60.1393C311.579 61.2909 309.852 61.8668 307.894 61.8668C305.591 61.8668 303.345 61.1182 301.157 59.621C299.084 58.0087 298.047 55.5902 298.047 52.3655C298.047 50.638 298.393 48.9105 299.084 47.183C299.775 45.3403 300.811 43.6704 302.193 42.1733C303.575 40.5609 305.303 39.2941 307.376 38.3728C309.449 37.3363 311.867 36.818 314.631 36.818H362.138L329.142 109.891C329.833 109.891 330.812 109.949 332.079 110.064C333.346 110.179 334.67 110.294 336.052 110.41C337.55 110.525 338.989 110.64 340.371 110.755C341.868 110.87 343.193 110.928 344.344 110.928C346.417 110.928 348.145 110.697 349.527 110.237C351.024 109.776 351.773 108.855 351.773 107.473Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
'Gitroom'
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{user?.orgId &&
|
||||
(user.tier !== 'FREE' || !isGeneral || !billingEnabled) ? (
|
||||
<TopMenu />
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div id = "systray-buttons" className="flex items-center justify-self-end gap-[8px] order-2 md:order-3">
|
||||
<ModeComponent />
|
||||
<SettingsComponent />
|
||||
<NotificationComponent />
|
||||
<OrganizationSelector />
|
||||
<MantineWrapper>
|
||||
{user.tier === 'FREE' && searchParams.get('check') && (
|
||||
<CheckPayment check={searchParams.get('check')!} mutate={mutate} />
|
||||
)}
|
||||
<ToolTip />
|
||||
<ShowMediaBoxModal />
|
||||
<ShowLinkedinCompany />
|
||||
<Toaster />
|
||||
<ShowPostSelector />
|
||||
<NewSubscription />
|
||||
{user.tier !== 'FREE' && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-6 text-textColor flex flex-col">
|
||||
{user?.admin && <Impersonate />}
|
||||
<nav className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-2xl flex items-center gap-[10px] text-textColor order-1"
|
||||
>
|
||||
<div className="min-w-[55px]">
|
||||
<Image
|
||||
src={isGeneral ? '/postiz.svg' : '/logo.svg'}
|
||||
width={55}
|
||||
height={53}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="flex-1 flex">
|
||||
<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">
|
||||
Join 1000+ Entrepreneurs Who Use Postiz
|
||||
<br />
|
||||
To Manage All Your Social Media Channels
|
||||
</h1>
|
||||
<div
|
||||
className={clsx(!isGeneral ? 'mt-[12px]' : 'min-w-[80px]')}
|
||||
>
|
||||
{isGeneral ? (
|
||||
<svg
|
||||
width="80"
|
||||
height="75"
|
||||
viewBox="0 0 366 167"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24.9659 30.4263V43.3825C26.9237 41.3095 29.3998 39.582 32.3941 38.2C35.3885 36.7028 39.0162 35.9543 43.2774 35.9543C47.1931 35.9543 50.8784 36.7028 54.3334 38.2C57.9036 39.6972 61.0131 42.1157 63.6619 45.4555C66.4259 48.6802 68.6141 52.9989 70.2264 58.4118C71.8387 63.8246 72.6449 70.3891 72.6449 78.1053C72.6449 83.6333 72.1266 89.1613 71.0902 94.6893C70.1688 100.217 68.4989 105.169 66.0804 109.546C63.6619 113.922 60.3796 117.492 56.2336 120.256C52.2028 122.905 47.1355 124.23 41.0316 124.23C36.6553 124.23 33.2003 123.654 30.6666 122.502C28.1329 121.235 26.2327 119.796 24.9659 118.183V160.162L0.0898438 166.381V30.4263H24.9659ZM32.7396 109.2C35.734 109.2 38.2676 108.221 40.3406 106.264C42.4136 104.191 44.026 101.542 45.1776 98.3171C46.4445 95.0924 47.3082 91.5222 47.7689 87.6066C48.3447 83.5757 48.6326 79.6025 48.6326 75.6868C48.6326 69.3526 48.0568 64.3429 46.9051 60.6575C45.8686 56.9722 44.6018 54.2658 43.1046 52.5383C41.6075 50.6956 40.1103 49.5439 38.6131 49.0833C37.2311 48.6226 36.137 48.3923 35.3309 48.3923C33.2579 48.3923 31.2425 49.1409 29.2846 50.638C27.3268 52.02 25.8872 54.1506 24.9659 57.0298V105.227C25.5417 106.148 26.463 107.07 27.7299 107.991C28.9967 108.797 30.6666 109.2 32.7396 109.2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M188.176 31.4627C191.055 42.5188 193.588 51.5593 195.777 58.5845C197.965 65.4945 199.807 71.3105 201.305 76.0323C202.917 80.7541 204.126 84.9001 204.932 88.4703C205.854 92.0405 206.314 96.0137 206.314 100.39C208.272 99.1232 210.172 97.7988 212.015 96.4168C213.858 94.9196 215.413 93.5376 216.679 92.2708H223.935C220.825 96.9926 217.543 100.908 214.088 104.018C210.633 107.012 207.293 109.661 204.069 111.964C201.996 116.456 198.829 119.623 194.567 121.466C190.306 123.308 185.872 124.23 181.266 124.23C176.083 124.23 171.649 123.539 167.964 122.157C164.279 120.659 161.227 118.702 158.808 116.283C156.505 113.749 154.777 110.87 153.626 107.646C152.474 104.421 151.898 101.023 151.898 97.4533C151.898 93.5376 152.819 90.4857 154.662 88.2975C156.62 85.9942 158.866 84.8426 161.399 84.8426C168.424 84.8426 171.937 87.6641 171.937 93.3073C171.937 95.15 171.304 96.7047 170.037 97.9716C168.77 99.2384 167.158 99.8718 165.2 99.8718C164.278 99.8718 163.3 99.7566 162.263 99.5263C161.342 99.1808 160.593 98.5474 160.017 97.6261C160.939 101.657 162.436 104.824 164.509 107.127C166.697 109.431 169.461 110.582 172.801 110.582C175.68 110.582 177.811 109.891 179.193 108.509C180.575 107.012 181.266 104.478 181.266 100.908C181.266 97.1078 180.92 93.7104 180.229 90.7161C179.653 87.6066 178.732 84.2091 177.465 80.5238C176.198 76.8385 174.644 72.4621 172.801 67.3948C170.958 62.2123 168.885 55.5326 166.582 47.3558C160.823 59.6786 153.222 67.5675 143.779 71.0225C143.779 71.9439 143.779 72.8652 143.779 73.7865C143.894 74.5927 143.952 75.4565 143.952 76.3778C143.952 83.0575 143.376 89.334 142.224 95.2076C141.072 100.966 139.115 106.033 136.351 110.41C133.702 114.671 130.247 118.068 125.986 120.602C121.724 123.02 116.484 124.23 110.265 124.23C106.004 124.23 101.916 123.596 98 122.329C94.1995 120.947 90.8021 118.759 87.8078 115.765C84.8134 112.655 82.3949 108.624 80.5523 103.672C78.8248 98.605 77.961 92.4436 77.961 85.188C77.961 80.2359 78.4793 74.9382 79.5158 69.295C80.5523 63.5367 82.4525 58.1814 85.2165 53.2293C87.9805 48.2771 91.7234 44.1887 96.4453 40.964C101.282 37.6242 107.444 35.9543 114.93 35.9543C122.646 35.9543 128.807 38.0273 133.414 42.1733C138.136 46.3193 141.303 52.9989 142.915 62.2123C146.946 61.2909 150.574 58.5269 153.798 53.9203C157.138 49.1984 160.305 42.8643 163.3 34.9177L188.176 31.4627ZM115.102 107.991C117.521 107.991 119.594 107.185 121.321 105.573C123.164 103.845 124.661 101.542 125.813 98.6626C126.964 95.6682 127.771 92.1556 128.231 88.1248C128.807 84.094 129.095 79.7176 129.095 74.9958V72.75C124.488 71.7135 122.185 68.3161 122.185 62.5578C122.185 58.8724 123.682 56.4539 126.677 55.3023C125.41 51.6169 123.855 49.1984 122.012 48.0468C120.285 46.8951 118.788 46.3193 117.521 46.3193C114.987 46.3193 112.799 47.5285 110.956 49.947C109.229 52.2504 107.789 55.2447 106.638 58.93C105.486 62.5002 104.622 66.4734 104.046 70.8498C103.586 75.2261 103.355 79.4297 103.355 83.4605C103.355 88.6431 103.701 92.8466 104.392 96.0713C105.198 99.296 106.177 101.772 107.329 103.5C108.48 105.227 109.747 106.436 111.129 107.127C112.511 107.703 113.835 107.991 115.102 107.991Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M239.554 9.52348V36.818H250.092V43.728H239.554V95.5531C239.554 100.39 240.187 103.615 241.454 105.227C242.836 106.724 245.197 107.473 248.537 107.473C251.877 107.473 254.641 106.033 256.829 103.154C259.132 100.275 260.457 96.6471 260.802 92.2708H268.058C267.136 99.296 265.524 104.939 263.221 109.2C260.917 113.346 258.326 116.571 255.447 118.874C252.568 121.062 249.631 122.502 246.637 123.193C243.642 123.884 240.993 124.23 238.69 124.23C229.822 124.23 223.603 121.811 220.033 116.974C216.463 112.022 214.678 105.515 214.678 97.4533V43.728H209.15V36.818H214.678V12.9785L239.554 9.52348Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M258.833 13.8422C258.833 10.0417 260.158 6.81706 262.806 4.16823C265.455 1.40422 268.68 0.0222168 272.48 0.0222168C276.281 0.0222168 279.506 1.40422 282.154 4.16823C284.918 6.81706 286.3 10.0417 286.3 13.8422C286.3 17.6427 284.918 20.8674 282.154 23.5162C279.506 26.1651 276.281 27.4895 272.48 27.4895C268.68 27.4895 265.455 26.1651 262.806 23.5162C260.158 20.8674 258.833 17.6427 258.833 13.8422ZM285.609 36.818V95.5531C285.609 100.39 286.243 103.615 287.51 105.227C288.892 106.724 291.253 107.473 294.592 107.473C296.09 107.473 297.184 107.358 297.875 107.127C298.681 106.897 299.372 106.667 299.948 106.436C300.063 107.012 300.12 107.588 300.12 108.164C300.12 108.74 300.12 109.315 300.12 109.891C300.12 112.77 299.602 115.131 298.566 116.974C297.644 118.817 296.377 120.314 294.765 121.466C293.268 122.502 291.598 123.193 289.755 123.539C288.028 123.999 286.358 124.23 284.746 124.23C275.878 124.23 269.659 121.811 266.089 116.974C262.518 112.022 260.733 105.515 260.733 97.4533V36.818H285.609ZM351.773 107.473C350.391 107.358 349.354 106.897 348.663 106.091C347.972 105.169 347.627 104.133 347.627 102.981C347.627 101.484 348.26 100.045 349.527 98.6626C350.794 97.1654 352.867 96.4168 355.746 96.4168C358.971 96.4168 361.389 97.5109 363.001 99.6991C364.614 101.772 365.42 104.248 365.42 107.127C365.42 108.97 365.074 110.87 364.383 112.828C363.692 114.671 362.598 116.398 361.101 118.011C359.604 119.508 357.761 120.775 355.573 121.811C353.385 122.732 350.851 123.193 347.972 123.193H300.293L334.152 46.1465H321.369C318.835 46.1465 316.704 46.3193 314.977 46.6648C313.365 46.8951 312.558 47.5285 312.558 48.565C312.558 49.0257 312.674 49.256 312.904 49.256C313.249 49.256 313.595 49.3712 313.94 49.6015C314.401 49.8318 314.747 50.2925 314.977 50.9835C315.322 51.6745 315.495 52.8838 315.495 54.6113C315.495 57.1449 314.689 58.9876 313.077 60.1393C311.579 61.2909 309.852 61.8668 307.894 61.8668C305.591 61.8668 303.345 61.1182 301.157 59.621C299.084 58.0087 298.047 55.5902 298.047 52.3655C298.047 50.638 298.393 48.9105 299.084 47.183C299.775 45.3403 300.811 43.6704 302.193 42.1733C303.575 40.5609 305.303 39.2941 307.376 38.3728C309.449 37.3363 311.867 36.818 314.631 36.818H362.138L329.142 109.891C329.833 109.891 330.812 109.949 332.079 110.064C333.346 110.179 334.67 110.294 336.052 110.41C337.55 110.525 338.989 110.64 340.371 110.755C341.868 110.87 343.193 110.928 344.344 110.928C346.417 110.928 348.145 110.697 349.527 110.237C351.024 109.776 351.773 108.855 351.773 107.473Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
'Gitroom'
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{user?.orgId &&
|
||||
(user.tier !== 'FREE' || !isGeneral || !billingEnabled) ? (
|
||||
<TopMenu />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div
|
||||
id="systray-buttons"
|
||||
className="flex items-center justify-self-end gap-[8px] order-2 md:order-3"
|
||||
>
|
||||
<ModeComponent />
|
||||
<SettingsComponent />
|
||||
<NotificationComponent />
|
||||
<OrganizationSelector />
|
||||
</div>
|
||||
</nav>
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1 rounded-3xl px-0 py-[17px] flex flex-col">
|
||||
{user.tier === 'FREE' && isGeneral && billingEnabled ? (
|
||||
<>
|
||||
<div className="text-center mb-[20px] text-xl [@media(max-width:1024px)]:text-xl">
|
||||
<h1 className="text-3xl [@media(max-width:1024px)]:text-xl">
|
||||
Join 1000+ Entrepreneurs Who Use Postiz
|
||||
<br />
|
||||
To Manage All Your Social Media Channels
|
||||
</h1>
|
||||
<br />
|
||||
{user?.allowTrial && (
|
||||
<div className="table mx-auto">
|
||||
<div className="flex gap-[5px] items-center">
|
||||
<div>
|
||||
|
|
@ -203,20 +212,20 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
<div>Cancel anytime, hassle-free</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BillingComponent />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Title />
|
||||
<div className="flex flex-1 flex-col">{children}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BillingComponent />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Title />
|
||||
<div className="flex flex-1 flex-col">{children}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</MantineWrapper>
|
||||
</ContextWrapper>
|
||||
</div>
|
||||
</MantineWrapper>
|
||||
</CopilotKit>
|
||||
</ContextWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
'use client';
|
||||
import ReactLoading from 'react-loading';
|
||||
import { FC } from 'react';
|
||||
|
||||
export const LoadingComponent = () => {
|
||||
return <div className="flex-1 flex justify-center pt-[100px]"><ReactLoading type="spin" color="#fff" width={100} height={100} /></div>;
|
||||
export const LoadingComponent: FC<{width?: number, height?: number}> = (props) => {
|
||||
return (
|
||||
<div className="flex-1 flex justify-center pt-[100px]">
|
||||
<ReactLoading type="spin" color="#fff" width={props.width || 100} height={props.height || 100} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -88,113 +89,114 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
|
|||
!getRef && 'p-[32px] rounded-[4px] border border-customColor6'
|
||||
)}
|
||||
>
|
||||
{!getRef && (
|
||||
<button
|
||||
onClick={close}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{!getRef && (
|
||||
<div className="text-[24px] font-[600]">Profile Settings</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<div className="text-[20px] font-[500]">Profile</div>
|
||||
<div className="text-[14px] text-customColor18 font-[400]">
|
||||
Add profile information
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[4px] border border-customColor6 p-[24px] flex flex-col">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="w-[455px]">
|
||||
<Input label="Full Name" name="fullname" />
|
||||
</div>
|
||||
<div className="flex gap-[8px] mb-[10px]">
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-customColor38">
|
||||
{!!picture?.path && (
|
||||
<img
|
||||
src={picture?.path}
|
||||
alt="profile"
|
||||
className="w-full h-full rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-[2px]">
|
||||
<div className="text-[14px]">Profile Picture</div>
|
||||
<div className="flex gap-[8px]">
|
||||
<button
|
||||
className="h-[24px] w-[120px] bg-forth rounded-[4px] flex justify-center gap-[4px] items-center cursor-pointer"
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M12.25 8.3126V11.3751C12.25 11.6072 12.1578 11.8297 11.9937 11.9938C11.8296 12.1579 11.6071 12.2501 11.375 12.2501H2.625C2.39294 12.2501 2.17038 12.1579 2.00628 11.9938C1.84219 11.8297 1.75 11.6072 1.75 11.3751V8.3126C1.75 8.19657 1.79609 8.08529 1.87814 8.00324C1.96019 7.92119 2.07147 7.8751 2.1875 7.8751C2.30353 7.8751 2.41481 7.92119 2.49686 8.00324C2.57891 8.08529 2.625 8.19657 2.625 8.3126V11.3751H11.375V8.3126C11.375 8.19657 11.4211 8.08529 11.5031 8.00324C11.5852 7.92119 11.6965 7.8751 11.8125 7.8751C11.9285 7.8751 12.0398 7.92119 12.1219 8.00324C12.2039 8.08529 12.25 8.19657 12.25 8.3126ZM5.12203 4.68463L6.5625 3.24362V8.3126C6.5625 8.42863 6.60859 8.53991 6.69064 8.62196C6.77269 8.70401 6.88397 8.7501 7 8.7501C7.11603 8.7501 7.22731 8.70401 7.30936 8.62196C7.39141 8.53991 7.4375 8.42863 7.4375 8.3126V3.24362L8.87797 4.68463C8.96006 4.76672 9.0714 4.81284 9.1875 4.81284C9.3036 4.81284 9.41494 4.76672 9.49703 4.68463C9.57912 4.60254 9.62524 4.4912 9.62524 4.3751C9.62524 4.259 9.57912 4.14766 9.49703 4.06557L7.30953 1.87807C7.2689 1.83739 7.22065 1.80512 7.16754 1.78311C7.11442 1.76109 7.05749 1.74976 7 1.74976C6.94251 1.74976 6.88558 1.76109 6.83246 1.78311C6.77935 1.80512 6.7311 1.83739 6.69047 1.87807L4.50297 4.06557C4.42088 4.14766 4.37476 4.259 4.37476 4.3751C4.37476 4.4912 4.42088 4.60254 4.50297 4.68463C4.58506 4.76672 4.6964 4.81284 4.8125 4.81284C4.9286 4.81284 5.03994 4.76672 5.12203 4.68463Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] text-white" onClick={openMedia}>
|
||||
Upload image
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className="h-[24px] w-[88px] rounded-[4px] border-2 border-customColor21 hover:text-red-600 flex justify-center items-center gap-[4px]"
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] " onClick={remove}>
|
||||
Remove
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Textarea label="Bio" name="bio" className="resize-none" />
|
||||
</div>
|
||||
</div>
|
||||
{!getRef && (
|
||||
<div className="justify-end flex">
|
||||
<Button type="submit" className='rounded-md'>Save</Button>
|
||||
</div>
|
||||
)}
|
||||
{/*{!getRef && (*/}
|
||||
{/* <button*/}
|
||||
{/* onClick={close}*/}
|
||||
{/* className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"*/}
|
||||
{/* type="button"*/}
|
||||
{/* >*/}
|
||||
{/* <svg*/}
|
||||
{/* viewBox="0 0 15 15"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* width="16"*/}
|
||||
{/* height="16"*/}
|
||||
{/* >*/}
|
||||
{/* <path*/}
|
||||
{/* d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"*/}
|
||||
{/* fill="currentColor"*/}
|
||||
{/* fillRule="evenodd"*/}
|
||||
{/* clipRule="evenodd"*/}
|
||||
{/* ></path>*/}
|
||||
{/* </svg>*/}
|
||||
{/* </button>*/}
|
||||
{/*)}*/}
|
||||
{/*{!getRef && (*/}
|
||||
{/* <div className="text-[24px] font-[600]">Profile Settings</div>*/}
|
||||
{/*)}*/}
|
||||
{/*<div className="flex flex-col gap-[4px]">*/}
|
||||
{/* <div className="text-[20px] font-[500]">Profile</div>*/}
|
||||
{/* <div className="text-[14px] text-customColor18 font-[400]">*/}
|
||||
{/* Add profile information*/}
|
||||
{/* </div>*/}
|
||||
{/*</div>*/}
|
||||
{/*<div className="rounded-[4px] border border-customColor6 p-[24px] flex flex-col">*/}
|
||||
{/* <div className="flex justify-between items-center">*/}
|
||||
{/* <div className="w-[455px]">*/}
|
||||
{/* <Input label="Full Name" name="fullname" />*/}
|
||||
{/* </div>*/}
|
||||
{/* <div className="flex gap-[8px] mb-[10px]">*/}
|
||||
{/* <div className="w-[48px] h-[48px] rounded-full bg-customColor38">*/}
|
||||
{/* {!!picture?.path && (*/}
|
||||
{/* <img*/}
|
||||
{/* src={picture?.path}*/}
|
||||
{/* alt="profile"*/}
|
||||
{/* className="w-full h-full rounded-full"*/}
|
||||
{/* />*/}
|
||||
{/* )}*/}
|
||||
{/* </div>*/}
|
||||
{/* <div className="flex flex-col gap-[2px]">*/}
|
||||
{/* <div className="text-[14px]">Profile Picture</div>*/}
|
||||
{/* <div className="flex gap-[8px]">*/}
|
||||
{/* <button*/}
|
||||
{/* className="h-[24px] w-[120px] bg-forth rounded-[4px] flex justify-center gap-[4px] items-center cursor-pointer"*/}
|
||||
{/* type="button"*/}
|
||||
{/* >*/}
|
||||
{/* <div>*/}
|
||||
{/* <svg*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* width="14"*/}
|
||||
{/* height="14"*/}
|
||||
{/* viewBox="0 0 14 14"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* >*/}
|
||||
{/* <path*/}
|
||||
{/* d="M12.25 8.3126V11.3751C12.25 11.6072 12.1578 11.8297 11.9937 11.9938C11.8296 12.1579 11.6071 12.2501 11.375 12.2501H2.625C2.39294 12.2501 2.17038 12.1579 2.00628 11.9938C1.84219 11.8297 1.75 11.6072 1.75 11.3751V8.3126C1.75 8.19657 1.79609 8.08529 1.87814 8.00324C1.96019 7.92119 2.07147 7.8751 2.1875 7.8751C2.30353 7.8751 2.41481 7.92119 2.49686 8.00324C2.57891 8.08529 2.625 8.19657 2.625 8.3126V11.3751H11.375V8.3126C11.375 8.19657 11.4211 8.08529 11.5031 8.00324C11.5852 7.92119 11.6965 7.8751 11.8125 7.8751C11.9285 7.8751 12.0398 7.92119 12.1219 8.00324C12.2039 8.08529 12.25 8.19657 12.25 8.3126ZM5.12203 4.68463L6.5625 3.24362V8.3126C6.5625 8.42863 6.60859 8.53991 6.69064 8.62196C6.77269 8.70401 6.88397 8.7501 7 8.7501C7.11603 8.7501 7.22731 8.70401 7.30936 8.62196C7.39141 8.53991 7.4375 8.42863 7.4375 8.3126V3.24362L8.87797 4.68463C8.96006 4.76672 9.0714 4.81284 9.1875 4.81284C9.3036 4.81284 9.41494 4.76672 9.49703 4.68463C9.57912 4.60254 9.62524 4.4912 9.62524 4.3751C9.62524 4.259 9.57912 4.14766 9.49703 4.06557L7.30953 1.87807C7.2689 1.83739 7.22065 1.80512 7.16754 1.78311C7.11442 1.76109 7.05749 1.74976 7 1.74976C6.94251 1.74976 6.88558 1.76109 6.83246 1.78311C6.77935 1.80512 6.7311 1.83739 6.69047 1.87807L4.50297 4.06557C4.42088 4.14766 4.37476 4.259 4.37476 4.3751C4.37476 4.4912 4.42088 4.60254 4.50297 4.68463C4.58506 4.76672 4.6964 4.81284 4.8125 4.81284C4.9286 4.81284 5.03994 4.76672 5.12203 4.68463Z"*/}
|
||||
{/* fill="white"*/}
|
||||
{/* />*/}
|
||||
{/* </svg>*/}
|
||||
{/* </div>*/}
|
||||
{/* <div className="text-[12px] text-white" onClick={openMedia}>*/}
|
||||
{/* Upload image*/}
|
||||
{/* </div>*/}
|
||||
{/* </button>*/}
|
||||
{/* <button*/}
|
||||
{/* className="h-[24px] w-[88px] rounded-[4px] border-2 border-customColor21 hover:text-red-600 flex justify-center items-center gap-[4px]"*/}
|
||||
{/* type="button"*/}
|
||||
{/* >*/}
|
||||
{/* <div>*/}
|
||||
{/* <svg*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg"*/}
|
||||
{/* width="14"*/}
|
||||
{/* height="14"*/}
|
||||
{/* viewBox="0 0 14 14"*/}
|
||||
{/* fill="none"*/}
|
||||
{/* >*/}
|
||||
{/* <path*/}
|
||||
{/* d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"*/}
|
||||
{/* fill="currentColor"*/}
|
||||
{/* />*/}
|
||||
{/* </svg>*/}
|
||||
{/* </div>*/}
|
||||
{/* <div className="text-[12px] " onClick={remove}>*/}
|
||||
{/* Remove*/}
|
||||
{/* </div>*/}
|
||||
{/* </button>*/}
|
||||
{/* </div>*/}
|
||||
{/* </div>*/}
|
||||
{/* </div>*/}
|
||||
{/* </div>*/}
|
||||
{/* <div>*/}
|
||||
{/* <Textarea label="Bio" name="bio" className="resize-none" />*/}
|
||||
{/* </div>*/}
|
||||
{/*</div>*/}
|
||||
{/*{!getRef && (*/}
|
||||
{/* <div className="justify-end flex">*/}
|
||||
{/* <Button type="submit" className='rounded-md'>Save</Button>*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
{!!user?.tier?.team_members && isGeneral && <TeamsComponent />}
|
||||
{!!user?.tier?.public_api && isGeneral && showLogout && <PublicComponent />}
|
||||
{showLogout && <LogoutComponent />}
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ export const UserContext = createContext<
|
|||
| (User & {
|
||||
orgId: string;
|
||||
tier: PricingInnerInterface;
|
||||
publicApi: string;
|
||||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
totalChannels: number;
|
||||
isLifetime?: boolean;
|
||||
impersonate: boolean;
|
||||
allowTrial: boolean;
|
||||
})
|
||||
>(undefined);
|
||||
|
||||
|
|
@ -24,6 +26,7 @@ export const ContextWrapper: FC<{
|
|||
orgId: string;
|
||||
tier: 'FREE' | 'STANDARD' | 'PRO' | 'ULTIMATE' | 'TEAM';
|
||||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
publicApi: string;
|
||||
totalChannels: number;
|
||||
};
|
||||
children: ReactNode;
|
||||
|
|
|
|||
|
|
@ -282,6 +282,7 @@ export const Post: FC<{
|
|||
children: (
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
allIntegrations: [],
|
||||
date: dayjs(),
|
||||
integration,
|
||||
value: [],
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
|||
import { MultipartFileUploader } from '@gitroom/frontend/components/media/new.uploader';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { AiImage } from '@gitroom/frontend/components/launches/ai.image';
|
||||
const Polonto = dynamic(
|
||||
() => import('@gitroom/frontend/components/launches/polonto')
|
||||
);
|
||||
|
|
@ -83,6 +84,11 @@ export const MediaBox: FC<{
|
|||
|
||||
const { data, mutate } = useSWR('get-media', loadMedia);
|
||||
|
||||
const finishUpload = useCallback(async () => {
|
||||
const newData = await mutate();
|
||||
setNewMedia(newData.results[0])();
|
||||
}, [mutate, setNewMedia]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.pages) {
|
||||
setPages(data.pages);
|
||||
|
|
@ -127,7 +133,7 @@ export const MediaBox: FC<{
|
|||
>
|
||||
<div className="relative flex gap-2 items-center justify-center">
|
||||
<MultipartFileUploader
|
||||
onUploadSuccess={mutate}
|
||||
onUploadSuccess={finishUpload}
|
||||
allowedFileTypes={
|
||||
type === 'video'
|
||||
? 'video/mp4'
|
||||
|
|
@ -147,12 +153,12 @@ export const MediaBox: FC<{
|
|||
)}
|
||||
>
|
||||
{!mediaList.length && (
|
||||
<div className="flex flex-col text-center">
|
||||
<div className="flex flex-col text-center items-center justify-center mx-auto">
|
||||
<div>You don{"'"}t have any assets yet.</div>
|
||||
<div>Click the button below to upload one</div>
|
||||
<div className="mt-[10px] justify-center items-center flex flex-col-reverse gap-[10px]">
|
||||
<MultipartFileUploader
|
||||
onUploadSuccess={mutate}
|
||||
onUploadSuccess={finishUpload}
|
||||
allowedFileTypes={
|
||||
type === 'video'
|
||||
? 'video/mp4'
|
||||
|
|
@ -185,7 +191,7 @@ export const MediaBox: FC<{
|
|||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(media.path)}
|
||||
alt='media'
|
||||
alt="media"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -207,19 +213,21 @@ export const MultiMediaComponent: FC<{
|
|||
label: string;
|
||||
description: string;
|
||||
value?: Array<{ path: string; id: string }>;
|
||||
text: string;
|
||||
name: string;
|
||||
error?: any;
|
||||
onChange: (event: {
|
||||
target: { name: string; value?: Array<{ id: string; path: string }> };
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { name, label, error, description, onChange, value } = props;
|
||||
const { name, label, error, text, description, onChange, value } = props;
|
||||
const user = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setCurrentMedia(value);
|
||||
}
|
||||
}, []);
|
||||
}, [value]);
|
||||
|
||||
const [modal, setShowModal] = useState(false);
|
||||
const [mediaModal, setMediaModal] = useState(false);
|
||||
|
|
@ -261,7 +269,7 @@ export const MultiMediaComponent: FC<{
|
|||
<>
|
||||
<div className="flex flex-col gap-[8px] bg-input rounded-bl-[8px]">
|
||||
{modal && <MediaBox setMedia={changeMedia} closeModal={showModal} />}
|
||||
{mediaModal && !!user?.tier?.ai && (
|
||||
{mediaModal && !!user?.tier?.ai && (
|
||||
<Polonto setMedia={changeMedia} closeModal={closeDesignModal} />
|
||||
)}
|
||||
<div className="flex gap-[10px]">
|
||||
|
|
@ -270,44 +278,59 @@ export const MultiMediaComponent: FC<{
|
|||
onClick={showModal}
|
||||
className="ml-[10px] rounded-[4px] mb-[10px] gap-[8px] !text-primary justify-center items-center w-[127px] flex border border-dashed border-customColor21 bg-input"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
className="!text-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex gap-[5px] items-center">
|
||||
<div>
|
||||
<svg
|
||||
className="!text-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] !text-current">
|
||||
Insert Media
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] text-primary">Insert Media</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={designMedia}
|
||||
className="ml-[10px] rounded-[4px] mb-[10px] gap-[8px] justify-center items-center w-[127px] flex border border-dashed border-customColor21 !bg-customColor45"
|
||||
className="ml-[10px] rounded-[4px] mb-[10px] gap-[8px] !text-primary justify-center items-center w-[127px] flex border border-dashed border-customColor21 bg-input"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex gap-[5px] items-center">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] !text-current">
|
||||
Design Media
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] !text-white">Design Media</div>
|
||||
</Button>
|
||||
|
||||
{!!user?.tier?.ai && (
|
||||
<AiImage
|
||||
value={text}
|
||||
onChange={changeMedia}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!currentMedia &&
|
||||
|
|
@ -354,7 +377,8 @@ export const MediaComponent: FC<{
|
|||
width?: number;
|
||||
height?: number;
|
||||
}> = (props) => {
|
||||
const { name, type, label, description, onChange, value, width, height } = props;
|
||||
const { name, type, label, description, onChange, value, width, height } =
|
||||
props;
|
||||
const { getValues } = useSettings();
|
||||
const user = useUser();
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import Uppy, { UploadResult } from '@uppy/core';
|
||||
// @ts-ignore
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { getUppyUploadPlugin } from '@gitroom/react/helpers/uppy.upload';
|
||||
import { getUppyUploadPlugin } from '@gitroom/react/helpers/uppy.upload';
|
||||
import { FileInput, ProgressBar } from '@uppy/react';
|
||||
|
||||
// Uppy styles
|
||||
import '@uppy/core/dist/style.min.css';
|
||||
import '@uppy/dashboard/dist/style.min.css';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import Compressor from '@uppy/compressor';
|
||||
|
||||
export function MultipartFileUploader({
|
||||
onUploadSuccess,
|
||||
|
|
@ -22,10 +23,13 @@ export function MultipartFileUploader({
|
|||
const [loaded, setLoaded] = useState(false);
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const onUploadSuccessExtended = useCallback((result: UploadResult<any,any>) => {
|
||||
setReload(true);
|
||||
onUploadSuccess(result);
|
||||
}, [onUploadSuccess]);
|
||||
const onUploadSuccessExtended = useCallback(
|
||||
(result: UploadResult<any, any>) => {
|
||||
setReload(true);
|
||||
onUploadSuccess(result);
|
||||
},
|
||||
[onUploadSuccess]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (reload) {
|
||||
|
|
@ -51,18 +55,16 @@ export function MultipartFileUploader({
|
|||
);
|
||||
}
|
||||
|
||||
export function MultipartFileUploaderAfter({
|
||||
onUploadSuccess,
|
||||
allowedFileTypes,
|
||||
}: {
|
||||
export function useUppyUploader(props: {
|
||||
// @ts-ignore
|
||||
onUploadSuccess: (result: UploadResult) => void;
|
||||
allowedFileTypes: string;
|
||||
}) {
|
||||
const {storageProvider, backendUrl} = useVariables();
|
||||
const { storageProvider, backendUrl } = useVariables();
|
||||
const { onUploadSuccess, allowedFileTypes } = props;
|
||||
const fetch = useFetch();
|
||||
|
||||
const uppy = useMemo(() => {
|
||||
|
||||
return useMemo(() => {
|
||||
const uppy2 = new Uppy({
|
||||
autoProceed: true,
|
||||
restrictions: {
|
||||
|
|
@ -71,16 +73,26 @@ export function MultipartFileUploaderAfter({
|
|||
maxFileSize: 1000000000,
|
||||
},
|
||||
});
|
||||
|
||||
const { plugin, options } = getUppyUploadPlugin(storageProvider, fetch, backendUrl)
|
||||
uppy2.use(plugin, options)
|
||||
|
||||
const { plugin, options } = getUppyUploadPlugin(
|
||||
storageProvider,
|
||||
fetch,
|
||||
backendUrl
|
||||
);
|
||||
uppy2.use(plugin, options);
|
||||
uppy2.use(Compressor, {
|
||||
convertTypes: ['image/jpeg'],
|
||||
maxWidth: 1000,
|
||||
maxHeight: 1000,
|
||||
quality: 1,
|
||||
});
|
||||
// Set additional metadata when a file is added
|
||||
uppy2.on('file-added', (file) => {
|
||||
uppy2.setFileMeta(file.id, {
|
||||
useCloudflare: storageProvider === 'cloudflare' ? 'true' : 'false', // Example of adding a custom field
|
||||
// Add more fields as needed
|
||||
});
|
||||
uppy2.setFileMeta(file.id, {
|
||||
useCloudflare: storageProvider === 'cloudflare' ? 'true' : 'false', // Example of adding a custom field
|
||||
// Add more fields as needed
|
||||
});
|
||||
});
|
||||
|
||||
uppy2.on('complete', (result) => {
|
||||
onUploadSuccess(result);
|
||||
|
|
@ -88,9 +100,9 @@ export function MultipartFileUploaderAfter({
|
|||
|
||||
uppy2.on('upload-success', (file, response) => {
|
||||
// @ts-ignore
|
||||
uppy.setFileState(file.id, {
|
||||
uppy2.setFileState(file.id, {
|
||||
// @ts-ignore
|
||||
progress: uppy.getState().files[file.id].progress,
|
||||
progress: uppy2.getState().files[file.id].progress,
|
||||
// @ts-ignore
|
||||
uploadURL: response.body.Location,
|
||||
response: response,
|
||||
|
|
@ -100,6 +112,17 @@ export function MultipartFileUploaderAfter({
|
|||
|
||||
return uppy2;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function MultipartFileUploaderAfter({
|
||||
onUploadSuccess,
|
||||
allowedFileTypes,
|
||||
}: {
|
||||
// @ts-ignore
|
||||
onUploadSuccess: (result: UploadResult) => void;
|
||||
allowedFileTypes: string;
|
||||
}) {
|
||||
const uppy = useUppyUploader({ onUploadSuccess, allowedFileTypes });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -111,9 +134,9 @@ export function MultipartFileUploaderAfter({
|
|||
strings: {
|
||||
chooseFiles: 'Upload',
|
||||
},
|
||||
pluralize: (n) => n
|
||||
pluralize: (n) => n,
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const NotificationOpenComponent = () => {
|
|||
const { data, isLoading } = useSWR('notifications', loadNotifications);
|
||||
|
||||
return (
|
||||
<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 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-[20]">
|
||||
<div className={`p-[16px] border-b border-tableBorder ${interClass} font-bold`}>
|
||||
Notifications
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
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';
|
||||
|
|
@ -14,9 +8,14 @@ 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 {
|
||||
ApiModal,
|
||||
CustomVariables,
|
||||
} from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export const ConnectChannels: FC = () => {
|
||||
const fetch = useFetch();
|
||||
|
|
@ -24,6 +23,8 @@ export const ConnectChannels: FC = () => {
|
|||
const router = useRouter();
|
||||
const [identifier, setIdentifier] = useState<any>(undefined);
|
||||
const [popup, setPopups] = useState<undefined | string[]>(undefined);
|
||||
const toaster = useToaster();
|
||||
const [showCustom, setShowCustom] = useState<any>(undefined);
|
||||
|
||||
const getIntegrations = useCallback(async () => {
|
||||
return (await fetch('/integrations')).json();
|
||||
|
|
@ -31,17 +32,117 @@ export const ConnectChannels: FC = () => {
|
|||
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const getSocialLink = useCallback(
|
||||
(identifier: string) => async () => {
|
||||
// const getSocialLink = useCallback(
|
||||
// (identifier: string) => async () => {
|
||||
// const { url } = await (
|
||||
// await fetch('/integrations/social/' + identifier)
|
||||
// ).json();
|
||||
//
|
||||
// window.open(url, 'Social Connect', 'width=700,height=700');
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
const refreshChannel = useCallback(
|
||||
(integration: Integration & { identifier: string }) => async () => {
|
||||
const { url } = await (
|
||||
await fetch('/integrations/social/' + identifier)
|
||||
await fetch(
|
||||
`/integrations/social/${integration.identifier}?refresh=${integration.internalId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
window.open(url, 'Social Connect', 'width=700,height=700');
|
||||
window.location.href = url;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
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));
|
||||
|
|
@ -114,6 +215,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]">
|
||||
|
|
@ -141,7 +249,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>
|
||||
|
|
@ -246,6 +358,7 @@ export const ConnectChannels: FC = () => {
|
|||
mutate={mutate}
|
||||
onChange={update}
|
||||
id={integration.id}
|
||||
refreshChannel={refreshChannel}
|
||||
canEnable={
|
||||
user?.totalChannels! > totalNonDisabledChannels &&
|
||||
integration.disabled
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ const Welcome: FC = () => {
|
|||
<div className="bg-sixth p-[32px] w-full max-w-[920px] mx-auto flex flex-col gap-[24px] rounded-[4px] border border-customColor6 relative">
|
||||
<h1 className="text-[24px]">Onboarding</h1>
|
||||
<div className="flex">
|
||||
<Step title="Profile" step={1} currentStep={step} lastStep={lastStep} />
|
||||
<Step title="Connect Channels" step={1} currentStep={step} lastStep={lastStep} />
|
||||
<StepSpace />
|
||||
{!isGeneral && (
|
||||
<>
|
||||
|
|
@ -168,19 +168,19 @@ const Welcome: FC = () => {
|
|||
title="Connect Github"
|
||||
step={2}
|
||||
currentStep={step}
|
||||
lastStep={4}
|
||||
lastStep={2}
|
||||
/>
|
||||
<StepSpace />
|
||||
</>
|
||||
)}
|
||||
<Step
|
||||
title="Connect Channels"
|
||||
title="Finish"
|
||||
step={3 - (isGeneral ? 1 : 0)}
|
||||
currentStep={step}
|
||||
lastStep={4}
|
||||
lastStep={2}
|
||||
/>
|
||||
<StepSpace />
|
||||
<Step title="Finish" step={4 - (isGeneral ? 1 : 0)} currentStep={step} lastStep={lastStep} />
|
||||
{/*<StepSpace />*/}
|
||||
{/*<Step title="Finish" step={4 - (isGeneral ? 1 : 0)} currentStep={step} lastStep={lastStep} />*/}
|
||||
{seller && (
|
||||
<>
|
||||
<StepSpace />
|
||||
|
|
@ -193,36 +193,25 @@ const Welcome: FC = () => {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
{step === 1 && (
|
||||
<>
|
||||
<div>
|
||||
<SettingsPopup getRef={ref} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-[8px]">
|
||||
<SkipOnboarding />
|
||||
<Button onClick={firstNext}>Next</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === 2 && !isGeneral && (
|
||||
{step === 1 && !isGeneral && (
|
||||
<div>
|
||||
<GithubOnboarding />
|
||||
<div className="flex justify-end gap-[8px]">
|
||||
<SkipOnboarding />
|
||||
{/*<SkipOnboarding />*/}
|
||||
<Button onClick={nextStep()}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 2 - (isGeneral ? 1 : 0) && (
|
||||
<div>
|
||||
<ConnectChannels />
|
||||
<div className="flex justify-end gap-[8px]">
|
||||
{/*<SkipOnboarding />*/}
|
||||
<Button onClick={nextStep()}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 3 - (isGeneral ? 1 : 0) && (
|
||||
<div>
|
||||
<ConnectChannels />
|
||||
<div className="flex justify-end gap-[8px]">
|
||||
<SkipOnboarding />
|
||||
<Button onClick={nextStep()}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 4 - (isGeneral ? 1 : 0) && (
|
||||
<div className="items-center justify-center flex flex-col gap-[24px]">
|
||||
<div className="items-center justify-center flex flex-col">
|
||||
<img src="/success.svg" alt="success" />
|
||||
|
|
@ -238,14 +227,14 @@ const Welcome: FC = () => {
|
|||
<Button onClick={goToLaunches}>Schedule a new post</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-[8px]">
|
||||
<Button onClick={buyPosts}>Buy posts from Influencers</Button>
|
||||
<Button onClick={sellPosts}>Sell your services</Button>
|
||||
</div>
|
||||
{/*<div className="grid grid-cols-2 gap-[8px]">*/}
|
||||
{/* /!*<Button onClick={buyPosts}>Buy posts from Influencers</Button>*!/*/}
|
||||
{/* /!*<Button onClick={sellPosts}>Sell your services</Button>*!/*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 5 - (isGeneral ? 1 : 0) && (
|
||||
{step === 4 - (isGeneral ? 1 : 0) && (
|
||||
<div>
|
||||
<div className="text-[24px] mb-[24px]">To sell posts you would have to:</div>
|
||||
<ul>
|
||||
|
|
|
|||
|
|
@ -32,19 +32,34 @@ export const RenderAnalytics: FC<{ integration: Integration; date: number }> = (
|
|||
revalidateOnMount: true,
|
||||
});
|
||||
|
||||
const refreshChannel = useCallback(
|
||||
(integration: Integration & { identifier: string }) => async () => {
|
||||
const { url } = await (
|
||||
await fetch(
|
||||
`/integrations/social/${integration.identifier}?refresh=${integration.internalId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
window.location.href = url;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const total = useMemo(() => {
|
||||
return data?.map(
|
||||
(p: any) => {
|
||||
const value = (p?.data.reduce((acc: number, curr: any) => acc + curr.total, 0) || 0) /
|
||||
(p.average ? p.data.length : 1);
|
||||
return data?.map((p: any) => {
|
||||
const value =
|
||||
(p?.data.reduce((acc: number, curr: any) => acc + curr.total, 0) || 0) /
|
||||
(p.average ? p.data.length : 1);
|
||||
|
||||
if (p.average) {
|
||||
return value.toFixed(2) + '%';
|
||||
}
|
||||
|
||||
return value;
|
||||
if (p.average) {
|
||||
return value.toFixed(2) + '%';
|
||||
}
|
||||
);
|
||||
|
||||
return value;
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -58,7 +73,10 @@ export const RenderAnalytics: FC<{ integration: Integration; date: number }> = (
|
|||
return (
|
||||
<div className="grid grid-cols-3 gap-[20px]">
|
||||
{data?.length === 0 && (
|
||||
<div>This channel needs to be refreshed</div>
|
||||
<div>
|
||||
This channel needs to be refreshed,{' '}
|
||||
<div className="underline hover:font-bold cursor-pointer" onClick={refreshChannel(integration as any)}>click here to refresh</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.map((p: any, index: number) => (
|
||||
<div key={`pl-${index}`} className="flex">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
'use client';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { FieldValues, SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
||||
export const RenderComponents: FC<{ postId: string }> = (props) => {
|
||||
const { postId } = props;
|
||||
const fetch = useFetch();
|
||||
|
||||
const comments = useCallback(async () => {
|
||||
return (await fetch(`/public/posts/${postId}/comments`)).json();
|
||||
}, [postId]);
|
||||
|
||||
const { data, mutate, isLoading } = useSWR('comments', comments);
|
||||
const mapUsers = useMemo(() => {
|
||||
return (data?.comments || []).reduce((all: any, current: any) => {
|
||||
all.users[current.userId] = all.users[current.userId] || all.counter++;
|
||||
return all;
|
||||
}, {users: {}, counter: 1}).users;
|
||||
}, [data]);
|
||||
|
||||
const {handleSubmit, register, setValue} = useForm();
|
||||
const submit: SubmitHandler<FieldValues> = useCallback(async (e) => {
|
||||
setValue('comment', '');
|
||||
await fetch(`/posts/${postId}/comments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(e),
|
||||
});
|
||||
|
||||
mutate();
|
||||
}, [postId, mutate]);
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex space-x-3">
|
||||
<form className="flex-1 space-y-2" onSubmit={handleSubmit(submit)}>
|
||||
<textarea
|
||||
{...register('comment', { required: true })}
|
||||
className="flex w-full px-3 py-2 h-[98px] text-sm ring-offset-background placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 min-h-[80px] resize-none text-white bg-third border border-tableBorder placeholder-gray-500 focus:ring-0"
|
||||
placeholder="Add a comment..."
|
||||
defaultValue={''}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-send mr-2 h-4 w-4"
|
||||
>
|
||||
<path d="m22 2-7 20-4-9-9-4Z" />
|
||||
<path d="M22 2 11 13" />
|
||||
</svg>
|
||||
Post
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{!!data.comments.length && (
|
||||
<h3 className="text-lg font-semibold">Comments</h3>
|
||||
)}
|
||||
{data.comments.map((comment: any) => (
|
||||
<div
|
||||
key={comment.id}
|
||||
className="flex space-x-3 border-t border-tableBorder py-3"
|
||||
>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="text-sm font-semibold">User {mapUsers[comment.userId]}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300">
|
||||
{comment.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const CommentsComponents: FC<{ postId: string }> = (props) => {
|
||||
const user = useUser();
|
||||
const { postId } = props;
|
||||
|
||||
const goToComments = useCallback(() => {
|
||||
window.location.href = `/auth?returnUrl=${window.location.href}`;
|
||||
}, []);
|
||||
|
||||
if (!user?.id) {
|
||||
return (
|
||||
<Button onClick={goToComments}>Login / Register to add comments</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return <RenderComponents postId={postId} />;
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useCallback } from 'react';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export const CopyClient = () => {
|
||||
const toast = useToaster();
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
toast.show('Link copied to clipboard', 'success');
|
||||
copy(window.location.href.split?.('?')?.shift()!);
|
||||
}, []);
|
||||
|
||||
return <Button onClick={copyToClipboard}>Share with a client</Button>;
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { ContextWrapper } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Toaster } from '@gitroom/react/toaster/toaster';
|
||||
|
||||
export const PreviewWrapper = ({ children }: { children: ReactNode }) => {
|
||||
const fetch = useFetch();
|
||||
|
||||
const load = useCallback(async (path: string) => {
|
||||
return await (await fetch(path)).json();
|
||||
}, []);
|
||||
|
||||
const { data: user } = useSWR('/user/self', load, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<ContextWrapper user={user}>
|
||||
<Toaster />
|
||||
{children}
|
||||
</ContextWrapper>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
'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.
|
||||
<br />
|
||||
<a
|
||||
className="underline hover:text-white"
|
||||
href="https://docs.postiz.com/public-api"
|
||||
target="_blank"
|
||||
>
|
||||
Read how to use it over the documentation.
|
||||
</a>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@ import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
|
|||
export async function middleware(request: NextRequest) {
|
||||
const nextUrl = request.nextUrl;
|
||||
const authCookie = request.cookies.get('auth');
|
||||
if (nextUrl.pathname.startsWith('/uploads/')) {
|
||||
if (nextUrl.pathname.startsWith('/uploads/') || nextUrl.pathname.startsWith('/p/') || nextUrl.pathname.startsWith('/icons/')) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
// If the URL is logout, delete the cookie and redirect to login
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ module.exports = {
|
|||
},
|
||||
animation: {
|
||||
fade: 'fadeOut 0.5s ease-in-out',
|
||||
normalFadeIn: 'normalFadeIn 0.5s ease-in-out',
|
||||
normalFadeOut: 'normalFadeOut 0.5s linear 5s forwards',
|
||||
overflow: 'overFlow 0.5s ease-in-out forwards',
|
||||
overflowReverse: 'overFlowReverse 0.5s ease-in-out forwards',
|
||||
|
|
@ -120,6 +121,10 @@ module.exports = {
|
|||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
},
|
||||
normalFadeIn: {
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
},
|
||||
overFlow: {
|
||||
'0%': { overflow: 'hidden' },
|
||||
'99%': { overflow: 'hidden' },
|
||||
|
|
@ -131,11 +136,11 @@ module.exports = {
|
|||
'100%': { overflow: 'hidden' },
|
||||
},
|
||||
fadeDown: {
|
||||
'0%': { opacity: 0, transform: 'translateY(-30px)' },
|
||||
'10%': { opacity: 1, transform: 'translateY(0)' },
|
||||
'85%': { opacity: 1, transform: 'translateY(0)' },
|
||||
'90%': { opacity: 1, transform: 'translateY(10px)' },
|
||||
'100%': { opacity: 0, transform: 'translateY(-30px)' },
|
||||
'0%': { opacity: 0, marginTop: -30 },
|
||||
'10%': { opacity: 1, marginTop: 0 },
|
||||
'85%': { opacity: 1, marginTop: 0 },
|
||||
'90%': { opacity: 1, marginTop: 10 },
|
||||
'100%': { opacity: 0, marginTop: -30 },
|
||||
},
|
||||
normalFadeDown: {
|
||||
'0%': { opacity: 0, transform: 'translateY(-30px)' },
|
||||
|
|
@ -144,12 +149,15 @@ module.exports = {
|
|||
newMessages: {
|
||||
'0%': { backgroundColor: 'var(--color-seventh)', fontWeight: 'bold' },
|
||||
'99%': { backgroundColor: 'var(--color-third)', fontWeight: 'bold' },
|
||||
'100%': { backgroundColor: 'var(--color-third)', fontWeight: 'normal' },
|
||||
'100%': {
|
||||
backgroundColor: 'var(--color-third)',
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
},
|
||||
}),
|
||||
screens: {
|
||||
custom: { raw: '(max-height: 800px)' },
|
||||
xs: { max: '401px'} ,
|
||||
xs: { max: '401px' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/in
|
|||
|
||||
@Controller()
|
||||
export class PlugsController {
|
||||
constructor(
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
constructor(private _integrationService: IntegrationService) {}
|
||||
|
||||
@EventPattern('plugs', Transport.REDIS)
|
||||
async plug(data: {
|
||||
|
|
@ -18,4 +16,17 @@ export class PlugsController {
|
|||
}) {
|
||||
return this._integrationService.processPlugs(data);
|
||||
}
|
||||
|
||||
@EventPattern('internal-plugs', Transport.REDIS)
|
||||
async internalPlug(data: {
|
||||
post: string;
|
||||
originalIntegration: string;
|
||||
integration: string;
|
||||
plugName: string;
|
||||
orgId: string;
|
||||
delay: number;
|
||||
information: any;
|
||||
}) {
|
||||
return this._integrationService.processInternalPlug(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export class PostsController {
|
|||
constructor(private _postsService: PostsService) {}
|
||||
@EventPattern('post', Transport.REDIS)
|
||||
async post(data: { id: string }) {
|
||||
console.log('proceccsing', data);
|
||||
console.log('processing', data);
|
||||
return this._postsService.post(data.id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
export function PostPlug(params: {
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string;
|
||||
pickIntegration: string[];
|
||||
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:internal_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:internal_plug', existingMetadata, target);
|
||||
};
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import twitter from 'twitter-text';
|
|||
export const textSlicer = (
|
||||
integrationType: string,
|
||||
end: number,
|
||||
text: string
|
||||
text: string,
|
||||
): {start: number, end: number} => {
|
||||
if (integrationType !== 'x') {
|
||||
return {
|
||||
|
|
@ -13,7 +13,21 @@ export const textSlicer = (
|
|||
}
|
||||
}
|
||||
|
||||
const {validRangeEnd, valid} = twitter.parseTweet(text);
|
||||
const {validRangeEnd, valid} = twitter.parseTweet(text, {
|
||||
version: 3,
|
||||
maxWeightedTweetLength: end,
|
||||
scale: 100,
|
||||
defaultWeight: 200,
|
||||
emojiParsingEnabled: true,
|
||||
transformedURLLength: 23,
|
||||
ranges: [
|
||||
{ start: 0, end: 4351, weight: 100 },
|
||||
{ start: 8192, end: 8205, weight: 100 },
|
||||
{ start: 8208, end: 8223, weight: 100 },
|
||||
{ start: 8242, end: 8247, weight: 100 }
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
start: 0,
|
||||
end: valid ? end : validRangeEnd
|
||||
|
|
|
|||
|
|
@ -3,10 +3,23 @@
|
|||
import { FC, useCallback, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
||||
import { useTrack } from '@gitroom/react/helpers/use.track';
|
||||
|
||||
const UtmSaver: FC = () => {
|
||||
const query = useSearchParams();
|
||||
const [value, setValue] = useLocalStorage({ key: 'utm', defaultValue: '' });
|
||||
const searchParams = useSearchParams();
|
||||
const fireEvents = useFireEvents();
|
||||
const track = useTrack();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('check')) {
|
||||
fireEvents('purchase');
|
||||
track(TrackEnum.StartTrial);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const landingUrl = localStorage.getItem('landingUrl');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
export const agentCategories = [
|
||||
'Educational',
|
||||
'Inspirational',
|
||||
'Promotional',
|
||||
'Entertaining',
|
||||
'Interactive',
|
||||
'Behind The Scenes',
|
||||
'Testimonial',
|
||||
'Informative',
|
||||
'Humorous',
|
||||
'Seasonal',
|
||||
'News',
|
||||
'Challenge',
|
||||
'Contest',
|
||||
'Tips',
|
||||
'Tutorial',
|
||||
'Poll',
|
||||
'Survey',
|
||||
'Quote',
|
||||
'Event',
|
||||
'FAQ',
|
||||
'Story',
|
||||
'Meme',
|
||||
'Review',
|
||||
'Announcement',
|
||||
'Highlight',
|
||||
'Celebration',
|
||||
'Reminder',
|
||||
'Debate',
|
||||
'Update',
|
||||
'Trend',
|
||||
];
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import { agentCategories } from '@gitroom/nestjs-libraries/agent/agent.categories';
|
||||
import { z } from 'zod';
|
||||
import { agentTopics } from '@gitroom/nestjs-libraries/agent/agent.topics';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
interface WorkflowChannelsState {
|
||||
messages: BaseMessage[];
|
||||
topic?: string;
|
||||
category: string;
|
||||
hook?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
const category = z.object({
|
||||
category: z.string().describe('The category for the post'),
|
||||
});
|
||||
|
||||
const topic = z.object({
|
||||
topic: z.string().describe('The topic of the post'),
|
||||
});
|
||||
|
||||
const hook = z.object({
|
||||
hook: z.string().describe('The hook of the post'),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class AgentGraphInsertService {
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
) {
|
||||
}
|
||||
static state = () =>
|
||||
new StateGraph<WorkflowChannelsState>({
|
||||
channels: {
|
||||
messages: {
|
||||
reducer: (currentState, updateValue) =>
|
||||
currentState.concat(updateValue),
|
||||
default: () => [],
|
||||
},
|
||||
topic: null,
|
||||
category: null,
|
||||
hook: null,
|
||||
content: null,
|
||||
},
|
||||
});
|
||||
|
||||
async findCategory(state: WorkflowChannelsState) {
|
||||
const { messages } = state;
|
||||
const structuredOutput = model.withStructuredOutput(category);
|
||||
return ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that get a social media post and categorize it into to one from the following categories:
|
||||
{categories}
|
||||
Here is the post:
|
||||
{post}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
post: messages[0].content,
|
||||
categories: agentCategories.join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
findTopic(state: WorkflowChannelsState) {
|
||||
const { messages } = state;
|
||||
const structuredOutput = model.withStructuredOutput(topic);
|
||||
return ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that get a social media post and categorize it into one of the following topics:
|
||||
{topics}
|
||||
Here is the post:
|
||||
{post}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
post: messages[0].content,
|
||||
topics: agentTopics.join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
findHook(state: WorkflowChannelsState) {
|
||||
const { messages } = state;
|
||||
const structuredOutput = model.withStructuredOutput(hook);
|
||||
return ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that get a social media post and extract the hook, the hook is usually the first or second of both sentence of the post, but can be in a different place, make sure you don't change the wording of the post use the exact text:
|
||||
{post}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
post: messages[0].content,
|
||||
});
|
||||
}
|
||||
|
||||
async savePost(state: WorkflowChannelsState) {
|
||||
await this._postsService.createPopularPosts({
|
||||
category: state.category,
|
||||
topic: state.topic!,
|
||||
hook: state.hook!,
|
||||
content: state.messages[0].content! as string
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
newPost(post: string) {
|
||||
const state = AgentGraphInsertService.state();
|
||||
const workflow = state
|
||||
.addNode('find-category', this.findCategory)
|
||||
.addNode('find-topic', this.findTopic)
|
||||
.addNode('find-hook', this.findHook)
|
||||
.addNode('save-post', this.savePost.bind(this))
|
||||
.addEdge(START, 'find-category')
|
||||
.addEdge('find-category', 'find-topic')
|
||||
.addEdge('find-topic', 'find-hook')
|
||||
.addEdge('find-hook', 'save-post')
|
||||
.addEdge('save-post', END);
|
||||
|
||||
const app = workflow.compile();
|
||||
return app.invoke({
|
||||
messages: [new HumanMessage(post)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BaseMessage,
|
||||
HumanMessage,
|
||||
ToolMessage,
|
||||
} from '@langchain/core/messages';
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { ChatOpenAI, DallEAPIWrapper } from '@langchain/openai';
|
||||
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
|
||||
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import dayjs from 'dayjs';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { z } from 'zod';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
|
||||
const tools = !process.env.TAVILY_API_KEY ? [] : [new TavilySearchResults({ maxResults: 3 })];
|
||||
const toolNode = new ToolNode(tools);
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
const dalle = new DallEAPIWrapper({
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
|
||||
model: 'dall-e-3',
|
||||
});
|
||||
|
||||
interface WorkflowChannelsState {
|
||||
messages: BaseMessage[];
|
||||
orgId: string;
|
||||
question: string;
|
||||
hook?: string;
|
||||
fresearch?: string;
|
||||
category?: string;
|
||||
topic?: string;
|
||||
date?: string;
|
||||
format: 'one_short' | 'one_long' | 'thread_short' | 'thread_long';
|
||||
tone: 'personal' | 'company';
|
||||
content?: {
|
||||
content: string;
|
||||
website?: string;
|
||||
prompt?: string;
|
||||
image?: string;
|
||||
}[];
|
||||
isPicture?: boolean;
|
||||
popularPosts?: { content: string; hook: string }[];
|
||||
}
|
||||
|
||||
const category = z.object({
|
||||
category: z.string().describe('The category for the post'),
|
||||
});
|
||||
|
||||
const topic = z.object({
|
||||
topic: z.string().describe('The topic for the post'),
|
||||
});
|
||||
|
||||
const hook = z.object({
|
||||
hook: z
|
||||
.string()
|
||||
.describe(
|
||||
'Hook for the new post, don\'t take it from "the request of the user"'
|
||||
),
|
||||
});
|
||||
|
||||
const contentZod = (
|
||||
isPicture: boolean,
|
||||
format: 'one_short' | 'one_long' | 'thread_short' | 'thread_long'
|
||||
) => {
|
||||
const content = z.object({
|
||||
content: z.string().describe('Content for the new post'),
|
||||
website: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Website for the new post if exists, If one of the post present a brand, website link must be to the root domain of the brand or don't include it, website url should contain the brand name"
|
||||
),
|
||||
...(isPicture
|
||||
? {
|
||||
prompt: z
|
||||
.string()
|
||||
.describe(
|
||||
"Prompt to generate a picture for this post later, make sure it doesn't contain brand names and make it very descriptive in terms of style"
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
return z.object({
|
||||
content:
|
||||
format === 'one_short' || format === 'one_long'
|
||||
? content
|
||||
: z.array(content).min(2).describe(`Content for the new post`),
|
||||
});
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AgentGraphService {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _mediaService: MediaService
|
||||
) {}
|
||||
static state = () =>
|
||||
new StateGraph<WorkflowChannelsState>({
|
||||
channels: {
|
||||
messages: {
|
||||
reducer: (currentState, updateValue) =>
|
||||
currentState.concat(updateValue),
|
||||
default: () => [],
|
||||
},
|
||||
fresearch: null,
|
||||
format: null,
|
||||
tone: null,
|
||||
question: null,
|
||||
orgId: null,
|
||||
hook: null,
|
||||
content: null,
|
||||
date: null,
|
||||
category: null,
|
||||
popularPosts: null,
|
||||
topic: null,
|
||||
isPicture: null,
|
||||
},
|
||||
});
|
||||
|
||||
async startCall(state: WorkflowChannelsState) {
|
||||
const runTools = model.bindTools(tools);
|
||||
const response = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
Today is ${dayjs().format()}, You are an assistant that gets a social media post or requests for a social media post.
|
||||
You research should be on the most possible recent data.
|
||||
You concat the text of the request together with an internet research based on the text.
|
||||
{text}
|
||||
`
|
||||
)
|
||||
.pipe(runTools)
|
||||
.invoke({
|
||||
text: state.messages[state.messages.length - 1].content,
|
||||
});
|
||||
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
async saveResearch(state: WorkflowChannelsState) {
|
||||
const content = state.messages.filter((f) => f instanceof ToolMessage);
|
||||
return { fresearch: content };
|
||||
}
|
||||
|
||||
async findCategories(state: WorkflowChannelsState) {
|
||||
const allCategories = await this._postsService.findAllExistingCategories();
|
||||
const structuredOutput = model.withStructuredOutput(category);
|
||||
const { category: outputCategory } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets a text that will be later summarized into a social media post
|
||||
and classify it to one of the following categories: {categories}
|
||||
text: {text}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
categories: allCategories.map((p) => p.category).join(', '),
|
||||
text: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
category: outputCategory,
|
||||
};
|
||||
}
|
||||
|
||||
async findTopic(state: WorkflowChannelsState) {
|
||||
const allTopics = await this._postsService.findAllExistingTopicsOfCategory(
|
||||
state?.category!
|
||||
);
|
||||
if (allTopics.length === 0) {
|
||||
return { topic: null };
|
||||
}
|
||||
|
||||
const structuredOutput = model.withStructuredOutput(topic);
|
||||
const { topic: outputTopic } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets a text that will be later summarized into a social media post
|
||||
and classify it to one of the following topics: {topics}
|
||||
text: {text}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
topics: allTopics.map((p) => p.topic).join(', '),
|
||||
text: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
topic: outputTopic,
|
||||
};
|
||||
}
|
||||
|
||||
async findPopularPosts(state: WorkflowChannelsState) {
|
||||
const popularPosts = await this._postsService.findPopularPosts(
|
||||
state.category!,
|
||||
state.topic
|
||||
);
|
||||
return { popularPosts };
|
||||
}
|
||||
|
||||
async generateHook(state: WorkflowChannelsState) {
|
||||
const structuredOutput = model.withStructuredOutput(hook);
|
||||
const { hook: outputHook } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets content for a social media post, and generate only the hook.
|
||||
The hook is the 1-2 sentences of the post that will be used to grab the attention of the reader.
|
||||
You will be provided existing hooks you should use as inspiration.
|
||||
- Avoid weird hook that starts with "Discover the secret...", "The best...", "The most...", "The top..."
|
||||
- Make sure it sounds ${state.tone}
|
||||
- Use ${state.tone === 'personal' ? '1st' : '3rd'} person mode
|
||||
- Make sure it's engaging
|
||||
- Don't be cringy
|
||||
- Use simple english
|
||||
- Make sure you add "\n" between the lines
|
||||
- Don't take the hook from "request of the user"
|
||||
|
||||
<!-- BEGIN request of the user -->
|
||||
{request}
|
||||
<!-- END request of the user -->
|
||||
|
||||
<!-- BEGIN existing hooks -->
|
||||
{hooks}
|
||||
<!-- END existing hooks -->
|
||||
|
||||
<!-- BEGIN current content -->
|
||||
{text}
|
||||
<!-- END current content -->
|
||||
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
request: state.messages[0].content,
|
||||
hooks: state.popularPosts!.map((p) => p.hook).join('\n'),
|
||||
text: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
hook: outputHook,
|
||||
};
|
||||
}
|
||||
|
||||
async generateContent(state: WorkflowChannelsState) {
|
||||
const structuredOutput = model.withStructuredOutput(
|
||||
contentZod(!!state.isPicture, state.format)
|
||||
);
|
||||
const { content: outputContent } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets existing hook of a social media, content and generate only the content.
|
||||
- Don't add any hashtags
|
||||
- Make sure it sounds ${state.tone}
|
||||
- Use ${state.tone === 'personal' ? '1st' : '3rd'} person mode
|
||||
- ${
|
||||
state.format === 'one_short' || state.format === 'thread_short'
|
||||
? 'Post should be maximum 200 chars to fit twitter'
|
||||
: 'Post should be long'
|
||||
}
|
||||
- ${
|
||||
state.format === 'one_short' || state.format === 'one_long'
|
||||
? 'Post should have only 1 item'
|
||||
: 'Post should have minimum 2 items'
|
||||
}
|
||||
- Use the hook as inspiration
|
||||
- Make sure it's engaging
|
||||
- Don't be cringy
|
||||
- Use simple english
|
||||
- The Content should not contain the hook
|
||||
- Try to put some call to action at the end of the post
|
||||
- Make sure you add "\n" between the lines
|
||||
- Add "\n" after every "."
|
||||
|
||||
Hook:
|
||||
{hook}
|
||||
|
||||
User request:
|
||||
{request}
|
||||
|
||||
current content information:
|
||||
{information}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
hook: state.hook,
|
||||
request: state.messages[0].content,
|
||||
information: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
content: outputContent,
|
||||
};
|
||||
}
|
||||
|
||||
async fixArray(state: WorkflowChannelsState) {
|
||||
if (state.format === 'one_short' || state.format === 'one_long') {
|
||||
return {
|
||||
content: [state.content],
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async generatePictures(state: WorkflowChannelsState) {
|
||||
if (!state.isPicture) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const newContent = await Promise.all(
|
||||
(state.content || []).map(async (p) => {
|
||||
const image = await dalle.invoke(p.prompt!);
|
||||
return {
|
||||
...p,
|
||||
image,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
content: newContent,
|
||||
};
|
||||
}
|
||||
|
||||
async uploadPictures(state: WorkflowChannelsState) {
|
||||
const all = await Promise.all(
|
||||
(state.content || []).map(async (p) => {
|
||||
if (p.image) {
|
||||
const upload = await this.storage.uploadSimple(p.image);
|
||||
const name = upload.split('/').pop()!;
|
||||
const uploadWithId = await this._mediaService.saveFile(
|
||||
state.orgId,
|
||||
name,
|
||||
upload
|
||||
);
|
||||
|
||||
return {
|
||||
...p,
|
||||
image: uploadWithId,
|
||||
};
|
||||
}
|
||||
|
||||
return p;
|
||||
})
|
||||
);
|
||||
|
||||
return { content: all };
|
||||
}
|
||||
|
||||
async isGeneratePicture(state: WorkflowChannelsState) {
|
||||
if (state.isPicture) {
|
||||
return 'generate-picture';
|
||||
}
|
||||
|
||||
return 'post-time';
|
||||
}
|
||||
|
||||
async postDateTime(state: WorkflowChannelsState) {
|
||||
return { date: await this._postsService.findFreeDateTime(state.orgId) };
|
||||
}
|
||||
|
||||
start(orgId: string, body: GeneratorDto) {
|
||||
const state = AgentGraphService.state();
|
||||
const workflow = state
|
||||
.addNode('agent', this.startCall.bind(this))
|
||||
.addNode('research', toolNode)
|
||||
.addNode('save-research', this.saveResearch.bind(this))
|
||||
.addNode('find-category', this.findCategories.bind(this))
|
||||
.addNode('find-topic', this.findTopic.bind(this))
|
||||
.addNode('find-popular-posts', this.findPopularPosts.bind(this))
|
||||
.addNode('generate-hook', this.generateHook.bind(this))
|
||||
.addNode('generate-content', this.generateContent.bind(this))
|
||||
.addNode('generate-content-fix', this.fixArray.bind(this))
|
||||
.addNode('generate-picture', this.generatePictures.bind(this))
|
||||
.addNode('upload-pictures', this.uploadPictures.bind(this))
|
||||
.addNode('post-time', this.postDateTime.bind(this))
|
||||
.addEdge(START, 'agent')
|
||||
.addEdge('agent', 'research')
|
||||
.addEdge('research', 'save-research')
|
||||
.addEdge('save-research', 'find-category')
|
||||
.addEdge('find-category', 'find-topic')
|
||||
.addEdge('find-topic', 'find-popular-posts')
|
||||
.addEdge('find-popular-posts', 'generate-hook')
|
||||
.addEdge('generate-hook', 'generate-content')
|
||||
.addEdge('generate-content', 'generate-content-fix')
|
||||
.addConditionalEdges(
|
||||
'generate-content-fix',
|
||||
this.isGeneratePicture.bind(this)
|
||||
)
|
||||
.addEdge('generate-picture', 'upload-pictures')
|
||||
.addEdge('upload-pictures', 'post-time')
|
||||
.addEdge('post-time', END);
|
||||
|
||||
const app = workflow.compile();
|
||||
|
||||
return app.streamEvents(
|
||||
{
|
||||
messages: [new HumanMessage(body.research)],
|
||||
isPicture: body.isPicture,
|
||||
format: body.format,
|
||||
tone: body.tone,
|
||||
orgId,
|
||||
},
|
||||
{
|
||||
streamMode: 'values',
|
||||
version: 'v2',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [AgentGraphService, AgentGraphInsertService],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
},
|
||||
})
|
||||
export class AgentModule {}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
export const agentTopics = [
|
||||
'Business',
|
||||
'Marketing',
|
||||
'Finance',
|
||||
'Startups',
|
||||
'Networking',
|
||||
'Leadership',
|
||||
'Strategy',
|
||||
'Branding',
|
||||
'Analytics',
|
||||
'Growth',
|
||||
'Drawing',
|
||||
'Painting',
|
||||
'Design',
|
||||
'Photography',
|
||||
'Writing',
|
||||
'Sculpting',
|
||||
'Animation',
|
||||
'Sketching',
|
||||
'Crafting',
|
||||
'Calligraphy',
|
||||
'Mindset',
|
||||
'Productivity',
|
||||
'Motivation',
|
||||
'Education',
|
||||
'Learning',
|
||||
'Skills',
|
||||
'Success',
|
||||
'Wellness',
|
||||
'Goals',
|
||||
'Inspiration',
|
||||
'Fashion',
|
||||
'Travel',
|
||||
'Food',
|
||||
'Fitness',
|
||||
'Health',
|
||||
'Beauty',
|
||||
'Home',
|
||||
'Decor',
|
||||
'Hobbies',
|
||||
'Parenting',
|
||||
'Tech',
|
||||
'Gadgets',
|
||||
'AI',
|
||||
'Coding',
|
||||
'Software',
|
||||
'Innovation',
|
||||
'Apps',
|
||||
'Gaming',
|
||||
'Robotics',
|
||||
'Security',
|
||||
'Music',
|
||||
'Movies',
|
||||
'Sports',
|
||||
'Books',
|
||||
'Theater',
|
||||
'Comedy',
|
||||
'Dance',
|
||||
'Celebrities',
|
||||
'Culture',
|
||||
'Gaming',
|
||||
'Environment',
|
||||
'Equality',
|
||||
'Activism',
|
||||
'Justice',
|
||||
'Diversity',
|
||||
'Sustainability',
|
||||
'Inclusion',
|
||||
'Awareness',
|
||||
'Charity',
|
||||
'Peace',
|
||||
'Holidays',
|
||||
'Festivities',
|
||||
'Seasons',
|
||||
'Trends',
|
||||
'Celebrations',
|
||||
'Anniversaries',
|
||||
'Milestones',
|
||||
'Memories',
|
||||
'Promotions',
|
||||
'Updates',
|
||||
];
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import dayjs from 'dayjs';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class CommentsRepository {
|
||||
constructor(private _media: PrismaRepository<'comments'>) {}
|
||||
|
||||
addAComment(orgId: string, userId: string, comment: string, date: string) {
|
||||
return this._media.model.comments.create({
|
||||
data: {
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
content: comment,
|
||||
date: dayjs(date).toDate(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
addACommentToComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string,
|
||||
date: string
|
||||
) {
|
||||
return this._media.model.comments.create({
|
||||
data: {
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
content: comment,
|
||||
date: dayjs(date).toDate(),
|
||||
parentCommentId: commentId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateAComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string
|
||||
) {
|
||||
return this._media.model.comments.update({
|
||||
where: {
|
||||
id: commentId,
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
},
|
||||
data: {
|
||||
content: comment,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteAComment(orgId: string, userId: string, commentId: string) {
|
||||
return this._media.model.comments.update({
|
||||
where: {
|
||||
id: commentId,
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async loadAllCommentsAndSubCommentsForADate(orgId: string, date: string) {
|
||||
return this._media.model.comments.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
deletedAt: null,
|
||||
date: dayjs(date).toDate(),
|
||||
parentCommentId: null,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
childrenComment: {
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getAllCommentsByWeekYear(
|
||||
orgId: string,
|
||||
year: number,
|
||||
week: number,
|
||||
) {
|
||||
const dateYear = dayjs().year(year);
|
||||
const date = dateYear.isoWeek(week);
|
||||
const startDate = date.startOf('isoWeek').subtract(2, 'days').toDate();
|
||||
const endDate = date.endOf('isoWeek').add(2, 'days').toDate();
|
||||
|
||||
const load = await this._media.model.comments.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
deletedAt: null,
|
||||
parentCommentId: null,
|
||||
date: {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
date: true,
|
||||
_count: {
|
||||
select: {
|
||||
childrenComment: {
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const group = groupBy(load, (item) =>
|
||||
dayjs(item.date).format('YYYY-MM-DD HH:MM')
|
||||
);
|
||||
|
||||
return Object.values(group).reduce((all, current) => {
|
||||
return [
|
||||
...all,
|
||||
{
|
||||
date: current[0].date,
|
||||
total:
|
||||
current.length +
|
||||
current.reduce(
|
||||
(all2, current2) => all2 + current2._count.childrenComment,
|
||||
0
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [] as Array<{ date: Date; total: number }>);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { CommentsRepository } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CommentsService {
|
||||
constructor(private _commentsRepository: CommentsRepository) {}
|
||||
|
||||
addAComment(orgId: string, userId: string, comment: string, date: string) {
|
||||
return this._commentsRepository.addAComment(orgId, userId, comment, date);
|
||||
}
|
||||
|
||||
addACommentToComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string,
|
||||
date: string
|
||||
) {
|
||||
return this._commentsRepository.addACommentToComment(orgId, userId, commentId, comment, date);
|
||||
}
|
||||
|
||||
updateAComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string
|
||||
) {
|
||||
return this._commentsRepository.updateAComment(
|
||||
orgId,
|
||||
userId,
|
||||
commentId,
|
||||
comment
|
||||
);
|
||||
}
|
||||
|
||||
deleteAComment(orgId: string, userId: string, commentId: string) {
|
||||
return this._commentsRepository.deleteAComment(orgId, userId, commentId);
|
||||
}
|
||||
|
||||
loadAllCommentsAndSubCommentsForADate(orgId: string, date: string) {
|
||||
return this._commentsRepository.loadAllCommentsAndSubCommentsForADate(
|
||||
orgId,
|
||||
date
|
||||
);
|
||||
}
|
||||
|
||||
getAllCommentsByWeekYear(orgId: string, year: number, week: number) {
|
||||
return this._commentsRepository.getAllCommentsByWeekYear(orgId, year, week);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +16,6 @@ import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts
|
|||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media/media.repository';
|
||||
import { CommentsRepository } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.repository';
|
||||
import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service';
|
||||
import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/prisma/notifications/notifications.repository';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
import { ItemUserRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.repository';
|
||||
|
|
@ -29,6 +27,7 @@ import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.
|
|||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
|
||||
import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.repository';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -55,17 +54,16 @@ import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/ag
|
|||
MessagesRepository,
|
||||
MediaService,
|
||||
MediaRepository,
|
||||
CommentsRepository,
|
||||
ItemUserRepository,
|
||||
AgenciesService,
|
||||
AgenciesRepository,
|
||||
ItemUserService,
|
||||
MessagesService,
|
||||
CommentsService,
|
||||
IntegrationManager,
|
||||
ExtractContentService,
|
||||
OpenaiService,
|
||||
EmailService,
|
||||
TrackService,
|
||||
],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ export class IntegrationRepository {
|
|||
private _customers: PrismaRepository<'customer'>
|
||||
) {}
|
||||
|
||||
updateProviderSettings(org: string, id: string, settings: string) {
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
id,
|
||||
organizationId: org,
|
||||
},
|
||||
data: {
|
||||
additionalSettings: settings,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setTimes(org: string, id: string, times: IntegrationTimeDto) {
|
||||
return this._integration.model.integration.update({
|
||||
select: {
|
||||
|
|
@ -39,8 +51,8 @@ export class IntegrationRepository {
|
|||
id: plugId,
|
||||
},
|
||||
include: {
|
||||
integration: true
|
||||
}
|
||||
integration: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +105,17 @@ export class IntegrationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
createOrUpdateIntegration(
|
||||
async createOrUpdateIntegration(
|
||||
additionalSettings:
|
||||
| {
|
||||
title: string;
|
||||
description: string;
|
||||
type: 'checkbox' | 'text' | 'textarea';
|
||||
value: any;
|
||||
regex?: string;
|
||||
}[]
|
||||
| undefined,
|
||||
oneTimeToken: boolean,
|
||||
org: string,
|
||||
name: string,
|
||||
picture: string | undefined,
|
||||
|
|
@ -118,7 +140,7 @@ export class IntegrationRepository {
|
|||
]),
|
||||
}
|
||||
: {};
|
||||
return this._integration.model.integration.upsert({
|
||||
const upsert = await this._integration.model.integration.upsert({
|
||||
where: {
|
||||
organizationId_internalId: {
|
||||
internalId,
|
||||
|
|
@ -141,7 +163,11 @@ export class IntegrationRepository {
|
|||
...postTimes,
|
||||
organizationId: org,
|
||||
refreshNeeded: false,
|
||||
rootInternalId: internalId.split('_').pop(),
|
||||
...(customInstanceDetails ? { customInstanceDetails } : {}),
|
||||
additionalSettings: additionalSettings
|
||||
? JSON.stringify(additionalSettings)
|
||||
: '[]',
|
||||
},
|
||||
update: {
|
||||
type: type as any,
|
||||
|
|
@ -164,6 +190,38 @@ export class IntegrationRepository {
|
|||
refreshNeeded: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (oneTimeToken) {
|
||||
const rootId =
|
||||
(
|
||||
await this._integration.model.integration.findFirst({
|
||||
where: {
|
||||
organizationId: org,
|
||||
internalId: internalId,
|
||||
},
|
||||
})
|
||||
)?.rootInternalId || internalId.split('_').pop()!;
|
||||
|
||||
await this._integration.model.integration.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
not: upsert.id,
|
||||
},
|
||||
organizationId: org,
|
||||
rootInternalId: rootId,
|
||||
},
|
||||
data: {
|
||||
token,
|
||||
refreshToken,
|
||||
refreshNeeded: false,
|
||||
...(expiresIn
|
||||
? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return upsert;
|
||||
}
|
||||
|
||||
needsToBeRefreshed() {
|
||||
|
|
@ -486,4 +544,17 @@ export class IntegrationRepository {
|
|||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async getPostingTimes(orgId: string) {
|
||||
return this._integration.model.integration.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
disabled: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
postingTimes: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social
|
|||
import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider';
|
||||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { Integration, Organization } from '@prisma/client';
|
||||
|
|
@ -18,7 +19,9 @@ import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/
|
|||
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';
|
||||
import { difference, uniq } from 'lodash';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
dayjs.extend(utc);
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationService {
|
||||
|
|
@ -38,7 +41,27 @@ export class IntegrationService {
|
|||
return this._integrationRepository.setTimes(orgId, integrationId, times);
|
||||
}
|
||||
|
||||
updateProviderSettings(
|
||||
org: string,
|
||||
id: string,
|
||||
additionalSettings: string
|
||||
) {
|
||||
return this._integrationRepository.updateProviderSettings(
|
||||
org,
|
||||
id,
|
||||
additionalSettings
|
||||
);
|
||||
}
|
||||
|
||||
async createOrUpdateIntegration(
|
||||
additionalSettings: {
|
||||
title: string;
|
||||
description: string;
|
||||
type: 'checkbox' | 'text' | 'textarea';
|
||||
value: any;
|
||||
regex?: string;
|
||||
}[] | undefined,
|
||||
oneTimeToken: boolean,
|
||||
org: string,
|
||||
name: string,
|
||||
picture: string | undefined,
|
||||
|
|
@ -58,6 +81,8 @@ export class IntegrationService {
|
|||
? await this.storage.uploadSimple(picture)
|
||||
: undefined;
|
||||
return this._integrationRepository.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
oneTimeToken,
|
||||
org,
|
||||
name,
|
||||
uploadedPicture,
|
||||
|
|
@ -161,6 +186,8 @@ export class IntegrationService {
|
|||
const { refreshToken, accessToken, expiresIn } = data;
|
||||
|
||||
await this.createOrUpdateIntegration(
|
||||
undefined,
|
||||
!!provider.oneTimeToken,
|
||||
integration.organizationId,
|
||||
integration.name,
|
||||
undefined,
|
||||
|
|
@ -328,11 +355,28 @@ export class IntegrationService {
|
|||
dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
|
||||
forceRefresh
|
||||
) {
|
||||
const { accessToken, expiresIn, refreshToken } =
|
||||
await integrationProvider.refreshToken(getIntegration.refreshToken!);
|
||||
const { accessToken, expiresIn, refreshToken, additionalSettings } =
|
||||
await new Promise<AuthTokenDetails>((res) => {
|
||||
return integrationProvider
|
||||
.refreshToken(getIntegration.refreshToken!)
|
||||
.then((r) => res(r))
|
||||
.catch(() => {
|
||||
res({
|
||||
error: '',
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
additionalSettings: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (accessToken) {
|
||||
await this.createOrUpdateIntegration(
|
||||
additionalSettings,
|
||||
!!integrationProvider.oneTimeToken,
|
||||
getIntegration.organizationId,
|
||||
getIntegration.name,
|
||||
getIntegration.picture!,
|
||||
|
|
@ -380,7 +424,7 @@ export class IntegrationService {
|
|||
return loadAnalytics;
|
||||
} catch (e) {
|
||||
if (e instanceof RefreshToken) {
|
||||
return this.checkAnalytics(org, integration, date);
|
||||
return this.checkAnalytics(org, integration, date, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -399,6 +443,55 @@ export class IntegrationService {
|
|||
);
|
||||
}
|
||||
|
||||
async processInternalPlug(data: {
|
||||
post: string;
|
||||
originalIntegration: string;
|
||||
integration: string;
|
||||
plugName: string;
|
||||
orgId: string;
|
||||
delay: number;
|
||||
information: any;
|
||||
}) {
|
||||
const originalIntegration =
|
||||
await this._integrationRepository.getIntegrationById(
|
||||
data.orgId,
|
||||
data.originalIntegration
|
||||
);
|
||||
|
||||
const getIntegration = await this._integrationRepository.getIntegrationById(
|
||||
data.orgId,
|
||||
data.integration
|
||||
);
|
||||
|
||||
if (!getIntegration || !originalIntegration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getAllInternalPlugs = this._integrationManager
|
||||
.getInternalPlugs(getIntegration.providerIdentifier)
|
||||
.internalPlugs.find((p: any) => p.identifier === data.plugName);
|
||||
|
||||
if (!getAllInternalPlugs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getSocialIntegration = this._integrationManager.getSocialIntegration(
|
||||
getIntegration.providerIdentifier
|
||||
);
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
await getSocialIntegration?.[getAllInternalPlugs.methodName]?.(
|
||||
getIntegration,
|
||||
originalIntegration,
|
||||
data.post,
|
||||
data.information
|
||||
);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async processPlugs(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
|
|
@ -408,7 +501,7 @@ export class IntegrationService {
|
|||
}) {
|
||||
const getPlugById = await this._integrationRepository.getPlug(data.plugId);
|
||||
if (!getPlugById) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
|
||||
const integration = this._integrationManager.getSocialIntegration(
|
||||
|
|
@ -421,8 +514,6 @@ export class IntegrationService {
|
|||
(p) => p.identifier === getPlugById.integration.providerIdentifier
|
||||
)!;
|
||||
|
||||
console.log(data.postId);
|
||||
|
||||
// @ts-ignore
|
||||
const process = await integration[getPlugById.plugFunction](
|
||||
getPlugById.integration,
|
||||
|
|
@ -434,11 +525,11 @@ export class IntegrationService {
|
|||
);
|
||||
|
||||
if (process) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.totalRuns === data.currentRun) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
|
|
@ -500,4 +591,18 @@ export class IntegrationService {
|
|||
const loadOnlyIds = exisingData.map((p) => p.value);
|
||||
return difference(id, loadOnlyIds);
|
||||
}
|
||||
|
||||
async findFreeDateTime(orgId: string): Promise<number[]> {
|
||||
const findTimes = await this._integrationRepository.getPostingTimes(orgId);
|
||||
return uniq(
|
||||
findTimes.reduce((all: any, current: any) => {
|
||||
return [
|
||||
...all,
|
||||
...JSON.parse(current.postingTimes).map(
|
||||
(p: { time: number }) => p.time
|
||||
),
|
||||
];
|
||||
}, [] as number[])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ export class MediaService {
|
|||
private _subscriptionService: SubscriptionService
|
||||
){}
|
||||
|
||||
async generateImage(prompt: string, org: Organization) {
|
||||
const image = await this._openAi.generateImage(prompt);
|
||||
async generateImage(prompt: string, org: Organization, generatePromptFirst?: boolean) {
|
||||
if (generatePromptFirst) {
|
||||
prompt = await this._openAi.generatePromptForPicture(prompt);
|
||||
console.log('Prompt:', prompt);
|
||||
}
|
||||
const image = await this._openAi.generateImage(prompt, !!generatePromptFirst);
|
||||
await this._subscriptionService.useCredit(org);
|
||||
return image;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ export class NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
async sendEmail(to: string, subject: string, html: string) {
|
||||
await this._emailService.sendEmail(to, subject, html);
|
||||
async sendEmail(to: string, subject: string, html: string, replyTo?: string) {
|
||||
await this._emailService.sendEmail(to, subject, html, replyTo);
|
||||
}
|
||||
|
||||
hasEmailProvider() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Role, SubscriptionTier } from '@prisma/client';
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationRepository {
|
||||
|
|
@ -12,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: {
|
||||
|
|
@ -83,6 +101,17 @@ export class OrganizationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
updateApiKey(orgId: string) {
|
||||
return this._organization.model.organization.update({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
data: {
|
||||
apiKey: AuthService.fixedEncryption(makeId(20)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOrgsByUserId(userId: string) {
|
||||
return this._organization.model.organization.findMany({
|
||||
where: {
|
||||
|
|
@ -178,11 +207,15 @@ export class OrganizationRepository {
|
|||
|
||||
async createOrgAndUser(
|
||||
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string },
|
||||
hasEmail: boolean
|
||||
hasEmail: boolean,
|
||||
ip: string,
|
||||
userAgent: string
|
||||
) {
|
||||
return this._organization.model.organization.create({
|
||||
data: {
|
||||
name: body.company,
|
||||
apiKey: AuthService.fixedEncryption(makeId(20)),
|
||||
allowTrial: true,
|
||||
users: {
|
||||
create: {
|
||||
role: Role.SUPERADMIN,
|
||||
|
|
@ -196,6 +229,8 @@ export class OrganizationRepository {
|
|||
providerName: body.provider,
|
||||
providerId: body.providerId || '',
|
||||
timezone: 0,
|
||||
ip,
|
||||
agent: userAgent,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -212,6 +247,14 @@ export class OrganizationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getOrgByCustomerId(customerId: string) {
|
||||
return this._organization.model.organization.findFirst({
|
||||
where: {
|
||||
paymentId: customerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTeam(orgId: string) {
|
||||
return this._organization.model.organization.findUnique({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,16 @@ export class OrganizationService {
|
|||
private _notificationsService: NotificationService
|
||||
) {}
|
||||
async createOrgAndUser(
|
||||
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string }
|
||||
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string },
|
||||
ip: string,
|
||||
userAgent: string
|
||||
) {
|
||||
return this._organizationRepository.createOrgAndUser(body, this._notificationsService.hasEmailProvider());
|
||||
return this._organizationRepository.createOrgAndUser(
|
||||
body,
|
||||
this._notificationsService.hasEmailProvider(),
|
||||
ip,
|
||||
userAgent
|
||||
);
|
||||
}
|
||||
|
||||
addUserToOrg(
|
||||
|
|
@ -33,6 +40,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);
|
||||
}
|
||||
|
|
@ -41,10 +52,18 @@ export class OrganizationService {
|
|||
return this._organizationRepository.getOrgsByUserId(userId);
|
||||
}
|
||||
|
||||
updateApiKey(orgId: string) {
|
||||
return this._organizationRepository.updateApiKey(orgId);
|
||||
}
|
||||
|
||||
getTeam(orgId: string) {
|
||||
return this._organizationRepository.getTeam(orgId);
|
||||
}
|
||||
|
||||
getOrgByCustomerId(customerId: string) {
|
||||
return this._organizationRepository.getOrgByCustomerId(customerId);
|
||||
}
|
||||
|
||||
async inviteTeamMember(orgId: string, body: AddTeamMemberDto) {
|
||||
const timeLimit = dayjs().add(1, 'hour').format('YYYY-MM-DD HH:mm:ss');
|
||||
const id = makeId(5);
|
||||
|
|
@ -82,6 +101,9 @@ export class OrganizationService {
|
|||
}
|
||||
|
||||
disableOrEnableNonSuperAdminUsers(orgId: string, disable: boolean) {
|
||||
return this._organizationRepository.disableOrEnableNonSuperAdminUsers(orgId, disable);
|
||||
return this._organizationRepository.disableOrEnableNonSuperAdminUsers(
|
||||
orgId,
|
||||
disable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ dayjs.extend(weekOfYear);
|
|||
|
||||
@Injectable()
|
||||
export class PostsRepository {
|
||||
constructor(private _post: PrismaRepository<'post'>) {}
|
||||
constructor(
|
||||
private _post: PrismaRepository<'post'>,
|
||||
private _popularPosts: PrismaRepository<'popularPosts'>,
|
||||
private _comments: PrismaRepository<'comments'>
|
||||
) {}
|
||||
|
||||
getOldPosts(orgId: string, date: string) {
|
||||
return this._post.model.post.findMany({
|
||||
|
|
@ -106,6 +110,11 @@ export class PostsRepository {
|
|||
},
|
||||
deletedAt: null,
|
||||
parentPostId: null,
|
||||
...query.customer ? {
|
||||
integration: {
|
||||
customerId: query.customer,
|
||||
}
|
||||
}: {},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
|
@ -186,13 +195,14 @@ export class PostsRepository {
|
|||
});
|
||||
}
|
||||
|
||||
changeState(id: string, state: State) {
|
||||
changeState(id: string, state: State, err?: string) {
|
||||
return this._post.model.post.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
state,
|
||||
error: typeof err === 'string' ? err : JSON.stringify(err),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -387,4 +397,111 @@ export class PostsRepository {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
findAllExistingCategories() {
|
||||
return this._popularPosts.model.popularPosts.findMany({
|
||||
select: {
|
||||
category: true,
|
||||
},
|
||||
distinct: ['category'],
|
||||
});
|
||||
}
|
||||
|
||||
findAllExistingTopicsOfCategory(category: string) {
|
||||
return this._popularPosts.model.popularPosts.findMany({
|
||||
where: {
|
||||
category,
|
||||
},
|
||||
select: {
|
||||
topic: true,
|
||||
},
|
||||
distinct: ['topic'],
|
||||
});
|
||||
}
|
||||
|
||||
findPopularPosts(category: string, topic?: string) {
|
||||
return this._popularPosts.model.popularPosts.findMany({
|
||||
where: {
|
||||
category,
|
||||
...(topic ? { topic } : {}),
|
||||
},
|
||||
select: {
|
||||
content: true,
|
||||
hook: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createPopularPosts(post: {
|
||||
category: string;
|
||||
topic: string;
|
||||
content: string;
|
||||
hook: string;
|
||||
}) {
|
||||
return this._popularPosts.model.popularPosts.create({
|
||||
data: {
|
||||
category: 'category',
|
||||
topic: 'topic',
|
||||
content: 'content',
|
||||
hook: 'hook',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getPostsCountsByDates(
|
||||
orgId: string,
|
||||
times: number[],
|
||||
date: dayjs.Dayjs
|
||||
) {
|
||||
const dates = await this._post.model.post.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
organizationId: orgId,
|
||||
publishDate: {
|
||||
in: times.map((time) => {
|
||||
return date.clone().add(time, 'minutes').toDate();
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return times.filter(
|
||||
(time) =>
|
||||
date.clone().add(time, 'minutes').isAfter(dayjs.utc()) &&
|
||||
!dates.find((dateFind) => {
|
||||
return (
|
||||
dayjs
|
||||
.utc(dateFind.publishDate)
|
||||
.diff(date.clone().startOf('day'), 'minutes') == time
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getComments(postId: string) {
|
||||
return this._comments.model.comments.findMany({
|
||||
where: {
|
||||
postId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
postId: string,
|
||||
content: string
|
||||
) {
|
||||
return this._comments.model.comments.create({
|
||||
data: {
|
||||
organizationId: orgId,
|
||||
userId,
|
||||
postId,
|
||||
content,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue