postiz/apps/frontend/src/components/new-launch/editor.tsx

467 lines
16 KiB
TypeScript

'use client';
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';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { Transforms } from 'slate';
import EmojiPicker from 'emoji-picker-react';
import { Theme } from 'emoji-picker-react';
import { BoldText } from '@gitroom/frontend/components/new-launch/bold.text';
import { UText } from '@gitroom/frontend/components/new-launch/u.text';
import { SignatureBox } from '@gitroom/frontend/components/signature';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useShallow } from 'zustand/react/shallow';
import { AddPostButton } from '@gitroom/frontend/components/new-launch/add.post.button';
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;
}> = (props) => {
const {
setGlobalValueText,
setInternalValueText,
addRemoveInternal,
internal,
global,
current,
addInternalValue,
addGlobalValue,
setInternalValueMedia,
setGlobalValueMedia,
changeOrderGlobal,
changeOrderInternal,
isCreateSet,
deleteGlobalValue,
deleteInternalValue,
setGlobalValue,
setInternalValue,
internalFromAll,
totalChars,
} = 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,
setInternalValueText: state.setInternalValueText,
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,
totalChars: state.totalChars,
}))
);
const existingData = useExistingData();
const [loaded, setLoaded] = useState(true);
useEffect(() => {
if (loaded) {
return;
}
setLoaded(true);
}, [loaded]);
const canEdit = useMemo(() => {
return current === 'global' || !!internal;
}, [current, internal]);
const items = useMemo(() => {
if (internal) {
return internal.integrationValue;
}
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) {
return setInternalValueText(current, index, value);
}
return setGlobalValueText(index, value);
},
[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);
}
}, [addRemoveInternal, current]);
const addValue = useCallback(
(index: number) => () => {
if (internal) {
return addInternalValue(index, current, [
{
content: '',
id: makeId(10),
media: [],
},
]);
}
return addGlobalValue(index, [
{
content: '',
id: makeId(10),
media: [],
},
]);
},
[current, global, internal]
);
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 && !isCreateSet && (
<div
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]"
>
{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 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'}
totalChars={totalChars}
/>
</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)} />}
</div>
));
};
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;
totalChars?: number;
}> = (props) => {
const {
allValues,
pictures,
setImages,
num,
autoComplete,
validateChars,
identifier,
} = props;
const user = useUser();
const [id] = useState(makeId(10));
const newRef = useRef<any>(null);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
const t = useT();
const addText = useCallback(
(emoji: string) => {
setTimeout(() => {
// @ts-ignore
Transforms.insertText(newRef?.current?.editor!, emoji);
}, 10);
},
[props.value, id]
);
return (
<>
<div className="relative bg-customColor2" id={id}>
<div className="flex gap-[5px] bg-customColor55 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}
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 post...')}
autosuggestionsConfig={{
textareaPurpose: `Assist me in writing social media posts.`,
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-white 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] 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>
{(props?.totalChars || 0) > 0 && (
<div
className={clsx(
'text-end text-sm mt-1',
props.value.length > props.totalChars && '!text-red-500'
)}
>
{props.value.length}/{props.totalChars}
</div>
)}
</>
);
};