diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts
index b613fec1..e01b7e05 100644
--- a/apps/backend/src/api/api.module.ts
+++ b/apps/backend/src/api/api.module.ts
@@ -1,48 +1,60 @@
-import {MiddlewareConsumer, Module, NestModule} from '@nestjs/common';
-import {AuthController} from "@gitroom/backend/api/routes/auth.controller";
-import {AuthService} from "@gitroom/backend/services/auth/auth.service";
-import {UsersController} from "@gitroom/backend/api/routes/users.controller";
-import {AuthMiddleware} from "@gitroom/backend/services/auth/auth.middleware";
-import {StripeController} from "@gitroom/backend/api/routes/stripe.controller";
-import {StripeService} from "@gitroom/nestjs-libraries/services/stripe.service";
-import {AnalyticsController} from "@gitroom/backend/api/routes/analytics.controller";
-import {PoliciesGuard} from "@gitroom/backend/services/auth/permissions/permissions.guard";
-import {PermissionsService} from "@gitroom/backend/services/auth/permissions/permissions.service";
-import {IntegrationsController} from "@gitroom/backend/api/routes/integrations.controller";
-import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integration.manager";
-import {SettingsController} from "@gitroom/backend/api/routes/settings.controller";
-import {BullMqModule} from "@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module";
-import {ioRedis} from "@gitroom/nestjs-libraries/redis/redis.service";
-import {PostsController} from "@gitroom/backend/api/routes/posts.controller";
+import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
+import { AuthController } from '@gitroom/backend/api/routes/auth.controller';
+import { AuthService } from '@gitroom/backend/services/auth/auth.service';
+import { UsersController } from '@gitroom/backend/api/routes/users.controller';
+import { AuthMiddleware } from '@gitroom/backend/services/auth/auth.middleware';
+import { StripeController } from '@gitroom/backend/api/routes/stripe.controller';
+import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
+import { AnalyticsController } from '@gitroom/backend/api/routes/analytics.controller';
+import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
+import { PermissionsService } from '@gitroom/backend/services/auth/permissions/permissions.service';
+import { IntegrationsController } from '@gitroom/backend/api/routes/integrations.controller';
+import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
+import { SettingsController } from '@gitroom/backend/api/routes/settings.controller';
+import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module';
+import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
+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 {ServeStaticModule} from "@nestjs/serve-static";
const authenticatedController = [
- UsersController,
- AnalyticsController,
- IntegrationsController,
- SettingsController,
- PostsController
+ UsersController,
+ AnalyticsController,
+ IntegrationsController,
+ SettingsController,
+ PostsController,
+ MediaController,
];
@Module({
- imports: [BullMqModule.forRoot({
- connection: ioRedis
- })],
- controllers: [StripeController, AuthController, ...authenticatedController],
- providers: [
- AuthService,
- StripeService,
- AuthMiddleware,
- PoliciesGuard,
- PermissionsService,
- IntegrationManager,
- ],
- get exports() {
- return [...this.imports, ...this.providers];
- }
+ imports: [
+ UploadModule,
+ BullMqModule.forRoot({
+ connection: ioRedis,
+ }),
+ ServeStaticModule.forRoot({
+ rootPath: process.env.UPLOAD_DIRECTORY,
+ serveRoot: '/' + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY,
+ serveStaticOptions: {
+ index: false,
+ }
+ }),
+ ],
+ controllers: [StripeController, AuthController, ...authenticatedController],
+ providers: [
+ AuthService,
+ StripeService,
+ AuthMiddleware,
+ PoliciesGuard,
+ PermissionsService,
+ IntegrationManager,
+ ],
+ get exports() {
+ return [...this.imports, ...this.providers];
+ },
})
export class ApiModule implements NestModule {
- configure(consumer: MiddlewareConsumer) {
- consumer
- .apply(AuthMiddleware)
- .forRoutes(...authenticatedController);
- }
+ configure(consumer: MiddlewareConsumer) {
+ consumer.apply(AuthMiddleware).forRoutes(...authenticatedController);
+ }
}
diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts
index 9e645b88..d7232688 100644
--- a/apps/backend/src/api/routes/integrations.controller.ts
+++ b/apps/backend/src/api/routes/integrations.controller.ts
@@ -1,95 +1,181 @@
-import {Body, Controller, Get, Param, Post} from '@nestjs/common';
-import {ioRedis} from "@gitroom/nestjs-libraries/redis/redis.service";
-import {ConnectIntegrationDto} from "@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto";
-import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integration.manager";
-import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service";
-import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request";
-import {Organization} from "@prisma/client";
-import {ApiKeyDto} from "@gitroom/nestjs-libraries/dtos/integrations/api.key.dto";
+import { Body, Controller, Get, Param, Post } from '@nestjs/common';
+import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
+import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto';
+import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
+import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
+import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
+import { Organization } from '@prisma/client';
+import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
+import { IntegrationFunctionDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.function.dto';
@Controller('/integrations')
export class IntegrationsController {
- constructor(
- private _integrationManager: IntegrationManager,
- private _integrationService: IntegrationService
+ constructor(
+ private _integrationManager: IntegrationManager,
+ private _integrationService: IntegrationService
+ ) {}
+ @Get('/')
+ getIntegration() {
+ return this._integrationManager.getAllIntegrations();
+ }
+
+ @Get('/list')
+ async getIntegrationList(@GetOrgFromRequest() org: Organization) {
+ return {
+ integrations: (
+ await this._integrationService.getIntegrationsList(org.id)
+ ).map((p) => ({
+ name: p.name,
+ id: p.id,
+ picture: p.picture,
+ identifier: p.providerIdentifier,
+ type: p.type,
+ })),
+ };
+ }
+
+ @Get('/social/:integration')
+ async getIntegrationUrl(@Param('integration') integration: string) {
+ if (
+ !this._integrationManager
+ .getAllowedSocialsIntegrations()
+ .includes(integration)
) {
- }
- @Get('/')
- getIntegration() {
- return this._integrationManager.getAllIntegrations();
+ throw new Error('Integration not allowed');
}
- @Get('/list')
- async getIntegrationList(
- @GetOrgFromRequest() org: Organization,
+ const integrationProvider =
+ this._integrationManager.getSocialIntegration(integration);
+ const { codeVerifier, state, url } =
+ await integrationProvider.generateAuthUrl();
+ await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300);
+
+ return { url };
+ }
+
+ @Post('/function')
+ async functionIntegration(
+ @GetOrgFromRequest() org: Organization,
+ @Body() body: IntegrationFunctionDto
+ ) {
+ const getIntegration = await this._integrationService.getIntegrationById(
+ org.id,
+ body.id
+ );
+ if (!getIntegration) {
+ throw new Error('Invalid integration');
+ }
+
+ if (getIntegration.type === 'social') {
+ const integrationProvider = this._integrationManager.getSocialIntegration(
+ getIntegration.providerIdentifier
+ );
+ if (!integrationProvider) {
+ throw new Error('Invalid provider');
+ }
+
+ if (integrationProvider[body.name]) {
+ return integrationProvider[body.name](getIntegration.token, body.data);
+ }
+ throw new Error('Function not found');
+ }
+
+ if (getIntegration.type === 'article') {
+ const integrationProvider =
+ this._integrationManager.getArticlesIntegration(
+ getIntegration.providerIdentifier
+ );
+ if (!integrationProvider) {
+ throw new Error('Invalid provider');
+ }
+
+ if (integrationProvider[body.name]) {
+ return integrationProvider[body.name](getIntegration.token, body.data);
+ }
+ throw new Error('Function not found');
+ }
+ }
+
+ @Post('/article/:integration/connect')
+ async connectArticle(
+ @GetOrgFromRequest() org: Organization,
+ @Param('integration') integration: string,
+ @Body() api: ApiKeyDto
+ ) {
+ if (
+ !this._integrationManager
+ .getAllowedArticlesIntegrations()
+ .includes(integration)
) {
- return {integrations: (await this._integrationService.getIntegrationsList(org.id)).map(p => ({name: p.name, id: p.id, picture: p.picture, identifier: p.providerIdentifier, type: p.type}))};
+ throw new Error('Integration not allowed');
}
- @Get('/social/:integration')
- async getIntegrationUrl(
- @Param('integration') integration: string
+ if (!api) {
+ throw new Error('Missing api');
+ }
+
+ const integrationProvider =
+ this._integrationManager.getArticlesIntegration(integration);
+ const { id, name, token, picture } = await integrationProvider.authenticate(
+ api.api
+ );
+
+ if (!id) {
+ throw new Error('Invalid api key');
+ }
+
+ return this._integrationService.createIntegration(
+ org.id,
+ name,
+ picture,
+ 'article',
+ String(id),
+ integration,
+ token
+ );
+ }
+
+ @Post('/social/:integration/connect')
+ async connectSocialMedia(
+ @GetOrgFromRequest() org: Organization,
+ @Param('integration') integration: string,
+ @Body() body: ConnectIntegrationDto
+ ) {
+ if (
+ !this._integrationManager
+ .getAllowedSocialsIntegrations()
+ .includes(integration)
) {
- if (!this._integrationManager.getAllowedSocialsIntegrations().includes(integration)) {
- throw new Error('Integration not allowed');
- }
-
- const integrationProvider = this._integrationManager.getSocialIntegration(integration);
- const {codeVerifier, state, url} = await integrationProvider.generateAuthUrl();
- await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300);
-
- return {url};
+ throw new Error('Integration not allowed');
}
- @Post('/article/:integration/connect')
- async connectArticle(
- @GetOrgFromRequest() org: Organization,
- @Param('integration') integration: string,
- @Body() api: ApiKeyDto
- ) {
- if (!this._integrationManager.getAllowedArticlesIntegrations().includes(integration)) {
- throw new Error('Integration not allowed');
- }
-
- if (!api) {
- throw new Error('Missing api');
- }
-
- const integrationProvider = this._integrationManager.getArticlesIntegration(integration);
- const {id, name, token, picture} = await integrationProvider.authenticate(api.api);
-
- if (!id) {
- throw new Error('Invalid api key');
- }
-
- return this._integrationService.createIntegration(org.id, name, picture,'article', String(id), integration, token);
+ const getCodeVerifier = await ioRedis.get(`login:${body.state}`);
+ if (!getCodeVerifier) {
+ throw new Error('Invalid state');
}
- @Post('/social/:integration/connect')
- async connectSocialMedia(
- @GetOrgFromRequest() org: Organization,
- @Param('integration') integration: string,
- @Body() body: ConnectIntegrationDto
- ) {
- if (!this._integrationManager.getAllowedSocialsIntegrations().includes(integration)) {
- throw new Error('Integration not allowed');
- }
+ const integrationProvider =
+ this._integrationManager.getSocialIntegration(integration);
+ const { accessToken, expiresIn, refreshToken, id, name, picture } =
+ await integrationProvider.authenticate({
+ code: body.code,
+ codeVerifier: getCodeVerifier,
+ });
- const getCodeVerifier = await ioRedis.get(`login:${body.state}`);
- if (!getCodeVerifier) {
- throw new Error('Invalid state');
- }
-
- const integrationProvider = this._integrationManager.getSocialIntegration(integration);
- const {accessToken, expiresIn, refreshToken, id, name, picture} = await integrationProvider.authenticate({
- code: body.code,
- codeVerifier: getCodeVerifier
- });
-
- if (!id) {
- throw new Error('Invalid api key');
- }
-
- return this._integrationService.createIntegration(org.id, name, picture, 'social', String(id), integration, accessToken, refreshToken, expiresIn);
+ if (!id) {
+ throw new Error('Invalid api key');
}
+
+ return this._integrationService.createIntegration(
+ org.id,
+ name,
+ picture,
+ 'social',
+ String(id),
+ integration,
+ accessToken,
+ refreshToken,
+ expiresIn
+ );
+ }
}
diff --git a/apps/backend/src/api/routes/media.controller.ts b/apps/backend/src/api/routes/media.controller.ts
new file mode 100644
index 00000000..641bc56c
--- /dev/null
+++ b/apps/backend/src/api/routes/media.controller.ts
@@ -0,0 +1,42 @@
+import {
+ Controller, FileTypeValidator, Get, MaxFileSizeValidator, ParseFilePipe, Post, Query, UploadedFile, UseInterceptors,
+} from '@nestjs/common';
+import { FileInterceptor } from '@nestjs/platform-express';
+import { Express } from 'express';
+import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request";
+import {Organization} from "@prisma/client";
+import {MediaService} from "@gitroom/nestjs-libraries/database/prisma/media/media.service";
+
+@Controller('/media')
+export class MediaController {
+ constructor(
+ private _mediaService: MediaService
+ ) {
+ }
+ @Post('/')
+ @UseInterceptors(FileInterceptor('file'))
+ uploadFile(
+ @GetOrgFromRequest() org: Organization,
+ @UploadedFile(
+ 'file',
+ new ParseFilePipe({
+ validators: [
+ new MaxFileSizeValidator({ maxSize: 10 * 1024 * 1024 }),
+ new FileTypeValidator({ fileType: 'image/*' }),
+ ],
+ })
+ )
+ file: Express.Multer.File
+ ) {
+ const filePath = file.path.replace(process.env.UPLOAD_DIRECTORY, '');
+ return this._mediaService.saveFile(org.id, file.originalname, filePath);
+ }
+
+ @Get('/')
+ getMedia(
+ @GetOrgFromRequest() org: Organization,
+ @Query('page') page: number,
+ ) {
+ return this._mediaService.getMedia(org.id, page);
+ }
+}
diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts
index 3e6950ec..aa386946 100644
--- a/apps/backend/src/api/routes/posts.controller.ts
+++ b/apps/backend/src/api/routes/posts.controller.ts
@@ -1,8 +1,9 @@
-import {Body, Controller, Post} from '@nestjs/common';
+import {Body, Controller, Get, Param, Post, Put, Query} from '@nestjs/common';
import {PostsService} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.service";
import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request";
import {Organization} from "@prisma/client";
import {CreatePostDto} from "@gitroom/nestjs-libraries/dtos/posts/create.post.dto";
+import {GetPostsDto} from "@gitroom/nestjs-libraries/dtos/posts/get.posts.dto";
@Controller('/posts')
export class PostsController {
@@ -11,6 +12,22 @@ export class PostsController {
) {
}
+ @Get('/')
+ getPosts(
+ @GetOrgFromRequest() org: Organization,
+ @Query() query: GetPostsDto
+ ) {
+ return this._postsService.getPosts(org.id, query);
+ }
+
+ @Get('/:id')
+ getPost(
+ @GetOrgFromRequest() org: Organization,
+ @Param('id') id: string,
+ ) {
+ return this._postsService.getPost(org.id, id);
+ }
+
@Post('/')
createPost(
@GetOrgFromRequest() org: Organization,
@@ -18,4 +35,13 @@ export class PostsController {
) {
return this._postsService.createPost(org.id, body);
}
+
+ @Put('/:id/date')
+ changeDate(
+ @GetOrgFromRequest() org: Organization,
+ @Param('id') id: string,
+ @Body('date') date: string
+ ) {
+ return this._postsService.changeDate(org.id, id, date);
+ }
}
diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts
index f39d7596..58f4eace 100644
--- a/apps/backend/src/main.ts
+++ b/apps/backend/src/main.ts
@@ -11,7 +11,7 @@ async function bootstrap() {
cors: {
credentials: true,
exposedHeaders: ['reload'],
- origin: [process.env.FRONTEND_URL]
+ origin: [process.env.FRONTEND_URL],
}
});
diff --git a/apps/backend/tsconfig.app.json b/apps/backend/tsconfig.app.json
index a2ce7652..693a8392 100644
--- a/apps/backend/tsconfig.app.json
+++ b/apps/backend/tsconfig.app.json
@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
- "types": ["node"],
+ "types": ["node", "Multer"],
"emitDecoratorMetadata": true,
"target": "es2021"
},
diff --git a/apps/frontend/src/app/(site)/err/page.tsx b/apps/frontend/src/app/(site)/err/page.tsx
new file mode 100644
index 00000000..c89fe5bb
--- /dev/null
+++ b/apps/frontend/src/app/(site)/err/page.tsx
@@ -0,0 +1,5 @@
+export default async function Page() {
+ return (
+
We are experiencing some difficulty, try to refresh the page
+ )
+}
\ No newline at end of file
diff --git a/apps/frontend/src/app/global.css b/apps/frontend/src/app/global.css
index 3ef18c18..4c853287 100644
--- a/apps/frontend/src/app/global.css
+++ b/apps/frontend/src/app/global.css
@@ -2,75 +2,253 @@
@tailwind components;
@tailwind utilities;
-body, html {
- background-color: black;
+body,
+html {
+ background-color: black;
}
.box {
- position: relative;
- padding: 8px 24px;
+ position: relative;
+ padding: 8px 24px;
}
.box span {
- position: relative;
- z-index: 2;
+ position: relative;
+ z-index: 2;
}
.box:after {
- border-radius: 50px;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- content: "";
- position: absolute;
- background: white;
- opacity: 0;
- z-index: 1;
- transition: all 0.3s ease-in-out;
+ border-radius: 50px;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ content: '';
+ position: absolute;
+ background: white;
+ opacity: 0;
+ z-index: 1;
+ transition: all 0.3s ease-in-out;
}
.showbox {
- color: black;
+ color: black;
}
.showbox:after {
- opacity: 1;
- background: white;
- transition: all 0.3s ease-in-out;
+ opacity: 1;
+ background: white;
+ transition: all 0.3s ease-in-out;
}
.table1 {
- width: 100%;
- border-collapse: collapse;
+ width: 100%;
+ border-collapse: collapse;
}
.table1 thead {
- background-color: #0F1524;
- height: 44px;
- font-size: 12px;
- border-bottom: 1px solid #28344F;
+ background-color: #0f1524;
+ height: 44px;
+ font-size: 12px;
+ border-bottom: 1px solid #28344f;
}
-.table1 thead th, .table1 tbody td {
- text-align: left;
- padding: 14px 24px;
+.table1 thead th,
+.table1 tbody td {
+ text-align: left;
+ padding: 14px 24px;
}
.table1 tbody td {
- padding: 16px 24px;
- font-family: Inter;
- font-size: 14px;
+ padding: 16px 24px;
+ font-family: Inter;
+ font-size: 14px;
}
.swal2-modal {
- background-color: black !important;
- border: 2px solid #0B101B;
+ background-color: black !important;
+ border: 2px solid #0b101b;
}
.swal2-modal * {
- color: white !important;
+ color: white !important;
}
.swal2-icon {
- color: white !important;
- border-color: white !important;
+ color: white !important;
+ border-color: white !important;
}
.swal2-confirm {
- background-color: #262373 !important;
-}
\ No newline at end of file
+ background-color: #262373 !important;
+}
+
+.w-md-editor-text {
+ min-height: 100% !important;
+}
+
+.react-tags {
+ position: relative;
+ border: 1px solid #28344f;
+ padding-left: 16px;
+ height: 44px;
+ border-radius: 4px;
+ background: #131b2c;
+ /* shared font styles */
+ font-size: 14px;
+ line-height: 1.2;
+ /* clicking anywhere will focus the input */
+ cursor: text;
+ display: flex;
+ align-items: center;
+}
+
+.react-tags.is-active {
+ border-color: #4f46e5;
+}
+
+.react-tags.is-disabled {
+ opacity: 0.75;
+ background-color: #eaeef2;
+ /* Prevent any clicking on the component */
+ pointer-events: none;
+ cursor: not-allowed;
+}
+
+.react-tags.is-invalid {
+ border-color: #fd5956;
+ box-shadow: 0 0 0 2px rgba(253, 86, 83, 0.25);
+}
+
+.react-tags__label {
+ position: absolute;
+ left: -10000px;
+ top: auto;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+}
+
+.react-tags__list {
+ /* Do not use display: contents, it's too buggy */
+ display: inline;
+ padding: 0;
+}
+
+.react-tags__list-item {
+ display: inline;
+ list-style: none;
+}
+
+.react-tags__tag {
+ margin: 0 0.25rem 0 0;
+ padding: 0.15rem 0.5rem;
+ border: 0;
+ border-radius: 3px;
+ background: #7236f1;
+ /* match the font styles */
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.react-tags__tag:hover {
+ color: #ffffff;
+ background-color: #4f46e5;
+}
+
+.react-tags__tag::after {
+ content: '';
+ display: inline-block;
+ width: 0.65rem;
+ height: 0.65rem;
+ clip-path: polygon(
+ 10% 0,
+ 0 10%,
+ 40% 50%,
+ 0 90%,
+ 10% 100%,
+ 50% 60%,
+ 90% 100%,
+ 100% 90%,
+ 60% 50%,
+ 100% 10%,
+ 90% 0,
+ 50% 40%
+ );
+ margin-left: 0.5rem;
+ font-size: 0.875rem;
+ background-color: #7c7d86;
+}
+
+.react-tags__tag:hover::after {
+ background-color: #ffffff;
+}
+
+.react-tags__combobox {
+ display: inline-block;
+ /* match tag layout */
+ /* prevents autoresize overflowing the container */
+ max-width: 100%;
+}
+
+.react-tags__combobox-input {
+ /* prevent autoresize overflowing the container */
+ max-width: 100%;
+ /* remove styles and layout from this element */
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: none;
+ background: none;
+ /* match the font styles */
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.react-tags__combobox-input::placeholder {
+ color: #7c7d86;
+ opacity: 1;
+}
+
+.react-tags__listbox {
+ position: absolute;
+ z-index: 1;
+ top: calc(100% + 5px);
+ /* Negate the border width on the container */
+ left: -2px;
+ right: -2px;
+ max-height: 12.5rem;
+ overflow-y: auto;
+ background: #131b2c;
+ border: 1px solid #afb8c1;
+ border-radius: 6px;
+ box-shadow: rgba(0, 0, 0, 0.1) 0 10px 15px -4px,
+ rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+}
+
+.react-tags__listbox-option {
+ padding: 0.375rem 0.5rem;
+}
+
+.react-tags__listbox-option:hover {
+ cursor: pointer;
+ background: #080b13;
+}
+
+.react-tags__listbox-option:not([aria-disabled='true']).is-active {
+ background: #4f46e5;
+ color: #ffffff;
+}
+
+.react-tags__listbox-option[aria-disabled='true'] {
+ color: #7c7d86;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.react-tags__listbox-option[aria-selected='true']::after {
+ content: '✓';
+ margin-left: 0.5rem;
+}
+
+.react-tags__listbox-option[aria-selected='true']:not(.is-active)::after {
+ color: #4f46e5;
+}
+
+.react-tags__listbox-option-highlight {
+ background-color: #ffdd00;
+}
diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx
index 7177d176..fe90cca4 100644
--- a/apps/frontend/src/app/layout.tsx
+++ b/apps/frontend/src/app/layout.tsx
@@ -1,4 +1,5 @@
import './global.css';
+import 'react-tooltip/dist/react-tooltip.css'
import LayoutContext from "@gitroom/frontend/components/layout/layout.context";
import {ReactNode} from "react";
diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx
index 3b513eef..20253626 100644
--- a/apps/frontend/src/components/launches/add.edit.model.tsx
+++ b/apps/frontend/src/components/launches/add.edit.model.tsx
@@ -22,6 +22,7 @@ import {
useMoveToIntegration,
useMoveToIntegrationListener,
} from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
+import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
export const PickPlatforms: FC<{
integrations: Integrations[];
@@ -157,7 +158,7 @@ export const PickPlatforms: FC<{
export const PreviewComponent: FC<{
integrations: Integrations[];
- editorValue: string[];
+ editorValue: Array<{ id?: string; content: string }>;
}> = (props) => {
const { integrations, editorValue } = props;
const [selectedIntegrations, setSelectedIntegrations] = useState([
@@ -201,7 +202,9 @@ export const AddEditModal: FC<{
>([]);
// value of each editor
- const [value, setValue] = useState(['']);
+ const [value, setValue] = useState>([
+ { content: '' },
+ ]);
const fetch = useFetch();
@@ -217,6 +220,18 @@ export const AddEditModal: FC<{
// hook to open a new modal
const modal = useModals();
+ // are we in edit mode?
+ const existingData = useExistingData();
+
+ // if it's edit just set the current integration
+ useEffect(() => {
+ if (existingData.integration) {
+ setSelectedIntegrations([
+ integrations.find((p) => p.id === existingData.integration)!,
+ ]);
+ }
+ }, [existingData.integration]);
+
// if the user exit the popup we reset the global variable with all the values
useEffect(() => {
return () => {
@@ -228,7 +243,7 @@ export const AddEditModal: FC<{
const changeValue = useCallback(
(index: number) => (newValue: string) => {
return setValue((prev) => {
- prev[index] = newValue;
+ prev[index].content = newValue;
return [...prev];
});
},
@@ -239,7 +254,7 @@ export const AddEditModal: FC<{
const addValue = useCallback(
(index: number) => () => {
setValue((prev) => {
- prev.splice(index + 1, 0, '');
+ prev.splice(index + 1, 0, { content: '' });
return [...prev];
});
},
@@ -307,20 +322,22 @@ export const AddEditModal: FC<{
-
- {!showHide.hideTopEditor ? (
+ {!existingData.integration && (
+
+ )}
+ {!existingData.integration && !showHide.hideTopEditor ? (
<>
{value.map((p, index) => (
<>
1 ? 150 : 500}
- value={p}
+ value={p.content}
preview="edit"
// @ts-ignore
onChange={changeValue(index)}
@@ -332,9 +349,11 @@ export const AddEditModal: FC<{
))}
>
) : (
-
- Global Editor Hidden
-
+ !existingData.integration && (
+
+ Global Editor Hidden
+
+ )
)}
{!!selectedIntegrations.length && (
{},
+ posts: [] as Array,
+ setFilters: (filters: { currentWeek: number, currentYear: number }) => {},
+ changeDate: (id: string, date: dayjs.Dayjs) => {},
});
export interface Integrations {
@@ -27,11 +33,44 @@ export const CalendarWeekProvider: FC<{ children: ReactNode, integrations: Integ
children,
integrations
}) => {
+ const fetch = useFetch();
+ const [internalData, setInternalData] = useState([] as any[]);
+
const [filters, setFilters] = useState({
currentWeek: dayjs().week(),
+ currentYear: dayjs().year(),
});
+
+ const params = useMemo(() => {
+ return new URLSearchParams({
+ week: filters.currentWeek.toString(),
+ year: filters.currentYear.toString(),
+ }).toString();
+ }, [filters]);
+
+ const loadData = useCallback(async(url: string) => {
+ return (await fetch(url)).json();
+ }, [filters]);
+
+ const {data, isLoading} = useSWR(`/posts?${params}`, loadData);
+
+ const changeDate = useCallback((id: string, date: dayjs.Dayjs) => {
+ setInternalData(d => d.map((post: Post) => {
+ if (post.id === id) {
+ return {...post, publishDate: date.format('YYYY-MM-DDTHH:mm:ss')};
+ }
+ return post;
+ }));
+ }, [data, internalData]);
+
+ useEffect(() => {
+ if (data) {
+ setInternalData(data);
+ }
+ }, [data]);
+
return (
-
+
{children}
);
diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx
index 35e34cdc..90ea4bdc 100644
--- a/apps/frontend/src/components/launches/calendar.tsx
+++ b/apps/frontend/src/components/launches/calendar.tsx
@@ -2,12 +2,18 @@
import { FC, useCallback, useMemo } from 'react';
import {
+ Integrations,
useCalendar,
} from '@gitroom/frontend/components/launches/calendar.context';
import dayjs from 'dayjs';
import { useModals } from '@mantine/modals';
-import {AddEditModal} from "@gitroom/frontend/components/launches/add.edit.model";
-import clsx from "clsx";
+import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
+import clsx from 'clsx';
+import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
+import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
+import { useDrag, useDrop } from 'react-dnd';
+import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
+import { Integration, Post } from '@prisma/client';
const days = [
'',
@@ -46,44 +52,179 @@ const hours = [
'23:00',
];
+const CalendarItem: FC<{
+ date: dayjs.Dayjs;
+ editPost: () => void;
+ integrations: Integrations[];
+ post: Post & { integration: Integration };
+}> = (props) => {
+ const { editPost, post, date, integrations } = props;
+ const [{ opacity }, dragRef] = useDrag(
+ () => ({
+ type: 'post',
+ item: { id: post.id, date },
+ collect: (monitor) => ({
+ opacity: monitor.isDragging() ? 0 : 1,
+ }),
+ }),
+ []
+ );
+ return (
+ p.identifier === post.integration?.providerIdentifier
+ )?.name
+ }: ${post.content.slice(0, 100)}`}
+ >
+
p.identifier === post.integration?.providerIdentifier
+ )?.picture!
+ }
+ />
+
+
+ );
+};
+
const CalendarColumn: FC<{ day: number; hour: string }> = (props) => {
const { day, hour } = props;
- const week = useCalendar();
+ const { currentWeek, integrations, posts, changeDate } = useCalendar();
const modal = useModals();
+ const fetch = useFetch();
const getDate = useMemo(() => {
const date =
- dayjs().isoWeek(week.currentWeek).isoWeekday(day).format('YYYY-MM-DD') +
+ dayjs().isoWeek(currentWeek).isoWeekday(day).format('YYYY-MM-DD') +
'T' +
hour +
':00';
return dayjs(date);
- }, [week.currentWeek]);
+ }, [currentWeek]);
+
+ const postList = useMemo(() => {
+ return posts.filter((post) => {
+ return dayjs(post.publishDate).local().isSame(getDate);
+ });
+ }, [posts]);
+
+ const isBeforeNow = useMemo(() => {
+ return getDate.isBefore(dayjs());
+ }, [getDate]);
+
+ const [{ canDrop }, drop] = useDrop(() => ({
+ accept: 'post',
+ drop: (item: any) => {
+ if (isBeforeNow) return;
+ fetch(`/posts/${item.id}/date`, {
+ method: 'PUT',
+ body: JSON.stringify({ date: getDate.utc().format('YYYY-MM-DDTHH:mm:ss') }),
+ });
+ changeDate(item.id, getDate);
+ },
+ collect: (monitor) => ({
+ canDrop: isBeforeNow ? false : !!monitor.canDrop() && !!monitor.isOver(),
+ }),
+ }));
+
+ const editPost = useCallback(
+ (id: string) => async () => {
+ const data = await (await fetch(`/posts/${id}`)).json();
+
+ modal.openModal({
+ closeOnClickOutside: false,
+ closeOnEscape: false,
+ withCloseButton: false,
+ children: (
+
+ f.id === data.integration
+ )}
+ date={getDate}
+ />
+
+ ),
+ size: '80%',
+ title: `Edit post for ${getDate.format('DD/MM/YYYY HH:mm')}`,
+ });
+ },
+ []
+ );
const addModal = useCallback(() => {
modal.openModal({
closeOnClickOutside: false,
closeOnEscape: false,
withCloseButton: false,
- children: (
-
- ),
+ children: ,
size: '80%',
title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
});
}, []);
- const isBeforeNow = useMemo(() => {
- return getDate.isBefore(dayjs());
- }, [getDate]);
-
return (
-
-
- {isBeforeNow ? '' : '+ Add'}
+
+
+
+ {postList.map((post) => (
+
1 && 'w-[33px] basis-[28px]',
+ 'h-full text-white relative flex justify-center items-center flex-grow-0 flex-shrink-0'
+ )}
+ >
+
+
+
+
+ ))}
+ {!isBeforeNow && (
+
+ )}
+
);
@@ -91,51 +232,53 @@ const CalendarColumn: FC<{ day: number; hour: string }> = (props) => {
export const Calendar = () => {
return (
-
-
- {days.map((day) => (
-
- {day}
-
- ))}
- {hours.map((hour) =>
- days.map((day, index) => (
- <>
- {index === 0 ? (
-
- {['00', '10', '20', '30', '40', '50'].map((num) => (
-
- {hour.split(':')[0] + ':' + num}
-
- ))}
-
- ) : (
-
- {['00', '10', '20', '30', '40', '50'].map((num) => (
-
- ))}
-
- )}
- >
- ))
- )}
+
+
+
+ {days.map((day) => (
+
+ {day}
+
+ ))}
+ {hours.map((hour) =>
+ days.map((day, index) => (
+ <>
+ {index === 0 ? (
+
+ {['00', '10', '20', '30', '40', '50'].map((num) => (
+
+ {hour.split(':')[0] + ':' + num}
+
+ ))}
+
+ ) : (
+
+ {['00', '10', '20', '30', '40', '50'].map((num) => (
+
+ ))}
+
+ )}
+ >
+ ))
+ )}
+
-
+
);
};
diff --git a/apps/frontend/src/components/launches/helpers/dnd.provider.tsx b/apps/frontend/src/components/launches/helpers/dnd.provider.tsx
new file mode 100644
index 00000000..907eb2d8
--- /dev/null
+++ b/apps/frontend/src/components/launches/helpers/dnd.provider.tsx
@@ -0,0 +1,13 @@
+"use client";
+
+import {FC, ReactNode} from "react";
+import { HTML5Backend } from 'react-dnd-html5-backend'
+import { DndProvider } from 'react-dnd'
+
+export const DNDProvider: FC<{children: ReactNode}> = ({children}) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts b/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts
new file mode 100644
index 00000000..c71c4c40
--- /dev/null
+++ b/apps/frontend/src/components/launches/helpers/use.custom.provider.function.ts
@@ -0,0 +1,25 @@
+import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
+import { useCallback } from 'react';
+import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
+
+export const useCustomProviderFunction = () => {
+ const { integration } = useIntegration();
+ const fetch = useFetch();
+ const get = useCallback(
+ async (funcName: string, customData?: string) => {
+ return (
+ await fetch('/integrations/function', {
+ method: 'POST',
+ body: JSON.stringify({
+ name: funcName,
+ id: integration?.id!,
+ data: customData,
+ }),
+ })
+ ).json();
+ },
+ [integration]
+ );
+
+ return { get };
+};
diff --git a/apps/frontend/src/components/launches/helpers/use.existing.data.tsx b/apps/frontend/src/components/launches/helpers/use.existing.data.tsx
new file mode 100644
index 00000000..4c7d7ad0
--- /dev/null
+++ b/apps/frontend/src/components/launches/helpers/use.existing.data.tsx
@@ -0,0 +1,20 @@
+import {createContext, FC, ReactNode, useContext} from "react";
+import {Post} from "@prisma/client";
+
+const ExistingDataContext = createContext({
+ integration: '',
+ posts: [] as Post[],
+ settings: {} as any
+});
+
+
+export const ExistingDataContextProvider: FC<{children: ReactNode, value: any}> = ({children, value}) => {
+ return (
+
+ {children}
+
+ );
+}
+
+
+export const useExistingData = () => useContext(ExistingDataContext);
\ No newline at end of file
diff --git a/apps/frontend/src/components/launches/helpers/use.formatting.ts b/apps/frontend/src/components/launches/helpers/use.formatting.ts
index ad2362d8..61e94d9a 100644
--- a/apps/frontend/src/components/launches/helpers/use.formatting.ts
+++ b/apps/frontend/src/components/launches/helpers/use.formatting.ts
@@ -1,19 +1,19 @@
import removeMd from "remove-markdown";
import {useMemo} from "react";
-export const useFormatting = (text: string[], params: {
+export const useFormatting = (text: Array<{content: string, id?: string}>, params: {
removeMarkdown?: boolean,
saveBreaklines?: boolean,
specialFunc?: (text: string) => string,
}) => {
return useMemo(() => {
return text.map((value) => {
- let newText = value;
+ let newText = value.content;
if (params.saveBreaklines) {
newText = newText.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢');
}
if (params.removeMarkdown) {
- newText = removeMd(value);
+ newText = removeMd(value.content);
}
if (params.saveBreaklines) {
newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n');
@@ -22,6 +22,7 @@ export const useFormatting = (text: string[], params: {
newText = params.specialFunc(newText);
}
return {
+ id: value.id,
text: newText,
count: params.removeMarkdown && params.saveBreaklines ? newText.replace(/\n/g, ' ').length : newText.length,
}
diff --git a/apps/frontend/src/components/launches/helpers/use.integration.ts b/apps/frontend/src/components/launches/helpers/use.integration.ts
index 515f146b..36e31022 100644
--- a/apps/frontend/src/components/launches/helpers/use.integration.ts
+++ b/apps/frontend/src/components/launches/helpers/use.integration.ts
@@ -3,6 +3,6 @@
import {createContext, useContext} from "react";
import {Integrations} from "@gitroom/frontend/components/launches/calendar.context";
-export const IntegrationContext = createContext<{integration: Integrations|undefined, value: string[]}>({integration: undefined, value: []});
+export const IntegrationContext = createContext<{integration: Integrations|undefined, value: Array<{content: string, id?: string}>}>({integration: undefined, value: []});
export const useIntegration = () => useContext(IntegrationContext);
\ No newline at end of file
diff --git a/apps/frontend/src/components/launches/helpers/use.values.ts b/apps/frontend/src/components/launches/helpers/use.values.ts
index 8732fe5c..baa2fdeb 100644
--- a/apps/frontend/src/components/launches/helpers/use.values.ts
+++ b/apps/frontend/src/components/launches/helpers/use.values.ts
@@ -1,25 +1,24 @@
import {useEffect, useMemo} from 'react';
-import { useForm } from 'react-hook-form';
-import { UseFormProps } from 'react-hook-form/dist/types';
-import {allProvidersSettings} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings";
+import {useForm, useFormContext} from 'react-hook-form';
import {classValidatorResolver} from "@hookform/resolvers/class-validator";
const finalInformation = {} as {
- [key: string]: { posts: string[]; settings: () => object; isValid: boolean };
+ [key: string]: { posts: Array<{id?: string, content: string, media?: Array
}>; settings: () => object; isValid: boolean };
};
-export const useValues = (identifier: string, integration: string, value: string[]) => {
+export const useValues = (initialValues: object, integration: string, identifier: string, value: Array<{id?: string, content: string, media?: Array}>, dto: any) => {
const resolver = useMemo(() => {
- const findValidator = allProvidersSettings.find((provider) => provider.identifier === identifier)!;
- return classValidatorResolver(findValidator?.validator);
+ return classValidatorResolver(dto);
}, [integration]);
const form = useForm({
- resolver
+ resolver,
+ values: initialValues,
+ mode: 'onChange'
});
const getValues = useMemo(() => {
- return form.getValues;
- }, [form]);
+ return () => ({...form.getValues(), __type: identifier});
+ }, [form, integration]);
finalInformation[integration]= finalInformation[integration] || {};
finalInformation[integration].posts = value;
@@ -35,20 +34,7 @@ export const useValues = (identifier: string, integration: string, value: string
return form;
};
-export const useSettings = (formProps?: Omit) => {
- // const { integration } = useIntegration();
- // const form = useForm({
- // ...formProps,
- // mode: 'onChange',
- // });
- //
- // finalInformation[integration?.identifier!].settings = {
- // __type: integration?.identifier!,
- // ...form.getValues(),
- // };
- // return form;
-};
-
+export const useSettings = () => useFormContext();
export const getValues = () => finalInformation;
export const resetValues = () => {
Object.keys(finalInformation).forEach((key) => {
diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx
index f30e42be..c7fedd3b 100644
--- a/apps/frontend/src/components/launches/launches.component.tsx
+++ b/apps/frontend/src/components/launches/launches.component.tsx
@@ -10,16 +10,18 @@ export const LaunchesComponent: FC<{
integrations: Integrations[]
}> = (props) => {
const { integrations } = props;
+
const sortedIntegrations = useMemo(() => {
return orderBy(integrations, ['type', 'identifier'], ['desc', 'asc']);
}, [integrations]);
+
return (
-
+
Channels
{sortedIntegrations.map((integration) => (
diff --git a/apps/frontend/src/components/launches/providers/devto.provider.tsx b/apps/frontend/src/components/launches/providers/devto.provider.tsx
deleted file mode 100644
index 8c0c338e..00000000
--- a/apps/frontend/src/components/launches/providers/devto.provider.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import {FC} from "react";
-import {withProvider} from "@gitroom/frontend/components/launches/providers/high.order.provider";
-
-const DevtoPreview: FC = () => {
- return
asd
-};
-
-const DevtoSettings: FC = () => {
- return
asdfasd
-};
-
-export default withProvider(DevtoSettings, DevtoPreview);
\ No newline at end of file
diff --git a/apps/frontend/src/components/launches/providers/devto/devto.provider.tsx b/apps/frontend/src/components/launches/providers/devto/devto.provider.tsx
new file mode 100644
index 00000000..6ad8e930
--- /dev/null
+++ b/apps/frontend/src/components/launches/providers/devto/devto.provider.tsx
@@ -0,0 +1,31 @@
+import {FC} from "react";
+import {withProvider} from "@gitroom/frontend/components/launches/providers/high.order.provider";
+import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto";
+import {useSettings} from "@gitroom/frontend/components/launches/helpers/use.values";
+import {Input} from "@gitroom/react/form/input";
+import {MediaComponent} from "@gitroom/frontend/components/media/media.component";
+import {SelectOrganization} from "@gitroom/frontend/components/launches/providers/devto/select.organization";
+import {DevtoTags} from "@gitroom/frontend/components/launches/providers/devto/devto.tags";
+
+const DevtoPreview: FC = () => {
+ return
asd
+};
+
+const DevtoSettings: FC = () => {
+ const form = useSettings();
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+};
+
+export default withProvider(DevtoSettings, DevtoPreview, DevToSettingsDto);
\ No newline at end of file
diff --git a/apps/frontend/src/components/launches/providers/devto/devto.tags.tsx b/apps/frontend/src/components/launches/providers/devto/devto.tags.tsx
new file mode 100644
index 00000000..b03669fd
--- /dev/null
+++ b/apps/frontend/src/components/launches/providers/devto/devto.tags.tsx
@@ -0,0 +1,61 @@
+import { FC, useCallback, useEffect, useState } from 'react';
+import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
+import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
+import { ReactTags } from 'react-tag-autocomplete';
+
+export const DevtoTags: FC<{
+ name: string;
+ label: string;
+ onChange: (event: { target: { value: any[]; name: string } }) => void;
+}> = (props) => {
+ const { onChange, name, label } = props;
+ const customFunc = useCustomProviderFunction();
+ const [tags, setTags] = useState
([]);
+ const { getValues } = useSettings();
+ const [tagValue, setTagValue] = useState([]);
+
+ const onDelete = useCallback(
+ (tagIndex: number) => {
+ const modify = tagValue.filter((_, i) => i !== tagIndex);
+ setTagValue(modify);
+ onChange({ target: { value: modify, name } });
+ },
+ [tagValue]
+ );
+
+ const onAddition = useCallback(
+ (newTag: any) => {
+ if (tagValue.length >= 4) {
+ return;
+ }
+ const modify = [...tagValue, newTag];
+ setTagValue(modify);
+ onChange({ target: { value: modify, name } });
+ },
+ [tagValue]
+ );
+
+ useEffect(() => {
+ customFunc.get('tags').then((data) => setTags(data));
+ const settings = getValues()[props.name];
+ if (settings) {
+ setTagValue(settings);
+ }
+ }, []);
+
+ if (!tags.length) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/apps/frontend/src/components/launches/providers/devto/select.organization.tsx b/apps/frontend/src/components/launches/providers/devto/select.organization.tsx
new file mode 100644
index 00000000..09e0f5ea
--- /dev/null
+++ b/apps/frontend/src/components/launches/providers/devto/select.organization.tsx
@@ -0,0 +1,44 @@
+import { FC, useEffect, useState } from 'react';
+import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
+import { Select } from '@gitroom/react/form/select';
+import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
+
+export const SelectOrganization: FC<{
+ name: string;
+ onChange: (event: { target: { value: string; name: string } }) => void;
+}> = (props) => {
+ const { onChange, name } = props;
+ const customFunc = useCustomProviderFunction();
+ const [orgs, setOrgs] = useState([]);
+ const { getValues } = useSettings();
+ const [currentMedia, setCurrentMedia] = useState();
+
+ const onChangeInner = (event: { target: { value: string, name: string } }) => {
+ setCurrentMedia(event.target.value);
+ onChange(event);
+ };
+
+ useEffect(() => {
+ customFunc.get('organizations').then((data) => setOrgs(data));
+ const settings = getValues()[props.name];
+ if (settings) {
+ setCurrentMedia(settings);
+ }
+ }, []);
+
+
+ if (!orgs.length) {
+ return null;
+ }
+
+ return (
+
+ --Select--
+ {orgs.map((org: any) => (
+
+ {org.name}
+
+ ))}
+
+ );
+};
diff --git a/apps/frontend/src/components/launches/providers/high.order.provider.tsx b/apps/frontend/src/components/launches/providers/high.order.provider.tsx
index f823d2e8..128e51d0 100644
--- a/apps/frontend/src/components/launches/providers/high.order.provider.tsx
+++ b/apps/frontend/src/components/launches/providers/high.order.provider.tsx
@@ -8,6 +8,11 @@ import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/
import { useValues } from '@gitroom/frontend/components/launches/helpers/use.values';
import { FormProvider } from 'react-hook-form';
import { useMoveToIntegrationListener } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
+import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
+import {
+ IntegrationContext,
+ useIntegration,
+} from '@gitroom/frontend/components/launches/helpers/use.integration';
// This is a simple function that if we edit in place, we hide the editor on top
export const EditorWrapper: FC = (props) => {
@@ -22,16 +27,28 @@ export const EditorWrapper: FC = (props) => {
return null;
};
-export const withProvider = (SettingsComponent: FC, PreviewComponent: FC) => {
+export const withProvider = (
+ SettingsComponent: FC,
+ PreviewComponent: FC,
+ dto?: any
+) => {
return (props: {
identifier: string;
id: string;
- value: string[];
+ value: Array<{ content: string; id?: string }>;
show: boolean;
}) => {
- const [editInPlace, setEditInPlace] = useState(false);
- const [InPlaceValue, setInPlaceValue] = useState(['']);
- const [showTab, setShowTab] = useState(0);
+ const existingData = useExistingData();
+ const { integration } = useIntegration();
+ const [editInPlace, setEditInPlace] = useState(!!existingData.integration);
+ const [InPlaceValue, setInPlaceValue] = useState<
+ Array<{ id?: string; content: string }>
+ >(
+ existingData.integration
+ ? existingData.posts.map((p) => ({ id: p.id, content: p.content }))
+ : [{ content: '' }]
+ );
+ const [showTab, setShowTab] = useState(existingData.integration ? 1 : 0);
// in case there is an error on submit, we change to the settings tab for the specific provider
useMoveToIntegrationListener(true, (identifier) => {
@@ -42,16 +59,18 @@ export const withProvider = (SettingsComponent: FC, PreviewComponent: FC) => {
// this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation
const form = useValues(
- props.identifier,
+ existingData.settings,
props.id,
- editInPlace ? InPlaceValue : props.value
+ props.identifier,
+ editInPlace ? InPlaceValue : props.value,
+ dto
);
// change editor value
const changeValue = useCallback(
(index: number) => (newValue: string) => {
return setInPlaceValue((prev) => {
- prev[index] = newValue;
+ prev[index].content = newValue;
return [...prev];
});
},
@@ -62,7 +81,7 @@ export const withProvider = (SettingsComponent: FC, PreviewComponent: FC) => {
const addValue = useCallback(
(index: number) => () => {
setInPlaceValue((prev) => {
- prev.splice(index + 1, 0, '');
+ prev.splice(index + 1, 0, { content: '' });
return [...prev];
});
},
@@ -85,12 +104,15 @@ export const withProvider = (SettingsComponent: FC, PreviewComponent: FC) => {
setShowTab(editor ? 1 : 0);
if (editor && !editInPlace) {
setEditInPlace(true);
- setInPlaceValue(props.value);
+ setInPlaceValue(
+ props.value.map((p) => ({ id: p.id, content: p.content }))
+ );
}
},
[props.value, editInPlace]
);
+ // this is a trick to prevent the data from being deleted, yet we don't render the elements
if (!props.show) {
return null;
}
@@ -123,7 +145,7 @@ export const withProvider = (SettingsComponent: FC, PreviewComponent: FC) => {
1 ? 200 : 500}
- value={val}
+ value={val.content}
preview="edit"
// @ts-ignore
onChange={changeValue(index)}
@@ -135,8 +157,21 @@ export const withProvider = (SettingsComponent: FC, PreviewComponent: FC) => {
))}
)}
- {showTab === 2 &&
}
- {showTab === 0 &&
}
+ {showTab === 2 && (
+
+
+
+ )}
+ {showTab === 0 && (
+
+
+
+ )}
);
diff --git a/apps/frontend/src/components/launches/providers/linkedin.provider.tsx b/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx
similarity index 86%
rename from apps/frontend/src/components/launches/providers/linkedin.provider.tsx
rename to apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx
index 122a7e2a..c8d84716 100644
--- a/apps/frontend/src/components/launches/providers/linkedin.provider.tsx
+++ b/apps/frontend/src/components/launches/providers/linkedin/linkedin.provider.tsx
@@ -1,42 +1,21 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
-import localFont from 'next/font/local';
import clsx from 'clsx';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
-const chirp = localFont({
- src: [
- {
- path: './fonts/x/Chirp-Regular.woff2',
- weight: '400',
- style: 'normal',
- },
- {
- path: './fonts/x/Chirp-Bold.woff2',
- weight: '700',
- style: 'normal',
- },
- ],
-});
-
const LinkedinPreview: FC = (props) => {
const { value: topValue, integration } = useIntegration();
const newValues = useFormatting(topValue, {
removeMarkdown: true,
saveBreaklines: true,
specialFunc: (text: string) => {
- return text.slice(0, 280);
- }
+ return text.slice(0, 280);
+ },
});
return (
-
+
{newValues.map((value, index) => (
{
@username
-
{value.text}
+
{value.text}
))}
diff --git a/apps/frontend/src/components/launches/providers/reddit.provider.tsx b/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx
similarity index 90%
rename from apps/frontend/src/components/launches/providers/reddit.provider.tsx
rename to apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx
index 1093b14d..08500d52 100644
--- a/apps/frontend/src/components/launches/providers/reddit.provider.tsx
+++ b/apps/frontend/src/components/launches/providers/reddit/reddit.provider.tsx
@@ -1,25 +1,9 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
-import localFont from 'next/font/local';
import clsx from 'clsx';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
-const chirp = localFont({
- src: [
- {
- path: './fonts/x/Chirp-Regular.woff2',
- weight: '400',
- style: 'normal',
- },
- {
- path: './fonts/x/Chirp-Bold.woff2',
- weight: '700',
- style: 'normal',
- },
- ],
-});
-
const RedditPreview: FC = (props) => {
const { value: topValue, integration } = useIntegration();
const newValues = useFormatting(topValue, {
@@ -34,7 +18,6 @@ const RedditPreview: FC = (props) => {
@@ -77,7 +60,7 @@ const RedditPreview: FC = (props) => {
@username
-
{value.text}
+
{value.text}
))}
diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx
index 39e72fb8..b8d5bc5c 100644
--- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx
+++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx
@@ -1,9 +1,9 @@
import {FC} from "react";
import {Integrations} from "@gitroom/frontend/components/launches/calendar.context";
-import DevtoProvider from "@gitroom/frontend/components/launches/providers/devto.provider";
-import XProvider from "@gitroom/frontend/components/launches/providers/x.provider";
-import LinkedinProvider from "@gitroom/frontend/components/launches/providers/linkedin.provider";
-import RedditProvider from "@gitroom/frontend/components/launches/providers/reddit.provider";
+import DevtoProvider from "@gitroom/frontend/components/launches/providers/devto/devto.provider";
+import XProvider from "@gitroom/frontend/components/launches/providers/x/x.provider";
+import LinkedinProvider from "@gitroom/frontend/components/launches/providers/linkedin/linkedin.provider";
+import RedditProvider from "@gitroom/frontend/components/launches/providers/reddit/reddit.provider";
const Providers = [
{identifier: 'devto', component: DevtoProvider},
@@ -12,7 +12,7 @@ const Providers = [
{identifier: 'reddit', component: RedditProvider},
];
-export const ShowAllProviders: FC<{integrations: Integrations[], value: string[], selectedProvider?: Integrations}> = (props) => {
+export const ShowAllProviders: FC<{integrations: Integrations[], value: Array<{content: string, id?: string}>, selectedProvider?: Integrations}> = (props) => {
const {integrations, value, selectedProvider} = props;
return (
<>
diff --git a/apps/frontend/src/components/launches/providers/fonts/x/Chirp-Bold.woff2 b/apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Bold.woff2
similarity index 100%
rename from apps/frontend/src/components/launches/providers/fonts/x/Chirp-Bold.woff2
rename to apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Bold.woff2
diff --git a/apps/frontend/src/components/launches/providers/fonts/x/Chirp-Regular.woff2 b/apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Regular.woff2
similarity index 100%
rename from apps/frontend/src/components/launches/providers/fonts/x/Chirp-Regular.woff2
rename to apps/frontend/src/components/launches/providers/x/fonts/x/Chirp-Regular.woff2
diff --git a/apps/frontend/src/components/launches/providers/x.provider.tsx b/apps/frontend/src/components/launches/providers/x/x.provider.tsx
similarity index 100%
rename from apps/frontend/src/components/launches/providers/x.provider.tsx
rename to apps/frontend/src/components/launches/providers/x/x.provider.tsx
diff --git a/apps/frontend/src/components/layout/layout.context.tsx b/apps/frontend/src/components/layout/layout.context.tsx
index 4310ae73..b2c27d19 100644
--- a/apps/frontend/src/components/layout/layout.context.tsx
+++ b/apps/frontend/src/components/layout/layout.context.tsx
@@ -23,6 +23,7 @@ function LayoutContextInner(params: {children: ReactNode}) {
baseUrl={process.env.NEXT_PUBLIC_BACKEND_URL!}
afterRequest={afterRequest}
>
+
{params?.children || <>>}
)
diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx
index 4fda7403..95ce3af7 100644
--- a/apps/frontend/src/components/layout/layout.settings.tsx
+++ b/apps/frontend/src/components/layout/layout.settings.tsx
@@ -5,12 +5,14 @@ import {ContextWrapper} from "@gitroom/frontend/components/layout/user.context";
import {NotificationComponent} from "@gitroom/frontend/components/notifications/notification.component";
import {TopMenu} from "@gitroom/frontend/components/layout/top.menu";
import {MantineWrapper} from "@gitroom/react/helpers/mantine.wrapper";
+import {ToolTip} from "@gitroom/frontend/components/layout/top.tip";
export const LayoutSettings = ({children}: {children: ReactNode}) => {
const user = JSON.parse(headers().get('user')!);
return (
+
diff --git a/apps/frontend/src/components/layout/top.tip.tsx b/apps/frontend/src/components/layout/top.tip.tsx
new file mode 100644
index 00000000..47638d9e
--- /dev/null
+++ b/apps/frontend/src/components/layout/top.tip.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+import { Tooltip } from 'react-tooltip';
+
+export const ToolTip = () => {
+ return
;
+};
diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx
new file mode 100644
index 00000000..d95b989c
--- /dev/null
+++ b/apps/frontend/src/components/media/media.component.tsx
@@ -0,0 +1,200 @@
+'use client';
+
+import { ChangeEvent, FC, useCallback, useEffect, useState } from 'react';
+import { Button } from '@gitroom/react/form/button';
+import useSWR from 'swr';
+import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
+import { Media } from '@prisma/client';
+import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
+import {useFormState} from "react-hook-form";
+import {useSettings} from "@gitroom/frontend/components/launches/helpers/use.values";
+
+export const MediaBox: FC<{
+ setMedia: (params: { id: string; path: string }) => void;
+ closeModal: () => void;
+}> = (props) => {
+ const { setMedia, closeModal } = props;
+ const [pages, setPages] = useState(0);
+ const [mediaList, setListMedia] = useState
([]);
+ const fetch = useFetch();
+ const mediaDirectory = useMediaDirectory();
+
+ const loadMedia = useCallback(async () => {
+ return (await fetch('/media')).json();
+ }, []);
+
+ const uploadMedia = useCallback(
+ async (file: ChangeEvent) => {
+ const maxFileSize = 10 * 1024 * 1024;
+ if (
+ !file?.target?.files?.length ||
+ file?.target?.files?.[0]?.size > maxFileSize
+ )
+ return;
+ const formData = new FormData();
+ formData.append('file', file?.target?.files?.[0]);
+ const data = await (
+ await fetch('/media', {
+ method: 'POST',
+ body: formData,
+ })
+ ).json();
+
+ console.log(data);
+ setListMedia([...mediaList, data]);
+ },
+ [mediaList]
+ );
+
+ const setNewMedia = useCallback(
+ (media: Media) => () => {
+ setMedia(media);
+ closeModal();
+ },
+ []
+ );
+
+ const { data } = useSWR('get-media', loadMedia);
+
+ useEffect(() => {
+ if (data?.pages) {
+ setPages(data.pages);
+ }
+ if (data?.results && data?.results?.length) {
+ setListMedia([...data.results]);
+ }
+ }, [data]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Upload assets
+
+
+
+
+ {mediaList.map((media) => (
+
+
+
+ ))}
+
+
+
+ );
+};
+export const MediaComponent: FC<{
+ label: string;
+ description: string;
+ value?: { path: string; id: string };
+ name: string;
+ onChange: (event: {
+ target: { name: string; value?: { id: string; path: string } };
+ }) => void;
+}> = (props) => {
+ const { name, label, description, onChange, value } = props;
+ const {getValues} = useSettings();
+ useEffect(() => {
+ const settings = getValues()[props.name];
+ if (settings) {
+ setCurrentMedia(settings);
+ }
+ }, []);
+ const [modal, setShowModal] = useState(false);
+ const [currentMedia, setCurrentMedia] = useState(value);
+ const mediaDirectory = useMediaDirectory();
+
+ const changeMedia = useCallback((m: { path: string; id: string }) => {
+ setCurrentMedia(m);
+ onChange({ target: { name, value: m } });
+ }, []);
+
+ const showModal = useCallback(() => {
+ setShowModal(!modal);
+ }, [modal]);
+
+ const clearMedia = useCallback(() => {
+ setCurrentMedia(undefined);
+ onChange({ target: { name, value: undefined } });
+ }, [value]);
+
+ return (
+
+ {modal &&
}
+
{label}
+
{description}
+ {!!currentMedia && (
+
+
window.open(mediaDirectory.set(currentMedia.path))}
+ />
+
+ )}
+
+ Select
+
+ Clear
+
+
+
+ );
+};
diff --git a/apps/frontend/src/middleware.ts b/apps/frontend/src/middleware.ts
index 1e02d8fe..e5a8c741 100644
--- a/apps/frontend/src/middleware.ts
+++ b/apps/frontend/src/middleware.ts
@@ -34,11 +34,21 @@ export async function middleware(request: NextRequest) {
}
try {
- const user = await (await fetchBackend('/user/self', {
+ const userResponse = await fetchBackend('/user/self', {
headers: {
auth: authCookie?.value!
}
- })).json();
+ });
+
+ if (userResponse.status === 401) {
+ return NextResponse.redirect(new URL('/auth/logout', nextUrl.href));
+ }
+
+ if ([200, 201].indexOf(userResponse.status) === -1) {
+ return NextResponse.redirect(new URL('/err', nextUrl.href));
+ }
+
+ const user = await userResponse.json();
const next = NextResponse.next();
next.headers.set('user', JSON.stringify(user));
diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js
index 84babd43..b836639e 100644
--- a/apps/frontend/tailwind.config.js
+++ b/apps/frontend/tailwind.config.js
@@ -23,6 +23,7 @@ module.exports = {
forth: '#612AD5',
fifth: '#28344F',
sixth: '#0B101B',
+ seventh: '#7236f1',
gray: '#8C8C8C',
input: '#131B2C',
inputText: '#64748B',
@@ -30,7 +31,18 @@ module.exports = {
},
gridTemplateColumns: {
'13': 'repeat(13, minmax(0, 1fr));'
- }
+ },
+ animation: {
+ fade: 'fadeOut 0.5s ease-in-out',
+ },
+
+ // that is actual animation
+ keyframes: theme => ({
+ fadeOut: {
+ '0%': { opacity: 0, transform: 'translateY(30px)' },
+ '100%': { opacity: 1, transform: 'translateY(0)' },
+ },
+ })
},
},
plugins: [
diff --git a/libraries/helpers/src/utils/custom.fetch.func.ts b/libraries/helpers/src/utils/custom.fetch.func.ts
index 1e7ba077..a6e84fe3 100644
--- a/libraries/helpers/src/utils/custom.fetch.func.ts
+++ b/libraries/helpers/src/utils/custom.fetch.func.ts
@@ -1,28 +1,32 @@
export interface Params {
- baseUrl: string,
- beforeRequest?: (url: string, options: RequestInit) => Promise,
- afterRequest?: (url: string, options: RequestInit, response: Response) => Promise
+ baseUrl: string;
+ beforeRequest?: (url: string, options: RequestInit) => Promise;
+ afterRequest?: (
+ url: string,
+ options: RequestInit,
+ response: Response
+ ) => Promise;
}
export const customFetch = (params: Params, auth?: string) => {
- return async function newFetch (url: string, options: RequestInit = {}) {
- const newRequestObject = await params?.beforeRequest?.(url, options);
- const fetchRequest = await fetch(params.baseUrl + url, {
- credentials: 'include',
- ...(
- newRequestObject || options
- ),
- headers: {
- ...options?.headers,
- ...auth ? {auth} : {},
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- }
- });
- await params?.afterRequest?.(url, options, fetchRequest);
- return fetchRequest;
- }
-}
+ return async function newFetch(url: string, options: RequestInit = {}) {
+ const newRequestObject = await params?.beforeRequest?.(url, options);
+ const fetchRequest = await fetch(params.baseUrl + url, {
+ credentials: 'include',
+ ...(newRequestObject || options),
+ headers: {
+ ...(auth ? { auth } : {}),
+ ...(options.body instanceof FormData
+ ? {}
+ : { 'Content-Type': 'application/json' }),
+ Accept: 'application/json',
+ ...options?.headers,
+ },
+ });
+ await params?.afterRequest?.(url, options, fetchRequest);
+ return fetchRequest;
+ };
+};
export const fetchBackend = customFetch({
- baseUrl: process.env.NEXT_PUBLIC_BACKEND_URL!
+ baseUrl: process.env.NEXT_PUBLIC_BACKEND_URL!,
});
diff --git a/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts b/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts
index e488ce49..2ea68379 100644
--- a/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts
+++ b/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts
@@ -74,7 +74,7 @@ export class BullMqClient extends ClientProxy {
return () => void 0;
}
- async delay(pattern: string, jobId: string, delay: number) {
+ delay(pattern: string, jobId: string, delay: number) {
const queue = this.getQueue(pattern);
return queue.getJob(jobId).then((job) => job?.changeDelay(delay));
}
@@ -84,6 +84,11 @@ export class BullMqClient extends ClientProxy {
return queue.getJob(jobId).then((job) => job?.remove());
}
+ job(pattern: string, jobId: string) {
+ const queue = this.getQueue(pattern);
+ return queue.getJob(jobId);
+ }
+
protected async dispatchEvent(
packet: ReadPacket>,
): Promise {
diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
index 549764f1..eb0025e6 100644
--- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
@@ -14,6 +14,8 @@ import {IntegrationRepository} from "@gitroom/nestjs-libraries/database/prisma/i
import {PostsService} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.service";
import {PostsRepository} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.repository";
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";
@Global()
@Module({
@@ -35,6 +37,8 @@ import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integra
IntegrationRepository,
PostsService,
PostsRepository,
+ MediaService,
+ MediaRepository,
IntegrationManager
],
get exports() {
diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts
index 50a58631..dd77663f 100644
--- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts
@@ -24,6 +24,15 @@ export class IntegrationRepository {
})
}
+ getIntegrationById(org: string, id: string) {
+ return this._integration.model.integration.findFirst({
+ where: {
+ organizationId: org,
+ id
+ }
+ });
+ }
+
getIntegrationsList(org: string) {
return this._integration.model.integration.findMany({
where: {
diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
index 692fdfa4..8286a5e6 100644
--- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts
@@ -14,4 +14,8 @@ export class IntegrationService {
getIntegrationsList(org: string) {
return this._integrationRepository.getIntegrationsList(org);
}
+
+ getIntegrationById(org: string, id: string) {
+ return this._integrationRepository.getIntegrationById(org, id);
+ }
}
diff --git a/libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts b/libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts
new file mode 100644
index 00000000..b0658035
--- /dev/null
+++ b/libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts
@@ -0,0 +1,50 @@
+import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
+import { Injectable } from '@nestjs/common';
+
+@Injectable()
+export class MediaRepository {
+ constructor(private _media: PrismaRepository<'media'>) {}
+
+ saveFile(org: string, fileName: string, filePath: string) {
+ return this._media.model.media.create({
+ data: {
+ organization: {
+ connect: {
+ id: org,
+ },
+ },
+ name: fileName,
+ path: filePath,
+ },
+ });
+ }
+
+ async getMedia(org: string, page: number) {
+ const pageNum = (page || 1) - 1;
+ const query = {
+ where: {
+ organization: {
+ id: org,
+ },
+ },
+ };
+ const pages =
+ pageNum === 0
+ ? Math.ceil((await this._media.model.media.count(query)) / 10)
+ : 0;
+ const results = await this._media.model.media.findMany({
+ where: {
+ organization: {
+ id: org,
+ },
+ },
+ skip: pageNum * 10,
+ take: 10,
+ });
+
+ return {
+ pages,
+ results,
+ };
+ }
+}
diff --git a/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts
new file mode 100644
index 00000000..78227061
--- /dev/null
+++ b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts
@@ -0,0 +1,17 @@
+import {Injectable} from "@nestjs/common";
+import {MediaRepository} from "@gitroom/nestjs-libraries/database/prisma/media/media.repository";
+
+@Injectable()
+export class MediaService {
+ constructor(
+ private _mediaRepository: MediaRepository
+ ){}
+
+ saveFile(org: string, fileName: string, filePath: string) {
+ return this._mediaRepository.saveFile(org, fileName, filePath);
+ }
+
+ getMedia(org: string, page: number) {
+ return this._mediaRepository.getMedia(org, page);
+ }
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts
index d12e152b..ecc9cca0 100644
--- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts
@@ -1,17 +1,56 @@
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { Post as PostBody } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
-import dayjs from 'dayjs';
import { Integration, Post } from '@prisma/client';
+import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
+import dayjs from 'dayjs';
+import isoWeek from 'dayjs/plugin/isoWeek';
+import { v4 as uuidv4 } from 'uuid';
+import {instanceToInstance, instanceToPlain} from "class-transformer";
+import {validate} from "class-validator";
+
+dayjs.extend(isoWeek);
@Injectable()
export class PostsRepository {
constructor(private _post: PrismaRepository<'post'>) {}
- getPost(id: string, includeIntegration = false) {
+ getPosts(orgId: string, query: GetPostsDto) {
+ const date = dayjs().year(query.year).isoWeek(query.week);
+
+ const startDate = date.startOf('isoWeek').toDate();
+ const endDate = date.endOf('isoWeek').toDate();
+
+ return this._post.model.post.findMany({
+ where: {
+ organizationId: orgId,
+ publishDate: {
+ gte: startDate,
+ lte: endDate,
+ },
+ parentPostId: null,
+ },
+ select: {
+ id: true,
+ content: true,
+ publishDate: true,
+ releaseURL: true,
+ state: true,
+ integration: {
+ select: {
+ id: true,
+ providerIdentifier: true,
+ },
+ },
+ },
+ });
+ }
+
+ getPost(id: string, includeIntegration = false, orgId?: string) {
return this._post.model.post.findUnique({
where: {
id,
+ ...(orgId ? { organizationId: orgId } : {}),
},
include: {
...(includeIntegration ? { integration: true } : {}),
@@ -33,35 +72,56 @@ export class PostsRepository {
});
}
- async createPost(orgId: string, date: string, body: PostBody) {
+ async changeDate(orgId: string, id: string, date: string) {
+ return this._post.model.post.update({
+ where: {
+ organizationId: orgId,
+ id,
+ },
+ data: {
+ publishDate: dayjs(date).toDate(),
+ },
+ });
+ }
+
+ async createOrUpdatePost(orgId: string, date: string, body: PostBody) {
const posts: Post[] = [];
+
for (const value of body.value) {
- posts.push(
- await this._post.model.post.create({
- data: {
- publishDate: dayjs(date).toDate(),
- integration: {
- connect: {
- id: body.integration.id,
- organizationId: orgId,
- },
- },
- ...(posts.length
- ? {
- parentPost: {
- connect: {
- id: posts[posts.length - 1]?.id,
- },
- },
- }
- : {}),
- content: value,
- organization: {
- connect: {
- id: orgId,
- },
- },
+ const updateData = {
+ publishDate: dayjs(date).toDate(),
+ integration: {
+ connect: {
+ id: body.integration.id,
+ organizationId: orgId,
},
+ },
+ ...(posts.length
+ ? {
+ parentPost: {
+ connect: {
+ id: posts[posts.length - 1]?.id,
+ },
+ },
+ }
+ : {}),
+ content: value.content,
+ settings: JSON.stringify(body.settings),
+ organization: {
+ connect: {
+ id: orgId,
+ },
+ },
+ };
+
+
+ posts.push(
+ await this._post.model.post.upsert({
+ where: {
+ id: value.id || uuidv4()
+ },
+ create: updateData,
+ update: updateData,
})
);
}
diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
index 094f9aba..f0f1aea1 100644
--- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
@@ -5,6 +5,7 @@ import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport/client
import dayjs from 'dayjs';
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import { Integration, Post } from '@prisma/client';
+import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
type PostWithConditionals = Post & {
integration?: Integration;
@@ -21,17 +22,35 @@ export class PostsService {
async getPostsRecursively(
id: string,
- includeIntegration = false
+ includeIntegration = false,
+ orgId?: string
): Promise {
- const post = await this._postRepository.getPost(id, includeIntegration);
+ const post = await this._postRepository.getPost(
+ id,
+ includeIntegration,
+ orgId
+ );
return [
post!,
...(post?.childrenPost?.length
- ? await this.getPostsRecursively(post.childrenPost[0].id)
+ ? await this.getPostsRecursively(post.childrenPost[0].id, false, orgId)
: []),
];
}
+ getPosts(orgId: string, query: GetPostsDto) {
+ return this._postRepository.getPosts(orgId, query);
+ }
+
+ async getPost(orgId: string, id: string) {
+ const posts = await this.getPostsRecursively(id, false, orgId);
+ return {
+ posts,
+ integration: posts[0].integrationId,
+ settings: JSON.parse(posts[0].settings || '{}'),
+ };
+ }
+
async post(id: string) {
const [firstPost, ...morePosts] = await this.getPostsRecursively(id, true);
if (!firstPost) {
@@ -39,49 +58,71 @@ export class PostsService {
}
if (firstPost.integration?.type === 'article') {
- return this.postArticle(firstPost.integration!, [firstPost, ...morePosts]);
+ return this.postArticle(firstPost.integration!, [
+ firstPost,
+ ...morePosts,
+ ]);
}
return this.postSocial(firstPost.integration!, [firstPost, ...morePosts]);
}
private async postSocial(integration: Integration, posts: Post[]) {
- const getIntegration = this._integrationManager.getSocialIntegration(integration.providerIdentifier);
- if (!getIntegration) {
- return;
- }
+ const getIntegration = this._integrationManager.getSocialIntegration(
+ integration.providerIdentifier
+ );
+ if (!getIntegration) {
+ return;
+ }
- const publishedPosts = await getIntegration.post(integration.internalId, integration.token, posts.map(p => ({
+ const publishedPosts = await getIntegration.post(
+ integration.internalId,
+ integration.token,
+ posts.map((p) => ({
id: p.id,
message: p.content,
settings: JSON.parse(p.settings || '{}'),
- })));
+ }))
+ );
- for (const post of publishedPosts) {
- await this._postRepository.updatePost(post.id, post.postId, post.releaseURL);
- }
+ for (const post of publishedPosts) {
+ await this._postRepository.updatePost(
+ post.id,
+ post.postId,
+ post.releaseURL
+ );
+ }
}
private async postArticle(integration: Integration, posts: Post[]) {
- const getIntegration = this._integrationManager.getArticlesIntegration(integration.providerIdentifier);
- if (!getIntegration) {
- return;
- }
- const {postId, releaseURL} = await getIntegration.post(integration.token, posts.map(p => p.content).join('\n\n'), JSON.parse(posts[0].settings || '{}'));
- await this._postRepository.updatePost(posts[0].id, postId, releaseURL);
+ const getIntegration = this._integrationManager.getArticlesIntegration(
+ integration.providerIdentifier
+ );
+ if (!getIntegration) {
+ return;
+ }
+ const { postId, releaseURL } = await getIntegration.post(
+ integration.token,
+ posts.map((p) => p.content).join('\n\n'),
+ JSON.parse(posts[0].settings || '{}')
+ );
+ await this._postRepository.updatePost(posts[0].id, postId, releaseURL);
}
async createPost(orgId: string, body: CreatePostDto) {
for (const post of body.posts) {
- const posts = await this._postRepository.createPost(
+ const posts = await this._postRepository.createOrUpdatePost(
orgId,
body.date,
post
);
+
+ await this._workerServiceProducer.delete('post', posts[0].id);
+
this._workerServiceProducer.emit('post', {
id: posts[0].id,
options: {
- delay: 0 // dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
+ delay: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
},
payload: {
id: posts[0].id,
@@ -89,4 +130,18 @@ export class PostsService {
});
}
}
+
+ async changeDate(orgId: string, id: string, date: string) {
+ await this._workerServiceProducer.delete('post', id);
+ this._workerServiceProducer.emit('post', {
+ id: id,
+ options: {
+ delay: dayjs(date).diff(dayjs(), 'millisecond'),
+ },
+ payload: {
+ id: id,
+ },
+ });
+ return this._postRepository.changeDate(orgId, id, date);
+ }
}
diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
index 49b38347..18c026b8 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -101,7 +101,7 @@ model Star {
model Media {
id String @id @default(uuid())
name String
- url String
+ path String
organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
posts PostMedia[]
diff --git a/libraries/nestjs-libraries/src/dtos/integrations/integration.function.dto.ts b/libraries/nestjs-libraries/src/dtos/integrations/integration.function.dto.ts
new file mode 100644
index 00000000..5f2faa4e
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/integrations/integration.function.dto.ts
@@ -0,0 +1,13 @@
+import {IsDefined, IsString} from "class-validator";
+
+export class IntegrationFunctionDto {
+ @IsString()
+ @IsDefined()
+ name: string;
+
+ @IsString()
+ @IsDefined()
+ id: string;
+
+ data: any;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/media/media.dto.ts b/libraries/nestjs-libraries/src/dtos/media/media.dto.ts
new file mode 100644
index 00000000..568fb14a
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/media/media.dto.ts
@@ -0,0 +1,11 @@
+import {IsDefined, IsString} from "class-validator";
+
+export class MediaDto {
+ @IsString()
+ @IsDefined()
+ id: string;
+
+ @IsString()
+ @IsDefined()
+ path: string;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts
index b47b48c0..4a063f25 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts
@@ -1,12 +1,24 @@
-import {ArrayMinSize, IsArray, IsDateString, IsDefined, IsString, ValidateNested} from "class-validator";
-import {Type} from "class-transformer";
-import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto";
+import {
+ ArrayMinSize, IsArray, IsDateString, IsDefined, IsOptional, IsString, ValidateNested,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
export class EmptySettings {}
export class Integration {
@IsDefined()
@IsString()
- id: string
+ id: string;
+}
+
+export class PostContent {
+ @IsDefined()
+ @IsString()
+ content: string;
+
+ @IsOptional()
+ @IsString()
+ id: string;
}
export class Post {
@@ -18,19 +30,18 @@ export class Post {
@IsDefined()
@ArrayMinSize(1)
@IsArray()
- @IsString({ each: true })
- value: string[];
+ @Type(() => PostContent)
+ @ValidateNested({ each: true })
+ value: PostContent[];
@Type(() => EmptySettings, {
- keepDiscriminatorProperty: true,
+ keepDiscriminatorProperty: false,
discriminator: {
property: '__type',
- subTypes: [
- { value: DevToSettingsDto, name: 'devto' },
- ],
+ subTypes: [{ value: DevToSettingsDto, name: 'devto' }],
},
})
- settings: DevToSettingsDto
+ settings: DevToSettingsDto;
}
export class CreatePostDto {
@@ -41,7 +52,7 @@ export class CreatePostDto {
@IsDefined()
@Type(() => Post)
@IsArray()
- @ValidateNested({each: true})
+ @ValidateNested({ each: true })
@ArrayMinSize(1)
- posts: Post[]
+ posts: Post[];
}
diff --git a/libraries/nestjs-libraries/src/dtos/posts/get.posts.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/get.posts.dto.ts
new file mode 100644
index 00000000..525c504d
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/posts/get.posts.dto.ts
@@ -0,0 +1,17 @@
+import {Type} from "class-transformer";
+import {IsNumber, Max, Min} from "class-validator";
+import dayjs from "dayjs";
+
+export class GetPostsDto {
+ @Type(() => Number)
+ @IsNumber()
+ @Max(52)
+ @Min(1)
+ week: number;
+
+ @Type(() => Number)
+ @IsNumber()
+ @Max(dayjs().add(10, 'year').year())
+ @Min(2022)
+ year: number;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
index d1e91366..9caa68af 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
@@ -1,7 +1,2 @@
import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto";
-export const allProvidersSettings = [{
- identifier: 'devto',
- validator: DevToSettingsDto
-}];
-
export type AllProvidersSettings = DevToSettingsDto;
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts
index 433a24d5..0a835efb 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts
@@ -1,24 +1,32 @@
-import {IsArray, IsDefined, IsOptional, IsString} from "class-validator";
+import {ArrayMaxSize, IsArray, IsDefined, IsOptional, IsString, Matches, MinLength, ValidateNested} from "class-validator";
+import {MediaDto} from "@gitroom/nestjs-libraries/dtos/media/media.dto";
+import {Type} from "class-transformer";
+import {DevToTagsSettings} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.tags.settings";
export class DevToSettingsDto {
@IsString()
+ @MinLength(2)
@IsDefined()
title: string;
- @IsString()
@IsOptional()
- main_image?: number;
+ @ValidateNested()
+ @Type(() => MediaDto)
+ main_image?: MediaDto;
+ @IsOptional()
@IsString()
- canonical: string;
-
- @IsString({
- each: true
+ @Matches(/^(|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/, {
+ message: 'Invalid URL'
})
- @IsArray()
- tags: string[];
+ canonical?: string;
@IsString()
@IsOptional()
organization?: string;
+
+ @IsArray()
+ @ArrayMaxSize(4)
+ @IsOptional()
+ tags: DevToTagsSettings[];
}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.tags.settings.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.tags.settings.ts
new file mode 100644
index 00000000..f71b241e
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.tags.settings.ts
@@ -0,0 +1,9 @@
+import {IsNumber, IsString} from "class-validator";
+
+export class DevToTagsSettings {
+ @IsNumber()
+ value: number;
+
+ @IsString()
+ label: string;
+}
\ No newline at end of file
diff --git a/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts
index f13ff12e..0ba8f2d7 100644
--- a/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/article/dev.to.provider.ts
@@ -1,27 +1,99 @@
-import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface";
+import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
+import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
export class DevToProvider implements ArticleProvider {
- identifier = 'devto';
- name = 'Dev.to';
- async authenticate(token: string) {
- const {name, id, profile_image} = await (await fetch('https://dev.to/api/users/me', {
+ identifier = 'devto';
+ name = 'Dev.to';
+ async authenticate(token: string) {
+ const { name, id, profile_image } = await (
+ await fetch('https://dev.to/api/users/me', {
+ headers: {
+ 'api-key': token,
+ },
+ })
+ ).json();
+
+ return {
+ id,
+ name,
+ token,
+ picture: profile_image,
+ };
+ }
+
+ async tags(token: string) {
+ const tags = await (
+ await fetch('https://dev.to/api/tags?per_page=1000&page=1', {
+ headers: {
+ 'api-key': token,
+ },
+ })
+ ).json();
+
+ return tags.map((p: any) => ({ value: p.id, label: p.name }));
+ }
+
+ async organizations(token: string) {
+ const orgs = await (
+ await fetch('https://dev.to/api/articles/me/all?per_page=1000', {
+ headers: {
+ 'api-key': token,
+ },
+ })
+ ).json();
+
+ const allOrgs: string[] = [
+ ...new Set(
+ orgs
+ .flatMap((org: any) => org?.organization?.username)
+ .filter((f: string) => f)
+ ),
+ ] as string[];
+ const fullDetails = await Promise.all(
+ allOrgs.map(async (org: string) => {
+ return (
+ await fetch(`https://dev.to/api/organizations/${org}`, {
headers: {
- 'api-key': token
- }
- })).json();
+ 'api-key': token,
+ },
+ })
+ ).json();
+ })
+ );
- return {
- id,
- name,
- token,
- picture: profile_image
- }
- }
+ return fullDetails.map((org: any) => ({
+ id: org.id,
+ name: org.name,
+ username: org.username,
+ }));
+ }
- async post(token: string, content: string, settings: object) {
- return {
- postId: '123',
- releaseURL: 'https://dev.to'
- }
- }
-}
\ No newline at end of file
+ async post(token: string, content: string, settings: DevToSettingsDto) {
+ const { id, url } = await (
+ await fetch(`https://dev.to/api/articles`, {
+ method: 'POST',
+ body: JSON.stringify({
+ article: {
+ title: settings.title,
+ body_markdown: content,
+ published: false,
+ main_image: settings?.main_image?.path
+ ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${settings?.main_image?.path}`
+ : undefined,
+ tags: settings?.tags?.map((t) => t.label),
+ organization_id: settings.organization,
+ },
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'api-key': token,
+ },
+ })
+ ).json();
+
+ return {
+ postId: String(id),
+ releaseURL: url,
+ };
+ }
+}
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
index b745221f..ac5c258a 100644
--- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
@@ -54,7 +54,7 @@ export class LinkedinProvider implements SocialProvider {
}&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/linkedin`
)}&state=${state}&scope=${encodeURIComponent(
- 'openid profile w_member_social r_liteprofile'
+ 'openid profile w_member_social'
)}`;
return {
url,
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index 43da34d3..d49c6893 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -82,6 +82,7 @@ export class XProvider implements SocialProvider {
accessToken: string,
postDetails: PostDetails[],
): Promise {
+ console.log('hello');
const client = new TwitterApi(accessToken);
const {data: {username}} = await client.v2.me({
"user.fields": "username"
diff --git a/libraries/nestjs-libraries/src/upload/upload.module.ts b/libraries/nestjs-libraries/src/upload/upload.module.ts
new file mode 100644
index 00000000..4f8e8cb1
--- /dev/null
+++ b/libraries/nestjs-libraries/src/upload/upload.module.ts
@@ -0,0 +1,39 @@
+import { Global, Module } from '@nestjs/common';
+import {MulterModule} from "@nestjs/platform-express";
+import {diskStorage} from "multer";
+import {mkdirSync} from 'fs';
+import {extname} from 'path';
+
+const storage = diskStorage({
+ destination: (req, file, cb) => {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, '0'); // Month is zero-based, hence +1
+ const day = String(now.getDate()).padStart(2, '0');
+
+ const dir = `${process.env.UPLOAD_DIRECTORY}/${year}/${month}/${day}`;
+
+ // Create the directory if it doesn't exist
+ mkdirSync(dir, { recursive: true });
+
+ cb(null, dir);
+ },
+ filename: (req, file, cb) => {
+ // Generate a unique filename here if needed
+ const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('');
+ cb(null, `${randomName}${extname(file.originalname)}`);
+ },
+});
+
+@Global()
+@Module({
+ imports: [
+ MulterModule.register({
+ storage
+ }),
+ ],
+ get exports() {
+ return this.imports;
+ },
+})
+export class UploadModule {}
diff --git a/libraries/react-shared-libraries/src/form/select.tsx b/libraries/react-shared-libraries/src/form/select.tsx
new file mode 100644
index 00000000..6f61a9f3
--- /dev/null
+++ b/libraries/react-shared-libraries/src/form/select.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import {DetailedHTMLProps, FC, SelectHTMLAttributes, useMemo} from "react";
+import {clsx} from "clsx";
+import {useFormContext} from "react-hook-form";
+
+export const Select: FC, HTMLSelectElement> & {label: string, name: string}> = (props) => {
+ const {label, className, ...rest} = props;
+ const form = useFormContext();
+ const err = useMemo(() => {
+ if (!form || !form.formState.errors[props?.name!]) return;
+ return form?.formState?.errors?.[props?.name!]?.message! as string;
+ }, [form?.formState?.errors?.[props?.name!]?.message]);
+
+ return (
+
+
{label}
+
+
{err || <> >}
+
+ )
+}
\ No newline at end of file
diff --git a/libraries/react-shared-libraries/src/helpers/use.media.directory.ts b/libraries/react-shared-libraries/src/helpers/use.media.directory.ts
new file mode 100644
index 00000000..de5ebf9f
--- /dev/null
+++ b/libraries/react-shared-libraries/src/helpers/use.media.directory.ts
@@ -0,0 +1,11 @@
+import {useCallback} from "react";
+
+export const useMediaDirectory = () => {
+ const set = useCallback((path: string) => {
+ return `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${path}`;
+ }, []);
+
+ return {
+ set,
+ }
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 829b7d6c..bd101d12 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"@nestjs/microservices": "^10.3.1",
"@nestjs/platform-express": "^10.0.2",
"@nestjs/schedule": "^4.0.0",
+ "@nestjs/serve-static": "^4.0.1",
"@novu/node": "^0.23.0",
"@novu/notification-center": "^0.23.0",
"@prisma/client": "^5.8.1",
@@ -46,14 +47,19 @@
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"md5": "^2.3.0",
+ "multer": "^1.4.5-lts.1",
"nestjs-command": "^3.1.4",
"next": "13.4.4",
"prisma-paginate": "^5.2.1",
"react": "18.2.0",
+ "react-dnd": "^16.0.1",
+ "react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"react-query": "^3.39.3",
"react-router-dom": "6.11.2",
+ "react-tag-autocomplete": "^7.2.0",
+ "react-tooltip": "^5.26.2",
"redis": "^4.6.12",
"reflect-metadata": "^0.1.13",
"remove-markdown": "^0.5.0",
@@ -61,6 +67,7 @@
"simple-statistics": "^7.8.3",
"stripe": "^14.14.0",
"sweetalert2": "^11.10.5",
+ "swr": "^2.2.5",
"tslib": "^2.3.0",
"twitter-api-v2": "^1.16.0",
"yargs": "^17.7.2"
@@ -86,6 +93,7 @@
"@testing-library/react": "14.0.0",
"@types/cookie-parser": "^1.4.6",
"@types/jest": "^29.4.0",
+ "@types/multer": "^1.4.11",
"@types/node": "18.16.9",
"@types/react": "18.2.33",
"@types/react-dom": "18.2.14",
@@ -4048,6 +4056,23 @@
"@nestjs/core": "^10.0.0"
}
},
+ "node_modules/@nestjs/platform-express/node_modules/multer": {
+ "version": "1.4.4-lts.1",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
+ "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.0.0",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.4",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/@nestjs/schedule": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.0.tgz",
@@ -4090,6 +4115,37 @@
"typescript": ">=4.8.2"
}
},
+ "node_modules/@nestjs/serve-static": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@nestjs/serve-static/-/serve-static-4.0.1.tgz",
+ "integrity": "sha512-AoOrVdAe+WmsceuCcA8nWmKUYmaOsg9pqBCbIj7PS4W3XdikJQMtfxgSIoOlyUksZdhTBFjHqKh0Yhpj6pulwQ==",
+ "dependencies": {
+ "path-to-regexp": "0.2.5"
+ },
+ "peerDependencies": {
+ "@fastify/static": "^6.5.0",
+ "@nestjs/common": "^9.0.0 || ^10.0.0",
+ "@nestjs/core": "^9.0.0 || ^10.0.0",
+ "express": "^4.18.1",
+ "fastify": "^4.7.0"
+ },
+ "peerDependenciesMeta": {
+ "@fastify/static": {
+ "optional": true
+ },
+ "express": {
+ "optional": true
+ },
+ "fastify": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@nestjs/serve-static/node_modules/path-to-regexp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz",
+ "integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q=="
+ },
"node_modules/@nestjs/testing": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.1.tgz",
@@ -5428,6 +5484,21 @@
"react": "^16.8 || ^17.0 || ^18.0"
}
},
+ "node_modules/@react-dnd/asap": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
+ "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
+ },
+ "node_modules/@react-dnd/invariant": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
+ "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
+ },
+ "node_modules/@react-dnd/shallowequal": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
+ "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
+ },
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
@@ -6869,6 +6940,15 @@
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
+ "node_modules/@types/multer": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz",
+ "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "18.16.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.9.tgz",
@@ -9556,6 +9636,11 @@
"validator": "^13.9.0"
}
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
+ },
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -11010,6 +11095,16 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
+ "node_modules/dnd-core": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
+ "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
+ "dependencies": {
+ "@react-dnd/asap": "^5.0.1",
+ "@react-dnd/invariant": "^4.0.1",
+ "redux": "^4.2.0"
+ }
+ },
"node_modules/dns-packet": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -12537,8 +12632,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.2.7",
@@ -17618,9 +17712,9 @@
}
},
"node_modules/multer": {
- "version": "1.4.4-lts.1",
- "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
- "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+ "version": "1.4.5-lts.1",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
+ "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
@@ -19741,6 +19835,43 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-dnd": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
+ "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
+ "dependencies": {
+ "@react-dnd/invariant": "^4.0.1",
+ "@react-dnd/shallowequal": "^4.0.1",
+ "dnd-core": "^16.0.1",
+ "fast-deep-equal": "^3.1.3",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "peerDependencies": {
+ "@types/hoist-non-react-statics": ">= 3.3.1",
+ "@types/node": ">= 12",
+ "@types/react": ">= 16",
+ "react": ">= 16.14"
+ },
+ "peerDependenciesMeta": {
+ "@types/hoist-non-react-statics": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-dnd-html5-backend": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
+ "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
+ "dependencies": {
+ "dnd-core": "^16.0.1"
+ }
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -19874,6 +20005,17 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-tag-autocomplete": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/react-tag-autocomplete/-/react-tag-autocomplete-7.2.0.tgz",
+ "integrity": "sha512-ahTJGOZJfwdQ0cWw7U27/KUqY40BEadAClp62vuHxuNMSCV3bGXcWas5PoTGISuBJyN3qRypTDrZSNJ5GY0Iug==",
+ "engines": {
+ "node": ">= 16.12.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
"node_modules/react-textarea-autosize": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz",
@@ -19890,6 +20032,19 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-tooltip": {
+ "version": "5.26.2",
+ "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.26.2.tgz",
+ "integrity": "sha512-C1qHiqWYn6l5c98kL/NKFyJSw5G11vUVJkgOPcKgn306c5iL5317LxMNn5Qg1GSSM7Qvtsd6KA5MvwfgxFF7Dg==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.6.1",
+ "classnames": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.14.0",
+ "react-dom": ">=16.14.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -19972,6 +20127,14 @@
"node": ">=4"
}
},
+ "node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
"node_modules/reflect-metadata": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
@@ -21908,6 +22071,18 @@
"url": "https://github.com/sponsors/limonte"
}
},
+ "node_modules/swr": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
+ "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
+ "dependencies": {
+ "client-only": "^0.0.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
diff --git a/package.json b/package.json
index cb56d008..69d5232f 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@nestjs/microservices": "^10.3.1",
"@nestjs/platform-express": "^10.0.2",
"@nestjs/schedule": "^4.0.0",
+ "@nestjs/serve-static": "^4.0.1",
"@novu/node": "^0.23.0",
"@novu/notification-center": "^0.23.0",
"@prisma/client": "^5.8.1",
@@ -46,14 +47,19 @@
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"md5": "^2.3.0",
+ "multer": "^1.4.5-lts.1",
"nestjs-command": "^3.1.4",
"next": "13.4.4",
"prisma-paginate": "^5.2.1",
"react": "18.2.0",
+ "react-dnd": "^16.0.1",
+ "react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"react-query": "^3.39.3",
"react-router-dom": "6.11.2",
+ "react-tag-autocomplete": "^7.2.0",
+ "react-tooltip": "^5.26.2",
"redis": "^4.6.12",
"reflect-metadata": "^0.1.13",
"remove-markdown": "^0.5.0",
@@ -61,6 +67,7 @@
"simple-statistics": "^7.8.3",
"stripe": "^14.14.0",
"sweetalert2": "^11.10.5",
+ "swr": "^2.2.5",
"tslib": "^2.3.0",
"twitter-api-v2": "^1.16.0",
"yargs": "^17.7.2"
@@ -86,6 +93,7 @@
"@testing-library/react": "14.0.0",
"@types/cookie-parser": "^1.4.6",
"@types/jest": "^29.4.0",
+ "@types/multer": "^1.4.11",
"@types/node": "18.16.9",
"@types/react": "18.2.33",
"@types/react-dom": "18.2.14",