feat: paste and drag and drop image, with loader
This commit is contained in:
parent
d12ff06a9b
commit
ca671654e5
|
|
@ -13,6 +13,7 @@ import { useVariables } from '@gitroom/react/helpers/variable.context';
|
|||
import Compressor from '@uppy/compressor';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
|
||||
|
||||
export function MultipartFileUploader({
|
||||
onUploadSuccess,
|
||||
|
|
@ -59,6 +60,7 @@ export function useUppyUploader(props: {
|
|||
onUploadSuccess: (result: UploadResult) => void;
|
||||
allowedFileTypes: string;
|
||||
}) {
|
||||
const setLocked = useLaunchStore(state => state.setLocked);
|
||||
const toast = useToaster();
|
||||
const { storageProvider, backendUrl, disableImageCompression } =
|
||||
useVariables();
|
||||
|
|
@ -135,12 +137,17 @@ export function useUppyUploader(props: {
|
|||
}
|
||||
// Set additional metadata when a file is added
|
||||
uppy2.on('file-added', (file) => {
|
||||
setLocked(true);
|
||||
uppy2.setFileMeta(file.id, {
|
||||
useCloudflare: storageProvider === 'cloudflare' ? 'true' : 'false', // Example of adding a custom field
|
||||
// Add more fields as needed
|
||||
});
|
||||
});
|
||||
uppy2.on('error', (result) => {
|
||||
setLocked(false);
|
||||
})
|
||||
uppy2.on('complete', (result) => {
|
||||
setLocked(false);
|
||||
onUploadSuccess(result);
|
||||
});
|
||||
uppy2.on('upload-success', (file, response) => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import React, {
|
|||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
ClipboardEvent,
|
||||
} from 'react';
|
||||
import { CopilotTextarea } from '@copilotkit/react-textarea';
|
||||
import clsx from 'clsx';
|
||||
|
|
@ -32,6 +33,10 @@ import {
|
|||
LinkedinCompanyPop,
|
||||
ShowLinkedinCompany,
|
||||
} from '@gitroom/frontend/components/launches/helpers/linkedin.component';
|
||||
import { DropEvent, FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
|
||||
import { UploadResult } from '@uppy/core';
|
||||
import { ProgressBar } from '@uppy/react';
|
||||
export const EditorWrapper: FC<{
|
||||
totalPosts: number;
|
||||
value: string;
|
||||
|
|
@ -46,6 +51,8 @@ export const EditorWrapper: FC<{
|
|||
addInternalValue,
|
||||
addGlobalValue,
|
||||
setInternalValueMedia,
|
||||
appendInternalValueMedia,
|
||||
appendGlobalValueMedia,
|
||||
setGlobalValueMedia,
|
||||
changeOrderGlobal,
|
||||
changeOrderInternal,
|
||||
|
|
@ -77,6 +84,8 @@ export const EditorWrapper: FC<{
|
|||
setGlobalValue: state.setGlobalValue,
|
||||
setInternalValue: state.setInternalValue,
|
||||
totalChars: state.totalChars,
|
||||
appendInternalValueMedia: state.appendInternalValueMedia,
|
||||
appendGlobalValueMedia: state.appendGlobalValueMedia,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
@ -101,7 +110,7 @@ export const EditorWrapper: FC<{
|
|||
}
|
||||
|
||||
return global;
|
||||
}, [current, internal, global]);
|
||||
}, [internal, global]);
|
||||
|
||||
const setValue = useCallback(
|
||||
(value: string[]) => {
|
||||
|
|
@ -165,6 +174,17 @@ export const EditorWrapper: FC<{
|
|||
[current, global, internal]
|
||||
);
|
||||
|
||||
const appendImages = useCallback(
|
||||
(index: number) => (value: any[]) => {
|
||||
if (internal) {
|
||||
return appendInternalValueMedia(current, index, value);
|
||||
}
|
||||
|
||||
return appendGlobalValueMedia(index, value);
|
||||
},
|
||||
[current, global, internal]
|
||||
);
|
||||
|
||||
const changeOrder = useCallback(
|
||||
(index: number) => (direction: 'up' | 'down') => {
|
||||
if (internal) {
|
||||
|
|
@ -274,6 +294,7 @@ export const EditorWrapper: FC<{
|
|||
validateChars={true}
|
||||
identifier={internalFromAll?.identifier || 'global'}
|
||||
totalChars={totalChars}
|
||||
appendImages={appendImages(index)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-[10px]">
|
||||
|
|
@ -337,6 +358,7 @@ export const Editor: FC<{
|
|||
allValues?: any[];
|
||||
onChange: (value: string) => void;
|
||||
setImages?: (value: any[]) => void;
|
||||
appendImages?: (value: any[]) => void;
|
||||
autoComplete?: boolean;
|
||||
validateChars?: boolean;
|
||||
identifier?: string;
|
||||
|
|
@ -350,13 +372,60 @@ export const Editor: FC<{
|
|||
autoComplete,
|
||||
validateChars,
|
||||
identifier,
|
||||
appendImages,
|
||||
} = props;
|
||||
const user = useUser();
|
||||
const [id] = useState(makeId(10));
|
||||
const newRef = useRef<any>(null);
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const t = useT();
|
||||
|
||||
const uppy = useUppyUploader({
|
||||
onUploadSuccess: (result: UploadResult<any, any>) => {
|
||||
appendImages([
|
||||
...result.successful.map((p) => ({
|
||||
id: p.response.body.saved.id,
|
||||
path: p.response.body.saved.path,
|
||||
})),
|
||||
]);
|
||||
uppy.clear();
|
||||
},
|
||||
allowedFileTypes: 'image/*,video/mp4',
|
||||
});
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
for (const file of acceptedFiles) {
|
||||
uppy.addFile(file);
|
||||
}
|
||||
},
|
||||
[uppy]
|
||||
);
|
||||
|
||||
const paste = useCallback(
|
||||
async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
|
||||
// @ts-ignore
|
||||
const clipboardItems = event.clipboardData?.items;
|
||||
if (!clipboardItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
for (const item of clipboardItems) {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
uppy.addFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[uppy]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||
|
||||
const addText = useCallback(
|
||||
(emoji: string) => {
|
||||
setTimeout(() => {
|
||||
|
|
@ -367,8 +436,16 @@ export const Editor: FC<{
|
|||
[props.value, id]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div {...getRootProps()}>
|
||||
<div className="relative bg-customColor2" id={id}>
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute left-0 top-0 w-full h-full bg-black/70 z-[300] transition-all items-center justify-center flex text-white text-sm',
|
||||
!isDragActive ? 'pointer-events-none opacity-0' : 'opacity-100'
|
||||
)}
|
||||
>
|
||||
Drop your files here to upload
|
||||
</div>
|
||||
<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
|
||||
|
|
@ -401,30 +478,35 @@ export const Editor: FC<{
|
|||
</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: ['.', '?', '!'],
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute end-0 bottom-0 z-[200]">
|
||||
<ProgressBar id={`prog-${num}`} uppy={uppy} />
|
||||
</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={paste}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
disabled: user?.tier?.ai ? !autoComplete : true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{validateChars && props.value.length < 6 && (
|
||||
<div className="px-3 text-sm bg-red-600 !text-white mb-[4px]">
|
||||
{t(
|
||||
|
|
@ -461,6 +543,6 @@ export const Editor: FC<{
|
|||
{props?.value?.length}/{props.totalChars}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -102,6 +102,15 @@ interface StoreState {
|
|||
setTags: (tags: { label: string; value: string }[]) => void;
|
||||
setIsCreateSet: (isCreateSet: boolean) => void;
|
||||
setTotalChars?: (totalChars: number) => void;
|
||||
appendInternalValueMedia: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
media: { id: string; path: string }[]
|
||||
) => void;
|
||||
appendGlobalValueMedia: (
|
||||
index: number,
|
||||
media: { id: string; path: string }[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -452,4 +461,30 @@ export const useLaunchStore = create<StoreState>()((set) => ({
|
|||
set((state) => ({
|
||||
totalChars,
|
||||
})),
|
||||
appendInternalValueMedia: (
|
||||
integrationId: string,
|
||||
index: number,
|
||||
media: { id: string; path: string }[]
|
||||
) =>
|
||||
set((state) => ({
|
||||
internal: state.internal.map((item) =>
|
||||
item.integration.id === integrationId
|
||||
? {
|
||||
...item,
|
||||
integrationValue: item.integrationValue.map((v, i) =>
|
||||
i === index ? { ...v, media: [...v.media, ...media] } : v
|
||||
),
|
||||
}
|
||||
: item
|
||||
),
|
||||
})),
|
||||
appendGlobalValueMedia: (
|
||||
index: number,
|
||||
media: { id: string; path: string }[]
|
||||
) =>
|
||||
set((state) => ({
|
||||
global: state.global.map((item, i) =>
|
||||
i === index ? { ...item, media: [...item.media, ...media] } : item
|
||||
),
|
||||
})),
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in New Issue