feat: building
This commit is contained in:
parent
313380e183
commit
0f17d56522
|
|
@ -88,7 +88,7 @@ export const BoldText: FC<{
|
|||
return (
|
||||
<div
|
||||
onClick={mark}
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import 'dayjs/locale/tr';
|
|||
import 'dayjs/locale/vi';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
|
||||
import { AddEditModal as AddEditModal2 } from '@gitroom/frontend/components/launches/add.edit.model';
|
||||
import clsx from 'clsx';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
|
||||
|
|
@ -53,6 +53,7 @@ import { useInterval } from '@mantine/hooks';
|
|||
import { StatisticsModal } from '@gitroom/frontend/components/launches/statistics';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import i18next from 'i18next';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
|
||||
|
||||
// Extend dayjs with necessary plugins
|
||||
extend(isSameOrAfter);
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const UText: FC<{
|
|||
return (
|
||||
<div
|
||||
onClick={mark}
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, Fragment, useRef } from 'react';
|
||||
import { AddEditModalProps } from '@gitroom/frontend/components/new-launch/add.edit.modal';
|
||||
import clsx from 'clsx';
|
||||
import { TopTitle } from '@gitroom/frontend/components/new-launch/helpers/top.title.component';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { PicksSocialsComponent } from '@gitroom/frontend/components/new-launch/picks.socials.component';
|
||||
import {
|
||||
Editor,
|
||||
EditorWrapper,
|
||||
} from '@gitroom/frontend/components/new-launch/editor';
|
||||
import { SelectCurrent } from '@gitroom/frontend/components/new-launch/select.current';
|
||||
import { ShowAllProviders } from '@gitroom/frontend/components/new-launch/providers/show.all.providers';
|
||||
|
||||
export const AddEditModalInnerInner: FC<AddEditModalProps> = () => {
|
||||
const t = useT();
|
||||
const ref = useRef(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="add-edit-modal"
|
||||
className={clsx(
|
||||
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0">
|
||||
<TopTitle title="Create Post">
|
||||
<div className="flex items-center">asd</div>
|
||||
</TopTitle>
|
||||
|
||||
<PicksSocialsComponent toolTip={true} />
|
||||
<div>
|
||||
<SelectCurrent />
|
||||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 editor text-textColor gap-[10px] flex-col flex">
|
||||
<EditorWrapper totalPosts={1} value="" />
|
||||
<div className="flex">
|
||||
<div className="flex-1">media</div>
|
||||
<div className="flex bg-customColor20 rounded-br-[8px] text-customColor19"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative min-h-[68px] flex flex-col rounded-[4px] border border-customColor6 bg-sixth">
|
||||
<div className="gap-[10px] relative flex flex-col justify-center items-center min-h-full pe-[16px]">
|
||||
<div
|
||||
id="add-edit-post-dialog-buttons"
|
||||
className="flex flex-row flex-wrap w-full h-full gap-[10px] justify-end items-center"
|
||||
>
|
||||
<div className="flex justify-center items-center gap-[5px] h-full">
|
||||
<div
|
||||
className="h-full flex items-center text-white"
|
||||
onClick={async () => {
|
||||
console.log(await ref.current.checkAllValid());
|
||||
}}
|
||||
>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex gap-[20px] flex-col rounded-[4px] border-customColor6 bg-sixth flex-1 transition-all duration-700'
|
||||
)}
|
||||
>
|
||||
<div className="mx-[16px]">
|
||||
<TopTitle title="" removeTitle={true}>
|
||||
<svg
|
||||
width="10"
|
||||
height="11"
|
||||
viewBox="0 0 10 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<path
|
||||
d="M9.85403 9.64628C9.90048 9.69274 9.93733 9.74789 9.96247 9.80859C9.98762 9.86928 10.0006 9.93434 10.0006 10C10.0006 10.0657 9.98762 10.1308 9.96247 10.1915C9.93733 10.2522 9.90048 10.3073 9.85403 10.3538C9.80757 10.4002 9.75242 10.4371 9.69173 10.4622C9.63103 10.4874 9.56598 10.5003 9.50028 10.5003C9.43458 10.5003 9.36953 10.4874 9.30883 10.4622C9.24813 10.4371 9.19298 10.4002 9.14653 10.3538L5.00028 6.20691L0.854028 10.3538C0.760208 10.4476 0.63296 10.5003 0.500278 10.5003C0.367596 10.5003 0.240348 10.4476 0.146528 10.3538C0.0527077 10.26 2.61548e-09 10.1327 0 10C-2.61548e-09 9.86735 0.0527077 9.7401 0.146528 9.64628L4.2934 5.50003L0.146528 1.35378C0.0527077 1.25996 0 1.13272 0 1.00003C0 0.867352 0.0527077 0.740104 0.146528 0.646284C0.240348 0.552464 0.367596 0.499756 0.500278 0.499756C0.63296 0.499756 0.760208 0.552464 0.854028 0.646284L5.00028 4.79316L9.14653 0.646284C9.24035 0.552464 9.3676 0.499756 9.50028 0.499756C9.63296 0.499756 9.76021 0.552464 9.85403 0.646284C9.94785 0.740104 10.0006 0.867352 10.0006 1.00003C10.0006 1.13272 9.94785 1.25996 9.85403 1.35378L5.70715 5.50003L9.85403 9.64628Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</TopTitle>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col p-[16px] pt-0">
|
||||
<ShowAllProviders ref={ref} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
'use client';
|
||||
import 'reflect-metadata';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import dayjs from 'dayjs';
|
||||
import type { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useExistingData } from '@gitroom/frontend/components/new-launch/helpers/use.existing.data';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { AddEditModalInnerInner } from '@gitroom/frontend/components/new-launch/add.edit.modal.inner';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
|
||||
export interface AddEditModalProps {
|
||||
date: dayjs.Dayjs;
|
||||
integrations: Integrations[];
|
||||
allIntegrations?: Integrations[];
|
||||
set?: CreatePostDto;
|
||||
addEditSets?: (data: any) => void;
|
||||
reopenModal: () => void;
|
||||
mutate: () => void;
|
||||
padding?: string;
|
||||
customClose?: () => void;
|
||||
onlyValues?: Array<{
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{
|
||||
id: string;
|
||||
path: string;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const AddEditModal: FC<AddEditModalProps> = (props) => {
|
||||
const setAllIntegrations = useLaunchStore(
|
||||
(state) => state.setAllIntegrations
|
||||
);
|
||||
|
||||
const integrations = useLaunchStore((state) => state.integrations);
|
||||
|
||||
useEffect(() => {
|
||||
setAllIntegrations(props.integrations || []);
|
||||
}, []);
|
||||
|
||||
if (!integrations.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <AddEditModalInner {...props} />;
|
||||
};
|
||||
|
||||
export const AddEditModalInner: FC<AddEditModalProps> = (props) => {
|
||||
const existingData = useExistingData();
|
||||
const reset = useLaunchStore((state) => state.reset);
|
||||
|
||||
const addOrRemoveSelectedIntegration = useLaunchStore(
|
||||
(state) => state.addOrRemoveSelectedIntegration
|
||||
);
|
||||
|
||||
const addGlobalValue = useLaunchStore((state) => state.addGlobalValue);
|
||||
const addInternalValue = useLaunchStore((state) => state.addInternalValue);
|
||||
const selectedIntegrations = useLaunchStore((state) => state.selectedIntegrations);
|
||||
const global = useLaunchStore((state) => state.global);
|
||||
const internal = useLaunchStore((state) => state.internal);
|
||||
|
||||
useEffect(() => {
|
||||
if (existingData.integration) {
|
||||
const integration = props.integrations.find(
|
||||
(i) => i.id === existingData.integration
|
||||
);
|
||||
addOrRemoveSelectedIntegration(integration, existingData.settings);
|
||||
|
||||
addInternalValue(0, existingData.settings, existingData.posts.map((post) => ({
|
||||
content: post.content,
|
||||
id: post.id,
|
||||
// @ts-ignore
|
||||
media: post.image as any[],
|
||||
})));
|
||||
}
|
||||
else {
|
||||
addGlobalValue(0, [{
|
||||
content: '',
|
||||
id: makeId(10),
|
||||
media: [],
|
||||
}]);
|
||||
}
|
||||
|
||||
return () => {
|
||||
reset();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!selectedIntegrations.length && !global.length && !internal.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <AddEditModalInnerInner {...props} />;
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import React, { FC } from 'react';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const AddPostButton: FC<{
|
||||
onClick: () => void;
|
||||
num: number;
|
||||
}> = (props) => {
|
||||
const { onClick, num } = props;
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
className="!h-[24px] rounded-[3px] flex gap-[4px] w-[102px] text-[12px] font-[500]"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 1.3125C5.87512 1.3125 4.7755 1.64607 3.8402 2.27102C2.90489 2.89597 2.17591 3.78423 1.74544 4.82349C1.31496 5.86274 1.20233 7.00631 1.42179 8.10958C1.64124 9.21284 2.18292 10.2263 2.97833 11.0217C3.77374 11.8171 4.78716 12.3588 5.89043 12.5782C6.99369 12.7977 8.13726 12.685 9.17651 12.2546C10.2158 11.8241 11.104 11.0951 11.729 10.1598C12.3539 9.2245 12.6875 8.12488 12.6875 7C12.6859 5.49207 12.0862 4.04636 11.0199 2.98009C9.95365 1.91382 8.50793 1.31409 7 1.3125ZM7 11.8125C6.04818 11.8125 5.11773 11.5303 4.32632 11.0014C3.53491 10.4726 2.91808 9.72103 2.55383 8.84166C2.18959 7.96229 2.09428 6.99466 2.27997 6.06113C2.46566 5.12759 2.92401 4.27009 3.59705 3.59705C4.27009 2.92401 5.1276 2.46566 6.06113 2.27997C6.99466 2.09428 7.9623 2.18958 8.84167 2.55383C9.72104 2.91808 10.4726 3.53491 11.0015 4.32632C11.5303 5.11773 11.8125 6.04818 11.8125 7C11.8111 8.27591 11.3036 9.49915 10.4014 10.4014C9.49915 11.3036 8.27591 11.8111 7 11.8125ZM9.625 7C9.625 7.11603 9.57891 7.22731 9.49686 7.30936C9.41481 7.39141 9.30353 7.4375 9.1875 7.4375H7.4375V9.1875C7.4375 9.30353 7.39141 9.41481 7.30936 9.49686C7.22731 9.57891 7.11603 9.625 7 9.625C6.88397 9.625 6.77269 9.57891 6.69064 9.49686C6.6086 9.41481 6.5625 9.30353 6.5625 9.1875V7.4375H4.8125C4.69647 7.4375 4.58519 7.39141 4.50314 7.30936C4.4211 7.22731 4.375 7.11603 4.375 7C4.375 6.88397 4.4211 6.77269 4.50314 6.69064C4.58519 6.60859 4.69647 6.5625 4.8125 6.5625H6.5625V4.8125C6.5625 4.69647 6.6086 4.58519 6.69064 4.50314C6.77269 4.42109 6.88397 4.375 7 4.375C7.11603 4.375 7.22731 4.42109 7.30936 4.50314C7.39141 4.58519 7.4375 4.69647 7.4375 4.8125V6.5625H9.1875C9.30353 6.5625 9.41481 6.60859 9.49686 6.69064C9.57891 6.77269 9.625 6.88397 9.625 7Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-white">{t('add_comment', 'Add comment')}</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback } from 'react';
|
||||
import { Editor, Transforms } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
const originalMap = {
|
||||
a: '𝗮',
|
||||
b: '𝗯',
|
||||
c: '𝗰',
|
||||
d: '𝗱',
|
||||
e: '𝗲',
|
||||
f: '𝗳',
|
||||
g: '𝗴',
|
||||
h: '𝗵',
|
||||
i: '𝗶',
|
||||
j: '𝗷',
|
||||
k: '𝗸',
|
||||
l: '𝗹',
|
||||
m: '𝗺',
|
||||
n: '𝗻',
|
||||
o: '𝗼',
|
||||
p: '𝗽',
|
||||
q: '𝗾',
|
||||
r: '𝗿',
|
||||
s: '𝘀',
|
||||
t: '𝘁',
|
||||
u: '𝘂',
|
||||
v: '𝘃',
|
||||
w: '𝘄',
|
||||
x: '𝘅',
|
||||
y: '𝘆',
|
||||
z: '𝘇',
|
||||
A: '𝗔',
|
||||
B: '𝗕',
|
||||
C: '𝗖',
|
||||
D: '𝗗',
|
||||
E: '𝗘',
|
||||
F: '𝗙',
|
||||
G: '𝗚',
|
||||
H: '𝗛',
|
||||
I: '𝗜',
|
||||
J: '𝗝',
|
||||
K: '𝗞',
|
||||
L: '𝗟',
|
||||
M: '𝗠',
|
||||
N: '𝗡',
|
||||
O: '𝗢',
|
||||
P: '𝗣',
|
||||
Q: '𝗤',
|
||||
R: '𝗥',
|
||||
S: '𝗦',
|
||||
T: '𝗧',
|
||||
U: '𝗨',
|
||||
V: '𝗩',
|
||||
W: '𝗪',
|
||||
X: '𝗫',
|
||||
Y: '𝗬',
|
||||
Z: '𝗭',
|
||||
'1': '𝟭',
|
||||
'2': '𝟮',
|
||||
'3': '𝟯',
|
||||
'4': '𝟰',
|
||||
'5': '𝟱',
|
||||
'6': '𝟲',
|
||||
'7': '𝟳',
|
||||
'8': '𝟴',
|
||||
'9': '𝟵',
|
||||
'0': '𝟬',
|
||||
};
|
||||
const reverseMap = Object.fromEntries(
|
||||
Object.entries(originalMap).map(([key, value]) => [value, key])
|
||||
);
|
||||
export const BoldText: FC<{
|
||||
editor: any;
|
||||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const mark = () => {
|
||||
const selectedText = Editor.string(editor, editor.selection);
|
||||
const newText = Array.from(
|
||||
!selectedText ? prompt('What do you want to write?') || '' : selectedText
|
||||
)
|
||||
.map((char) => {
|
||||
// @ts-ignore
|
||||
return originalMap?.[char] || reverseMap?.[char] || char;
|
||||
})
|
||||
.join('');
|
||||
Transforms.insertText(editor, newText);
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
onClick={mark}
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_31_12616)">
|
||||
<path
|
||||
d="M14.7686 12.24C15.4192 12.3787 15.9419 12.704 16.3366 13.216C16.7312 13.7173 16.9286 14.2933 16.9286 14.944C16.9286 15.8827 16.5979 16.6293 15.9366 17.184C15.2859 17.728 14.3739 18 13.2006 18H7.96856V6.768H13.0246C14.1659 6.768 15.0566 7.02933 15.6966 7.552C16.3472 8.07467 16.6726 8.784 16.6726 9.68C16.6726 10.3413 16.4966 10.8907 16.1446 11.328C15.8032 11.7653 15.3446 12.0693 14.7686 12.24ZM10.7046 11.312H12.4966C12.9446 11.312 13.2859 11.216 13.5206 11.024C13.7659 10.8213 13.8886 10.528 13.8886 10.144C13.8886 9.76 13.7659 9.46667 13.5206 9.264C13.2859 9.06133 12.9446 8.96 12.4966 8.96H10.7046V11.312ZM12.7206 15.792C13.1792 15.792 13.5312 15.6907 13.7766 15.488C14.0326 15.2747 14.1606 14.9707 14.1606 14.576C14.1606 14.1813 14.0272 13.872 13.7606 13.648C13.5046 13.424 13.1472 13.312 12.6886 13.312H10.7046V15.792H12.7206Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_31_12616">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.25)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import clsx from 'clsx';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { Transforms } from 'slate';
|
||||
import EmojiPicker from 'emoji-picker-react';
|
||||
import { Theme } from 'emoji-picker-react';
|
||||
import { BoldText } from '@gitroom/frontend/components/new-launch/bold.text';
|
||||
import { UText } from '@gitroom/frontend/components/new-launch/u.text';
|
||||
import { SignatureBox } from '@gitroom/frontend/components/signature';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { AddPostButton } from '@gitroom/frontend/components/new-launch/add.post.button';
|
||||
export const EditorWrapper: FC<{
|
||||
totalPosts: number;
|
||||
value: string;
|
||||
}> = (props) => {
|
||||
const {
|
||||
setGlobalValueText,
|
||||
setInternalValueText,
|
||||
addRemoveInternal,
|
||||
internal,
|
||||
global,
|
||||
current,
|
||||
addInternalValue,
|
||||
addGlobalValue,
|
||||
} = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
internal: state.internal.find((p) => p.integration.id === state.current),
|
||||
global: state.global,
|
||||
current: state.current,
|
||||
addRemoveInternal: state.addRemoveInternal,
|
||||
setInternalValueText: state.setInternalValueText,
|
||||
setGlobalValueText: state.setGlobalValueText,
|
||||
addInternalValue: state.addInternalValue,
|
||||
addGlobalValue: state.addGlobalValue,
|
||||
}))
|
||||
);
|
||||
|
||||
const canEdit = useMemo(() => {
|
||||
return current === 'global' || !!internal;
|
||||
}, [current, internal]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (internal) {
|
||||
return internal.integrationValue;
|
||||
}
|
||||
|
||||
return global;
|
||||
}, [current, internal, global]);
|
||||
|
||||
const changeValue = useCallback(
|
||||
(index: number) => (value: string) => {
|
||||
if (internal) {
|
||||
return setInternalValueText(current, index, value);
|
||||
}
|
||||
|
||||
return setGlobalValueText(index, value);
|
||||
},
|
||||
[current, global, internal]
|
||||
);
|
||||
|
||||
const addValue = useCallback(
|
||||
(index: number) => () => {
|
||||
if (internal) {
|
||||
return addInternalValue(index, current, [
|
||||
{
|
||||
content: '',
|
||||
id: makeId(10),
|
||||
media: [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return addGlobalValue(index, [
|
||||
{
|
||||
content: '',
|
||||
id: makeId(10),
|
||||
media: [],
|
||||
},
|
||||
]);
|
||||
},
|
||||
[current, global, internal]
|
||||
);
|
||||
|
||||
return items.map((g, index) => (
|
||||
<div key={g.id} className="relative flex flex-col gap-[10px]">
|
||||
{!canEdit && (
|
||||
<div
|
||||
onClick={() => addRemoveInternal(current)}
|
||||
className="select-none cursor-pointer absolute w-full h-full left-0 top-0 bg-red-600/10 z-[100]"
|
||||
>
|
||||
<div className="absolute left-[50%] top-[50%] bg-red-400 -translate-x-[50%] z-[101] -translate-y-[50%] border-dashed border p-[10px] border-black">
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Editor
|
||||
onChange={changeValue(index)}
|
||||
key={index}
|
||||
totalPosts={global.length}
|
||||
value={g.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{canEdit && <AddPostButton num={index} onClick={addValue(index)} />}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export const Editor: FC<{
|
||||
totalPosts: number;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}> = (props) => {
|
||||
const user = useUser();
|
||||
const [id] = useState(makeId(10));
|
||||
const newRef = useRef<any>(null);
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
const t = useT();
|
||||
|
||||
const addText = useCallback(
|
||||
(emoji: string) => {
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
Transforms.insertText(newRef?.current?.editor!, emoji);
|
||||
}, 10);
|
||||
},
|
||||
[props.value, id]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="relative bg-customColor2" id={id}>
|
||||
<CopilotTextarea
|
||||
disableBranding={true}
|
||||
ref={newRef}
|
||||
className={clsx(
|
||||
'!min-h-40 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none',
|
||||
props.totalPosts > 1 && '!max-h-80'
|
||||
)}
|
||||
value={props.value}
|
||||
onChange={(e) => {
|
||||
props?.onChange?.(e.target.value);
|
||||
}}
|
||||
// onPaste={props.onPaste}
|
||||
placeholder={t('write_your_reply', 'Write your reply...')}
|
||||
autosuggestionsConfig={{
|
||||
textareaPurpose: `Assist me in writing social media posts.`,
|
||||
chatApiConfigs: {},
|
||||
disabled: !user?.tier?.ai,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-[5px] justify-end bg-customColor2">
|
||||
<SignatureBox editor={newRef?.current?.editor!} />
|
||||
<UText editor={newRef?.current?.editor!} currentValue={props.value!} />
|
||||
<BoldText
|
||||
editor={newRef?.current?.editor!}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<div
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center"
|
||||
onClick={() => setEmojiPickerOpen(!emojiPickerOpen)}
|
||||
>
|
||||
{t('', '\uD83D\uDE00')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute z-[200] end-0">
|
||||
<EmojiPicker
|
||||
theme={(localStorage.getItem('mode') as Theme) || Theme.DARK}
|
||||
onEmojiClick={(e) => {
|
||||
addText(e.emoji);
|
||||
setEmojiPickerOpen(false);
|
||||
}}
|
||||
open={emojiPickerOpen}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
'use client';
|
||||
|
||||
import { Slider } from '@gitroom/react/form/slider';
|
||||
import clsx from 'clsx';
|
||||
import { Editor } from '@gitroom/frontend/components/new-launch/editor';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
||||
export const ThreadFinisher = () => {
|
||||
const integration = useIntegration();
|
||||
const { register, watch, setValue } = useSettings();
|
||||
const t = useT();
|
||||
|
||||
register('active_thread_finisher', {
|
||||
value: false,
|
||||
});
|
||||
|
||||
register('thread_finisher', {
|
||||
value: t('that_a_wrap', {
|
||||
username:
|
||||
integration.integration?.display || integration.integration?.name,
|
||||
}),
|
||||
});
|
||||
|
||||
const slider = watch('active_thread_finisher');
|
||||
const value = watch('thread_finisher');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[10px] border-tableBorder border p-[15px] rounded-lg mb-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1">Add a thread finisher</div>
|
||||
<div>
|
||||
<Slider
|
||||
value={slider ? 'on' : 'off'}
|
||||
onChange={(p) => setValue('active_thread_finisher', p === 'on')}
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full mt-[40px]">
|
||||
<div
|
||||
className={clsx(
|
||||
!slider && 'relative opacity-25 pointer-events-none editor'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex gap-[4px]">
|
||||
<div className="flex-1 editor text-textColor">
|
||||
<Editor
|
||||
onChange={(val) => setValue('thread_finisher', val)}
|
||||
value={value}
|
||||
height={150}
|
||||
totalPosts={1}
|
||||
order={1}
|
||||
preview="edit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { Calendar, TimeInput } from '@mantine/dates';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { isUSCitizen } from './isuscitizen.utils';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const DatePicker: FC<{
|
||||
date: dayjs.Dayjs;
|
||||
onChange: (day: dayjs.Dayjs) => void;
|
||||
}> = (props) => {
|
||||
const { date, onChange } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
const t = useT();
|
||||
|
||||
const changeShow = useCallback(() => {
|
||||
setOpen((prev) => !prev);
|
||||
}, []);
|
||||
const ref = useClickOutside<HTMLDivElement>(() => {
|
||||
setOpen(false);
|
||||
});
|
||||
const changeDate = useCallback(
|
||||
(type: 'date' | 'time') => (day: Date) => {
|
||||
onChange(
|
||||
dayjs(
|
||||
type === 'time'
|
||||
? date.format('YYYY-MM-DD') + ' ' + dayjs(day).format('HH:mm:ss')
|
||||
: dayjs(day).format('YYYY-MM-DD') + ' ' + date.format('HH:mm:ss')
|
||||
)
|
||||
);
|
||||
},
|
||||
[date]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex gap-[8px] items-center relative px-[16px] select-none"
|
||||
onClick={changeShow}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
{date.format(isUSCitizen() ? 'MM/DD/YYYY hh:mm A' : 'DD/MM/YYYY HH:mm')}
|
||||
</div>
|
||||
<div className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="21"
|
||||
viewBox="0 0 20 21"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M16.25 3H14.375V2.375C14.375 2.20924 14.3092 2.05027 14.1919 1.93306C14.0747 1.81585 13.9158 1.75 13.75 1.75C13.5842 1.75 13.4253 1.81585 13.3081 1.93306C13.1908 2.05027 13.125 2.20924 13.125 2.375V3H6.875V2.375C6.875 2.20924 6.80915 2.05027 6.69194 1.93306C6.57473 1.81585 6.41576 1.75 6.25 1.75C6.08424 1.75 5.92527 1.81585 5.80806 1.93306C5.69085 2.05027 5.625 2.20924 5.625 2.375V3H3.75C3.41848 3 3.10054 3.1317 2.86612 3.36612C2.6317 3.60054 2.5 3.91848 2.5 4.25V16.75C2.5 17.0815 2.6317 17.3995 2.86612 17.6339C3.10054 17.8683 3.41848 18 3.75 18H16.25C16.5815 18 16.8995 17.8683 17.1339 17.6339C17.3683 17.3995 17.5 17.0815 17.5 16.75V4.25C17.5 3.91848 17.3683 3.60054 17.1339 3.36612C16.8995 3.1317 16.5815 3 16.25 3ZM16.25 6.75H3.75V4.25H5.625V4.875C5.625 5.04076 5.69085 5.19973 5.80806 5.31694C5.92527 5.43415 6.08424 5.5 6.25 5.5C6.41576 5.5 6.57473 5.43415 6.69194 5.31694C6.80915 5.19973 6.875 5.04076 6.875 4.875V4.25H13.125V4.875C13.125 5.04076 13.1908 5.19973 13.3081 5.31694C13.4253 5.43415 13.5842 5.5 13.75 5.5C13.9158 5.5 14.0747 5.43415 14.1919 5.31694C14.3092 5.19973 14.375 5.04076 14.375 4.875V4.25H16.25V6.75Z"
|
||||
fill="#B69DEC"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{open && (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="animate-normalFadeDown absolute top-[100%] mt-[16px] end-0 bg-sixth border border-tableBorder text-textColor rounded-[16px] z-[300] p-[16px] flex flex-col"
|
||||
>
|
||||
<Calendar
|
||||
onChange={changeDate('date')}
|
||||
value={date.toDate()}
|
||||
dayClassName={(date, modifiers) => {
|
||||
if (modifiers.weekend) {
|
||||
return '!text-customColor28';
|
||||
}
|
||||
if (modifiers.outside) {
|
||||
return '!text-gray';
|
||||
}
|
||||
if (modifiers.selected) {
|
||||
return '!text-white !bg-seventh !outline-none';
|
||||
}
|
||||
return '!text-textColor';
|
||||
}}
|
||||
classNames={{
|
||||
day: 'hover:bg-seventh',
|
||||
calendarHeaderControl: 'text-textColor hover:bg-third',
|
||||
calendarHeaderLevel: 'text-textColor hover:bg-third', // cell: 'child:!text-textColor'
|
||||
}}
|
||||
/>
|
||||
<TimeInput
|
||||
onChange={changeDate('time')}
|
||||
label="Pick time"
|
||||
classNames={{
|
||||
label: 'text-textColor py-[12px]',
|
||||
input:
|
||||
'bg-sixth h-[40px] border border-tableBorder text-textColor rounded-[4px] outline-none',
|
||||
}}
|
||||
defaultValue={date.toDate()}
|
||||
/>
|
||||
<Button className="mt-[12px]" onClick={changeShow}>
|
||||
{t('save', 'Save')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { FC, ReactNode } from 'react';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
export const DNDProvider: FC<{
|
||||
children: ReactNode;
|
||||
}> = ({ children }) => {
|
||||
return <DndProvider backend={HTML5Backend}>{children}</DndProvider>;
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
|
||||
export const isUSCitizen = () => {
|
||||
const userLanguage = navigator.language || navigator.languages[0];
|
||||
return userLanguage.startsWith('en-US');
|
||||
};
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
'use client';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { TopTitle } from '@gitroom/frontend/components/new-launch/helpers/top.title.component';
|
||||
import {
|
||||
executeCommand,
|
||||
ExecuteState,
|
||||
ICommand,
|
||||
selectWord,
|
||||
TextAreaTextApi,
|
||||
} from '@uiw/react-md-editor';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
const postUrlEmitter = new EventEmitter();
|
||||
export const ShowLinkedinCompany = () => {
|
||||
const [showPostSelector, setShowPostSelector] = useState(false);
|
||||
const [id, setId] = useState('');
|
||||
const [callback, setCallback] = useState<{
|
||||
callback: (tag: string) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
} | null>({
|
||||
callback: (tag: string) => {},
|
||||
} as any);
|
||||
useEffect(() => {
|
||||
postUrlEmitter.on(
|
||||
'show',
|
||||
(params: { id: string; callback: (url: string) => void }) => {
|
||||
setCallback(params);
|
||||
setId(params.id);
|
||||
setShowPostSelector(true);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
setShowPostSelector(false);
|
||||
setCallback(null);
|
||||
setId('');
|
||||
postUrlEmitter.removeAllListeners();
|
||||
};
|
||||
}, []);
|
||||
const close = useCallback(() => {
|
||||
setShowPostSelector(false);
|
||||
setCallback(null);
|
||||
setId('');
|
||||
}, []);
|
||||
if (!showPostSelector) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<LinkedinCompany id={id} onClose={close} onSelect={callback?.callback!} />
|
||||
);
|
||||
};
|
||||
export const showPostSelector = (id: string) => {
|
||||
return new Promise<string>((resolve) => {
|
||||
postUrlEmitter.emit('show', {
|
||||
id,
|
||||
callback: (tag: string) => {
|
||||
resolve(tag);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
export const LinkedinCompany: FC<{
|
||||
onClose: () => void;
|
||||
onSelect: (tag: string) => void;
|
||||
id: string;
|
||||
}> = (props) => {
|
||||
const { onClose, onSelect, id } = props;
|
||||
const fetch = useFetch();
|
||||
const [company, setCompany] = useState<any>(null);
|
||||
const toast = useToaster();
|
||||
const t = useT();
|
||||
const getCompany = async () => {
|
||||
if (!company) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { options } = await (
|
||||
await fetch('/integrations/function', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name: 'company',
|
||||
data: {
|
||||
url: company,
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
onSelect(options.value);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast.show('Failed to load profile', 'warning');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="text-textColor fixed start-0 top-0 bg-primary/80 z-[300] w-full h-full p-[60px] animate-fade justify-center flex">
|
||||
<div className="flex flex-col w-[500px] h-[250px] bg-sixth border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TopTitle title={'Select Company'} />
|
||||
</div>
|
||||
<button
|
||||
className="outline-none absolute end-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-primary hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-[10px]">
|
||||
<Input
|
||||
name="url"
|
||||
disableForm={true}
|
||||
label="URL"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
placeholder="https://www.linkedin.com/company/gitroom"
|
||||
/>
|
||||
<Button onClick={getCompany}>{t('add', 'Add')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const linkedinCompany = (identifier: string, id: string): ICommand[] => {
|
||||
if (identifier !== 'linkedin' && identifier !== 'linkedin-page') {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: 'linkedinCompany',
|
||||
keyCommand: 'linkedinCompany',
|
||||
shortcuts: 'ctrlcmd+p',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
buttonProps: {
|
||||
'aria-label': 'Add Post Url',
|
||||
title: 'Add Post Url',
|
||||
},
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M27 3H5C4.46957 3 3.96086 3.21071 3.58579 3.58579C3.21071 3.96086 3 4.46957 3 5V27C3 27.5304 3.21071 28.0391 3.58579 28.4142C3.96086 28.7893 4.46957 29 5 29H27C27.5304 29 28.0391 28.7893 28.4142 28.4142C28.7893 28.0391 29 27.5304 29 27V5C29 4.46957 28.7893 3.96086 28.4142 3.58579C28.0391 3.21071 27.5304 3 27 3ZM27 27H5V5H27V27ZM12 14V22C12 22.2652 11.8946 22.5196 11.7071 22.7071C11.5196 22.8946 11.2652 23 11 23C10.7348 23 10.4804 22.8946 10.2929 22.7071C10.1054 22.5196 10 22.2652 10 22V14C10 13.7348 10.1054 13.4804 10.2929 13.2929C10.4804 13.1054 10.7348 13 11 13C11.2652 13 11.5196 13.1054 11.7071 13.2929C11.8946 13.4804 12 13.7348 12 14ZM23 17.5V22C23 22.2652 22.8946 22.5196 22.7071 22.7071C22.5196 22.8946 22.2652 23 22 23C21.7348 23 21.4804 22.8946 21.2929 22.7071C21.1054 22.5196 21 22.2652 21 22V17.5C21 16.837 20.7366 16.2011 20.2678 15.7322C19.7989 15.2634 19.163 15 18.5 15C17.837 15 17.2011 15.2634 16.7322 15.7322C16.2634 16.2011 16 16.837 16 17.5V22C16 22.2652 15.8946 22.5196 15.7071 22.7071C15.5196 22.8946 15.2652 23 15 23C14.7348 23 14.4804 22.8946 14.2929 22.7071C14.1054 22.5196 14 22.2652 14 22V14C14.0012 13.7551 14.0923 13.5191 14.256 13.3369C14.4197 13.1546 14.6446 13.0388 14.888 13.0114C15.1314 12.9839 15.3764 13.0468 15.5765 13.188C15.7767 13.3292 15.918 13.539 15.9738 13.7775C16.6502 13.3186 17.4389 13.0526 18.2552 13.0081C19.0714 12.9637 19.8844 13.1424 20.6067 13.5251C21.329 13.9078 21.9335 14.48 22.3551 15.1803C22.7768 15.8806 22.9997 16.6825 23 17.5ZM12.5 10.5C12.5 10.7967 12.412 11.0867 12.2472 11.3334C12.0824 11.58 11.8481 11.7723 11.574 11.8858C11.2999 11.9994 10.9983 12.0291 10.7074 11.9712C10.4164 11.9133 10.1491 11.7704 9.93934 11.5607C9.72956 11.3509 9.5867 11.0836 9.52882 10.7926C9.47094 10.5017 9.50065 10.2001 9.61418 9.92597C9.72771 9.65189 9.91997 9.41762 10.1666 9.2528C10.4133 9.08797 10.7033 9 11 9C11.3978 9 11.7794 9.15804 12.0607 9.43934C12.342 9.72064 12.5 10.1022 12.5 10.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
execute: async (state: ExecuteState, api: TextAreaTextApi) => {
|
||||
const newSelectionRange = selectWord({
|
||||
text: state.text,
|
||||
selection: state.selection,
|
||||
prefix: state.command.prefix!,
|
||||
suffix: state.command.suffix,
|
||||
});
|
||||
const state1 = api.setSelectionRange(newSelectionRange);
|
||||
const media = await showPostSelector(id);
|
||||
executeCommand({
|
||||
api,
|
||||
selectedText: state1.selectedText,
|
||||
selection: state.selection,
|
||||
prefix: media,
|
||||
suffix: '',
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
executeCommand,
|
||||
ExecuteState,
|
||||
ICommand,
|
||||
selectWord,
|
||||
TextAreaTextApi,
|
||||
} from '@uiw/react-md-editor';
|
||||
import { showMediaBox } from '@gitroom/frontend/components/media/media.component';
|
||||
import { loadVars } from '@gitroom/react/helpers/variable.context';
|
||||
export const newImage: ICommand = {
|
||||
name: 'image',
|
||||
keyCommand: 'image',
|
||||
shortcuts: 'ctrlcmd+k',
|
||||
prefix: '',
|
||||
buttonProps: {
|
||||
'aria-label': 'Add image (ctrl + k)',
|
||||
title: 'Add image (ctrl + k)',
|
||||
},
|
||||
icon: (
|
||||
<svg width="13" height="13" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
execute: (state: ExecuteState, api: TextAreaTextApi) => {
|
||||
const { uploadDirectory, backendUrl } = loadVars();
|
||||
let newSelectionRange = selectWord({
|
||||
text: state.text,
|
||||
selection: state.selection,
|
||||
prefix: state.command.prefix!,
|
||||
suffix: state.command.suffix,
|
||||
});
|
||||
let state1 = api.setSelectionRange(newSelectionRange);
|
||||
if (
|
||||
state1.selectedText.includes('http') ||
|
||||
state1.selectedText.includes('www') ||
|
||||
state1.selectedText.includes('(post:')
|
||||
) {
|
||||
executeCommand({
|
||||
api,
|
||||
selectedText: state1.selectedText,
|
||||
selection: state.selection,
|
||||
prefix: state.command.prefix!,
|
||||
suffix: state.command.suffix,
|
||||
});
|
||||
return;
|
||||
}
|
||||
newSelectionRange = selectWord({
|
||||
text: state.text,
|
||||
selection: state.selection,
|
||||
prefix: '![',
|
||||
suffix: ']()',
|
||||
});
|
||||
state1 = api.setSelectionRange(newSelectionRange);
|
||||
showMediaBox((media) => {
|
||||
if (media) {
|
||||
if (state1.selectedText.length > 0) {
|
||||
executeCommand({
|
||||
api,
|
||||
selectedText: state1.selectedText,
|
||||
selection: state.selection,
|
||||
prefix: ' === -1
|
||||
? `${backendUrl}/${uploadDirectory}`
|
||||
: ``
|
||||
}${media.path})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
executeCommand({
|
||||
api,
|
||||
selectedText: state1.selectedText,
|
||||
selection: state.selection,
|
||||
prefix: ' === -1
|
||||
? `${backendUrl}/${uploadDirectory}`
|
||||
: ``
|
||||
}${media.path})`,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
'use client';
|
||||
|
||||
import { FC, ReactNode } from 'react';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
||||
export const TopTitle: FC<{
|
||||
title: string;
|
||||
shouldExpend?: boolean;
|
||||
removeTitle?: boolean;
|
||||
expend?: () => void;
|
||||
collapse?: () => void;
|
||||
children?: ReactNode;
|
||||
}> = (props) => {
|
||||
const { title, removeTitle, children, shouldExpend, expend, collapse } =
|
||||
props;
|
||||
const t = useT();
|
||||
|
||||
// Translate the title using a key derived from the title itself
|
||||
// This creates a consistent key pattern for each title
|
||||
const translatedTitle = t(
|
||||
// Convert to lowercase, replace spaces with underscores
|
||||
`top_title_${title
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^\w]/g, '')}`,
|
||||
title
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-[57px] border-b flex items-center border-customColor6 px-[16px] -mx-[16px]">
|
||||
{!removeTitle && <div className="flex-1">{translatedTitle}</div>}
|
||||
{children}
|
||||
{shouldExpend !== undefined && (
|
||||
<div className="cursor-pointer">
|
||||
{!shouldExpend ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
onClick={expend}
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M20.25 5V9.5C20.25 9.69891 20.171 9.88968 20.0303 10.0303C19.8897 10.171 19.6989 10.25 19.5 10.25C19.3011 10.25 19.1103 10.171 18.9697 10.0303C18.829 9.88968 18.75 9.69891 18.75 9.5V6.81031L14.0306 11.5306C13.8899 11.6714 13.699 11.7504 13.5 11.7504C13.301 11.7504 13.1101 11.6714 12.9694 11.5306C12.8286 11.3899 12.7496 11.199 12.7496 11C12.7496 10.801 12.8286 10.6101 12.9694 10.4694L17.6897 5.75H15C14.8011 5.75 14.6103 5.67098 14.4697 5.53033C14.329 5.38968 14.25 5.19891 14.25 5C14.25 4.80109 14.329 4.61032 14.4697 4.46967C14.6103 4.32902 14.8011 4.25 15 4.25H19.5C19.6989 4.25 19.8897 4.32902 20.0303 4.46967C20.171 4.61032 20.25 4.80109 20.25 5ZM9.96937 13.4694L5.25 18.1897V15.5C5.25 15.3011 5.17098 15.1103 5.03033 14.9697C4.88968 14.829 4.69891 14.75 4.5 14.75C4.30109 14.75 4.11032 14.829 3.96967 14.9697C3.82902 15.1103 3.75 15.3011 3.75 15.5V20C3.75 20.1989 3.82902 20.3897 3.96967 20.5303C4.11032 20.671 4.30109 20.75 4.5 20.75H9C9.19891 20.75 9.38968 20.671 9.53033 20.5303C9.67098 20.3897 9.75 20.1989 9.75 20C9.75 19.8011 9.67098 19.6103 9.53033 19.4697C9.38968 19.329 9.19891 19.25 9 19.25H6.31031L11.0306 14.5306C11.1714 14.3899 11.2504 14.199 11.2504 14C11.2504 13.801 11.1714 13.6101 11.0306 13.4694C10.8899 13.3286 10.699 13.2496 10.5 13.2496C10.301 13.2496 10.1101 13.3286 9.96937 13.4694Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
onClick={collapse}
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.5004 10.2499V6.49993C13.5004 6.30102 13.5794 6.11025 13.7201 5.9696C13.8607 5.82895 14.0515 5.74993 14.2504 5.74993C14.4493 5.74993 14.6401 5.82895 14.7807 5.9696C14.9214 6.11025 15.0004 6.30102 15.0004 6.49993V8.43962L18.9698 4.4693C19.1105 4.32857 19.3014 4.24951 19.5004 4.24951C19.6994 4.24951 19.8903 4.32857 20.031 4.4693C20.1718 4.61003 20.2508 4.80091 20.2508 4.99993C20.2508 5.19895 20.1718 5.38982 20.031 5.53055L16.0607 9.49993H18.0004C18.1993 9.49993 18.3901 9.57895 18.5307 9.7196C18.6714 9.86025 18.7504 10.051 18.7504 10.2499C18.7504 10.4488 18.6714 10.6396 18.5307 10.7803C18.3901 10.9209 18.1993 10.9999 18.0004 10.9999H14.2504C14.0515 10.9999 13.8607 10.9209 13.7201 10.7803C13.5794 10.6396 13.5004 10.4488 13.5004 10.2499ZM9.75042 13.9999H6.00042C5.8015 13.9999 5.61074 14.0789 5.47009 14.2196C5.32943 14.3603 5.25042 14.551 5.25042 14.7499C5.25042 14.9488 5.32943 15.1396 5.47009 15.2803C5.61074 15.4209 5.8015 15.4999 6.00042 15.4999H7.9401L3.96979 19.4693C3.82906 19.61 3.75 19.8009 3.75 19.9999C3.75 20.199 3.82906 20.3898 3.96979 20.5306C4.11052 20.6713 4.30139 20.7503 4.50042 20.7503C4.69944 20.7503 4.89031 20.6713 5.03104 20.5306L9.00042 16.5602V18.4999C9.00042 18.6988 9.07943 18.8896 9.22009 19.0303C9.36074 19.1709 9.5515 19.2499 9.75042 19.2499C9.94933 19.2499 10.1401 19.1709 10.2807 19.0303C10.4214 18.8896 10.5004 18.6988 10.5004 18.4999V14.7499C10.5004 14.551 10.4214 14.3603 10.2807 14.2196C10.1401 14.0789 9.94933 13.9999 9.75042 13.9999ZM16.0607 15.4999H18.0004C18.1993 15.4999 18.3901 15.4209 18.5307 15.2803C18.6714 15.1396 18.7504 14.9488 18.7504 14.7499C18.7504 14.551 18.6714 14.3603 18.5307 14.2196C18.3901 14.0789 18.1993 13.9999 18.0004 13.9999H14.2504C14.0515 13.9999 13.8607 14.0789 13.7201 14.2196C13.5794 14.3603 13.5004 14.551 13.5004 14.7499V18.4999C13.5004 18.6988 13.5794 18.8896 13.7201 19.0303C13.8607 19.1709 14.0515 19.2499 14.2504 19.2499C14.4493 19.2499 14.6401 19.1709 14.7807 19.0303C14.9214 18.8896 15.0004 18.6988 15.0004 18.4999V16.5602L18.9698 20.5306C19.0395 20.6002 19.1222 20.6555 19.2132 20.6932C19.3043 20.7309 19.4019 20.7503 19.5004 20.7503C19.599 20.7503 19.6965 20.7309 19.7876 20.6932C19.8786 20.6555 19.9614 20.6002 20.031 20.5306C20.1007 20.4609 20.156 20.3781 20.1937 20.2871C20.2314 20.1961 20.2508 20.0985 20.2508 19.9999C20.2508 19.9014 20.2314 19.8038 20.1937 19.7128C20.156 19.6217 20.1007 19.539 20.031 19.4693L16.0607 15.4999ZM9.75042 5.74993C9.5515 5.74993 9.36074 5.82895 9.22009 5.9696C9.07943 6.11025 9.00042 6.30102 9.00042 6.49993V8.43962L5.03104 4.4693C4.89031 4.32857 4.69944 4.24951 4.50042 4.24951C4.30139 4.24951 4.11052 4.32857 3.96979 4.4693C3.82906 4.61003 3.75 4.80091 3.75 4.99993C3.75 5.19895 3.82906 5.38982 3.96979 5.53055L7.9401 9.49993H6.00042C5.8015 9.49993 5.61074 9.57895 5.47009 9.7196C5.32943 9.86025 5.25042 10.051 5.25042 10.2499C5.25042 10.4488 5.32943 10.6396 5.47009 10.7803C5.61074 10.9209 5.8015 10.9999 6.00042 10.9999H9.75042C9.94933 10.9999 10.1401 10.9209 10.2807 10.7803C10.4214 10.6396 10.5004 10.4488 10.5004 10.2499V6.49993C10.5004 6.30102 10.4214 6.11025 10.2807 5.9696C10.1401 5.82895 9.94933 5.74993 9.75042 5.74993Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
'use client';
|
||||
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useCallback } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
export const useCustomProviderFunction = () => {
|
||||
const { integration } = useIntegration();
|
||||
const fetch = useFetch();
|
||||
const get = useCallback(
|
||||
async (funcName: string, customData?: any) => {
|
||||
const load = await fetch('/integrations/function', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: funcName,
|
||||
id: integration?.id!,
|
||||
data: customData,
|
||||
}),
|
||||
});
|
||||
if (load.status > 299 && load.status < 200) {
|
||||
throw new Error('Failed to fetch');
|
||||
}
|
||||
return load.json();
|
||||
},
|
||||
[integration]
|
||||
);
|
||||
return {
|
||||
get,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, FC, ReactNode, useContext } from 'react';
|
||||
import { Post } from '@prisma/client';
|
||||
const ExistingDataContext = createContext({
|
||||
integration: '',
|
||||
group: undefined as undefined | string,
|
||||
posts: [] as Post[],
|
||||
settings: {} as any,
|
||||
});
|
||||
export const ExistingDataContextProvider: FC<{
|
||||
children: ReactNode;
|
||||
value: any;
|
||||
}> = ({ children, value }) => {
|
||||
return (
|
||||
<ExistingDataContext.Provider value={value}>
|
||||
{children}
|
||||
</ExistingDataContext.Provider>
|
||||
);
|
||||
};
|
||||
export const useExistingData = () => useContext(ExistingDataContext);
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use client';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { useEffect, useState } from 'react';
|
||||
const emitter = new EventEmitter();
|
||||
export const useExpend = () => {
|
||||
const [expend, setExpend] = useState(false);
|
||||
useEffect(() => {
|
||||
const hide = () => {
|
||||
setExpend(false);
|
||||
};
|
||||
const show = () => {
|
||||
setExpend(true);
|
||||
};
|
||||
emitter.on('hide', hide);
|
||||
emitter.on('show', show);
|
||||
return () => {
|
||||
emitter.off('hide', hide);
|
||||
emitter.off('show', show);
|
||||
};
|
||||
}, []);
|
||||
return {
|
||||
expend,
|
||||
hide: () => {
|
||||
emitter.emit('hide');
|
||||
},
|
||||
show: () => {
|
||||
emitter.emit('show');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
export const useFormatting = (
|
||||
text: Array<{
|
||||
content: string;
|
||||
image?: Array<{
|
||||
id: string;
|
||||
path: string;
|
||||
}>;
|
||||
id?: string;
|
||||
}>,
|
||||
params: {
|
||||
removeMarkdown?: boolean;
|
||||
saveBreaklines?: boolean;
|
||||
specialFunc?: (text: string) => any;
|
||||
beforeSpecialFunc?: (text: string) => string;
|
||||
}
|
||||
) => {
|
||||
return useMemo(() => {
|
||||
return text.map((value) => {
|
||||
let newText = value.content;
|
||||
if (params.beforeSpecialFunc) {
|
||||
newText = params.beforeSpecialFunc(newText);
|
||||
}
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('\n', '𝔫𝔢𝔴𝔩𝔦𝔫𝔢');
|
||||
}
|
||||
newText = newText.replace(/@\w{1,15}/g, function (match) {
|
||||
return `<strong>${match}</strong>`;
|
||||
});
|
||||
if (params.saveBreaklines) {
|
||||
newText = newText.replace('𝔫𝔢𝔴𝔩𝔦𝔫𝔢', '\n');
|
||||
}
|
||||
if (params.specialFunc) {
|
||||
newText = params.specialFunc(newText);
|
||||
}
|
||||
return {
|
||||
id: value.id,
|
||||
text: newText,
|
||||
images: value.image,
|
||||
count:
|
||||
params.removeMarkdown && params.saveBreaklines
|
||||
? newText.replace(/\n/g, ' ').length
|
||||
: newText.length,
|
||||
};
|
||||
});
|
||||
}, [text]);
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'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,27 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
|
||||
const IntegrationContext = createContext<{
|
||||
date: dayjs.Dayjs;
|
||||
integration: Integrations | undefined;
|
||||
allIntegrations: Integrations[];
|
||||
value: Array<{
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{
|
||||
path: string;
|
||||
id: string;
|
||||
}>;
|
||||
}>;
|
||||
}>({
|
||||
integration: undefined,
|
||||
value: [],
|
||||
date: dayjs(),
|
||||
allIntegrations: [],
|
||||
});
|
||||
|
||||
const useIntegration = () => useContext(IntegrationContext);
|
||||
export {IntegrationContext, useIntegration};
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
'use client';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
const emitter = new EventEmitter();
|
||||
export const useMoveToIntegration = () => {
|
||||
return useCallback(
|
||||
({
|
||||
identifier,
|
||||
toPreview,
|
||||
}: {
|
||||
identifier: string;
|
||||
toPreview?: boolean;
|
||||
}) => {
|
||||
emitter.emit('moveToIntegration', {
|
||||
identifier,
|
||||
toPreview,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
export const useMoveToIntegrationListener = (
|
||||
useEffectParams: any[],
|
||||
enabled: boolean,
|
||||
callback: ({
|
||||
identifier,
|
||||
toPreview,
|
||||
}: {
|
||||
identifier: string;
|
||||
toPreview: boolean;
|
||||
}) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
return load();
|
||||
}, useEffectParams);
|
||||
const load = useCallback(() => {
|
||||
emitter.off('moveToIntegration', callback);
|
||||
emitter.on('moveToIntegration', callback);
|
||||
return () => {
|
||||
emitter.off('moveToIntegration', callback);
|
||||
};
|
||||
}, useEffectParams);
|
||||
};
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useForm, useFormContext } from 'react-hook-form';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
class Empty {
|
||||
@IsOptional()
|
||||
empty: string;
|
||||
}
|
||||
|
||||
const finalInformation = {} as {
|
||||
[key: string]: {
|
||||
posts: Array<{
|
||||
id?: string;
|
||||
content: string;
|
||||
media?: Array<string>;
|
||||
}>;
|
||||
settings: () => object;
|
||||
trigger: () => Promise<boolean>;
|
||||
isValid: boolean;
|
||||
checkValidity?: (
|
||||
value: Array<
|
||||
Array<{
|
||||
path: string;
|
||||
}>
|
||||
>,
|
||||
settings: any,
|
||||
additionalSettings: any,
|
||||
) => Promise<string | true>;
|
||||
maximumCharacters?: number;
|
||||
};
|
||||
};
|
||||
export const useValues = (
|
||||
initialValues: object,
|
||||
integration: string,
|
||||
identifier: string,
|
||||
value: Array<{
|
||||
id?: string;
|
||||
content: string;
|
||||
media?: Array<string>;
|
||||
}>,
|
||||
dto: any,
|
||||
checkValidity?: (
|
||||
value: Array<
|
||||
Array<{
|
||||
path: string;
|
||||
}>
|
||||
>,
|
||||
settings: any,
|
||||
additionalSettings: any,
|
||||
) => Promise<string | true>,
|
||||
maximumCharacters?: number
|
||||
) => {
|
||||
|
||||
const form = useForm({
|
||||
resolver: classValidatorResolver(dto || Empty),
|
||||
values: initialValues,
|
||||
mode: 'onChange',
|
||||
criteriaMode: 'all',
|
||||
});
|
||||
|
||||
const getValues = useMemo(() => {
|
||||
return () => ({
|
||||
...form.getValues(),
|
||||
__type: identifier,
|
||||
});
|
||||
}, [form, integration]);
|
||||
|
||||
// @ts-ignore
|
||||
finalInformation[integration] = finalInformation[integration] || {};
|
||||
finalInformation[integration].posts = value;
|
||||
finalInformation[integration].isValid = form.formState.isValid;
|
||||
finalInformation[integration].settings = getValues;
|
||||
finalInformation[integration].trigger = form.trigger;
|
||||
if (checkValidity) {
|
||||
finalInformation[integration].checkValidity = checkValidity;
|
||||
}
|
||||
if (maximumCharacters) {
|
||||
finalInformation[integration].maximumCharacters = maximumCharacters;
|
||||
}
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
delete finalInformation[integration];
|
||||
};
|
||||
}, []);
|
||||
return form;
|
||||
};
|
||||
export const useSettings = () => useFormContext();
|
||||
export const getValues = () => finalInformation;
|
||||
export const resetValues = () => {
|
||||
Object.keys(finalInformation).forEach((key) => {
|
||||
delete finalInformation[key];
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
export const PicksSocialsComponent: FC<{ toolTip?: boolean }> = ({
|
||||
toolTip,
|
||||
}) => {
|
||||
const { addOrRemoveSelectedIntegration, integrations, selectedIntegrations } =
|
||||
useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
integrations: state.integrations,
|
||||
selectedIntegrations: state.selectedIntegrations,
|
||||
addOrRemoveSelectedIntegration: state.addOrRemoveSelectedIntegration,
|
||||
}))
|
||||
);
|
||||
return (
|
||||
<div className={clsx('flex')}>
|
||||
<div className="flex">
|
||||
<div className="innerComponent">
|
||||
<div className="flex">
|
||||
{integrations
|
||||
.filter((f) => !f.inBetweenSteps)
|
||||
.map((integration) => (
|
||||
<div
|
||||
key={integration.id}
|
||||
className="flex gap-[8px] items-center me-[10px]"
|
||||
{...(toolTip && {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content': integration.name,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
addOrRemoveSelectedIntegration(integration, {})
|
||||
}
|
||||
className={clsx(
|
||||
'cursor-pointer relative w-[34px] h-[34px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
selectedIntegrations.findIndex(
|
||||
(p) => p.integration.id === integration.id
|
||||
) === -1
|
||||
? 'opacity-40'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{integration.identifier === 'youtube' ? (
|
||||
<img
|
||||
src="/icons/platforms/youtube.svg"
|
||||
className="absolute z-10 -bottom-[5px] -end-[5px]"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -end-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { ThreadFinisher } from '@gitroom/frontend/components/new-launch/finisher/thread.finisher';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
const SettingsComponent = () => {
|
||||
const form = useFormContext();
|
||||
return <ThreadFinisher />;
|
||||
};
|
||||
|
||||
export default withProvider(
|
||||
SettingsComponent,
|
||||
undefined,
|
||||
undefined,
|
||||
async (posts) => {
|
||||
if (
|
||||
posts.some(
|
||||
(p) => p.some((a) => a.path.indexOf('mp4') > -1) && p.length > 1
|
||||
)
|
||||
) {
|
||||
return 'You can only upload one video to Bluesky per post.';
|
||||
}
|
||||
|
||||
if (posts.some((p) => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
300
|
||||
);
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import useSWR from 'swr';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const FacebookContinue: FC<{
|
||||
closeModal: () => void;
|
||||
existingId: string[];
|
||||
}> = (props) => {
|
||||
const { closeModal, existingId } = props;
|
||||
const call = useCustomProviderFunction();
|
||||
const { integration } = useIntegration();
|
||||
const [page, setSelectedPage] = useState<null | string>(null);
|
||||
const fetch = useFetch();
|
||||
const loadPages = useCallback(async () => {
|
||||
try {
|
||||
const pages = await call.get('pages');
|
||||
return pages;
|
||||
} catch (e) {
|
||||
closeModal();
|
||||
}
|
||||
}, []);
|
||||
const setPage = useCallback(
|
||||
(id: string) => () => {
|
||||
setSelectedPage(id);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, isLoading } = useSWR('load-pages', loadPages, {
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
revalidateOnReconnect: false,
|
||||
refreshInterval: 0,
|
||||
});
|
||||
const t = useT();
|
||||
|
||||
const saveInstagram = useCallback(async () => {
|
||||
await fetch(`/integrations/facebook/${integration?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
page,
|
||||
}),
|
||||
});
|
||||
closeModal();
|
||||
}, [integration, page]);
|
||||
const filteredData = useMemo(() => {
|
||||
return (
|
||||
data?.filter((p: { id: string }) => !existingId.includes(p.id)) || []
|
||||
);
|
||||
}, [data]);
|
||||
if (!isLoading && !data?.length) {
|
||||
return (
|
||||
<div className="text-center flex justify-center items-center text-[18px] leading-[50px] h-[300px]">
|
||||
{t(
|
||||
'we_couldn_t_find_any_business_connected_to_the_selected_pages',
|
||||
"We couldn't find any business connected to the selected pages."
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'we_recommend_you_to_connect_all_the_pages_and_all_the_businesses',
|
||||
'We recommend you to connect all the pages and all the businesses.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'please_close_this_dialog_delete_your_integration_and_add_a_new_channel_again',
|
||||
'Please close this dialog, delete your integration and add a new channel\n again.'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div>{t('select_page', 'Select Page:')}</div>
|
||||
<div className="grid grid-cols-3 justify-items-center select-none cursor-pointer">
|
||||
{filteredData?.map(
|
||||
(p: {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
picture: {
|
||||
data: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
}) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={clsx(
|
||||
'flex flex-col w-full text-center gap-[10px] border border-input p-[10px] hover:bg-seventh',
|
||||
page === p.id && 'bg-seventh'
|
||||
)}
|
||||
onClick={setPage(p.id)}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
className="w-full"
|
||||
src={p.picture.data.url}
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<div>{p.name}</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={!page} onClick={saveInstagram}>
|
||||
{t('save', 'Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import useSWR from 'swr';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const InstagramContinue: FC<{
|
||||
closeModal: () => void;
|
||||
existingId: string[];
|
||||
}> = (props) => {
|
||||
const { closeModal, existingId } = props;
|
||||
const call = useCustomProviderFunction();
|
||||
const { integration } = useIntegration();
|
||||
const [page, setSelectedPage] = useState<null | {
|
||||
id: string;
|
||||
pageId: string;
|
||||
}>(null);
|
||||
const fetch = useFetch();
|
||||
const loadPages = useCallback(async () => {
|
||||
try {
|
||||
const pages = await call.get('pages');
|
||||
return pages;
|
||||
} catch (e) {
|
||||
closeModal();
|
||||
}
|
||||
}, []);
|
||||
const t = useT();
|
||||
|
||||
const setPage = useCallback(
|
||||
(param: { id: string; pageId: string }) => () => {
|
||||
setSelectedPage(param);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, isLoading } = useSWR('load-pages', loadPages, {
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
revalidateOnReconnect: false,
|
||||
refreshInterval: 0,
|
||||
});
|
||||
const saveInstagram = useCallback(async () => {
|
||||
await fetch(`/integrations/instagram/${integration?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(page),
|
||||
});
|
||||
closeModal();
|
||||
}, [integration, page]);
|
||||
const filteredData = useMemo(() => {
|
||||
return (
|
||||
data?.filter((p: { id: string }) => !existingId.includes(p.id)) || []
|
||||
);
|
||||
}, [data]);
|
||||
if (!isLoading && !data?.length) {
|
||||
return (
|
||||
<div className="text-center flex justify-center items-center text-[18px] leading-[50px] h-[300px]">
|
||||
{t(
|
||||
'we_couldn_t_find_any_business_connected_to_the_selected_pages',
|
||||
"We couldn't find any business connected to the selected pages."
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'we_recommend_you_to_connect_all_the_pages_and_all_the_businesses',
|
||||
'We recommend you to connect all the pages and all the businesses.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'please_close_this_dialog_delete_your_integration_and_add_a_new_channel_again',
|
||||
'Please close this dialog, delete your integration and add a new channel\n again.'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div>{t('select_instagram_account', 'Select Instagram Account:')}</div>
|
||||
<div className="grid grid-cols-3 justify-items-center select-none cursor-pointer">
|
||||
{filteredData?.map(
|
||||
(p: {
|
||||
id: string;
|
||||
pageId: string;
|
||||
username: string;
|
||||
name: string;
|
||||
picture: {
|
||||
data: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
}) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={clsx(
|
||||
'flex flex-col w-full text-center gap-[10px] border border-input p-[10px] hover:bg-seventh',
|
||||
page?.id === p.id && 'bg-seventh'
|
||||
)}
|
||||
onClick={setPage(p)}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
className="w-full"
|
||||
src={p.picture.data.url}
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<div>{p.name}</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={!page} onClick={saveInstagram}>
|
||||
{t('save', 'Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import useSWR from 'swr';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const LinkedinContinue: FC<{
|
||||
closeModal: () => void;
|
||||
existingId: string[];
|
||||
}> = (props) => {
|
||||
const { closeModal, existingId } = props;
|
||||
const t = useT();
|
||||
|
||||
const call = useCustomProviderFunction();
|
||||
const { integration } = useIntegration();
|
||||
const [page, setSelectedPage] = useState<null | {
|
||||
id: string;
|
||||
pageId: string;
|
||||
}>(null);
|
||||
const fetch = useFetch();
|
||||
const loadPages = useCallback(async () => {
|
||||
try {
|
||||
const pages = await call.get('companies');
|
||||
return pages;
|
||||
} catch (e) {
|
||||
closeModal();
|
||||
}
|
||||
}, []);
|
||||
const setPage = useCallback(
|
||||
(param: { id: string; pageId: string }) => () => {
|
||||
setSelectedPage(param);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, isLoading } = useSWR('load-pages', loadPages, {
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnMount: true,
|
||||
revalidateOnReconnect: false,
|
||||
refreshInterval: 0,
|
||||
});
|
||||
const saveLinkedin = useCallback(async () => {
|
||||
await fetch(`/integrations/linkedin-page/${integration?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(page),
|
||||
});
|
||||
closeModal();
|
||||
}, [integration, page]);
|
||||
const filteredData = useMemo(() => {
|
||||
return (
|
||||
data?.filter((p: { id: string }) => !existingId.includes(p.id)) || []
|
||||
);
|
||||
}, [data]);
|
||||
if (!isLoading && !data?.length) {
|
||||
return (
|
||||
<div className="text-center flex justify-center items-center text-[18px] leading-[50px] h-[300px]">
|
||||
{t(
|
||||
'we_couldn_t_find_any_business_connected_to_your_linkedin_page',
|
||||
"We couldn't find any business connected to your LinkedIn Page."
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'please_close_this_dialog_create_a_new_page_and_add_a_new_channel_again',
|
||||
'Please close this dialog, create a new page, and add a new channel again.'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div>{t('select_linkedin_page', 'Select Linkedin Page:')}</div>
|
||||
<div className="grid grid-cols-3 justify-items-center select-none cursor-pointer">
|
||||
{filteredData?.map(
|
||||
(p: {
|
||||
id: string;
|
||||
pageId: string;
|
||||
username: string;
|
||||
name: string;
|
||||
picture: string;
|
||||
}) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={clsx(
|
||||
'flex flex-col w-full text-center gap-[10px] border border-input p-[10px] hover:bg-seventh',
|
||||
page?.id === p.id && 'bg-seventh'
|
||||
)}
|
||||
onClick={setPage(p)}
|
||||
>
|
||||
<div>
|
||||
<img className="w-full" src={p.picture} alt="profile" />
|
||||
</div>
|
||||
<div>{p.name}</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={!page} onClick={saveLinkedin}>
|
||||
{t('save', 'Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { InstagramContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/instagram/instagram.continue';
|
||||
import { FacebookContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/facebook/facebook.continue';
|
||||
import { LinkedinContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/linkedin/linkedin.continue';
|
||||
export const continueProviderList = {
|
||||
instagram: InstagramContinue,
|
||||
facebook: FacebookContinue,
|
||||
'linkedin-page': LinkedinContinue,
|
||||
};
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { SelectOrganization } from '@gitroom/frontend/components/new-launch/providers/devto/select.organization';
|
||||
import { DevtoTags } from '@gitroom/frontend/components/new-launch/providers/devto/devto.tags';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import clsx from 'clsx';
|
||||
import localFont from 'next/font/local';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { Canonical } from '@gitroom/react/form/canonical';
|
||||
const font = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/SFNS.woff2',
|
||||
},
|
||||
],
|
||||
});
|
||||
const DevtoPreview: FC = () => {
|
||||
const { value } = useIntegration();
|
||||
const settings = useSettings();
|
||||
const image = useMediaDirectory();
|
||||
const [coverPicture, title, tags] = settings.watch([
|
||||
'main_image',
|
||||
'title',
|
||||
'tags',
|
||||
]);
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
font.className,
|
||||
'font-[800] flex h-[1000px] w-[699.8px] rounded-[10px] bg-customColor32 overflow-hidden overflow-y-auto flex-col gap-[32px]'
|
||||
)}
|
||||
>
|
||||
{!!coverPicture?.path && (
|
||||
<div className="h-[338.672px]">
|
||||
<img
|
||||
className="object-cover w-full h-full"
|
||||
src={image.set(coverPicture.path)}
|
||||
alt="cover_picture"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-[60px]">
|
||||
<div className="text-[48px] leading-[60px] mb-[8px]">{title}</div>
|
||||
<div className="flex gap-[16px]">
|
||||
{tags?.map((p: any) => (
|
||||
<div key={p.label}>#{p.label}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-[60px]">
|
||||
<MDEditor.Markdown
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
className={font.className}
|
||||
skipHtml={true}
|
||||
source={value.map((p) => p.content).join('\n')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const DevtoSettings: FC = () => {
|
||||
const form = useSettings();
|
||||
const { date } = useIntegration();
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Canonical
|
||||
date={date}
|
||||
label="Canonical Link"
|
||||
{...form.register('canonical')}
|
||||
/>
|
||||
<MediaComponent
|
||||
label="Cover picture"
|
||||
description="Add a cover picture"
|
||||
{...form.register('main_image')}
|
||||
/>
|
||||
<div className="mt-[20px]">
|
||||
<SelectOrganization {...form.register('organization')} />
|
||||
</div>
|
||||
<div>
|
||||
<DevtoTags label="Tags (Maximum 4)" {...form.register('tags')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withProvider(DevtoSettings, DevtoPreview, DevToSettingsDto);
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { ReactTags } from 'react-tag-autocomplete';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
export const DevtoTags: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: any[];
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [tags, setTags] = useState<any[]>([]);
|
||||
const { getValues } = useSettings();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
if (tagValue.length >= 4) {
|
||||
return;
|
||||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
useEffect(() => {
|
||||
customFunc.get('tags').then((data) => setTags(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!tags.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
|
||||
<ReactTags
|
||||
suggestions={tags}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const SelectOrganization: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [orgs, setOrgs] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('organizations').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!orgs.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select organization"
|
||||
onChange={onChangeInner}
|
||||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{orgs.map((org: any) => (
|
||||
<option key={org.id} value={org.id}>
|
||||
{org.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const DiscordChannelSelect: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [publications, setOrgs] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('channels').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!publications.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select Channel"
|
||||
onChange={onChangeInner}
|
||||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{publications.map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { FC } from 'react';
|
||||
import { DiscordDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/discord.dto';
|
||||
import { DiscordChannelSelect } from '@gitroom/frontend/components/new-launch/providers/discord/discord.channel.select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
const DiscordComponent: FC = () => {
|
||||
const form = useSettings();
|
||||
return (
|
||||
<div>
|
||||
<DiscordChannelSelect {...form.register('channel')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
DiscordComponent,
|
||||
undefined,
|
||||
DiscordDto,
|
||||
undefined,
|
||||
1980
|
||||
);
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { DribbbleTeams } from '@gitroom/frontend/components/new-launch/providers/dribbble/dribbble.teams';
|
||||
import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto';
|
||||
const DribbbleSettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Input label={'Title'} {...register('title')} />
|
||||
<DribbbleTeams {...register('team')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
DribbbleSettings,
|
||||
undefined,
|
||||
DribbbleDto,
|
||||
async ([firstItem, ...otherItems]) => {
|
||||
const isMp4 = firstItem?.find((item) => item.path.indexOf('mp4') > -1);
|
||||
if (firstItem.length !== 1) {
|
||||
return 'Dribbble requires one item';
|
||||
}
|
||||
if (isMp4) {
|
||||
return 'Dribbble does not support mp4 files';
|
||||
}
|
||||
const details = await new Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>((resolve, reject) => {
|
||||
const url = new Image();
|
||||
url.onload = function () {
|
||||
// @ts-ignore
|
||||
resolve({ width: this.width, height: this.height });
|
||||
};
|
||||
url.src = firstItem[0].path;
|
||||
});
|
||||
if (
|
||||
(details?.width === 400 && details?.height === 300) ||
|
||||
(details?.width === 800 && details?.height === 600)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return 'Invalid image size. Dribbble requires 400x300 or 800x600 px images.';
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const DribbbleTeams: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [orgs, setOrgs] = useState<undefined | any[]>();
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('teams').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!orgs) {
|
||||
return null;
|
||||
}
|
||||
if (!orgs.length) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select a team"
|
||||
onChange={onChangeInner}
|
||||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{orgs.map((org: any) => (
|
||||
<option key={org.id} value={org.id}>
|
||||
{org.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
export default withProvider(null, undefined, undefined, undefined, 63206);
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { HashnodePublications } from '@gitroom/frontend/components/new-launch/providers/hashnode/hashnode.publications';
|
||||
import { HashnodeTags } from '@gitroom/frontend/components/new-launch/providers/hashnode/hashnode.tags';
|
||||
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import clsx from 'clsx';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import { Plus_Jakarta_Sans } from 'next/font/google';
|
||||
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { Canonical } from '@gitroom/react/form/canonical';
|
||||
const font = Plus_Jakarta_Sans({
|
||||
subsets: ['latin'],
|
||||
});
|
||||
const HashnodePreview: FC = () => {
|
||||
const { value } = useIntegration();
|
||||
const settings = useSettings();
|
||||
const image = useMediaDirectory();
|
||||
const [coverPicture, title, subtitle] = settings.watch([
|
||||
'main_image',
|
||||
'title',
|
||||
'subtitle',
|
||||
]);
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
font.className,
|
||||
'text-center text-black flex h-[1000px] w-[699.8px] rounded-[10px] bg-white overflow-hidden overflow-y-auto flex-col gap-[32px]'
|
||||
)}
|
||||
>
|
||||
{!!coverPicture?.path && (
|
||||
<div className="h-[338.672px]">
|
||||
<img
|
||||
className="object-cover w-full h-full"
|
||||
src={image.set(coverPicture.path)}
|
||||
alt="cover_picture"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-[60px]">
|
||||
<div className="font-[800] text-[48px] leading-[60px] mb-[8px]">
|
||||
{title}
|
||||
</div>
|
||||
<div className="font-[400] text-[30px] leading-[60px] mb-[8px] text-customColor34">
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-[60px] text-start">
|
||||
<MDEditor.Markdown
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: 'black',
|
||||
}}
|
||||
className={font.className}
|
||||
skipHtml={true}
|
||||
source={value.map((p) => p.content).join('\n')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const HashnodeSettings: FC = () => {
|
||||
const form = useSettings();
|
||||
const { date } = useIntegration();
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Input label="Subtitle" {...form.register('subtitle')} />
|
||||
<Canonical
|
||||
date={date}
|
||||
label="Canonical Link"
|
||||
{...form.register('canonical')}
|
||||
/>
|
||||
<MediaComponent
|
||||
label="Cover picture"
|
||||
description="Add a cover picture"
|
||||
{...form.register('main_image')}
|
||||
/>
|
||||
<div className="mt-[20px]">
|
||||
<HashnodePublications {...form.register('publication')} />
|
||||
</div>
|
||||
<div>
|
||||
<HashnodeTags label="Tags" {...form.register('tags')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
HashnodeSettings,
|
||||
HashnodePreview,
|
||||
HashnodeSettingsDto
|
||||
);
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const HashnodePublications: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [publications, setOrgs] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('publications').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!publications.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select publication"
|
||||
onChange={onChangeInner}
|
||||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{publications.map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { ReactTags } from 'react-tag-autocomplete';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
export const HashnodeTags: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: any[];
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [tags, setTags] = useState<any[]>([]);
|
||||
const { getValues, formState: form } = useSettings();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
if (tagValue.length >= 4) {
|
||||
return;
|
||||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
useEffect(() => {
|
||||
customFunc.get('tags').then((data) => setTags(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
}, []);
|
||||
const err = useMemo(() => {
|
||||
if (!form || !form.errors[props?.name!]) return;
|
||||
return form?.errors?.[props?.name!]?.message! as string;
|
||||
}, [form?.errors?.[props?.name!]?.message]);
|
||||
if (!tags.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
|
||||
<ReactTags
|
||||
suggestions={tags}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { IntegrationContext } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
|
||||
class Empty {
|
||||
@IsOptional()
|
||||
empty: string;
|
||||
}
|
||||
|
||||
export const TriggerComponent: FC<{form: any}> = ({ form }) => {
|
||||
useEffect(() => {
|
||||
form.trigger();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const withProvider = function <T extends object>(
|
||||
SettingsComponent: FC<{
|
||||
values?: any;
|
||||
}> | null,
|
||||
CustomPreviewComponent?: FC<{
|
||||
maximumCharacters?: number;
|
||||
}>,
|
||||
dto?: any,
|
||||
checkValidity?: (
|
||||
value: Array<
|
||||
Array<{
|
||||
path: string;
|
||||
}>
|
||||
>,
|
||||
settings: T,
|
||||
additionalSettings: any
|
||||
) => Promise<string | true>,
|
||||
maximumCharacters?: number | ((settings: any) => number)
|
||||
) {
|
||||
return forwardRef((props: { id: string }, ref) => {
|
||||
const {
|
||||
current,
|
||||
integrations,
|
||||
selectedIntegration,
|
||||
internal,
|
||||
global,
|
||||
date,
|
||||
} = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
date: state.date,
|
||||
global: state.global,
|
||||
internal: state.internal.find((p) => p.integration.id === props.id),
|
||||
integrations: state.selectedIntegrations,
|
||||
current: state.current === props.id,
|
||||
selectedIntegration: state.selectedIntegrations.find(
|
||||
(p) => p.integration.id === props.id
|
||||
),
|
||||
}))
|
||||
);
|
||||
|
||||
const value = useMemo(() => {
|
||||
if (internal) {
|
||||
return internal.integrationValue;
|
||||
}
|
||||
|
||||
return global;
|
||||
}, []);
|
||||
|
||||
const form = useForm({
|
||||
resolver: classValidatorResolver(dto || Empty),
|
||||
values: {...selectedIntegration.settings},
|
||||
mode: 'all',
|
||||
criteriaMode: 'all',
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
isValid: async () => {
|
||||
return {
|
||||
id: props.id,
|
||||
identifier: selectedIntegration.integration.identifier,
|
||||
valid: form.formState.isValid,
|
||||
values: form.getValues(),
|
||||
errors: form.formState.errors,
|
||||
};
|
||||
},
|
||||
}), [form, props.id, selectedIntegration]);
|
||||
|
||||
return (
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
date,
|
||||
integration: selectedIntegration.integration,
|
||||
allIntegrations: integrations.map((p) => p.integration),
|
||||
value: value,
|
||||
}}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<form>
|
||||
{SettingsComponent && (
|
||||
<div className={current ? '' : 'hidden'}>
|
||||
<SettingsComponent />
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</IntegrationContext.Provider>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { FC } from 'react';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { InstagramCollaboratorsTags } from '@gitroom/frontend/components/new-launch/providers/instagram/instagram.tags';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
const postType = [
|
||||
{
|
||||
value: 'post',
|
||||
label: 'Post / Reel',
|
||||
},
|
||||
{
|
||||
value: 'story',
|
||||
label: 'Story',
|
||||
},
|
||||
];
|
||||
const InstagramCollaborators: FC<{
|
||||
values?: any;
|
||||
}> = (props) => {
|
||||
const t = useT();
|
||||
const { watch, register, formState, control } = useSettings();
|
||||
const postCurrentType = watch('post_type');
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
label="Post Type"
|
||||
{...register('post_type', {
|
||||
value: 'post',
|
||||
})}
|
||||
>
|
||||
<option value="">{t('select_post_type', 'Select Post Type...')}</option>
|
||||
{postType.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{postCurrentType !== 'story' && (
|
||||
<InstagramCollaboratorsTags
|
||||
label="Collaborators (max 3) - accounts can't be private"
|
||||
{...register('collaborators', {
|
||||
value: [],
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withProvider<InstagramDto>(
|
||||
InstagramCollaborators,
|
||||
undefined,
|
||||
InstagramDto,
|
||||
async ([firstPost, ...otherPosts], settings) => {
|
||||
if (!firstPost.length) {
|
||||
return 'Instagram should have at least one media';
|
||||
}
|
||||
if (firstPost.length > 1 && settings.post_type === 'story') {
|
||||
return 'Instagram stories can only have one media';
|
||||
}
|
||||
const checkVideosLength = await Promise.all(
|
||||
firstPost
|
||||
.filter((f) => f.path.indexOf('mp4') > -1)
|
||||
.flatMap((p) => p.path)
|
||||
.map((p) => {
|
||||
return new Promise<number>((res) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.src = p;
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
res(video.duration);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
for (const video of checkVideosLength) {
|
||||
if (video > 60 && settings.post_type === 'story') {
|
||||
return 'Instagram stories should be maximum 60 seconds';
|
||||
}
|
||||
if (video > 180 && settings.post_type === 'post') {
|
||||
return 'Instagram reel should be maximum 180 seconds';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
2200
|
||||
);
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { ReactTags } from 'react-tag-autocomplete';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import clsx from 'clsx';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
||||
export const InstagramCollaboratorsTags: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: any[];
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const { getValues } = useSettings();
|
||||
const { integration } = useIntegration();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
const [suggestions, setSuggestions] = useState<string>('');
|
||||
const t = useT();
|
||||
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
if (tagValue.length >= 3) {
|
||||
return;
|
||||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
useEffect(() => {
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
}, []);
|
||||
const suggestionsArray = useMemo(() => {
|
||||
return [
|
||||
...tagValue,
|
||||
{
|
||||
label: suggestions,
|
||||
value: suggestions,
|
||||
},
|
||||
].filter((f) => f.label);
|
||||
}, [suggestions, tagValue]);
|
||||
return (
|
||||
<div
|
||||
{...(integration?.identifier === 'instagram-standalone'
|
||||
? {
|
||||
'data-tooltip-id': 'tooltip',
|
||||
'data-tooltip-content':
|
||||
'Instagram Standalone does not support collaborators',
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
integration?.identifier === 'instagram-standalone' &&
|
||||
'opacity-50 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<div className={clsx(`${interClass} text-[14px] mb-[6px]`)}>
|
||||
{label}
|
||||
</div>
|
||||
<ReactTags
|
||||
placeholderText={t('add_a_tag', 'Add a tag')}
|
||||
suggestions={suggestionsArray}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onInput={setSuggestions}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { Subreddit } from './subreddit';
|
||||
import { LemmySettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/lemmy.dto';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
const LemmySettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
// control props comes from useForm (optional: if you are using FormContext)
|
||||
name: 'subreddit', // unique name for your Field Array
|
||||
});
|
||||
const t = useT();
|
||||
|
||||
const addField = useCallback(() => {
|
||||
append({});
|
||||
}, [fields, append]);
|
||||
const deleteField = useCallback(
|
||||
(index: number) => async () => {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
t(
|
||||
'are_you_sure_you_want_to_delete_this_subreddit',
|
||||
'Are you sure you want to delete this Subreddit?'
|
||||
)
|
||||
))
|
||||
)
|
||||
return;
|
||||
remove(index);
|
||||
},
|
||||
[fields, remove]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[20px] mb-[20px]">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col relative">
|
||||
<div
|
||||
onClick={deleteField(index)}
|
||||
className="absolute -start-[10px] justify-center items-center flex -top-[10px] w-[20px] h-[20px] bg-red-600 rounded-full text-textColor"
|
||||
>
|
||||
x
|
||||
</div>
|
||||
<Subreddit {...register(`subreddit.${index}.value`)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={addField}>{t('add_community', 'Add Community')}</Button>
|
||||
{fields.length === 0 && (
|
||||
<div className="text-red-500 text-[12px] mt-[10px]">
|
||||
{t(
|
||||
'please_add_at_least_one_subreddit',
|
||||
'Please add at least one Subreddit'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
LemmySettings,
|
||||
undefined,
|
||||
LemmySettingsDto,
|
||||
async (items) => {
|
||||
const [firstItems] = items;
|
||||
if (
|
||||
firstItems.length &&
|
||||
firstItems[0].path.indexOf('png') === -1 &&
|
||||
firstItems[0].path.indexOf('jpg') === -1 &&
|
||||
firstItems[0].path.indexOf('jpef') === -1 &&
|
||||
firstItems[0].path.indexOf('gif') === -1
|
||||
) {
|
||||
return 'You can set only one picture for a cover';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
10000
|
||||
);
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
'use client';
|
||||
|
||||
import { FC, FormEvent, useCallback, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
export const Subreddit: FC<{
|
||||
onChange: (event: {
|
||||
target: {
|
||||
name: string;
|
||||
value: {
|
||||
id: string;
|
||||
subreddit: string;
|
||||
title: string;
|
||||
name: string;
|
||||
url: string;
|
||||
body: string;
|
||||
media: any[];
|
||||
};
|
||||
};
|
||||
}) => void;
|
||||
name: string;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const state = useSettings();
|
||||
const split = name.split('.');
|
||||
const [loading, setLoading] = useState(false);
|
||||
// @ts-ignore
|
||||
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
|
||||
const [results, setResults] = useState([]);
|
||||
const func = useCustomProviderFunction();
|
||||
const value = useWatch({
|
||||
name,
|
||||
});
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const setResult = (result: { id: string; name: string }) => async () => {
|
||||
setLoading(true);
|
||||
setSearchValue('');
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
id: String(result.id),
|
||||
subreddit: result.name,
|
||||
title: '',
|
||||
name: '',
|
||||
url: '',
|
||||
body: '',
|
||||
media: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
const setTitle = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
title: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const setURL = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
url: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const search = useDebouncedCallback(
|
||||
useCallback(async (e: FormEvent<HTMLInputElement>) => {
|
||||
// @ts-ignore
|
||||
setResults([]);
|
||||
// @ts-ignore
|
||||
if (!e.target.value) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const results = await func.get('subreddits', { word: e.target.value });
|
||||
// @ts-ignore
|
||||
setResults(results);
|
||||
}, []),
|
||||
500
|
||||
);
|
||||
return (
|
||||
<div className="bg-primary p-[20px]">
|
||||
{value?.subreddit ? (
|
||||
<>
|
||||
<Input
|
||||
error={errors?.subreddit?.message}
|
||||
disableForm={true}
|
||||
value={value.subreddit}
|
||||
readOnly={true}
|
||||
label="Community"
|
||||
name="subreddit"
|
||||
/>
|
||||
<Input
|
||||
error={errors?.title?.message}
|
||||
value={value.title}
|
||||
disableForm={true}
|
||||
label="Title"
|
||||
name="title"
|
||||
onChange={setTitle}
|
||||
/>
|
||||
<Input
|
||||
error={errors?.url?.message}
|
||||
value={value.url}
|
||||
label="URL"
|
||||
name="url"
|
||||
disableForm={true}
|
||||
onChange={setURL}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Community"
|
||||
name="search"
|
||||
label="Search Community"
|
||||
readOnly={loading}
|
||||
value={searchValue}
|
||||
error={errors?.message}
|
||||
disableForm={true}
|
||||
onInput={async (e) => {
|
||||
// @ts-ignore
|
||||
setSearchValue(e.target.value);
|
||||
await search(e);
|
||||
}}
|
||||
/>
|
||||
{!!results.length && !loading && (
|
||||
<div className="z-[400] w-full absolute bg-input -mt-[20px] outline-none border-fifth border cursor-pointer">
|
||||
{results.map((r: { id: string; name: string }) => (
|
||||
<div
|
||||
onClick={setResult(r)}
|
||||
key={r.id}
|
||||
className="px-[16px] py-[5px] hover:bg-secondary"
|
||||
>
|
||||
{r.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { Checkbox } from '@gitroom/react/form/checkbox';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto';
|
||||
|
||||
const LinkedInSettings = () => {
|
||||
const t = useT();
|
||||
const { watch, register, formState, control } = useSettings();
|
||||
|
||||
return (
|
||||
<div className="mb-[20px]">
|
||||
<Checkbox
|
||||
variant="hollow"
|
||||
label={t('post_as_images_carousel', 'Post as images carousel')}
|
||||
{...register('post_as_images_carousel', {
|
||||
value: false,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider<LinkedinDto>(
|
||||
LinkedInSettings,
|
||||
undefined,
|
||||
LinkedinDto,
|
||||
async (posts, vals) => {
|
||||
const [firstPost, ...restPosts] = posts;
|
||||
|
||||
if (
|
||||
vals.post_as_images_carousel &&
|
||||
(firstPost.length < 2 ||
|
||||
firstPost.some((p) => p.path.indexOf('mp4') > -1))
|
||||
) {
|
||||
return 'LinkedIn carousel can only be created with 2 or more images and no videos.';
|
||||
}
|
||||
|
||||
if (
|
||||
firstPost.length > 1 &&
|
||||
firstPost.some((p) => p.path.indexOf('mp4') > -1)
|
||||
) {
|
||||
return 'LinkedIn can have maximum 1 media when selecting a video.';
|
||||
}
|
||||
if (restPosts.some((p) => p.length > 0)) {
|
||||
return 'LinkedIn comments can only contain text.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
3000
|
||||
);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
export default withProvider(null, undefined, undefined, undefined, 500);
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on July 10, 2013 */
|
||||
|
||||
@font-face {
|
||||
font-family: 'charterbold_italic';
|
||||
src: url('charter_bold_italic-webfont.eot');
|
||||
src: url('charter_bold_italic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_bold_italic-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'charterbold';
|
||||
src: url('charter_bold-webfont.eot');
|
||||
src: url('charter_bold-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_bold-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'charteritalic';
|
||||
src: url('charter_italic-webfont.eot');
|
||||
src: url('charter_italic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_italic-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'charterregular';
|
||||
src: url('charter_regular-webfont.eot');
|
||||
src: url('charter_regular-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('charter_regular-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { MediumPublications } from '@gitroom/frontend/components/new-launch/providers/medium/medium.publications';
|
||||
import { MediumTags } from '@gitroom/frontend/components/new-launch/providers/medium/medium.tags';
|
||||
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import clsx from 'clsx';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import localFont from 'next/font/local';
|
||||
import { Canonical } from '@gitroom/react/form/canonical';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
const charter = localFont({
|
||||
src: [
|
||||
{
|
||||
path: './fonts/Charter Regular.ttf',
|
||||
weight: 'normal',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/Charter Italic.ttf',
|
||||
weight: 'normal',
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
path: './fonts/Charter Bold.ttf',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './fonts/Charter Bold Italic.ttf',
|
||||
weight: '700',
|
||||
style: 'italic',
|
||||
},
|
||||
],
|
||||
});
|
||||
const MediumPreview: FC = () => {
|
||||
const { value } = useIntegration();
|
||||
const settings = useSettings();
|
||||
const [title, subtitle] = settings.watch(['title', 'subtitle']);
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
`font-[800] flex h-[1000px] w-[699.8px] text-customColor35 ${interClass} rounded-[10px] bg-white overflow-hidden overflow-y-auto flex-col gap-[56px]`
|
||||
)}
|
||||
>
|
||||
<div className="px-[60px] pt-[20px]">
|
||||
<div className="text-[48px] leading-[60px] mb-[8px]">{title}</div>
|
||||
<div className="text-[22px] font-[400] text-customColor36">
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-[60px]">
|
||||
<MDEditor.Markdown
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: '#242424',
|
||||
}}
|
||||
className={charter.className}
|
||||
skipHtml={true}
|
||||
source={value.map((p) => p.content).join('\n')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const MediumSettings: FC = () => {
|
||||
const form = useSettings();
|
||||
const { date } = useIntegration();
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<Input label="Subtitle" {...form.register('subtitle')} />
|
||||
<Canonical
|
||||
date={date}
|
||||
label="Canonical Link"
|
||||
{...form.register('canonical')}
|
||||
/>
|
||||
<div>
|
||||
<MediumPublications {...form.register('publication')} />
|
||||
</div>
|
||||
<div>
|
||||
<MediumTags label="Topics" {...form.register('tags')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withProvider(MediumSettings, MediumPreview, MediumSettingsDto);
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const MediumPublications: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [publications, setOrgs] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('publications').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!publications.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select publication"
|
||||
onChange={onChangeInner}
|
||||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{publications.map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { ReactTags } from 'react-tag-autocomplete';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
||||
export const MediumTags: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: any[];
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, label } = props;
|
||||
const { getValues } = useSettings();
|
||||
const [tagValue, setTagValue] = useState<any[]>([]);
|
||||
const [suggestions, setSuggestions] = useState<string>('');
|
||||
const t = useT();
|
||||
|
||||
const onDelete = useCallback(
|
||||
(tagIndex: number) => {
|
||||
const modify = tagValue.filter((_, i) => i !== tagIndex);
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
const onAddition = useCallback(
|
||||
(newTag: any) => {
|
||||
if (tagValue.length >= 3) {
|
||||
return;
|
||||
}
|
||||
const modify = [...tagValue, newTag];
|
||||
setTagValue(modify);
|
||||
onChange({
|
||||
target: {
|
||||
value: modify,
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tagValue]
|
||||
);
|
||||
useEffect(() => {
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
}, []);
|
||||
const suggestionsArray = useMemo(() => {
|
||||
return [
|
||||
...tagValue,
|
||||
{
|
||||
label: suggestions,
|
||||
value: suggestions,
|
||||
},
|
||||
].filter((f) => f.label);
|
||||
}, [suggestions, tagValue]);
|
||||
return (
|
||||
<div>
|
||||
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
|
||||
<ReactTags
|
||||
placeholderText={t('add_a_tag', 'Add a tag')}
|
||||
suggestions={suggestionsArray}
|
||||
selected={tagValue}
|
||||
onAdd={onAddition}
|
||||
onInput={setSuggestions}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async () => {
|
||||
return true;
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const PinterestBoard: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [orgs, setOrgs] = useState<undefined | any[]>();
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('boards').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!orgs) {
|
||||
return null;
|
||||
}
|
||||
if (!orgs.length) {
|
||||
return 'No boards found, you have to create a board first';
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select board"
|
||||
onChange={onChangeInner}
|
||||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{orgs.map((org: any) => (
|
||||
<option key={org.id} value={org.id}>
|
||||
{org.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { PinterestBoard } from '@gitroom/frontend/components/new-launch/providers/pinterest/pinterest.board';
|
||||
import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/pinterest.dto';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { ColorPicker } from '@gitroom/react/form/color.picker';
|
||||
const PinterestSettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Input label={'Title'} {...register('title')} />
|
||||
<Input label={'Link'} {...register('link')} />
|
||||
<PinterestBoard {...register('board')} />
|
||||
<ColorPicker
|
||||
label="Select Pin Color"
|
||||
name="dominant_color"
|
||||
enabled={false}
|
||||
canBeCancelled={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
PinterestSettings,
|
||||
undefined,
|
||||
PinterestSettingsDto,
|
||||
async ([firstItem, ...otherItems]) => {
|
||||
const isMp4 = firstItem?.find((item) => item.path.indexOf('mp4') > -1);
|
||||
const isPicture = firstItem?.find(
|
||||
(item) => item.path.indexOf('mp4') === -1
|
||||
);
|
||||
if (firstItem.length === 0) {
|
||||
return 'Pinterest requires at least one media';
|
||||
}
|
||||
if (isMp4 && firstItem.length !== 2 && !isPicture) {
|
||||
return 'If posting a video to Pinterest you have to also include a cover image as second media';
|
||||
}
|
||||
if (isMp4 && firstItem.length > 2) {
|
||||
return 'If posting a video to Pinterest you can only have two media items';
|
||||
}
|
||||
if (otherItems.length) {
|
||||
return 'Pinterest can only have one post';
|
||||
}
|
||||
if (
|
||||
firstItem.length > 1 &&
|
||||
firstItem.every((p) => p.path.indexOf('mp4') == -1)
|
||||
) {
|
||||
const loadAll: Array<{
|
||||
width: number;
|
||||
height: number;
|
||||
}> = (await Promise.all(
|
||||
firstItem.map((p) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new Image();
|
||||
url.onload = function () {
|
||||
// @ts-ignore
|
||||
resolve({ width: this.width, height: this.height });
|
||||
};
|
||||
url.src = p.path;
|
||||
});
|
||||
})
|
||||
)) as any;
|
||||
const checkAllTheSameWidthHeight = loadAll.every((p, i, arr) => {
|
||||
return p.width === arr[0].width && p.height === arr[0].height;
|
||||
});
|
||||
if (!checkAllTheSameWidthHeight) {
|
||||
return 'Pinterest requires all images to have the same width and height';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
500
|
||||
);
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useFormatting } from '@gitroom/frontend/components/new-launch/helpers/use.formatting';
|
||||
import { Subreddit } from '@gitroom/frontend/components/new-launch/providers/reddit/subreddit';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useFieldArray, useWatch } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import {
|
||||
RedditSettingsDto,
|
||||
RedditSettingsValueDto,
|
||||
} from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto';
|
||||
import clsx from 'clsx';
|
||||
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import Image from 'next/image';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
const RenderRedditComponent: FC<{
|
||||
type: string;
|
||||
images?: Array<{
|
||||
id: string;
|
||||
path: string;
|
||||
}>;
|
||||
}> = (props) => {
|
||||
const { value: topValue } = useIntegration();
|
||||
const showMedia = useMediaDirectory();
|
||||
const t = useT();
|
||||
|
||||
const { type, images } = props;
|
||||
const [firstPost] = topValue;
|
||||
switch (type) {
|
||||
case 'self':
|
||||
return (
|
||||
<MDEditor.Markdown
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
skipHtml={true}
|
||||
disallowedElements={['img']}
|
||||
source={firstPost?.content}
|
||||
/>
|
||||
);
|
||||
case 'link':
|
||||
return (
|
||||
<div className="h-[375px] bg-primary rounded-[16px] flex justify-center items-center">
|
||||
{t('link', 'Link')}
|
||||
</div>
|
||||
);
|
||||
case 'media':
|
||||
return (
|
||||
<div className="h-[375px] bg-primary rounded-[16px] flex justify-center items-center">
|
||||
{!!images?.length &&
|
||||
images.map((image, index) => (
|
||||
<a
|
||||
key={`image_${index}`}
|
||||
href={showMedia.set(image.path)}
|
||||
className="flex-1 h-full"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={showMedia.set(image.path)}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
const RedditPreview: FC = (props) => {
|
||||
const { value: topValue, integration } = useIntegration();
|
||||
const settings = useWatch({
|
||||
name: 'subreddit',
|
||||
}) as Array<RedditSettingsValueDto>;
|
||||
const [, ...restOfPosts] = useFormatting(topValue, {
|
||||
removeMarkdown: true,
|
||||
saveBreaklines: true,
|
||||
specialFunc: (text: string) => {
|
||||
return text.slice(0, 280);
|
||||
},
|
||||
});
|
||||
if (!settings || !settings.length) {
|
||||
return <>Please add at least one Subreddit from the settings</>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-[40px] w-full">
|
||||
{settings
|
||||
.filter(({ value }) => value?.subreddit)
|
||||
.map(({ value }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx(
|
||||
`bg-customColor37 w-full p-[10px] flex flex-col ${interClass} border-tableBorder border`
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row gap-[8px]">
|
||||
<div className="w-[40px] h-[40px] bg-white rounded-full" />
|
||||
<div className="flex flex-col">
|
||||
<div className="text-[12px] font-[700]">
|
||||
{value.subreddit}
|
||||
</div>
|
||||
<div className="text-[12px]">{integration?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-[600] text-[24px] mb-[16px]">
|
||||
{value.title}
|
||||
</div>
|
||||
<RenderRedditComponent type={value.type} images={value.media} />
|
||||
<div
|
||||
className={clsx(
|
||||
restOfPosts.length && 'mt-[40px] flex flex-col gap-[20px]'
|
||||
)}
|
||||
>
|
||||
{restOfPosts.map((p, index) => (
|
||||
<div className="flex gap-[8px]" key={index}>
|
||||
<div className="w-[32px] h-[32px] relative">
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
src={integration?.picture!}
|
||||
alt="x"
|
||||
className="rounded-full w-full h-full relative z-[2]"
|
||||
/>
|
||||
<Image
|
||||
width={24}
|
||||
height={24}
|
||||
src={`/icons/platforms/${integration?.identifier!}.png`}
|
||||
alt="x"
|
||||
className="rounded-full absolute -end-[5px] -bottom-[5px] z-[2]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col leading-[16px] w-full pe-[64px] pb-[8px] rounded-[8px]">
|
||||
<div className="text-[14px] font-[600]">
|
||||
{integration?.name}
|
||||
</div>
|
||||
<MDEditor.Markdown
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
skipHtml={true}
|
||||
source={p.text}
|
||||
disallowedElements={['img']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const RedditSettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
// control props comes from useForm (optional: if you are using FormContext)
|
||||
name: 'subreddit', // unique name for your Field Array
|
||||
});
|
||||
const t = useT();
|
||||
|
||||
const addField = useCallback(() => {
|
||||
append({});
|
||||
}, [fields, append]);
|
||||
const deleteField = useCallback(
|
||||
(index: number) => async () => {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
t(
|
||||
'are_you_sure_you_want_to_delete_this_subreddit',
|
||||
'Are you sure you want to delete this Subreddit?'
|
||||
)
|
||||
))
|
||||
)
|
||||
return;
|
||||
remove(index);
|
||||
},
|
||||
[fields, remove]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[20px] mb-[20px]">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col relative">
|
||||
<div
|
||||
onClick={deleteField(index)}
|
||||
className="absolute -start-[10px] justify-center items-center flex -top-[10px] w-[20px] h-[20px] bg-red-600 rounded-full text-textColor"
|
||||
>
|
||||
x
|
||||
</div>
|
||||
<Subreddit {...register(`subreddit.${index}.value`)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={addField}>{t('add_subreddit', 'Add Subreddit')}</Button>
|
||||
{fields.length === 0 && (
|
||||
<div className="text-red-500 text-[12px] mt-[10px]">
|
||||
{t(
|
||||
'please_add_at_least_one_subreddit',
|
||||
'Please add at least one Subreddit'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
RedditSettings,
|
||||
RedditPreview,
|
||||
RedditSettingsDto,
|
||||
undefined,
|
||||
10000
|
||||
);
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
'use client';
|
||||
|
||||
import { FC, FormEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import clsx from 'clsx';
|
||||
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { Canonical } from '@gitroom/react/form/canonical';
|
||||
import { useIntegration } from '@gitroom/frontend/components/new-launch/helpers/use.integration';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const RenderOptions: FC<{
|
||||
options: Array<'self' | 'link' | 'media'>;
|
||||
onClick: (current: 'self' | 'link' | 'media') => void;
|
||||
value: 'self' | 'link' | 'media';
|
||||
}> = (props) => {
|
||||
const { options, onClick, value } = props;
|
||||
const mapValues = useMemo(() => {
|
||||
return options.map((p) => ({
|
||||
children: (
|
||||
<>
|
||||
{p === 'self'
|
||||
? 'Post'
|
||||
: p === 'link'
|
||||
? 'Link'
|
||||
: p === 'media'
|
||||
? 'Media'
|
||||
: ''}
|
||||
</>
|
||||
),
|
||||
id: p,
|
||||
onClick: () => onClick(p),
|
||||
}));
|
||||
}, [options]);
|
||||
return (
|
||||
<div className="flex">
|
||||
{mapValues.map((p) => (
|
||||
<Button
|
||||
className={clsx('flex-1', p.id !== value && 'bg-secondary')}
|
||||
key={p.id}
|
||||
{...p}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Subreddit: FC<{
|
||||
onChange: (event: {
|
||||
target: {
|
||||
name: string;
|
||||
value: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}) => void;
|
||||
name: string;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const state = useSettings();
|
||||
const t = useT();
|
||||
|
||||
const { date } = useIntegration();
|
||||
const split = name.split('.');
|
||||
const [loading, setLoading] = useState(false);
|
||||
// @ts-ignore
|
||||
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
|
||||
const [results, setResults] = useState([]);
|
||||
const func = useCustomProviderFunction();
|
||||
const value = useWatch({
|
||||
name,
|
||||
});
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const setResult = (result: { id: string; name: string }) => async () => {
|
||||
setLoading(true);
|
||||
setSearchValue('');
|
||||
const restrictions = await func.get('restrictions', {
|
||||
subreddit: result.name,
|
||||
});
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...restrictions,
|
||||
type: restrictions.allow[0],
|
||||
media: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
const setTitle = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
title: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const setType = useCallback(
|
||||
(e: string) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
type: e,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const setMedia = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
media: e.target.value.map((p: any) => p),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const setURL = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
url: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const setFlair = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
flair: value.flairs.find((p: any) => p.id === e.target.value),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const search = useDebouncedCallback(
|
||||
useCallback(async (e: FormEvent<HTMLInputElement>) => {
|
||||
// @ts-ignore
|
||||
setResults([]);
|
||||
// @ts-ignore
|
||||
if (!e.target.value) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const results = await func.get('subreddits', { word: e.target.value });
|
||||
// @ts-ignore
|
||||
setResults(results);
|
||||
}, []),
|
||||
500
|
||||
);
|
||||
return (
|
||||
<div className="bg-primary p-[20px]">
|
||||
{value?.subreddit ? (
|
||||
<>
|
||||
<Input
|
||||
error={errors?.subreddit?.message}
|
||||
disableForm={true}
|
||||
value={value.subreddit}
|
||||
readOnly={true}
|
||||
label="Subreddit"
|
||||
name="subreddit"
|
||||
/>
|
||||
<div className="mb-[12px]">
|
||||
<RenderOptions
|
||||
value={value.type}
|
||||
options={value.allow}
|
||||
onClick={setType}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
error={errors?.title?.message}
|
||||
value={value.title}
|
||||
disableForm={true}
|
||||
label="Title"
|
||||
name="title"
|
||||
onChange={setTitle}
|
||||
/>
|
||||
<Select
|
||||
error={errors?.flair?.message}
|
||||
onChange={setFlair}
|
||||
value={value?.flair?.id}
|
||||
disableForm={true}
|
||||
label="Flair"
|
||||
name="flair"
|
||||
>
|
||||
<option value="">{t('select_flair', '--Select Flair--')}</option>
|
||||
{value.flairs.map((f: any) => (
|
||||
<option key={f.name} value={f.id}>
|
||||
{f.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{value.type === 'link' && (
|
||||
<Canonical
|
||||
date={date}
|
||||
error={errors?.url?.message}
|
||||
value={value.url}
|
||||
label="URL"
|
||||
name="url"
|
||||
disableForm={true}
|
||||
onChange={setURL}
|
||||
/>
|
||||
)}
|
||||
{value.type === 'media' && (
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full h-[10px] bg-input rounded-tr-[8px] rounded-tl-[8px]" />
|
||||
<div className="flex flex-col text-nowrap">
|
||||
<MultiMediaComponent
|
||||
allData={[]}
|
||||
text=""
|
||||
description=""
|
||||
name="media"
|
||||
label="Media"
|
||||
value={value.media}
|
||||
onChange={setMedia}
|
||||
error={errors?.media?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="/r/selfhosted"
|
||||
name="search"
|
||||
label="Search Subreddit"
|
||||
readOnly={loading}
|
||||
value={searchValue}
|
||||
error={errors?.message}
|
||||
disableForm={true}
|
||||
onInput={async (e) => {
|
||||
// @ts-ignore
|
||||
setSearchValue(e.target.value);
|
||||
await search(e);
|
||||
}}
|
||||
/>
|
||||
{!!results.length && !loading && (
|
||||
<div className="z-[400] w-full absolute bg-input -mt-[20px] outline-none border-fifth border cursor-pointer">
|
||||
{results.map((r: { id: string; name: string }) => (
|
||||
<div
|
||||
onClick={setResult(r)}
|
||||
key={r.id}
|
||||
className="px-[16px] py-[5px] hover:bg-secondary"
|
||||
>
|
||||
{r.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
'use client';
|
||||
|
||||
import DevtoProvider from '@gitroom/frontend/components/new-launch/providers/devto/devto.provider';
|
||||
import XProvider from '@gitroom/frontend/components/new-launch/providers/x/x.provider';
|
||||
import LinkedinProvider from '@gitroom/frontend/components/new-launch/providers/linkedin/linkedin.provider';
|
||||
import RedditProvider from '@gitroom/frontend/components/new-launch/providers/reddit/reddit.provider';
|
||||
import MediumProvider from '@gitroom/frontend/components/new-launch/providers/medium/medium.provider';
|
||||
import HashnodeProvider from '@gitroom/frontend/components/new-launch/providers/hashnode/hashnode.provider';
|
||||
import FacebookProvider from '@gitroom/frontend/components/new-launch/providers/facebook/facebook.provider';
|
||||
import InstagramProvider from '@gitroom/frontend/components/new-launch/providers/instagram/instagram.collaborators';
|
||||
import YoutubeProvider from '@gitroom/frontend/components/new-launch/providers/youtube/youtube.provider';
|
||||
import TiktokProvider from '@gitroom/frontend/components/new-launch/providers/tiktok/tiktok.provider';
|
||||
import PinterestProvider from '@gitroom/frontend/components/new-launch/providers/pinterest/pinterest.provider';
|
||||
import DribbbleProvider from '@gitroom/frontend/components/new-launch/providers/dribbble/dribbble.provider';
|
||||
import ThreadsProvider from '@gitroom/frontend/components/new-launch/providers/threads/threads.provider';
|
||||
import DiscordProvider from '@gitroom/frontend/components/new-launch/providers/discord/discord.provider';
|
||||
import SlackProvider from '@gitroom/frontend/components/new-launch/providers/slack/slack.provider';
|
||||
import MastodonProvider from '@gitroom/frontend/components/new-launch/providers/mastodon/mastodon.provider';
|
||||
import BlueskyProvider from '@gitroom/frontend/components/new-launch/providers/bluesky/bluesky.provider';
|
||||
import LemmyProvider from '@gitroom/frontend/components/new-launch/providers/lemmy/lemmy.provider';
|
||||
import WarpcastProvider from '@gitroom/frontend/components/new-launch/providers/warpcast/warpcast.provider';
|
||||
import TelegramProvider from '@gitroom/frontend/components/new-launch/providers/telegram/telegram.provider';
|
||||
import NostrProvider from '@gitroom/frontend/components/new-launch/providers/nostr/nostr.provider';
|
||||
import VkProvider from '@gitroom/frontend/components/new-launch/providers/vk/vk.provider';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { createRef, FC, forwardRef, useImperativeHandle } from 'react';
|
||||
export const Providers = [
|
||||
{
|
||||
identifier: 'devto',
|
||||
component: DevtoProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'x',
|
||||
component: XProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'linkedin',
|
||||
component: LinkedinProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'linkedin-page',
|
||||
component: LinkedinProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'reddit',
|
||||
component: RedditProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'medium',
|
||||
component: MediumProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'hashnode',
|
||||
component: HashnodeProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'facebook',
|
||||
component: FacebookProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'instagram',
|
||||
component: InstagramProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'instagram-standalone',
|
||||
component: InstagramProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'youtube',
|
||||
component: YoutubeProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'tiktok',
|
||||
component: TiktokProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'pinterest',
|
||||
component: PinterestProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'dribbble',
|
||||
component: DribbbleProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'threads',
|
||||
component: ThreadsProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'discord',
|
||||
component: DiscordProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'slack',
|
||||
component: SlackProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'mastodon',
|
||||
component: MastodonProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'bluesky',
|
||||
component: BlueskyProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'lemmy',
|
||||
component: LemmyProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'wrapcast',
|
||||
component: WarpcastProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'telegram',
|
||||
component: TelegramProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'nostr',
|
||||
component: NostrProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'vk',
|
||||
component: VkProvider,
|
||||
},
|
||||
];
|
||||
export const ShowAllProviders = forwardRef((props, ref) => {
|
||||
const { current, selectedIntegrations } = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
selectedIntegrations: state.selectedIntegrations,
|
||||
current: state.current,
|
||||
}))
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
checkAllValid: async () => {
|
||||
return Promise.all(
|
||||
selectedIntegrations.map(async (p) => await p.ref?.current.isValid())
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedIntegrations.map((integration) => {
|
||||
const { component: ProviderComponent } = Providers.find(
|
||||
(provider) =>
|
||||
provider.identifier === integration.integration.identifier
|
||||
) || {
|
||||
component: Empty,
|
||||
};
|
||||
|
||||
return (
|
||||
<ProviderComponent
|
||||
ref={integration.ref}
|
||||
key={integration.integration.id}
|
||||
id={integration.integration.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const Empty: FC = () => {
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const SlackChannelSelect: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [publications, setOrgs] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentMedia, setCurrentMedia] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentMedia(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('channels').then((data) => setOrgs(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentMedia(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!publications.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select Channel"
|
||||
onChange={onChangeInner}
|
||||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{publications.map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { FC } from 'react';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { SlackChannelSelect } from '@gitroom/frontend/components/new-launch/providers/slack/slack.channel.select';
|
||||
import { SlackDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/slack.dto';
|
||||
const SlackComponent: FC = () => {
|
||||
const form = useSettings();
|
||||
return (
|
||||
<div>
|
||||
<SlackChannelSelect {...form.register('channel')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
SlackComponent,
|
||||
undefined,
|
||||
SlackDto,
|
||||
undefined,
|
||||
280
|
||||
);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async () => {
|
||||
return true;
|
||||
},
|
||||
4096
|
||||
);
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { ThreadFinisher } from '@gitroom/frontend/components/new-launch/finisher/thread.finisher';
|
||||
const SettingsComponent = () => {
|
||||
return <ThreadFinisher />;
|
||||
};
|
||||
|
||||
export default withProvider(
|
||||
SettingsComponent,
|
||||
undefined,
|
||||
undefined,
|
||||
async () => {
|
||||
return true;
|
||||
},
|
||||
500
|
||||
);
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
FC,
|
||||
ReactEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Checkbox } from '@gitroom/react/form/checkbox';
|
||||
import clsx from 'clsx';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
|
||||
const CheckTikTokValidity: FC<{
|
||||
picture: string;
|
||||
}> = (props) => {
|
||||
const { register } = useSettings();
|
||||
const t = useT();
|
||||
|
||||
const func = useCustomProviderFunction();
|
||||
const [maxVideoLength, setMaxVideoLength] = useState(0);
|
||||
const [isValidVideo, setIsValidVideo] = useState<undefined | boolean>(
|
||||
undefined
|
||||
);
|
||||
const registerVideo = register('isValidVideo');
|
||||
const video = useMemo(() => {
|
||||
return props.picture;
|
||||
}, [props.picture]);
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, []);
|
||||
const loadStats = useCallback(async () => {
|
||||
const { maxDurationSeconds } = await func.get('maxVideoLength');
|
||||
// setMaxVideoLength(5);
|
||||
setMaxVideoLength(maxDurationSeconds);
|
||||
}, []);
|
||||
const loadVideo: ReactEventHandler<HTMLVideoElement> = useCallback(
|
||||
(e) => {
|
||||
// @ts-ignore
|
||||
setIsValidVideo(e.target.duration <= maxVideoLength);
|
||||
registerVideo.onChange({
|
||||
target: {
|
||||
name: 'isValidVideo',
|
||||
// @ts-ignore
|
||||
value: String(e.target.duration <= maxVideoLength),
|
||||
},
|
||||
});
|
||||
},
|
||||
[maxVideoLength, registerVideo]
|
||||
);
|
||||
if (!maxVideoLength || !video || video.indexOf('mp4') === -1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isValidVideo === false && (
|
||||
<div className="text-red-600 my-[20px]">
|
||||
{t(
|
||||
'video_length_is_invalid_must_be_up_to',
|
||||
'Video length is invalid, must be up to'
|
||||
)}
|
||||
{maxVideoLength}
|
||||
{t('seconds', 'seconds')}
|
||||
</div>
|
||||
)}
|
||||
<video
|
||||
controls
|
||||
onLoadedMetadata={loadVideo}
|
||||
className="w-0 h-0 overflow-hidden pointer-events-none"
|
||||
>
|
||||
<source src={video} type="video/mp4" />
|
||||
</video>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TikTokSettings: FC<{
|
||||
values?: any;
|
||||
}> = (props) => {
|
||||
const { watch, register, formState, control } = useSettings();
|
||||
const t = useT();
|
||||
|
||||
const disclose = watch('disclose');
|
||||
const brand_organic_toggle = watch('brand_organic_toggle');
|
||||
const brand_content_toggle = watch('brand_content_toggle');
|
||||
const content_posting_method = watch('content_posting_method');
|
||||
const isUploadMode = content_posting_method === 'UPLOAD';
|
||||
|
||||
const privacyLevel = [
|
||||
{
|
||||
value: 'PUBLIC_TO_EVERYONE',
|
||||
label: t('public_to_everyone', 'Public to everyone'),
|
||||
},
|
||||
{
|
||||
value: 'MUTUAL_FOLLOW_FRIENDS',
|
||||
label: t('mutual_follow_friends', 'Mutual follow friends'),
|
||||
},
|
||||
{
|
||||
value: 'FOLLOWER_OF_CREATOR',
|
||||
label: t('follower_of_creator', 'Follower of creator'),
|
||||
},
|
||||
{
|
||||
value: 'SELF_ONLY',
|
||||
label: t('self_only', 'Self only'),
|
||||
},
|
||||
];
|
||||
const contentPostingMethod = [
|
||||
{
|
||||
value: 'DIRECT_POST',
|
||||
label: t(
|
||||
'post_content_directly_to_tiktok',
|
||||
'Post content directly to TikTok'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'UPLOAD',
|
||||
label: t(
|
||||
'upload_content_to_tiktok_without_posting',
|
||||
'Upload content to TikTok without posting it'
|
||||
),
|
||||
},
|
||||
];
|
||||
const yesNo = [
|
||||
{
|
||||
value: 'yes',
|
||||
label: t('yes', 'Yes'),
|
||||
},
|
||||
{
|
||||
value: 'no',
|
||||
label: t('no', 'No'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<CheckTikTokValidity picture={props?.values?.[0]?.image?.[0]?.path} />
|
||||
<Select
|
||||
label={t('label_who_can_see_this_video', 'Who can see this video?')}
|
||||
hideErrors={true}
|
||||
disabled={isUploadMode}
|
||||
{...register('privacy_level', {
|
||||
value: 'PUBLIC_TO_EVERYONE',
|
||||
})}
|
||||
>
|
||||
<option value="">{t('select', 'Select')}</option>
|
||||
{privacyLevel.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="text-[14px] mt-[10px] mb-[18px] text-balance">
|
||||
{t(
|
||||
'choose_upload_without_posting_description',
|
||||
`Choose upload without posting if you want to review and edit your content within TikTok's app before publishing.
|
||||
This gives you access to TikTok's built-in editing tools and lets you make final adjustments before posting.`
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
label={t('label_content_posting_method', 'Content posting method')}
|
||||
disabled={isUploadMode}
|
||||
{...register('content_posting_method', {
|
||||
value: 'DIRECT_POST',
|
||||
})}
|
||||
>
|
||||
<option value="">{t('select', 'Select')}</option>
|
||||
{contentPostingMethod.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
hideErrors={true}
|
||||
label={t('label_auto_add_music', 'Auto add music')}
|
||||
{...register('autoAddMusic', {
|
||||
value: 'no',
|
||||
})}
|
||||
>
|
||||
<option value="">{t('select', 'Select')}</option>
|
||||
{yesNo.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="text-[14px] mt-[10px] mb-[24px] text-balance">
|
||||
{t(
|
||||
'this_feature_available_only_for_photos',
|
||||
'This feature available only for photos, it will add a default music that\n you can change later.'
|
||||
)}
|
||||
</div>
|
||||
<hr className="mb-[15px] border-tableBorder" />
|
||||
<div className="text-[14px] mb-[10px]">
|
||||
{t('allow_user_to', 'Allow User To:')}
|
||||
</div>
|
||||
<div className="flex gap-[40px]">
|
||||
<Checkbox
|
||||
variant="hollow"
|
||||
label={t('label_duet', 'Duet')}
|
||||
disabled={isUploadMode}
|
||||
{...register('duet', {
|
||||
value: false,
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('label_stitch', 'Stitch')}
|
||||
variant="hollow"
|
||||
disabled={isUploadMode}
|
||||
{...register('stitch', {
|
||||
value: false,
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t('label_comments', 'Comments')}
|
||||
variant="hollow"
|
||||
disabled={isUploadMode}
|
||||
{...register('comment', {
|
||||
value: false,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<hr className="my-[15px] mb-[25px] border-tableBorder" />
|
||||
<div className="flex flex-col">
|
||||
<Checkbox
|
||||
variant="hollow"
|
||||
label={t('label_disclose_video_content', 'Disclose Video Content')}
|
||||
disabled={isUploadMode}
|
||||
{...register('disclose', {
|
||||
value: false,
|
||||
})}
|
||||
/>
|
||||
{disclose && (
|
||||
<div className="bg-tableBorder p-[10px] mt-[10px] rounded-[10px] flex gap-[20px] items-center">
|
||||
<div>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M22.201 17.6335L14.0026 3.39569C13.7977 3.04687 13.5052 2.75764 13.1541 2.55668C12.803 2.35572 12.4055 2.25 12.001 2.25C11.5965 2.25 11.199 2.35572 10.8479 2.55668C10.4968 2.75764 10.2043 3.04687 9.99944 3.39569L1.80101 17.6335C1.60388 17.9709 1.5 18.3546 1.5 18.7454C1.5 19.1361 1.60388 19.5199 1.80101 19.8572C2.00325 20.2082 2.29523 20.499 2.64697 20.6998C2.99871 20.9006 3.39755 21.0043 3.80257 21.0001H20.1994C20.6041 21.0039 21.0026 20.9001 21.354 20.6993C21.7054 20.4985 21.997 20.2079 22.1991 19.8572C22.3965 19.52 22.5007 19.1364 22.5011 18.7456C22.5014 18.3549 22.3978 17.9711 22.201 17.6335ZM11.251 9.75006C11.251 9.55115 11.33 9.36038 11.4707 9.21973C11.6113 9.07908 11.8021 9.00006 12.001 9.00006C12.1999 9.00006 12.3907 9.07908 12.5313 9.21973C12.672 9.36038 12.751 9.55115 12.751 9.75006V13.5001C12.751 13.699 12.672 13.8897 12.5313 14.0304C12.3907 14.171 12.1999 14.2501 12.001 14.2501C11.8021 14.2501 11.6113 14.171 11.4707 14.0304C11.33 13.8897 11.251 13.699 11.251 13.5001V9.75006ZM12.001 18.0001C11.7785 18.0001 11.561 17.9341 11.376 17.8105C11.191 17.6868 11.0468 17.5111 10.9616 17.3056C10.8765 17.1 10.8542 16.8738 10.8976 16.6556C10.941 16.4374 11.0482 16.2369 11.2055 16.0796C11.3628 15.9222 11.5633 15.8151 11.7815 15.7717C11.9998 15.7283 12.226 15.7505 12.4315 15.8357C12.6371 15.9208 12.8128 16.065 12.9364 16.25C13.06 16.4351 13.126 16.6526 13.126 16.8751C13.126 17.1734 13.0075 17.4596 12.7965 17.6706C12.5855 17.8815 12.2994 18.0001 12.001 18.0001Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'your_video_will_be_labeled_promotional',
|
||||
'Your video will be labeled "Promotional Content".'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'this_cannot_be_changed_once_posted',
|
||||
'This cannot be changed once your video is posted.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[14px] my-[10px] text-balance">
|
||||
{t(
|
||||
'turn_on_to_disclose_video_promotes',
|
||||
'Turn on to disclose that this video promotes goods or services in\n exchange for something of value. You video could promote yourself, a\n third party, or both.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(!disclose && 'invisible', 'mt-[20px]')}>
|
||||
<Checkbox
|
||||
variant="hollow"
|
||||
label={t('label_your_brand', 'Your brand')}
|
||||
disabled={isUploadMode}
|
||||
{...register('brand_organic_toggle', {
|
||||
value: false,
|
||||
})}
|
||||
/>
|
||||
<div className="text-balance my-[10px] text-[14px]">
|
||||
{t(
|
||||
'you_are_promoting_yourself',
|
||||
'You are promoting yourself or your own brand.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'this_video_will_be_classified_brand_organic',
|
||||
'This video will be classified as Brand Organic.'
|
||||
)}
|
||||
</div>
|
||||
<Checkbox
|
||||
variant="hollow"
|
||||
label={t('label_branded_content', 'Branded content')}
|
||||
disabled={isUploadMode}
|
||||
{...register('brand_content_toggle', {
|
||||
value: false,
|
||||
})}
|
||||
/>
|
||||
<div className="text-balance my-[10px] text-[14px]">
|
||||
{t(
|
||||
'you_are_promoting_another_brand',
|
||||
'You are promoting another brand or a third party.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'this_video_will_be_classified_branded_content',
|
||||
'This video will be classified as Branded Content.'
|
||||
)}
|
||||
</div>
|
||||
{(brand_organic_toggle || brand_content_toggle) && (
|
||||
<div className="my-[10px] text-[14px] text-balance">
|
||||
{t(
|
||||
'by_posting_you_agree_to_tiktoks',
|
||||
"By posting, you agree to TikTok's"
|
||||
)}
|
||||
{[
|
||||
brand_organic_toggle || brand_content_toggle ? (
|
||||
<a
|
||||
target="_blank"
|
||||
className="text-[#B69DEC] hover:underline"
|
||||
href="https://www.tiktok.com/legal/page/global/music-usage-confirmation/en"
|
||||
>
|
||||
{t('music_usage_confirmation', 'Music Usage Confirmation')}
|
||||
</a>
|
||||
) : undefined,
|
||||
brand_content_toggle ? <> {t('and', 'and')} </> : undefined,
|
||||
brand_content_toggle ? (
|
||||
<a
|
||||
target="_blank"
|
||||
className="text-[#B69DEC] hover:underline"
|
||||
href="https://www.tiktok.com/legal/page/global/bc-policy/en"
|
||||
>
|
||||
{t('branded_content_policy', 'Branded Content Policy')}
|
||||
</a>
|
||||
) : undefined,
|
||||
].filter((f) => f)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
TikTokSettings,
|
||||
undefined,
|
||||
TikTokDto,
|
||||
async (items) => {
|
||||
const [firstItems] = items;
|
||||
if (items.length !== 1) {
|
||||
return 'Tiktok items should be one';
|
||||
}
|
||||
if (firstItems.length === 0) {
|
||||
return 'No video / images selected';
|
||||
}
|
||||
if (
|
||||
firstItems.length > 1 &&
|
||||
firstItems?.some((p) => p?.path?.indexOf('mp4') > -1)
|
||||
) {
|
||||
return 'Only pictures are supported when selecting multiple items';
|
||||
} else if (
|
||||
firstItems?.length !== 1 &&
|
||||
firstItems?.[0]?.path?.indexOf('mp4') > -1
|
||||
) {
|
||||
return 'You need one media';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
2000
|
||||
);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async (posts) => {
|
||||
return true;
|
||||
},
|
||||
2048
|
||||
);
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
'use client';
|
||||
|
||||
import { FC, FormEvent, useCallback, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
export const Subreddit: FC<{
|
||||
onChange: (event: {
|
||||
target: {
|
||||
name: string;
|
||||
value: {
|
||||
id: string;
|
||||
subreddit: string;
|
||||
title: string;
|
||||
name: string;
|
||||
url: string;
|
||||
body: string;
|
||||
media: any[];
|
||||
};
|
||||
};
|
||||
}) => void;
|
||||
name: string;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const state = useSettings();
|
||||
const split = name.split('.');
|
||||
const [loading, setLoading] = useState(false);
|
||||
// @ts-ignore
|
||||
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
|
||||
const [results, setResults] = useState([]);
|
||||
const func = useCustomProviderFunction();
|
||||
const value = useWatch({
|
||||
name,
|
||||
});
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const setResult = (result: { id: string; name: string }) => async () => {
|
||||
setLoading(true);
|
||||
setSearchValue('');
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
id: String(result.id),
|
||||
subreddit: result.name,
|
||||
title: '',
|
||||
name: '',
|
||||
url: '',
|
||||
body: '',
|
||||
media: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
const setTitle = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
title: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const setURL = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
url: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const search = useDebouncedCallback(
|
||||
useCallback(async (e: FormEvent<HTMLInputElement>) => {
|
||||
// @ts-ignore
|
||||
setResults([]);
|
||||
// @ts-ignore
|
||||
if (!e.target.value) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const results = await func.get('subreddits', { word: e.target.value });
|
||||
// @ts-ignore
|
||||
setResults(results);
|
||||
}, []),
|
||||
500
|
||||
);
|
||||
return (
|
||||
<div className="bg-primary p-[20px]">
|
||||
{value?.subreddit ? (
|
||||
<>
|
||||
<Input
|
||||
error={errors?.subreddit?.message}
|
||||
disableForm={true}
|
||||
value={value.subreddit}
|
||||
readOnly={true}
|
||||
label="Channel"
|
||||
name="subreddit"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Channel"
|
||||
name="search"
|
||||
label="Search Channel"
|
||||
readOnly={loading}
|
||||
value={searchValue}
|
||||
error={errors?.message}
|
||||
disableForm={true}
|
||||
onInput={async (e) => {
|
||||
// @ts-ignore
|
||||
setSearchValue(e.target.value);
|
||||
await search(e);
|
||||
}}
|
||||
/>
|
||||
{!!results.length && !loading && (
|
||||
<div className="z-[400] w-full absolute bg-input -mt-[20px] outline-none border-fifth border cursor-pointer">
|
||||
{results.map((r: { id: string; name: string }) => (
|
||||
<div
|
||||
onClick={setResult(r)}
|
||||
key={r.id}
|
||||
className="px-[16px] py-[5px] hover:bg-secondary"
|
||||
>
|
||||
{r.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { Subreddit } from './subreddit';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
const WrapcastProvider: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
// control props comes from useForm (optional: if you are using FormContext)
|
||||
name: 'subreddit', // unique name for your Field Array
|
||||
});
|
||||
const t = useT();
|
||||
|
||||
const addField = useCallback(() => {
|
||||
append({});
|
||||
}, [fields, append]);
|
||||
const deleteField = useCallback(
|
||||
(index: number) => async () => {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
t(
|
||||
'are_you_sure_you_want_to_delete_this_subreddit',
|
||||
'Are you sure you want to delete this Subreddit?'
|
||||
)
|
||||
))
|
||||
)
|
||||
return;
|
||||
remove(index);
|
||||
},
|
||||
[fields, remove]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[20px] mb-[20px]">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col relative">
|
||||
<div
|
||||
onClick={deleteField(index)}
|
||||
className="absolute -start-[10px] justify-center items-center flex -top-[10px] w-[20px] h-[20px] bg-red-600 rounded-full text-textColor"
|
||||
>
|
||||
x
|
||||
</div>
|
||||
<Subreddit {...register(`subreddit.${index}.value`)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={addField}>{t('add_channel', 'Add Channel')}</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
WrapcastProvider,
|
||||
undefined,
|
||||
undefined,
|
||||
async (list) => {
|
||||
if (
|
||||
list.some((item) => item.some((field) => field.path.indexOf('mp4') > -1))
|
||||
) {
|
||||
return 'Warpcast can only accept images';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
800
|
||||
);
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
'use client';
|
||||
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { ThreadFinisher } from '@gitroom/frontend/components/new-launch/finisher/thread.finisher';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { XDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/x.dto';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
|
||||
const whoCanReply = [
|
||||
{
|
||||
label: 'Everyone',
|
||||
value: 'everyone',
|
||||
},
|
||||
{
|
||||
label: 'Accounts you follow',
|
||||
value: 'following',
|
||||
},
|
||||
{
|
||||
label: 'Mentioned accounts',
|
||||
value: 'mentionedUsers',
|
||||
},
|
||||
{
|
||||
label: 'Subscribers',
|
||||
value: 'subscribers',
|
||||
},
|
||||
{
|
||||
label: 'Verified accounts',
|
||||
value: 'verified',
|
||||
}
|
||||
]
|
||||
|
||||
const SettingsComponent = () => {
|
||||
const t = useT();
|
||||
const { register, watch, setValue } = useSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
label={t('label_who_can_reply_to_this_post', 'Who can reply to this post?')}
|
||||
className="mb-5"
|
||||
hideErrors={true}
|
||||
{...register('who_can_reply_post', {
|
||||
value: 'everyone',
|
||||
})}
|
||||
>
|
||||
{whoCanReply.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input label={'Post to a community, URL (Ex: https://x.com/i/communities/1493446837214187523)'} {...register('community')} />
|
||||
|
||||
<ThreadFinisher />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider(
|
||||
SettingsComponent,
|
||||
undefined,
|
||||
XDto,
|
||||
async (posts, settings, additionalSettings: any) => {
|
||||
const premium = additionalSettings?.find((p: any) => p?.title === 'Verified')?.value || false;
|
||||
if (posts.some((p) => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
if (
|
||||
posts.some(
|
||||
(p) => p.some((m) => m.path.indexOf('mp4') > -1) && p.length > 1
|
||||
)
|
||||
) {
|
||||
return 'There can be maximum 1 video in a post.';
|
||||
}
|
||||
for (const load of posts.flatMap((p) => p.flatMap((a) => a.path))) {
|
||||
if (load.indexOf('mp4') > -1) {
|
||||
const isValid = await checkVideoDuration(load, premium);
|
||||
if (!isValid) {
|
||||
return 'Video duration must be less than or equal to 140 seconds.';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
(settings) => {
|
||||
if (settings?.[0]?.value) {
|
||||
return 4000;
|
||||
}
|
||||
return 280;
|
||||
}
|
||||
);
|
||||
const checkVideoDuration = async (url: string, isPremium = false): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.src = url;
|
||||
video.preload = 'metadata';
|
||||
video.onloadedmetadata = () => {
|
||||
// Check if the duration is less than or equal to 140 seconds
|
||||
const duration = video.duration;
|
||||
if ((!isPremium && duration <= 140) || isPremium) {
|
||||
resolve(true); // Video duration is acceptable
|
||||
} else {
|
||||
resolve(false); // Video duration exceeds 140 seconds
|
||||
}
|
||||
};
|
||||
video.onerror = () => {
|
||||
reject(new Error('Failed to load video metadata.'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
|
||||
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { MediumTags } from '@gitroom/frontend/components/new-launch/providers/medium/medium.tags';
|
||||
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
const type = [
|
||||
{
|
||||
label: 'Public',
|
||||
value: 'public',
|
||||
},
|
||||
{
|
||||
label: 'Private',
|
||||
value: 'private',
|
||||
},
|
||||
{
|
||||
label: 'Unlisted',
|
||||
value: 'unlisted',
|
||||
},
|
||||
];
|
||||
const YoutubeSettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Input label="Title" {...register('title')} maxLength={100} />
|
||||
<Select
|
||||
label="Type"
|
||||
{...register('type', {
|
||||
value: 'public',
|
||||
})}
|
||||
>
|
||||
{type.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<MediumTags label="Tags" {...register('tags')} />
|
||||
<div className="mt-[20px]">
|
||||
<MediaComponent
|
||||
type="image"
|
||||
width={1280}
|
||||
height={720}
|
||||
label="Thumbnail"
|
||||
description="Thumbnail picture (optional)"
|
||||
{...register('thumbnail')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider(
|
||||
YoutubeSettings,
|
||||
undefined,
|
||||
YoutubeSettingsDto,
|
||||
async (items) => {
|
||||
const [firstItems] = items;
|
||||
if (items.length !== 1) {
|
||||
return 'Youtube items should be one';
|
||||
}
|
||||
if (items[0].length !== 1) {
|
||||
return 'You need one media';
|
||||
}
|
||||
if (firstItems[0].path.indexOf('mp4') === -1) {
|
||||
return 'Item must be a video';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
5000
|
||||
);
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
export const SelectCurrent: FC = () => {
|
||||
const { selectedIntegrations, current, setCurrent } = useLaunchStore(
|
||||
useShallow((state) => ({
|
||||
selectedIntegrations: state.selectedIntegrations,
|
||||
current: state.current,
|
||||
setCurrent: state.setCurrent,
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-[3px]">
|
||||
<div
|
||||
onClick={() => setCurrent('global')}
|
||||
className="cursor-pointer flex gap-[8px] items-center bg-customColor2 p-[10px] rounded-tl-[4px] rounded-tr-[4px]"
|
||||
>
|
||||
<div
|
||||
onClick={() => setCurrent('global')}
|
||||
className={clsx(current !== 'global' ? 'opacity-40' : '')}
|
||||
>
|
||||
T
|
||||
</div>
|
||||
</div>
|
||||
{selectedIntegrations.map(({ integration }) => (
|
||||
<div
|
||||
onClick={() => setCurrent(integration.id)}
|
||||
key={integration.id}
|
||||
className="cursor-pointer flex gap-[8px] items-center bg-customColor2 p-[10px] rounded-tl-[4px] rounded-tr-[4px]"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative w-[20px] h-[20px] rounded-full flex justify-center items-center bg-fifth filter transition-all duration-500',
|
||||
current !== integration.id ? 'opacity-40' : ''
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{integration.identifier === 'youtube' ? (
|
||||
<img
|
||||
src="/icons/platforms/youtube.svg"
|
||||
className="absolute z-10 -bottom-[5px] -end-[5px]"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={`/icons/platforms/${integration.identifier}.png`}
|
||||
className="rounded-full absolute z-10 -bottom-[5px] -end-[5px] border border-fifth"
|
||||
alt={integration.identifier}
|
||||
width={15}
|
||||
height={15}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { createRef, RefObject } from 'react';
|
||||
|
||||
interface Values {
|
||||
id: string;
|
||||
content: string;
|
||||
media: { id: string; path: string }[];
|
||||
}
|
||||
|
||||
interface Internal {
|
||||
integration: Integrations;
|
||||
integrationValue: Values[];
|
||||
}
|
||||
|
||||
interface SelectedIntegrations {
|
||||
settings: any;
|
||||
integration: Integrations;
|
||||
ref?: RefObject<any>;
|
||||
}
|
||||
|
||||
interface StoreState {
|
||||
date: dayjs.Dayjs;
|
||||
current: string;
|
||||
integrations: Integrations[];
|
||||
selectedIntegrations: SelectedIntegrations[];
|
||||
global: Values[];
|
||||
internal: Internal[];
|
||||
addGlobalValue: (index: number, value: Values[]) => void;
|
||||
addInternalValue: (
|
||||
index: number,
|
||||
integrationId: string,
|
||||
value: Values[]
|
||||
) => void;
|
||||
deleteGlobalValue: (index: number) => void;
|
||||
deleteInternalValue: (integrationId: string, index: number) => void;
|
||||
addRemoveInternal: (integrationId: string) => void;
|
||||
changeOrderGlobal: (fromIndex: number, toIndex: number) => void;
|
||||
changeOrderInternal: (
|
||||
integrationId: string,
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
) => void;
|
||||
setGlobalValueText: (index: number, content: string) => void;
|
||||
addGlobalValueMedia: (
|
||||
index: number,
|
||||
media: { id: string; path: string }[]
|
||||
) => void;
|
||||
removeGlobalValueMedia: (index: number, mediaIndex: number) => void;
|
||||
setInternalValueText: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
content: string
|
||||
) => void;
|
||||
addInternalValueMedia: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
media: { id: string; path: string }[]
|
||||
) => void;
|
||||
removeInternalValueMedia: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
mediaIndex: number
|
||||
) => void;
|
||||
setAllIntegrations: (integrations: Integrations[]) => void;
|
||||
setCurrent: (current: string) => void;
|
||||
addOrRemoveSelectedIntegration: (
|
||||
integration: Integrations,
|
||||
settings: any
|
||||
) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
date: dayjs(),
|
||||
current: 'global',
|
||||
integrations: [] as Integrations[],
|
||||
selectedIntegrations: [] as SelectedIntegrations[],
|
||||
global: [] as Values[],
|
||||
internal: [] as Internal[],
|
||||
};
|
||||
|
||||
export const useLaunchStore = create<StoreState>()((set) => ({
|
||||
...initialState,
|
||||
setCurrent: (current: string) =>
|
||||
set((state) => ({
|
||||
current: current,
|
||||
})),
|
||||
addOrRemoveSelectedIntegration: (
|
||||
integration: Integrations,
|
||||
settings: any
|
||||
) => {
|
||||
set((state) => {
|
||||
const existingIndex = state.selectedIntegrations.findIndex(
|
||||
(i) => i.integration.id === integration.id
|
||||
);
|
||||
|
||||
if (existingIndex > -1) {
|
||||
return {
|
||||
selectedIntegrations: state.selectedIntegrations.filter(
|
||||
(_, index) => index !== existingIndex
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
selectedIntegrations: [
|
||||
...state.selectedIntegrations,
|
||||
{ integration, settings, ref: createRef() },
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
addGlobalValue: (index: number, value: Values[]) =>
|
||||
set((state) => {
|
||||
if (!state.global.length) {
|
||||
return { global: value };
|
||||
}
|
||||
|
||||
return {
|
||||
global: state.global.reduce((acc, item, i) => {
|
||||
console.log(i, index);
|
||||
acc.push(item);
|
||||
if (i === index) {
|
||||
acc.push(...value);
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
};
|
||||
}),
|
||||
// Add value after index
|
||||
addInternalValue: (index: number, integrationId: string, value: Values[]) =>
|
||||
set((state) => {
|
||||
const newInternal = state.internal.map((i) => {
|
||||
if (i.integration.id === integrationId) {
|
||||
const newIntegrationValue = [...i.integrationValue];
|
||||
newIntegrationValue.splice(index + 1, 0, ...value);
|
||||
return { ...i, integrationValue: newIntegrationValue };
|
||||
}
|
||||
return i;
|
||||
});
|
||||
return { internal: newInternal };
|
||||
}),
|
||||
deleteGlobalValue: (index: number) =>
|
||||
set((state) => ({
|
||||
global: state.global.filter((_, i) => i !== index),
|
||||
})),
|
||||
deleteInternalValue: (integrationId: string, index: number) =>
|
||||
set((state) => {
|
||||
return {
|
||||
internal: state.internal.map((i) => {
|
||||
if (i.integration.id === integrationId) {
|
||||
return {
|
||||
...i,
|
||||
integrationValue: i.integrationValue.filter(
|
||||
(_, idx) => idx !== index
|
||||
),
|
||||
};
|
||||
}
|
||||
return i;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
addRemoveInternal: (integrationId: string) =>
|
||||
set((state) => {
|
||||
const integration = state.selectedIntegrations.find(
|
||||
(i) => i.integration.id === integrationId
|
||||
);
|
||||
const findIntegrationIndex = state.internal.findIndex(
|
||||
(i) => i.integration.id === integrationId
|
||||
);
|
||||
|
||||
if (findIntegrationIndex > -1) {
|
||||
return {
|
||||
internal: state.internal.filter(
|
||||
(i) => i.integration.id !== integrationId
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
internal: [
|
||||
...state.internal,
|
||||
{
|
||||
integration: integration.integration,
|
||||
integrationValue: state.global.slice(0).map((p) => p),
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
changeOrderGlobal: (fromIndex: number, toIndex: number) =>
|
||||
set((state) => {
|
||||
const updatedGlobal = [...state.global];
|
||||
const [movedItem] = updatedGlobal.splice(fromIndex, 1);
|
||||
updatedGlobal.splice(toIndex, 0, movedItem);
|
||||
return { global: updatedGlobal };
|
||||
}),
|
||||
changeOrderInternal: (
|
||||
integrationId: string,
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
) =>
|
||||
set((state) => {
|
||||
const updatedInternal = state.internal.map((i) => {
|
||||
if (i.integration.id === integrationId) {
|
||||
const updatedValues = [...i.integrationValue];
|
||||
const [movedItem] = updatedValues.splice(fromIndex, 1);
|
||||
updatedValues.splice(toIndex, 0, movedItem);
|
||||
return { ...i, integrationValue: updatedValues };
|
||||
}
|
||||
return i;
|
||||
});
|
||||
return { internal: updatedInternal };
|
||||
}),
|
||||
setGlobalValueText: (index: number, content: string) =>
|
||||
set((state) => ({
|
||||
global: state.global.map((item, i) =>
|
||||
i === index ? { ...item, content } : item
|
||||
),
|
||||
})),
|
||||
addGlobalValueMedia: (index: number, media: { id: string; path: string }[]) =>
|
||||
set((state) => ({
|
||||
global: state.global.map((item, i) =>
|
||||
i === index ? { ...item, media: [...item.media, ...media] } : item
|
||||
),
|
||||
})),
|
||||
removeGlobalValueMedia: (index: number, mediaIndex: number) =>
|
||||
set((state) => ({
|
||||
global: state.global.map((item, i) =>
|
||||
i === index
|
||||
? {
|
||||
...item,
|
||||
media: item.media.filter((_, idx) => idx !== mediaIndex),
|
||||
}
|
||||
: item
|
||||
),
|
||||
})),
|
||||
setInternalValueText: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
content: string
|
||||
) =>
|
||||
set((state) => ({
|
||||
internal: state.internal.map((item) =>
|
||||
item.integration.id === integrationId
|
||||
? {
|
||||
...item,
|
||||
integrationValue: item.integrationValue.map((v, i) =>
|
||||
i === index ? { ...v, content } : v
|
||||
),
|
||||
}
|
||||
: item
|
||||
),
|
||||
})),
|
||||
addInternalValueMedia: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
media: { id: string; path: string }[]
|
||||
) =>
|
||||
set((state) => ({
|
||||
internal: state.internal.map((item) =>
|
||||
item.integration.id === integrationId
|
||||
? {
|
||||
...item,
|
||||
integrationValue: item.integrationValue.map((v, i) =>
|
||||
i === index ? { ...v, media: [...v.media, ...media] } : v
|
||||
),
|
||||
}
|
||||
: item
|
||||
),
|
||||
})),
|
||||
removeInternalValueMedia: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
mediaIndex: number
|
||||
) =>
|
||||
set((state) => ({
|
||||
internal: state.internal.map((item) =>
|
||||
item.integration.id === integrationId
|
||||
? {
|
||||
...item,
|
||||
integrationValue: item.integrationValue.map((v, i) =>
|
||||
i === index
|
||||
? {
|
||||
...v,
|
||||
media: v.media.filter((_, idx) => idx !== mediaIndex),
|
||||
}
|
||||
: v
|
||||
),
|
||||
}
|
||||
: item
|
||||
),
|
||||
})),
|
||||
reset: () =>
|
||||
set((state) => ({
|
||||
...state,
|
||||
...initialState,
|
||||
})),
|
||||
setAllIntegrations: (integrations: Integrations[]) =>
|
||||
set((state) => ({
|
||||
integrations: integrations,
|
||||
})),
|
||||
}));
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useCallback } from 'react';
|
||||
import { Editor, Transforms } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
const underlineMap = {
|
||||
a: 'a̲',
|
||||
b: 'b̲',
|
||||
c: 'c̲',
|
||||
d: 'd̲',
|
||||
e: 'e̲',
|
||||
f: 'f̲',
|
||||
g: 'g̲',
|
||||
h: 'h̲',
|
||||
i: 'i̲',
|
||||
j: 'j̲',
|
||||
k: 'k̲',
|
||||
l: 'l̲',
|
||||
m: 'm̲',
|
||||
n: 'n̲',
|
||||
o: 'o̲',
|
||||
p: 'p̲',
|
||||
q: 'q̲',
|
||||
r: 'r̲',
|
||||
s: 's̲',
|
||||
t: 't̲',
|
||||
u: 'u̲',
|
||||
v: 'v̲',
|
||||
w: 'w̲',
|
||||
x: 'x̲',
|
||||
y: 'y̲',
|
||||
z: 'z̲',
|
||||
A: 'A̲',
|
||||
B: 'B̲',
|
||||
C: 'C̲',
|
||||
D: 'D̲',
|
||||
E: 'E̲',
|
||||
F: 'F̲',
|
||||
G: 'G̲',
|
||||
H: 'H̲',
|
||||
I: 'I̲',
|
||||
J: 'J̲',
|
||||
K: 'K̲',
|
||||
L: 'L̲',
|
||||
M: 'M̲',
|
||||
N: 'N̲',
|
||||
O: 'O̲',
|
||||
P: 'P̲',
|
||||
Q: 'Q̲',
|
||||
R: 'R̲',
|
||||
S: 'S̲',
|
||||
T: 'T̲',
|
||||
U: 'U̲',
|
||||
V: 'V̲',
|
||||
W: 'W̲',
|
||||
X: 'X̲',
|
||||
Y: 'Y̲',
|
||||
Z: 'Z̲',
|
||||
'1': '1̲',
|
||||
'2': '2̲',
|
||||
'3': '3̲',
|
||||
'4': '4̲',
|
||||
'5': '5̲',
|
||||
'6': '6̲',
|
||||
'7': '7̲',
|
||||
'8': '8̲',
|
||||
'9': '9̲',
|
||||
'0': '0̲',
|
||||
};
|
||||
const reverseMap = Object.fromEntries(
|
||||
Object.entries(underlineMap).map(([key, value]) => [value, key])
|
||||
);
|
||||
export const UText: FC<{
|
||||
editor: any;
|
||||
currentValue: string;
|
||||
}> = ({ editor }) => {
|
||||
const mark = () => {
|
||||
const selectedText = Editor.string(editor, editor.selection);
|
||||
const setUnderline = selectedText.indexOf('̲') === -1;
|
||||
const newText = Array.from(
|
||||
!selectedText
|
||||
? prompt('What do you want to write?') || ''
|
||||
: selectedText.replace(/̲/g, '')
|
||||
)
|
||||
.map((char) => {
|
||||
// @ts-ignore
|
||||
return ((setUnderline ? underlineMap?.[char] : reverseMap?.[char]) || char);
|
||||
})
|
||||
.join('');
|
||||
Transforms.insertText(editor, newText);
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
onClick={mark}
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_31_12620)">
|
||||
<path
|
||||
d="M10.4119 6.47V12.77C10.4119 13.4 10.5669 13.885 10.8769 14.225C11.1869 14.565 11.6419 14.735 12.2419 14.735C12.8419 14.735 13.3019 14.565 13.6219 14.225C13.9419 13.885 14.1019 13.4 14.1019 12.77V6.47H16.6669V12.755C16.6669 13.695 16.4669 14.49 16.0669 15.14C15.6669 15.79 15.1269 16.28 14.4469 16.61C13.7769 16.94 13.0269 17.105 12.1969 17.105C11.3669 17.105 10.6219 16.945 9.96191 16.625C9.31191 16.295 8.79691 15.805 8.41691 15.155C8.03691 14.495 7.84691 13.695 7.84691 12.755V6.47H10.4119Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M6.96191 18.5H17.5369V19.25H6.96191V18.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_31_12620">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.25)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -23,7 +23,7 @@ export const SignatureBox: FC<{
|
|||
)}
|
||||
<div
|
||||
onClick={addSignature}
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center rounded-tl-lg rounded-tr-lg"
|
||||
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
|
|
|
|||
|
|
@ -213,7 +213,8 @@
|
|||
"ws": "^8.18.0",
|
||||
"yargs": "^17.7.2",
|
||||
"yup": "^1.4.0",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.32",
|
||||
|
|
|
|||
|
|
@ -519,6 +519,9 @@ importers:
|
|||
zod:
|
||||
specifier: ^3.24.1
|
||||
version: 3.24.4
|
||||
zustand:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5(@types/react@18.3.1)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1))
|
||||
devDependencies:
|
||||
'@crxjs/vite-plugin':
|
||||
specifier: ^2.0.0-beta.32
|
||||
|
|
@ -15760,6 +15763,24 @@ packages:
|
|||
zod@3.24.4:
|
||||
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
|
||||
|
||||
zustand@5.0.5:
|
||||
resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=18.0.0'
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
|
|
@ -19602,21 +19623,6 @@ snapshots:
|
|||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nrwl/js@19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@nx/js': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.5.4)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
- '@types/node'
|
||||
- debug
|
||||
- nx
|
||||
- supports-color
|
||||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nrwl/nest@19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(chokidar@3.5.3)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(typescript@5.5.4))(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@nx/nest': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(chokidar@3.5.3)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(ts-node@10.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(typescript@5.5.4))(typescript@5.5.4)
|
||||
|
|
@ -19963,7 +19969,7 @@ snapshots:
|
|||
'@babel/preset-env': 7.27.1(@babel/core@7.27.1)
|
||||
'@babel/preset-typescript': 7.27.1(@babel/core@7.27.1)
|
||||
'@babel/runtime': 7.27.1
|
||||
'@nrwl/js': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.5.4)
|
||||
'@nrwl/js': 19.7.2(@babel/traverse@7.27.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))(@types/node@18.16.9)(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))(typescript@5.4.5)
|
||||
'@nx/devkit': 19.7.2(nx@19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13)))
|
||||
'@nx/workspace': 19.7.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.13))(@swc/types@0.1.7)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.13))
|
||||
babel-plugin-const-enum: 1.2.0(@babel/core@7.27.1)
|
||||
|
|
@ -35453,4 +35459,11 @@ snapshots:
|
|||
|
||||
zod@3.24.4: {}
|
||||
|
||||
zustand@5.0.5(@types/react@18.3.1)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)):
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.1
|
||||
immer: 9.0.21
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.5.0(react@18.3.1)
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue