diff --git a/.gitignore b/.gitignore index 3d97dd0e..33b2d6a9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ tmp # dependencies node_modules +uploads + # IDEs and editors /.idea .project diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 38c434de..10b8c08f 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -26,7 +26,6 @@ import { import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useMoveToIntegration } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration'; import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data'; -import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; import { useExpend } from '@gitroom/frontend/components/launches/helpers/use.expend'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component'; @@ -50,6 +49,7 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import Image from 'next/image'; import { weightedLength } from '@gitroom/helpers/utils/count.length'; +import { MultiMediaComponent } from '../media/multimedia.component'; import { uniqBy } from 'lodash'; import { Select } from '@gitroom/react/form/select'; @@ -512,7 +512,7 @@ export const AddEditModal: FC<{
-
+
1 ? 150 : 250} @@ -533,7 +533,7 @@ export const AddEditModal: FC<{ {showError && (!p.content || p.content.length < 6) && ( -
+
The post should be at least 6 characters long
)} diff --git a/apps/frontend/src/components/launches/bot.picture.tsx b/apps/frontend/src/components/launches/bot.picture.tsx index cca8f426..399eca7f 100644 --- a/apps/frontend/src/components/launches/bot.picture.tsx +++ b/apps/frontend/src/components/launches/bot.picture.tsx @@ -6,7 +6,7 @@ import { Input } from '@gitroom/react/form/input'; import { Button } from '@gitroom/react/form/button'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useToaster } from '@gitroom/react/toaster/toaster'; -import { showMediaBox } from '@gitroom/frontend/components/media/media.component'; +import { showMediaBox } from '../media/mediaboxmodal.component'; export const BotPicture: FC<{ integration: Integrations; diff --git a/apps/frontend/src/components/launches/helpers/new.image.component.tsx b/apps/frontend/src/components/launches/helpers/new.image.component.tsx index 3078dfc5..0182360b 100644 --- a/apps/frontend/src/components/launches/helpers/new.image.component.tsx +++ b/apps/frontend/src/components/launches/helpers/new.image.component.tsx @@ -6,8 +6,8 @@ import { selectWord, TextAreaTextApi, } from '@uiw/react-md-editor'; -import { showMediaBox } from '@gitroom/frontend/components/media/media.component'; import { loadVars } from '@gitroom/react/helpers/variable.context'; +import { showMediaBox } from '../../media/mediaboxmodal.component'; export const newImage: ICommand = { name: 'image', diff --git a/apps/frontend/src/components/launches/providers/high.order.provider.tsx b/apps/frontend/src/components/launches/providers/high.order.provider.tsx index 86b67569..ad1bafde 100644 --- a/apps/frontend/src/components/launches/providers/high.order.provider.tsx +++ b/apps/frontend/src/components/launches/providers/high.order.provider.tsx @@ -21,7 +21,6 @@ import { IntegrationContext, useIntegration, } from '@gitroom/frontend/components/launches/helpers/use.integration'; -import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; import { createPortal } from 'react-dom'; import clsx from 'clsx'; import { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component'; @@ -37,6 +36,7 @@ import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core'; import { AddPostButton } from '@gitroom/frontend/components/launches/add.post.button'; import { GeneralPreviewComponent } from '@gitroom/frontend/components/launches/general.preview.component'; import { capitalize } from 'lodash'; +import { MultiMediaComponent } from '../../media/multimedia.component'; import { useModals } from '@mantine/modals'; // Simple component to change back to settings on after changing tab @@ -375,7 +375,7 @@ export const withProvider = function ( onChange={changeValue(index)} /> {(!val.content || val.content.length < 6) && ( -
+
The post should be at least 6 characters long
)} diff --git a/apps/frontend/src/components/launches/providers/reddit/subreddit.tsx b/apps/frontend/src/components/launches/providers/reddit/subreddit.tsx index b9049476..815efc7f 100644 --- a/apps/frontend/src/components/launches/providers/reddit/subreddit.tsx +++ b/apps/frontend/src/components/launches/providers/reddit/subreddit.tsx @@ -4,12 +4,12 @@ import { Input } from '@gitroom/react/form/input'; import { useDebouncedCallback } from 'use-debounce'; import { Button } from '@gitroom/react/form/button'; import clsx from 'clsx'; -import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; import { useWatch } from 'react-hook-form'; import { Select } from '@gitroom/react/form/select'; import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; import { Canonical } from '@gitroom/react/form/canonical'; import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; +import { MultiMediaComponent } from '@gitroom/frontend/components/media/multimedia.component'; export const RenderOptions: FC<{ options: Array<'self' | 'link' | 'media'>; diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 38ce4d9d..1ee81271 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -6,7 +6,6 @@ import { ContextWrapper } from '@gitroom/frontend/components/layout/user.context import { TopMenu } from '@gitroom/frontend/components/layout/top.menu'; import { MantineWrapper } from '@gitroom/react/helpers/mantine.wrapper'; import { ToolTip } from '@gitroom/frontend/components/layout/top.tip'; -import { ShowMediaBoxModal } from '@gitroom/frontend/components/media/media.component'; import Image from 'next/image'; import { Toaster } from '@gitroom/react/toaster/toaster'; import { ShowPostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector'; @@ -37,6 +36,7 @@ const ModeComponent = dynamic( ); import { extend } from 'dayjs'; +import { ShowMediaBoxModal } from '../media/mediaboxmodal.component'; extend(utc); extend(weekOfYear); diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index f4d56eea..da720ac9 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -6,7 +6,6 @@ import { Input } from '@gitroom/react/form/input'; import { Button } from '@gitroom/react/form/button'; import { Textarea } from '@gitroom/react/form/textarea'; import { FormProvider, useForm } from 'react-hook-form'; -import { showMediaBox } from '@gitroom/frontend/components/media/media.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { classValidatorResolver } from '@hookform/resolvers/class-validator'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; @@ -18,6 +17,7 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { LogoutComponent } from '@gitroom/frontend/components/layout/logout.component'; import { useSearchParams } from 'next/navigation'; import { useVariables } from '@gitroom/react/helpers/variable.context'; +import { showMediaBox } from '../media/mediaboxmodal.component'; export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => { const {isGeneral} = useVariables(); diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index d9393e58..9cabccb9 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -2,345 +2,15 @@ import { FC, useCallback, useEffect, useState } from 'react'; import { Button } from '@gitroom/react/form/button'; -import useSWR from 'swr'; -import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; -import { Media } from '@prisma/client'; import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory'; import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; -import EventEmitter from 'events'; -import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; -import clsx from 'clsx'; -import { VideoFrame } from '@gitroom/react/helpers/video.frame'; -import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; -import { MultipartFileUploader } from '@gitroom/frontend/components/media/new.uploader'; import dynamic from 'next/dynamic'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { MediaBox } from './mediabox.component'; const Polonto = dynamic( () => import('@gitroom/frontend/components/launches/polonto') ); -const showModalEmitter = new EventEmitter(); -export const ShowMediaBoxModal: FC = () => { - const [showModal, setShowModal] = useState(false); - const [callBack, setCallBack] = - useState<(params: { id: string; path: string }) => void | undefined>(); - - const closeModal = useCallback(() => { - setShowModal(false); - setCallBack(undefined); - }, []); - - useEffect(() => { - showModalEmitter.on('show-modal', (cCallback) => { - setShowModal(true); - setCallBack(() => cCallback); - }); - return () => { - showModalEmitter.removeAllListeners('show-modal'); - }; - }, []); - if (!showModal) return null; - - return ( -
- -
- ); -}; - -export const showMediaBox = ( - callback: (params: { id: string; path: string }) => void -) => { - showModalEmitter.emit('show-modal', callback); -}; - -const CHUNK_SIZE = 1024 * 1024; - -export const MediaBox: FC<{ - setMedia: (params: { id: string; path: string }) => void; - type?: 'image' | 'video'; - closeModal: () => void; -}> = (props) => { - const { setMedia, type, closeModal } = props; - const [pages, setPages] = useState(0); - const [mediaList, setListMedia] = useState([]); - const fetch = useFetch(); - const mediaDirectory = useMediaDirectory(); - - const [loading, setLoading] = useState(false); - - const loadMedia = useCallback(async () => { - return (await fetch('/media')).json(); - }, []); - - const setNewMedia = useCallback( - (media: Media) => () => { - setMedia(media); - closeModal(); - }, - [] - ); - - const { data, mutate } = useSWR('get-media', loadMedia); - - useEffect(() => { - if (data?.pages) { - setPages(data.pages); - } - if (data?.results && data?.results?.length) { - setListMedia([...data.results]); - } - }, [data]); - - return ( -
-
-
-
- -
- - - {!!mediaList.length && ( - - )} -
-
- {!mediaList.length && ( -
-
You don{"'"}t have any assets yet.
-
Click the button below to upload one
-
- -
-
- )} - {mediaList - .filter((f) => { - if (type === 'video') { - return f.path.indexOf('mp4') > -1; - } else if (type === 'image') { - return f.path.indexOf('mp4') === -1; - } - return true; - }) - .map((media) => ( -
- {media.path.indexOf('mp4') > -1 ? ( - - ) : ( - media - )} -
- ))} - {loading && ( -
-
- -
-
- )} -
-
-
- ); -}; - -export const MultiMediaComponent: FC<{ - label: string; - description: string; - value?: Array<{ path: string; id: string }>; - name: string; - error?: any; - onChange: (event: { - target: { name: string; value?: Array<{ id: string; path: string }> }; - }) => void; -}> = (props) => { - const { name, label, error, description, onChange, value } = props; - const user = useUser(); - useEffect(() => { - if (value) { - setCurrentMedia(value); - } - }, []); - - const [modal, setShowModal] = useState(false); - const [mediaModal, setMediaModal] = useState(false); - - const [currentMedia, setCurrentMedia] = useState(value); - const mediaDirectory = useMediaDirectory(); - - const changeMedia = useCallback( - (m: { path: string; id: string }) => { - const newMedia = [...(currentMedia || []), m]; - setCurrentMedia(newMedia); - onChange({ target: { name, value: newMedia } }); - }, - [currentMedia] - ); - - const showModal = useCallback(() => { - setShowModal(!modal); - }, [modal]); - - const closeDesignModal = useCallback(() => { - setMediaModal(false); - }, [modal]); - - const clearMedia = useCallback( - (topIndex: number) => () => { - const newMedia = currentMedia?.filter((f, index) => index !== topIndex); - setCurrentMedia(newMedia); - onChange({ target: { name, value: newMedia } }); - }, - [currentMedia] - ); - - const designMedia = useCallback(() => { - setMediaModal(true); - }, []); - - return ( - <> -
- {modal && } - {mediaModal && !!user?.tier?.ai && ( - - )} -
-
- - - -
- - {!!currentMedia && - currentMedia.map((media, index) => ( - <> -
-
window.open(mediaDirectory.set(media.path))} - > - {media.path.indexOf('mp4') > -1 ? ( - - ) : ( - - )} -
-
- x -
-
- - ))} -
-
-
{error}
- - ); -}; export const MediaComponent: FC<{ label: string; @@ -357,17 +27,19 @@ export const MediaComponent: FC<{ const { name, type, label, description, onChange, value, width, height } = props; const { getValues } = useSettings(); const user = useUser(); + + const [modal, setShowModal] = useState(false); + const [mediaModal, setMediaModal] = useState(false); + const [currentMedia, setCurrentMedia] = useState(value); + const mediaDirectory = useMediaDirectory(); + useEffect(() => { const settings = getValues()[props.name]; if (settings) { setCurrentMedia(settings); } }, []); - const [modal, setShowModal] = useState(false); - const [mediaModal, setMediaModal] = useState(false); - const [currentMedia, setCurrentMedia] = useState(value); - const mediaDirectory = useMediaDirectory(); - + const closeDesignModal = useCallback(() => { setMediaModal(false); }, [modal]); diff --git a/apps/frontend/src/components/media/mediabox.component.tsx b/apps/frontend/src/components/media/mediabox.component.tsx new file mode 100644 index 00000000..d8e6f43c --- /dev/null +++ b/apps/frontend/src/components/media/mediabox.component.tsx @@ -0,0 +1,146 @@ +import { useFetch } from "@gitroom/helpers/utils/custom.fetch"; +import { useMediaDirectory } from "@gitroom/react/helpers/use.media.directory"; +import { FC, useCallback, useEffect, useState } from "react"; +import { Media } from '@prisma/client'; +import useSWR from "swr"; +import { TopTitle } from "../launches/helpers/top.title.component"; +import { MultipartFileUploader } from "./new.uploader"; +import clsx from "clsx"; +import { VideoFrame } from '@gitroom/react/helpers/video.frame'; +import Image from "next/image"; + +export const MediaBox: FC<{ + setMedia: (params: { id: string; path: string }) => void; + type?: 'image' | 'video'; + closeModal: () => void; + }> = (props) => { + const { setMedia, type, closeModal } = props; + const [mediaList, setListMedia] = useState([]); + const fetch = useFetch(); + const mediaDirectory = useMediaDirectory(); + + const loadMedia = useCallback(async () => { + return (await fetch('/media')).json(); + }, [fetch]); + + const setNewMedia = useCallback( + (media: Media) => () => { + setMedia(media); + closeModal(); + }, + [closeModal] + ); + + const { data, mutate } = useSWR('get-media', loadMedia); + + useEffect(() => { + if (data?.results && data?.results?.length) { + setListMedia([...data.results]); + } + }, [data]); + + return ( +
+
+
+
+ +
+ + + {!!mediaList.length && ( + + )} +
+
+ {!mediaList.length && ( +
+
You don{"'"}t have any assets yet.
+
Click the button below to upload one
+
+ +
+
+ )} + {mediaList + .filter((f) => { + if (type === 'video') { + return f.path.indexOf('mp4') > -1; + } else if (type === 'image') { + return f.path.indexOf('mp4') === -1; + } + return true; + }) + .map((media) => ( +
+ {media.path.indexOf('mp4') > -1 ? ( + + ) : ( + media + )} +
+ ))} +
+
+
+ ); + }; \ No newline at end of file diff --git a/apps/frontend/src/components/media/mediaboxmodal.component.tsx b/apps/frontend/src/components/media/mediaboxmodal.component.tsx new file mode 100644 index 00000000..9356571c --- /dev/null +++ b/apps/frontend/src/components/media/mediaboxmodal.component.tsx @@ -0,0 +1,39 @@ +import { FC, useCallback, useEffect, useState } from "react"; +import { MediaBox } from "./mediabox.component"; +import EventEmitter from "events"; + +const showModalEmitter = new EventEmitter(); + +export const showMediaBox = ( + callback: (params: { id: string; path: string }) => void +) => { + showModalEmitter.emit('show-modal', callback); +}; + +export const ShowMediaBoxModal: FC = () => { + const [showModal, setShowModal] = useState(false); + const [callBack, setCallBack] = + useState<(params: { id: string; path: string }) => void | undefined>(); + + const closeModal = useCallback(() => { + setShowModal(false); + setCallBack(undefined); + }, []); + + useEffect(() => { + showModalEmitter.on('show-modal', (cCallback: any) => { + setShowModal(true); + setCallBack(() => cCallback); + }); + return () => { + showModalEmitter.removeAllListeners('show-modal'); + }; + }, []); + if (!showModal) return null; + + return callBack && ( +
+ +
+ ); + }; \ No newline at end of file diff --git a/apps/frontend/src/components/media/multimedia.component.tsx b/apps/frontend/src/components/media/multimedia.component.tsx new file mode 100644 index 00000000..98b24d9e --- /dev/null +++ b/apps/frontend/src/components/media/multimedia.component.tsx @@ -0,0 +1,142 @@ +import { FC, useCallback, useEffect, useState } from "react"; +import { useUser } from "../layout/user.context"; +import { useMediaDirectory } from "@gitroom/react/helpers/use.media.directory"; +import Polonto from "../launches/polonto"; +import { Button } from "@gitroom/react/form/button"; +import { VideoFrame } from '@gitroom/react/helpers/video.frame'; +import { MediaBox } from "./mediabox.component"; +import Image from "next/image"; + +export const MultiMediaComponent: FC<{ + label: string; + description: string; + value?: Array<{ path: string; id: string }>; + name: string; + error?: any; + onChange: (event: { + target: { name: string; value?: Array<{ id: string; path: string }> }; + }) => void; + }> = (props) => { + const { name, error, onChange, value } = props; + const user = useUser(); + + const [modal, setShowModal] = useState(false); + const [mediaModal, setMediaModal] = useState(false); + + const mediaDirectory = useMediaDirectory(); + + const changeMedia = useCallback( + (m: { path: string; id: string }) => { + const newMedia = [...(value || []), m]; + onChange({ target: { name, value: newMedia } }); + }, + [value, name] + ); + + const showModal = useCallback(() => { + setShowModal(!modal); + }, [modal]); + + const closeDesignModal = useCallback(() => { + setMediaModal(false); + }, []); + + const clearMedia = useCallback( + (topIndex: number) => () => { + const newMedia = value?.filter((f: any, index: number) => index !== topIndex); + onChange({ target: { name, value: newMedia } }); + }, + [value] + ); + + const designMedia = useCallback(() => { + setMediaModal(true); + }, []); + + return ( + <> +
+ {modal && } + {mediaModal && !!user?.tier?.ai && ( + + )} +
+
+ + + +
+ + {!!value && + value.map((media: { path: string ; }, index: number) => ( + <> +
+
window.open(mediaDirectory.set(media.path))} + > + {media.path.indexOf('mp4') > -1 ? ( + + ) : ( + {'media'+index} + )} +
+
+ x +
+
+ + ))} +
+
+
{error}
+ + ); + }; \ No newline at end of file