feat: a bunch of cose
This commit is contained in:
parent
bfa36a0dcf
commit
c215375bea
|
|
@ -14,7 +14,8 @@
|
|||
"rules": {
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off"
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"react/display-name": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,15 +11,21 @@ import {PermissionsService} from "@gitroom/backend/services/auth/permissions/per
|
|||
import {IntegrationsController} from "@gitroom/backend/api/routes/integrations.controller";
|
||||
import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integration.manager";
|
||||
import {SettingsController} from "@gitroom/backend/api/routes/settings.controller";
|
||||
import {BullMqModule} from "@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module";
|
||||
import {ioRedis} from "@gitroom/nestjs-libraries/redis/redis.service";
|
||||
import {PostsController} from "@gitroom/backend/api/routes/posts.controller";
|
||||
|
||||
const authenticatedController = [
|
||||
UsersController,
|
||||
AnalyticsController,
|
||||
IntegrationsController,
|
||||
SettingsController
|
||||
SettingsController,
|
||||
PostsController
|
||||
];
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [BullMqModule.forRoot({
|
||||
connection: ioRedis
|
||||
})],
|
||||
controllers: [StripeController, AuthController, ...authenticatedController],
|
||||
providers: [
|
||||
AuthService,
|
||||
|
|
@ -27,7 +33,7 @@ const authenticatedController = [
|
|||
AuthMiddleware,
|
||||
PoliciesGuard,
|
||||
PermissionsService,
|
||||
IntegrationManager
|
||||
IntegrationManager,
|
||||
],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export class AuthController {
|
|||
@Res({ passthrough: true }) response: Response
|
||||
) {
|
||||
try {
|
||||
console.log('heghefrgefg');
|
||||
const jwt = await this._authService.routeAuth(body.provider, body);
|
||||
response.cookie('auth', jwt, {
|
||||
domain: '.' + new URL(process.env.FRONTEND_URL!).hostname,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {IntegrationManager} from "@gitroom/nestjs-libraries/integrations/integra
|
|||
import {IntegrationService} from "@gitroom/nestjs-libraries/database/prisma/integrations/integration.service";
|
||||
import {GetOrgFromRequest} from "@gitroom/nestjs-libraries/user/org.from.request";
|
||||
import {Organization} from "@prisma/client";
|
||||
import {ApiKeyDto} from "@gitroom/nestjs-libraries/dtos/integrations/api.key.dto";
|
||||
|
||||
@Controller('/integrations')
|
||||
export class IntegrationsController {
|
||||
|
|
@ -13,6 +14,18 @@ export class IntegrationsController {
|
|||
private _integrationService: IntegrationService
|
||||
) {
|
||||
}
|
||||
@Get('/')
|
||||
getIntegration() {
|
||||
return this._integrationManager.getAllIntegrations();
|
||||
}
|
||||
|
||||
@Get('/list')
|
||||
async getIntegrationList(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
) {
|
||||
return {integrations: (await this._integrationService.getIntegrationsList(org.id)).map(p => ({name: p.name, id: p.id, picture: p.picture, identifier: p.providerIdentifier, type: p.type}))};
|
||||
}
|
||||
|
||||
@Get('/social/:integration')
|
||||
async getIntegrationUrl(
|
||||
@Param('integration') integration: string
|
||||
|
|
@ -25,14 +38,14 @@ export class IntegrationsController {
|
|||
const {codeVerifier, state, url} = await integrationProvider.generateAuthUrl();
|
||||
await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 300);
|
||||
|
||||
return url;
|
||||
return {url};
|
||||
}
|
||||
|
||||
@Post('/article/:integration/connect')
|
||||
async connectArticle(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('integration') integration: string,
|
||||
@Body('code') api: string
|
||||
@Body() api: ApiKeyDto
|
||||
) {
|
||||
if (!this._integrationManager.getAllowedArticlesIntegrations().includes(integration)) {
|
||||
throw new Error('Integration not allowed');
|
||||
|
|
@ -43,13 +56,13 @@ export class IntegrationsController {
|
|||
}
|
||||
|
||||
const integrationProvider = this._integrationManager.getArticlesIntegration(integration);
|
||||
const {id, name, token} = await integrationProvider.authenticate(api);
|
||||
const {id, name, token, picture} = await integrationProvider.authenticate(api.api);
|
||||
|
||||
if (!id) {
|
||||
throw new Error('Invalid api key');
|
||||
}
|
||||
|
||||
return this._integrationService.createIntegration(org.id, name, 'article', String(id), integration, token);
|
||||
return this._integrationService.createIntegration(org.id, name, picture,'article', String(id), integration, token);
|
||||
}
|
||||
|
||||
@Post('/social/:integration/connect')
|
||||
|
|
@ -68,7 +81,7 @@ export class IntegrationsController {
|
|||
}
|
||||
|
||||
const integrationProvider = this._integrationManager.getSocialIntegration(integration);
|
||||
const {accessToken, expiresIn, refreshToken, id, name} = await integrationProvider.authenticate({
|
||||
const {accessToken, expiresIn, refreshToken, id, name, picture} = await integrationProvider.authenticate({
|
||||
code: body.code,
|
||||
codeVerifier: getCodeVerifier
|
||||
});
|
||||
|
|
@ -77,6 +90,6 @@ export class IntegrationsController {
|
|||
throw new Error('Invalid api key');
|
||||
}
|
||||
|
||||
return this._integrationService.createIntegration(org.id, name, 'social', String(id), integration, accessToken, refreshToken, expiresIn);
|
||||
return this._integrationService.createIntegration(org.id, name, picture, 'social', String(id), integration, accessToken, refreshToken, expiresIn);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import {Body, Controller, Post} 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";
|
||||
|
||||
@Controller('/posts')
|
||||
export class PostsController {
|
||||
constructor(
|
||||
private _postsService: PostsService
|
||||
) {
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
createPost(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: CreatePostDto
|
||||
) {
|
||||
return this._postsService.createPost(org.id, body);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
process.env.TZ='UTC';
|
||||
|
||||
import cookieParser from 'cookie-parser';
|
||||
import {Logger, ValidationPipe} from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 983 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,5 +1,9 @@
|
|||
import {LaunchesComponent} from "@gitroom/frontend/components/launches/launches.component";
|
||||
import {internalFetch} from "@gitroom/helpers/utils/internal.fetch";
|
||||
|
||||
export default async function Index() {
|
||||
const {integrations} = await (await internalFetch('/integrations/list')).json();
|
||||
return (
|
||||
<>asd</>
|
||||
<LaunchesComponent integrations={integrations} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import '../global.css';
|
||||
import {LayoutSettings} from "@gitroom/frontend/components/layout/layout.settings";
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/*body, html {*/
|
||||
/* overflow-x: hidden;*/
|
||||
/*}*/
|
||||
body, html {
|
||||
background-color: black;
|
||||
}
|
||||
.box {
|
||||
position: relative;
|
||||
padding: 8px 24px;
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
}
|
||||
|
||||
.table1 thead {
|
||||
background-color: #111423;
|
||||
background-color: #0F1524;
|
||||
height: 44px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #28344F;
|
||||
|
|
@ -55,4 +55,22 @@
|
|||
padding: 16px 24px;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.swal2-modal {
|
||||
background-color: black !important;
|
||||
border: 2px solid #0B101B;
|
||||
}
|
||||
|
||||
.swal2-modal * {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.swal2-icon {
|
||||
color: white !important;
|
||||
border-color: white !important;
|
||||
}
|
||||
|
||||
.swal2-confirm {
|
||||
background-color: #262373 !important;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import './global.css';
|
||||
|
||||
import LayoutContext from "@gitroom/frontend/components/layout/layout.context";
|
||||
import {ReactNode} from "react";
|
||||
import {Chakra_Petch} from "next/font/google";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,349 @@
|
|||
'use client';
|
||||
|
||||
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 { 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';
|
||||
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';
|
||||
|
||||
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: 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;
|
||||
|
||||
// selected integrations to allow edit
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useState<
|
||||
Integrations[]
|
||||
>([]);
|
||||
|
||||
// value of each editor
|
||||
const [value, setValue] = useState<string[]>(['']);
|
||||
|
||||
const fetch = useFetch();
|
||||
|
||||
// prevent the window exit by mistake
|
||||
usePreventWindowUnload(true);
|
||||
|
||||
// hook to move the settings in the right place to fix missing fields
|
||||
const moveToIntegration = useMoveToIntegration();
|
||||
|
||||
// hook to test if the top editor should be hidden
|
||||
const showHide = useHideTopEditor();
|
||||
|
||||
// hook to open a new modal
|
||||
const modal = useModals();
|
||||
|
||||
// if the user exit the popup we reset the global variable with all the values
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resetValues();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Change the value of the global editor
|
||||
const changeValue = useCallback(
|
||||
(index: number) => (newValue: string) => {
|
||||
return setValue((prev) => {
|
||||
prev[index] = newValue;
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
// Add another editor
|
||||
const addValue = useCallback(
|
||||
(index: number) => () => {
|
||||
setValue((prev) => {
|
||||
prev.splice(index + 1, 0, '');
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
// override the close modal to ask the user if he is sure to close
|
||||
const askClose = useCallback(async () => {
|
||||
if (
|
||||
await deleteDialog(
|
||||
'Are you sure you want to close this modal? (all data will be lost)',
|
||||
'Yes, close it!'
|
||||
)
|
||||
) {
|
||||
modal.closeAll();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 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(),
|
||||
}));
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (!key.valid) {
|
||||
moveToIntegration(key?.integration?.identifier!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetch('/posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
date: date.utc().format('YYYY-MM-DDTHH:mm:ss'),
|
||||
posts: allKeys,
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
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"
|
||||
>
|
||||
<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]">
|
||||
<PickPlatforms
|
||||
integrations={integrations}
|
||||
selectedIntegrations={[]}
|
||||
singleSelect={false}
|
||||
onChange={setSelectedIntegrations}
|
||||
/>
|
||||
{!showHide.hideTopEditor ? (
|
||||
<>
|
||||
{value.map((p, index) => (
|
||||
<>
|
||||
<MDEditor
|
||||
key={`edit_${index}`}
|
||||
height={value.length > 1 ? 150 : 500}
|
||||
value={p}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
<div>
|
||||
<Button onClick={addValue(index)}>Add post</Button>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-[100px] flex justify-center items-center bg-sixth border-tableBorder border-2">
|
||||
Global Editor Hidden
|
||||
</div>
|
||||
)}
|
||||
{!!selectedIntegrations.length && (
|
||||
<PreviewComponent
|
||||
integrations={selectedIntegrations}
|
||||
editorValue={value}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={schedule}>Schedule</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
"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 { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
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
|
||||
});
|
||||
|
||||
const submit = useCallback(async (data: FieldValues) => {
|
||||
const add = await fetch(`/integrations/article/${props.identifier}/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({api: data.api})
|
||||
});
|
||||
|
||||
if (add.ok) {
|
||||
modal.closeAll();
|
||||
router.refresh();
|
||||
return ;
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
'use client';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
import {createContext, FC, ReactNode, useContext, useState} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(utc);
|
||||
|
||||
const CalendarContext = createContext({
|
||||
currentWeek: dayjs().week(),
|
||||
integrations: [] as Integrations[],
|
||||
setFilters: (filters: { currentWeek: number }) => {},
|
||||
});
|
||||
|
||||
export interface Integrations {
|
||||
name: string;
|
||||
id: string;
|
||||
identifier: string;
|
||||
type: string;
|
||||
picture: string;
|
||||
}
|
||||
export const CalendarWeekProvider: FC<{ children: ReactNode, integrations: Integrations[] }> = ({
|
||||
children,
|
||||
integrations
|
||||
}) => {
|
||||
const [filters, setFilters] = useState({
|
||||
currentWeek: dayjs().week(),
|
||||
});
|
||||
return (
|
||||
<CalendarContext.Provider value={{ ...filters, integrations, setFilters }}>
|
||||
{children}
|
||||
</CalendarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCalendar = () => useContext(CalendarContext);
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useCalendar,
|
||||
} from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import dayjs from 'dayjs';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import {AddEditModal} from "@gitroom/frontend/components/launches/add.edit.model";
|
||||
import clsx from "clsx";
|
||||
|
||||
const days = [
|
||||
'',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
];
|
||||
const hours = [
|
||||
'00:00',
|
||||
'01:00',
|
||||
'02:00',
|
||||
'03:00',
|
||||
'04:00',
|
||||
'05:00',
|
||||
'06:00',
|
||||
'07:00',
|
||||
'08:00',
|
||||
'09:00',
|
||||
'10:00',
|
||||
'11:00',
|
||||
'12:00',
|
||||
'13:00',
|
||||
'14:00',
|
||||
'15:00',
|
||||
'16:00',
|
||||
'17:00',
|
||||
'18:00',
|
||||
'19:00',
|
||||
'20:00',
|
||||
'21:00',
|
||||
'22:00',
|
||||
'23:00',
|
||||
];
|
||||
|
||||
const CalendarColumn: FC<{ day: number; hour: string }> = (props) => {
|
||||
const { day, hour } = props;
|
||||
const week = useCalendar();
|
||||
const modal = useModals();
|
||||
|
||||
const getDate = useMemo(() => {
|
||||
const date =
|
||||
dayjs().isoWeek(week.currentWeek).isoWeekday(day).format('YYYY-MM-DD') +
|
||||
'T' +
|
||||
hour +
|
||||
':00';
|
||||
return dayjs(date);
|
||||
}, [week.currentWeek]);
|
||||
|
||||
const addModal = useCallback(() => {
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
children: (
|
||||
<AddEditModal integrations={week.integrations} date={getDate} />
|
||||
),
|
||||
size: '80%',
|
||||
title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isBeforeNow = useMemo(() => {
|
||||
return getDate.isBefore(dayjs());
|
||||
}, [getDate]);
|
||||
|
||||
return (
|
||||
<div className={clsx("h-[calc(216px/6)] text-[12px] hover:bg-white/20 pointer flex justify-center items-center", isBeforeNow && 'bg-white/10 pointer-events-none')}>
|
||||
<div
|
||||
onClick={addModal}
|
||||
className="flex-1 h-full flex justify-center items-center"
|
||||
>
|
||||
{isBeforeNow ? '' : '+ Add'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Calendar = () => {
|
||||
return (
|
||||
<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"
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
import {useCalendar} from "@gitroom/frontend/components/launches/calendar.context";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
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>;
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import removeMd from "remove-markdown";
|
||||
import {useMemo} from "react";
|
||||
|
||||
export const useFormatting = (text: string[], params: {
|
||||
removeMarkdown?: boolean,
|
||||
saveBreaklines?: boolean,
|
||||
specialFunc?: (text: string) => string,
|
||||
}) => {
|
||||
return useMemo(() => {
|
||||
return text.map((value) => {
|
||||
let newText = value;
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢');
|
||||
}
|
||||
if (params.removeMarkdown) {
|
||||
newText = removeMd(value);
|
||||
}
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n');
|
||||
}
|
||||
if (params.specialFunc) {
|
||||
newText = params.specialFunc(newText);
|
||||
}
|
||||
return {
|
||||
text: newText,
|
||||
count: params.removeMarkdown && params.saveBreaklines ? newText.replace(/\n/g, ' ').length : newText.length,
|
||||
}
|
||||
});
|
||||
}, [text]);
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
"use client";
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
export const useHideTopEditor = () => {
|
||||
const [hideTopEditor, setHideTopEditor] = useState(false);
|
||||
useEffect(() => {
|
||||
const hide = () => {
|
||||
setHideTopEditor(true);
|
||||
};
|
||||
const show = () => {
|
||||
setHideTopEditor(false);
|
||||
};
|
||||
emitter.on('hide', hide);
|
||||
emitter.on('show', show);
|
||||
return () => {
|
||||
emitter.off('hide', hide);
|
||||
emitter.off('show', show);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
hideTopEditor,
|
||||
hide: () => {
|
||||
emitter.emit('hide');
|
||||
},
|
||||
show: () => {
|
||||
emitter.emit('show');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import {createContext, useContext} from "react";
|
||||
import {Integrations} from "@gitroom/frontend/components/launches/calendar.context";
|
||||
|
||||
export const IntegrationContext = createContext<{integration: Integrations|undefined, value: string[]}>({integration: undefined, value: []});
|
||||
|
||||
export const useIntegration = () => useContext(IntegrationContext);
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
export const useMoveToIntegration = () => {
|
||||
return useCallback((identifier: string) => {
|
||||
emitter.emit('moveToIntegration', identifier);
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useMoveToIntegrationListener = (
|
||||
enabled: boolean,
|
||||
callback: (identifier: string) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
return load();
|
||||
}, []);
|
||||
|
||||
const load = useCallback(() => {
|
||||
emitter.on('moveToIntegration', callback);
|
||||
return () => {
|
||||
emitter.off('moveToIntegration', callback);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import {useEffect, useMemo} from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UseFormProps } from 'react-hook-form/dist/types';
|
||||
import {allProvidersSettings} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings";
|
||||
import {classValidatorResolver} from "@hookform/resolvers/class-validator";
|
||||
|
||||
const finalInformation = {} as {
|
||||
[key: string]: { posts: string[]; settings: () => object; isValid: boolean };
|
||||
};
|
||||
export const useValues = (identifier: string, integration: string, value: string[]) => {
|
||||
const resolver = useMemo(() => {
|
||||
const findValidator = allProvidersSettings.find((provider) => provider.identifier === identifier)!;
|
||||
return classValidatorResolver(findValidator?.validator);
|
||||
}, [integration]);
|
||||
|
||||
const form = useForm({
|
||||
resolver
|
||||
});
|
||||
|
||||
const getValues = useMemo(() => {
|
||||
return form.getValues;
|
||||
}, [form]);
|
||||
|
||||
finalInformation[integration]= finalInformation[integration] || {};
|
||||
finalInformation[integration].posts = value;
|
||||
finalInformation[integration].isValid = form.formState.isValid;
|
||||
finalInformation[integration].settings = getValues;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
delete finalInformation[integration];
|
||||
};
|
||||
}, []);
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
export const useSettings = (formProps?: Omit<UseFormProps, 'mode'>) => {
|
||||
// const { integration } = useIntegration();
|
||||
// const form = useForm({
|
||||
// ...formProps,
|
||||
// mode: 'onChange',
|
||||
// });
|
||||
//
|
||||
// finalInformation[integration?.identifier!].settings = {
|
||||
// __type: integration?.identifier!,
|
||||
// ...form.getValues(),
|
||||
// };
|
||||
// return form;
|
||||
};
|
||||
|
||||
export const getValues = () => finalInformation;
|
||||
export const resetValues = () => {
|
||||
Object.keys(finalInformation).forEach((key) => {
|
||||
delete finalInformation[key];
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { AddProviderButton } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
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 { Filters } from '@gitroom/frontend/components/launches/filters';
|
||||
|
||||
export const LaunchesComponent: FC<{
|
||||
integrations: Integrations[]
|
||||
}> = (props) => {
|
||||
const { integrations } = props;
|
||||
const sortedIntegrations = useMemo(() => {
|
||||
return orderBy(integrations, ['type', 'identifier'], ['desc', 'asc']);
|
||||
}, [integrations]);
|
||||
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-[330px] 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.map((integration) => (
|
||||
<div
|
||||
key={integration.id}
|
||||
className="flex gap-[8px] items-center"
|
||||
>
|
||||
<div className="relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth">
|
||||
<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 className="flex-1">{integration.name}</div>
|
||||
<div>3</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CalendarWeekProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import {FC} from "react";
|
||||
import {withProvider} from "@gitroom/frontend/components/launches/providers/high.order.provider";
|
||||
|
||||
const DevtoPreview: FC = () => {
|
||||
return <div>asd</div>
|
||||
};
|
||||
|
||||
const DevtoSettings: FC = () => {
|
||||
return <div>asdfasd</div>
|
||||
};
|
||||
|
||||
export default withProvider(DevtoSettings, DevtoPreview);
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,144 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect, 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 { 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';
|
||||
import { useMoveToIntegrationListener } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
|
||||
|
||||
// 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();
|
||||
useEffect(() => {
|
||||
showHide.hide();
|
||||
return () => {
|
||||
showHide.show();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const withProvider = (SettingsComponent: FC, PreviewComponent: FC) => {
|
||||
return (props: {
|
||||
identifier: string;
|
||||
id: string;
|
||||
value: string[];
|
||||
show: boolean;
|
||||
}) => {
|
||||
const [editInPlace, setEditInPlace] = useState(false);
|
||||
const [InPlaceValue, setInPlaceValue] = useState(['']);
|
||||
const [showTab, setShowTab] = useState(0);
|
||||
|
||||
// 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) {
|
||||
setShowTab(2);
|
||||
}
|
||||
});
|
||||
|
||||
// this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation
|
||||
const form = useValues(
|
||||
props.identifier,
|
||||
props.id,
|
||||
editInPlace ? InPlaceValue : props.value
|
||||
);
|
||||
|
||||
// change editor value
|
||||
const changeValue = useCallback(
|
||||
(index: number) => (newValue: string) => {
|
||||
return setInPlaceValue((prev) => {
|
||||
prev[index] = newValue;
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[InPlaceValue]
|
||||
);
|
||||
|
||||
// add another local editor
|
||||
const addValue = useCallback(
|
||||
(index: number) => () => {
|
||||
setInPlaceValue((prev) => {
|
||||
prev.splice(index + 1, 0, '');
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
[InPlaceValue]
|
||||
);
|
||||
|
||||
// This is a function if we want to switch from the global editor to edit in place
|
||||
const changeToEditor = useCallback(
|
||||
(editor: boolean) => async () => {
|
||||
if (
|
||||
editor &&
|
||||
!editInPlace &&
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to edit in place?',
|
||||
'Yes, edit in place!'
|
||||
))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
setShowTab(editor ? 1 : 0);
|
||||
if (editor && !editInPlace) {
|
||||
setEditInPlace(true);
|
||||
setInPlaceValue(props.value);
|
||||
}
|
||||
},
|
||||
[props.value, editInPlace]
|
||||
);
|
||||
|
||||
if (!props.show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<div className="mt-[15px]">
|
||||
{editInPlace && <EditorWrapper />}
|
||||
<div className="flex">
|
||||
<div>
|
||||
<Button secondary={showTab !== 0} onClick={changeToEditor(false)}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button secondary={showTab !== 2} onClick={() => setShowTab(2)}>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button secondary={showTab !== 1} onClick={changeToEditor(true)}>
|
||||
Editor
|
||||
</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}
|
||||
preview="edit"
|
||||
// @ts-ignore
|
||||
onChange={changeValue(index)}
|
||||
/>
|
||||
<div>
|
||||
<Button onClick={addValue(index)}>Add post</Button>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showTab === 2 && <SettingsComponent />}
|
||||
{showTab === 0 && <PreviewComponent />}
|
||||
</div>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import localFont from 'next/font/local';
|
||||
import clsx from 'clsx';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
|
||||
const chirp = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/x/Chirp-Regular.woff2',
|
||||
weight: '400',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/x/Chirp-Bold.woff2',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const LinkedinPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const newValues = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
return text.slice(0, 280);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<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]">
|
||||
{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 className={chirp.className}>{value.text}</pre>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkedinSettings: FC = () => {
|
||||
return <div>asdfasd</div>;
|
||||
};
|
||||
|
||||
export default withProvider(LinkedinSettings, LinkedinPreview);
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import localFont from 'next/font/local';
|
||||
import clsx from 'clsx';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
|
||||
const chirp = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/x/Chirp-Regular.woff2',
|
||||
weight: '400',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/x/Chirp-Bold.woff2',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const RedditPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const newValues = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
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]">
|
||||
{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 className={chirp.className}>{value.text}</pre>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RedditSettings: FC = () => {
|
||||
return <div>asdfasd</div>;
|
||||
};
|
||||
|
||||
export default withProvider(RedditSettings, RedditPreview);
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import {FC} from "react";
|
||||
import {Integrations} from "@gitroom/frontend/components/launches/calendar.context";
|
||||
import DevtoProvider from "@gitroom/frontend/components/launches/providers/devto.provider";
|
||||
import XProvider from "@gitroom/frontend/components/launches/providers/x.provider";
|
||||
import LinkedinProvider from "@gitroom/frontend/components/launches/providers/linkedin.provider";
|
||||
import RedditProvider from "@gitroom/frontend/components/launches/providers/reddit.provider";
|
||||
|
||||
const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
{identifier: 'x', component: XProvider},
|
||||
{identifier: 'linkedin', component: LinkedinProvider},
|
||||
{identifier: 'reddit', component: RedditProvider},
|
||||
];
|
||||
|
||||
export const ShowAllProviders: FC<{integrations: Integrations[], value: string[], selectedProvider?: Integrations}> = (props) => {
|
||||
const {integrations, value, selectedProvider} = props;
|
||||
return (
|
||||
<>
|
||||
{integrations.map((integration) => {
|
||||
const {component: ProviderComponent} = Providers.find(provider => provider.identifier === integration.identifier) || {component: null};
|
||||
if (!ProviderComponent || integrations.map(p => p.id).indexOf(selectedProvider?.id!) === -1) {
|
||||
return null;
|
||||
}
|
||||
return <ProviderComponent key={integration.id} {...integration} value={value} show={selectedProvider?.id === integration.id} />;
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import localFont from 'next/font/local';
|
||||
import clsx from 'clsx';
|
||||
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
|
||||
import {useSettings} from "@gitroom/frontend/components/launches/helpers/use.values";
|
||||
|
||||
const chirp = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/x/Chirp-Regular.woff2',
|
||||
weight: '400',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/x/Chirp-Bold.woff2',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const XPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const newValues = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
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]">
|
||||
{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 className={chirp.className}>{value.text}</pre>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const XSettings: FC = () => {
|
||||
const settings = useSettings({
|
||||
|
||||
});
|
||||
return <div>asdfasd</div>;
|
||||
};
|
||||
|
||||
export default withProvider(XSettings, XPreview);
|
||||
|
|
@ -4,30 +4,33 @@ 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";
|
||||
|
||||
export const LayoutSettings = ({children}: {children: ReactNode}) => {
|
||||
const user = JSON.parse(headers().get('user')!);
|
||||
return (
|
||||
<ContextWrapper user={user}>
|
||||
<div className="min-h-[100vh] 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
|
||||
<MantineWrapper>
|
||||
<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>
|
||||
<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 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>
|
||||
</div>
|
||||
</MantineWrapper>
|
||||
</ContextWrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,15 +3,26 @@ import Image from "next/image";
|
|||
import {Button} from "@gitroom/react/form/button";
|
||||
import {FC, useCallback, useEffect, useState} from "react";
|
||||
import {useFetch} from "@gitroom/helpers/utils/custom.fetch";
|
||||
import {deleteDialog} from "@gitroom/react/helpers/delete.dialog";
|
||||
|
||||
const ConnectedComponent: FC<{id: string, login: string}> = (props) => {
|
||||
const {id, login} = props;
|
||||
const ConnectedComponent: FC<{id: string, login: string, deleteRepository: () => void}> = (props) => {
|
||||
const {id, login, deleteRepository} = props;
|
||||
const fetch = useFetch();
|
||||
const disconnect = useCallback(async () => {
|
||||
if (!await deleteDialog('Are you sure you want to disconnect this repository?')) {
|
||||
return ;
|
||||
}
|
||||
deleteRepository();
|
||||
await fetch(`/settings/repository/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div className="my-[16px] mt-[16px] h-[90px] bg-sixth border-fifth border rounded-[4px] p-[24px]">
|
||||
<div className="flex items-center gap-[8px] font-[Inter]">
|
||||
<div><Image src="/icons/github.svg" alt="GitHub" width={40} height={40}/></div>
|
||||
<div className="flex-1"><strong>Connected:</strong> {login}</div>
|
||||
<Button>Disconnect</Button>
|
||||
<Button onClick={disconnect}>Disconnect</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -93,10 +104,18 @@ export const GithubComponent: FC<{ organizations: Array<{ login: string, id: str
|
|||
if (git.id === g.id) {
|
||||
return {id: g.id, login: name};
|
||||
}
|
||||
return g;
|
||||
return git;
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
}, [githubState]);
|
||||
|
||||
const deleteConnect = useCallback((g: {id: string, login: string}) => () => {
|
||||
setGithubState((gitlibs) => {
|
||||
return gitlibs.filter((git, index) => {
|
||||
return git.id !== g.id;
|
||||
})
|
||||
});
|
||||
}, [githubState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -105,7 +124,7 @@ export const GithubComponent: FC<{ organizations: Array<{ login: string, id: str
|
|||
{!g.login ? (
|
||||
<ConnectComponent setConnected={setConnected(g)} organizations={organizations} {...g} />
|
||||
): (
|
||||
<ConnectedComponent {...g} />
|
||||
<ConnectedComponent deleteRepository={deleteConnect(g)} {...g} />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,20 @@ module.exports = {
|
|||
primary: '#000',
|
||||
secondary: '#090B13',
|
||||
third: '#080B13',
|
||||
forth: '#262373',
|
||||
fifth: '#172034',
|
||||
forth: '#612AD5',
|
||||
fifth: '#28344F',
|
||||
sixth: '#0B101B',
|
||||
gray: '#8C8C8C',
|
||||
input: '#131B2C',
|
||||
inputText: '#64748B',
|
||||
tableBorder: '#1F2941'
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
'13': 'repeat(13, minmax(0, 1fr));'
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require('tailwind-scrollbar')
|
||||
],
|
||||
};
|
||||
|
|
@ -6,12 +6,13 @@ import {DatabaseModule} from "@gitroom/nestjs-libraries/database/prisma/database
|
|||
import {BullMqModule} from "@gitroom/nestjs-libraries/bull-mq-transport/bull-mq.module";
|
||||
import {ioRedis} from "@gitroom/nestjs-libraries/redis/redis.service";
|
||||
import {TrendingService} from "@gitroom/nestjs-libraries/services/trending.service";
|
||||
import {PostsController} from "@gitroom/workers/app/posts.controller";
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule, DatabaseModule, BullMqModule.forRoot({
|
||||
connection: ioRedis
|
||||
})],
|
||||
controllers: [StarsController],
|
||||
controllers: [StarsController, PostsController],
|
||||
providers: [TrendingService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import {Controller} from '@nestjs/common';
|
||||
import {EventPattern, Transport} from '@nestjs/microservices';
|
||||
import {PostsService} from "@gitroom/nestjs-libraries/database/prisma/posts/posts.service";
|
||||
|
||||
@Controller()
|
||||
export class PostsController {
|
||||
constructor(
|
||||
private _postsService: PostsService
|
||||
) {
|
||||
}
|
||||
@EventPattern('post', Transport.REDIS)
|
||||
async checkStars(data: {id: string}) {
|
||||
return this._postsService.post(data.id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import { DynamicModule, Module } from '@nestjs/common';
|
||||
import { DynamicModule } from '@nestjs/common';
|
||||
import { BullMqCoreModule } from './bull-mq-core.module';
|
||||
import { IBullMqModuleOptionsAsync } from './interfaces/bull-mq-module-options-async.interface';
|
||||
import { IBullMqModuleOptions } from './interfaces/bull-mq-module-options.interface';
|
||||
|
||||
@Module({})
|
||||
export class BullMqModule {
|
||||
static forRoot(options: IBullMqModuleOptions): DynamicModule {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -74,10 +74,21 @@ export class BullMqClient extends ClientProxy {
|
|||
return () => void 0;
|
||||
}
|
||||
|
||||
async delay(pattern: string, jobId: string, delay: number) {
|
||||
const queue = this.getQueue(pattern);
|
||||
return queue.getJob(jobId).then((job) => job?.changeDelay(delay));
|
||||
}
|
||||
|
||||
async delete(pattern: string, jobId: string) {
|
||||
const queue = this.getQueue(pattern);
|
||||
return queue.getJob(jobId).then((job) => job?.remove());
|
||||
}
|
||||
|
||||
protected async dispatchEvent(
|
||||
packet: ReadPacket<IBullMqEvent<any>>,
|
||||
): Promise<any> {
|
||||
const queue = this.getQueue(packet.pattern);
|
||||
console.log(packet);
|
||||
await queue.add(packet.pattern, packet.data, {
|
||||
jobId: packet.data.id ?? v4(),
|
||||
...packet.data.options,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import {SubscriptionRepository} from "@gitroom/nestjs-libraries/database/prisma/
|
|||
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";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -29,7 +32,10 @@ import {IntegrationRepository} from "@gitroom/nestjs-libraries/database/prisma/i
|
|||
SubscriptionRepository,
|
||||
NotificationService,
|
||||
IntegrationService,
|
||||
IntegrationRepository
|
||||
IntegrationRepository,
|
||||
PostsService,
|
||||
PostsRepository,
|
||||
IntegrationManager
|
||||
],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ export class IntegrationRepository {
|
|||
) {
|
||||
}
|
||||
|
||||
createIntegration(org: string, name: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn = 999999999) {
|
||||
createIntegration(org: string, name: string, picture: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn = 999999999) {
|
||||
return this._integration.model.integration.create({
|
||||
data: {
|
||||
type: type as any,
|
||||
name,
|
||||
providerIdentifier: provider,
|
||||
token,
|
||||
picture,
|
||||
refreshToken,
|
||||
...expiresIn ? {tokenExpiration: new Date(Date.now() + expiresIn * 1000)} :{},
|
||||
internalId,
|
||||
|
|
@ -22,4 +23,12 @@ export class IntegrationRepository {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
getIntegrationsList(org: string) {
|
||||
return this._integration.model.integration.findMany({
|
||||
where: {
|
||||
organizationId: org
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,11 @@ export class IntegrationService {
|
|||
private _integrationRepository: IntegrationRepository,
|
||||
) {
|
||||
}
|
||||
createIntegration(org: string, name: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn?: number) {
|
||||
return this._integrationRepository.createIntegration(org, name, type, internalId, provider, token, refreshToken, expiresIn);
|
||||
createIntegration(org: string, name: string, picture: string, type: 'article' | 'social' , internalId: string, provider: string, token: string, refreshToken = '', expiresIn?: number) {
|
||||
return this._integrationRepository.createIntegration(org, name, picture, type, internalId, provider, token, refreshToken, expiresIn);
|
||||
}
|
||||
|
||||
getIntegrationsList(org: string) {
|
||||
return this._integrationRepository.getIntegrationsList(org);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Post as PostBody } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration, Post } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PostsRepository {
|
||||
constructor(private _post: PrismaRepository<'post'>) {}
|
||||
|
||||
getPost(id: string, includeIntegration = false) {
|
||||
return this._post.model.post.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
...(includeIntegration ? { integration: true } : {}),
|
||||
childrenPost: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updatePost(id: string, postId: string, releaseURL: string) {
|
||||
return this._post.model.post.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
state: 'PUBLISHED',
|
||||
releaseURL,
|
||||
releaseId: postId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createPost(orgId: string, date: string, body: PostBody) {
|
||||
const posts: Post[] = [];
|
||||
for (const value of body.value) {
|
||||
posts.push(
|
||||
await this._post.model.post.create({
|
||||
data: {
|
||||
publishDate: dayjs(date).toDate(),
|
||||
integration: {
|
||||
connect: {
|
||||
id: body.integration.id,
|
||||
organizationId: orgId,
|
||||
},
|
||||
},
|
||||
...(posts.length
|
||||
? {
|
||||
parentPost: {
|
||||
connect: {
|
||||
id: posts[posts.length - 1]?.id,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
content: value,
|
||||
organization: {
|
||||
connect: {
|
||||
id: orgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return posts;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.repository';
|
||||
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client';
|
||||
import dayjs from 'dayjs';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { Integration, Post } from '@prisma/client';
|
||||
|
||||
type PostWithConditionals = Post & {
|
||||
integration?: Integration;
|
||||
childrenPost: Post[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PostsService {
|
||||
constructor(
|
||||
private _postRepository: PostsRepository,
|
||||
private _workerServiceProducer: BullMqClient,
|
||||
private _integrationManager: IntegrationManager
|
||||
) {}
|
||||
|
||||
async getPostsRecursively(
|
||||
id: string,
|
||||
includeIntegration = false
|
||||
): Promise<PostWithConditionals[]> {
|
||||
const post = await this._postRepository.getPost(id, includeIntegration);
|
||||
return [
|
||||
post!,
|
||||
...(post?.childrenPost?.length
|
||||
? await this.getPostsRecursively(post.childrenPost[0].id)
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
async post(id: string) {
|
||||
const [firstPost, ...morePosts] = await this.getPostsRecursively(id, true);
|
||||
if (!firstPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstPost.integration?.type === 'article') {
|
||||
return this.postArticle(firstPost.integration!, [firstPost, ...morePosts]);
|
||||
}
|
||||
|
||||
return this.postSocial(firstPost.integration!, [firstPost, ...morePosts]);
|
||||
}
|
||||
|
||||
private async postSocial(integration: Integration, posts: Post[]) {
|
||||
const getIntegration = this._integrationManager.getSocialIntegration(integration.providerIdentifier);
|
||||
if (!getIntegration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const publishedPosts = await getIntegration.post(integration.internalId, integration.token, posts.map(p => ({
|
||||
id: p.id,
|
||||
message: p.content,
|
||||
settings: JSON.parse(p.settings || '{}'),
|
||||
})));
|
||||
|
||||
for (const post of publishedPosts) {
|
||||
await this._postRepository.updatePost(post.id, post.postId, post.releaseURL);
|
||||
}
|
||||
}
|
||||
|
||||
private async postArticle(integration: Integration, posts: Post[]) {
|
||||
const getIntegration = this._integrationManager.getArticlesIntegration(integration.providerIdentifier);
|
||||
if (!getIntegration) {
|
||||
return;
|
||||
}
|
||||
const {postId, releaseURL} = await getIntegration.post(integration.token, posts.map(p => p.content).join('\n\n'), JSON.parse(posts[0].settings || '{}'));
|
||||
await this._postRepository.updatePost(posts[0].id, postId, releaseURL);
|
||||
}
|
||||
|
||||
async createPost(orgId: string, body: CreatePostDto) {
|
||||
for (const post of body.posts) {
|
||||
const posts = await this._postRepository.createPost(
|
||||
orgId,
|
||||
body.date,
|
||||
post
|
||||
);
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: posts[0].id,
|
||||
options: {
|
||||
delay: 0 // dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: posts[0].id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -128,6 +128,7 @@ model Integration {
|
|||
organizationId String
|
||||
name String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
picture String?
|
||||
providerIdentifier String
|
||||
type String
|
||||
token String
|
||||
|
|
@ -183,21 +184,19 @@ model Slots {
|
|||
model Post {
|
||||
id String @id @default(cuid())
|
||||
state State @default(QUEUE)
|
||||
queueId String?
|
||||
publishDate DateTime
|
||||
organizationId String
|
||||
IntegrationId String
|
||||
integrationId String
|
||||
content String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
Integration Integration @relation(fields: [IntegrationId], references: [id])
|
||||
integration Integration @relation(fields: [integrationId], references: [id])
|
||||
title String?
|
||||
description String?
|
||||
canonicalUrl String?
|
||||
canonicalPostId String?
|
||||
parentPostId String?
|
||||
releaseId String?
|
||||
releaseURL String?
|
||||
canonicalPost Post? @relation("canonicalPostId", fields: [canonicalPostId], references: [id])
|
||||
settings String?
|
||||
parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id])
|
||||
canonicalChildren Post[] @relation("canonicalPostId")
|
||||
childrenPost Post[] @relation("parentPostId")
|
||||
tags PostTag[]
|
||||
media PostMedia[]
|
||||
|
|
@ -207,7 +206,8 @@ model Post {
|
|||
|
||||
enum State {
|
||||
QUEUE
|
||||
SENT
|
||||
PUBLISHED
|
||||
ERROR
|
||||
DRAFT
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {chunk, groupBy} from "lodash";
|
|||
import dayjs from "dayjs";
|
||||
import {NotificationService} from "@gitroom/nestjs-libraries/notifications/notification.service";
|
||||
import {StarsListDto} from "@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto";
|
||||
import * as console from "console";
|
||||
import {BullMqClient} from "@gitroom/nestjs-libraries/bull-mq-transport/client/bull-mq.client";
|
||||
enum Inform {
|
||||
Removed,
|
||||
New,
|
||||
|
|
@ -14,7 +14,8 @@ enum Inform {
|
|||
export class StarsService {
|
||||
constructor(
|
||||
private _starsRepository: StarsRepository,
|
||||
private _notificationsService: NotificationService
|
||||
private _notificationsService: NotificationService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
){}
|
||||
|
||||
getGitHubRepositoriesByOrgId(org: string) {
|
||||
|
|
@ -49,7 +50,6 @@ export class StarsService {
|
|||
}
|
||||
|
||||
async syncProcess(login: string, page = 1) {
|
||||
console.log('processing', login, page);
|
||||
const starsRequest = await fetch(`https://api.github.com/repos/${login}/stargazers?page=${page}&per_page=100`, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3.star+json',
|
||||
|
|
@ -221,6 +221,7 @@ export class StarsService {
|
|||
}
|
||||
|
||||
async updateGitHubLogin(orgId: string, id: string, login: string) {
|
||||
this._workerServiceProducer.emit('sync_all_stars', {payload: {login}}).subscribe();
|
||||
return this._starsRepository.updateGitHubLogin(orgId, id, login);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import {IsString, MinLength} from "class-validator";
|
||||
|
||||
export class ApiKeyDto {
|
||||
@IsString()
|
||||
@MinLength(4, {
|
||||
message: 'Must be at least 4 characters'
|
||||
})
|
||||
api: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import {ArrayMinSize, IsArray, IsDateString, IsDefined, IsString, ValidateNested} from "class-validator";
|
||||
import {Type} from "class-transformer";
|
||||
import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto";
|
||||
|
||||
export class EmptySettings {}
|
||||
export class Integration {
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
id: string
|
||||
}
|
||||
|
||||
export class Post {
|
||||
@IsDefined()
|
||||
@Type(() => Integration)
|
||||
@ValidateNested()
|
||||
integration: Integration;
|
||||
|
||||
@IsDefined()
|
||||
@ArrayMinSize(1)
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
value: string[];
|
||||
|
||||
@Type(() => EmptySettings, {
|
||||
keepDiscriminatorProperty: true,
|
||||
discriminator: {
|
||||
property: '__type',
|
||||
subTypes: [
|
||||
{ value: DevToSettingsDto, name: 'devto' },
|
||||
],
|
||||
},
|
||||
})
|
||||
settings: DevToSettingsDto
|
||||
}
|
||||
|
||||
export class CreatePostDto {
|
||||
@IsDefined()
|
||||
@IsDateString()
|
||||
date: string;
|
||||
|
||||
@IsDefined()
|
||||
@Type(() => Post)
|
||||
@IsArray()
|
||||
@ValidateNested({each: true})
|
||||
@ArrayMinSize(1)
|
||||
posts: Post[]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import {DevToSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto";
|
||||
export const allProvidersSettings = [{
|
||||
identifier: 'devto',
|
||||
validator: DevToSettingsDto
|
||||
}];
|
||||
|
||||
export type AllProvidersSettings = DevToSettingsDto;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import {IsArray, IsDefined, IsOptional, IsString} from "class-validator";
|
||||
|
||||
export class DevToSettingsDto {
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
main_image?: number;
|
||||
|
||||
@IsString()
|
||||
canonical: string;
|
||||
|
||||
@IsString({
|
||||
each: true
|
||||
})
|
||||
@IsArray()
|
||||
tags: string[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
organization?: string;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export interface ArticleIntegrationsInterface {
|
||||
authenticate(token: string): Promise<{id: string, name: string, token: string}>;
|
||||
publishPost(token: string, content: string): Promise<string>;
|
||||
authenticate(token: string): Promise<{id: string, name: string, token: string, picture: string}>;
|
||||
post(token: string, content: string, settings: object): Promise<{postId: string, releaseURL: string}>;
|
||||
}
|
||||
|
||||
export interface ArticleProvider extends ArticleIntegrationsInterface {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import {ArticleProvider} from "@gitroom/nestjs-libraries/integrations/article/ar
|
|||
export class DevToProvider implements ArticleProvider {
|
||||
identifier = 'devto';
|
||||
name = 'Dev.to';
|
||||
async authenticate(token: string): Promise<{ id: string; name: string; token: string; }> {
|
||||
const {name, id} = await (await fetch('https://dev.to/api/users/me', {
|
||||
async authenticate(token: string) {
|
||||
const {name, id, profile_image} = await (await fetch('https://dev.to/api/users/me', {
|
||||
headers: {
|
||||
'api-key': token
|
||||
}
|
||||
|
|
@ -13,11 +13,15 @@ export class DevToProvider implements ArticleProvider {
|
|||
return {
|
||||
id,
|
||||
name,
|
||||
token
|
||||
token,
|
||||
picture: profile_image
|
||||
}
|
||||
}
|
||||
|
||||
async publishPost(token: string, content: string): Promise<string> {
|
||||
return '';
|
||||
async post(token: string, content: string, settings: object) {
|
||||
return {
|
||||
postId: '123',
|
||||
releaseURL: 'https://dev.to'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ import {ArticleIntegrationsInterface, ArticleProvider} from "@gitroom/nestjs-lib
|
|||
export class HashnodeProvider implements ArticleProvider {
|
||||
identifier = 'hashnode';
|
||||
name = 'Hashnode';
|
||||
async authenticate(token: string): Promise<{ id: string; name: string; token: string; }> {
|
||||
async authenticate(token: string) {
|
||||
try {
|
||||
const {data: {me: {name, id}}} = await (await fetch('https://gql.hashnode.com', {
|
||||
const {data: {me: {name, id, profilePicture}}} = await (await fetch('https://gql.hashnode.com', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -16,7 +16,8 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
query {
|
||||
me {
|
||||
name,
|
||||
id
|
||||
id,
|
||||
profilePicture
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -24,19 +25,23 @@ export class HashnodeProvider implements ArticleProvider {
|
|||
})).json();
|
||||
|
||||
return {
|
||||
id, name, token
|
||||
id, name, token, picture: profilePicture
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
token: ''
|
||||
token: '',
|
||||
picture: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async publishPost(token: string, content: string): Promise<string> {
|
||||
return '';
|
||||
async post(token: string, content: string, settings: object) {
|
||||
return {
|
||||
postId: '123',
|
||||
releaseURL: 'https://dev.to'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ export class MediumProvider implements ArticleProvider {
|
|||
identifier = 'medium';
|
||||
name = 'Medium';
|
||||
|
||||
async authenticate(token: string): Promise<{ id: string; name: string; token: string; }> {
|
||||
const {data: {name, id}} = await (await fetch('https://api.medium.com/v1/me', {
|
||||
async authenticate(token: string) {
|
||||
const {data: {name, id, imageUrl}} = await (await fetch('https://api.medium.com/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
|
|
@ -14,11 +14,15 @@ export class MediumProvider implements ArticleProvider {
|
|||
return {
|
||||
id,
|
||||
name,
|
||||
token
|
||||
token,
|
||||
picture: imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
async publishPost(token: string, content: string): Promise<string> {
|
||||
return '';
|
||||
async post(token: string, content: string, settings: object) {
|
||||
return {
|
||||
postId: '123',
|
||||
releaseURL: 'https://dev.to'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,12 @@ const articleIntegrationList = [
|
|||
|
||||
@Injectable()
|
||||
export class IntegrationManager {
|
||||
getAllIntegrations() {
|
||||
return {
|
||||
social: socialIntegrationList.map(p => ({name: p.name, identifier: p.identifier})),
|
||||
article: articleIntegrationList.map(p => ({name: p.name, identifier: p.identifier})),
|
||||
};
|
||||
}
|
||||
getAllowedSocialsIntegrations() {
|
||||
return socialIntegrationList.map(p => p.identifier);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,82 +1,202 @@
|
|||
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 LinkedinProvider implements SocialProvider {
|
||||
identifier = 'linkedin';
|
||||
name = 'LinkedIn';
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
const {access_token: accessToken, refresh_token: refreshToken} = await (await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
identifier = 'linkedin';
|
||||
name = 'LinkedIn';
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
const { access_token: accessToken, refresh_token: refreshToken } = await (
|
||||
await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token,
|
||||
client_id: process.env.LINKEDIN_CLIENT_ID!,
|
||||
client_secret: process.env.LINKEDIN_CLIENT_SECRET!,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
const {
|
||||
name,
|
||||
sub: id,
|
||||
picture,
|
||||
} = await (
|
||||
await fetch('https://api.linkedin.com/v2/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
name,
|
||||
picture,
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
const codeVerifier = makeId(30);
|
||||
const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${
|
||||
process.env.LINKEDIN_CLIENT_ID
|
||||
}&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/integrations/social/linkedin`
|
||||
)}&state=${state}&scope=${encodeURIComponent(
|
||||
'openid profile w_member_social r_liteprofile'
|
||||
)}`;
|
||||
return {
|
||||
url,
|
||||
codeVerifier,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: { code: string; codeVerifier: string }) {
|
||||
const body = new URLSearchParams();
|
||||
body.append('grant_type', 'authorization_code');
|
||||
body.append('code', params.code);
|
||||
body.append(
|
||||
'redirect_uri',
|
||||
`${process.env.FRONTEND_URL}/integrations/social/linkedin`
|
||||
);
|
||||
body.append('client_id', process.env.LINKEDIN_CLIENT_ID!);
|
||||
body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!);
|
||||
|
||||
const {
|
||||
access_token: accessToken,
|
||||
expires_in: expiresIn,
|
||||
refresh_token: refreshToken,
|
||||
} = await (
|
||||
await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body,
|
||||
})
|
||||
).json();
|
||||
|
||||
const {
|
||||
name,
|
||||
sub: id,
|
||||
picture,
|
||||
} = await (
|
||||
await fetch('https://api.linkedin.com/v2/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
name,
|
||||
picture,
|
||||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...restPosts] = postDetails;
|
||||
console.log('posting');
|
||||
const data = await fetch('https://api.linkedin.com/v2/posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
author: `urn:li:person:${id}`,
|
||||
commentary: firstPost.message,
|
||||
visibility: 'PUBLIC',
|
||||
distribution: {
|
||||
feedDistribution: 'MAIN_FEED',
|
||||
targetEntities: [],
|
||||
thirdPartyDistributionChannels: [],
|
||||
},
|
||||
lifecycleState: 'PUBLISHED',
|
||||
isReshareDisabledByAuthor: false,
|
||||
// content: {
|
||||
// // contentEntities: [
|
||||
// // {
|
||||
// // entityLocation: 'URL_OF_THE_CONTENT_TO_SHARE',
|
||||
// // thumbnails: [
|
||||
// // {
|
||||
// // resolvedUrl: 'URL_OF_THE_THUMBNAIL_IMAGE',
|
||||
// // },
|
||||
// // ],
|
||||
// // },
|
||||
// // ],
|
||||
// title: firstPost.message,
|
||||
// },
|
||||
// distribution: {
|
||||
// linkedInDistributionTarget: {},
|
||||
// },
|
||||
// owner: `urn:li:person:${id}`,
|
||||
// subject: firstPost.message,
|
||||
// text: {
|
||||
// text: firstPost.message,
|
||||
// },
|
||||
}),
|
||||
});
|
||||
|
||||
const topPostId = data.headers.get('x-restli-id')!;
|
||||
const ids = [
|
||||
{
|
||||
status: 'posted',
|
||||
postId: topPostId,
|
||||
id: firstPost.id,
|
||||
releaseURL: `https://www.linkedin.com/feed/update/${topPostId}`,
|
||||
},
|
||||
];
|
||||
for (const post of restPosts) {
|
||||
const {object} = await (await fetch(
|
||||
`https://api.linkedin.com/v2/socialActions/${decodeURIComponent(
|
||||
topPostId
|
||||
)}/comments`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
actor: `urn:li:person:${id}`,
|
||||
object: topPostId,
|
||||
message: {
|
||||
text: post.message,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token,
|
||||
client_id: process.env.LINKEDIN_CLIENT_ID!,
|
||||
client_secret: process.env.LINKEDIN_CLIENT_SECRET!
|
||||
})
|
||||
})).json()
|
||||
|
||||
const {id, localizedFirstName, localizedLastName} = await (await fetch('https://api.linkedin.com/v2/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
name: `${localizedFirstName} ${localizedLastName}`
|
||||
}),
|
||||
}
|
||||
)).json()
|
||||
|
||||
ids.push({
|
||||
status: 'posted',
|
||||
postId: object,
|
||||
id: post.id,
|
||||
releaseURL: `https://www.linkedin.com/embed/feed/update/${object}`,
|
||||
});
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
const codeVerifier = makeId(30);
|
||||
const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${process.env.LINKEDIN_CLIENT_ID}&redirect_uri=${encodeURIComponent(`${process.env.FRONTEND_URL}/integrations/social/linkedin`)}&state=${state}&scope=${encodeURIComponent('openid profile w_member_social')}`;
|
||||
return {
|
||||
url,
|
||||
codeVerifier,
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate(params: {code: string, codeVerifier: string}) {
|
||||
const body = new URLSearchParams();
|
||||
body.append('grant_type', 'authorization_code');
|
||||
body.append('code', params.code);
|
||||
body.append('redirect_uri', `${process.env.FRONTEND_URL}/integrations/social/linkedin`);
|
||||
body.append('client_id', process.env.LINKEDIN_CLIENT_ID!);
|
||||
body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!);
|
||||
|
||||
const {access_token: accessToken, expires_in: expiresIn, refresh_token: refreshToken, ...data} = await (await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body
|
||||
})).json()
|
||||
|
||||
console.log({accessToken, expiresIn, refreshToken, data});
|
||||
|
||||
const {name, sub: id} = await (await fetch('https://api.linkedin.com/v2/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).json();
|
||||
|
||||
return {
|
||||
id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
async schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export class RedditProvider implements SocialProvider {
|
|||
})
|
||||
})).json();
|
||||
|
||||
const {name, id} = await (await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
const {name, id, icon_img} = await (await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
|
|
@ -28,7 +28,8 @@ export class RedditProvider implements SocialProvider {
|
|||
name,
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ export class RedditProvider implements SocialProvider {
|
|||
})
|
||||
})).json();
|
||||
|
||||
const {name, id} = await (await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
const {name, id, icon_img} = await (await fetch('https://oauth.reddit.com/api/v1/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
|
|
@ -68,14 +69,28 @@ export class RedditProvider implements SocialProvider {
|
|||
name,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn
|
||||
expiresIn,
|
||||
picture: icon_img.split('?')[0]
|
||||
}
|
||||
}
|
||||
|
||||
async schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> {
|
||||
return [{
|
||||
postId: '123',
|
||||
status: 'scheduled'
|
||||
}];
|
||||
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'
|
||||
})
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -16,22 +16,26 @@ export type AuthTokenDetails = {
|
|||
accessToken: string; // The obtained access token
|
||||
refreshToken?: string; // The refresh token, if applicable
|
||||
expiresIn?: number; // The duration in seconds for which the access token is valid
|
||||
picture?: string;
|
||||
};
|
||||
|
||||
export interface ISocialMediaIntegration {
|
||||
schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]>; // Schedules a new post
|
||||
post(id: string, accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]>; // Schedules a new post
|
||||
}
|
||||
|
||||
export type PostResponse = {
|
||||
id: string; // The db internal id of the post
|
||||
postId: string; // The ID of the scheduled post returned by the platform
|
||||
releaseURL: string; // The URL of the post on the platform
|
||||
status: string; // Status of the operation or initial post status
|
||||
};
|
||||
|
||||
export type PostDetails = {
|
||||
id: string;
|
||||
message: string;
|
||||
scheduledTime: Date; // The time when the post should be published
|
||||
media?: MediaContent[]; // Optional array of media content to be attached with the post
|
||||
poll?: PollDetails; // Optional poll details
|
||||
settings: object;
|
||||
media?: MediaContent[];
|
||||
poll?: PollDetails;
|
||||
};
|
||||
|
||||
export type PollDetails = {
|
||||
|
|
|
|||
|
|
@ -1,67 +1,105 @@
|
|||
import { TwitterApi } from 'twitter-api-v2';
|
||||
import {AuthTokenDetails, PostDetails, PostResponse, SocialProvider} from "@gitroom/nestjs-libraries/integrations/social/social.integrations.interface";
|
||||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
|
||||
export class XProvider implements SocialProvider {
|
||||
identifier = 'x';
|
||||
name = 'X';
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const startingClient = new TwitterApi({ clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET! });
|
||||
const { accessToken, refreshToken: newRefreshToken, expiresIn, client } = await startingClient.refreshOAuth2Token(refreshToken);
|
||||
const {data: {id, name}} = await client.v2.me();
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn
|
||||
}
|
||||
identifier = 'x';
|
||||
name = 'X';
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
const startingClient = new TwitterApi({
|
||||
clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
});
|
||||
const {
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn,
|
||||
client,
|
||||
} = await startingClient.refreshOAuth2Token(refreshToken);
|
||||
const {
|
||||
data: { id, name, profile_image_url },
|
||||
} = await client.v2.me();
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn,
|
||||
picture: profile_image_url,
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const client = new TwitterApi({
|
||||
clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
});
|
||||
const { url, codeVerifier, state } = client.generateOAuth2AuthLink(
|
||||
process.env.FRONTEND_URL + '/integrations/social/x',
|
||||
{ scope: ['tweet.read', 'users.read', 'tweet.write', 'offline.access'] }
|
||||
);
|
||||
return {
|
||||
url,
|
||||
codeVerifier,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: { code: string; codeVerifier: string }) {
|
||||
const startingClient = new TwitterApi({
|
||||
clientId: process.env.TWITTER_CLIENT_ID!,
|
||||
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
});
|
||||
const { accessToken, refreshToken, expiresIn, client } =
|
||||
await startingClient.loginWithOAuth2({
|
||||
code: params.code,
|
||||
codeVerifier: params.codeVerifier,
|
||||
redirectUri: process.env.FRONTEND_URL + '/integrations/social/x',
|
||||
});
|
||||
|
||||
const {
|
||||
data: { id, name, profile_image_url },
|
||||
} = await client.v2.me({
|
||||
'user.fields': 'profile_image_url',
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
accessToken,
|
||||
name,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
picture: profile_image_url,
|
||||
};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
): Promise<PostResponse[]> {
|
||||
const client = new TwitterApi(accessToken);
|
||||
const {data: {username}} = await client.v2.me({
|
||||
"user.fields": "username"
|
||||
});
|
||||
const ids: Array<{postId: string, id: string, releaseURL: string}> = [];
|
||||
for (const post of postDetails) {
|
||||
const { data }: { data: { id: string } } = await client.v2.tweet({
|
||||
text: post.message,
|
||||
...(ids.length
|
||||
? { reply: { in_reply_to_tweet_id: ids[ids.length - 1].postId } }
|
||||
: {}),
|
||||
});
|
||||
ids.push({postId: data.id, id: post.id, releaseURL: `https://twitter.com/${username}/status/${data.id}`});
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const client = new TwitterApi({ clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET! });
|
||||
const {url, codeVerifier, state} = client.generateOAuth2AuthLink(
|
||||
process.env.FRONTEND_URL + '/integrations/social/x',
|
||||
{ scope: ['tweet.read', 'users.read', 'tweet.write', 'offline.access'] });
|
||||
return {
|
||||
url,
|
||||
codeVerifier,
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate(params: {code: string, codeVerifier: string}) {
|
||||
const startingClient = new TwitterApi({ clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET! });
|
||||
const {accessToken, refreshToken, expiresIn, client} = await startingClient.loginWithOAuth2({
|
||||
code: params.code,
|
||||
codeVerifier: params.codeVerifier,
|
||||
redirectUri: process.env.FRONTEND_URL + '/integrations/social/x'
|
||||
});
|
||||
|
||||
const {data: {id, name}} = await client.v2.me();
|
||||
|
||||
return {
|
||||
id,
|
||||
accessToken,
|
||||
name,
|
||||
refreshToken,
|
||||
expiresIn
|
||||
}
|
||||
}
|
||||
|
||||
async schedulePost(accessToken: string, postDetails: PostDetails[]): Promise<PostResponse[]> {
|
||||
const client = new TwitterApi(accessToken);
|
||||
const ids: string[] = [];
|
||||
for (const post of postDetails) {
|
||||
const {data}: {data: {id: string}} = await client.v2.tweet({
|
||||
text: post.message,
|
||||
...ids.length ? { reply: {in_reply_to_tweet_id: ids[ids.length - 1]} } : {},
|
||||
});
|
||||
ids.push(data.id);
|
||||
}
|
||||
|
||||
return ids.map(p => ({
|
||||
postId: p,
|
||||
status: 'posted'
|
||||
}));
|
||||
}
|
||||
}
|
||||
return ids.map((p) => ({
|
||||
...p,
|
||||
status: 'posted',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {ButtonHTMLAttributes, DetailedHTMLProps, FC} from "react";
|
||||
import {clsx} from "clsx";
|
||||
|
||||
export const Button: FC<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>> = (props) => {
|
||||
export const Button: FC<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {secondary?: boolean}> = (props) => {
|
||||
return (
|
||||
<button {...props} type={props.type || 'button'} className={clsx('bg-forth px-[24px] h-[40px] cursor-pointer items-center justify-center flex', props?.className)} />
|
||||
<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)} />
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
"use client";
|
||||
|
||||
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;
|
||||
const form = useFormContext();
|
||||
const err = useMemo(() => {
|
||||
if (!form || !form.formState.errors[props?.name!]) return;
|
||||
return form?.formState?.errors?.[props?.name!]?.message! as string;
|
||||
}, [form?.formState?.errors?.[props?.name!]?.message]);
|
||||
|
||||
return (
|
||||
<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} />
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import Swal from "sweetalert2";
|
||||
|
||||
export const deleteDialog = async (message: string, confirmButton?: string) => {
|
||||
const fire = await Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: message,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: confirmButton || 'Yes, delete it!',
|
||||
cancelButtonText: 'No, cancel!',
|
||||
});
|
||||
|
||||
return fire.isConfirmed;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
"use client";
|
||||
import {ReactNode} from "react";
|
||||
import {MantineProvider} from "@mantine/core";
|
||||
import {ModalsProvider} from "@mantine/modals";
|
||||
|
||||
export const MantineWrapper = (props: { children: ReactNode }) => {
|
||||
return (
|
||||
<MantineProvider>
|
||||
<ModalsProvider modalProps={{
|
||||
classNames: {
|
||||
modal: 'bg-primary text-white border-fifth border',
|
||||
close: 'bg-black hover:bg-black cursor-pointer',
|
||||
}
|
||||
}}>
|
||||
{props.children}
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
)
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import {useEffect} from "react";
|
||||
|
||||
export const usePreventWindowUnload = (preventDefault: boolean) => {
|
||||
useEffect(() => {
|
||||
if (!preventDefault) return;
|
||||
const handleBeforeUnload = (event: any) => event.preventDefault();
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, [preventDefault]);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"stripe listen --forward-to localhost:3000/payment\" \"nx run-many --target=serve --projects=frontend,backend --parallel=4\"",
|
||||
"dev": "concurrently \"stripe listen --forward-to localhost:3000/payment\" \"nx run-many --target=serve --projects=frontend,backend,workers --parallel=4\"",
|
||||
"workers": "nx run workers:serve:development",
|
||||
"cron": "nx run cron:serve:development",
|
||||
"command": "nx run commands:build && nx run commands:command",
|
||||
|
|
@ -13,6 +13,9 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@mantine/core": "^5.10.5",
|
||||
"@mantine/modals": "^5.10.5",
|
||||
"@nestjs/common": "^10.0.2",
|
||||
"@nestjs/core": "^10.0.2",
|
||||
"@nestjs/microservices": "^10.3.1",
|
||||
|
|
@ -22,11 +25,14 @@
|
|||
"@novu/notification-center": "^0.23.0",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"@swc/helpers": "~0.5.2",
|
||||
"@sweetalert2/theme-dark": "^5.0.16",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/remove-markdown": "^0.3.4",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"@uiw/react-md-editor": "^4.0.3",
|
||||
"axios": "^1.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.1.5",
|
||||
|
|
@ -45,14 +51,16 @@
|
|||
"prisma-paginate": "^5.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "6.11.2",
|
||||
"redis": "^4.6.12",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"simple-statistics": "^7.8.3",
|
||||
"stripe": "^14.14.0",
|
||||
"sweetalert2": "^11.10.5",
|
||||
"tslib": "^2.3.0",
|
||||
"twitter-api-v2": "^1.16.0",
|
||||
"yargs": "^17.7.2"
|
||||
|
|
@ -107,6 +115,7 @@
|
|||
"prisma": "^5.8.1",
|
||||
"react-refresh": "^0.10.0",
|
||||
"sass": "1.62.1",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue