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
+ ),
+ })),
}));