feat: new postiz modal

This commit is contained in:
Nevo David 2025-06-26 20:00:42 +07:00
parent 0f17d56522
commit af2f952ed0
149 changed files with 1722 additions and 7025 deletions

View File

@ -99,6 +99,7 @@ html {
cursor: text;
display: flex;
align-items: center;
margin-right: 0 !important;
}
.react-tags.is-active {
@ -191,6 +192,7 @@ html {
.react-tags__combobox-input {
/* prevent autoresize overflowing the container */
max-width: 100%;
width: 100% !important;
/* remove styles and layout from this element */
margin: 0;
padding: 0;
@ -198,7 +200,7 @@ html {
outline: none;
background: none;
/* match the font styles */
font-size: inherit;
font-size: 16px;
line-height: inherit;
}
@ -434,7 +436,7 @@ div div .set-font-family {
}
.tags-top .react-tags__combobox {
height: 35px;
height: 44px;
display: flex;
background-color: #141c2c;
padding-left: 10px;

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
import { Button } from '@gitroom/react/form/button';
import React, { FC } from 'react';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
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();
useCopilotAction({
name: 'addPost_' + num,
description: 'Add a post after the post number ' + num,
handler: () => {
onClick();
},
});
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>
);
};

View File

@ -4,6 +4,7 @@ import clsx from 'clsx';
import Loading from 'react-loading';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
const list = [
'Realistic',
'Cartoon',
@ -27,10 +28,12 @@ export const AiImage: FC<{
const t = useT();
const { value, onChange } = props;
const [loading, setLoading] = useState(false);
const setLocked = useLaunchStore(p => p.setLocked);
const fetch = useFetch();
const generateImage = useCallback(
(type: string) => async () => {
setLoading(true);
setLocked(true);
const image = await (
await fetch('/media/generate-image-with-prompt', {
method: 'POST',
@ -49,6 +52,7 @@ ${type}
})
).json();
setLoading(false);
setLocked(false);
onChange(image);
},
[value, onChange]

View File

@ -1,119 +0,0 @@
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>
);
};

View File

@ -31,7 +31,6 @@ import 'dayjs/locale/tr';
import 'dayjs/locale/vi';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { useModals } from '@mantine/modals';
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';

View File

@ -1,123 +0,0 @@
import { forwardRef, useCallback, useRef, useState } from 'react';
import type { MDEditorProps } from '@uiw/react-md-editor/src/Types';
import { RefMDEditor } from '@uiw/react-md-editor/src/Editor';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import { CopilotTextarea } from '@copilotkit/react-textarea';
import clsx from 'clsx';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { Transforms } from 'slate';
import EmojiPicker from 'emoji-picker-react';
import { Theme } from 'emoji-picker-react';
import { BoldText } from '@gitroom/frontend/components/launches/bold.text';
import { UText } from '@gitroom/frontend/components/launches/u.text';
import { SignatureBox } from '@gitroom/frontend/components/signature';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const Editor = forwardRef<
RefMDEditor,
MDEditorProps & {
order: number;
totalPosts: number;
disabledCopilot?: boolean;
}
>(
(
props: MDEditorProps & {
order: number;
totalPosts: number;
disabledCopilot?: boolean;
},
ref: React.ForwardedRef<RefMDEditor>
) => {
const user = useUser();
const [id] = useState(makeId(10));
const newRef = useRef<any>(null);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
const t = useT();
useCopilotReadable({
...(props.disabledCopilot ? { available: 'disabled' } : {}),
description: 'Content of the post number ' + (props.order + 1),
value: JSON.stringify({
content: props.value,
order: props.order,
allowAddContent: props?.value?.length === 0,
}),
});
useCopilotAction({
...(props.disabledCopilot ? { available: 'disabled' } : {}),
name: 'editPost_' + props.order,
description: `Edit the content of post number ${props.order}`,
parameters: [
{
name: 'content',
type: 'string',
},
],
handler: async ({ content }) => {
props?.onChange?.(content);
},
});
const addText = useCallback(
(emoji: string) => {
setTimeout(() => {
// @ts-ignore
Transforms.insertText(newRef?.current?.editor!, emoji);
}, 10);
},
[props.value, id]
);
return (
<>
<div className="flex gap-[5px] justify-end -mt-[30px]">
<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 rounded-tl-lg rounded-tr-lg"
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>
<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>
</>
);
}
);

View File

@ -1,64 +0,0 @@
import { Slider } from '@gitroom/react/form/slider';
import clsx from 'clsx';
import { useState } from 'react';
import { Editor } from '@gitroom/frontend/components/launches/editor';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useSettings } from '@gitroom/frontend/components/launches/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>
);
};

View File

@ -6,10 +6,13 @@ import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
import { FC } from 'react';
import { textSlicer } from '@gitroom/helpers/utils/count.length';
import interClass from '@gitroom/react/helpers/inter.font';
import Image from 'next/image';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
export const GeneralPreviewComponent: FC<{
maximumCharacters?: number;
}> = (props) => {
const { value: topValue, integration } = useIntegration();
const current = useLaunchStore((state) => state.current);
const mediaDir = useMediaDirectory();
const newValues = useFormatting(topValue, {
removeMarkdown: true,
@ -41,11 +44,23 @@ export const GeneralPreviewComponent: FC<{
)}
>
<div className="w-[40px] flex flex-col items-center">
<img
src={integration?.picture || '/no-picture.jpg'}
alt="x"
className="rounded-full relative z-[2]"
/>
<div className="relative">
<img
src={current === 'global' ? '/no-picture.jpg' : (integration?.picture || '/no-picture.jpg')}
alt="x"
className="rounded-full relative z-[2]"
/>
{current !== 'global' && (
<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>
{index !== topValue.length - 1 && (
<div className="flex-1 w-[2px] h-[calc(100%-10px)] bg-customColor25 absolute top-[10px] z-[1]" />
)}
@ -53,7 +68,7 @@ export const GeneralPreviewComponent: FC<{
<div className="flex-1 flex flex-col gap-[4px]">
<div className="flex">
<div className="h-[22px] text-[15px] font-[700]">
{integration?.name}
{current === 'global' ? 'Global Edit' : integration?.name}
</div>
<div className="text-[15px] text-customColor26 mt-[1px] ms-[2px]">
<svg
@ -69,7 +84,7 @@ export const GeneralPreviewComponent: FC<{
</svg>
</div>
<div className="text-[15px] font-[400] text-customColor27 ms-[4px]">
{integration?.display || '@username'}
{current === 'global' ? '' : integration?.display || '@username'}
</div>
</div>
<pre

View File

@ -15,10 +15,10 @@ import {
CalendarWeekProvider,
useCalendar,
} from '@gitroom/frontend/components/launches/calendar.context';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import dayjs from 'dayjs';
import { Select } from '@gitroom/react/form/select';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
const FirstStep: FC = (props) => {
const { integrations, reloadCalendarView } = useCalendar();
const modal = useModals();

View File

@ -15,6 +15,7 @@ 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';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
const postUrlEmitter = new EventEmitter();
export const ShowLinkedinCompany = () => {
const [showPostSelector, setShowPostSelector] = useState(false);
@ -53,6 +54,37 @@ export const ShowLinkedinCompany = () => {
<LinkedinCompany id={id} onClose={close} onSelect={callback?.callback!} />
);
};
export const LinkedinCompanyPop: FC<{
addText: (value: any) => void;
}> = (props) => {
const current = useLaunchStore((state) => state.current);
return (
<svg
onClick={() => {
postUrlEmitter.emit('show', {
id: current,
callback: (value: any) => {
props.addText(value);
},
});
}}
data-tooltip-id="tooltip"
data-tooltip-content="Add a LinkedIn Company"
className="mx-[10px] cursor-pointer"
width="20"
height="20"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M24 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585786C0.210714 0.960859 0 1.46957 0 2V24C0 24.5304 0.210714 25.0391 0.585786 25.4142C0.960859 25.7893 1.46957 26 2 26H24C24.5304 26 25.0391 25.7893 25.4142 25.4142C25.7893 25.0391 26 24.5304 26 24V2C26 1.46957 25.7893 0.960859 25.4142 0.585786C25.0391 0.210714 24.5304 0 24 0ZM9 19C9 19.2652 8.89464 19.5196 8.70711 19.7071C8.51957 19.8946 8.26522 20 8 20C7.73478 20 7.48043 19.8946 7.29289 19.7071C7.10536 19.5196 7 19.2652 7 19V11C7 10.7348 7.10536 10.4804 7.29289 10.2929C7.48043 10.1054 7.73478 10 8 10C8.26522 10 8.51957 10.1054 8.70711 10.2929C8.89464 10.4804 9 10.7348 9 11V19ZM8 9C7.70333 9 7.41332 8.91203 7.16665 8.7472C6.91997 8.58238 6.72771 8.34811 6.61418 8.07403C6.50065 7.79994 6.47094 7.49834 6.52882 7.20736C6.5867 6.91639 6.72956 6.64912 6.93934 6.43934C7.14912 6.22956 7.41639 6.0867 7.70736 6.02882C7.99834 5.97094 8.29994 6.00065 8.57403 6.11418C8.84811 6.22771 9.08238 6.41997 9.2472 6.66665C9.41203 6.91332 9.5 7.20333 9.5 7.5C9.5 7.89782 9.34196 8.27936 9.06066 8.56066C8.77936 8.84196 8.39782 9 8 9ZM20 19C20 19.2652 19.8946 19.5196 19.7071 19.7071C19.5196 19.8946 19.2652 20 19 20C18.7348 20 18.4804 19.8946 18.2929 19.7071C18.1054 19.5196 18 19.2652 18 19V14.5C18 13.837 17.7366 13.2011 17.2678 12.7322C16.7989 12.2634 16.163 12 15.5 12C14.837 12 14.2011 12.2634 13.7322 12.7322C13.2634 13.2011 13 13.837 13 14.5V19C13 19.2652 12.8946 19.5196 12.7071 19.7071C12.5196 19.8946 12.2652 20 12 20C11.7348 20 11.4804 19.8946 11.2929 19.7071C11.1054 19.5196 11 19.2652 11 19V11C11.0012 10.7551 11.0923 10.5191 11.256 10.3369C11.4197 10.1546 11.6446 10.0388 11.888 10.0114C12.1314 9.98392 12.3764 10.0468 12.5765 10.188C12.7767 10.3292 12.918 10.539 12.9738 10.7775C13.6502 10.3186 14.4389 10.0526 15.2552 10.0081C16.0714 9.96368 16.8844 10.1424 17.6067 10.5251C18.329 10.9078 18.9335 11.48 19.3551 12.1803C19.7768 12.8806 19.9997 13.6825 20 14.5V19Z"
fill="currentColor"
/>
</svg>
);
};
export const showPostSelector = (id: string) => {
return new Promise<string>((resolve) => {
postUrlEmitter.emit('show', {

View File

@ -1,12 +1,12 @@
import React, { useCallback } from 'react';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import { useModals } from '@mantine/modals';
import dayjs from 'dayjs';
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { SetSelectionModal } from '@gitroom/frontend/components/launches/calendar';
import { useSet } from '@gitroom/frontend/components/launches/set.context';
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
export const NewPost = () => {
const fetch = useFetch();
const modal = useModals();

View File

@ -1,82 +0,0 @@
'use client';
import { FC, useEffect } from 'react';
import { CustomSelect } from '@gitroom/react/form/custom.select';
import { FormProvider, useForm } from 'react-hook-form';
export interface Information {
buyer: Buyer;
usedIds: Array<{
id: string;
status: 'NO' | 'WAITING_CONFIRMATION' | 'YES';
}>;
id: string;
missing: Missing[];
}
export interface Buyer {
id: string;
name: string;
picture: Picture;
}
export interface Picture {
id: string;
path: string;
}
export interface Missing {
integration: Integration;
missing: number;
}
export interface Integration {
quantity: number;
integration: Integration2;
}
export interface Integration2 {
id: string;
name: string;
providerIdentifier: string;
}
export const PostToOrganization: FC<{
information: Information[];
onChange: (order?: Information) => void;
selected?: string;
}> = (props) => {
const { information, onChange, selected } = props;
const form = useForm();
const postFor = form.watch('post_for');
useEffect(() => {
onChange(information?.find((p) => p.id === postFor?.value)!);
}, [postFor]);
useEffect(() => {
if (!selected || !information?.length) {
return;
}
const findIt = information?.find((p) => p.id === selected);
form.setValue('post_for', {
value: findIt?.id,
});
onChange(information?.find((p) => p.id === selected)!);
}, [selected, information]);
if (!information?.length) {
return null;
}
return (
<FormProvider {...form}>
<CustomSelect
className="w-[240px]"
removeError={true}
label=""
placeholder="Select order from marketplace"
name="post_for"
options={information?.map((p) => ({
label: 'For: ' + p?.buyer?.name,
value: p?.id,
icon: (
<img
src={p?.buyer?.picture?.path}
className="w-[24px] h-[24px] rounded-full"
/>
),
}))}
/>
</FormProvider>
);
};

View File

@ -1,54 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { ShowAllProviders } from '@gitroom/frontend/components/launches/providers/show.all.providers';
import dayjs from 'dayjs';
import { useStateCallback } from '@gitroom/react/helpers/use.state.callback';
export const ProvidersOptions: FC<{
hideEditOnlyThis: boolean;
integrations: Integrations[];
allIntegrations: Integrations[];
editorValue: Array<{
id?: string;
content: string;
}>;
date: dayjs.Dayjs;
}> = (props) => {
const { integrations, editorValue, date, hideEditOnlyThis } = props;
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback([
integrations[0],
]);
useEffect(() => {
if (integrations.indexOf(selectedIntegrations[0]) === -1) {
setSelectedIntegrations([integrations[0]]);
}
}, [integrations, selectedIntegrations]);
return (
<div className="flex flex-1 flex-col">
<PickPlatforms
integrations={integrations}
selectedIntegrations={selectedIntegrations}
onChange={setSelectedIntegrations}
singleSelect={true}
hide={integrations.length === 1}
isMain={false}
/>
<IntegrationContext.Provider
value={{
value: editorValue,
integration: selectedIntegrations?.[0],
date,
allIntegrations: props.allIntegrations,
}}
>
<ShowAllProviders
hideEditOnlyThis={hideEditOnlyThis}
value={editorValue}
integrations={integrations}
selectedProvider={selectedIntegrations?.[0]}
/>
</IntegrationContext.Provider>
</div>
);
};

View File

@ -1,27 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/launches/finisher/thread.finisher';
const SettingsComponent = () => {
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
);

View File

@ -1,119 +0,0 @@
import { FC, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/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/launches/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>
);
};

View File

@ -1,121 +0,0 @@
import { FC, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/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/launches/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>
);
};

View File

@ -1,108 +0,0 @@
import { FC, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/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/launches/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>
);
};

View File

@ -1,8 +0,0 @@
import { InstagramContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/instagram/instagram.continue';
import { FacebookContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/facebook/facebook.continue';
import { LinkedinContinue } from '@gitroom/frontend/components/launches/providers/continue-provider/linkedin/linkedin.continue';
export const continueProviderList = {
instagram: InstagramContinue,
facebook: FacebookContinue,
'linkedin-page': LinkedinContinue,
};

View File

@ -1,93 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
import { SelectOrganization } from '@gitroom/frontend/components/launches/providers/devto/select.organization';
import { DevtoTags } from '@gitroom/frontend/components/launches/providers/devto/devto.tags';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import clsx from 'clsx';
import localFont from 'next/font/local';
import MDEditor from '@uiw/react-md-editor';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
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);

View File

@ -1,71 +0,0 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { ReactTags } from 'react-tag-autocomplete';
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>
);
};

View File

@ -1,55 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
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>
);
};

View File

@ -1,55 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
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>
);
};

View File

@ -1,20 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/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/launches/providers/discord/discord.channel.select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const DiscordComponent: FC = () => {
const form = useSettings();
return (
<div>
<DiscordChannelSelect {...form.register('channel')} />
</div>
);
};
export default withProvider(
DiscordComponent,
undefined,
DiscordDto,
undefined,
1980
);

View File

@ -1,47 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { DribbbleTeams } from '@gitroom/frontend/components/launches/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.';
}
);

View File

@ -1,58 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
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>
);
};

View File

@ -1,2 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(null, undefined, undefined, undefined, 63206);

View File

@ -1,95 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { HashnodePublications } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.publications';
import { HashnodeTags } from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.tags';
import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import clsx from 'clsx';
import MDEditor from '@uiw/react-md-editor';
import { Plus_Jakarta_Sans } from 'next/font/google';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
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
);

View File

@ -1,55 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
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>
);
};

View File

@ -1,76 +0,0 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { ReactTags } from 'react-tag-autocomplete';
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 || <>&nbsp;</>}</div>
</div>
);
};

View File

