feat: more logics
This commit is contained in:
parent
23e96e8e03
commit
4ebc751849
|
|
@ -16,7 +16,8 @@ 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";
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { CommentsController } from '@gitroom/backend/api/routes/comments.controller';
|
||||
|
||||
const authenticatedController = [
|
||||
UsersController,
|
||||
|
|
@ -25,6 +26,7 @@ const authenticatedController = [
|
|||
SettingsController,
|
||||
PostsController,
|
||||
MediaController,
|
||||
CommentsController,
|
||||
];
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -37,7 +39,7 @@ const authenticatedController = [
|
|||
serveRoot: '/' + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY,
|
||||
serveStaticOptions: {
|
||||
index: false,
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [StripeController, AuthController, ...authenticatedController],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import {Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common';
|
||||
import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { AddCommentDto } from '@gitroom/nestjs-libraries/dtos/comments/add.comment.dto';
|
||||
|
||||
@Controller('/comments')
|
||||
export class CommentsController {
|
||||
constructor(private _commentsService: CommentsService) {}
|
||||
|
||||
@Post('/')
|
||||
addComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() addCommentDto: AddCommentDto
|
||||
) {
|
||||
return this._commentsService.addAComment(
|
||||
org.id,
|
||||
user.id,
|
||||
addCommentDto.content,
|
||||
addCommentDto.date
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/:id')
|
||||
addCommentTocComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() addCommentDto: AddCommentDto,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._commentsService.addACommentToComment(
|
||||
org.id,
|
||||
user.id,
|
||||
id,
|
||||
addCommentDto.content,
|
||||
addCommentDto.date
|
||||
);
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
editComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() addCommentDto: AddCommentDto,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._commentsService.updateAComment(
|
||||
org.id,
|
||||
user.id,
|
||||
id,
|
||||
addCommentDto.content
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
deleteComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._commentsService.deleteAComment(
|
||||
org.id,
|
||||
user.id,
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/:date')
|
||||
loadAllCommentsAndSubCommentsForADate(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('date') date: string
|
||||
) {
|
||||
return this._commentsService.loadAllCommentsAndSubCommentsForADate(
|
||||
org.id,
|
||||
date
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,74 @@
|
|||
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";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
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';
|
||||
import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service';
|
||||
|
||||
@Controller('/posts')
|
||||
export class PostsController {
|
||||
constructor(
|
||||
private _postsService: PostsService
|
||||
) {
|
||||
}
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _commentsService: CommentsService
|
||||
) {}
|
||||
|
||||
@Get('/')
|
||||
getPosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsDto
|
||||
) {
|
||||
return this._postsService.getPosts(org.id, query);
|
||||
}
|
||||
@Get('/')
|
||||
async getPosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Query() query: GetPostsDto
|
||||
) {
|
||||
const [posts, comments] = await Promise.all([
|
||||
this._postsService.getPosts(org.id, query),
|
||||
this._commentsService.getAllCommentsByWeekYear(
|
||||
org.id,
|
||||
query.year,
|
||||
query.week
|
||||
),
|
||||
]);
|
||||
|
||||
@Get('/:id')
|
||||
getPost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return this._postsService.getPost(org.id, id);
|
||||
}
|
||||
return {
|
||||
posts,
|
||||
comments,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
createPost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: CreatePostDto
|
||||
) {
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
@Get('/:id')
|
||||
getPost(@GetOrgFromRequest() org: Organization, @Param('id') id: string) {
|
||||
return this._postsService.getPost(org.id, id);
|
||||
}
|
||||
|
||||
@Put('/:id/date')
|
||||
changeDate(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('date') date: string
|
||||
) {
|
||||
return this._postsService.changeDate(org.id, id, date);
|
||||
}
|
||||
@Post('/')
|
||||
createPost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: CreatePostDto
|
||||
) {
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
|
||||
@Delete('/:group')
|
||||
deletePost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('group') group: string
|
||||
) {
|
||||
return this._postsService.deletePost(org.id, group);
|
||||
}
|
||||
|
||||
@Put('/:id/date')
|
||||
changeDate(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('date') date: string
|
||||
) {
|
||||
return this._postsService.changeDate(org.id, id, date);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export class AuthMiddleware implements NestMiddleware {
|
|||
|
||||
delete user.password;
|
||||
const organization = await this._organizationService.getFirstOrgByUserId(user.id);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
req.user = user;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 942 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" style="enable-background:new 0 0 485.5 142;" xml:space="preserve" viewBox="3 4 138.5 134.02">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;fill:#080B13;}
|
||||
.st2{enable-background:new ;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<g class="st2">
|
||||
<g>
|
||||
<path class="st3" d="M141.5,33.7c-4-1.1-7.5-2.4-11.1-3.2c-6-1.3-9.4-4.8-10.7-10.7c-0.8-3.4-1.9-6.7-2.9-10.1 c0-0.2-0.2-0.3-0.3-0.7c-1.2,4.1-2.4,8-3.6,11.9c-0.3,1-0.7,2.1-1.1,3.1c-0.9,2.3-2.5,4.1-4.9,4.9c-4.3,1.4-8.7,2.7-13.1,4.1 c-0.6,0.2-1.3,0.4-2.3,0.7c3.7,1.1,7,2,10.4,3c1.5,0.4,3,0.9,4.4,1.4c2.9,1,4.8,2.9,5.8,5.8c1.5,4.7,2.9,9.4,4.4,14.2 c0.1-0.3,0.3-0.6,0.4-0.9c1.3-4.3,2.6-8.6,3.9-12.9c1-3.4,3.3-5.6,6.7-6.6C132,36.6,136.5,35.2,141.5,33.7z"/>
|
||||
<path class="st3" d="M98.1,4c-0.7,2.2-1.3,4-1.7,5.8c-0.6,2.3-2,3.6-4.2,4.1c-1.8,0.5-3.7,1.1-5.9,1.7c2.3,0.7,4.2,1.3,6.2,1.8 c2.1,0.6,3.3,1.8,3.9,3.9c0.5,1.9,1.1,3.7,1.8,5.9c0.6-2.1,1.2-3.7,1.6-5.4c0.6-2.4,2-4,4.5-4.6c1.8-0.4,3.5-1,5.7-1.6 c-1.2-0.4-2-0.6-2.8-0.9c-6.6-1.9-6.2-1.6-8.1-7.9C98.7,6.1,98.4,5.3,98.1,4z"/>
|
||||
</g>
|
||||
<g id="Vector_Smart_Object_xA0_Image_00000107568830299365483660000013795074849348738710_">
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="2.9587" y1="85.8536" x2="109.8734" y2="85.8536">
|
||||
<stop offset="0" style="stop-color:#713CE2"/>
|
||||
<stop offset="1" style="stop-color:#6730D9"/>
|
||||
</linearGradient>
|
||||
<path class="st4" d="M56.4,33.7C26.9,33.7,3,57.6,3,87.2c0,23,14.7,43.4,36.6,50.7c2.7,0.5,3.7-1.1,3.7-2.5 c0-0.8,0.1-8.4,0.1-15.5c0-4.4,0.8-7,2.8-8.6c-10.3-4.1-17.6-14.2-17.6-25.9c0-15.4,12.5-27.9,27.9-27.9 c15.4,0,27.9,12.5,27.9,27.9c0,11.8-7.3,21.8-17.6,25.9c1.4,1.3,3.3,3.3,3.1,9.4c-0.3,7.1-0.1,12.9-0.1,14.7c0,1.4,1,3.1,3.7,2.5 c21.8-7.3,36.4-27.8,36.4-50.7C109.9,57.6,86,33.7,56.4,33.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,5 +1,11 @@
|
|||
import {AnalyticsComponent} from "@gitroom/frontend/components/analytics/analytics.component";
|
||||
import {internalFetch} from "@gitroom/helpers/utils/internal.fetch";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Gitroom Analytics',
|
||||
description: '',
|
||||
}
|
||||
|
||||
export default async function Index() {
|
||||
const analytics = await (await internalFetch('/analytics')).json();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Error',
|
||||
description: '',
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div>We are experiencing some difficulty, try to refresh the page</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import {LaunchesComponent} from "@gitroom/frontend/components/launches/launches.component";
|
||||
import {internalFetch} from "@gitroom/helpers/utils/internal.fetch";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Gitroom Launches',
|
||||
description: '',
|
||||
}
|
||||
|
||||
export default async function Index() {
|
||||
const {integrations} = await (await internalFetch('/integrations/list')).json();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ import {SettingsComponent} from "@gitroom/frontend/components/settings/settings.
|
|||
import {internalFetch} from "@gitroom/helpers/utils/internal.fetch";
|
||||
import {redirect} from "next/navigation";
|
||||
import {RedirectType} from "next/dist/client/components/redirect";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Gitroom Settings',
|
||||
description: '',
|
||||
}
|
||||
export default async function Index({searchParams}: {searchParams: {code: string}}) {
|
||||
if (searchParams.code) {
|
||||
await internalFetch('/settings/github', {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,30 @@
|
|||
import '../global.css';
|
||||
import {ReactNode} from "react";
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default async function AuthLayout({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<div className="h-[100vh] w-[100vw] overflow-hidden bg-purple-400 flex justify-center items-center">
|
||||
<div className="absolute w-60 h-60 rounded-xl bg-purple-300 -top-5 -left-16 z-0 transform rotate-45 hidden md:block"></div>
|
||||
<div className="absolute w-48 h-48 rounded-xl bg-purple-300 -bottom-6 -right-10 transform rotate-12 hidden md:block"></div>
|
||||
<div className="py-12 px-12 bg-white rounded-2xl shadow-xl z-20">
|
||||
{children}
|
||||
export default async function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute left-0 top-0 z-[0] h-[100vh] w-[100vw] overflow-hidden bg-loginBg bg-cover bg-left-top" />
|
||||
<div className="relative z-[1] pr-[100px] flex justify-end items-center h-[100vh] w-[100vw] overflow-hidden">
|
||||
<div className="w-[557px] flex h-[614px] bg-loginBox bg-contain">
|
||||
<div className="p-[32px] absolute w-[557px] h-[614px] text-white">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex-1 flex justify-end">
|
||||
<div className="absolute top-0 bg-gradient-to-t from-[#354258] w-[1px] translate-x-[22px] h-full" />
|
||||
</div>
|
||||
<div className="w-40 h-40 absolute bg-purple-300 rounded-full top-0 right-12 hidden md:block"></div>
|
||||
<div className="w-20 h-40 absolute bg-purple-300 rounded-full bottom-20 left-10 transform rotate-45 hidden md:block"></div>
|
||||
<div>
|
||||
<div className="absolute right-0 bg-gradient-to-l from-[#354258] h-[1px] translate-y-[22px] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 bg-gradient-to-t from-[#354258] w-[1px] -translate-x-[22px] h-full" />
|
||||
<div className="absolute right-0 bg-gradient-to-l from-[#354258] h-[1px] -translate-y-[22px] w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,3 +252,29 @@ html {
|
|||
.react-tags__listbox-option-highlight {
|
||||
background-color: #ffdd00;
|
||||
}
|
||||
|
||||
#renderEditor:not(:has(:first-child)) {
|
||||
display: none;
|
||||
}
|
||||
.w-md-editor {
|
||||
background-color: #131B2C !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 8px !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.w-md-editor-toolbar {
|
||||
height: 40px !important;
|
||||
min-height: 40px !important;
|
||||
background-color: #131B2C !important;
|
||||
padding: 0 8px !important;
|
||||
border-color: #28344F !important;
|
||||
}
|
||||
|
||||
.wmde-markdown {
|
||||
background: transparent !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import './global.css';
|
||||
import 'react-tooltip/dist/react-tooltip.css'
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
|
||||
import LayoutContext from "@gitroom/frontend/components/layout/layout.context";
|
||||
import {ReactNode} from "react";
|
||||
import {Chakra_Petch} from "next/font/google";
|
||||
const chakra = Chakra_Petch({weight: '400', subsets: ['latin']})
|
||||
export default async function AppLayout({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<html>
|
||||
<body className={chakra.className}>
|
||||
<LayoutContext>
|
||||
{children}
|
||||
</LayoutContext>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
import LayoutContext from '@gitroom/frontend/components/layout/layout.context';
|
||||
import { ReactNode } from 'react';
|
||||
import { Chakra_Petch } from 'next/font/google';
|
||||
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
|
||||
export default async function AppLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.png" sizes="any" />
|
||||
</head>
|
||||
<body className={chakra.className}>
|
||||
<LayoutContext>{children}</LayoutContext>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ export function Login() {
|
|||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-center mb-4 cursor-pointer">Create An Account</h1>
|
||||
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">Create An Account</h1>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 text-black">
|
||||
<input {...register('email')} type="email" placeholder="Email Addres" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
|
||||
<input {...register('password')} autoComplete="off" type="password" placeholder="Password" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ export function Register() {
|
|||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-center mb-4 cursor-pointer">Create An Account</h1>
|
||||
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">Create An Account</h1>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 text-black">
|
||||
<input {...register('email')} type="email" placeholder="Email Addres" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
|
||||
<input {...register('password')} autoComplete="off" type="password" placeholder="Password" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
|
||||
<input {...register('company')} autoComplete="off" type="text" placeholder="Company" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
|
||||
|
|
|
|||
|
|
@ -3,198 +3,37 @@
|
|||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import Image from 'next/image';
|
||||
import clsx from 'clsx';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||
import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { ShowAllProviders } from '@gitroom/frontend/components/launches/providers/show.all.providers';
|
||||
import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
// @ts-ignore
|
||||
import useKeypress from 'react-use-keypress';
|
||||
import {
|
||||
getValues,
|
||||
resetValues,
|
||||
} from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import {
|
||||
useMoveToIntegration,
|
||||
useMoveToIntegrationListener,
|
||||
} from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
|
||||
import { useMoveToIntegration } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
|
||||
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
import { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { useExpend } from '@gitroom/frontend/components/launches/helpers/use.expend';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
|
||||
import { ProvidersOptions } from '@gitroom/frontend/components/launches/providers.options';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useSWRConfig } from 'swr';
|
||||
|
||||
export const PickPlatforms: FC<{
|
||||
integrations: Integrations[];
|
||||
selectedIntegrations: Integrations[];
|
||||
onChange: (integrations: Integrations[]) => void;
|
||||
singleSelect: boolean;
|
||||
}> = (props) => {
|
||||
const { integrations, selectedIntegrations, onChange } = props;
|
||||
const [selectedAccounts, setSelectedAccounts] =
|
||||
useState<Integrations[]>(selectedIntegrations);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
props.singleSelect &&
|
||||
selectedAccounts.length &&
|
||||
integrations.indexOf(selectedAccounts?.[0]) === -1
|
||||
) {
|
||||
addPlatform(integrations[0])();
|
||||
}
|
||||
}, [integrations, selectedAccounts]);
|
||||
|
||||
useMoveToIntegrationListener(props.singleSelect, (identifier) => {
|
||||
const findIntegration = integrations.find(
|
||||
(p) => p.identifier === identifier
|
||||
);
|
||||
if (findIntegration) {
|
||||
addPlatform(findIntegration)();
|
||||
}
|
||||
});
|
||||
|
||||
const addPlatform = useCallback(
|
||||
(integration: Integrations) => async () => {
|
||||
if (props.singleSelect) {
|
||||
onChange([integration]);
|
||||
setSelectedAccounts([integration]);
|
||||
return;
|
||||
}
|
||||
if (selectedAccounts.includes(integration)) {
|
||||
const changedIntegrations = selectedAccounts.filter(
|
||||
({ id }) => id !== integration.id
|
||||
);
|
||||
|
||||
if (
|
||||
!props.singleSelect &&
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to remove this platform?'
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onChange(changedIntegrations);
|
||||
setSelectedAccounts(changedIntegrations);
|
||||
} else {
|
||||
const changedIntegrations = [...selectedAccounts, integration];
|
||||
onChange(changedIntegrations);
|
||||
setSelectedAccounts(changedIntegrations);
|
||||
}
|
||||
},
|
||||
[selectedAccounts]
|
||||
);
|
||||
return (
|
||||
<div className="flex">
|
||||
{integrations.map((integration) =>
|
||||
!props.singleSelect ? (
|
||||
<div
|
||||
key={integration.id}
|
||||
className="flex gap-[8px] items-center mr-[10px]"
|
||||
>
|
||||
<div
|
||||
onClick={addPlatform(integration)}
|
||||
className={clsx(
|
||||
'cursor-pointer relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
selectedAccounts.findIndex((p) => p.id === integration.id) ===
|
||||
-1
|
||||
? 'grayscale opacity-65'
|
||||
: 'grayscale-0'
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div key={integration.id} className="flex w-full">
|
||||
<div
|
||||
onClick={addPlatform(integration)}
|
||||
className={clsx(
|
||||
'cursor-pointer flex-1 relative h-[40px] flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
selectedAccounts.findIndex((p) => p.id === integration.id) ===
|
||||
-1
|
||||
? 'bg-sixth'
|
||||
: 'bg-forth'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-[10px]">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
<div>{integration.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PreviewComponent: FC<{
|
||||
integrations: Integrations[];
|
||||
editorValue: Array<{ id?: string; content: string }>;
|
||||
}> = (props) => {
|
||||
const { integrations, editorValue } = props;
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useState([
|
||||
integrations[0],
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrations.indexOf(selectedIntegrations[0]) === -1) {
|
||||
setSelectedIntegrations([integrations[0]]);
|
||||
}
|
||||
}, [integrations, selectedIntegrations]);
|
||||
return (
|
||||
<div>
|
||||
<PickPlatforms
|
||||
integrations={integrations}
|
||||
selectedIntegrations={selectedIntegrations}
|
||||
onChange={setSelectedIntegrations}
|
||||
singleSelect={true}
|
||||
/>
|
||||
<IntegrationContext.Provider
|
||||
value={{ value: editorValue, integration: selectedIntegrations?.[0] }}
|
||||
>
|
||||
<ShowAllProviders
|
||||
value={editorValue}
|
||||
integrations={integrations}
|
||||
selectedProvider={selectedIntegrations?.[0]}
|
||||
/>
|
||||
</IntegrationContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const AddEditModal: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
integrations: Integrations[];
|
||||
}> = (props) => {
|
||||
const { date, integrations } = props;
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
// selected integrations to allow edit
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useState<
|
||||
|
|
@ -202,9 +41,13 @@ export const AddEditModal: FC<{
|
|||
>([]);
|
||||
|
||||
// value of each editor
|
||||
const [value, setValue] = useState<Array<{ content: string; id?: string }>>([
|
||||
{ content: '' },
|
||||
]);
|
||||
const [value, setValue] = useState<
|
||||
Array<{
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{ id: string; path: string }>;
|
||||
}>
|
||||
>([{ content: '' }]);
|
||||
|
||||
const fetch = useFetch();
|
||||
|
||||
|
|
@ -217,12 +60,16 @@ export const AddEditModal: FC<{
|
|||
// hook to test if the top editor should be hidden
|
||||
const showHide = useHideTopEditor();
|
||||
|
||||
const [showError, setShowError] = useState(false);
|
||||
|
||||
// hook to open a new modal
|
||||
const modal = useModals();
|
||||
|
||||
// are we in edit mode?
|
||||
const existingData = useExistingData();
|
||||
|
||||
const expend = useExpend();
|
||||
|
||||
// if it's edit just set the current integration
|
||||
useEffect(() => {
|
||||
if (existingData.integration) {
|
||||
|
|
@ -250,6 +97,19 @@ export const AddEditModal: FC<{
|
|||
[value]
|
||||
);
|
||||
|
||||
const changeImage = useCallback(
|
||||
(index: number) =>
|
||||
(newValue: {
|
||||
target: { name: string; value?: Array<{ id: string; path: string }> };
|
||||
}) => {
|
||||
return setValue((prev) => {
|
||||
prev[index].image = newValue.target.value;
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
// Add another editor
|
||||
const addValue = useCallback(
|
||||
(index: number) => () => {
|
||||
|
|
@ -261,6 +121,25 @@ export const AddEditModal: FC<{
|
|||
[value]
|
||||
);
|
||||
|
||||
// Delete post
|
||||
const deletePost = useCallback(
|
||||
(index: number) => async () => {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to delete this post?',
|
||||
'Yes, delete it!'
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setValue((prev) => {
|
||||
prev.splice(index, 1);
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
// override the close modal to ask the user if he is sure to close
|
||||
const askClose = useCallback(async () => {
|
||||
if (
|
||||
|
|
@ -273,95 +152,257 @@ export const AddEditModal: FC<{
|
|||
}
|
||||
}, []);
|
||||
|
||||
// function to send to the server and save
|
||||
const schedule = useCallback(async () => {
|
||||
const values = getValues();
|
||||
const allKeys = Object.keys(values).map((v) => ({
|
||||
integration: integrations.find((p) => p.id === v),
|
||||
value: values[v].posts,
|
||||
valid: values[v].isValid,
|
||||
settings: values[v].settings(),
|
||||
}));
|
||||
// sometimes it's easier to click escape to close
|
||||
useKeypress('Escape', askClose);
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (!key.valid) {
|
||||
moveToIntegration(key?.integration?.identifier!);
|
||||
// function to send to the server and save
|
||||
const schedule = useCallback(
|
||||
(type: 'draft' | 'now' | 'schedule' | 'delete') => async () => {
|
||||
if (type === 'delete') {
|
||||
if (!await deleteDialog('Are you sure you want to delete this post?', 'Yes, delete it!')) {
|
||||
return ;
|
||||
}
|
||||
await fetch(`/posts/${existingData.group}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
mutate('/posts');
|
||||
modal.closeAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetch('/posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
date: date.utc().format('YYYY-MM-DDTHH:mm:ss'),
|
||||
posts: allKeys,
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
const values = getValues();
|
||||
const allKeys = Object.keys(values).map((v) => ({
|
||||
integration: integrations.find((p) => p.id === v),
|
||||
value: values[v].posts,
|
||||
valid: values[v].isValid,
|
||||
group: existingData?.group,
|
||||
trigger: values[v].trigger,
|
||||
settings: values[v].settings(),
|
||||
}));
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (key.value.some((p) => !p.content || p.content.length < 6)) {
|
||||
setShowError(true);
|
||||
return ;
|
||||
}
|
||||
|
||||
if (!key.valid) {
|
||||
await key.trigger();
|
||||
moveToIntegration(key?.integration?.id!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetch('/posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
date: date.utc().format('YYYY-MM-DDTHH:mm:ss'),
|
||||
posts: allKeys,
|
||||
}),
|
||||
});
|
||||
|
||||
existingData.group = uuidv4();
|
||||
|
||||
mutate('/posts');
|
||||
modal.closeAll();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={askClose}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
<div className="flex gap-[20px] bg-black">
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
|
||||
!expend.expend
|
||||
? 'flex-1 w-1 animate-overflow'
|
||||
: 'w-0 overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
{!existingData.integration && (
|
||||
<PickPlatforms
|
||||
integrations={integrations}
|
||||
selectedIntegrations={[]}
|
||||
singleSelect={false}
|
||||
onChange={setSelectedIntegrations}
|
||||
/>
|
||||
)}
|
||||
{!existingData.integration && !showHide.hideTopEditor ? (
|
||||
<>
|
||||
{value.map((p, index) => (
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-[#172034] bg-[#0B101B] p-[16px] pt-0">
|
||||
<TopTitle
|
||||
title={existingData?.group ? 'Edit Post' : 'Create Post'}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={askClose}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{!existingData.integration && (
|
||||
<PickPlatforms
|
||||
integrations={integrations}
|
||||
selectedIntegrations={[]}
|
||||
singleSelect={false}
|
||||
onChange={setSelectedIntegrations}
|
||||
/>
|
||||
)}
|
||||
<div id="renderEditor" />
|
||||
{!existingData.integration && !showHide.hideTopEditor ? (
|
||||
<>
|
||||
<MDEditor
|
||||
key={`edit_${index}`}
|
||||
height={value.length > 1 ? 150 : 500}
|
||||
value={p.content}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
<div>
|
||||
<Button onClick={addValue(index)}>Add post</Button>
|
||||
</div>
|
||||
<div>You are in global editing mode</div>
|
||||
{value.map((p, index) => (
|
||||
<>
|
||||
<div>
|
||||
<MDEditor
|
||||
key={`edit_${index}`}
|
||||
height={value.length > 1 ? 150 : 250}
|
||||
commands={[
|
||||
...commands
|
||||
.getCommands()
|
||||
.filter((f) => f.name !== 'image'),
|
||||
newImage,
|
||||
]}
|
||||
value={p.content}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
{showError && (!p.content || p.content.length < 6) && (
|
||||
<div className="my-[5px] text-[#F97066] text-[12px] font-[500]">
|
||||
The post should be at least 6 characters long
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<MultiMediaComponent
|
||||
label="Attachments"
|
||||
description=""
|
||||
value={p.image}
|
||||
name="image"
|
||||
onChange={changeImage(index)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex bg-[#121b2c] rounded-br-[8px] text-[#F97066]">
|
||||
{value.length > 1 && (
|
||||
<div
|
||||
className="flex cursor-pointer gap-[4px] justify-center items-center flex-1"
|
||||
onClick={deletePost(index)}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
|
||||
fill="#F97066"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] pr-[10px]">
|
||||
Delete Post
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={addValue(index)}
|
||||
className="!h-[24px] rounded-[3px] flex gap-[4px] w-[102px] text-[12px] font-[500]"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 1.3125C5.87512 1.3125 4.7755 1.64607 3.8402 2.27102C2.90489 2.89597 2.17591 3.78423 1.74544 4.82349C1.31496 5.86274 1.20233 7.00631 1.42179 8.10958C1.64124 9.21284 2.18292 10.2263 2.97833 11.0217C3.77374 11.8171 4.78716 12.3588 5.89043 12.5782C6.99369 12.7977 8.13726 12.685 9.17651 12.2546C10.2158 11.8241 11.104 11.0951 11.729 10.1598C12.3539 9.2245 12.6875 8.12488 12.6875 7C12.6859 5.49207 12.0862 4.04636 11.0199 2.98009C9.95365 1.91382 8.50793 1.31409 7 1.3125ZM7 11.8125C6.04818 11.8125 5.11773 11.5303 4.32632 11.0014C3.53491 10.4726 2.91808 9.72103 2.55383 8.84166C2.18959 7.96229 2.09428 6.99466 2.27997 6.06113C2.46566 5.12759 2.92401 4.27009 3.59705 3.59705C4.27009 2.92401 5.1276 2.46566 6.06113 2.27997C6.99466 2.09428 7.9623 2.18958 8.84167 2.55383C9.72104 2.91808 10.4726 3.53491 11.0015 4.32632C11.5303 5.11773 11.8125 6.04818 11.8125 7C11.8111 8.27591 11.3036 9.49915 10.4014 10.4014C9.49915 11.3036 8.27591 11.8111 7 11.8125ZM9.625 7C9.625 7.11603 9.57891 7.22731 9.49686 7.30936C9.41481 7.39141 9.30353 7.4375 9.1875 7.4375H7.4375V9.1875C7.4375 9.30353 7.39141 9.41481 7.30936 9.49686C7.22731 9.57891 7.11603 9.625 7 9.625C6.88397 9.625 6.77269 9.57891 6.69064 9.49686C6.6086 9.41481 6.5625 9.30353 6.5625 9.1875V7.4375H4.8125C4.69647 7.4375 4.58519 7.39141 4.50314 7.30936C4.4211 7.22731 4.375 7.11603 4.375 7C4.375 6.88397 4.4211 6.77269 4.50314 6.69064C4.58519 6.60859 4.69647 6.5625 4.8125 6.5625H6.5625V4.8125C6.5625 4.69647 6.6086 4.58519 6.69064 4.50314C6.77269 4.42109 6.88397 4.375 7 4.375C7.11603 4.375 7.22731 4.42109 7.30936 4.50314C7.39141 4.58519 7.4375 4.69647 7.4375 4.8125V6.5625H9.1875C9.30353 6.5625 9.41481 6.60859 9.49686 6.69064C9.57891 6.77269 9.625 6.88397 9.625 7Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Add post</div>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
!existingData.integration && (
|
||||
<div className="h-[100px] flex justify-center items-center bg-sixth border-tableBorder border-2">
|
||||
Global Editor Hidden
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative h-[68px] flex flex-col rounded-[4px] border border-[#172034] bg-[#0B101B]">
|
||||
<div className="flex flex-1 gap-[10px] relative">
|
||||
<div className="absolute w-full h-full flex gap-[10px] justify-end items-center right-[16px] overflow-hidden">
|
||||
{!!existingData.integration && (
|
||||
<Button
|
||||
onClick={schedule('delete')}
|
||||
className="rounded-[4px] border-2 border-red-400 text-red-400"
|
||||
secondary={true}
|
||||
>
|
||||
Delete Post
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={schedule('draft')}
|
||||
className="rounded-[4px] border-2 border-[#506490]"
|
||||
secondary={true}
|
||||
disabled={selectedIntegrations.length === 0}
|
||||
>
|
||||
Save as draft
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={schedule('schedule')}
|
||||
className="rounded-[4px]"
|
||||
disabled={selectedIntegrations.length === 0}
|
||||
>
|
||||
{!existingData.integration ? 'Add to calendar' : 'Update'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{!!selectedIntegrations.length && (
|
||||
<PreviewComponent
|
||||
integrations={selectedIntegrations}
|
||||
editorValue={value}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={schedule}>Schedule</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex gap-[20px] flex-col rounded-[4px] border-[#172034] bg-[#0B101B] flex-1 transition-all duration-700',
|
||||
!selectedIntegrations.length
|
||||
? 'flex-grow-0 overflow-hidden'
|
||||
: 'flex-grow-1 border animate-overflow'
|
||||
)}
|
||||
>
|
||||
<div className="mx-[16px]">
|
||||
<TopTitle
|
||||
title=""
|
||||
expend={expend.show}
|
||||
collapse={expend.hide}
|
||||
shouldExpend={expend.expend}
|
||||
/>
|
||||
</div>
|
||||
{!!selectedIntegrations.length && (
|
||||
<div className="flex-1 flex flex-col p-[16px] pt-0">
|
||||
<ProvidersOptions
|
||||
integrations={selectedIntegrations}
|
||||
editorValue={value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,109 +1,142 @@
|
|||
"use client";
|
||||
'use client';
|
||||
|
||||
import {useModals} from "@mantine/modals";
|
||||
import {FC, useCallback} from "react";
|
||||
import {useFetch} from "@gitroom/helpers/utils/custom.fetch";
|
||||
import {Input} from "@gitroom/react/form/input";
|
||||
import {FieldValues, FormProvider, useForm} from "react-hook-form";
|
||||
import {Button} from "@gitroom/react/form/button";
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import {ApiKeyDto} from "@gitroom/nestjs-libraries/dtos/integrations/api.key.dto";
|
||||
import {useRouter} from "next/navigation";
|
||||
import { ApiKeyDto } from '@gitroom/nestjs-libraries/dtos/integrations/api.key.dto';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const resolver = classValidatorResolver(ApiKeyDto);
|
||||
|
||||
export const AddProviderButton = () => {
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
const openModal = useCallback(async () => {
|
||||
const data = await (await fetch('/integrations')).json();
|
||||
modal.openModal({
|
||||
title: 'Add Channel',
|
||||
children: <AddProviderComponent {...data} />
|
||||
})
|
||||
}, []);
|
||||
return (
|
||||
<button
|
||||
className="text-white p-[8px] rounded-md bg-forth"
|
||||
onClick={openModal}
|
||||
>
|
||||
Add Channel
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export const ApiModal: FC<{identifier: string, name: string}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const modal = useModals();
|
||||
const methods = useForm({
|
||||
mode: 'onChange',
|
||||
resolver
|
||||
export const useAddProvider = () => {
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
return useCallback(async () => {
|
||||
const data = await (await fetch('/integrations')).json();
|
||||
modal.openModal({
|
||||
title: 'Add Channel',
|
||||
children: <AddProviderComponent {...data} />,
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const submit = useCallback(async (data: FieldValues) => {
|
||||
const add = await fetch(`/integrations/article/${props.identifier}/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({api: data.api})
|
||||
});
|
||||
export const AddProviderButton = () => {
|
||||
const add = useAddProvider();
|
||||
return (
|
||||
<button
|
||||
className="text-white p-[8px] rounded-md bg-forth"
|
||||
onClick={add}
|
||||
>
|
||||
Add Channel
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
if (add.ok) {
|
||||
modal.closeAll();
|
||||
router.refresh();
|
||||
return ;
|
||||
}
|
||||
export const ApiModal: FC<{ identifier: string; name: string }> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const modal = useModals();
|
||||
const methods = useForm({
|
||||
mode: 'onChange',
|
||||
resolver,
|
||||
});
|
||||
|
||||
methods.setError('api', {
|
||||
message: 'Invalid API key'
|
||||
});
|
||||
}, []);
|
||||
const submit = useCallback(async (data: FieldValues) => {
|
||||
const add = await fetch(
|
||||
`/integrations/article/${props.identifier}/connect`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ api: data.api }),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className="gap-[8px] flex flex-col" onSubmit={methods.handleSubmit(submit)}>
|
||||
<div><Input label="API Key" name="api"/></div>
|
||||
<div><Button type="submit">Add platform</Button></div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
export const AddProviderComponent: FC<{social: Array<{identifier: string, name: string}>, article: Array<{identifier: string, name: string}>}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const modal = useModals();
|
||||
const {social, article} = props;
|
||||
const getSocialLink = useCallback((identifier: string) => async () => {
|
||||
const {url} = await (await fetch('/integrations/social/' + identifier)).json();
|
||||
window.location.href = url;
|
||||
}, []);
|
||||
if (add.ok) {
|
||||
modal.closeAll();
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const showApiButton = useCallback((identifier: string, name: string) => async () => {
|
||||
modal.openModal({
|
||||
title: `Add ${name}`,
|
||||
children: <ApiModal name={name} identifier={identifier} />
|
||||
})
|
||||
}, []);
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col">
|
||||
<h2>Social</h2>
|
||||
<div className="flex flex-wrap gap-[10px]">
|
||||
{social.map((item) => (
|
||||
<div key={item.identifier} onClick={getSocialLink(item.identifier)} className="w-[100px] h-[100px] bg-forth text-white justify-center items-center flex">
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h2>Articles</h2>
|
||||
<div className="flex flex-wrap gap-[10px]">
|
||||
{article.map((item) => (
|
||||
<div key={item.identifier} onClick={showApiButton(item.identifier, item.name)} className="w-[100px] h-[100px] bg-forth text-white justify-center items-center flex">
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
methods.setError('api', {
|
||||
message: 'Invalid API key',
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className="gap-[8px] flex flex-col"
|
||||
onSubmit={methods.handleSubmit(submit)}
|
||||
>
|
||||
<div>
|
||||
<Input label="API Key" name="api" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<Button type="submit">Add platform</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
export const AddProviderComponent: FC<{
|
||||
social: Array<{ identifier: string; name: string }>;
|
||||
article: Array<{ identifier: string; name: string }>;
|
||||
}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const modal = useModals();
|
||||
const { social, article } = props;
|
||||
const getSocialLink = useCallback(
|
||||
(identifier: string) => async () => {
|
||||
const { url } = await (
|
||||
await fetch('/integrations/social/' + identifier)
|
||||
).json();
|
||||
window.location.href = url;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const showApiButton = useCallback(
|
||||
(identifier: string, name: string) => async () => {
|
||||
modal.openModal({
|
||||
title: `Add ${name}`,
|
||||
children: <ApiModal name={name} identifier={identifier} />,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col">
|
||||
<h2>Social</h2>
|
||||
<div className="flex flex-wrap gap-[10px]">
|
||||
{social.map((item) => (
|
||||
<div
|
||||
key={item.identifier}
|
||||
onClick={getSocialLink(item.identifier)}
|
||||
className="w-[100px] h-[100px] bg-forth text-white justify-center items-center flex"
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h2>Articles</h2>
|
||||
<div className="flex flex-wrap gap-[10px]">
|
||||
{article.map((item) => (
|
||||
<div
|
||||
key={item.identifier}
|
||||
onClick={showApiButton(item.identifier, item.name)}
|
||||
className="w-[100px] h-[100px] bg-forth text-white justify-center items-center flex"
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
'use client';
|
||||
import "reflect-metadata";
|
||||
import 'reflect-metadata';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
import {createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import useSWR from "swr";
|
||||
import {useFetch} from "@gitroom/helpers/utils/custom.fetch";
|
||||
import {Post, Integration} from '@prisma/client';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Post, Integration } from '@prisma/client';
|
||||
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
|
|
@ -16,9 +25,11 @@ dayjs.extend(utc);
|
|||
|
||||
const CalendarContext = createContext({
|
||||
currentWeek: dayjs().week(),
|
||||
currentYear: dayjs().year(),
|
||||
comments: [] as Array<{ date: string; total: number }>,
|
||||
integrations: [] as Integrations[],
|
||||
posts: [] as Array<Post & {integration: Integration}>,
|
||||
setFilters: (filters: { currentWeek: number, currentYear: number }) => {},
|
||||
posts: [] as Array<Post & { integration: Integration }>,
|
||||
setFilters: (filters: { currentWeek: number; currentYear: number }) => {},
|
||||
changeDate: (id: string, date: dayjs.Dayjs) => {},
|
||||
});
|
||||
|
||||
|
|
@ -29,51 +40,82 @@ export interface Integrations {
|
|||
type: string;
|
||||
picture: string;
|
||||
}
|
||||
export const CalendarWeekProvider: FC<{ children: ReactNode, integrations: Integrations[] }> = ({
|
||||
children,
|
||||
integrations
|
||||
}) => {
|
||||
export const CalendarWeekProvider: FC<{
|
||||
children: ReactNode;
|
||||
integrations: Integrations[];
|
||||
}> = ({ children, integrations }) => {
|
||||
const fetch = useFetch();
|
||||
const [internalData, setInternalData] = useState([] as any[]);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
currentWeek: dayjs().week(),
|
||||
currentYear: dayjs().year(),
|
||||
currentWeek: dayjs().week(),
|
||||
currentYear: dayjs().year(),
|
||||
});
|
||||
|
||||
const setFiltersWrapper = useCallback(
|
||||
(filters: { currentWeek: number; currentYear: number }) => {
|
||||
setFilters(filters);
|
||||
setTimeout(() => {
|
||||
mutate('/posts');
|
||||
});
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
|
||||
const params = useMemo(() => {
|
||||
return new URLSearchParams({
|
||||
week: filters.currentWeek.toString(),
|
||||
year: filters.currentYear.toString(),
|
||||
week: filters.currentWeek.toString(),
|
||||
year: filters.currentYear.toString(),
|
||||
}).toString();
|
||||
}, [filters]);
|
||||
|
||||
const loadData = useCallback(async(url: string) => {
|
||||
return (await fetch(url)).json();
|
||||
}, [filters]);
|
||||
const loadData = useCallback(
|
||||
async (url: string) => {
|
||||
return (await fetch(`${url}?${params}`)).json();
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
|
||||
const {data, isLoading} = useSWR(`/posts?${params}`, loadData);
|
||||
const swr = useSWR(`/posts`, loadData);
|
||||
const { isLoading } = swr;
|
||||
const { posts, comments } = swr?.data || { posts: [], comments: [] };
|
||||
|
||||
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]);
|
||||
console.log(comments);
|
||||
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;
|
||||
})
|
||||
);
|
||||
},
|
||||
[posts, internalData]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setInternalData(data);
|
||||
if (posts) {
|
||||
setInternalData(posts);
|
||||
}
|
||||
}, [data]);
|
||||
}, [posts]);
|
||||
|
||||
return (
|
||||
<CalendarContext.Provider value={{ ...filters, posts: isLoading ? [] : internalData, integrations, setFilters, changeDate }}>
|
||||
<CalendarContext.Provider
|
||||
value={{
|
||||
...filters,
|
||||
posts: isLoading ? [] : internalData,
|
||||
integrations,
|
||||
setFilters: setFiltersWrapper,
|
||||
changeDate,
|
||||
comments,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CalendarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCalendar = () => useContext(CalendarContext);
|
||||
export const useCalendar = () => useContext(CalendarContext);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
useCalendar,
|
||||
} from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import dayjs from 'dayjs';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { openModal, useModals } from '@mantine/modals';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
|
||||
import clsx from 'clsx';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
@ -14,6 +14,9 @@ import { ExistingDataContextProvider } from '@gitroom/frontend/components/launch
|
|||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
|
||||
import { Integration, Post } from '@prisma/client';
|
||||
import { useAddProvider } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { CommentComponent } from '@gitroom/frontend/components/launches/comments/comment.component';
|
||||
import { useSWRConfig } from 'swr';
|
||||
|
||||
const days = [
|
||||
'',
|
||||
|
|
@ -52,6 +55,235 @@ const hours = [
|
|||
'23:00',
|
||||
];
|
||||
|
||||
export const Calendar = () => {
|
||||
const { currentWeek, currentYear, comments } = useCalendar();
|
||||
|
||||
const firstDay = useMemo(() => {
|
||||
return dayjs().year(currentYear).isoWeek(currentWeek).isoWeekday(1);
|
||||
}, [currentYear, currentWeek]);
|
||||
|
||||
return (
|
||||
<DNDProvider>
|
||||
<div className="select-none">
|
||||
<div className="grid grid-cols-8 text-center border-tableBorder border-r">
|
||||
{days.map((day, index) => (
|
||||
<div
|
||||
className="border-tableBorder gap-[4px] border-l border-b h-[36px] border-t flex items-center justify-center bg-input text-[14px] sticky top-0 z-[100]"
|
||||
key={day}
|
||||
>
|
||||
<div>{day} </div>
|
||||
<div className="text-[12px]">
|
||||
{day && `(${firstDay.add(index - 1, 'day').format('DD/MM')})`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{hours.map((hour) =>
|
||||
days.map((day, index) => (
|
||||
<>
|
||||
{index === 0 ? (
|
||||
<div
|
||||
className="border-tableBorder border-l border-b h-[216px]"
|
||||
key={day + hour}
|
||||
>
|
||||
{['00', '10', '20', '30', '40', '50'].map((num) => (
|
||||
<div
|
||||
key={day + hour + num}
|
||||
className="h-[calc(216px/6)] text-[12px] flex justify-center items-center"
|
||||
>
|
||||
{hour.split(':')[0] + ':' + num}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="group relative border-tableBorder border-l border-b h-[216px] flex flex-col overflow-hidden"
|
||||
key={day + hour}
|
||||
>
|
||||
<CommentBox
|
||||
totalComments={
|
||||
comments.find(
|
||||
(p) =>
|
||||
dayjs
|
||||
.utc(p.date)
|
||||
.local()
|
||||
.format('YYYY-MM-DD HH:mm') ===
|
||||
dayjs()
|
||||
.isoWeekday(index + 1)
|
||||
.hour(+hour.split(':')[0] - 1)
|
||||
.minute(0)
|
||||
.format('YYYY-MM-DD HH:mm')
|
||||
)?.total || 0
|
||||
}
|
||||
date={dayjs()
|
||||
.isoWeekday(index + 1)
|
||||
.hour(+hour.split(':')[0] - 1)
|
||||
.minute(0)}
|
||||
/>
|
||||
{['00', '10', '20', '30', '40', '50'].map((num) => (
|
||||
<CalendarColumn
|
||||
key={day + hour + num + currentWeek + currentYear}
|
||||
day={index}
|
||||
hour={hour.split(':')[0] + ':' + num}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DNDProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const CalendarColumn: FC<{ day: number; hour: string }> = (props) => {
|
||||
const { day, hour } = props;
|
||||
const { currentWeek, currentYear, integrations, posts, changeDate } =
|
||||
useCalendar();
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
|
||||
const getDate = useMemo(() => {
|
||||
const date =
|
||||
dayjs()
|
||||
.year(currentYear)
|
||||
.isoWeek(currentWeek)
|
||||
.isoWeekday(day)
|
||||
.format('YYYY-MM-DD') +
|
||||
'T' +
|
||||
hour +
|
||||
':00';
|
||||
return dayjs(date);
|
||||
}, [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,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-white',
|
||||
},
|
||||
children: (
|
||||
<ExistingDataContextProvider value={data}>
|
||||
<AddEditModal
|
||||
integrations={integrations.filter(
|
||||
(f) => f.id === data.integration
|
||||
)}
|
||||
date={getDate}
|
||||
/>
|
||||
</ExistingDataContextProvider>
|
||||
),
|
||||
size: '80%',
|
||||
title: ``,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const addModal = useCallback(() => {
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-white',
|
||||
},
|
||||
children: <AddEditModal integrations={integrations} date={getDate} />,
|
||||
size: '80%',
|
||||
// title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addProvider = useAddProvider();
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<div className="absolute left-0 top-0 w-full h-full">
|
||||
<div
|
||||
ref={drop}
|
||||
className={clsx(
|
||||
'h-[calc(216px/6)] gap-[2.5px] text-[12px] pointer w-full overflow-hidden justify-center overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
isBeforeNow && 'bg-secondary',
|
||||
canDrop && 'bg-white/80'
|
||||
)}
|
||||
>
|
||||
{postList.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className={clsx(
|
||||
postList.length > 1 && 'w-[33px] basis-[28px]',
|
||||
'h-full text-white relative flex justify-center items-center flex-grow-0 flex-shrink-0'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex gap-[5px] items-center">
|
||||
<CalendarItem
|
||||
date={getDate}
|
||||
editPost={editPost(post.id)}
|
||||
post={post}
|
||||
integrations={integrations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!isBeforeNow && (
|
||||
<div
|
||||
className={clsx(
|
||||
!postList.length ? 'justify-center flex-1' : 'ml-[2px]',
|
||||
'flex items-center cursor-pointer gap-[2.5px]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={
|
||||
'Schedule for ' + getDate.format('DD/MM/YYYY HH:mm')
|
||||
}
|
||||
onClick={integrations.length ? addModal : addProvider}
|
||||
className={clsx(
|
||||
'w-[20px] h-[20px] bg-forth rounded-full flex justify-center items-center hover:bg-seventh'
|
||||
)}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CalendarItem: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
editPost: () => void;
|
||||
|
|
@ -98,187 +330,61 @@ const CalendarItem: FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const CalendarColumn: FC<{ day: number; hour: string }> = (props) => {
|
||||
const { day, hour } = props;
|
||||
const { currentWeek, integrations, posts, changeDate } = useCalendar();
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
export const CommentBox: FC<{ totalComments: number; date: dayjs.Dayjs }> = (
|
||||
props
|
||||
) => {
|
||||
const { totalComments, date } = props;
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const getDate = useMemo(() => {
|
||||
const date =
|
||||
dayjs().isoWeek(currentWeek).isoWeekday(day).format('YYYY-MM-DD') +
|
||||
'T' +
|
||||
hour +
|
||||
':00';
|
||||
return dayjs(date);
|
||||
}, [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: (
|
||||
<ExistingDataContextProvider value={data}>
|
||||
<AddEditModal
|
||||
integrations={integrations.filter(
|
||||
(f) => f.id === data.integration
|
||||
)}
|
||||
date={getDate}
|
||||
/>
|
||||
</ExistingDataContextProvider>
|
||||
),
|
||||
size: '80%',
|
||||
title: `Edit post for ${getDate.format('DD/MM/YYYY HH:mm')}`,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const addModal = useCallback(() => {
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
const openCommentsModal = useCallback(() => {
|
||||
openModal({
|
||||
children: <CommentComponent date={date} />,
|
||||
withCloseButton: false,
|
||||
children: <AddEditModal integrations={integrations} date={getDate} />,
|
||||
onClose() {
|
||||
mutate(`/posts`);
|
||||
},
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-white',
|
||||
},
|
||||
size: '80%',
|
||||
title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
|
||||
});
|
||||
}, []);
|
||||
}, [date]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<div className="absolute left-0 top-0 w-full h-full">
|
||||
<div className={totalComments === 0 ? 'transition-opacity opacity-0 group-hover:opacity-100' : ''}>
|
||||
<div
|
||||
onClick={openCommentsModal}
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content="Add / View comments"
|
||||
className={clsx(
|
||||
'group absolute right-0 bottom-0 w-[20px] h-[20px] z-[10] hover:opacity-95 cursor-pointer hover:right-[3px] hover:bottom-[3px] transition-all duration-300 ease-in-out',
|
||||
totalComments === 0 ? 'opacity-50' : 'opacity-95'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={drop}
|
||||
className={clsx(
|
||||
'h-[calc(216px/6)] text-[12px] pointer w-full overflow-hidden justify-center overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
|
||||
isBeforeNow && 'bg-secondary',
|
||||
canDrop && 'bg-white/80'
|
||||
'relative w-full h-full group-hover:opacity-100',
|
||||
totalComments === 0 && 'opacity-0'
|
||||
)}
|
||||
>
|
||||
{postList.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className={clsx(
|
||||
postList.length > 1 && 'w-[33px] basis-[28px]',
|
||||
'h-full text-white relative flex justify-center items-center flex-grow-0 flex-shrink-0'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex gap-[5px] items-center">
|
||||
<CalendarItem
|
||||
date={getDate}
|
||||
editPost={editPost(post.id)}
|
||||
post={post}
|
||||
integrations={integrations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!isBeforeNow && (
|
||||
<div
|
||||
className={clsx(
|
||||
!postList.length ? 'justify-center flex-1' : 'ml-[2px]',
|
||||
'flex items-center cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={
|
||||
'Schedule for ' + getDate.format('DD/MM/YYYY HH:mm')
|
||||
}
|
||||
onClick={addModal}
|
||||
className={clsx(
|
||||
'w-[20px] h-[20px] bg-forth rounded-full flex justify-center items-center hover:bg-seventh'
|
||||
)}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
{totalComments > 0 && (
|
||||
<div className="absolute right-0 bottom-[10px] w-[10px] h-[10px] text-[8px] bg-red-500 z-[20] rounded-full flex justify-center items-center text-white">
|
||||
{totalComments}
|
||||
</div>
|
||||
)}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
id="comment"
|
||||
>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M25.784 21.017A10.992 10.992 0 0 0 27 16c0-6.065-4.935-11-11-11S5 9.935 5 16s4.935 11 11 11c1.742 0 3.468-.419 5.018-1.215l4.74 1.185a.996.996 0 0 0 .949-.263 1 1 0 0 0 .263-.95l-1.186-4.74zm-2.033.11.874 3.498-3.498-.875a1.006 1.006 0 0 0-.731.098A8.99 8.99 0 0 1 16 25c-4.963 0-9-4.038-9-9s4.037-9 9-9 9 4.038 9 9a8.997 8.997 0 0 1-1.151 4.395.995.995 0 0 0-.098.732z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-0 w-[0] h-[0] shadow-yellow bg-[rgba(0,0,0,0)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Calendar = () => {
|
||||
return (
|
||||
<DNDProvider>
|
||||
<div>
|
||||
<div className="grid grid-cols-8 text-center border-tableBorder border-r">
|
||||
{days.map((day) => (
|
||||
<div
|
||||
className="border-tableBorder border-l border-b h-[36px] border-t flex items-center justify-center bg-input text-[14px] sticky top-0 z-[100]"
|
||||
key={day}
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
{hours.map((hour) =>
|
||||
days.map((day, index) => (
|
||||
<>
|
||||
{index === 0 ? (
|
||||
<div
|
||||
className="border-tableBorder border-l border-b h-[216px]"
|
||||
key={day + hour}
|
||||
>
|
||||
{['00', '10', '20', '30', '40', '50'].map((num) => (
|
||||
<div
|
||||
key={day + hour + num}
|
||||
className="h-[calc(216px/6)] text-[12px] flex justify-center items-center"
|
||||
>
|
||||
{hour.split(':')[0] + ':' + num}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="border-tableBorder border-l border-b h-[216px] flex flex-col"
|
||||
key={day + hour}
|
||||
>
|
||||
{['00', '10', '20', '30', '40', '50'].map((num) => (
|
||||
<CalendarColumn
|
||||
key={day + hour + num}
|
||||
day={index}
|
||||
hour={hour.split(':')[0] + ':' + num}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DNDProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,380 @@
|
|||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import clsx from 'clsx';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
|
||||
export const CommentBox: FC<{
|
||||
value?: string;
|
||||
type: 'textarea' | 'input';
|
||||
onChange: (comment: string) => void;
|
||||
}> = (props) => {
|
||||
const { value, onChange, type } = props;
|
||||
const Component = type === 'textarea' ? Textarea : Input;
|
||||
const [newComment, setNewComment] = useState(value || '');
|
||||
|
||||
const newCommentFunc = useCallback(
|
||||
(event: { target: { value: string } }) => {
|
||||
setNewComment(event.target.value);
|
||||
},
|
||||
[newComment]
|
||||
);
|
||||
|
||||
const changeIt = useCallback(() => {
|
||||
onChange(newComment);
|
||||
setNewComment('');
|
||||
}, [newComment]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex',
|
||||
type === 'textarea' ? 'flex-col' : 'flex-row flex items-end gap-[10px]'
|
||||
)}
|
||||
>
|
||||
<div className={clsx(type === 'input' && 'flex-1')}>
|
||||
<Component
|
||||
label={type === 'textarea' ? 'Add comment' : ''}
|
||||
placeholder={type === 'input' ? 'Add comment' : ''}
|
||||
name="comment"
|
||||
disableForm={true}
|
||||
value={newComment}
|
||||
onChange={newCommentFunc}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={newComment.length < 2}
|
||||
onClick={changeIt}
|
||||
className={clsx(type === 'input' && 'mb-[27px]')}
|
||||
>
|
||||
{value ? 'Update' : 'Add comment'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface Comments {
|
||||
id: string;
|
||||
content: string;
|
||||
user: { email: string; id: string };
|
||||
childrenComment: Comments[];
|
||||
}
|
||||
|
||||
export const EditableCommentComponent: FC<{
|
||||
comment: Comments;
|
||||
onEdit: (content: string) => void;
|
||||
onDelete: () => void;
|
||||
}> = (props) => {
|
||||
const { comment, onEdit, onDelete } = props;
|
||||
const [commentContent, setCommentContent] = useState(comment.content);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const user = useUser();
|
||||
|
||||
const updateComment = useCallback((commentValue: string) => {
|
||||
if (commentValue !== comment.content) {
|
||||
setCommentContent(commentValue);
|
||||
onEdit(commentValue);
|
||||
}
|
||||
setEditMode(false);
|
||||
}, []);
|
||||
|
||||
const deleteCommentFunction = useCallback(async () => {
|
||||
if (
|
||||
await deleteDialog(
|
||||
'Are you sure you want to delete this comment?',
|
||||
'Yes, Delete'
|
||||
)
|
||||
) {
|
||||
onDelete();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (editMode) {
|
||||
return (
|
||||
<CommentBox
|
||||
type="input"
|
||||
value={commentContent}
|
||||
onChange={updateComment}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-[5px]">
|
||||
<pre className="text-wrap">{commentContent}</pre>
|
||||
{user?.id === comment.user.id && (
|
||||
<>
|
||||
<svg
|
||||
onClick={() => setEditMode(!editMode)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M28.415 9.17119L22.8288 3.58619C22.643 3.40043 22.4225 3.25307 22.1799 3.15253C21.9372 3.05199 21.6771 3.00024 21.4144 3.00024C21.1517 3.00024 20.8916 3.05199 20.6489 3.15253C20.4062 3.25307 20.1857 3.40043 20 3.58619L4.58626 18.9999C4.39973 19.185 4.25185 19.4053 4.15121 19.648C4.05057 19.8907 3.99917 20.151 4.00001 20.4137V25.9999C4.00001 26.5304 4.21072 27.0391 4.5858 27.4142C4.96087 27.7892 5.46958 27.9999 6.00001 27.9999H27C27.2652 27.9999 27.5196 27.8946 27.7071 27.7071C27.8947 27.5195 28 27.2652 28 26.9999C28 26.7347 27.8947 26.4804 27.7071 26.2928C27.5196 26.1053 27.2652 25.9999 27 25.9999H14.415L28.415 11.9999C28.6008 11.8142 28.7481 11.5937 28.8487 11.351C28.9492 11.1084 29.001 10.8482 29.001 10.5856C29.001 10.3229 28.9492 10.0628 28.8487 9.82009C28.7481 9.57741 28.6008 9.35692 28.415 9.17119ZM11.5863 25.9999H6.00001V20.4137L17 9.41369L22.5863 14.9999L11.5863 25.9999ZM24 13.5862L18.415 7.99994L21.415 4.99994L27 10.5862L24 13.5862Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
onClick={deleteCommentFunction}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M27 6H22V5C22 4.20435 21.6839 3.44129 21.1213 2.87868C20.5587 2.31607 19.7956 2 19 2H13C12.2044 2 11.4413 2.31607 10.8787 2.87868C10.3161 3.44129 10 4.20435 10 5V6H5C4.73478 6 4.48043 6.10536 4.29289 6.29289C4.10536 6.48043 4 6.73478 4 7C4 7.26522 4.10536 7.51957 4.29289 7.70711C4.48043 7.89464 4.73478 8 5 8H6V26C6 26.5304 6.21071 27.0391 6.58579 27.4142C6.96086 27.7893 7.46957 28 8 28H24C24.5304 28 25.0391 27.7893 25.4142 27.4142C25.7893 27.0391 26 26.5304 26 26V8H27C27.2652 8 27.5196 7.89464 27.7071 7.70711C27.8946 7.51957 28 7.26522 28 7C28 6.73478 27.8946 6.48043 27.7071 6.29289C27.5196 6.10536 27.2652 6 27 6ZM12 5C12 4.73478 12.1054 4.48043 12.2929 4.29289C12.4804 4.10536 12.7348 4 13 4H19C19.2652 4 19.5196 4.10536 19.7071 4.29289C19.8946 4.48043 20 4.73478 20 5V6H12V5ZM24 26H8V8H24V26ZM14 13V21C14 21.2652 13.8946 21.5196 13.7071 21.7071C13.5196 21.8946 13.2652 22 13 22C12.7348 22 12.4804 21.8946 12.2929 21.7071C12.1054 21.5196 12 21.2652 12 21V13C12 12.7348 12.1054 12.4804 12.2929 12.2929C12.4804 12.1054 12.7348 12 13 12C13.2652 12 13.5196 12.1054 13.7071 12.2929C13.8946 12.4804 14 12.7348 14 13ZM20 13V21C20 21.2652 19.8946 21.5196 19.7071 21.7071C19.5196 21.8946 19.2652 22 19 22C18.7348 22 18.4804 21.8946 18.2929 21.7071C18.1054 21.5196 18 21.2652 18 21V13C18 12.7348 18.1054 12.4804 18.2929 12.2929C18.4804 12.1054 18.7348 12 19 12C19.2652 12 19.5196 12.1054 19.7071 12.2929C19.8946 12.4804 20 12.7348 20 13Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommentComponent: FC<{ date: dayjs.Dayjs }> = (props) => {
|
||||
const { date } = props;
|
||||
const { closeAll } = useModals();
|
||||
const [commentsList, setCommentsList] = useState<Comments[]>([]);
|
||||
const user = useUser();
|
||||
const fetch = useFetch();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const data = await (
|
||||
await fetch(`/comments/${date.utc().format('YYYY-MM-DDTHH:mm:00')}`)
|
||||
).json();
|
||||
|
||||
setCommentsList(data);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const editComment = useCallback(
|
||||
(comment: Comments) => async (content: string) => {
|
||||
fetch(`/comments/${comment.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
date: date.utc().format('YYYY-MM-DDTHH:mm:00'),
|
||||
}),
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const addComment = useCallback(
|
||||
async (content: string) => {
|
||||
const { id } = await (
|
||||
await fetch('/comments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
date: date.utc().format('YYYY-MM-DDTHH:mm:00'),
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
setCommentsList(list => ([
|
||||
{
|
||||
id,
|
||||
user: { email: user?.email!, id: user?.id! },
|
||||
content,
|
||||
childrenComment: [],
|
||||
},
|
||||
...list,
|
||||
]));
|
||||
},
|
||||
[commentsList, setCommentsList]
|
||||
);
|
||||
|
||||
const deleteComment = useCallback(
|
||||
(comment: Comments) => async () => {
|
||||
await fetch(`/comments/${comment.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
setCommentsList((list) => list.filter((item) => item.id !== comment.id));
|
||||
},
|
||||
[commentsList, setCommentsList]
|
||||
);
|
||||
|
||||
const deleteChildrenComment = useCallback(
|
||||
(parent: Comments, children: Comments) => async () => {
|
||||
await fetch(`/comments/${children.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
setCommentsList((list) =>
|
||||
list.map((item) => {
|
||||
if (item.id === parent.id) {
|
||||
return {
|
||||
...item,
|
||||
childrenComment: item.childrenComment.filter(
|
||||
(child) => child.id !== children.id
|
||||
),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
},
|
||||
[commentsList, setCommentsList]
|
||||
);
|
||||
|
||||
const addChildrenComment = useCallback(
|
||||
(comment: Comments) => async (content: string) => {
|
||||
const { id } = await (
|
||||
await fetch(`/comments/${comment.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
date: date.utc().format('YYYY-MM-DDTHH:mm:00'),
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
setCommentsList((list) =>
|
||||
list.map((item) => {
|
||||
if (item.id === comment.id) {
|
||||
return {
|
||||
...item,
|
||||
childrenComment: [
|
||||
...item.childrenComment,
|
||||
{
|
||||
id,
|
||||
user: { email: user?.email!, id: user?.id! },
|
||||
content,
|
||||
childrenComment: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
},
|
||||
[commentsList]
|
||||
);
|
||||
|
||||
const extractNameFromEmailAndCapitalize = useCallback((email: string) => {
|
||||
return (
|
||||
email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-[#172034] bg-[#0B101B] p-[16px] pt-0">
|
||||
<TopTitle title={`Comments for ${date.format('DD/MM/YYYY HH:mm')}`} />
|
||||
<button
|
||||
onClick={closeAll}
|
||||
className="outline-none absolute right-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<CommentBox type="textarea" onChange={addComment} />
|
||||
|
||||
<div>
|
||||
{commentsList.map((comment, index) => (
|
||||
<>
|
||||
<div
|
||||
key={`comment_${index}_${comment.content}`}
|
||||
className={clsx(
|
||||
`flex relative flex-col`,
|
||||
comment?.childrenComment?.length && 'gap-[10px]'
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-[8px]">
|
||||
<div className="w-[40px] flex flex-col items-center">
|
||||
<div className="rounded-full relative z-[2] text-blue-500 font-bold font-['Inter'] flex justify-center items-center w-[40px] h-[40px] bg-white border-tableBorder border">
|
||||
{comment.user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 w-[2px] h-[calc(100%-10px)] bg-[#2E3336] absolute top-[10px] z-[1]" />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[4px]">
|
||||
<div className="flex">
|
||||
<div className="h-[22px] text-[15px] font-[700]">
|
||||
{extractNameFromEmailAndCapitalize(comment.user.email)}
|
||||
</div>
|
||||
</div>
|
||||
<EditableCommentComponent
|
||||
onDelete={deleteComment(comment)}
|
||||
onEdit={editComment(comment)}
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
{comment?.childrenComment?.map((childComment, index2) => (
|
||||
<div
|
||||
key={`comment2_${index2}_${childComment.content}`}
|
||||
className={clsx(`flex gap-[8px] relative`)}
|
||||
>
|
||||
<div className="w-[40px] flex flex-col items-center">
|
||||
<div className="rounded-full relative z-[2] text-blue-500 font-bold font-['Inter'] flex justify-center items-center w-[40px] h-[40px] bg-white border-tableBorder border">
|
||||
{childComment.user.email[0].toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[4px]">
|
||||
<div className="flex">
|
||||
<div className="h-[22px] text-[15px] font-[700]">
|
||||
{extractNameFromEmailAndCapitalize(
|
||||
childComment.user.email
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EditableCommentComponent
|
||||
onDelete={deleteChildrenComment(comment, childComment)}
|
||||
onEdit={editComment(childComment)}
|
||||
comment={childComment}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="relative w-[40px] flex flex-col items-center">
|
||||
<div className="h-[30px] w-[2px] bg-[#2E3336] absolute top-0 z-[1]" />
|
||||
<div className="h-[2px] w-[21px] bg-[#2E3336] absolute top-[30px] right-0 z-[1]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CommentBox
|
||||
type="input"
|
||||
onChange={addChildrenComment(comment)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,61 @@
|
|||
"use client";
|
||||
import {useCalendar} from "@gitroom/frontend/components/launches/calendar.context";
|
||||
import dayjs from "dayjs";
|
||||
'use client';
|
||||
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import dayjs from 'dayjs';
|
||||
import {useCallback} from "react";
|
||||
|
||||
export const Filters = () => {
|
||||
const week = useCalendar();
|
||||
const betweenDates = dayjs().isoWeek(week.currentWeek).startOf('isoWeek').format('DD/MM/YYYY') + ' - ' + dayjs().isoWeek(week.currentWeek).endOf('isoWeek').format('DD/MM/YYYY');
|
||||
return <div className="text-white h-[50px]" onClick={() => week.setFilters({currentWeek: week.currentWeek + 1})}>Week {week.currentWeek} ({betweenDates})</div>;
|
||||
const betweenDates =
|
||||
dayjs().year(week.currentYear).isoWeek(week.currentWeek).startOf('isoWeek').format('DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs().year(week.currentYear).isoWeek(week.currentWeek).endOf('isoWeek').format('DD/MM/YYYY');
|
||||
|
||||
const nextWeek = useCallback(() => {
|
||||
week.setFilters({
|
||||
currentWeek: week.currentWeek === 52 ? 1 : week.currentWeek + 1,
|
||||
currentYear: week.currentWeek === 52 ? week.currentYear + 1 : week.currentYear,
|
||||
});
|
||||
}, [week.currentWeek, week.currentYear]);
|
||||
|
||||
const previousWeek = useCallback(() => {
|
||||
week.setFilters({
|
||||
currentWeek: week.currentWeek === 1 ? 52 : week.currentWeek - 1,
|
||||
currentYear: week.currentWeek === 1 ? week.currentYear - 1 : week.currentYear,
|
||||
});
|
||||
}, [week.currentWeek, week.currentYear]);
|
||||
|
||||
return (
|
||||
<div className="text-white h-[50px] flex gap-[8px] items-center select-none">
|
||||
<div onClick={previousWeek}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.1644 15.5866C13.3405 15.7628 13.4395 16.0016 13.4395 16.2507C13.4395 16.4998 13.3405 16.7387 13.1644 16.9148C12.9883 17.0909 12.7494 17.1898 12.5003 17.1898C12.2513 17.1898 12.0124 17.0909 11.8363 16.9148L5.58629 10.6648C5.49889 10.5777 5.42954 10.4742 5.38222 10.3602C5.3349 10.2463 5.31055 10.1241 5.31055 10.0007C5.31055 9.87732 5.3349 9.75515 5.38222 9.64119C5.42954 9.52724 5.49889 9.42375 5.58629 9.33665L11.8363 3.08665C12.0124 2.91053 12.2513 2.81158 12.5003 2.81158C12.7494 2.81158 12.9883 2.91053 13.1644 3.08665C13.3405 3.26277 13.4395 3.50164 13.4395 3.75071C13.4395 3.99978 13.3405 4.23865 13.1644 4.41477L7.57925 9.99993L13.1644 15.5866Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Week {week.currentWeek}</div>
|
||||
<div onClick={nextWeek}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M14.4137 10.6633L8.16374 16.9133C7.98761 17.0894 7.74874 17.1884 7.49967 17.1884C7.2506 17.1884 7.01173 17.0894 6.83561 16.9133C6.65949 16.7372 6.56055 16.4983 6.56055 16.2492C6.56055 16.0002 6.65949 15.7613 6.83561 15.5852L12.4223 10L6.83717 4.41331C6.74997 4.3261 6.68079 4.22257 6.6336 4.10863C6.5864 3.99469 6.56211 3.87257 6.56211 3.74925C6.56211 3.62592 6.5864 3.5038 6.6336 3.38986C6.68079 3.27592 6.74997 3.17239 6.83717 3.08518C6.92438 2.99798 7.02791 2.9288 7.14185 2.88161C7.25579 2.83441 7.37791 2.81012 7.50124 2.81012C7.62456 2.81012 7.74668 2.83441 7.86062 2.88161C7.97456 2.9288 8.07809 2.99798 8.1653 3.08518L14.4153 9.33518C14.5026 9.42238 14.5718 9.52596 14.619 9.63997C14.6662 9.75398 14.6904 9.87618 14.6903 9.99957C14.6901 10.123 14.6656 10.2451 14.6182 10.359C14.5707 10.4729 14.5012 10.5763 14.4137 10.6633Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>{betweenDates}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
executeCommand,
|
||||
ExecuteState,
|
||||
ICommand,
|
||||
selectWord,
|
||||
TextAreaTextApi,
|
||||
} from '@uiw/react-md-editor';
|
||||
import { showMediaBox } from '@gitroom/frontend/components/media/media.component';
|
||||
|
||||
export const newImage: ICommand = {
|
||||
name: 'image',
|
||||
keyCommand: 'image',
|
||||
shortcuts: 'ctrlcmd+k',
|
||||
prefix: '',
|
||||
buttonProps: {
|
||||
'aria-label': 'Add image (ctrl + k)',
|
||||
title: 'Add image (ctrl + k)',
|
||||
},
|
||||
icon: (
|
||||
<svg width="13" height="13" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
execute: (state: ExecuteState, api: TextAreaTextApi) => {
|
||||
let newSelectionRange = selectWord({
|
||||
text: state.text,
|
||||
selection: state.selection,
|
||||
prefix: state.command.prefix!,
|
||||
suffix: state.command.suffix,
|
||||
});
|
||||
|
||||
let state1 = api.setSelectionRange(newSelectionRange);
|
||||
|
||||
if (
|
||||
state1.selectedText.includes('http') ||
|
||||
state1.selectedText.includes('www')
|
||||
) {
|
||||
executeCommand({
|
||||
api,
|
||||
selectedText: state1.selectedText,
|
||||
selection: state.selection,
|
||||
prefix: state.command.prefix!,
|
||||
suffix: state.command.suffix,
|
||||
});
|
||||
|
||||
return ;
|
||||
}
|
||||
|
||||
newSelectionRange = selectWord({
|
||||
text: state.text,
|
||||
selection: state.selection,
|
||||
prefix: '![',
|
||||
suffix: ']()',
|
||||
});
|
||||
state1 = api.setSelectionRange(newSelectionRange);
|
||||
|
||||
showMediaBox((media) => {
|
||||
if (media) {
|
||||
if (state1.selectedText.length > 0) {
|
||||
executeCommand({
|
||||
api,
|
||||
selectedText: state1.selectedText,
|
||||
selection: state.selection,
|
||||
prefix: '`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
executeCommand({
|
||||
api,
|
||||
selectedText: state1.selectedText,
|
||||
selection: state.selection,
|
||||
prefix: '`,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { useMoveToIntegrationListener } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
|
||||
export const PickPlatforms: FC<{
|
||||
integrations: Integrations[];
|
||||
selectedIntegrations: Integrations[];
|
||||
onChange: (integrations: Integrations[]) => void;
|
||||
singleSelect: boolean;
|
||||
hide?: boolean;
|
||||
}> = (props) => {
|
||||
const { hide, integrations, selectedIntegrations, onChange } = props;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isLeft, setIsLeft] = useState(false);
|
||||
const [isRight, setIsRight] = useState(false);
|
||||
|
||||
const [selectedAccounts, setSelectedAccounts] =
|
||||
useState<Integrations[]>(selectedIntegrations);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
props.singleSelect &&
|
||||
selectedAccounts.length &&
|
||||
integrations.indexOf(selectedAccounts?.[0]) === -1
|
||||
) {
|
||||
addPlatform(integrations[0])();
|
||||
}
|
||||
}, [integrations, selectedAccounts]);
|
||||
|
||||
const checkLeftRight = (test = true) => {
|
||||
const scrollWidth = ref.current?.scrollWidth;
|
||||
const offsetWidth = +(ref.current?.offsetWidth || 0);
|
||||
const scrollOffset = ref.current?.scrollLeft || 0;
|
||||
|
||||
const totalScroll = scrollOffset + offsetWidth + 100;
|
||||
|
||||
setIsLeft(!!scrollOffset);
|
||||
setIsRight(!!scrollWidth && !!offsetWidth && scrollWidth > totalScroll);
|
||||
};
|
||||
|
||||
const changeOffset = useCallback(
|
||||
(offset: number) => () => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollLeft += offset;
|
||||
checkLeftRight();
|
||||
}
|
||||
},
|
||||
[selectedIntegrations, integrations, selectedAccounts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
checkLeftRight();
|
||||
}, [selectedIntegrations, integrations]);
|
||||
|
||||
useMoveToIntegrationListener([integrations], props.singleSelect, (identifier) => {
|
||||
const findIntegration = integrations.find(
|
||||
(p) => p.id === identifier
|
||||
);
|
||||
|
||||
if (findIntegration) {
|
||||
addPlatform(findIntegration)();
|
||||
}
|
||||
});
|
||||
|
||||
const addPlatform = useCallback(
|
||||
(integration: Integrations) => async () => {
|
||||
if (props.singleSelect) {
|
||||
onChange([integration]);
|
||||
setSelectedAccounts([integration]);
|
||||
return;
|
||||
}
|
||||
if (selectedAccounts.includes(integration)) {
|
||||
const changedIntegrations = selectedAccounts.filter(
|
||||
({ id }) => id !== integration.id
|
||||
);
|
||||
|
||||
if (
|
||||
!props.singleSelect &&
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to remove this platform?'
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onChange(changedIntegrations);
|
||||
setSelectedAccounts(changedIntegrations);
|
||||
} else {
|
||||
const changedIntegrations = [...selectedAccounts, integration];
|
||||
onChange(changedIntegrations);
|
||||
setSelectedAccounts(changedIntegrations);
|
||||
}
|
||||
},
|
||||
[selectedAccounts]
|
||||
);
|
||||
|
||||
if (hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('flex select-none', props.singleSelect && 'gap-[10px]')}>
|
||||
{props.singleSelect && (
|
||||
<div className="flex items-center">
|
||||
{isLeft && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
onClick={changeOffset(-200)}
|
||||
>
|
||||
<path
|
||||
d="M10.3541 12.6463C10.4006 12.6927 10.4374 12.7479 10.4626 12.8086C10.4877 12.8693 10.5007 12.9343 10.5007 13C10.5007 13.0657 10.4877 13.1308 10.4626 13.1915C10.4374 13.2522 10.4006 13.3073 10.3541 13.3538C10.3077 13.4002 10.2525 13.4371 10.1918 13.4622C10.1311 13.4874 10.0661 13.5003 10.0004 13.5003C9.9347 13.5003 9.86964 13.4874 9.80894 13.4622C9.74825 13.4371 9.6931 13.4002 9.64664 13.3538L4.64664 8.35378C4.60015 8.30735 4.56328 8.2522 4.53811 8.1915C4.51295 8.13081 4.5 8.06574 4.5 8.00003C4.5 7.93433 4.51295 7.86926 4.53811 7.80856C4.56328 7.74786 4.60015 7.69272 4.64664 7.64628L9.64664 2.64628C9.74046 2.55246 9.86771 2.49976 10.0004 2.49976C10.1331 2.49976 10.2603 2.55246 10.3541 2.64628C10.448 2.7401 10.5007 2.86735 10.5007 3.00003C10.5007 3.13272 10.448 3.25996 10.3541 3.35378L5.70727 8.00003L10.3541 12.6463Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 flex',
|
||||
props.singleSelect && 'relative h-[40px]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
props.singleSelect
|
||||
? 'absolute w-full h-[40px] flex flex-nowrap overflow-hidden transition-all'
|
||||
: 'flex-1 flex'
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="innerComponent">
|
||||
<div className="flex">
|
||||
{integrations.map((integration) =>
|
||||
!props.singleSelect ? (
|
||||
<div
|
||||
key={integration.id}
|
||||
className="flex gap-[8px] items-center mr-[10px]"
|
||||
>
|
||||
<div
|
||||
onClick={addPlatform(integration)}
|
||||
className={clsx(
|
||||
'cursor-pointer relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
selectedAccounts.findIndex(
|
||||
(p) => p.id === integration.id
|
||||
) === -1
|
||||
? 'opacity-40'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div key={integration.id} className="">
|
||||
<div
|
||||
onClick={addPlatform(integration)}
|
||||
className={clsx(
|
||||
'cursor-pointer rounded-[50px] w-[200px] relative h-[40px] flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
selectedAccounts.findIndex(
|
||||
(p) => p.id === integration.id
|
||||
) === -1
|
||||
? 'bg-third border border-third'
|
||||
: 'bg-[#291259] border border-[#5826C2]'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-[10px]">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={integration.picture}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -right-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={15}
|
||||
height={15}
|
||||
/>
|
||||
</div>
|
||||
<div>{integration.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.singleSelect && isRight && (
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
className={!isRight ? 'pointer-events-none invisible' : ''}
|
||||
onClick={changeOffset(200)}
|
||||
>
|
||||
<path
|
||||
d="M5.64586 12.6463C5.5994 12.6927 5.56255 12.7479 5.53741 12.8086C5.51227 12.8693 5.49933 12.9343 5.49933 13C5.49933 13.0657 5.51227 13.1308 5.53741 13.1915C5.56255 13.2522 5.5994 13.3073 5.64586 13.3538C5.69231 13.4002 5.74746 13.4371 5.80816 13.4622C5.86886 13.4874 5.93391 13.5003 5.99961 13.5003C6.0653 13.5003 6.13036 13.4874 6.19106 13.4622C6.25175 13.4371 6.3069 13.4002 6.35336 13.3538L11.3534 8.35378C11.3998 8.30735 11.4367 8.2522 11.4619 8.1915C11.487 8.13081 11.5 8.06574 11.5 8.00003C11.5 7.93433 11.487 7.86926 11.4619 7.80856C11.4367 7.74786 11.3998 7.69272 11.3534 7.64628L6.35336 2.64628C6.25954 2.55246 6.13229 2.49976 5.99961 2.49976C5.86692 2.49976 5.73968 2.55246 5.64586 2.64628C5.55204 2.7401 5.49933 2.86735 5.49933 3.00003C5.49933 3.13272 5.55204 3.25996 5.64586 3.35378L10.2927 8.00003L5.64586 12.6463Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import {FC} from "react";
|
||||
|
||||
export const TopTitle: FC<{
|
||||
title: string;
|
||||
shouldExpend?: boolean;
|
||||
expend?: () => void;
|
||||
collapse?: () => void;
|
||||
}> = (props) => {
|
||||
const { title, shouldExpend, expend, collapse } = props;
|
||||
|
||||
return (
|
||||
<div className="h-[57px] border-b flex items-center border-[#172034] px-[16px] -mx-[16px]">
|
||||
<div className="flex-1">{title}</div>
|
||||
{shouldExpend !== undefined && (
|
||||
<div className="cursor-pointer">
|
||||
{!shouldExpend ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
onClick={expend}
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M20.25 5V9.5C20.25 9.69891 20.171 9.88968 20.0303 10.0303C19.8897 10.171 19.6989 10.25 19.5 10.25C19.3011 10.25 19.1103 10.171 18.9697 10.0303C18.829 9.88968 18.75 9.69891 18.75 9.5V6.81031L14.0306 11.5306C13.8899 11.6714 13.699 11.7504 13.5 11.7504C13.301 11.7504 13.1101 11.6714 12.9694 11.5306C12.8286 11.3899 12.7496 11.199 12.7496 11C12.7496 10.801 12.8286 10.6101 12.9694 10.4694L17.6897 5.75H15C14.8011 5.75 14.6103 5.67098 14.4697 5.53033C14.329 5.38968 14.25 5.19891 14.25 5C14.25 4.80109 14.329 4.61032 14.4697 4.46967C14.6103 4.32902 14.8011 4.25 15 4.25H19.5C19.6989 4.25 19.8897 4.32902 20.0303 4.46967C20.171 4.61032 20.25 4.80109 20.25 5ZM9.96937 13.4694L5.25 18.1897V15.5C5.25 15.3011 5.17098 15.1103 5.03033 14.9697C4.88968 14.829 4.69891 14.75 4.5 14.75C4.30109 14.75 4.11032 14.829 3.96967 14.9697C3.82902 15.1103 3.75 15.3011 3.75 15.5V20C3.75 20.1989 3.82902 20.3897 3.96967 20.5303C4.11032 20.671 4.30109 20.75 4.5 20.75H9C9.19891 20.75 9.38968 20.671 9.53033 20.5303C9.67098 20.3897 9.75 20.1989 9.75 20C9.75 19.8011 9.67098 19.6103 9.53033 19.4697C9.38968 19.329 9.19891 19.25 9 19.25H6.31031L11.0306 14.5306C11.1714 14.3899 11.2504 14.199 11.2504 14C11.2504 13.801 11.1714 13.6101 11.0306 13.4694C10.8899 13.3286 10.699 13.2496 10.5 13.2496C10.301 13.2496 10.1101 13.3286 9.96937 13.4694Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
onClick={collapse}
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.5004 10.2499V6.49993C13.5004 6.30102 13.5794 6.11025 13.7201 5.9696C13.8607 5.82895 14.0515 5.74993 14.2504 5.74993C14.4493 5.74993 14.6401 5.82895 14.7807 5.9696C14.9214 6.11025 15.0004 6.30102 15.0004 6.49993V8.43962L18.9698 4.4693C19.1105 4.32857 19.3014 4.24951 19.5004 4.24951C19.6994 4.24951 19.8903 4.32857 20.031 4.4693C20.1718 4.61003 20.2508 4.80091 20.2508 4.99993C20.2508 5.19895 20.1718 5.38982 20.031 5.53055L16.0607 9.49993H18.0004C18.1993 9.49993 18.3901 9.57895 18.5307 9.7196C18.6714 9.86025 18.7504 10.051 18.7504 10.2499C18.7504 10.4488 18.6714 10.6396 18.5307 10.7803C18.3901 10.9209 18.1993 10.9999 18.0004 10.9999H14.2504C14.0515 10.9999 13.8607 10.9209 13.7201 10.7803C13.5794 10.6396 13.5004 10.4488 13.5004 10.2499ZM9.75042 13.9999H6.00042C5.8015 13.9999 5.61074 14.0789 5.47009 14.2196C5.32943 14.3603 5.25042 14.551 5.25042 14.7499C5.25042 14.9488 5.32943 15.1396 5.47009 15.2803C5.61074 15.4209 5.8015 15.4999 6.00042 15.4999H7.9401L3.96979 19.4693C3.82906 19.61 3.75 19.8009 3.75 19.9999C3.75 20.199 3.82906 20.3898 3.96979 20.5306C4.11052 20.6713 4.30139 20.7503 4.50042 20.7503C4.69944 20.7503 4.89031 20.6713 5.03104 20.5306L9.00042 16.5602V18.4999C9.00042 18.6988 9.07943 18.8896 9.22009 19.0303C9.36074 19.1709 9.5515 19.2499 9.75042 19.2499C9.94933 19.2499 10.1401 19.1709 10.2807 19.0303C10.4214 18.8896 10.5004 18.6988 10.5004 18.4999V14.7499C10.5004 14.551 10.4214 14.3603 10.2807 14.2196C10.1401 14.0789 9.94933 13.9999 9.75042 13.9999ZM16.0607 15.4999H18.0004C18.1993 15.4999 18.3901 15.4209 18.5307 15.2803C18.6714 15.1396 18.7504 14.9488 18.7504 14.7499C18.7504 14.551 18.6714 14.3603 18.5307 14.2196C18.3901 14.0789 18.1993 13.9999 18.0004 13.9999H14.2504C14.0515 13.9999 13.8607 14.0789 13.7201 14.2196C13.5794 14.3603 13.5004 14.551 13.5004 14.7499V18.4999C13.5004 18.6988 13.5794 18.8896 13.7201 19.0303C13.8607 19.1709 14.0515 19.2499 14.2504 19.2499C14.4493 19.2499 14.6401 19.1709 14.7807 19.0303C14.9214 18.8896 15.0004 18.6988 15.0004 18.4999V16.5602L18.9698 20.5306C19.0395 20.6002 19.1222 20.6555 19.2132 20.6932C19.3043 20.7309 19.4019 20.7503 19.5004 20.7503C19.599 20.7503 19.6965 20.7309 19.7876 20.6932C19.8786 20.6555 19.9614 20.6002 20.031 20.5306C20.1007 20.4609 20.156 20.3781 20.1937 20.2871C20.2314 20.1961 20.2508 20.0985 20.2508 19.9999C20.2508 19.9014 20.2314 19.8038 20.1937 19.7128C20.156 19.6217 20.1007 19.539 20.031 19.4693L16.0607 15.4999ZM9.75042 5.74993C9.5515 5.74993 9.36074 5.82895 9.22009 5.9696C9.07943 6.11025 9.00042 6.30102 9.00042 6.49993V8.43962L5.03104 4.4693C4.89031 4.32857 4.69944 4.24951 4.50042 4.24951C4.30139 4.24951 4.11052 4.32857 3.96979 4.4693C3.82906 4.61003 3.75 4.80091 3.75 4.99993C3.75 5.19895 3.82906 5.38982 3.96979 5.53055L7.9401 9.49993H6.00042C5.8015 9.49993 5.61074 9.57895 5.47009 9.7196C5.32943 9.86025 5.25042 10.051 5.25042 10.2499C5.25042 10.4488 5.32943 10.6396 5.47009 10.7803C5.61074 10.9209 5.8015 10.9999 6.00042 10.9999H9.75042C9.94933 10.9999 10.1401 10.9209 10.2807 10.7803C10.4214 10.6396 10.5004 10.4488 10.5004 10.2499V6.49993C10.5004 6.30102 10.4214 6.11025 10.2807 5.9696C10.1401 5.82895 9.94933 5.74993 9.75042 5.74993Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@ export const useCustomProviderFunction = () => {
|
|||
const { integration } = useIntegration();
|
||||
const fetch = useFetch();
|
||||
const get = useCallback(
|
||||
async (funcName: string, customData?: string) => {
|
||||
async (funcName: string, customData?: any) => {
|
||||
return (
|
||||
await fetch('/integrations/function', {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {Post} from "@prisma/client";
|
|||
|
||||
const ExistingDataContext = createContext({
|
||||
integration: '',
|
||||
group: undefined as undefined | string,
|
||||
posts: [] as Post[],
|
||||
settings: {} as any
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
"use client";
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
export const useExpend = () => {
|
||||
const [expend, setExpend] = useState(false);
|
||||
useEffect(() => {
|
||||
const hide = () => {
|
||||
setExpend(false);
|
||||
};
|
||||
const show = () => {
|
||||
setExpend(true);
|
||||
};
|
||||
emitter.on('hide', hide);
|
||||
emitter.on('show', show);
|
||||
return () => {
|
||||
emitter.off('hide', hide);
|
||||
emitter.off('show', show);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
expend,
|
||||
hide: () => {
|
||||
emitter.emit('hide');
|
||||
},
|
||||
show: () => {
|
||||
emitter.emit('show');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,42 @@
|
|||
import removeMd from "remove-markdown";
|
||||
import {useMemo} from "react";
|
||||
import removeMd from 'remove-markdown';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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.content;
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢');
|
||||
}
|
||||
if (params.removeMarkdown) {
|
||||
newText = removeMd(value.content);
|
||||
}
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n');
|
||||
}
|
||||
if (params.specialFunc) {
|
||||
newText = params.specialFunc(newText);
|
||||
}
|
||||
return {
|
||||
id: value.id,
|
||||
text: newText,
|
||||
count: params.removeMarkdown && params.saveBreaklines ? newText.replace(/\n/g, ' ').length : newText.length,
|
||||
}
|
||||
});
|
||||
}, [text]);
|
||||
}
|
||||
export const useFormatting = (
|
||||
text: Array<{
|
||||
content: string;
|
||||
image?: Array<{ id: string; path: string }>;
|
||||
id?: string;
|
||||
}>,
|
||||
params: {
|
||||
removeMarkdown?: boolean;
|
||||
saveBreaklines?: boolean;
|
||||
specialFunc?: (text: string) => string;
|
||||
}
|
||||
) => {
|
||||
return useMemo(() => {
|
||||
return text.map((value) => {
|
||||
let newText = value.content;
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢');
|
||||
}
|
||||
if (params.removeMarkdown) {
|
||||
newText = removeMd(value.content);
|
||||
}
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n');
|
||||
}
|
||||
if (params.specialFunc) {
|
||||
newText = params.specialFunc(newText);
|
||||
}
|
||||
return {
|
||||
id: value.id,
|
||||
text: newText,
|
||||
images: value.image,
|
||||
count:
|
||||
params.removeMarkdown && params.saveBreaklines
|
||||
? newText.replace(/\n/g, ' ').length
|
||||
: newText.length,
|
||||
};
|
||||
});
|
||||
}, [text]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: Array<{content: string, id?: string}>}>({integration: undefined, value: []});
|
||||
export const IntegrationContext = createContext<{integration: Integrations|undefined, value: Array<{content: string, id?: string, image?: Array<{path: string, id: string}>}>}>({integration: undefined, value: []});
|
||||
|
||||
export const useIntegration = () => useContext(IntegrationContext);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
export const useMoveToIntegration = () => {
|
||||
|
|
@ -11,6 +11,7 @@ export const useMoveToIntegration = () => {
|
|||
};
|
||||
|
||||
export const useMoveToIntegrationListener = (
|
||||
useEffectParams: any[],
|
||||
enabled: boolean,
|
||||
callback: (identifier: string) => void
|
||||
) => {
|
||||
|
|
@ -19,12 +20,13 @@ export const useMoveToIntegrationListener = (
|
|||
return;
|
||||
}
|
||||
return load();
|
||||
}, []);
|
||||
}, useEffectParams);
|
||||
|
||||
const load = useCallback(() => {
|
||||
emitter.off('moveToIntegration', callback);
|
||||
emitter.on('moveToIntegration', callback);
|
||||
return () => {
|
||||
emitter.off('moveToIntegration', callback);
|
||||
};
|
||||
}, []);
|
||||
}, useEffectParams);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {useForm, useFormContext} from 'react-hook-form';
|
|||
import {classValidatorResolver} from "@hookform/resolvers/class-validator";
|
||||
|
||||
const finalInformation = {} as {
|
||||
[key: string]: { posts: Array<{id?: string, content: string, media?: Array<string>}>; settings: () => object; isValid: boolean };
|
||||
[key: string]: { posts: Array<{id?: string, content: string, media?: Array<string>}>; settings: () => object; trigger: () => Promise<boolean>; isValid: boolean };
|
||||
};
|
||||
export const useValues = (initialValues: object, integration: string, identifier: string, value: Array<{id?: string, content: string, media?: Array<string>}>, dto: any) => {
|
||||
const resolver = useMemo(() => {
|
||||
|
|
@ -13,7 +13,8 @@ export const useValues = (initialValues: object, integration: string, identifier
|
|||
const form = useForm({
|
||||
resolver,
|
||||
values: initialValues,
|
||||
mode: 'onChange'
|
||||
mode: 'onChange',
|
||||
criteriaMode: 'all',
|
||||
});
|
||||
|
||||
const getValues = useMemo(() => {
|
||||
|
|
@ -24,6 +25,7 @@ export const useValues = (initialValues: object, integration: string, identifier
|
|||
finalInformation[integration].posts = value;
|
||||
finalInformation[integration].isValid = form.formState.isValid;
|
||||
finalInformation[integration].settings = getValues;
|
||||
finalInformation[integration].trigger = form.trigger;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ import { FC, useMemo } from 'react';
|
|||
import Image from 'next/image';
|
||||
import { orderBy } from 'lodash';
|
||||
import { Calendar } from '@gitroom/frontend/components/launches/calendar';
|
||||
import {CalendarWeekProvider, Integrations} from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import {
|
||||
CalendarWeekProvider,
|
||||
Integrations,
|
||||
} from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { Filters } from '@gitroom/frontend/components/launches/filters';
|
||||
|
||||
export const LaunchesComponent: FC<{
|
||||
integrations: Integrations[]
|
||||
integrations: Integrations[];
|
||||
}> = (props) => {
|
||||
const { integrations } = props;
|
||||
|
||||
|
|
@ -18,12 +21,14 @@ export const LaunchesComponent: FC<{
|
|||
return (
|
||||
<CalendarWeekProvider integrations={sortedIntegrations}>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<Filters />
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="absolute w-full h-full flex flex-1 gap-[30px] overflow-hidden overflow-y-scroll scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
|
||||
<div className="w-[220px] bg-third p-[16px] flex flex-col gap-[24px] sticky top-0">
|
||||
<h2 className="text-[20px]">Channels</h2>
|
||||
<div className="gap-[16px] flex flex-col">
|
||||
{sortedIntegrations.length === 0 && (
|
||||
<div className="text-[12px]">No channels</div>
|
||||
)}
|
||||
{sortedIntegrations.map((integration) => (
|
||||
<div
|
||||
key={integration.id}
|
||||
|
|
@ -46,13 +51,13 @@ export const LaunchesComponent: FC<{
|
|||
/>
|
||||
</div>
|
||||
<div className="flex-1">{integration.name}</div>
|
||||
<div>3</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Filters />
|
||||
<Calendar />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import {FC, useEffect, useState} from "react";
|
||||
import {Integrations} from "@gitroom/frontend/components/launches/calendar.context";
|
||||
import {PickPlatforms} from "@gitroom/frontend/components/launches/helpers/pick.platform.component";
|
||||
import {IntegrationContext} from "@gitroom/frontend/components/launches/helpers/use.integration";
|
||||
import {ShowAllProviders} from "@gitroom/frontend/components/launches/providers/show.all.providers";
|
||||
|
||||
export const ProvidersOptions: FC<{
|
||||
integrations: Integrations[];
|
||||
editorValue: Array<{ id?: string; content: string }>;
|
||||
}> = (props) => {
|
||||
const { integrations, editorValue } = props;
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useState([
|
||||
integrations[0],
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrations.indexOf(selectedIntegrations[0]) === -1) {
|
||||
setSelectedIntegrations([integrations[0]]);
|
||||
}
|
||||
}, [integrations, selectedIntegrations]);
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<PickPlatforms
|
||||
integrations={integrations}
|
||||
selectedIntegrations={selectedIntegrations}
|
||||
onChange={setSelectedIntegrations}
|
||||
singleSelect={true}
|
||||
hide={integrations.length === 1}
|
||||
/>
|
||||
<IntegrationContext.Provider
|
||||
value={{ value: editorValue, integration: selectedIntegrations?.[0] }}
|
||||
>
|
||||
<ShowAllProviders
|
||||
value={editorValue}
|
||||
integrations={integrations}
|
||||
selectedProvider={selectedIntegrations?.[0]}
|
||||
/>
|
||||
</IntegrationContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,31 +1,85 @@
|
|||
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";
|
||||
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';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import clsx from 'clsx';
|
||||
import localFont from 'next/font/local';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
|
||||
const font = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/SFNS.woff2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const DevtoPreview: FC = () => {
|
||||
return <div>asd</div>
|
||||
const { value} = useIntegration();
|
||||
const settings = useSettings();
|
||||
const image = useMediaDirectory();
|
||||
const [coverPicture, title, tags] = settings.watch([
|
||||
'main_image',
|
||||
'title',
|
||||
'tags',
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
font.className,
|
||||
'font-[800] flex h-[1000px] w-[699.8px] rounded-[10px] bg-[#181818] overflow-hidden overflow-y-auto flex-col gap-[32px]'
|
||||
)}
|
||||
>
|
||||
{!!coverPicture?.path && (
|
||||
<div className="h-[338.672px]">
|
||||
<img
|
||||
className="object-cover w-full h-full"
|
||||
src={image.set(coverPicture.path)}
|
||||
alt="cover_picture"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-[60px]">
|
||||
<div className="text-[48px] leading-[60px] mb-[8px]">{title}</div>
|
||||
<div className="flex gap-[16px]">
|
||||
{tags?.map((p: any) => (
|
||||
<div key={p.label}>#{p.label}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-[60px]">
|
||||
<MDEditor.Markdown style={{ whiteSpace: 'pre-wrap' }} className={font.className} skipHtml={true} source={value.map(p => p.content).join('\n')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DevtoSettings: FC = () => {
|
||||
const form = useSettings();
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Input label="Canonical Link" {...form.register('canonical')} />
|
||||
<MediaComponent label="Cover picture" description="Add a cover picture" {...form.register('main_image')} />
|
||||
<div className="mt-[20px]">
|
||||
<SelectOrganization {...form.register('organization') } />
|
||||
</div>
|
||||
<div>
|
||||
<DevtoTags label="Tags (Maximum 4)" {...form.register('tags')} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
const form = useSettings();
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Input label="Canonical Link" {...form.register('canonical')} />
|
||||
<MediaComponent
|
||||
label="Cover picture"
|
||||
description="Add a cover picture"
|
||||
{...form.register('main_image')}
|
||||
/>
|
||||
<div className="mt-[20px]">
|
||||
<SelectOrganization {...form.register('organization')} />
|
||||
</div>
|
||||
<div>
|
||||
<DevtoTags label="Tags (Maximum 4)" {...form.register('tags')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider(DevtoSettings, DevtoPreview, DevToSettingsDto);
|
||||
export default withProvider(DevtoSettings, DevtoPreview, DevToSettingsDto);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,87 @@
|
|||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { HashnodePublications } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.publications';
|
||||
import { HashnodeTags } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.tags';
|
||||
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import clsx from 'clsx';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import { Plus_Jakarta_Sans } from 'next/font/google';
|
||||
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
|
||||
const font = Plus_Jakarta_Sans({
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
const HashnodePreview: FC = () => {
|
||||
const { value } = useIntegration();
|
||||
const settings = useSettings();
|
||||
const image = useMediaDirectory();
|
||||
const [coverPicture, title, subtitle] = settings.watch([
|
||||
'main_image',
|
||||
'title',
|
||||
'subtitle',
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
font.className,
|
||||
'text-center text-black flex h-[1000px] w-[699.8px] rounded-[10px] bg-white overflow-hidden overflow-y-auto flex-col gap-[32px]'
|
||||
)}
|
||||
>
|
||||
{!!coverPicture?.path && (
|
||||
<div className="h-[338.672px]">
|
||||
<img
|
||||
className="object-cover w-full h-full"
|
||||
src={image.set(coverPicture.path)}
|
||||
alt="cover_picture"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-[60px]">
|
||||
<div className="font-[800] text-[48px] leading-[60px] mb-[8px]">{title}</div>
|
||||
<div className="font-[400] text-[30px] leading-[60px] mb-[8px] text-[#334155]">{subtitle}</div>
|
||||
</div>
|
||||
<div className="px-[60px] text-left">
|
||||
<MDEditor.Markdown
|
||||
style={{ whiteSpace: 'pre-wrap', color: 'black' }}
|
||||
className={font.className}
|
||||
skipHtml={true}
|
||||
source={value.map((p) => p.content).join('\n')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HashnodeSettings: FC = () => {
|
||||
const form = useSettings();
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Input label="Subtitle" {...form.register('subtitle')} />
|
||||
<Input label="Canonical Link" {...form.register('canonical')} />
|
||||
<MediaComponent
|
||||
label="Cover picture"
|
||||
description="Add a cover picture"
|
||||
{...form.register('main_image')}
|
||||
/>
|
||||
<div className="mt-[20px]">
|
||||
<HashnodePublications {...form.register('publication')} />
|
||||
</div>
|
||||
<div>
|
||||
<HashnodeTags label="Tags" {...form.register('tags')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider(
|
||||
HashnodeSettings,
|
||||
HashnodePreview,
|
||||
HashnodeSettingsDto
|
||||
);
|
||||
|
|
@ -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 HashnodePublications: FC<{
|
||||
name: string;
|
||||
onChange: (event: { target: { value: string; name: string } }) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [publications, setOrgs] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string|undefined>();
|
||||
|
||||
const onChangeInner = (event: { target: { value: string, name: string } }) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
customFunc.get('publications').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
if (!publications.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select name={name} label="Select publication" onChange={onChangeInner} value={currentMedia}>
|
||||
<option value="">--Select--</option>
|
||||
{publications.map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { FC, useCallback, useEffect, useMemo, 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 HashnodeTags: 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<any[]>([]);
|
||||
const { getValues, formState: form } = useSettings();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const err = useMemo(() => {
|
||||
if (!form || !form.errors[props?.name!]) return;
|
||||
return form?.errors?.[props?.name!]?.message! as string;
|
||||
}, [form?.errors?.[props?.name!]?.message]);
|
||||
|
||||
if (!tags.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-['Inter'] text-[14px] mb-[6px]">{label}</div>
|
||||
<ReactTags
|
||||
suggestions={tags}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,16 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
FC,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||
import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor';
|
||||
import { useValues } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
|
|
@ -13,46 +20,87 @@ import {
|
|||
IntegrationContext,
|
||||
useIntegration,
|
||||
} from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { createPortal } from 'react-dom';
|
||||
import clsx from 'clsx';
|
||||
import { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component';
|
||||
|
||||
// This is a simple function that if we edit in place, we hide the editor on top
|
||||
export const EditorWrapper: FC = (props) => {
|
||||
const showHide = useHideTopEditor();
|
||||
// Simple component to change back to settings on after changing tab
|
||||
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
|
||||
useEffect(() => {
|
||||
showHide.hide();
|
||||
return () => {
|
||||
showHide.show();
|
||||
setTimeout(() => {
|
||||
props.changeTab();
|
||||
}, 500);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// This is a simple function that if we edit in place, we hide the editor on top
|
||||
export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const showHide = useHideTopEditor();
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowEditor(true);
|
||||
showHide.hide();
|
||||
return () => {
|
||||
showHide.show();
|
||||
setShowEditor(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!showEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export const withProvider = (
|
||||
SettingsComponent: FC,
|
||||
SettingsComponent: FC | null,
|
||||
PreviewComponent: FC,
|
||||
dto?: any
|
||||
) => {
|
||||
return (props: {
|
||||
identifier: string;
|
||||
id: string;
|
||||
value: Array<{ content: string; id?: string }>;
|
||||
value: Array<{
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{ path: string; id: string }>;
|
||||
}>;
|
||||
show: boolean;
|
||||
}) => {
|
||||
const existingData = useExistingData();
|
||||
const { integration } = useIntegration();
|
||||
const [editInPlace, setEditInPlace] = useState(!!existingData.integration);
|
||||
const [InPlaceValue, setInPlaceValue] = useState<
|
||||
Array<{ id?: string; content: string }>
|
||||
Array<{
|
||||
id?: string;
|
||||
content: string;
|
||||
image?: Array<{ id: string; path: string }>;
|
||||
}>
|
||||
>(
|
||||
// @ts-ignore
|
||||
existingData.integration
|
||||
? existingData.posts.map((p) => ({ id: p.id, content: p.content }))
|
||||
? existingData.posts.map((p) => ({
|
||||
id: p.id,
|
||||
content: p.content,
|
||||
image: p.image,
|
||||
}))
|
||||
: [{ content: '' }]
|
||||
);
|
||||
const [showTab, setShowTab] = useState(existingData.integration ? 1 : 0);
|
||||
const [showTab, setShowTab] = useState(0);
|
||||
|
||||
const Component = useMemo(() => {
|
||||
return SettingsComponent ? SettingsComponent : () => <></>;
|
||||
}, [SettingsComponent]);
|
||||
|
||||
// in case there is an error on submit, we change to the settings tab for the specific provider
|
||||
useMoveToIntegrationListener(true, (identifier) => {
|
||||
if (identifier === props.identifier) {
|
||||
useMoveToIntegrationListener([props.id], true, (identifier) => {
|
||||
if (identifier === props.id) {
|
||||
setShowTab(2);
|
||||
}
|
||||
});
|
||||
|
|
@ -77,6 +125,19 @@ export const withProvider = (
|
|||
[InPlaceValue]
|
||||
);
|
||||
|
||||
const changeImage = useCallback(
|
||||
(index: number) =>
|
||||
(newValue: {
|
||||
target: { name: string; value?: Array<{ id: string; path: string }> };
|
||||
}) => {
|
||||
return setInPlaceValue((prev) => {
|
||||
prev[index].image = newValue.target.value;
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[InPlaceValue]
|
||||
);
|
||||
|
||||
// add another local editor
|
||||
const addValue = useCallback(
|
||||
(index: number) => () => {
|
||||
|
|
@ -88,30 +149,46 @@ export const withProvider = (
|
|||
[InPlaceValue]
|
||||
);
|
||||
|
||||
// This is a function if we want to switch from the global editor to edit in place
|
||||
const changeToEditor = useCallback(
|
||||
(editor: boolean) => async () => {
|
||||
// Delete post
|
||||
const deletePost = useCallback(
|
||||
(index: number) => async () => {
|
||||
if (
|
||||
editor &&
|
||||
!editInPlace &&
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to edit in place?',
|
||||
'Yes, edit in place!'
|
||||
'Are you sure you want to delete this post?',
|
||||
'Yes, delete it!'
|
||||
))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
setShowTab(editor ? 1 : 0);
|
||||
if (editor && !editInPlace) {
|
||||
setEditInPlace(true);
|
||||
setInPlaceValue(
|
||||
props.value.map((p) => ({ id: p.id, content: p.content }))
|
||||
);
|
||||
return;
|
||||
}
|
||||
setInPlaceValue((prev) => {
|
||||
prev.splice(index, 1);
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[props.value, editInPlace]
|
||||
[InPlaceValue]
|
||||
);
|
||||
|
||||
// This is a function if we want to switch from the global editor to edit in place
|
||||
const changeToEditor = useCallback(async () => {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
!editInPlace
|
||||
? 'Are you sure you want to edit only this?'
|
||||
: 'Are you sure you want to revert it back to global editing?',
|
||||
'Yes, edit in place!'
|
||||
))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setEditInPlace(!editInPlace);
|
||||
setInPlaceValue(
|
||||
editInPlace
|
||||
? [{ content: '' }]
|
||||
: 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;
|
||||
|
|
@ -119,58 +196,161 @@ export const withProvider = (
|
|||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<div className="mt-[15px]">
|
||||
{editInPlace && <EditorWrapper />}
|
||||
<div className="flex">
|
||||
<div>
|
||||
<Button secondary={showTab !== 0} onClick={changeToEditor(false)}>
|
||||
<SetTab changeTab={() => setShowTab(0)} />
|
||||
<div className="mt-[15px] w-full flex flex-col flex-1">
|
||||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 flex">
|
||||
<Button
|
||||
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
|
||||
secondary={showTab !== 0}
|
||||
onClick={() => setShowTab(0)}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button secondary={showTab !== 2} onClick={() => setShowTab(2)}>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button secondary={showTab !== 1} onClick={changeToEditor(true)}>
|
||||
Editor
|
||||
{!!SettingsComponent && (
|
||||
<div className="flex-1 flex">
|
||||
<Button
|
||||
className={clsx(
|
||||
'flex-1 overflow-hidden whitespace-nowrap',
|
||||
showTab === 2 && 'rounded-[4px]'
|
||||
)}
|
||||
secondary={showTab !== 2}
|
||||
onClick={() => setShowTab(2)}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex">
|
||||
<Button
|
||||
className="rounded-[4px] flex-1 !bg-red-700 overflow-hidden whitespace-nowrap"
|
||||
secondary={showTab !== 1}
|
||||
onClick={changeToEditor}
|
||||
>
|
||||
{editInPlace ? 'Edit globally' : 'Edit only this'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showTab === 1 && (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
{InPlaceValue.map((val, index) => (
|
||||
<>
|
||||
<MDEditor
|
||||
key={`edit_inner_${index}`}
|
||||
height={InPlaceValue.length > 1 ? 200 : 500}
|
||||
value={val.content}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
<div>
|
||||
<Button onClick={addValue(index)}>Add post</Button>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{editInPlace &&
|
||||
createPortal(
|
||||
<EditorWrapper>
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
{!existingData?.integration && (
|
||||
<div className="bg-red-800">
|
||||
This will edit only this provider
|
||||
</div>
|
||||
)}
|
||||
{InPlaceValue.map((val, index) => (
|
||||
<>
|
||||
<div>
|
||||
<MDEditor
|
||||
key={`edit_inner_${index}`}
|
||||
height={InPlaceValue.length > 1 ? 200 : 250}
|
||||
value={val.content}
|
||||
commands={[
|
||||
...commands
|
||||
.getCommands()
|
||||
.filter((f) => f.name !== 'image'),
|
||||
newImage,
|
||||
]}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
{(!val.content || val.content.length < 6) && (
|
||||
<div className="my-[5px] text-[#F97066] text-[12px] font-[500]">
|
||||
The post should be at least 6 characters long
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<MultiMediaComponent
|
||||
label="Attachments"
|
||||
description=""
|
||||
name="image"
|
||||
value={val.image}
|
||||
onChange={changeImage(index)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex bg-[#121b2c] rounded-br-[8px] text-[#F97066]">
|
||||
{InPlaceValue.length > 1 && (
|
||||
<div
|
||||
className="flex cursor-pointer gap-[4px] justify-center items-center flex-1"
|
||||
onClick={deletePost(index)}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
|
||||
fill="#F97066"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500] pr-[10px]">
|
||||
Delete Post
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={addValue(index)}
|
||||
className="!h-[24px] rounded-[3px] flex gap-[4px] w-[102px] text-[12px] font-[500]"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 1.3125C5.87512 1.3125 4.7755 1.64607 3.8402 2.27102C2.90489 2.89597 2.17591 3.78423 1.74544 4.82349C1.31496 5.86274 1.20233 7.00631 1.42179 8.10958C1.64124 9.21284 2.18292 10.2263 2.97833 11.0217C3.77374 11.8171 4.78716 12.3588 5.89043 12.5782C6.99369 12.7977 8.13726 12.685 9.17651 12.2546C10.2158 11.8241 11.104 11.0951 11.729 10.1598C12.3539 9.2245 12.6875 8.12488 12.6875 7C12.6859 5.49207 12.0862 4.04636 11.0199 2.98009C9.95365 1.91382 8.50793 1.31409 7 1.3125ZM7 11.8125C6.04818 11.8125 5.11773 11.5303 4.32632 11.0014C3.53491 10.4726 2.91808 9.72103 2.55383 8.84166C2.18959 7.96229 2.09428 6.99466 2.27997 6.06113C2.46566 5.12759 2.92401 4.27009 3.59705 3.59705C4.27009 2.92401 5.1276 2.46566 6.06113 2.27997C6.99466 2.09428 7.9623 2.18958 8.84167 2.55383C9.72104 2.91808 10.4726 3.53491 11.0015 4.32632C11.5303 5.11773 11.8125 6.04818 11.8125 7C11.8111 8.27591 11.3036 9.49915 10.4014 10.4014C9.49915 11.3036 8.27591 11.8111 7 11.8125ZM9.625 7C9.625 7.11603 9.57891 7.22731 9.49686 7.30936C9.41481 7.39141 9.30353 7.4375 9.1875 7.4375H7.4375V9.1875C7.4375 9.30353 7.39141 9.41481 7.30936 9.49686C7.22731 9.57891 7.11603 9.625 7 9.625C6.88397 9.625 6.77269 9.57891 6.69064 9.49686C6.6086 9.41481 6.5625 9.30353 6.5625 9.1875V7.4375H4.8125C4.69647 7.4375 4.58519 7.39141 4.50314 7.30936C4.4211 7.22731 4.375 7.11603 4.375 7C4.375 6.88397 4.4211 6.77269 4.50314 6.69064C4.58519 6.60859 4.69647 6.5625 4.8125 6.5625H6.5625V4.8125C6.5625 4.69647 6.6086 4.58519 6.69064 4.50314C6.77269 4.42109 6.88397 4.375 7 4.375C7.11603 4.375 7.22731 4.42109 7.30936 4.50314C7.39141 4.58519 7.4375 4.69647 7.4375 4.8125V6.5625H9.1875C9.30353 6.5625 9.41481 6.60859 9.49686 6.69064C9.57891 6.77269 9.625 6.88397 9.625 7Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Add post</div>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</EditorWrapper>,
|
||||
document.querySelector('#renderEditor')!
|
||||
)}
|
||||
{showTab === 2 && (
|
||||
<div className="mt-[20px]">
|
||||
<SettingsComponent />
|
||||
<Component />
|
||||
</div>
|
||||
)}
|
||||
{showTab === 0 && (
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
value: editInPlace ? InPlaceValue : props.value,
|
||||
integration,
|
||||
}}
|
||||
>
|
||||
<PreviewComponent />
|
||||
</IntegrationContext.Provider>
|
||||
<div className="mt-[20px] flex flex-col items-center">
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
value: editInPlace ? InPlaceValue : props.value,
|
||||
integration,
|
||||
}}
|
||||
>
|
||||
{(editInPlace ? InPlaceValue : props.value)
|
||||
.map((p) => p.content)
|
||||
.join('').length ? (
|
||||
<PreviewComponent />
|
||||
) : (
|
||||
<>No Content Yet</>
|
||||
)}
|
||||
</IntegrationContext.Provider>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormProvider>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import clsx from 'clsx';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
|
||||
const LinkedinPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const mediaDir = useMediaDirectory();
|
||||
const newValues = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
|
|
@ -14,59 +15,91 @@ const LinkedinPreview: FC = (props) => {
|
|||
},
|
||||
});
|
||||
|
||||
const [firstPost, ...morePosts] = newValues;
|
||||
if (!firstPost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('max-w-[598px] px-[16px] border border-[#2E3336]')}>
|
||||
<div className="w-full h-full relative flex flex-col pt-[12px]">
|
||||
{newValues.map((value, index) => (
|
||||
<div
|
||||
key={`tweet_${index}`}
|
||||
className={`flex gap-[8px] pb-[${
|
||||
index === topValue.length - 1 ? '12px' : '24px'
|
||||
}] relative`}
|
||||
>
|
||||
<div className="w-[40px] flex flex-col items-center">
|
||||
<img
|
||||
src={integration?.picture}
|
||||
alt="x"
|
||||
className="rounded-full relative z-[2]"
|
||||
/>
|
||||
{index !== topValue.length - 1 && (
|
||||
<div className="flex-1 w-[2px] h-[calc(100%-10px)] bg-[#2E3336] absolute top-[10px] z-[1]" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[4px]">
|
||||
<div className="flex">
|
||||
<div className="h-[22px] text-[15px] font-[700]">
|
||||
{integration?.name}
|
||||
</div>
|
||||
<div className="text-[15px] text-[#1D9BF0] mt-[1px] ml-[2px]">
|
||||
<svg
|
||||
viewBox="0 0 22 22"
|
||||
aria-label="Verified account"
|
||||
role="img"
|
||||
className="max-w-[20px] max-h-[20px] fill-current h-[1.25em]"
|
||||
data-testid="icon-verified"
|
||||
>
|
||||
<g>
|
||||
<path d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[15px] font-[400] text-[#71767b] ml-[4px]">
|
||||
@username
|
||||
</div>
|
||||
</div>
|
||||
<pre>{value.text}</pre>
|
||||
</div>
|
||||
<div className="rounded-[8px] flex flex-col gap-[8px] border border-black/90 w-[555px] pt-[12px] pl-[16px] pb-[12px] pr-[40px] bg-white text-black font-['helvetica']">
|
||||
<div className="flex gap-[8px]">
|
||||
<div className="w-[48px] h-[48px]">
|
||||
<img
|
||||
src={integration?.picture}
|
||||
alt="x"
|
||||
className="rounded-full w-full h-full relative z-[2]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col leading-[16px]">
|
||||
<div className="text-[14px] font-[600]">{integration?.name}</div>
|
||||
<div className="text-[12px] font-[400] text-black/60">
|
||||
CEO @ Gitroom
|
||||
</div>
|
||||
))}
|
||||
<div className="text-[12px] font-[400] text-black/60">1m</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<pre className="font-['helvetica'] text-[14px] font-[400] text-wrap">
|
||||
{firstPost?.text}
|
||||
</pre>
|
||||
|
||||
{!!firstPost?.images?.length && (
|
||||
<div className="-ml-[16px] -mr-[40px] flex-1 h-[555px] flex overflow-hidden mt-[12px] gap-[2px]">
|
||||
{firstPost.images.map((image, index) => (
|
||||
<a
|
||||
key={`image_${index}`}
|
||||
href={mediaDir.set(image.path)}
|
||||
className="flex-1"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{morePosts.map((p, index) => (
|
||||
<div className="flex gap-[8px]" key={index}>
|
||||
<div className="w-[40px] h-[40px]">
|
||||
<img
|
||||
src={integration?.picture}
|
||||
alt="x"
|
||||
className="rounded-full w-full h-full relative z-[2]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col leading-[16px] bg-[#F2F2F2] w-full pt-[8px] pr-[64px] pl-[12px] pb-[8px] rounded-[8px]">
|
||||
<div className="text-[14px] font-[600]">{integration?.name}</div>
|
||||
<div className="text-[12px] font-[400] text-black/60">
|
||||
CEO @ Gitroom
|
||||
</div>
|
||||
<div className="text-[14px] mt-[8px] font-[400] text-black/90">
|
||||
{p.text}
|
||||
</div>
|
||||
|
||||
{!!p?.images?.length && (
|
||||
<div className="w-full h-[120px] flex overflow-hidden mt-[12px] gap-[2px]">
|
||||
{p.images.map((image, index) => (
|
||||
<a
|
||||
key={`image_${index}`}
|
||||
href={mediaDir.set(image.path)}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-[120px] h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkedinSettings: FC = () => {
|
||||
return <div>asdfasd</div>;
|
||||
};
|
||||
|
||||
export default withProvider(LinkedinSettings, LinkedinPreview);
|
||||
export default withProvider(null, LinkedinPreview);
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
apps/frontend/src/components/launches/providers/medium/fonts/Charter Regular.ttf
Executable file
BIN
apps/frontend/src/components/launches/providers/medium/fonts/Charter Regular.ttf
Executable file
Binary file not shown.
|
|
@ -0,0 +1,52 @@
|
|||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on July 10, 2013 */
|
||||
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'charterbold_italic';
|
||||
src: url('charter_bold_italic-webfont.eot');
|
||||
src: url('charter_bold_italic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_bold_italic-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'charterbold';
|
||||
src: url('charter_bold-webfont.eot');
|
||||
src: url('charter_bold-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_bold-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'charteritalic';
|
||||
src: url('charter_italic-webfont.eot');
|
||||
src: url('charter_italic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_italic-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'charterregular';
|
||||
src: url('charter_regular-webfont.eot');
|
||||
src: url('charter_regular-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_regular-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { MediumPublications } from '@gitroom/frontend/components/launches/providers/medium/medium.publications';
|
||||
import { MediumTags } from '@gitroom/frontend/components/launches/providers/medium/medium.tags';
|
||||
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import clsx from 'clsx';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import localFont from 'next/font/local'
|
||||
|
||||
const charter = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/Charter Regular.ttf',
|
||||
weight: 'normal',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/Charter Italic.ttf',
|
||||
weight: 'normal',
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
path: './fonts/Charter Bold.ttf',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/Charter Bold Italic.ttf',
|
||||
weight: '700',
|
||||
style: 'italic',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const MediumPreview: FC = () => {
|
||||
const { value } = useIntegration();
|
||||
const settings = useSettings();
|
||||
const [title, subtitle] = settings.watch([
|
||||
'title',
|
||||
'subtitle',
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'font-[800] flex h-[1000px] w-[699.8px] text-[#242424] font-[\'Inter\'] rounded-[10px] bg-white overflow-hidden overflow-y-auto flex-col gap-[56px]'
|
||||
)}
|
||||
>
|
||||
<div className="px-[60px] pt-[20px]">
|
||||
<div className="text-[48px] leading-[60px] mb-[8px]">{title}</div>
|
||||
<div className="text-[22px] font-[400] text-[#6b6b6b]">{subtitle}</div>
|
||||
</div>
|
||||
<div className="px-[60px]">
|
||||
<MDEditor.Markdown
|
||||
style={{ whiteSpace: 'pre-wrap', color: '#242424' }}
|
||||
className={charter.className}
|
||||
skipHtml={true}
|
||||
source={value.map((p) => p.content).join('\n')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MediumSettings: FC = () => {
|
||||
const form = useSettings();
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Input label="Subtitle" {...form.register('subtitle')} />
|
||||
<Input label="Canonical Link" {...form.register('canonical')} />
|
||||
<div>
|
||||
<MediumPublications {...form.register('publication')} />
|
||||
</div>
|
||||
<div>
|
||||
<MediumTags label="Topics" {...form.register('tags')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider(MediumSettings, MediumPreview, MediumSettingsDto);
|
||||
|
|
@ -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 MediumPublications: FC<{
|
||||
name: string;
|
||||
onChange: (event: { target: { value: string; name: string } }) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [publications, setOrgs] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string|undefined>();
|
||||
|
||||
const onChangeInner = (event: { target: { value: string, name: string } }) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
customFunc.get('publications').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
if (!publications.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select name={name} label="Select publication" onChange={onChangeInner} value={currentMedia}>
|
||||
<option value="">--Select--</option>
|
||||
{publications.map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { ReactTags } from 'react-tag-autocomplete';
|
||||
|
||||
export const MediumTags: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
onChange: (event: { target: { value: any[]; name: string } }) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const { getValues } = useSettings();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
const [suggestions, setSuggestions] = useState<string>('');
|
||||
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({ target: { value: modify, name } });
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
if (tagValue.length >= 3) {
|
||||
return;
|
||||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({ target: { value: modify, name } });
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const suggestionsArray = useMemo(() => {
|
||||
return [...tagValue, { label: suggestions, value: suggestions }].filter(f => f.label);
|
||||
}, [suggestions, tagValue]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-['Inter'] text-[14px] mb-[6px]">{label}</div>
|
||||
<ReactTags
|
||||
suggestions={suggestionsArray}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onInput={setSuggestions}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,76 +1,187 @@
|
|||
import { FC } from 'react';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import clsx from 'clsx';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import { Subreddit } from '@gitroom/frontend/components/launches/providers/reddit/subreddit';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { useFieldArray, useWatch } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import {
|
||||
RedditSettingsDto,
|
||||
RedditSettingsValueDto,
|
||||
} from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto';
|
||||
import clsx from 'clsx';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import {deleteDialog} from "@gitroom/react/helpers/delete.dialog";
|
||||
|
||||
const RedditPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const newValues = useFormatting(topValue, {
|
||||
const RenderRedditComponent: FC<{
|
||||
type: string;
|
||||
images?: Array<{ id: string; path: string }>;
|
||||
}> = (props) => {
|
||||
const { value: topValue } = useIntegration();
|
||||
const showMedia = useMediaDirectory();
|
||||
|
||||
const { type, images } = props;
|
||||
|
||||
const [firstPost] = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
return text.slice(0, 280);
|
||||
}
|
||||
return text.slice(0, 280);
|
||||
},
|
||||
});
|
||||
|
||||
switch (type) {
|
||||
case 'self':
|
||||
return (
|
||||
<pre className="font-['Inter'] text-[14px] text-wrap">
|
||||
{firstPost?.text}
|
||||
</pre>
|
||||
);
|
||||
case 'link':
|
||||
return (
|
||||
<div className="h-[375px] bg-primary rounded-[16px] flex justify-center items-center">
|
||||
Link
|
||||
</div>
|
||||
);
|
||||
case 'media':
|
||||
return (
|
||||
<div className="h-[375px] bg-primary rounded-[16px] flex justify-center items-center">
|
||||
{!!images?.length &&
|
||||
images.map((image, index) => (
|
||||
<a
|
||||
key={`image_${index}`}
|
||||
href={showMedia.set(image.path)}
|
||||
className="flex-1 h-full"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={showMedia.set(image.path)}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const RedditPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const settings = useWatch({
|
||||
name: 'subreddit',
|
||||
}) as Array<RedditSettingsValueDto>;
|
||||
|
||||
const [, ...restOfPosts] = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
return text.slice(0, 280);
|
||||
},
|
||||
});
|
||||
console.log(settings);
|
||||
|
||||
if (!settings || !settings.length) {
|
||||
return <>Please add at least one Subreddit from the settings</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'max-w-[598px] px-[16px] border border-[#2E3336]',
|
||||
)}
|
||||
>
|
||||
<div className="w-full h-full relative flex flex-col pt-[12px]">
|
||||
{newValues.map((value, index) => (
|
||||
<div className="flex flex-col gap-[40px] w-full">
|
||||
{settings
|
||||
.filter(({ value }) => value?.subreddit)
|
||||
.map(({ value }, index) => (
|
||||
<div
|
||||
key={`tweet_${index}`}
|
||||
className={`flex gap-[8px] pb-[${
|
||||
index === topValue.length - 1 ? '12px' : '24px'
|
||||
}] relative`}
|
||||
key={index}
|
||||
className={clsx(
|
||||
"bg-[#0B1416] w-full p-[10px] flex flex-col font-['Inter'] border-tableBorder border"
|
||||
)}
|
||||
>
|
||||
<div className="w-[40px] flex flex-col items-center">
|
||||
<img
|
||||
src={integration?.picture}
|
||||
alt="x"
|
||||
className="rounded-full relative z-[2]"
|
||||
/>
|
||||
{index !== topValue.length - 1 && (
|
||||
<div className="flex-1 w-[2px] h-[calc(100%-10px)] bg-[#2E3336] absolute top-[10px] z-[1]" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[4px]">
|
||||
<div className="flex">
|
||||
<div className="h-[22px] text-[15px] font-[700]">
|
||||
{integration?.name}
|
||||
</div>
|
||||
<div className="text-[15px] text-[#1D9BF0] mt-[1px] ml-[2px]">
|
||||
<svg
|
||||
viewBox="0 0 22 22"
|
||||
aria-label="Verified account"
|
||||
role="img"
|
||||
className="max-w-[20px] max-h-[20px] fill-current h-[1.25em]"
|
||||
data-testid="icon-verified"
|
||||
>
|
||||
<g>
|
||||
<path d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[15px] font-[400] text-[#71767b] ml-[4px]">
|
||||
@username
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row gap-[8px]">
|
||||
<div className="w-[40px] h-[40px] bg-white rounded-full" />
|
||||
<div className="flex flex-col">
|
||||
<div className="text-[12px] font-[700]">
|
||||
{value.subreddit}
|
||||
</div>
|
||||
<div className="text-[12px]">{integration?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre>{value.text}</pre>
|
||||
<div className="font-[600] text-[24px] mb-[16px]">
|
||||
{value.title}
|
||||
</div>
|
||||
<RenderRedditComponent type={value.type} images={value.media} />
|
||||
<div
|
||||
className={clsx(
|
||||
restOfPosts.length && 'mt-[40px] flex flex-col gap-[20px]'
|
||||
)}
|
||||
>
|
||||
{restOfPosts.map((p, index) => (
|
||||
<div className="flex gap-[8px]" key={index}>
|
||||
<div className="w-[32px] h-[32px]">
|
||||
<img
|
||||
src={integration?.picture}
|
||||
alt="x"
|
||||
className="rounded-full w-full h-full relative z-[2]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col leading-[16px] w-full pr-[64px] pb-[8px] rounded-[8px]">
|
||||
<div className="text-[14px] font-[600]">
|
||||
{integration?.name}
|
||||
</div>
|
||||
<pre className="font-['Inter'] text-[14px] mt-[8px] font-[400] text-white">
|
||||
{p.text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RedditSettings: FC = () => {
|
||||
return <div>asdfasd</div>;
|
||||
const { register, control } = useSettings();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control, // control props comes from useForm (optional: if you are using FormContext)
|
||||
name: 'subreddit', // unique name for your Field Array
|
||||
});
|
||||
|
||||
const addField = useCallback(() => {
|
||||
append({});
|
||||
}, [fields, append]);
|
||||
|
||||
const deleteField = useCallback((index: number) => async () => {
|
||||
if (!await deleteDialog('Are you sure you want to delete this Subreddit?')) return;
|
||||
remove(index);
|
||||
}, [fields, remove]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[20px] mb-[20px]">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col relative">
|
||||
<div onClick={deleteField(index)} className="absolute -left-[10px] justify-center items-center flex -top-[10px] w-[20px] h-[20px] bg-red-600 rounded-full text-white">
|
||||
x
|
||||
</div>
|
||||
<Subreddit
|
||||
{...register(`subreddit.${index}.value`)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={addField}>Add Subreddit</Button>
|
||||
{fields.length === 0 && (
|
||||
<div className="text-red-500 text-[12px] mt-[10px]">
|
||||
Please add at least one Subreddit
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider(RedditSettings, RedditPreview);
|
||||
export default withProvider(RedditSettings, RedditPreview, RedditSettingsDto);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,286 @@
|
|||
import {
|
||||
FC,
|
||||
FormEvent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import clsx from 'clsx';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
|
||||
export const RenderOptions: FC<{
|
||||
options: Array<'self' | 'link' | 'media'>;
|
||||
onClick: (current: 'self' | 'link' | 'media') => void;
|
||||
value: 'self' | 'link' | 'media';
|
||||
}> = (props) => {
|
||||
const { options, onClick, value } = props;
|
||||
const mapValues = useMemo(() => {
|
||||
return options.map((p) => ({
|
||||
children: (
|
||||
<>
|
||||
{p === 'self'
|
||||
? 'Post'
|
||||
: p === 'link'
|
||||
? 'Link'
|
||||
: p === 'media'
|
||||
? 'Media'
|
||||
: ''}
|
||||
</>
|
||||
),
|
||||
id: p,
|
||||
onClick: () => onClick(p),
|
||||
}));
|
||||
}, [options]);
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{mapValues.map((p) => (
|
||||
<Button
|
||||
className={clsx('flex-1', p.id !== value && 'bg-secondary')}
|
||||
key={p.id}
|
||||
{...p}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Subreddit: FC<{
|
||||
onChange: (event: {
|
||||
target: { name: string; value: { id: string; name: string } };
|
||||
}) => void;
|
||||
name: string;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
|
||||
const state = useSettings();
|
||||
const split = name.split('.');
|
||||
const [loading, setLoading] = useState(false);
|
||||
// @ts-ignore
|
||||
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
|
||||
|
||||
const [results, setResults] = useState([]);
|
||||
const func = useCustomProviderFunction();
|
||||
const value = useWatch({ name });
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const setResult = (result: { id: string; name: string }) => async () => {
|
||||
setLoading(true);
|
||||
setSearchValue('');
|
||||
const restrictions = await func.get('restrictions', {
|
||||
subreddit: result.name,
|
||||
});
|
||||
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...restrictions,
|
||||
type: restrictions.allow[0],
|
||||
media: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const setTitle = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
title: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const setType = useCallback(
|
||||
(e: string) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
type: e,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const setMedia = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
media: e.target.value.map((p: any) => p),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const setURL = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
url: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const setFlair = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
flair: value.flairs.find((p: any) => p.id === e.target.value),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const search = useDebouncedCallback(
|
||||
useCallback(async (e: FormEvent<HTMLInputElement>) => {
|
||||
// @ts-ignore
|
||||
setResults([]);
|
||||
// @ts-ignore
|
||||
if (!e.target.value) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const results = await func.get('subreddits', { word: e.target.value });
|
||||
// @ts-ignore
|
||||
setResults(results);
|
||||
}, []),
|
||||
500
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-primary p-[20px]">
|
||||
{value?.subreddit ? (
|
||||
<>
|
||||
<Input
|
||||
error={errors?.subreddit?.message}
|
||||
disableForm={true}
|
||||
value={value.subreddit}
|
||||
readOnly={true}
|
||||
label="Subreddit"
|
||||
name="subreddit"
|
||||
/>
|
||||
<div className="mb-[12px]">
|
||||
<RenderOptions
|
||||
value={value.type}
|
||||
options={value.allow}
|
||||
onClick={setType}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
error={errors?.title?.message}
|
||||
value={value.title}
|
||||
disableForm={true}
|
||||
label="Title"
|
||||
name="title"
|
||||
onChange={setTitle}
|
||||
/>
|
||||
<Select
|
||||
error={errors?.flair?.message}
|
||||
onChange={setFlair}
|
||||
value={value?.flair?.id}
|
||||
disableForm={true}
|
||||
label="Flair"
|
||||
name="flair"
|
||||
>
|
||||
<option value="">--Select Flair--</option>
|
||||
{value.flairs.map((f: any) => (
|
||||
<option key={f.name} value={f.id}>
|
||||
{f.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{value.type === 'link' && (
|
||||
<Input
|
||||
error={errors?.url?.message}
|
||||
value={value.url}
|
||||
label="URL"
|
||||
name="url"
|
||||
disableForm={true}
|
||||
onChange={setURL}
|
||||
/>
|
||||
)}
|
||||
{value.type === 'media' && (
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full h-[10px] bg-[#131B2C] rounded-tr-[8px] rounded-tl-[8px]" />
|
||||
<div className="flex flex-col text-nowrap">
|
||||
<MultiMediaComponent
|
||||
description=""
|
||||
name="media"
|
||||
label="Media"
|
||||
value={value.media}
|
||||
onChange={setMedia}
|
||||
error={errors?.media?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="/r/selfhosted"
|
||||
name="search"
|
||||
label="Search Subreddit"
|
||||
readOnly={loading}
|
||||
value={searchValue}
|
||||
error={errors?.message}
|
||||
disableForm={true}
|
||||
onInput={async (e) => {
|
||||
// @ts-ignore
|
||||
setSearchValue(e.target.value);
|
||||
await search(e);
|
||||
}}
|
||||
/>
|
||||
{!!results.length && !loading && (
|
||||
<div className="w-full absolute bg-input -mt-[20px] outline-none border-fifth border cursor-pointer">
|
||||
{results.map((r: { id: string; name: string }) => (
|
||||
<div
|
||||
onClick={setResult(r)}
|
||||
key={r.id}
|
||||
className="px-[16px] py-[5px] hover:bg-secondary"
|
||||
>
|
||||
{r.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,14 +4,20 @@ import DevtoProvider from "@gitroom/frontend/components/launches/providers/devto
|
|||
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";
|
||||
import MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider";
|
||||
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
|
||||
|
||||
const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
{identifier: 'x', component: XProvider},
|
||||
{identifier: 'linkedin', component: LinkedinProvider},
|
||||
{identifier: 'reddit', component: RedditProvider},
|
||||
{identifier: 'medium', component: MediumProvider},
|
||||
{identifier: 'hashnode', component: HashnodeProvider},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export const ShowAllProviders: FC<{integrations: Integrations[], value: Array<{content: string, id?: string}>, selectedProvider?: Integrations}> = (props) => {
|
||||
const {integrations, value, selectedProvider} = props;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ 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';
|
||||
import {useSettings} from "@gitroom/frontend/components/launches/helpers/use.values";
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
|
||||
const chirp = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/x/Chirp-Regular.woff2',
|
||||
path: './fonts/Chirp-Regular.woff2',
|
||||
weight: '400',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/x/Chirp-Bold.woff2',
|
||||
path: './fonts/Chirp-Bold.woff2',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
|
|
@ -23,28 +23,26 @@ const chirp = localFont({
|
|||
|
||||
const XPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const mediaDir = useMediaDirectory();
|
||||
const newValues = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
return text.slice(0, 280);
|
||||
}
|
||||
return text.slice(0, 280);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'max-w-[598px] px-[16px] border border-[#2E3336]',
|
||||
chirp.className
|
||||
)}
|
||||
>
|
||||
<div className="w-full h-full relative flex flex-col pt-[12px]">
|
||||
<div className={clsx('w-[555px] px-[16px]', chirp.className)}>
|
||||
<div className="w-full h-full relative flex flex-col">
|
||||
{newValues.map((value, index) => (
|
||||
<div
|
||||
key={`tweet_${index}`}
|
||||
className={`flex gap-[8px] pb-[${
|
||||
index === topValue.length - 1 ? '12px' : '24px'
|
||||
}] relative`}
|
||||
style={{}}
|
||||
className={clsx(
|
||||
`flex gap-[8px] relative`,
|
||||
index === newValues.length - 1 ? 'pb-[12px]' : 'pb-[24px]'
|
||||
)}
|
||||
>
|
||||
<div className="w-[40px] flex flex-col items-center">
|
||||
<img
|
||||
|
|
@ -78,7 +76,21 @@ const XPreview: FC = (props) => {
|
|||
@username
|
||||
</div>
|
||||
</div>
|
||||
<pre className={chirp.className}>{value.text}</pre>
|
||||
<pre className={clsx(chirp.className, 'text-wrap')}>
|
||||
{value.text}
|
||||
</pre>
|
||||
{!!value?.images?.length && (
|
||||
<div className="w-full h-[270px] rounded-[16px] flex overflow-hidden mt-[12px]">
|
||||
{value.images.map((image, index) => (
|
||||
<a key={`image_${index}`} className="flex-1" href={mediaDir.set(image.path)} target="_blank">
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDir.set(image.path)}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -87,11 +99,4 @@ const XPreview: FC = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const XSettings: FC = () => {
|
||||
const settings = useSettings({
|
||||
|
||||
});
|
||||
return <div>asdfasd</div>;
|
||||
};
|
||||
|
||||
export default withProvider(XSettings, XPreview);
|
||||
export default withProvider(null, XPreview);
|
||||
|
|
|
|||
|
|
@ -1,38 +1,51 @@
|
|||
import {ReactNode} from "react";
|
||||
import {Title} from "@gitroom/frontend/components/layout/title";
|
||||
import {headers} from "next/headers";
|
||||
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";
|
||||
import { ReactNode } from 'react';
|
||||
import { Title } from '@gitroom/frontend/components/layout/title';
|
||||
import { headers } from 'next/headers';
|
||||
import { ContextWrapper } from '@gitroom/frontend/components/layout/user.context';
|
||||
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';
|
||||
import { ShowMediaBoxModal } from '@gitroom/frontend/components/media/media.component';
|
||||
import Image from 'next/image';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
export const LayoutSettings = ({children}: {children: ReactNode}) => {
|
||||
const user = JSON.parse(headers().get('user')!);
|
||||
return (
|
||||
<ContextWrapper user={user}>
|
||||
<MantineWrapper>
|
||||
<ToolTip />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col">
|
||||
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<div className="text-2xl">
|
||||
Gitroom
|
||||
</div>
|
||||
<TopMenu />
|
||||
<div>
|
||||
<NotificationComponent />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1 rounded-3xl px-[23px] py-[17px] flex flex-col">
|
||||
<Title />
|
||||
<div className="flex flex-1 flex-col">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MantineWrapper>
|
||||
</ContextWrapper>
|
||||
);
|
||||
}
|
||||
const NotificationComponent = dynamic(
|
||||
() =>
|
||||
import('@gitroom/frontend/components/notifications/notification.component'),
|
||||
{
|
||||
loading: () => <></>,
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
||||
const user = JSON.parse(headers().get('user')!);
|
||||
return (
|
||||
<ContextWrapper user={user}>
|
||||
<MantineWrapper>
|
||||
<ToolTip />
|
||||
<ShowMediaBoxModal />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col">
|
||||
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
|
||||
<div className="text-2xl flex items-center gap-[10px]">
|
||||
<div>
|
||||
<Image src="/logo.svg" width={55} height={53} alt="Logo" />
|
||||
</div>
|
||||
<div className="mt-[12px]">Gitroom</div>
|
||||
</div>
|
||||
<TopMenu />
|
||||
<div>
|
||||
<NotificationComponent />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1 rounded-3xl px-[23px] py-[17px] flex flex-col">
|
||||
<Title />
|
||||
<div className="flex flex-1 flex-col">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MantineWrapper>
|
||||
</ContextWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const TopMenu: FC = () => {
|
|||
<ul className="gap-5 flex flex-1 items-center text-[18px]">
|
||||
{menuItems.map((item, index) => (
|
||||
<li key={item.name}>
|
||||
<Link href={item.path} className={clsx("flex gap-2 items-center box", menuItems.map(p => p.path).indexOf(path) === index ? 'text-primary showbox' : 'text-gray')}>
|
||||
<Link prefetch={false} href={item.path} className={clsx("flex gap-2 items-center box", menuItems.map(p => p.path).indexOf(path) === index ? 'text-primary showbox' : 'text-gray')}>
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,45 @@ 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";
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import EventEmitter from 'events';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import clsx from 'clsx';
|
||||
const showModalEmitter = new EventEmitter();
|
||||
|
||||
export const ShowMediaBoxModal: FC = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [callBack, setCallBack] =
|
||||
useState<(params: { id: string; path: string }) => void | undefined>();
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setShowModal(false);
|
||||
setCallBack(undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
showModalEmitter.on('show-modal', (cCallback) => {
|
||||
setShowModal(true);
|
||||
setCallBack(() => cCallback);
|
||||
});
|
||||
return () => {
|
||||
showModalEmitter.removeAllListeners('show-modal');
|
||||
};
|
||||
}, []);
|
||||
if (!showModal) return null;
|
||||
|
||||
return (
|
||||
<div className="text-white">
|
||||
<MediaBox setMedia={callBack!} closeModal={closeModal} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const showMediaBox = (
|
||||
callback: (params: { id: string; path: string }) => void
|
||||
) => {
|
||||
showModalEmitter.emit('show-modal', callback);
|
||||
};
|
||||
|
||||
export const MediaBox: FC<{
|
||||
setMedia: (params: { id: string; path: string }) => void;
|
||||
|
|
@ -67,62 +104,104 @@ export const MediaBox: FC<{
|
|||
|
||||
return (
|
||||
<div className="fixed left-0 top-0 bg-black/80 z-[300] w-full h-full p-[60px] animate-fade">
|
||||
<div className="w-full h-full bg-black border-tableBorder border-2 rounded-xl p-[20px] relative">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="flex absolute right-[40px] top-[7px] pointer hover:bg-third rounded-lg transition-all group px-2.5 py-2.5 text-sm font-semibold bg-transparent text-gray-800 hover:bg-gray-100 focus:text-primary-500"
|
||||
type="button"
|
||||
>
|
||||
<div className="relative flex gap-2 items-center justify-center">
|
||||
<input
|
||||
type="file"
|
||||
className="absolute left-0 top-0 w-full h-full opacity-0"
|
||||
accept="image/*"
|
||||
onChange={uploadMedia}
|
||||
/>
|
||||
<span className="sc-dhKdcB fhJPPc w-4 h-4">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.5276 1.00176C7.3957 0.979897 8.25623 1.16248 9.04309 1.53435C9.82982 1.90617 10.5209 2.45677 11.065 3.14199C11.3604 3.51404 11.6084 3.92054 11.8045 4.3516C12.2831 4.21796 12.7853 4.17281 13.2872 4.22273C14.2108 4.3146 15.0731 4.72233 15.7374 5.3744C16.4012 6.02599 16.8292 6.88362 16.9586 7.808C17.088 8.73224 16.9124 9.67586 16.457 10.4887C16.1871 10.9706 15.5777 11.1424 15.0958 10.8724C14.614 10.6025 14.4422 9.99308 14.7122 9.51126C14.9525 9.08224 15.0471 8.57971 14.9779 8.08532C14.9087 7.59107 14.6807 7.13971 14.3364 6.8017C13.9925 6.46418 13.5528 6.25903 13.0892 6.21291C12.6258 6.16682 12.1584 6.28157 11.7613 6.5429C11.4874 6.7232 11.1424 6.7577 10.8382 6.63524C10.534 6.51278 10.3091 6.24893 10.2365 5.92912C10.1075 5.36148 9.8545 4.83374 9.49872 4.38568C9.14303 3.93773 8.69439 3.58166 8.18851 3.34258C7.68275 3.10355 7.13199 2.98717 6.57794 3.00112C6.02388 3.01507 5.47902 3.15905 4.98477 3.4235C4.49039 3.68801 4.05875 4.06664 3.72443 4.53247C3.39004 4.9984 3.16233 5.5387 3.06049 6.11239C2.95864 6.68613 2.98571 7.27626 3.1394 7.83712C3.29306 8.39792 3.56876 8.91296 3.94345 9.34361C4.30596 9.76027 4.26207 10.3919 3.84542 10.7544C3.42876 11.1169 2.79712 11.073 2.4346 10.6564C1.8607 9.99678 1.44268 9.213 1.2105 8.36566C0.978333 7.51837 0.937639 6.62828 1.09128 5.76282C1.24492 4.89732 1.58919 4.07751 2.09958 3.36634C2.61005 2.65507 3.27363 2.07075 4.04125 1.66005C4.80899 1.24927 5.65951 1.02361 6.5276 1.00176Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M8 12.4142L8 17C8 17.5523 8.44771 18 9 18C9.55228 18 10 17.5523 10 17V12.4142L11.2929 13.7071C11.6834 14.0976 12.3166 14.0976 12.7071 13.7071C13.0976 13.3166 13.0976 12.6834 12.7071 12.2929L9.70711 9.29289C9.61123 9.19702 9.50073 9.12468 9.38278 9.07588C9.26488 9.02699 9.13559 9 9 9C8.86441 9 8.73512 9.02699 8.61722 9.07588C8.50195 9.12357 8.3938 9.19374 8.29945 9.2864C8.29705 9.28875 8.29467 9.29111 8.2923 9.29349L5.29289 12.2929C4.90237 12.6834 4.90237 13.3166 5.29289 13.7071C5.68342 14.0976 6.31658 14.0976 6.70711 13.7071L8 12.4142Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span>Upload assets</span>
|
||||
<div className="w-full h-full bg-[#0B101B] border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TopTitle title="Media Library" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="flex flex-wrap gap-[10px] mt-[35px] pt-[20px] border-tableBorder border-t-2">
|
||||
{!!mediaList.length && (
|
||||
<button
|
||||
className="flex absolute right-[40px] top-[7px] pointer hover:bg-third rounded-lg transition-all group px-2.5 py-2.5 text-sm font-semibold bg-transparent text-gray-800 hover:bg-gray-100 focus:text-primary-500"
|
||||
type="button"
|
||||
>
|
||||
<div className="relative flex gap-2 items-center justify-center">
|
||||
<input
|
||||
type="file"
|
||||
className="absolute left-0 top-0 w-full h-full opacity-0"
|
||||
accept="image/*"
|
||||
onChange={uploadMedia}
|
||||
/>
|
||||
<button className="cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white font-['Inter'] border-[2px] border-[#506490]">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="15"
|
||||
viewBox="0 0 14 15"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 1.8125C5.87512 1.8125 4.7755 2.14607 3.8402 2.77102C2.90489 3.39597 2.17591 4.28423 1.74544 5.32349C1.31496 6.36274 1.20233 7.50631 1.42179 8.60958C1.64124 9.71284 2.18292 10.7263 2.97833 11.5217C3.77374 12.3171 4.78716 12.8588 5.89043 13.0782C6.99369 13.2977 8.13726 13.185 9.17651 12.7546C10.2158 12.3241 11.104 11.5951 11.729 10.6598C12.3539 9.7245 12.6875 8.62488 12.6875 7.5C12.6859 5.99207 12.0862 4.54636 11.0199 3.48009C9.95365 2.41382 8.50793 1.81409 7 1.8125ZM7 12.3125C6.04818 12.3125 5.11773 12.0303 4.32632 11.5014C3.53491 10.9726 2.91808 10.221 2.55383 9.34166C2.18959 8.46229 2.09428 7.49466 2.27997 6.56113C2.46566 5.62759 2.92401 4.77009 3.59705 4.09705C4.27009 3.42401 5.1276 2.96566 6.06113 2.77997C6.99466 2.59428 7.9623 2.68958 8.84167 3.05383C9.72104 3.41808 10.4726 4.03491 11.0015 4.82632C11.5303 5.61773 11.8125 6.54818 11.8125 7.5C11.8111 8.77591 11.3036 9.99915 10.4014 10.9014C9.49915 11.8036 8.27591 12.3111 7 12.3125ZM9.625 7.5C9.625 7.61603 9.57891 7.72731 9.49686 7.80936C9.41481 7.89141 9.30353 7.9375 9.1875 7.9375H7.4375V9.6875C7.4375 9.80353 7.39141 9.91481 7.30936 9.99686C7.22731 10.0789 7.11603 10.125 7 10.125C6.88397 10.125 6.77269 10.0789 6.69064 9.99686C6.6086 9.91481 6.5625 9.80353 6.5625 9.6875V7.9375H4.8125C4.69647 7.9375 4.58519 7.89141 4.50314 7.80936C4.4211 7.72731 4.375 7.61603 4.375 7.5C4.375 7.38397 4.4211 7.27269 4.50314 7.19064C4.58519 7.10859 4.69647 7.0625 4.8125 7.0625H6.5625V5.3125C6.5625 5.19647 6.6086 5.08519 6.69064 5.00314C6.77269 4.92109 6.88397 4.875 7 4.875C7.11603 4.875 7.22731 4.92109 7.30936 5.00314C7.39141 5.08519 7.4375 5.19647 7.4375 5.3125V7.0625H9.1875C9.30353 7.0625 9.41481 7.10859 9.49686 7.19064C9.57891 7.27269 9.625 7.38397 9.625 7.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Upload</div>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-wrap gap-[10px] mt-[35px] pt-[20px]',
|
||||
!mediaList.length && 'justify-center items-center text-white'
|
||||
)}
|
||||
>
|
||||
{!mediaList.length && (
|
||||
<div className="flex flex-col text-center">
|
||||
<div>You don{"'"}t have any assets yet.</div>
|
||||
<div>Click the button below to upload one</div>
|
||||
<div className="mt-[10px]">
|
||||
<div className="relative flex gap-2 items-center justify-center">
|
||||
<input
|
||||
type="file"
|
||||
className="absolute left-0 top-0 w-full h-full opacity-0"
|
||||
accept="image/*"
|
||||
onChange={uploadMedia}
|
||||
/>
|
||||
<button className="cursor-pointer font-[500] flex justify-center items-center gap-[4px] text-[12px] rounded-[4px] w-[107px] h-[25px] bg-[#0b0f1c] text-white font-['Inter'] border-[2px] border-[#506490]">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="15"
|
||||
viewBox="0 0 14 15"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 1.8125C5.87512 1.8125 4.7755 2.14607 3.8402 2.77102C2.90489 3.39597 2.17591 4.28423 1.74544 5.32349C1.31496 6.36274 1.20233 7.50631 1.42179 8.60958C1.64124 9.71284 2.18292 10.7263 2.97833 11.5217C3.77374 12.3171 4.78716 12.8588 5.89043 13.0782C6.99369 13.2977 8.13726 13.185 9.17651 12.7546C10.2158 12.3241 11.104 11.5951 11.729 10.6598C12.3539 9.7245 12.6875 8.62488 12.6875 7.5C12.6859 5.99207 12.0862 4.54636 11.0199 3.48009C9.95365 2.41382 8.50793 1.81409 7 1.8125ZM7 12.3125C6.04818 12.3125 5.11773 12.0303 4.32632 11.5014C3.53491 10.9726 2.91808 10.221 2.55383 9.34166C2.18959 8.46229 2.09428 7.49466 2.27997 6.56113C2.46566 5.62759 2.92401 4.77009 3.59705 4.09705C4.27009 3.42401 5.1276 2.96566 6.06113 2.77997C6.99466 2.59428 7.9623 2.68958 8.84167 3.05383C9.72104 3.41808 10.4726 4.03491 11.0015 4.82632C11.5303 5.61773 11.8125 6.54818 11.8125 7.5C11.8111 8.77591 11.3036 9.99915 10.4014 10.9014C9.49915 11.8036 8.27591 12.3111 7 12.3125ZM9.625 7.5C9.625 7.61603 9.57891 7.72731 9.49686 7.80936C9.41481 7.89141 9.30353 7.9375 9.1875 7.9375H7.4375V9.6875C7.4375 9.80353 7.39141 9.91481 7.30936 9.99686C7.22731 10.0789 7.11603 10.125 7 10.125C6.88397 10.125 6.77269 10.0789 6.69064 9.99686C6.6086 9.91481 6.5625 9.80353 6.5625 9.6875V7.9375H4.8125C4.69647 7.9375 4.58519 7.89141 4.50314 7.80936C4.4211 7.72731 4.375 7.61603 4.375 7.5C4.375 7.38397 4.4211 7.27269 4.50314 7.19064C4.58519 7.10859 4.69647 7.0625 4.8125 7.0625H6.5625V5.3125C6.5625 5.19647 6.6086 5.08519 6.69064 5.00314C6.77269 4.92109 6.88397 4.875 7 4.875C7.11603 4.875 7.22731 4.92109 7.30936 5.00314C7.39141 5.08519 7.4375 5.19647 7.4375 5.3125V7.0625H9.1875C9.30353 7.0625 9.41481 7.10859 9.49686 7.19064C9.57891 7.27269 9.625 7.38397 9.625 7.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Upload</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mediaList.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
|
|
@ -140,6 +219,103 @@ export const MediaBox: FC<{
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MultiMediaComponent: FC<{
|
||||
label: string;
|
||||
description: string;
|
||||
value?: Array<{ path: string; id: string }>;
|
||||
name: string;
|
||||
error?: any;
|
||||
onChange: (event: {
|
||||
target: { name: string; value?: Array<{ id: string; path: string }> };
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { name, label, error, description, onChange, value } = props;
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setCurrentMedia(value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [modal, setShowModal] = useState(false);
|
||||
const [currentMedia, setCurrentMedia] = useState(value);
|
||||
const mediaDirectory = useMediaDirectory();
|
||||
|
||||
const changeMedia = useCallback(
|
||||
(m: { path: string; id: string }) => {
|
||||
const newMedia = [...(currentMedia || []), m];
|
||||
setCurrentMedia(newMedia);
|
||||
onChange({ target: { name, value: newMedia } });
|
||||
},
|
||||
[currentMedia]
|
||||
);
|
||||
|
||||
const showModal = useCallback(() => {
|
||||
setShowModal(!modal);
|
||||
}, [modal]);
|
||||
|
||||
const clearMedia = useCallback(
|
||||
(topIndex: number) => () => {
|
||||
const newMedia = currentMedia?.filter((f, index) => index !== topIndex);
|
||||
setCurrentMedia(newMedia);
|
||||
onChange({ target: { name, value: newMedia } });
|
||||
},
|
||||
[currentMedia]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[8px] bg-[#131B2C] rounded-bl-[8px]">
|
||||
{modal && <MediaBox setMedia={changeMedia} closeModal={showModal} />}
|
||||
<div className="flex gap-[10px]">
|
||||
<div className="flex">
|
||||
<Button
|
||||
onClick={showModal}
|
||||
className="ml-[10px] rounded-[4px] mb-[10px] gap-[8px] justify-center items-center w-[127px] flex border border-dashed border-[#506490] bg-[#131B2C]"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px] font-[500]">Insert Media</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!!currentMedia &&
|
||||
currentMedia.map((media, index) => (
|
||||
<>
|
||||
<div className="cursor-pointer w-[40px] h-[40px] border-2 border-tableBorder relative">
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(media.path)}
|
||||
onClick={() => window.open(mediaDirectory.set(media.path))}
|
||||
/>
|
||||
<div
|
||||
onClick={clearMedia(index)}
|
||||
className="rounded-full w-[15px] h-[15px] bg-red-800 text-white flex justify-center items-center absolute -right-[4px] -top-[4px]"
|
||||
>
|
||||
x
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[12px] text-red-400">{error}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MediaComponent: FC<{
|
||||
label: string;
|
||||
description: string;
|
||||
|
|
@ -150,7 +326,7 @@ export const MediaComponent: FC<{
|
|||
}) => void;
|
||||
}> = (props) => {
|
||||
const { name, label, description, onChange, value } = props;
|
||||
const {getValues} = useSettings();
|
||||
const { getValues } = useSettings();
|
||||
useEffect(() => {
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import {NotificationBell, NovuProvider, PopoverNotificationCenter} from "@novu/notification-center";
|
||||
import {useUser} from "@gitroom/frontend/components/layout/user.context";
|
||||
|
||||
export const NotificationComponent = () => {
|
||||
const NotificationComponent = () => {
|
||||
const user = useUser();
|
||||
return (
|
||||
<NovuProvider
|
||||
|
|
@ -15,4 +15,6 @@ export const NotificationComponent = () => {
|
|||
</PopoverNotificationCenter>
|
||||
</NovuProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationComponent;
|
||||
|
|
@ -32,16 +32,34 @@ module.exports = {
|
|||
gridTemplateColumns: {
|
||||
'13': 'repeat(13, minmax(0, 1fr));'
|
||||
},
|
||||
backgroundImage: {
|
||||
loginBox: 'url(/auth/login-box.png)',
|
||||
loginBg: 'url(/auth/bg-login.png)'
|
||||
},
|
||||
animation: {
|
||||
fade: 'fadeOut 0.5s ease-in-out',
|
||||
overflow: 'overFlow 0.5s ease-in-out forwards',
|
||||
overflowReverse: 'overFlowReverse 0.5s ease-in-out forwards',
|
||||
},
|
||||
boxShadow: {
|
||||
yellow: '0 0 60px 20px #6b6237'
|
||||
},
|
||||
|
||||
// that is actual animation
|
||||
keyframes: theme => ({
|
||||
fadeOut: {
|
||||
'0%': { opacity: 0, transform: 'translateY(30px)' },
|
||||
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||
},
|
||||
overFlow: {
|
||||
'0%': { overflow: 'hidden' },
|
||||
'99%': { overflow: 'hidden' },
|
||||
'100%': { overflow: 'visible' },
|
||||
},
|
||||
overFlowReverse: {
|
||||
'0%': { overflow: 'visible' },
|
||||
'99%': { overflow: 'visible' },
|
||||
'100%': { overflow: 'hidden' },
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const customFetch = (params: Params, auth?: string) => {
|
|||
Accept: 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
cache: options.cache || 'no-store',
|
||||
});
|
||||
await params?.afterRequest?.(url, options, fetchRequest);
|
||||
return fetchRequest;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import dayjs from 'dayjs';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class CommentsRepository {
|
||||
constructor(private _media: PrismaRepository<'comments'>) {}
|
||||
|
||||
addAComment(orgId: string, userId: string, comment: string, date: string) {
|
||||
return this._media.model.comments.create({
|
||||
data: {
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
content: comment,
|
||||
date: dayjs(date).toDate(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
addACommentToComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string,
|
||||
date: string
|
||||
) {
|
||||
return this._media.model.comments.create({
|
||||
data: {
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
content: comment,
|
||||
date: dayjs(date).toDate(),
|
||||
parentCommentId: commentId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateAComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string
|
||||
) {
|
||||
return this._media.model.comments.update({
|
||||
where: {
|
||||
id: commentId,
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
},
|
||||
data: {
|
||||
content: comment,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteAComment(orgId: string, userId: string, commentId: string) {
|
||||
return this._media.model.comments.update({
|
||||
where: {
|
||||
id: commentId,
|
||||
organizationId: orgId,
|
||||
userId: userId,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async loadAllCommentsAndSubCommentsForADate(orgId: string, date: string) {
|
||||
return this._media.model.comments.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
deletedAt: null,
|
||||
date: dayjs(date).toDate(),
|
||||
parentCommentId: null,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
childrenComment: {
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getAllCommentsByWeekYear(orgId: string, year: number, week: number) {
|
||||
const date = dayjs().year(year).isoWeek(week);
|
||||
const startDate = date.startOf('isoWeek').toDate();
|
||||
const endDate = date.endOf('isoWeek').toDate();
|
||||
|
||||
const load = await this._media.model.comments.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
deletedAt: null,
|
||||
parentCommentId: null,
|
||||
date: {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
date: true,
|
||||
_count: {
|
||||
select: {
|
||||
childrenComment: {
|
||||
where: {
|
||||
deletedAt: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const group = groupBy(load, (item) =>
|
||||
dayjs(item.date).format('YYYY-MM-DD HH:MM')
|
||||
);
|
||||
|
||||
return Object.values(group).reduce((all, current) => {
|
||||
return [
|
||||
...all,
|
||||
{
|
||||
date: current[0].date,
|
||||
total:
|
||||
current.length +
|
||||
current.reduce(
|
||||
(all2, current2) => all2 + current2._count.childrenComment,
|
||||
0
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [] as Array<{ date: Date; total: number }>);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { CommentsRepository } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CommentsService {
|
||||
constructor(private _commentsRepository: CommentsRepository) {}
|
||||
|
||||
addAComment(orgId: string, userId: string, comment: string, date: string) {
|
||||
return this._commentsRepository.addAComment(orgId, userId, comment, date);
|
||||
}
|
||||
|
||||
addACommentToComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string,
|
||||
date: string
|
||||
) {
|
||||
return this._commentsRepository.addACommentToComment(orgId, userId, commentId, comment, date);
|
||||
}
|
||||
|
||||
updateAComment(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
commentId: string,
|
||||
comment: string
|
||||
) {
|
||||
return this._commentsRepository.updateAComment(
|
||||
orgId,
|
||||
userId,
|
||||
commentId,
|
||||
comment
|
||||
);
|
||||
}
|
||||
|
||||
deleteAComment(orgId: string, userId: string, commentId: string) {
|
||||
return this._commentsRepository.deleteAComment(orgId, userId, commentId);
|
||||
}
|
||||
|
||||
loadAllCommentsAndSubCommentsForADate(orgId: string, date: string) {
|
||||
return this._commentsRepository.loadAllCommentsAndSubCommentsForADate(
|
||||
orgId,
|
||||
date
|
||||
);
|
||||
}
|
||||
|
||||
getAllCommentsByWeekYear(orgId: string, year: number, week: number) {
|
||||
return this._commentsRepository.getAllCommentsByWeekYear(orgId, year, week);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +1,52 @@
|
|||
import {Global, Module} from "@nestjs/common";
|
||||
import {PrismaRepository, PrismaService} from "./prisma.service";
|
||||
import {OrganizationRepository} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository";
|
||||
import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service";
|
||||
import {UsersService} from "@gitroom/nestjs-libraries/database/prisma/users/users.service";
|
||||
import {UsersRepository} from "@gitroom/nestjs-libraries/database/prisma/users/users.repository";
|
||||
import {StarsService} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.service";
|
||||
import {StarsRepository} from "@gitroom/nestjs-libraries/database/prisma/stars/stars.repository";
|
||||
import {SubscriptionService} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service";
|
||||
import {SubscriptionRepository} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository";
|
||||
import {NotificationService} from "@gitroom/nestjs-libraries/notifications/notification.service";
|
||||
import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service";
|
||||
import {IntegrationRepository} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository";
|
||||
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";
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaRepository, PrismaService } from './prisma.service';
|
||||
import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { StarsRepository } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.repository';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { SubscriptionRepository } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/notifications/notification.service';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository';
|
||||
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';
|
||||
import { CommentsRepository } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.repository';
|
||||
import { CommentsService } from '@gitroom/nestjs-libraries/database/prisma/comments/comments.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [
|
||||
PrismaService,
|
||||
PrismaRepository,
|
||||
UsersService,
|
||||
UsersRepository,
|
||||
OrganizationService,
|
||||
OrganizationRepository,
|
||||
StarsService,
|
||||
StarsRepository,
|
||||
SubscriptionService,
|
||||
SubscriptionRepository,
|
||||
NotificationService,
|
||||
IntegrationService,
|
||||
IntegrationRepository,
|
||||
PostsService,
|
||||
PostsRepository,
|
||||
MediaService,
|
||||
MediaRepository,
|
||||
IntegrationManager
|
||||
],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
}
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [
|
||||
PrismaService,
|
||||
PrismaRepository,
|
||||
UsersService,
|
||||
UsersRepository,
|
||||
OrganizationService,
|
||||
OrganizationRepository,
|
||||
StarsService,
|
||||
StarsRepository,
|
||||
SubscriptionService,
|
||||
SubscriptionRepository,
|
||||
NotificationService,
|
||||
IntegrationService,
|
||||
IntegrationRepository,
|
||||
PostsService,
|
||||
PostsRepository,
|
||||
MediaService,
|
||||
MediaRepository,
|
||||
CommentsRepository,
|
||||
CommentsService,
|
||||
IntegrationManager,
|
||||
],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
},
|
||||
})
|
||||
export class DatabaseModule {
|
||||
|
||||
}
|
||||
export class DatabaseModule {}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
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 { Integration, Post } from '@prisma/client';
|
||||
import { 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);
|
||||
|
||||
|
|
@ -28,6 +26,7 @@ export class PostsRepository {
|
|||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
deletedAt: null,
|
||||
parentPostId: null,
|
||||
},
|
||||
select: {
|
||||
|
|
@ -46,11 +45,35 @@ export class PostsRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async deletePost(orgId: string, group: string) {
|
||||
await this._post.model.post.updateMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
group,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return this._post.model.post.findFirst({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
group,
|
||||
parentPostId: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getPost(id: string, includeIntegration = false, orgId?: string) {
|
||||
return this._post.model.post.findUnique({
|
||||
where: {
|
||||
id,
|
||||
...(orgId ? { organizationId: orgId } : {}),
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
...(includeIntegration ? { integration: true } : {}),
|
||||
|
|
@ -84,11 +107,12 @@ export class PostsRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async createOrUpdatePost(orgId: string, date: string, body: PostBody) {
|
||||
async createOrUpdatePost(state: 'draft' | 'schedule', orgId: string, date: string, body: PostBody) {
|
||||
const posts: Post[] = [];
|
||||
const uuid = uuidv4();
|
||||
|
||||
for (const value of body.value) {
|
||||
const updateData = {
|
||||
const updateData = (type: 'create' | 'update') => ({
|
||||
publishDate: dayjs(date).toDate(),
|
||||
integration: {
|
||||
connect: {
|
||||
|
|
@ -96,7 +120,7 @@ export class PostsRepository {
|
|||
organizationId: orgId,
|
||||
},
|
||||
},
|
||||
...(posts.length
|
||||
...(posts?.[posts.length - 1]?.id
|
||||
? {
|
||||
parentPost: {
|
||||
connect: {
|
||||
|
|
@ -104,28 +128,64 @@ export class PostsRepository {
|
|||
},
|
||||
},
|
||||
}
|
||||
: type === 'update'
|
||||
? {
|
||||
parentPost: {
|
||||
disconnect: true,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
content: value.content,
|
||||
group: uuid,
|
||||
state: state === 'draft' ? 'DRAFT' as const : 'QUEUE' as const,
|
||||
image: JSON.stringify(value.image),
|
||||
settings: JSON.stringify(body.settings),
|
||||
organization: {
|
||||
connect: {
|
||||
id: orgId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
posts.push(
|
||||
await this._post.model.post.upsert({
|
||||
where: {
|
||||
id: value.id || uuidv4()
|
||||
id: value.id || uuidv4(),
|
||||
},
|
||||
create: updateData,
|
||||
update: updateData,
|
||||
create: updateData('create'),
|
||||
update: updateData('update'),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return posts;
|
||||
const previousPost = body.group
|
||||
? (
|
||||
await this._post.model.post.findFirst({
|
||||
where: {
|
||||
group: body.group,
|
||||
deletedAt: null,
|
||||
parentPostId: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
)?.id!
|
||||
: undefined;
|
||||
|
||||
if (body.group) {
|
||||
await this._post.model.post.updateMany({
|
||||
where: {
|
||||
group: body.group,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: {
|
||||
parentPostId: null,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { previousPost, posts };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,11 @@ export class PostsService {
|
|||
async getPost(orgId: string, id: string) {
|
||||
const posts = await this.getPostsRecursively(id, false, orgId);
|
||||
return {
|
||||
posts,
|
||||
group: posts?.[0]?.group,
|
||||
posts: posts.map((post) => ({
|
||||
...post,
|
||||
image: JSON.parse(post.image || '[]'),
|
||||
})),
|
||||
integration: posts[0].integrationId,
|
||||
settings: JSON.parse(posts[0].settings || '{}'),
|
||||
};
|
||||
|
|
@ -109,25 +113,40 @@ export class PostsService {
|
|||
await this._postRepository.updatePost(posts[0].id, postId, releaseURL);
|
||||
}
|
||||
|
||||
async deletePost(orgId: string, group: string) {
|
||||
const post = await this._postRepository.deletePost(orgId, group);
|
||||
if (post?.id) {
|
||||
await this._workerServiceProducer.delete('post', post.id);
|
||||
}
|
||||
}
|
||||
|
||||
async createPost(orgId: string, body: CreatePostDto) {
|
||||
for (const post of body.posts) {
|
||||
const posts = await this._postRepository.createOrUpdatePost(
|
||||
orgId,
|
||||
body.date,
|
||||
post
|
||||
);
|
||||
const { previousPost, posts } =
|
||||
await this._postRepository.createOrUpdatePost(
|
||||
body.type,
|
||||
orgId,
|
||||
body.date,
|
||||
post
|
||||
);
|
||||
|
||||
await this._workerServiceProducer.delete('post', posts[0].id);
|
||||
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: posts[0].id,
|
||||
options: {
|
||||
delay: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: posts[0].id,
|
||||
},
|
||||
});
|
||||
if (posts?.length) {
|
||||
await this._workerServiceProducer.delete(
|
||||
'post',
|
||||
previousPost ? previousPost : posts?.[0]?.id
|
||||
);
|
||||
if (body.type === 'schedule') {
|
||||
// this._workerServiceProducer.emit('post', {
|
||||
// id: posts[0].id,
|
||||
// options: {
|
||||
// delay: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
|
||||
// },
|
||||
// payload: {
|
||||
// id: posts[0].id,
|
||||
// },
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,8 @@ model Organization {
|
|||
github GitHub[]
|
||||
subscription Subscription?
|
||||
Integration Integration[]
|
||||
tags Tag[]
|
||||
postTags PostTag[]
|
||||
postMedia PostMedia[]
|
||||
post Post[]
|
||||
slots Slots[]
|
||||
Comments Comments[]
|
||||
}
|
||||
|
||||
model User {
|
||||
|
|
@ -37,6 +34,7 @@ model User {
|
|||
providerId String?
|
||||
organizations UserOrganization[]
|
||||
timezone Int
|
||||
comments Comments[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
|
@ -52,6 +50,9 @@ model UserOrganization {
|
|||
role Role @default(USER)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([organizationId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model GitHub {
|
||||
|
|
@ -66,6 +67,7 @@ model GitHub {
|
|||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([login])
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model Trending {
|
||||
|
|
@ -78,6 +80,7 @@ model Trending {
|
|||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([language])
|
||||
@@index([hash])
|
||||
}
|
||||
|
||||
model TrendingLog {
|
||||
|
|
@ -104,9 +107,10 @@ model Media {
|
|||
path String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
organizationId String
|
||||
posts PostMedia[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
|
|
@ -120,6 +124,8 @@ model Subscription {
|
|||
period Period
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
model Integration {
|
||||
|
|
@ -139,46 +145,26 @@ model Integration {
|
|||
@@unique([organizationId, internalId])
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
name String
|
||||
posts PostTag[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
model Comments {
|
||||
id String @id @default(uuid())
|
||||
content String
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
date DateTime
|
||||
parentCommentId String?
|
||||
parentComment Comments? @relation("parentCommentId", fields: [parentCommentId], references: [id])
|
||||
childrenComment Comments[] @relation("parentCommentId")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
model PostTag {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
postId String
|
||||
post Post @relation(fields: [postId], references: [id])
|
||||
tagId String
|
||||
tag Tag @relation(fields: [tagId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model PostMedia {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
postId String
|
||||
post Post @relation(fields: [postId], references: [id])
|
||||
mediaId String
|
||||
media Media @relation(fields: [mediaId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Slots {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
day Int
|
||||
time Int
|
||||
@@index([createdAt])
|
||||
@@index([organizationId])
|
||||
@@index([date])
|
||||
@@index([userId])
|
||||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model Post {
|
||||
|
|
@ -188,6 +174,7 @@ model Post {
|
|||
organizationId String
|
||||
integrationId String
|
||||
content String
|
||||
group String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
integration Integration @relation(fields: [integrationId], references: [id])
|
||||
title String?
|
||||
|
|
@ -198,10 +185,17 @@ model Post {
|
|||
settings String?
|
||||
parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id])
|
||||
childrenPost Post[] @relation("parentPostId")
|
||||
tags PostTag[]
|
||||
media PostMedia[]
|
||||
image String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@index([group])
|
||||
@@index([deletedAt])
|
||||
@@index([publishDate])
|
||||
@@index([state])
|
||||
@@index([organizationId])
|
||||
@@index([parentPostId])
|
||||
}
|
||||
|
||||
enum State {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export class AddCommentDto {
|
||||
content: string;
|
||||
date: string;
|
||||
}
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
import {
|
||||
ArrayMinSize, IsArray, IsDateString, IsDefined, IsOptional, IsString, ValidateNested,
|
||||
ArrayMinSize, IsArray, IsDateString, IsDefined, IsIn, IsOptional, IsString, MinLength, ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
|
||||
import {MediaDto} from "@gitroom/nestjs-libraries/dtos/media/media.dto";
|
||||
import {AllProvidersSettings} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings";
|
||||
import {MediumSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto";
|
||||
import {HashnodeSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto";
|
||||
import {RedditSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto";
|
||||
|
||||
export class EmptySettings {}
|
||||
export class Integration {
|
||||
|
|
@ -14,11 +19,18 @@ export class Integration {
|
|||
export class PostContent {
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
content: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@Type(() => MediaDto)
|
||||
@ValidateNested({each: true})
|
||||
image: MediaDto[]
|
||||
}
|
||||
|
||||
export class Post {
|
||||
|
|
@ -34,17 +46,31 @@ export class Post {
|
|||
@ValidateNested({ each: true })
|
||||
value: PostContent[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
group: string;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EmptySettings, {
|
||||
keepDiscriminatorProperty: false,
|
||||
discriminator: {
|
||||
property: '__type',
|
||||
subTypes: [{ value: DevToSettingsDto, name: 'devto' }],
|
||||
subTypes: [
|
||||
{ value: DevToSettingsDto, name: 'devto' },
|
||||
{ value: MediumSettingsDto, name: 'medium' },
|
||||
{ value: HashnodeSettingsDto, name: 'hashnode' },
|
||||
{ value: RedditSettingsDto, name: 'reddit' },
|
||||
],
|
||||
},
|
||||
})
|
||||
settings: DevToSettingsDto;
|
||||
settings: AllProvidersSettings;
|
||||
}
|
||||
|
||||
export class CreatePostDto {
|
||||
@IsDefined()
|
||||
@IsIn(['draft', 'schedule'])
|
||||
type: 'draft' | 'schedule';
|
||||
|
||||
@IsDefined()
|
||||
@IsDateString()
|
||||
date: string;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,10 @@
|
|||
import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto";
|
||||
export type AllProvidersSettings = DevToSettingsDto;
|
||||
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
|
||||
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
|
||||
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
|
||||
import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto';
|
||||
|
||||
export type AllProvidersSettings =
|
||||
| DevToSettingsDto
|
||||
| MediumSettingsDto
|
||||
| HashnodeSettingsDto
|
||||
| RedditSettingsDto;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsDefined,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto';
|
||||
|
||||
export class HashnodeTagsSettings {
|
||||
@IsString()
|
||||
value: string;
|
||||
|
||||
@IsString()
|
||||
label: string;
|
||||
}
|
||||
|
||||
export class HashnodeSettingsDto {
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
@IsDefined()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsOptional()
|
||||
subtitle: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => MediaDto)
|
||||
main_image?: MediaDto;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@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',
|
||||
}
|
||||
)
|
||||
canonical?: string;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
publication?: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
tags: HashnodeTagsSettings[];
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import {ArrayMaxSize, IsArray, IsDefined, IsOptional, IsString, Matches, MinLength, ValidateNested} from "class-validator";
|
||||
|
||||
export class MediumTagsSettings {
|
||||
@IsString()
|
||||
value: string;
|
||||
|
||||
@IsString()
|
||||
label: string;
|
||||
}
|
||||
|
||||
export class MediumSettingsDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsDefined()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsDefined()
|
||||
subtitle: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@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'
|
||||
})
|
||||
canonical?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
publication?: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMaxSize(4)
|
||||
@IsOptional()
|
||||
tags: MediumTagsSettings[];
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
ArrayMinSize,
|
||||
IsBoolean,
|
||||
IsDefined,
|
||||
IsString,
|
||||
IsUrl,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class RedditFlairDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class RedditSettingsDtoInner {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsDefined()
|
||||
subreddit: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsDefined()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsDefined()
|
||||
type: string;
|
||||
|
||||
@ValidateIf((e) => e.type === 'link')
|
||||
@IsUrl()
|
||||
@IsDefined()
|
||||
url: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsDefined()
|
||||
is_flair_required: boolean;
|
||||
|
||||
@ValidateIf((e) => e.is_flair_required)
|
||||
@IsDefined()
|
||||
@ValidateNested()
|
||||
flair: RedditFlairDto;
|
||||
|
||||
@ValidateIf((e) => e.type === 'media')
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MediaDto)
|
||||
@ArrayMinSize(1)
|
||||
media: MediaDto[];
|
||||
}
|
||||
|
||||
export class RedditSettingsValueDto {
|
||||
@Type(() => RedditSettingsDtoInner)
|
||||
@IsDefined()
|
||||
@ValidateNested()
|
||||
value: RedditSettingsDtoInner;
|
||||
}
|
||||
|
||||
export class RedditSettingsDto {
|
||||
@Type(() => RedditSettingsValueDto)
|
||||
@ValidateNested({ each: true })
|
||||
@ArrayMinSize(1)
|
||||
subreddit: RedditSettingsValueDto[];
|
||||
}
|
||||
|
|
@ -1,18 +1,26 @@
|
|||
import {ArticleIntegrationsInterface, ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface";
|
||||
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
|
||||
import { tags } from '@gitroom/nestjs-libraries/integrations/article/hashnode.tags';
|
||||
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
||||
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
|
||||
|
||||
export class HashnodeProvider implements ArticleProvider {
|
||||
identifier = 'hashnode';
|
||||
name = 'Hashnode';
|
||||
async authenticate(token: string) {
|
||||
try {
|
||||
const {data: {me: {name, id, profilePicture}}} = await (await fetch('https://gql.hashnode.com', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
identifier = 'hashnode';
|
||||
name = 'Hashnode';
|
||||
async authenticate(token: string) {
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
me: { name, id, profilePicture },
|
||||
},
|
||||
} = await (
|
||||
await fetch('https://gql.hashnode.com', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query {
|
||||
me {
|
||||
name,
|
||||
|
|
@ -20,28 +28,125 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
profilePicture
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
})).json();
|
||||
`,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id, name, token, picture: profilePicture
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
token: '',
|
||||
picture: ''
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
token,
|
||||
picture: profilePicture,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
token: '',
|
||||
picture: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async post(token: string, content: string, settings: object) {
|
||||
return {
|
||||
postId: '123',
|
||||
releaseURL: 'https://dev.to'
|
||||
}
|
||||
}
|
||||
}
|
||||
async tags() {
|
||||
return tags.map((tag) => ({ value: tag.objectID, label: tag.name }));
|
||||
}
|
||||
|
||||
async publications(token: string) {
|
||||
const {
|
||||
data: {
|
||||
me: {
|
||||
publications: { edges },
|
||||
},
|
||||
},
|
||||
} = await (
|
||||
await fetch('https://gql.hashnode.com', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query {
|
||||
me {
|
||||
publications (first: 50) {
|
||||
edges{
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return edges.map(
|
||||
({ node: { id, title } }: { node: { id: string; title: string } }) => ({
|
||||
id,
|
||||
name: title,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async post(token: string, content: string, settings: HashnodeSettingsDto) {
|
||||
const query = jsonToGraphQLQuery({
|
||||
mutation: {
|
||||
publishPost: {
|
||||
__args: {
|
||||
input: {
|
||||
title: settings.title,
|
||||
publicationId: settings.publication,
|
||||
...(settings.canonical
|
||||
? { originalArticleURL: settings.canonical }
|
||||
: {}),
|
||||
contentMarkdown: content,
|
||||
tags: settings.tags.map((tag) => ({ id: tag.value })),
|
||||
...(settings.subtitle ? { subtitle: settings.subtitle } : {}),
|
||||
...(settings.main_image
|
||||
? {
|
||||
coverImageOptions: {
|
||||
coverImageURL: `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}${settings?.main_image?.path}`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
post: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {pretty: true});
|
||||
|
||||
const {
|
||||
data: {
|
||||
publishPost: {
|
||||
post: { id, url },
|
||||
},
|
||||
},
|
||||
} = await (
|
||||
await fetch('https://gql.hashnode.com', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
postId: id,
|
||||
releaseURL: url,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,28 +1,73 @@
|
|||
import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/article.integrations.interface";
|
||||
import { ArticleProvider } from '@gitroom/nestjs-libraries/integrations/article/article.integrations.interface';
|
||||
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
|
||||
|
||||
export class MediumProvider implements ArticleProvider {
|
||||
identifier = 'medium';
|
||||
name = 'Medium';
|
||||
identifier = 'medium';
|
||||
name = 'Medium';
|
||||
|
||||
async authenticate(token: string) {
|
||||
const {data: {name, id, imageUrl}} = await (await fetch('https://api.medium.com/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})).json();
|
||||
async authenticate(token: string) {
|
||||
const {
|
||||
data: { name, id, imageUrl },
|
||||
} = await (
|
||||
await fetch('https://api.medium.com/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
token,
|
||||
picture: imageUrl
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
token,
|
||||
picture: imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async publications(token: string) {
|
||||
const { id } = await this.authenticate(token);
|
||||
const { data } = await (
|
||||
await fetch(`https://api.medium.com/v1/users/${id}/publications`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async post(token: string, content: string, settings: MediumSettingsDto) {
|
||||
const { id } = await this.authenticate(token);
|
||||
const { data, ...all } = await (
|
||||
await fetch(
|
||||
settings?.publication
|
||||
? `https://api.medium.com/v1/publications/${settings?.publication}/posts`
|
||||
: `https://api.medium.com/v1/users/${id}/posts`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: settings.title,
|
||||
contentFormat: 'markdown',
|
||||
content,
|
||||
...(settings.canonical ? { canonicalUrl: settings.canonical } : {}),
|
||||
...(settings?.tags?.length
|
||||
? { tags: settings?.tags?.map(p => p.value) }
|
||||
: {}),
|
||||
publishStatus: settings?.publication ? 'draft' : 'public',
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
async post(token: string, content: string, settings: object) {
|
||||
return {
|
||||
postId: '123',
|
||||
releaseURL: 'https://dev.to'
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(all);
|
||||
return {
|
||||
postId: data.id,
|
||||
releaseURL: data.url,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,96 +1,217 @@
|
|||
import {AuthTokenDetails, PostDetails, PostResponse, SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface";
|
||||
import {makeId} from "@gitroom/nestjs-libraries/services/make.is";
|
||||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
||||
export class RedditProvider implements SocialProvider {
|
||||
identifier = 'reddit';
|
||||
name = 'Reddit';
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const {access_token: accessToken, refresh_token: newRefreshToken, expires_in: expiresIn} = await (await fetch('https://www.reddit.com/api/v1/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${Buffer.from(`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`).toString('base64')}`
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken
|
||||
})
|
||||
})).json();
|
||||
identifier = 'reddit';
|
||||
name = 'Reddit';
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const {
|
||||
access_token: accessToken,
|
||||
refresh_token: newRefreshToken,
|
||||
expires_in: expiresIn,
|
||||
} = await (
|
||||
await fetch('https://www.reddit.com/api/v1/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`
|
||||
).toString('base64')}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
const {name, id, icon_img} = await (await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).json();
|
||||
const { name, id, icon_img } = await (
|
||||
await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0]
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0],
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
const codeVerifier = makeId(30);
|
||||
const url = `https://www.reddit.com/api/v1/authorize?client_id=${
|
||||
process.env.REDDIT_CLIENT_ID
|
||||
}&response_type=code&state=${state}&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/reddit`
|
||||
)}&duration=permanent&scope=${encodeURIComponent(
|
||||
'read identity submit flair'
|
||||
)}`;
|
||||
return {
|
||||
url,
|
||||
codeVerifier,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: { code: string; codeVerifier: string }) {
|
||||
const {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
expires_in: expiresIn,
|
||||
} = await (
|
||||
await fetch('https://www.reddit.com/api/v1/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`
|
||||
).toString('base64')}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: params.code,
|
||||
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/reddit`,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
const { name, id, icon_img } = await (
|
||||
await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0],
|
||||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [post, ...rest] = postDetails;
|
||||
const response = await fetch('https://oauth.reddit.com/api/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
title: 'test',
|
||||
kind: 'self',
|
||||
text: post.message,
|
||||
sr: '/r/gitroom',
|
||||
}),
|
||||
});
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async subreddits(accessToken: string, data: any) {
|
||||
const {
|
||||
data: { children },
|
||||
} = await (
|
||||
await fetch(
|
||||
`https://oauth.reddit.com/subreddits/search?show=public&q=${data.word}&sort=activity&show_users=false&limit=10`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
console.log(children);
|
||||
return children.filter(({data} : {data: any}) => data.subreddit_type === "public").map(({ data: { title, url, id } }: any) => ({
|
||||
title,
|
||||
name: url,
|
||||
id,
|
||||
}));
|
||||
}
|
||||
|
||||
private getPermissions(submissionType: string, allow_images: string) {
|
||||
const permissions = [];
|
||||
if (['any', 'self'].indexOf(submissionType) > -1) {
|
||||
permissions.push('self');
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
const codeVerifier = makeId(30);
|
||||
const url = `https://www.reddit.com/api/v1/authorize?client_id=${process.env.REDDIT_CLIENT_ID}&response_type=code&state=${state}&redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/integrations/social/reddit`)}&duration=permanent&scope=${encodeURIComponent('identity submit flair')}`;
|
||||
return {
|
||||
url,
|
||||
codeVerifier,
|
||||
state
|
||||
}
|
||||
if (['any', 'link'].indexOf(submissionType) > -1) {
|
||||
permissions.push('link');
|
||||
}
|
||||
|
||||
async authenticate(params: {code: string, codeVerifier: string}) {
|
||||
const {access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn} = await (await fetch('https://www.reddit.com/api/v1/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${Buffer.from(`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`).toString('base64')}`
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: params.code,
|
||||
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/reddit`
|
||||
})
|
||||
})).json();
|
||||
|
||||
const {name, id, icon_img} = await (await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0]
|
||||
}
|
||||
if (submissionType === "any" || allow_images) {
|
||||
permissions.push('media');
|
||||
}
|
||||
|
||||
async post(id: string, accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> {
|
||||
const [post, ...rest] = postDetails;
|
||||
const response = await fetch('https://oauth.reddit.com/api/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
title: 'test',
|
||||
kind: 'self',
|
||||
text: post.message,
|
||||
sr: '/r/gitroom'
|
||||
})
|
||||
});
|
||||
return permissions;
|
||||
}
|
||||
|
||||
console.log(response);
|
||||
return [];
|
||||
async restrictions(accessToken: string, data: { subreddit: string }) {
|
||||
const {
|
||||
data: { submission_type, allow_images },
|
||||
} = await (
|
||||
await fetch(`https://oauth.reddit.com/${data.subreddit}/about`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
const {
|
||||
is_flair_required,
|
||||
} = await (
|
||||
await fetch(`https://oauth.reddit.com/api/v1/${data.subreddit.split('/r/')[1]}/post_requirements`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
const newData = await (
|
||||
await fetch(`https://oauth.reddit.com/${data.subreddit}/api/link_flair_v2`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
subreddit: data.subreddit,
|
||||
allow: this.getPermissions(submission_type, allow_images),
|
||||
is_flair_required,
|
||||
flairs: newData?.map?.((p: any) => ({
|
||||
id: p.id,
|
||||
name: p.text
|
||||
})) || []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ export class XProvider implements SocialProvider {
|
|||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
): Promise<PostResponse[]> {
|
||||
console.log('hello');
|
||||
const client = new TwitterApi(accessToken);
|
||||
const {data: {username}} = await client.v2.me({
|
||||
"user.fields": "username"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ import {clsx} from "clsx";
|
|||
|
||||
export const Button: FC<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {secondary?: boolean}> = (props) => {
|
||||
return (
|
||||
<button {...props} type={props.type || 'button'} className={clsx(`${props.secondary ? 'bg-sixth' : 'bg-forth'} px-[24px] h-[40px] cursor-pointer items-center justify-center flex`, props?.className)} />
|
||||
<button {...props} type={props.type || 'button'} className={clsx(props.disabled && 'opacity-50 pointer-events-none' ,`${props.secondary ? 'bg-third' : 'bg-forth'} px-[24px] h-[40px] cursor-pointer items-center justify-center flex`, props?.className)} />
|
||||
)
|
||||
}
|
||||
|
|
@ -4,18 +4,19 @@ import {DetailedHTMLProps, FC, InputHTMLAttributes, useMemo} from "react";
|
|||
import clsx from "clsx";
|
||||
import {useFormContext} from "react-hook-form";
|
||||
|
||||
export const Input: FC<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {label: string, name: string}> = (props) => {
|
||||
const {label, className, ...rest} = props;
|
||||
export const Input: FC<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => {
|
||||
const {label, className, disableForm, error, ...rest} = props;
|
||||
const form = useFormContext();
|
||||
const err = useMemo(() => {
|
||||
if (error) return error;
|
||||
if (!form || !form.formState.errors[props?.name!]) return;
|
||||
return form?.formState?.errors?.[props?.name!]?.message! as string;
|
||||
}, [form?.formState?.errors?.[props?.name!]?.message]);
|
||||
}, [form?.formState?.errors?.[props?.name!]?.message, error]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="font-['Inter'] text-[14px]">{label}</div>
|
||||
<input {...form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} />
|
||||
<input {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} />
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,18 +4,19 @@ import {DetailedHTMLProps, FC, SelectHTMLAttributes, useMemo} from "react";
|
|||
import {clsx} from "clsx";
|
||||
import {useFormContext} from "react-hook-form";
|
||||
|
||||
export const Select: FC<DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement> & {label: string, name: string}> = (props) => {
|
||||
const {label, className, ...rest} = props;
|
||||
export const Select: FC<DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => {
|
||||
const {label, className, disableForm, error, ...rest} = props;
|
||||
const form = useFormContext();
|
||||
const err = useMemo(() => {
|
||||
if (error) return error;
|
||||
if (!form || !form.formState.errors[props?.name!]) return;
|
||||
return form?.formState?.errors?.[props?.name!]?.message! as string;
|
||||
}, [form?.formState?.errors?.[props?.name!]?.message]);
|
||||
}, [form?.formState?.errors?.[props?.name!]?.message, error]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="font-['Inter'] text-[14px]">{label}</div>
|
||||
<select {...form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} />
|
||||
<select {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input h-[44px] px-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} />
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import {DetailedHTMLProps, FC, InputHTMLAttributes, useMemo} from "react";
|
||||
// @ts-ignore
|
||||
import clsx from "clsx";
|
||||
import {useFormContext} from "react-hook-form";
|
||||
|
||||
export const Textarea: FC<DetailedHTMLProps<InputHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement> & {error?: any, disableForm?: boolean, label: string, name: string}> = (props) => {
|
||||
const {label, className, disableForm, error, ...rest} = props;
|
||||
const form = useFormContext();
|
||||
const err = useMemo(() => {
|
||||
if (error) return error;
|
||||
if (!form || !form.formState.errors[props?.name!]) return;
|
||||
return form?.formState?.errors?.[props?.name!]?.message! as string;
|
||||
}, [form?.formState?.errors?.[props?.name!]?.message, error]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="font-['Inter'] text-[14px]">{label}</div>
|
||||
<textarea {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input min-h-[150px] p-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} />
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
"cookie-parser": "^1.4.6",
|
||||
"dayjs": "^1.11.10",
|
||||
"ioredis": "^5.3.2",
|
||||
"json-to-graphql-query": "^2.2.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
|
|
@ -60,6 +61,7 @@
|
|||
"react-router-dom": "6.11.2",
|
||||
"react-tag-autocomplete": "^7.2.0",
|
||||
"react-tooltip": "^5.26.2",
|
||||
"react-use-keypress": "^1.3.1",
|
||||
"redis": "^4.6.12",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"remove-markdown": "^0.5.0",
|
||||
|
|
@ -70,6 +72,7 @@
|
|||
"swr": "^2.2.5",
|
||||
"tslib": "^2.3.0",
|
||||
"twitter-api-v2": "^1.16.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -15954,6 +15957,11 @@
|
|||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json-to-graphql-query": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/json-to-graphql-query/-/json-to-graphql-query-2.2.5.tgz",
|
||||
"integrity": "sha512-5Nom9inkIMrtY992LMBBG1Zaekrc10JaRhyZgprwHBVMDtRgllTvzl0oBbg13wJsVZoSoFNNMaeIVQs0P04vsA=="
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
|
|
@ -20045,6 +20053,17 @@
|
|||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-keypress": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-use-keypress/-/react-use-keypress-1.3.1.tgz",
|
||||
"integrity": "sha512-fo+LQrxviMcZt7efCFPc6CX9/oNEPD+MJ/qSs4nK3/lyRNtquhG9f1J8GQq2VFfIYUVDUdPKz8fGIwErO1Pcuw==",
|
||||
"dependencies": {
|
||||
"tiny-invariant": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || 17 || 18"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
|
@ -22430,6 +22449,11 @@
|
|||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.2.tgz",
|
||||
"integrity": "sha512-oLXoWt7bk7SI3REp16Hesm0kTBTErhk+FWTvuujYMlIbX42bb3yLN98T3OyzFNkZ3WAjVYDL4sWykCR6kD2mqQ=="
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz",
|
||||
|
|
@ -23257,6 +23281,17 @@
|
|||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz",
|
||||
"integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"cookie-parser": "^1.4.6",
|
||||
"dayjs": "^1.11.10",
|
||||
"ioredis": "^5.3.2",
|
||||
"json-to-graphql-query": "^2.2.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
|
|
@ -60,6 +61,7 @@
|
|||
"react-router-dom": "6.11.2",
|
||||
"react-tag-autocomplete": "^7.2.0",
|
||||
"react-tooltip": "^5.26.2",
|
||||
"react-use-keypress": "^1.3.1",
|
||||
"redis": "^4.6.12",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"remove-markdown": "^0.5.0",
|
||||
|
|
@ -70,6 +72,7 @@
|
|||
"swr": "^2.2.5",
|
||||
"tslib": "^2.3.0",
|
||||
"twitter-api-v2": "^1.16.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue