feat: drag and drop pictures

This commit is contained in:
Nevo David 2024-12-27 13:47:15 +07:00
parent da63609101
commit 637c5f9cd5
11 changed files with 558 additions and 806 deletions

View File

@ -76,9 +76,9 @@ export class MediaController {
const name = upload.Location.split('/').pop();
// @ts-ignore
await this._mediaService.saveFile(org.id, name, upload.Location);
const saveFile = await this._mediaService.saveFile(org.id, name, upload.Location);
res.status(200).json(upload);
res.status(200).json({...upload, saved: saveFile});
// const filePath =
// file.path.indexOf('http') === 0
// ? file.path

View File

@ -1,7 +1,16 @@
'use client';
import React, {
FC, Fragment, MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState
ClipboardEventHandler,
FC,
Fragment,
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useRef,
ClipboardEvent,
useState,
} from 'react';
import dayjs from 'dayjs';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
@ -47,6 +56,9 @@ import { weightedLength } from '@gitroom/helpers/utils/count.length';
import { uniqBy } from 'lodash';
import { Select } from '@gitroom/react/form/select';
import { useClickOutside } from '@gitroom/frontend/components/layout/click.outside';
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
function countCharacters(text: string, type: string): number {
if (type !== 'x') {
@ -69,6 +81,8 @@ export const AddEditModal: FC<{
}> = (props) => {
const { date, integrations: ints, reopenModal, mutate, onlyValues } = props;
const [customer, setCustomer] = useState('');
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
// selected integrations to allow edit
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback<
@ -265,12 +279,14 @@ export const AddEditModal: FC<{
const schedule = useCallback(
(type: 'draft' | 'now' | 'schedule' | 'delete') => async () => {
if (type === 'delete') {
setLoading(true);
if (
!(await deleteDialog(
'Are you sure you want to delete this post?',
'Yes, delete it!'
))
) {
setLoading(false);
return;
}
await fetch(`/posts/${existingData.group}`, {
@ -341,6 +357,7 @@ export const AddEditModal: FC<{
}
}
setLoading(true);
await fetch('/posts', {
method: 'POST',
body: JSON.stringify({
@ -377,6 +394,68 @@ export const AddEditModal: FC<{
]
);
const uppy = useUppyUploader({
onUploadSuccess: () => {
/**empty**/
},
allowedFileTypes: 'image/*,video/mp4',
});
const pasteImages = useCallback(
(index: number, currentValue: any[], isFile?: boolean) => {
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
// @ts-ignore
const clipboardItems = isFile
? // @ts-ignore
event.map((p) => ({ kind: 'file', getAsFile: () => p }))
: // @ts-ignore
event.clipboardData?.items; // Ensure clipboardData is available
if (!clipboardItems) {
return;
}
const files: File[] = [];
// @ts-ignore
for (const item of clipboardItems) {
console.log(item);
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
if (isImage || isVideo) {
files.push(file); // Collect images or videos
}
}
}
}
if (files.length === 0) {
return;
}
setUploading(true);
const lastValues = [...currentValue];
for (const file of files) {
uppy.addFile(file);
const upload = await uppy.upload();
uppy.clear();
if (upload?.successful?.length) {
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
changeImage(index)({
target: {
name: 'image',
value: [...lastValues],
},
});
}
}
setUploading(false);
};
},
[changeImage]
);
const getPostsMarketplace = useCallback(async () => {
return (
await fetch(`/posts/marketplace/${existingData?.posts?.[0]?.id}`)
@ -427,6 +506,11 @@ export const AddEditModal: FC<{
'flex flex-col md:flex-row p-[10px] rounded-[4px] bg-primary gap-[20px]'
)}
>
{uploading && (
<div className="absolute left-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
<LoadingComponent width={100} height={100} />
</div>
)}
<div
className={clsx(
'flex flex-col gap-[16px] transition-all duration-700 whitespace-nowrap',
@ -534,23 +618,28 @@ export const AddEditModal: FC<{
<div>
<div className="flex gap-[4px]">
<div className="flex-1 editor text-textColor">
<Editor
order={index}
height={value.length > 1 ? 150 : 250}
commands={
[
// ...commands
// .getCommands()
// .filter((f) => f.name === 'image'),
// newImage,
// postSelector(dateState),
]
}
value={p.content}
preview="edit"
// @ts-ignore
onChange={changeValue(index)}
/>
<DropFiles
onDrop={pasteImages(index, p.image || [], true)}
>
<Editor
order={index}
height={value.length > 1 ? 150 : 250}
commands={
[
// ...commands
// .getCommands()
// .filter((f) => f.name === 'image'),
// newImage,
// postSelector(dateState),
]
}
value={p.content}
preview="edit"
onPaste={pasteImages(index, p.image || [])}
// @ts-ignore
onChange={changeValue(index)}
/>
</DropFiles>
{showError &&
(!p.content || p.content.length < 6) && (
@ -649,6 +738,7 @@ export const AddEditModal: FC<{
className="rounded-[4px] relative group"
disabled={
selectedIntegrations.length === 0 ||
loading ||
!canSendForPublication
}
>
@ -678,7 +768,11 @@ export const AddEditModal: FC<{
</svg>
<div
onClick={postNow}
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"
className={clsx(
'hidden group-hover:flex hover:flex flex-col justify-center absolute left-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder',
loading &&
'cursor-not-allowed pointer-events-none opacity-50'
)}
>
Post now
</div>

View File

@ -49,6 +49,7 @@ export const Editor = forwardRef<
)}
value={props.value}
onChange={(e) => props?.onChange?.(e.target.value)}
onPaste={props.onPaste}
placeholder="Write your reply..."
autosuggestionsConfig={{
textareaPurpose: `Assist me in writing social media posts.`,

View File

@ -8,6 +8,7 @@ import React, {
useEffect,
useMemo,
useState,
ClipboardEvent,
} from 'react';
import { Button } from '@gitroom/react/form/button';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
@ -38,6 +39,9 @@ import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.bu
import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component';
import { capitalize } from 'lodash';
import { useModals } from '@mantine/modals';
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
// Simple component to change back to settings on after changing tab
export const SetTab: FC<{ changeTab: () => void }> = (props) => {
@ -96,6 +100,7 @@ export const withProvider = function <T extends object>(
const existingData = useExistingData();
const { integration, date } = useIntegration();
const [showLinkedinPopUp, setShowLinkedinPopUp] = useState<any>(false);
const [uploading, setUploading] = useState(false);
useCopilotReadable({
description:
@ -276,6 +281,68 @@ export const withProvider = function <T extends object>(
[]
);
const uppy = useUppyUploader({
onUploadSuccess: () => {
/**empty**/
},
allowedFileTypes: 'image/*,video/mp4',
});
const pasteImages = useCallback(
(index: number, currentValue: any[], isFile?: boolean) => {
return async (event: ClipboardEvent<HTMLDivElement> | File[]) => {
// @ts-ignore
const clipboardItems = isFile
? // @ts-ignore
event.map((p) => ({ kind: 'file', getAsFile: () => p }))
: // @ts-ignore
event.clipboardData?.items; // Ensure clipboardData is available
if (!clipboardItems) {
return;
}
const files: File[] = [];
// @ts-ignore
for (const item of clipboardItems) {
console.log(item);
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
if (isImage || isVideo) {
files.push(file); // Collect images or videos
}
}
}
}
if (files.length === 0) {
return;
}
setUploading(true);
const lastValues = [...currentValue];
for (const file of files) {
uppy.addFile(file);
const upload = await uppy.upload();
uppy.clear();
if (upload?.successful?.length) {
lastValues.push(upload?.successful[0]?.response?.body?.saved!);
changeImage(index)({
target: {
name: 'image',
value: [...lastValues],
},
});
}
}
setUploading(false);
};
},
[changeImage]
);
// this is a trick to prevent the data from being deleted, yet we don't render the elements
if (!props.show) {
return null;
@ -329,6 +396,11 @@ export const withProvider = function <T extends object>(
{editInPlace &&
createPortal(
<EditorWrapper>
{uploading && (
<div className="absolute left-0 top-0 w-full h-full bg-black/40 z-[600] flex justify-center items-center">
<LoadingComponent width={100} height={100} />
</div>
)}
<div className="flex flex-col gap-[20px]">
{!existingData?.integration && (
<div className="bg-red-800 text-white">
@ -347,33 +419,36 @@ export const withProvider = function <T extends object>(
onClick={tagPersonOrCompany(
integration.id,
(newValue: string) =>
changeValue(index)(
val.content + newValue
)
changeValue(index)(val.content + newValue)
)}
>
Tag a company
</Button>
)}
<Editor
order={index}
height={InPlaceValue.length > 1 ? 200 : 250}
value={val.content}
commands={[
// ...commands
// .getCommands()
// .filter((f) => f.name !== 'image'),
// newImage,
postSelector(date),
...linkedinCompany(
integration?.identifier!,
integration?.id!
),
]}
preview="edit"
// @ts-ignore
onChange={changeValue(index)}
/>
<DropFiles
onDrop={pasteImages(index, val.image || [], true)}
>
<Editor
order={index}
height={InPlaceValue.length > 1 ? 200 : 250}
value={val.content}
commands={[
// ...commands
// .getCommands()
// .filter((f) => f.name !== 'image'),
// newImage,
postSelector(date),
...linkedinCompany(
integration?.identifier!,
integration?.id!
),
]}
preview="edit"
onPaste={pasteImages(index, val.image || [])}
// @ts-ignore
onChange={changeValue(index)}
/>
</DropFiles>
{(!val.content || val.content.length < 6) && (
<div className="my-[5px] text-customColor19 text-[12px] font-[500]">
The post should be at least 6 characters long

View File

@ -0,0 +1,19 @@
import { useDropzone } from 'react-dropzone';
import { FC, ReactNode } from 'react';
export const DropFiles: FC<{ children: ReactNode, onDrop: (files: File[]) => void }> = (props) => {
const { getRootProps, isDragActive } = useDropzone({
onDrop: props.onDrop
});
return (
<div {...getRootProps()} className="relative">
{isDragActive && (
<div className="absolute left-0 top-0 w-full h-full bg-black/90 flex items-center justify-center z-[200] animate-normalFadeIn">
Drag n drop some files here
</div>
)}
{props.children}
</div>
);
};

View File

@ -1,7 +1,11 @@
'use client';
import ReactLoading from 'react-loading';
import { FC } from 'react';
export const LoadingComponent = () => {
return <div className="flex-1 flex justify-center pt-[100px]"><ReactLoading type="spin" color="#fff" width={100} height={100} /></div>;
export const LoadingComponent: FC<{width?: number, height?: number}> = (props) => {
return (
<div className="flex-1 flex justify-center pt-[100px]">
<ReactLoading type="spin" color="#fff" width={props.width || 100} height={props.height || 100} />
</div>
);
};

View File

@ -83,6 +83,11 @@ export const MediaBox: FC<{
const { data, mutate } = useSWR('get-media', loadMedia);
const finishUpload = useCallback(async () => {
const newData = await mutate();
setNewMedia(newData.results[0])();
}, [mutate, setNewMedia]);
useEffect(() => {
if (data?.pages) {
setPages(data.pages);
@ -127,7 +132,7 @@ export const MediaBox: FC<{
>
<div className="relative flex gap-2 items-center justify-center">
<MultipartFileUploader
onUploadSuccess={mutate}
onUploadSuccess={finishUpload}
allowedFileTypes={
type === 'video'
? 'video/mp4'
@ -152,7 +157,7 @@ export const MediaBox: FC<{
<div>Click the button below to upload one</div>
<div className="mt-[10px] justify-center items-center flex flex-col-reverse gap-[10px]">
<MultipartFileUploader
onUploadSuccess={mutate}
onUploadSuccess={finishUpload}
allowedFileTypes={
type === 'video'
? 'video/mp4'
@ -185,7 +190,7 @@ export const MediaBox: FC<{
<img
className="w-full h-full object-cover"
src={mediaDirectory.set(media.path)}
alt='media'
alt="media"
/>
)}
</div>
@ -215,11 +220,12 @@ export const MultiMediaComponent: FC<{
}> = (props) => {
const { name, label, error, description, onChange, value } = props;
const user = useUser();
useEffect(() => {
if (value) {
setCurrentMedia(value);
}
}, []);
}, [value]);
const [modal, setShowModal] = useState(false);
const [mediaModal, setMediaModal] = useState(false);
@ -261,7 +267,7 @@ export const MultiMediaComponent: FC<{
<>
<div className="flex flex-col gap-[8px] bg-input rounded-bl-[8px]">
{modal && <MediaBox setMedia={changeMedia} closeModal={showModal} />}
{mediaModal && !!user?.tier?.ai && (
{mediaModal && !!user?.tier?.ai && (
<Polonto setMedia={changeMedia} closeModal={closeDesignModal} />
)}
<div className="flex gap-[10px]">
@ -285,7 +291,9 @@ export const MultiMediaComponent: FC<{
/>
</svg>
</div>
<div className="text-[12px] font-[500] text-primary">Insert Media</div>
<div className="text-[12px] font-[500] text-primary">
Insert Media
</div>
</Button>
<Button
@ -306,7 +314,9 @@ export const MultiMediaComponent: FC<{
/>
</svg>
</div>
<div className="text-[12px] font-[500] !text-white">Design Media</div>
<div className="text-[12px] font-[500] !text-white">
Design Media
</div>
</Button>
</div>
@ -354,7 +364,8 @@ export const MediaComponent: FC<{
width?: number;
height?: number;
}> = (props) => {
const { name, type, label, description, onChange, value, width, height } = props;
const { name, type, label, description, onChange, value, width, height } =
props;
const { getValues } = useSettings();
const user = useUser();
useEffect(() => {

View File

@ -55,18 +55,16 @@ export function MultipartFileUploader({
);
}
export function MultipartFileUploaderAfter({
onUploadSuccess,
allowedFileTypes,
}: {
export function useUppyUploader(props: {
// @ts-ignore
onUploadSuccess: (result: UploadResult) => void;
allowedFileTypes: string;
}) {
const { storageProvider, backendUrl } = useVariables();
const { onUploadSuccess, allowedFileTypes } = props;
const fetch = useFetch();
const uppy = useMemo(() => {
return useMemo(() => {
const uppy2 = new Uppy({
autoProceed: true,
restrictions: {
@ -86,7 +84,7 @@ export function MultipartFileUploaderAfter({
convertTypes: ['image/jpeg'],
maxWidth: 1000,
maxHeight: 1000,
quality: 1
quality: 1,
});
// Set additional metadata when a file is added
uppy2.on('file-added', (file) => {
@ -102,9 +100,9 @@ export function MultipartFileUploaderAfter({
uppy2.on('upload-success', (file, response) => {
// @ts-ignore
uppy.setFileState(file.id, {
uppy2.setFileState(file.id, {
// @ts-ignore
progress: uppy.getState().files[file.id].progress,
progress: uppy2.getState().files[file.id].progress,
// @ts-ignore
uploadURL: response.body.Location,
response: response,
@ -114,6 +112,17 @@ export function MultipartFileUploaderAfter({
return uppy2;
}, []);
}
export function MultipartFileUploaderAfter({
onUploadSuccess,
allowedFileTypes,
}: {
// @ts-ignore
onUploadSuccess: (result: UploadResult) => void;
allowedFileTypes: string;
}) {
const uppy = useUppyUploader({ onUploadSuccess, allowedFileTypes });
return (
<>

View File

@ -98,6 +98,7 @@ module.exports = {
},
animation: {
fade: 'fadeOut 0.5s ease-in-out',
normalFadeIn: 'normalFadeIn 0.5s ease-in-out',
normalFadeOut: 'normalFadeOut 0.5s linear 5s forwards',
overflow: 'overFlow 0.5s ease-in-out forwards',
overflowReverse: 'overFlowReverse 0.5s ease-in-out forwards',
@ -120,6 +121,10 @@ module.exports = {
'0%': { opacity: 1 },
'100%': { opacity: 0 },
},
normalFadeIn: {
'0%': { opacity: 0 },
'100%': { opacity: 1 },
},
overFlow: {
'0%': { overflow: 'hidden' },
'99%': { overflow: 'hidden' },
@ -131,7 +136,7 @@ module.exports = {
'100%': { overflow: 'hidden' },
},
fadeDown: {
'0%': { opacity: 0, marginTop: -30},
'0%': { opacity: 0, marginTop: -30 },
'10%': { opacity: 1, marginTop: 0 },
'85%': { opacity: 1, marginTop: 0 },
'90%': { opacity: 1, marginTop: 10 },
@ -144,12 +149,15 @@ module.exports = {
newMessages: {
'0%': { backgroundColor: 'var(--color-seventh)', fontWeight: 'bold' },
'99%': { backgroundColor: 'var(--color-third)', fontWeight: 'bold' },
'100%': { backgroundColor: 'var(--color-third)', fontWeight: 'normal' },
'100%': {
backgroundColor: 'var(--color-third)',
fontWeight: 'normal',
},
},
}),
screens: {
custom: { raw: '(max-height: 800px)' },
xs: { max: '401px'} ,
xs: { max: '401px' },
},
},
},

1011
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -81,6 +81,7 @@
"@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.11",
"@types/nodemailer": "^6.4.16",
"@types/react-dropzone": "^4.2.2",
"@types/remove-markdown": "^0.3.4",
"@types/sha256": "^0.2.2",
"@types/stripe": "^8.0.417",
@ -139,6 +140,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.3.1",
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.50.1",
"react-loading": "^2.0.3",
"react-tag-autocomplete": "^7.2.0",