@ -1,707 +0,0 @@
'use client';
import React, {
FC,
Fragment,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
ClipboardEvent,
memo,
} from 'react';
import { Button } from '@gitroom/react/form/button';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor';
import { useValues } from '@gitroom/frontend/components/launches/helpers/use.values';
import { FormProvider } from 'react-hook-form';
import { useMoveToIntegrationListener } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import {
IntegrationContext,
useIntegration,
} from '@gitroom/frontend/components/launches/helpers/use.integration';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
import { createPortal } from 'react-dom';
import clsx from 'clsx';
import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
import { arrayMoveImmutable } from 'array-move';
import {
LinkedinCompany,
linkedinCompany,
} from '@gitroom/frontend/components/launches/helpers/linkedin.component';
import { Editor } from '@gitroom/frontend/components/launches/editor';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.button';
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
import { capitalize } from 'lodash';
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { InternalChannels } from '@gitroom/frontend/components/launches/internal.channels';
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useSet } from '@gitroom/frontend/components/launches/set.context';
import { SeparatePost } from '@gitroom/frontend/components/launches/separate.post';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
// Simple component to change back to settings on after changing tab
export const SetTab: FC<{
changeTab: () => void;
}> = (props) => {
useEffect(() => {
return () => {
setTimeout(() => {
props.changeTab();
}, 500);
};
}, []);
return null;
};
// This is a simple function that if we edit in place, we hide the editor on top
export const EditorWrapper: FC<{
children: ReactNode;
}> = ({ children }) => {
const showHide = useHideTopEditor();
const [showEditor, setShowEditor] = useState(false);
useEffect(() => {
setShowEditor(true);
showHide.hide();
return () => {
showHide.show();
setShowEditor(false);
};
}, []);
if (!showEditor) {
return null;
}
return children;
};
export const withProvider = 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 (props: {
identifier: string;
id: string;
value: Array<{
content: string;
id?: string;
image?: Array<{
path: string;
id: string;
}>;
}>;
hideMenu?: boolean;
show: boolean;
hideEditOnlyThis?: boolean;
}) => {
const existingData = useExistingData();
const t = useT();
const { allIntegrations, integration, date } = useIntegration();
const [showLinkedinPopUp, setShowLinkedinPopUp] = useState<any>(false);
const [uploading, setUploading] = useState(false);
const [showComponent, setShowComponent] = useState(false);
const fetch = useFetch();
useEffect(() => {
setTimeout(() => {
setShowComponent(true);
}, 1);
}, []);
useCopilotReadable({
description:
integration?.type === 'social'
? 'force content always in MD format'
: 'force content always to be fit to social media',
value: '',
});
const [editInPlace, setEditInPlace] = useState(!!existingData.integration);
const [InPlaceValue, setInPlaceValue] = useState<
Array<{
id?: string;
content: string;
image?: Array<{
id: string;
path: string;
}>;
}>
>(
// @ts-ignore
existingData.integration
? existingData.posts.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))
: [
{
content: '',
},
]
);
const [showTab, setShowTab] = useState(0);
const Component = useMemo(() => {
return SettingsComponent ? SettingsComponent : () => <></>;
}, [SettingsComponent]);
// in case there is an error on submit, we change to the settings tab for the specific provider
useMoveToIntegrationListener(
[props.id],
true,
({ identifier, toPreview }) => {
if (identifier === props.id) {
setShowTab(toPreview ? 1 : 2);
}
}
);
const set = useSet();
// this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation
const form = useValues(
set?.set
? set?.set?.posts?.find((p) => p?.integration?.id === props?.id)
?.settings
: existingData.settings,
props.id,
props.identifier,
editInPlace ? InPlaceValue : props.value,
dto,
checkValidity,
!maximumCharacters
? undefined
: typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(JSON.parse(integration?.additionalSettings || '[]'))
);
// change editor value
const changeValue = useCallback(
(index: number) => (newValue: string) => {
return setInPlaceValue((prev) => {
prev[index].content = newValue;
return [...prev];
});
},
[InPlaceValue]
);
const merge = useCallback(() => {
setInPlaceValue(
InPlaceValue.reduce(
(all, current) => {
all[0].content = all[0].content + current.content + '\n';
all[0].image = [...all[0].image, ...(current.image || [])];
return all;
},
[
{
content: '',
id: InPlaceValue[0].id,
image: [] as {
id: string;
path: string;
}[],
},
]
)
);
}, [InPlaceValue]);
const separatePosts = useCallback(
(posts: string[]) => {
setInPlaceValue(
posts.map((p, i) => ({
content: p,
id: InPlaceValue?.[i]?.id || makeId(10),
image: InPlaceValue?.[i]?.image || [],
}))
);
},
[InPlaceValue]
);
const changeImage = useCallback(
(index: number) =>
(newValue: {
target: {
name: string;
value?: Array<{
id: string;
path: string;
}>;
};
}) => {
return setInPlaceValue((prev) => {
prev[index].image = newValue.target.value;
return [...prev];
});
},
[InPlaceValue]
);
// add another local editor
const addValue = useCallback(
(index: number) => () => {
setInPlaceValue((prev) => {
return prev.reduce(
(acc, p, i) => {
acc.push(p);
if (i === index) {
acc.push({
content: '',
});
}
return acc;
},
[] as Array<{
content: string;
}>
);
});
},
[]
);
const changePosition = useCallback(
(index: number) => (type: 'up' | 'down') => {
if (type === 'up' && index !== 0) {
setInPlaceValue((prev) => {
return arrayMoveImmutable(prev, index, index - 1);
});
} else if (type === 'down') {
setInPlaceValue((prev) => {
return arrayMoveImmutable(prev, index, index + 1);
});
}
},
[]
);
// Delete post
const deletePost = useCallback(
(index: number) => async () => {
if (
!(await deleteDialog(
'Are you sure you want to delete this post?',
'Yes, delete it!'
))
) {
return;
}
setInPlaceValue((prev) => {
prev.splice(index, 1);
return [...prev];
});
},
[InPlaceValue]
);
// This is a function if we want to switch from the global editor to edit in place
const changeToEditor = useCallback(async () => {
if (
!(await deleteDialog(
!editInPlace
? 'Are you sure you want to edit only this?'
: 'Are you sure you want to revert it back to global editing?',
'Yes, edit in place!'
))
) {
return false;
}
setEditInPlace(!editInPlace);
setInPlaceValue(
editInPlace
? [
{
content: '',
},
]
: props.value.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))
);
}, [props.value, editInPlace]);
useCopilotAction({
name: editInPlace
? 'switchToGlobalEdit'
: `editInPlace_${integration?.identifier}`,
description: editInPlace
? 'Switch to global editing'
: `Edit only ${integration?.identifier} this, if you want a different identifier, you have to use setSelectedIntegration first`,
handler: async () => {
await changeToEditor();
},
});
const tagPersonOrCompany = useCallback(
(integration: string, editor: (value: string) => void) => () => {
setShowLinkedinPopUp(
<LinkedinCompany
onSelect={(tag) => {
editor(tag);
}}
id={integration}
onClose={() => setShowLinkedinPopUp(false)}
/>
);
},
[]
);
const uppy = useUppyUploader({
onUploadSuccess: () => {
/**empty**/
},
allowedFileTypes: 'image/*,video/mp4',
});
const pasteImages = useCallback(
(index: number, currentValue: any[], isFile?: boolean) => {
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
// @ts-ignore
const clipboardItems = isFile
? // @ts-ignore
event.map((p) => ({
kind: 'file',
getAsFile: () => p,
}))
: // @ts-ignore
event.clipboardData?.items; // Ensure clipboardData is available
if (!clipboardItems) {
return;
}
const files: File[] = [];
// @ts-ignore
for (const item of clipboardItems) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
if (isImage || isVideo) {
files.push(file); // Collect images or videos
}
}
}
}
if (files.length === 0) {
return;
}
setUploading(true);
const lastValues = [...currentValue];
for (const file of files) {
uppy.addFile(file);
const upload = await uppy.upload();
uppy.clear();
if (upload?.successful?.length) {
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
changeImage(index)({
target: {
name: 'image',
value: [...lastValues],
},
});
}
}
setUploading(false);
};
},
[changeImage]
);
const getInternalPlugs = useCallback(async () => {
return (
await fetch(`/integrations/${props.identifier}/internal-plugs`)
).json();
}, [props.identifier]);
const { data, isLoading } = useSWR(
`internal-${props.identifier}`,
getInternalPlugs,
{
revalidateOnReconnect: true,
}
);
// this is a trick to prevent the data from being deleted, yet we don't render the elements
if (!props.show || !showComponent || isLoading) {
return null;
}
return (
<FormProvider {...form}>
<SetTab changeTab={() => setShowTab(0)} />
{showLinkedinPopUp ? showLinkedinPopUp : null}
<div className="mt-[15px] w-full flex flex-col flex-1">
{!props.hideMenu && (
<div className="flex gap-[4px]">
<div className="flex-1 flex">
<Button
className="rounded-[4px] flex-1 overflow-hidden whitespace-nowrap"
secondary={showTab !== 0}
onClick={() => setShowTab(0)}
>
{t('preview', 'Preview')}
</Button>
</div>
{(!!SettingsComponent || !!data?.internalPlugs?.length) && (
<div className="flex-1 flex">
<Button
className={clsx(
'flex-1 overflow-hidden whitespace-nowrap',
showTab === 2 && 'rounded-[4px]'
)}
secondary={showTab !== 2}
onClick={() => setShowTab(2)}
>
{t('settings', 'Settings')}
</Button>
</div>
)}
{!existingData.integration && !props.hideEditOnlyThis && (
<div className="flex-1 flex">
<Button
className="text-white rounded-[4px] flex-1 !bg-red-700 overflow-hidden whitespace-nowrap"
secondary={showTab !== 1}
onClick={changeToEditor}
>
{editInPlace
? 'Edit globally'
: `Edit only ${integration?.name.slice(0, 10)}${
(integration?.name?.length || 0) > 10 ? '...' : ''
} (${capitalize(
integration?.identifier.replace('-', ' ')
)})`}
</Button>
</div>
)}
</div>
)}
{editInPlace &&
createPortal(
<EditorWrapper>
{uploading && (
<div className="absolute start-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
<LoadingComponent width={100} height={100} />
</div>
)}
<div className="flex flex-col gap-[20px]">
{!existingData?.integration && (
<div className="bg-red-800 text-white">
{t(
'you_are_now_editing_only',
'You are now editing only'
)}
{integration?.name} (
{capitalize(integration?.identifier.replace('-', ' '))})
</div>
)}
{InPlaceValue.map((val, index) => (
<Fragment key={`edit_inner_${index}`}>
<div>
<div className="flex gap-[4px]">
<div className="flex-1 text-textColor editor">
{(integration?.identifier === 'linkedin' ||
integration?.identifier === 'linkedin-page') && (
<Button
className="mb-[5px]"
onClick={tagPersonOrCompany(
integration.id,
(newValue: string) =>
changeValue(index)(val.content + newValue)
)}
>
{t('tag_a_company', 'Tag a company')}
</Button>
)}
<DropFiles
onDrop={pasteImages(index, val.image || [], true)}
>
<Editor
order={index}
height={InPlaceValue.length > 1 ? 200 : 250}
value={val.content}
totalPosts={InPlaceValue.length}
commands={[
// ...commands
// .getCommands()
// .filter((f) => f.name !== 'image'),
// newImage,
postSelector(date),
...linkedinCompany(
integration?.identifier!,
integration?.id!
),
]}
preview="edit"
onPaste={pasteImages(index, val.image || [])}
// @ts-ignore
onChange={changeValue(index)}
/>
</DropFiles>
{(!val.content || val.content.length < 6) && (
<div className="my-[5px] !bg-red-600 text-[12px] font-[500]">
{t(
'the_post_should_be_at_least_6_characters_long',
'The post should be at least 6 characters long'
)}
</div>
)}
<div className="flex">
<div className="flex-1">
<MultiMediaComponent
allData={InPlaceValue}
text={val.content}
label="Attachments"
description=""
name="image"
value={val.image}
onChange={changeImage(index)}
/>
</div>
<div className="flex bg-customColor20 rounded-br-[8px] text-customColor19">
{InPlaceValue.length > 1 && (
<div
className="flex cursor-pointer gap-[4px] justify-center items-center flex-1"
onClick={deletePost(index)}
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 14 14"
fill="currentColor"
>
<path
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
fill="#F97066"
/>
</svg>
</div>
<div className="text-[12px] font-[500] pe-[10px]">
{t('delete_post', 'Delete Post')}
</div>
</div>
)}
</div>
</div>
</div>
<div>
<UpDownArrow
isUp={index !== 0}
isDown={
InPlaceValue.length !== 0 &&
InPlaceValue.length !== index + 1
}
onChange={changePosition(index)}
/>
</div>
</div>
</div>
<div>
<AddPostButton onClick={addValue(index)} num={index} />
</div>
</Fragment>
))}
<div className="flex gap-[4px]">
{InPlaceValue.length > 1 && (
<div>
<MergePost merge={merge} />
</div>
)}
<div>
<SeparatePost
changeLoading={setUploading}
posts={InPlaceValue.map((p) => p.content)}
len={
typeof maximumCharacters === 'number'
? maximumCharacters
: 10000
}
merge={separatePosts}
/>
</div>
</div>
</div>
</EditorWrapper>,
document.querySelector('#renderEditor')!
)}
{(showTab === 0 || showTab === 2) && (
<div className={clsx('mt-[20px]', showTab !== 2 && 'hidden')}>
<Component values={editInPlace ? InPlaceValue : props.value} />
{!!data?.internalPlugs?.length && (
<InternalChannels plugs={data?.internalPlugs} />
)}
</div>
)}
{showTab === 0 && (
<div className="mt-[20px] flex flex-col items-center">
<IntegrationContext.Provider
value={{
allIntegrations,
date,
value: editInPlace ? InPlaceValue : props.value,
integration,
}}
>
{(editInPlace ? InPlaceValue : props.value)
.map((p) => p.content)
.join('').length ? (
CustomPreviewComponent ? (
<CustomPreviewComponent
maximumCharacters={
!maximumCharacters
? undefined
: typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(
JSON.parse(
integration?.additionalSettings || '[]'
)
)
}
/>
) : (
<GeneralPreviewComponent
maximumCharacters={
!maximumCharacters
? undefined
: typeof maximumCharacters === 'number'
? maximumCharacters
: maximumCharacters(
JSON.parse(
integration?.additionalSettings || '[]'
)
)
}
/>
)
) : (
<>{t('no_content_yet', 'No Content Yet')}</>
)}
</IntegrationContext.Provider>
</div>
)}
</div>
</FormProvider>
);
};
};

