Skip to content

Commit 637c5f9

Browse files
committed
feat: drag and drop pictures
1 parent da63609 commit 637c5f9

File tree

11 files changed

+558
-806
lines changed

11 files changed

+558
-806
lines changed

apps/backend/src/api/routes/media.controller.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ export class MediaController {
7676
const name = upload.Location.split('/').pop();
7777

7878
// @ts-ignore
79-
await this._mediaService.saveFile(org.id, name, upload.Location);
79+
const saveFile = await this._mediaService.saveFile(org.id, name, upload.Location);
8080

81-
res.status(200).json(upload);
81+
res.status(200).json({...upload, saved: saveFile});
8282
// const filePath =
8383
// file.path.indexOf('http') === 0
8484
// ? file.path

apps/frontend/src/components/launches/add.edit.model.tsx

+113-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
'use client';
22

33
import React, {
4-
FC, Fragment, MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState
4+
ClipboardEventHandler,
5+
FC,
6+
Fragment,
7+
MouseEventHandler,
8+
useCallback,
9+
useEffect,
10+
useMemo,
11+
useRef,
12+
ClipboardEvent,
13+
useState,
514
} from 'react';
615
import dayjs from 'dayjs';
716
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
@@ -47,6 +56,9 @@ import { weightedLength } from '@gitroom/helpers/utils/count.length';
4756
import { uniqBy } from 'lodash';
4857
import { Select } from '@gitroom/react/form/select';
4958
import { useClickOutside } from '@gitroom/frontend/components/layout/click.outside';
59+
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
60+
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
61+
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
5062

5163
function countCharacters(text: string, type: string): number {
5264
if (type !== 'x') {
@@ -69,6 +81,8 @@ export const AddEditModal: FC<{
6981
}> = (props) => {
7082
const { date, integrations: ints, reopenModal, mutate, onlyValues } = props;
7183
const [customer, setCustomer] = useState('');
84+
const [loading, setLoading] = useState(false);
85+
const [uploading, setUploading] = useState(false);
7286

7387
// selected integrations to allow edit
7488
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback<
@@ -265,12 +279,14 @@ export const AddEditModal: FC<{
265279
const schedule = useCallback(
266280
(type: 'draft' | 'now' | 'schedule' | 'delete') => async () => {
267281
if (type === 'delete') {
282+
setLoading(true);
268283
if (
269284
!(await deleteDialog(
270285
'Are you sure you want to delete this post?',
271286
'Yes, delete it!'
272287
))
273288
) {
289+
setLoading(false);
274290
return;
275291
}
276292
await fetch(`/posts/${existingData.group}`, {
@@ -341,6 +357,7 @@ export const AddEditModal: FC<{
341357
}
342358
}
343359

360+
setLoading(true);
344361
await fetch('/posts', {
345362
method: 'POST',
346363
body: JSON.stringify({
@@ -377,6 +394,68 @@ export const AddEditModal: FC<{
377394
]
378395
);
379396

397+
const uppy = useUppyUploader({
398+
onUploadSuccess: () => {
399+
/**empty**/
400+
},
401+
allowedFileTypes: 'image/*,video/mp4',
402+
});
403+
404+
const pasteImages = useCallback(
405+
(index: number, currentValue: any[], isFile?: boolean) => {
406+
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
407+
// @ts-ignore
408+
const clipboardItems = isFile
409+
? // @ts-ignore
410+
event.map((p) => ({ kind: 'file', getAsFile: () => p }))
411+
: // @ts-ignore
412+
event.clipboardData?.items; // Ensure clipboardData is available
413+
if (!clipboardItems) {
414+
return;
415+
}
416+
417+
const files: File[] = [];
418+
419+
// @ts-ignore
420+
for (const item of clipboardItems) {
421+
console.log(item);
422+
if (item.kind === 'file') {
423+
const file = item.getAsFile();
424+
if (file) {
425+
const isImage = file.type.startsWith('image/');
426+
const isVideo = file.type.startsWith('video/');
427+
if (isImage || isVideo) {
428+
files.push(file); // Collect images or videos
429+
}
430+
}
431+
}
432+
}
433+
if (files.length === 0) {
434+
return;
435+
}
436+
437+
setUploading(true);
438+
const lastValues = [...currentValue];
439+
for (const file of files) {
440+
uppy.addFile(file);
441+
const upload = await uppy.upload();
442+
uppy.clear();
443+
if (upload?.successful?.length) {
444+
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
445+
changeImage(index)({
446+
target: {
447+
name: 'image',
448+
value: [...lastValues],
449+
},
450+
});
451+
}
452+
}
453+
setUploading(false);
454+
};
455+
},
456+
[changeImage]
457+
);
458+
380459
const getPostsMarketplace = useCallback(async () => {
381460
return (
382461
await fetch(`/posts/marketplace/${existingData?.posts?.[0]?.id}`)
@@ -427,6 +506,11 @@ export const AddEditModal: FC<{
427506
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
428507
)}
429508
>
509+
{uploading && (
510+
<div className="absolute left-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
511+
<LoadingComponent width={100} height={100} />
512+
</div>
513+
)}
430514
<div
431515
className={clsx(
432516
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
@@ -534,23 +618,28 @@ export const AddEditModal: FC<{
534618
<div>
535619
<div className="flex gap-[4px]">
536620
<div className="flex-1 editor text-textColor">
537-
<Editor
538-
order={index}
539-
height={value.length > 1 ? 150 : 250}
540-
commands={
541-
[
542-
// ...commands
543-
// .getCommands()
544-
// .filter((f) => f.name === 'image'),
545-
// newImage,
546-
// postSelector(dateState),
547-
]
548-
}
549-
value={p.content}
550-
preview="edit"
551-
// @ts-ignore
552-
onChange={changeValue(index)}
553-
/>
621+
<DropFiles
622+
onDrop={pasteImages(index, p.image || [], true)}
623+
>
624+
<Editor
625+
order={index}
626+
height={value.length > 1 ? 150 : 250}
627+
commands={
628+
[
629+
// ...commands
630+
// .getCommands()
631+
// .filter((f) => f.name === 'image'),
632+
// newImage,
633+
// postSelector(dateState),
634+
]
635+
}
636+
value={p.content}
637+
preview="edit"
638+
onPaste={pasteImages(index, p.image || [])}
639+
// @ts-ignore
640+
onChange={changeValue(index)}
641+
/>
642+
</DropFiles>
554643

555644
{showError &&
556645
(!p.content || p.content.length < 6) && (
@@ -649,6 +738,7 @@ export const AddEditModal: FC<{
649738
className="rounded-[4px] relative group"
650739
disabled={
651740
selectedIntegrations.length === 0 ||
741+
loading ||
652742
!canSendForPublication
653743
}
654744
>
@@ -678,7 +768,11 @@ export const AddEditModal: FC<{
678768
</svg>
679769
<div
680770
onClick={postNow}
681-
className="hidden group-hover:flex hover:flex flex-col justify-center absolute left-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder"
771+
className={clsx(
772+
'hidden group-hover:flex hover:flex flex-col justify-center absolute left-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder',
773+
loading &&
774+
'cursor-not-allowed pointer-events-none opacity-50'
775+
)}
682776
>
683777
Post now
684778
</div>

apps/frontend/src/components/launches/editor.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const Editor = forwardRef<
4949
)}
5050
value={props.value}
5151
onChange={(e) => props?.onChange?.(e.target.value)}
52+
onPaste={props.onPaste}
5253
placeholder="Write your reply..."
5354
autosuggestionsConfig={{
5455
textareaPurpose: `Assist me in writing social media posts.`,

apps/frontend/src/components/launches/providers/high.order.provider.tsx

+97-22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React, {
88
useEffect,
99
useMemo,
1010
useState,
11+
ClipboardEvent,
1112
} from 'react';
1213
import { Button } from '@gitroom/react/form/button';
1314
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
@@ -38,6 +39,9 @@ import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.bu
3839
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
3940
import { capitalize } from 'lodash';
4041
import { useModals } from '@mantine/modals';
42+
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
43+
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
44+
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
4145

4246
// Simple component to change back to settings on after changing tab
4347
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
@@ -96,6 +100,7 @@ export const withProvider = function <T extends object>(
96100
const existingData = useExistingData();
97101
const { integration, date } = useIntegration();
98102
const [showLinkedinPopUp, setShowLinkedinPopUp] = useState<any>(false);
103+
const [uploading, setUploading] = useState(false);
99104

100105
useCopilotReadable({
101106
description:
@@ -276,6 +281,68 @@ export const withProvider = function <T extends object>(
276281
[]
277282
);
278283

284+
const uppy = useUppyUploader({
285+
onUploadSuccess: () => {
286+
/**empty**/
287+
},
288+
allowedFileTypes: 'image/*,video/mp4',
289+
});
290+
291+
const pasteImages = useCallback(
292+
(index: number, currentValue: any[], isFile?: boolean) => {
293+
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
294+
// @ts-ignore
295+
const clipboardItems = isFile
296+
? // @ts-ignore
297+
event.map((p) => ({ kind: 'file', getAsFile: () => p }))
298+
: // @ts-ignore
299+
event.clipboardData?.items; // Ensure clipboardData is available
300+
if (!clipboardItems) {
301+
return;
302+
}
303+
304+
const files: File[] = [];
305+
306+
// @ts-ignore
307+
for (const item of clipboardItems) {
308+
console.log(item);
309+
if (item.kind === 'file') {
310+
const file = item.getAsFile();
311+
if (file) {
312+
const isImage = file.type.startsWith('image/');
313+
const isVideo = file.type.startsWith('video/');
314+
if (isImage || isVideo) {
315+
files.push(file); // Collect images or videos
316+
}
317+
}
318+
}
319+
}
320+
if (files.length === 0) {
321+
return;
322+
}
323+
324+
setUploading(true);
325+
const lastValues = [...currentValue];
326+
for (const file of files) {
327+
uppy.addFile(file);
328+
const upload = await uppy.upload();
329+
uppy.clear();
330+
if (upload?.successful?.length) {
331+
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
332+
changeImage(index)({
333+
target: {
334+
name: 'image',
335+
value: [...lastValues],
336+
},
337+
});
338+
}
339+
}
340+
setUploading(false);
341+
};
342+
},
343+
[changeImage]
344+
);
345+
279346
// this is a trick to prevent the data from being deleted, yet we don't render the elements
280347
if (!props.show) {
281348
return null;
@@ -329,6 +396,11 @@ export const withProvider = function <T extends object>(
329396
{editInPlace &&
330397
createPortal(
331398
<EditorWrapper>
399+
{uploading && (
400+
<div className="absolute left-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
401+
<LoadingComponent width={100} height={100} />
402+
</div>
403+
)}
332404
<div className="flex flex-col gap-[20px]">
333405
{!existingData?.integration && (
334406
<div className="bg-red-800 text-white">
@@ -347,33 +419,36 @@ export const withProvider = function <T extends object>(
347419
onClick={tagPersonOrCompany(
348420
integration.id,
349421
(newValue: string) =>
350-
changeValue(index)(
351-
val.content + newValue
352-
)
422+
changeValue(index)(val.content + newValue)
353423
)}
354424
>
355425
Tag a company
356426
</Button>
357427
)}
358-
<Editor
359-
order={index}
360-
height={InPlaceValue.length > 1 ? 200 : 250}
361-
value={val.content}
362-
commands={[
363-
// ...commands
364-
// .getCommands()
365-
// .filter((f) => f.name !== 'image'),
366-
// newImage,
367-
postSelector(date),
368-
...linkedinCompany(
369-
integration?.identifier!,
370-
integration?.id!
371-
),
372-
]}
373-
preview="edit"
374-
// @ts-ignore
375-
onChange={changeValue(index)}
376-
/>
428+
<DropFiles
429+
onDrop={pasteImages(index, val.image || [], true)}
430+
>
431+
<Editor
432+
order={index}
433+
height={InPlaceValue.length > 1 ? 200 : 250}
434+
value={val.content}
435+
commands={[
436+
// ...commands
437+
// .getCommands()
438+
// .filter((f) => f.name !== 'image'),
439+
// newImage,
440+
postSelector(date),
441+
...linkedinCompany(
442+
integration?.identifier!,
443+
integration?.id!
444+
),
445+
]}
446+
preview="edit"
447+
onPaste={pasteImages(index, val.image || [])}
448+
// @ts-ignore
449+
onChange={changeValue(index)}
450+
/>
451+
</DropFiles>
377452
{(!val.content || val.content.length < 6) && (
378453
<div className="my-[5px] text-customColor19 text-[12px] font-[500]">
379454
The post should be at least 6 characters long

0 commit comments

Comments
 (0)