From ca671654e555d2188edf01ae40e71afe10cc69e6 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 27 Jun 2025 18:05:21 +0700 Subject: [PATCH] feat: paste and drag and drop image, with loader --- .../src/components/media/new.uploader.tsx | 7 + .../src/components/new-launch/editor.tsx | 134 ++++++++++++++---- .../src/components/new-launch/store.ts | 35 +++++ 3 files changed, 150 insertions(+), 26 deletions(-) diff --git a/apps/frontend/src/components/media/new.uploader.tsx b/apps/frontend/src/components/media/new.uploader.tsx index ecd50de8..6db45aa5 100644 --- a/apps/frontend/src/components/media/new.uploader.tsx +++ b/apps/frontend/src/components/media/new.uploader.tsx @@ -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) => { diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 3f7b6169..1c5ad6e6 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -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)} />
@@ -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(null); const [emojiPickerOpen, setEmojiPickerOpen] = useState(false); + const [isUploading, setIsUploading] = useState(false); const t = useT(); + const uppy = useUppyUploader({ + onUploadSuccess: (result: UploadResult) => { + 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 | 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 ( - <> +
+
+ Drop your files here to upload +
- 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: ['.', '?', '!'], +
+
+ +
+ 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, + }} + /> +
{validateChars && props.value.length < 6 && (
{t( @@ -461,6 +543,6 @@ export const Editor: FC<{ {props?.value?.length}/{props.totalChars}
)} - +
); }; diff --git a/apps/frontend/src/components/new-launch/store.ts b/apps/frontend/src/components/new-launch/store.ts index 4a6e8d54..8a5660c8 100644 --- a/apps/frontend/src/components/new-launch/store.ts +++ b/apps/frontend/src/components/new-launch/store.ts @@ -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()((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 + ), + })), }));