View File

@ -1,88 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { FC } from 'react';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
import { InstagramCollaboratorsTags } from '@gitroom/frontend/components/launches/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
);

View File

@ -1,100 +0,0 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { ReactTags } from 'react-tag-autocomplete';
import interClass from '@gitroom/react/helpers/inter.font';
import { useIntegration } from '@gitroom/frontend/components/launches/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>
);
};

View File

@ -1,82 +0,0 @@
import { FC, useCallback } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useFieldArray } from 'react-hook-form';
import { Button } from '@gitroom/react/form/button';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { Subreddit } from './subreddit';
import { LemmySettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/lemmy.dto';
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
);

View File

@ -1,160 +0,0 @@
import { FC, FormEvent, useCallback, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Input } from '@gitroom/react/form/input';
import { useDebouncedCallback } from 'use-debounce';
import { useWatch } from 'react-hook-form';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const Subreddit: FC<{
onChange: (event: {
target: {
name: string;
value: {
id: string;
subreddit: string;
title: string;
name: string;
url: string;
body: string;
media: any[];
};
};
}) => void;
name: string;
}> = (props) => {
const { onChange, name } = props;
const state = useSettings();
const split = name.split('.');
const [loading, setLoading] = useState(false);
// @ts-ignore
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
const [results, setResults] = useState([]);
const func = useCustomProviderFunction();
const value = useWatch({
name,
});
const [searchValue, setSearchValue] = useState('');
const setResult = (result: { id: string; name: string }) => async () => {
setLoading(true);
setSearchValue('');
onChange({
target: {
name,
value: {
id: String(result.id),
subreddit: result.name,
title: '',
name: '',
url: '',
body: '',
media: [],
},
},
});
setLoading(false);
};
const setTitle = useCallback(
(e: any) => {
onChange({
target: {
name,
value: {
...value,
title: e.target.value,
},
},
});
},
[value]
);
const setURL = useCallback(
(e: any) => {
onChange({
target: {
name,
value: {
...value,
url: e.target.value,
},
},
});
},
[value]
);
const search = useDebouncedCallback(
useCallback(async (e: FormEvent<HTMLInputElement>) => {
// @ts-ignore
setResults([]);
// @ts-ignore
if (!e.target.value) {
return;
}
// @ts-ignore
const results = await func.get('subreddits', { word: e.target.value });
// @ts-ignore
setResults(results);
}, []),
500
);
return (
<div className="bg-primary p-[20px]">
{value?.subreddit ? (
<>
<Input
error={errors?.subreddit?.message}
disableForm={true}
value={value.subreddit}
readOnly={true}
label="Community"
name="subreddit"
/>
<Input
error={errors?.title?.message}
value={value.title}
disableForm={true}
label="Title"
name="title"
onChange={setTitle}
/>
<Input
error={errors?.url?.message}
value={value.url}
label="URL"
name="url"
disableForm={true}
onChange={setURL}
/>
</>
) : (
<div className="relative">
<Input
placeholder="Community"
name="search"
label="Search Community"
readOnly={loading}
value={searchValue}
error={errors?.message}
disableForm={true}
onInput={async (e) => {
// @ts-ignore
setSearchValue(e.target.value);
await search(e);
}}
/>
{!!results.length && !loading && (
<div className="z-[400] w-full absolute bg-input -mt-[20px] outline-none border-fifth border cursor-pointer">
{results.map((r: { id: string; name: string }) => (
<div
onClick={setResult(r)}
key={r.id}
className="px-[16px] py-[5px] hover:bg-secondary"
>
{r.name}
</div>
))}
</div>
)}
</div>
)}
</div>
);
};

View File

@ -1,50 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/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/launches/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
);

View File

@ -1,3 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(null, undefined, undefined, undefined, 500);

View File

@ -1,37 +0,0 @@
/* 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;
}

View File

@ -1,89 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { MediumPublications } from '@gitroom/frontend/components/launches/providers/medium/medium.publications';
import { MediumTags } from '@gitroom/frontend/components/launches/providers/medium/medium.tags';
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import clsx from 'clsx';
import MDEditor from '@uiw/react-md-editor';
import localFont from 'next/font/local';
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);

View File

@ -1,56 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
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>
);
};

View File

@ -1,80 +0,0 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/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>
);
};

View File

@ -1,10 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async () => {
return true;
},
undefined
);

View File

@ -1,59 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
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>
);
};

View File

@ -1,74 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { PinterestBoard } from '@gitroom/frontend/components/launches/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
);

View File

@ -1,219 +0,0 @@
import { FC, useCallback } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useFormatting } from '@gitroom/frontend/components/launches/helpers/use.formatting';
import { Subreddit } from '@gitroom/frontend/components/launches/providers/reddit/subreddit';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useFieldArray, useWatch } from 'react-hook-form';
import { Button } from '@gitroom/react/form/button';
import {
RedditSettingsDto,
RedditSettingsValueDto,
} from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto';
import clsx from 'clsx';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
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
);

View File

@ -1,283 +0,0 @@
import { FC, FormEvent, useCallback, useMemo, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Input } from '@gitroom/react/form/input';
import { useDebouncedCallback } from 'use-debounce';
import { Button } from '@gitroom/react/form/button';
import clsx from 'clsx';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
import { useWatch } from 'react-hook-form';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Canonical } from '@gitroom/react/form/canonical';
import { useIntegration } from '@gitroom/frontend/components/launches/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>
);
};

View File

@ -1,159 +0,0 @@
import { FC } from 'react';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import DevtoProvider from '@gitroom/frontend/components/launches/providers/devto/devto.provider';
import XProvider from '@gitroom/frontend/components/launches/providers/x/x.provider';
import LinkedinProvider from '@gitroom/frontend/components/launches/providers/linkedin/linkedin.provider';
import RedditProvider from '@gitroom/frontend/components/launches/providers/reddit/reddit.provider';
import MediumProvider from '@gitroom/frontend/components/launches/providers/medium/medium.provider';
import HashnodeProvider from '@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider';
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider';
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.collaborators';
import YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider';
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
import PinterestProvider from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.provider';
import DribbbleProvider from '@gitroom/frontend/components/launches/providers/dribbble/dribbble.provider';
import ThreadsProvider from '@gitroom/frontend/components/launches/providers/threads/threads.provider';
import DiscordProvider from '@gitroom/frontend/components/launches/providers/discord/discord.provider';
import SlackProvider from '@gitroom/frontend/components/launches/providers/slack/slack.provider';
import MastodonProvider from '@gitroom/frontend/components/launches/providers/mastodon/mastodon.provider';
import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider';
import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy/lemmy.provider';
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
import NostrProvider from '@gitroom/frontend/components/launches/providers/nostr/nostr.provider';
import VkProvider from '@gitroom/frontend/components/launches/providers/vk/vk.provider';
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: FC<{
integrations: Integrations[];
hideEditOnlyThis: boolean;
value: Array<{
content: string;
id?: string;
}>;
selectedProvider?: Integrations;
}> = (props) => {
const { integrations, value, selectedProvider, hideEditOnlyThis } = props;
return (
<>
{integrations.map((integration) => {
const { component: ProviderComponent } = Providers.find(
(provider) => provider.identifier === integration.identifier
) || {
component: null,
};
if (
!ProviderComponent ||
integrations.map((p) => p.id).indexOf(selectedProvider?.id!) === -1
) {
return null;
}
return (
<ProviderComponent
hideEditOnlyThis={hideEditOnlyThis}
key={integration.id}
{...integration}
value={value}
show={selectedProvider?.id === integration.id}
/>
);
})}
</>
);
};

View File

@ -1,55 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
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>
);
};

View File

@ -1,20 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { FC } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { SlackChannelSelect } from '@gitroom/frontend/components/launches/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
);

View File

@ -1,10 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async () => {
return true;
},
4096
);

View File

@ -1,15 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/launches/finisher/thread.finisher';
const SettingsComponent = () => {
return <ThreadFinisher />;
};
export default withProvider(
SettingsComponent,
undefined,
undefined,
async () => {
return true;
},
500
);

View File

@ -1,370 +0,0 @@
import {
FC,
ReactEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Select } from '@gitroom/react/form/select';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/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
);

View File

@ -1,10 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async (posts) => {
return true;
},
2048
);

View File

@ -1,144 +0,0 @@
import { FC, FormEvent, useCallback, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Input } from '@gitroom/react/form/input';
import { useDebouncedCallback } from 'use-debounce';
import { useWatch } from 'react-hook-form';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const Subreddit: FC<{
onChange: (event: {
target: {
name: string;
value: {
id: string;
subreddit: string;
title: string;
name: string;
url: string;
body: string;
media: any[];
};
};
}) => void;
name: string;
}> = (props) => {
const { onChange, name } = props;
const state = useSettings();
const split = name.split('.');
const [loading, setLoading] = useState(false);
// @ts-ignore
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
const [results, setResults] = useState([]);
const func = useCustomProviderFunction();
const value = useWatch({
name,
});
const [searchValue, setSearchValue] = useState('');
const setResult = (result: { id: string; name: string }) => async () => {
setLoading(true);
setSearchValue('');
onChange({
target: {
name,
value: {
id: String(result.id),
subreddit: result.name,
title: '',
name: '',
url: '',
body: '',
media: [],
},
},
});
setLoading(false);
};
const setTitle = useCallback(
(e: any) => {
onChange({
target: {
name,
value: {
...value,
title: e.target.value,
},
},
});
},
[value]
);
const setURL = useCallback(
(e: any) => {
onChange({
target: {
name,
value: {
...value,
url: e.target.value,
},
},
});
},
[value]
);
const search = useDebouncedCallback(
useCallback(async (e: FormEvent<HTMLInputElement>) => {
// @ts-ignore
setResults([]);
// @ts-ignore
if (!e.target.value) {
return;
}
// @ts-ignore
const results = await func.get('subreddits', { word: e.target.value });
// @ts-ignore
setResults(results);
}, []),
500
);
return (
<div className="bg-primary p-[20px]">
{value?.subreddit ? (
<>
<Input
error={errors?.subreddit?.message}
disableForm={true}
value={value.subreddit}
readOnly={true}
label="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>
);
};

View File

@ -1,68 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { FC, useCallback } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/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
);

View File

@ -1,111 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { ThreadFinisher } from '@gitroom/frontend/components/launches/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/launches/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.'));
};
});
};

View File

@ -1,72 +0,0 @@
import { FC } from 'react';
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { Input } from '@gitroom/react/form/input';
import { MediumTags } from '@gitroom/frontend/components/launches/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
);

View File

@ -1,41 +0,0 @@
import React, { FC, ReactNode, useCallback } from 'react';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const Submitted: FC<{
children: ReactNode;
postId: string;
status: 'YES' | 'NO' | 'WAITING_CONFIRMATION';
updateOrder: () => void;
}> = (props) => {
const { postId, updateOrder, status, children } = props;
const fetch = useFetch();
const t = useT();
const cancel = useCallback(async () => {
if (
!(await deleteDialog(
'Are you sure you want to cancel this publication?',
'Yes'
))
) {
return;
}
await fetch(`/marketplace/posts/${postId}/cancel`, {
method: 'POST',
});
updateOrder();
}, [postId]);
if (!status || status === 'NO') {
return <>{children}</>;
}
return (
<Button
className="rounded-[4px] border-2 border-red-400 text-red-400"
secondary={true}
onClick={cancel}
>
{t('cancel_publication', 'Cancel publication')}
</Button>
);
};

View File

@ -1,126 +0,0 @@
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>
);
};

View File

@ -1,11 +1,11 @@
import React, { FC, useCallback, useMemo } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { continueProviderList } from '@gitroom/frontend/components/launches/providers/continue-provider/list';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import dayjs from 'dayjs';
import useSWR, { useSWRConfig } from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { continueProviderList } from '@gitroom/frontend/components/new-launch/providers/continue-provider/list';
export const Null: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -1,7 +1,7 @@
import 'reflect-metadata';
import { FC } from 'react';
import { Post as PrismaPost } from '.prisma/client';
import { Providers } from '@gitroom/frontend/components/launches/providers/show.all.providers';
import { Providers } from '@gitroom/frontend/components/new-launch/providers/show.all.providers';
export const PreviewPopupDynamic: FC<{
postId: string;
providerId: string;
@ -15,17 +15,5 @@ export const PreviewPopupDynamic: FC<{
const { component: ProviderComponent } = Providers.find(
(p) => p.identifier === props.providerId
)!;
return (
<ProviderComponent
hideMenu={true}
show={true}
identifier={props.post.integration}
// @ts-ignore
value={props.post.posts.map((p) => ({
id: p.id,
content: p.content,
image: p.image,
}))}
/>
);
return null;
};

View File

@ -1,99 +0,0 @@
'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>
);
};

View File

@ -3,11 +3,12 @@ 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 { FC, useEffect } from 'react';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { AddEditModalInnerInner } from '@gitroom/frontend/components/new-launch/add.edit.modal.inner';
import { ManageModal } from '@gitroom/frontend/components/new-launch/manage.modal';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import { useShallow } from 'zustand/react/shallow';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
export interface AddEditModalProps {
date: dayjs.Dayjs;
@ -30,14 +31,22 @@ export interface AddEditModalProps {
}
export const AddEditModal: FC<AddEditModalProps> = (props) => {
const setAllIntegrations = useLaunchStore(
(state) => state.setAllIntegrations
const { setAllIntegrations, setDate, setIsCreateSet } = useLaunchStore(
useShallow((state) => ({
setAllIntegrations: state.setAllIntegrations,
setDate: state.setDate,
setIsCreateSet: state.setIsCreateSet,
}))
);
const integrations = useLaunchStore((state) => state.integrations);
useEffect(() => {
setAllIntegrations(props.integrations || []);
setDate(props.date || dayjs());
setAllIntegrations(
(props.integrations || []).filter((f) => !f.inBetweenSteps && !f.disabled)
);
setIsCreateSet(!!props.addEditSets);
}, []);
if (!integrations.length) {
@ -49,48 +58,118 @@ export const AddEditModal: FC<AddEditModalProps> = (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);
const { addOrRemoveSelectedIntegration, selectedIntegrations, integrations } =
useLaunchStore(
useShallow((state) => ({
integrations: state.integrations,
selectedIntegrations: state.selectedIntegrations,
addOrRemoveSelectedIntegration: state.addOrRemoveSelectedIntegration,
}))
);
useEffect(() => {
if (props?.set?.posts?.length) {
for (const post of props?.set?.posts) {
if (post.integration) {
const integration = integrations.find(
(i) => i.id === post.integration.id
);
addOrRemoveSelectedIntegration(integration, post.settings);
}
}
}
if (existingData.integration) {
const integration = props.integrations.find(
const integration = 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,
if (existingData.integration && selectedIntegrations.length === 0) {
return null;
}
return <AddEditModalInnerInner {...props} />;
};
export const AddEditModalInnerInner: FC<AddEditModalProps> = (props) => {
const existingData = useExistingData();
const {
reset,
addGlobalValue,
addInternalValue,
global,
setCurrent,
internal,
setTags,
} = useLaunchStore(
useShallow((state) => ({
reset: state.reset,
addGlobalValue: state.addGlobalValue,
addInternalValue: state.addInternalValue,
setCurrent: state.setCurrent,
global: state.global,
internal: state.internal,
setTags: state.setTags,
}))
);
useEffect(() => {
if (existingData.integration) {
setTags(
// @ts-ignore
media: post.image as any[],
})));
}
else {
addGlobalValue(0, [{
content: '',
id: makeId(10),
media: [],
}]);
existingData?.posts?.[0]?.tags?.map((p: any) => ({
label: p.tag.name,
value: p.tag.name,
})) || []
);
addInternalValue(
0,
existingData.integration,
existingData.posts.map((post) => ({
content: post.content,
id: post.id,
// @ts-ignore
media: post.image as any[],
}))
);
setCurrent(existingData.integration);
}
addGlobalValue(
0,
props.onlyValues?.length
? props.onlyValues.map((p) => ({
content: p.content,
id: makeId(10),
media: p.image || [],
}))
: props.set?.posts?.length
? props.set.posts[0].value.map((p) => ({
id: makeId(10),
content: p.content,
// @ts-ignore
media: p.media,
}))
: [
{
content: '',
id: makeId(10),
media: [],
},
]
);
return () => {
reset();
};
}, []);
if (!selectedIntegrations.length && !global.length && !internal.length) {
if (!global.length && !internal.length) {
return null;
}
return <AddEditModalInnerInner {...props} />;
return <ManageModal {...props} />;
};

View File

@ -90,7 +90,7 @@ export const BoldText: FC<{
return (
<div
onClick={mark}
className="select-none cursor-pointer bg-customColor2 w-[40px] p-[5px] text-center"
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
>
<svg
width="25"

View File

@ -1,6 +1,13 @@
'use client';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
import React, {
FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { CopilotTextarea } from '@copilotkit/react-textarea';
import clsx from 'clsx';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
@ -15,6 +22,16 @@ 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';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import {
LinkedinCompany,
LinkedinCompanyPop,
ShowLinkedinCompany,
} from '@gitroom/frontend/components/launches/helpers/linkedin.component';
export const EditorWrapper: FC<{
totalPosts: number;
value: string;
@ -28,9 +45,20 @@ export const EditorWrapper: FC<{
current,
addInternalValue,
addGlobalValue,
setInternalValueMedia,
setGlobalValueMedia,
changeOrderGlobal,
changeOrderInternal,
isCreateSet,
deleteGlobalValue,
deleteInternalValue,
setGlobalValue,
setInternalValue,
internalFromAll,
} = useLaunchStore(
useShallow((state) => ({
internal: state.internal.find((p) => p.integration.id === state.current),
internalFromAll: state.integrations.find(p => p.id === state.current),
global: state.global,
current: state.current,
addRemoveInternal: state.addRemoveInternal,
@ -38,9 +66,29 @@ export const EditorWrapper: FC<{
setGlobalValueText: state.setGlobalValueText,
addInternalValue: state.addInternalValue,
addGlobalValue: state.addGlobalValue,
setGlobalValueMedia: state.setGlobalValueMedia,
setInternalValueMedia: state.setInternalValueMedia,
changeOrderGlobal: state.changeOrderGlobal,
changeOrderInternal: state.changeOrderInternal,
isCreateSet: state.isCreateSet,
deleteGlobalValue: state.deleteGlobalValue,
deleteInternalValue: state.deleteInternalValue,
setGlobalValue: state.setGlobalValue,
setInternalValue: state.setInternalValue,
}))
);
const existingData = useExistingData();
const [loaded, setLoaded] = useState(true);
useEffect(() => {
if (loaded) {
return;
}
setLoaded(true);
}, [loaded]);
const canEdit = useMemo(() => {
return current === 'global' || !!internal;
}, [current, internal]);
@ -53,6 +101,46 @@ export const EditorWrapper: FC<{
return global;
}, [current, internal, global]);
const setValue = useCallback(
(value: string[]) => {
const newValue = value.map((p, index) => {
return {
id: makeId(10),
...(items?.[index]?.media
? { media: items[index].media }
: { media: [] }),
content: p,
};
});
if (internal) {
return setInternalValue(current, newValue);
}
return setGlobalValue(newValue);
},
[internal, items]
);
useCopilotReadable({
description: 'Current content of posts',
value: items.map((p) => p.content),
});
useCopilotAction({
name: 'setPosts',
description: 'a thread of posts',
parameters: [
{
name: 'content',
type: 'string[]',
description: 'a thread of posts',
},
],
handler: async ({ content }) => {
setValue(content);
},
});
const changeValue = useCallback(
(index: number) => (value: string) => {
if (internal) {
@ -64,6 +152,41 @@ export const EditorWrapper: FC<{
[current, global, internal]
);
const changeImages = useCallback(
(index: number) => (value: any[]) => {
if (internal) {
return setInternalValueMedia(current, index, value);
}
return setGlobalValueMedia(index, value);
},
[current, global, internal]
);
const changeOrder = useCallback(
(index: number) => (direction: 'up' | 'down') => {
if (internal) {
changeOrderInternal(current, index, direction);
return setLoaded(false);
}
changeOrderGlobal(index, direction);
setLoaded(false);
},
[changeOrderInternal, changeOrderGlobal, current, global, internal]
);
const goBackToGlobal = useCallback(async () => {
if (
await deleteDialog(
'This action is irreversible. Are you sure you want to go back to global mode?'
)
) {
setLoaded(false);
addRemoveInternal(current);
}
}, []);
const addValue = useCallback(
(index: number) => () => {
if (internal) {
@ -87,25 +210,115 @@ export const EditorWrapper: FC<{
[current, global, internal]
);
const deletePost = useCallback(
(index: number) => async () => {
if (
!(await deleteDialog(
'Are you sure you want to delete this post?',
'Yes, delete it!'
))
) {
return;
}
if (internal) {
deleteInternalValue(current, index);
return setLoaded(false);
}
deleteGlobalValue(index);
setLoaded(false);
},
[current, global, internal]
);
if (!loaded) {
return null;
}
return items.map((g, index) => (
<div key={g.id} className="relative flex flex-col gap-[10px]">
{!canEdit && (
{!canEdit && !isCreateSet && (
<div
onClick={() => addRemoveInternal(current)}
onClick={() => {
if (index !== 0) {
return;
}
setLoaded(false);
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>
{index === 0 && (
<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 className="flex gap-[5px]">
<div className="flex-1">
<Editor
allValues={items}
onChange={changeValue(index)}
key={index}
num={index}
totalPosts={global.length}
value={g.content}
pictures={g.media}
setImages={changeImages(index)}
autoComplete={canEdit}
validateChars={true}
identifier={internalFromAll?.identifier || 'global'}
/>
</div>
<div className="flex flex-col items-center gap-[10px]">
<UpDownArrow
isUp={index !== 0}
isDown={index !== items.length - 1}
onChange={changeOrder(index)}
/>
{index === 0 &&
current !== 'global' &&
canEdit &&
!existingData.integration && (
<svg
onClick={goBackToGlobal}
className="cursor-pointer"
data-tooltip-id="tooltip"
data-tooltip-content="Go back to global mode"
width="20"
height="20"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 3C13.4288 3 10.9154 3.76244 8.77759 5.1909C6.63975 6.61935 4.97351 8.64968 3.98957 11.0251C3.00563 13.4006 2.74819 16.0144 3.2498 18.5362C3.75141 21.0579 4.98953 23.3743 6.80762 25.1924C8.6257 27.0105 10.9421 28.2486 13.4638 28.7502C15.9856 29.2518 18.5995 28.9944 20.9749 28.0104C23.3503 27.0265 25.3807 25.3603 26.8091 23.2224C28.2376 21.0846 29 18.5712 29 16C28.9964 12.5533 27.6256 9.24882 25.1884 6.81163C22.7512 4.37445 19.4467 3.00364 16 3ZM12.7038 21H19.2963C18.625 23.2925 17.5 25.3587 16 26.9862C14.5 25.3587 13.375 23.2925 12.7038 21ZM12.25 19C11.9183 17.0138 11.9183 14.9862 12.25 13H19.75C20.0817 14.9862 20.0817 17.0138 19.75 19H12.25ZM5.00001 16C4.99914 14.9855 5.13923 13.9759 5.41626 13H10.2238C9.92542 14.9889 9.92542 17.0111 10.2238 19H5.41626C5.13923 18.0241 4.99914 17.0145 5.00001 16ZM19.2963 11H12.7038C13.375 8.7075 14.5 6.64125 16 5.01375C17.5 6.64125 18.625 8.7075 19.2963 11ZM21.7763 13H26.5838C27.1388 14.9615 27.1388 17.0385 26.5838 19H21.7763C22.0746 17.0111 22.0746 14.9889 21.7763 13ZM25.7963 11H21.3675C20.8572 8.99189 20.0001 7.0883 18.835 5.375C20.3236 5.77503 21.7119 6.48215 22.9108 7.45091C24.1097 8.41967 25.0926 9.62861 25.7963 11ZM13.165 5.375C11.9999 7.0883 11.1428 8.99189 10.6325 11H6.20376C6.90741 9.62861 7.89029 8.41967 9.08918 7.45091C10.2881 6.48215 11.6764 5.77503 13.165 5.375ZM6.20376 21H10.6325C11.1428 23.0081 11.9999 24.9117 13.165 26.625C11.6764 26.225 10.2881 25.5178 9.08918 24.5491C7.89029 23.5803 6.90741 22.3714 6.20376 21ZM18.835 26.625C20.0001 24.9117 20.8572 23.0081 21.3675 21H25.7963C25.0926 22.3714 24.1097 23.5803 22.9108 24.5491C21.7119 25.5178 20.3236 26.225 18.835 26.625Z"
fill="#ef4444"
/>
</svg>
)}
{items.length > 1 && (
<svg
onClick={deletePost(index)}
className="cursor-pointer"
data-tooltip-id="tooltip"
data-tooltip-content="Delete Post"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 14 14"
fill="currentColor"
>
<path
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
fill="#ef4444"
/>
</svg>
)}
</div>
</div>
{canEdit && <AddPostButton num={index} onClick={addValue(index)} />}
@ -116,8 +329,24 @@ export const EditorWrapper: FC<{
export const Editor: FC<{
totalPosts: number;
value: string;
num?: number;
pictures?: any[];
allValues?: any[];
onChange: (value: string) => void;
setImages?: (value: any[]) => void;
autoComplete?: boolean;
validateChars?: boolean;
identifier?: string;
}> = (props) => {
const {
allValues,
pictures,
setImages,
num,
autoComplete,
validateChars,
identifier,
} = props;
const user = useUser();
const [id] = useState(makeId(10));
const newRef = useRef<any>(null);
@ -136,6 +365,38 @@ export const Editor: FC<{
return (
<>
<div className="relative bg-customColor2" id={id}>
<div className="flex gap-[5px] bg-[#1b263b] border-b border-t border-customColor3 justify-center items-center p-[5px]">
<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 w-[40px] p-[5px] text-center"
onClick={() => setEmojiPickerOpen(!emojiPickerOpen)}
>
{'\uD83D\uDE00'}
</div>
{identifier === 'linkedin' || identifier === 'linkedin-page' ? (
<LinkedinCompanyPop addText={addText} />
) : null}
<div className="relative">
<div className="absolute z-[200] top-[35px] -start-[50px]">
<EmojiPicker
theme={(localStorage.getItem('mode') as Theme) || Theme.DARK}
onEmojiClick={(e) => {
addText(e.emoji);
setEmojiPickerOpen(false);
}}
open={emojiPickerOpen}
/>
</div>
</div>
</div>
<CopilotTextarea
disableBranding={true}
ref={newRef}
@ -148,37 +409,43 @@ export const Editor: FC<{
props?.onChange?.(e.target.value);
}}
// onPaste={props.onPaste}
placeholder={t('write_your_reply', 'Write your reply...')}
placeholder={t('write_your_reply', 'Write your post...')}
autosuggestionsConfig={{
textareaPurpose: `Assist me in writing social media posts.`,
chatApiConfigs: {},
disabled: !user?.tier?.ai,
chatApiConfigs: {
suggestionsApiConfig: {
maxTokens: 20,
stop: ['.', '?', '!'],
},
},
disabled: user?.tier?.ai ? !autoComplete : true,
}}
/>
{validateChars && props.value.length < 6 && (
<div className="px-3 text-sm bg-red-600 text-red-300 mb-[4px]">
{t(
'the_post_should_be_at_least_6_characters_long',
'The post should be at least 6 characters long'
)}
</div>
)}
</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 className="flex gap-[5px] bg-customColor2">
{setImages && (
<MultiMediaComponent
allData={allValues}
text={props.value}
label={t('attachments', 'Attachments')}
description=""
value={props.pictures}
name="image"
onChange={(value) => {
setImages(value.target.value);
}}
onOpen={() => {}}
onClose={() => {}}
/>
)}
</div>
</>
);

View File

@ -3,9 +3,9 @@
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';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const ThreadFinisher = () => {
const integration = useIntegration();
@ -50,10 +50,7 @@ export const ThreadFinisher = () => {
<Editor
onChange={(val) => setValue('thread_finisher', val)}
value={value}
height={150}
totalPosts={1}
order={1}
preview="edit"
/>
</div>
</div>

View File

@ -1,102 +0,0 @@
'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>
);
};

View File

@ -1,10 +0,0 @@
'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>;
};

View File

@ -1,6 +0,0 @@
'use client';
export const isUSCitizen = () => {
const userLanguage = navigator.language || navigator.languages[0];
return userLanguage.startsWith('en-US');
};

View File

@ -1,189 +0,0 @@
'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: '',
});
},
},
];
};

View File

@ -1,91 +0,0 @@
'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: '![image](',
suffix: ')',
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: '![',
suffix: `](${
media.path.indexOf('http') === -1
? `${backendUrl}/${uploadDirectory}`
: ``
}${media.path})`,
});
return;
}
executeCommand({
api,
selectedText: state1.selectedText,
selection: state.selection,
prefix: '![image',
suffix: `](${
media.path.indexOf('http') === -1
? `${backendUrl}/${uploadDirectory}`
: ``
}${media.path})`,
});
}
});
},
};

View File

@ -1,68 +0,0 @@
'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>
);
};

View File

@ -1,29 +0,0 @@
'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,
};
};

View File

@ -1,21 +0,0 @@
'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);

View File

@ -1,31 +0,0 @@
'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');
},
};
};

View File

@ -1,49 +0,0 @@
'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]);
};

View File

@ -1,31 +0,0 @@
'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');
},
};
};

View File

@ -1,27 +0,0 @@
'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};

View File

@ -1,47 +0,0 @@
'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);
};

View File

@ -1,96 +0,0 @@
'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];
});
};

View File

@ -0,0 +1,445 @@
'use client';
import React, { FC, useCallback, useRef, useState } from 'react';
import { AddEditModalProps } from '@gitroom/frontend/components/new-launch/add.edit.modal';
import clsx from 'clsx';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { PicksSocialsComponent } from '@gitroom/frontend/components/new-launch/picks.socials.component';
import { 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';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { DatePicker } from '@gitroom/frontend/components/launches/helpers/date.picker';
import { useShallow } from 'zustand/react/shallow';
import { RepeatComponent } from '@gitroom/frontend/components/launches/repeat.component';
import { TagsComponent } from '@gitroom/frontend/components/launches/tags.component';
import { Button } from '@gitroom/react/form/button';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { weightedLength } from '@gitroom/helpers/utils/count.length';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { useModals } from '@mantine/modals';
import { capitalize } from 'lodash';
import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload';
// @ts-ignore
import useKeypress from 'react-use-keypress';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
import { CopilotPopup } from '@copilotkit/react-ui';
function countCharacters(text: string, type: string): number {
if (type !== 'x') {
return text.length;
}
return weightedLength(text);
}
export const ManageModal: FC<AddEditModalProps> = (props) => {
const t = useT();
const fetch = useFetch();
const ref = useRef(null);
const existingData = useExistingData();
const [loading, setLoading] = useState(false);
const toaster = useToaster();
const modal = useModals();
usePreventWindowUnload(true);
const { addEditSets, mutate, customClose } = props;
const {
selectedIntegrations,
hide,
date,
setDate,
repeater,
setRepeater,
tags,
setTags,
integrations,
setSelectedIntegrations,
} = useLaunchStore(
useShallow((state) => ({
hide: state.hide,
date: state.date,
setDate: state.setDate,
repeater: state.repeater,
setRepeater: state.setRepeater,
tags: state.tags,
setTags: state.setTags,
selectedIntegrations: state.selectedIntegrations,
integrations: state.integrations,
setSelectedIntegrations: state.setSelectedIntegrations,
}))
);
const deletePost = useCallback(async () => {
if (
!(await deleteDialog(
'Are you sure you want to delete this post?',
'Yes, delete it!'
))
) {
setLoading(false);
return;
}
await fetch(`/posts/${existingData.group}`, {
method: 'DELETE',
});
mutate();
modal.closeAll();
return;
}, [existingData, mutate, modal]);
const askClose = useCallback(async () => {
if (
await deleteDialog(
t(
'are_you_sure_you_want_to_close_this_modal_all_data_will_be_lost',
'Are you sure you want to close this modal? (all data will be lost)'
),
t('yes_close_it', 'Yes, close it!')
)
) {
if (customClose) {
customClose();
return;
}
modal.closeAll();
}
}, []);
const changeCustomer = useCallback(
(customer: string) => {
const neededIntegrations = integrations.filter(
(p) => p?.customer?.id === customer
);
setSelectedIntegrations(
neededIntegrations.map((p) => ({
settings: {},
selectedIntegrations: p,
}))
);
},
[integrations]
);
useKeypress('Escape', askClose);
const schedule = useCallback(
(type: 'draft' | 'now' | 'schedule') => async () => {
const checkAllValid = await ref.current.checkAllValid();
if (type !== 'draft') {
const notEnoughChars = checkAllValid.filter((p: any) => {
return p.values.some((a: any) => {
return (
countCharacters(a.content, p?.integration?.identifier || '') < 6
);
});
});
for (const item of notEnoughChars) {
console.log('no enough');
toaster.show(
'' +
item.integration.name +
' post is too short, it must be at least 6 characters',
'warning'
);
item.preview();
return;
}
for (const item of checkAllValid) {
if (item.valid === false) {
toaster.show('Some fields are not valid', 'warning');
item.fix();
return;
}
if (item.errors !== true) {
toaster.show(
`${capitalize(item.integration.identifier.split('-')[0])} (${
item.integration.name
}): ${item.errors}`,
'warning'
);
item.preview();
return;
}
}
const sliceNeeded = checkAllValid.filter((p: any) => {
return p.values.some((a: any) => {
return (
countCharacters(a.content, p?.integration?.identifier || '') >
(p.maximumCharacters || 1000000)
);
});
});
for (const item of sliceNeeded) {
if (
!(await deleteDialog(
`${item?.integration?.name} (${item?.integration?.identifier}) post is too long, it will be cropped, do you want to continue?`,
'Yes, continue'
))
) {
item.preview();
return;
}
}
}
const shortLinkUrl = await (
await fetch('/posts/should-shortlink', {
method: 'POST',
body: JSON.stringify({
messages: checkAllValid.flatMap((p: any) =>
p.values.flatMap((a: any) => a.content)
),
}),
})
).json();
const shortLink = !shortLinkUrl.ask
? false
: await deleteDialog(
'Do you want to shortlink the URLs? it will let you get statistics over clicks',
'Yes, shortlink it!'
);
const group = existingData.group || makeId(10);
const data = {
type,
inter: repeater || undefined,
tags,
shortLink,
date: date.utc().format('YYYY-MM-DDTHH:mm:ss'),
posts: checkAllValid.map((p: any) => ({
integration: p.integration,
group,
settings: p.settings,
value: p.values.map((a: any) => ({
...a,
image: a.media || [],
content: a.content.slice(0, p.maximumCharacters || 1000000),
})),
})),
};
addEditSets
? addEditSets(data)
: await fetch('/posts', {
method: 'POST',
body: JSON.stringify(data),
});
if (!addEditSets) {
mutate();
toaster.show(
!existingData.integration
? 'Added successfully'
: 'Updated successfully'
);
}
if (customClose) {
setTimeout(() => {
customClose();
}, 2000);
}
if (!addEditSets) {
modal.closeAll();
}
},
[ref, repeater, tags, date, addEditSets]
);
return (
<>
<div
className={clsx(
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
)}
>
<div
className={clsx(
'flex flex-1 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={
existingData.integration
? t('update_post', 'Update Existing Post')
: t('create_new_post', 'Create Post')
}
>
<div className="flex items-center">
<RepeatComponent repeat={repeater} onChange={setRepeater} />
<DatePicker onChange={setDate} date={date} />
</div>
</TopTitle>
<PicksSocialsComponent toolTip={true} />
<div>
{!existingData.integration && <SelectCurrent />}
<div className="flex gap-[4px]">
<div className="flex-1 editor text-textColor gap-[10px] flex-col flex">
{!hide && <EditorWrapper totalPosts={1} value="" />}
</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">
{!!existingData.integration && (
<Button
onClick={deletePost}
className="rounded-[4px] border-2 border-red-400 text-red-400"
secondary={true}
>
{t('delete_post', 'Delete Post')}
</Button>
)}
{!addEditSets && (
<Button
onClick={schedule('draft')}
className="rounded-[4px] border-2 border-customColor21"
secondary={true}
disabled={selectedIntegrations.length === 0}
>
{t('save_as_draft', 'Save as draft')}
</Button>
)}
{addEditSets && (
<Button
className="rounded-[4px] relative group"
disabled={selectedIntegrations.length === 0 || loading}
onClick={schedule('draft')}
>
Save Set
</Button>
)}
{!addEditSets && (
<Button
className="rounded-[4px] relative group"
disabled={selectedIntegrations.length === 0 || loading}
onClick={schedule('schedule')}
>
<div className="flex justify-center items-center gap-[5px] h-full">
<div className="h-full flex items-center text-white">
{selectedIntegrations.length === 0
? t(
'select_channels_from_circles',
'Select channels from the circles above'
)
: !existingData?.integration
? t('add_to_calendar', 'Add to calendar')
: t('update', 'Update')}
</div>
<div className="h-full flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M15.0233 7.14804L9.39828 12.773C9.34604 12.8253 9.284 12.8668 9.21572 12.8951C9.14743 12.9234 9.07423 12.938 9.00031 12.938C8.92639 12.938 8.8532 12.9234 8.78491 12.8951C8.71662 12.8668 8.65458 12.8253 8.60234 12.773L2.97734 7.14804C2.8718 7.04249 2.8125 6.89934 2.8125 6.75007C2.8125 6.6008 2.8718 6.45765 2.97734 6.3521C3.08289 6.24655 3.22605 6.18726 3.37531 6.18726C3.52458 6.18726 3.66773 6.24655 3.77328 6.3521L9.00031 11.5798L14.2273 6.3521C14.2796 6.29984 14.3417 6.25838 14.4099 6.2301C14.4782 6.20181 14.5514 6.18726 14.6253 6.18726C14.6992 6.18726 14.7724 6.20181 14.8407 6.2301C14.909 6.25838 14.971 6.29984 15.0233 6.3521C15.0755 6.40436 15.117 6.46641 15.1453 6.53469C15.1736 6.60297 15.1881 6.67616 15.1881 6.75007C15.1881 6.82398 15.1736 6.89716 15.1453 6.96545C15.117 7.03373 15.0755 7.09578 15.0233 7.14804Z"
fill="white"
/>
</svg>
<div
onClick={schedule('now')}
className={clsx(
'hidden group-hover:flex hover:flex flex-col justify-center absolute start-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder',
loading &&
'cursor-not-allowed pointer-events-none opacity-50'
)}
>
{t('post_now', 'Post now')}
</div>
</div>
</div>
</Button>
)}
</div>
</div>
</div>
</div>
</div>
<div
className={clsx(
'flex-grow w-[650px] max-w-[650px] min-w-[650px] 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}>
<div className="flex flex-1 gap-[10px]">
<div>
<TagsComponent
name="tags"
label={t('tags', 'Tags')}
initial={tags}
onChange={(e) => setTags(e.target.value)}
/>
</div>
<SelectCustomer
onChange={changeCustomer}
integrations={integrations}
/>
</div>
<svg
onClick={askClose}
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>
<CopilotPopup
hitEscapeToClose={false}
clickOutsideToClose={true}
instructions={`
You are an assistant that help the user to schedule their social media posts,
Here are the things you can do:
- Add a new comment / post to the list of posts
- Delete a comment / post from the list of posts
- Add content to the comment / post
- Activate or deactivate the comment / post
Post content can be added using the addPostContentFor{num} function.
After using the addPostFor{num} it will create a new addPostContentFor{num+ 1} function.
`}
labels={{
title: 'Your Assistant',
initial: 'Hi! 👋 How can I assist you today?',
}}
/>
</>
);
};

View File

@ -5,38 +5,46 @@ import clsx from 'clsx';
import Image from 'next/image';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useShallow } from 'zustand/react/shallow';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
export const PicksSocialsComponent: FC<{ toolTip?: boolean }> = ({
toolTip,
}) => {
const { addOrRemoveSelectedIntegration, integrations, selectedIntegrations } =
const exising = useExistingData();
const { locked, addOrRemoveSelectedIntegration, integrations, selectedIntegrations } =
useLaunchStore(
useShallow((state) => ({
integrations: state.integrations,
selectedIntegrations: state.selectedIntegrations,
addOrRemoveSelectedIntegration: state.addOrRemoveSelectedIntegration,
locked: state.locked,
}))
);
return (
<div className={clsx('flex')}>
<div className={clsx('flex', locked && 'opacity-50 pointer-events-none')}>
<div className="flex">
<div className="innerComponent">
<div className="flex">
<div className="grid grid-cols-13 gap-[10px]">
{integrations
.filter((f) => !f.inBetweenSteps)
.map((integration) => (
<div
key={integration.id}
className="flex gap-[8px] items-center me-[10px]"
className="flex gap-[8px] items-center"
{...(toolTip && {
'data-tooltip-id': 'tooltip',
'data-tooltip-content': integration.name,
})}
>
<div
onClick={() =>
addOrRemoveSelectedIntegration(integration, {})
}
onClick={() => {
if (exising.integration) {
return;
}
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(

View File

@ -2,10 +2,8 @@
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 />;
};

View File

@ -1,13 +1,13 @@
'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';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
export const FacebookContinue: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -1,13 +1,13 @@
'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';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
export const InstagramContinue: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -1,13 +1,13 @@
'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';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
export const LinkedinContinue: FC<{
closeModal: () => void;
existingId: string[];

View File

@ -3,7 +3,6 @@
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';
@ -12,8 +11,10 @@ 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';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const font = localFont({
src: [
{

View File

@ -1,10 +1,10 @@
'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';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const DevtoTags: FC<{
name: string;
label: string;

View File

@ -1,10 +1,10 @@
'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';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
export const SelectOrganization: FC<{
name: string;
onChange: (event: {

View File

@ -1,9 +1,9 @@
'use client';
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/new-launch/helpers/use.custom.provider.function';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/new-launch/helpers/use.values';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const DiscordChannelSelect: FC<{
name: string;

View File

@ -4,7 +4,7 @@ import { withProvider } from '@gitroom/frontend/components/new-launch/providers/
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';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const DiscordComponent: FC = () => {
const form = useSettings();
return (

View File

@ -2,7 +2,7 @@
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 { useSettings } from '@gitroom/frontend/components/launches/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';

Some files were not shown because too many files have changed in this diff Show More