From 0b2e2667263b90002fe5236c43d390e3637d6dc2 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 30 Jun 2025 18:22:49 +0700 Subject: [PATCH 1/3] feat: assembly --- .../src/components/media/new.uploader.tsx | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/media/new.uploader.tsx b/apps/frontend/src/components/media/new.uploader.tsx index f1474bec..93038db3 100644 --- a/apps/frontend/src/components/media/new.uploader.tsx +++ b/apps/frontend/src/components/media/new.uploader.tsx @@ -69,7 +69,7 @@ export function useUppyUploader(props: { const uppy2 = new Uppy({ autoProceed: true, restrictions: { - maxNumberOfFiles: 5, + // maxNumberOfFiles: 5, allowedFileTypes: allowedFileTypes.split(','), maxFileSize: 1000000000, // Default 1GB, but we'll override with custom validation }, @@ -120,6 +120,67 @@ export function useUppyUploader(props: { }); }); + // required thumbnails + uppy2.addPreProcessor(async (fileIDs) => { + return new Promise(async (resolve, reject) => { + const findVideos = uppy2 + .getFiles() + .filter((f) => fileIDs.includes(f.id)) + .filter((f) => f.type?.startsWith('video/')); + + if (findVideos.length === 0) { + resolve(true); + return; + } + + for (const currentVideo of findVideos) { + const resolvedVideo = await new Promise((resolve, reject) => { + const video = document.createElement('video'); + const url = URL.createObjectURL(currentVideo.data); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + video.addEventListener('loadedmetadata', () => { + const duration = video.duration; + const randomTime = Math.random() * duration; + + video.currentTime = randomTime; + + video.addEventListener('seeked', function capture() { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + canvas.toBlob((blob) => { + resolve(blob); + }); + // Cleanup + video.removeEventListener('seeked', capture); + URL.revokeObjectURL(url); + }); + }); + + video.src = url; + }); + + uppy2.addFile({ + name: `thumbnail-${Date.now()}.jpg`, + type: 'image/jpeg', + data: resolvedVideo, + meta: { + thumbnail: true, + videoId: currentVideo.id, + }, + isRemote: false, + }); + } + + // add the new thumbnail to uppy seperate from the file + + resolve(true); + }); + }); + const { plugin, options } = getUppyUploadPlugin( transloadit.length > 0 ? 'transloadit' : storageProvider, fetch, @@ -154,6 +215,7 @@ export function useUppyUploader(props: { return; } + console.log(result); if (transloadit.length > 0) { // @ts-ignore const allRes = result.transloadit[0].results; From 382df926fa8d0e96f0c895ebe77c1e39fe18012f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 30 Jun 2025 23:10:11 +0700 Subject: [PATCH 2/3] feat: thumb --- .github/workflows/build-containers.yml | 4 +- Dockerfile.dev | 4 +- .../src/components/launches/ai.image.tsx | 2 +- .../helpers/media.settings.component.tsx | 114 ++++++++++ .../launches/launches.component.tsx | 2 +- .../src/components/layout/layout.settings.tsx | 2 + .../src/components/media/media.component.tsx | 168 ++++++++------ .../src/components/media/new.uploader.tsx | 62 +---- .../src/components/new-launch/editor.tsx | 213 +++++++++--------- .../third-parties/third-party.media.tsx | 2 +- .../database/prisma/media/media.repository.ts | 16 ++ .../src/database/prisma/schema.prisma | 2 + .../src/helpers/video.frame.tsx | 2 +- 13 files changed, 343 insertions(+), 250 deletions(-) create mode 100644 apps/frontend/src/components/launches/helpers/media.settings.component.tsx diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml index 9f7bb9d5..cf2cf0ca 100644 --- a/.github/workflows/build-containers.yml +++ b/.github/workflows/build-containers.yml @@ -52,12 +52,12 @@ jobs: - name: Build and Push Image env: CONTAINERVER: ${{ needs.build-containers-common.outputs.containerver }} - VERSION: ${{ github.ref_name }} + NEXT_PUBLIC_VERSION: ${{ github.ref_name }} run: | docker buildx build --platform linux/${{ matrix.arch }} \ -f Dockerfile.dev \ -t ghcr.io/gitroomhq/postiz-app:${{ env.CONTAINERVER }}-${{ matrix.arch }} \ - --build-arg VERSION=${{ env.VERSION }} \ + --build-arg NEXT_PUBLIC_VERSION=${{ env.NEXT_PUBLIC_VERSION }} \ --provenance=false --sbom=false \ --output "type=registry,name=ghcr.io/gitroomhq/postiz-app:${{ env.CONTAINERVER }}-${{ matrix.arch }}" . diff --git a/Dockerfile.dev b/Dockerfile.dev index 678cc1e0..55fe8690 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,6 +1,6 @@ FROM node:20-alpine3.19 -ARG VERSION -ENV VERSION=$VERSION +ARG NEXT_PUBLIC_VERSION +ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION RUN apk add --no-cache g++ make py3-pip supervisor bash caddy RUN npm --no-update-notifier --no-fund --global install pnpm@10.6.1 pm2 diff --git a/apps/frontend/src/components/launches/ai.image.tsx b/apps/frontend/src/components/launches/ai.image.tsx index 862fc67c..1fdc9f96 100644 --- a/apps/frontend/src/components/launches/ai.image.tsx +++ b/apps/frontend/src/components/launches/ai.image.tsx @@ -68,7 +68,7 @@ ${type} } : {})} className={clsx( - 'relative ms-[10px] rounded-[4px] mb-[10px] gap-[8px] !text-primary justify-center items-center flex border border-dashed border-customColor21 bg-input', + 'relative ms-[10px] rounded-[4px] gap-[8px] !text-primary justify-center items-center flex border border-dashed border-customColor21 bg-input', value.length < 30 && 'opacity-25' )} > diff --git a/apps/frontend/src/components/launches/helpers/media.settings.component.tsx b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx new file mode 100644 index 00000000..77539473 --- /dev/null +++ b/apps/frontend/src/components/launches/helpers/media.settings.component.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { EventEmitter } from 'events'; +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +const postUrlEmitter = new EventEmitter(); + +export const MediaSettingsLayout = () => { + const [showPostSelector, setShowPostSelector] = useState(false); + const [media, setMedia] = useState(undefined); + const [callback, setCallback] = useState<{ + callback: (tag: string) => void; + // eslint-disable-next-line @typescript-eslint/no-empty-function + } | null>({ + callback: (tag: string) => {}, + } as any); + useEffect(() => { + postUrlEmitter.on( + 'show', + (params: { media: any; callback: (url: string) => void }) => { + setCallback(params); + setMedia(params.media); + setShowPostSelector(true); + } + ); + return () => { + setShowPostSelector(false); + setCallback(null); + setMedia(undefined); + postUrlEmitter.removeAllListeners(); + }; + }, []); + const close = useCallback(() => { + setShowPostSelector(false); + setCallback(null); + setMedia(undefined); + }, []); + if (!showPostSelector) { + return <>; + } + return ( + + ); +}; + +export const useMediaSettings = () => { + return useCallback((media: any) => { + return new Promise((resolve) => { + postUrlEmitter.emit('show', { + media, + callback: (value: any) => { + resolve(value); + }, + }); + }); + }, []); +}; + +export const CreateThumbnail: FC<{ + media: + | { id: string; name: string; path: string; thumbnail: string } + | undefined; +}> = (props) => { + return null; +}; + +export const MediaComponentInner: FC<{ + onClose: () => void; + onSelect: (tag: string) => void; + media: + | { id: string; name: string; path: string; thumbnail: string } + | undefined; +}> = (props) => { + const { onClose, onSelect, media } = props; + + return ( +
+
+
+
+ +
+ +
+
+ {media?.path.indexOf('mp4') > -1 && } +
+
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index cd3bcf4b..2e568852 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -453,7 +453,7 @@ export const LaunchesComponent = () => { user?.tier?.ai && billingEnabled && }
- {process.env.VERSION ? `v${process.env.VERSION}` : ''} + {process.env.NEXT_PUBLIC_VERSION ? `v${process.env.NEXT_PUBLIC_VERSION}` : ''}
diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index a1dd1ac2..9c287e62 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -44,6 +44,7 @@ import { ChromeExtensionComponent } from '@gitroom/frontend/components/layout/ch import { LanguageComponent } from '@gitroom/frontend/components/layout/language.component'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import i18next from '@gitroom/react/translation/i18next'; +import { MediaSettingsLayout } from '@gitroom/frontend/components/launches/helpers/media.settings.component'; extend(utc); extend(weekOfYear); extend(isoWeek); @@ -79,6 +80,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { + diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index 1213d503..494d1970 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -1,6 +1,6 @@ 'use client'; -import { +import React, { ClipboardEvent, FC, Fragment, @@ -30,6 +30,7 @@ import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import { ThirdPartyMedia } from '@gitroom/frontend/components/third-parties/third-party.media'; import { ReactSortable } from 'react-sortablejs'; +import { useMediaSettings } from '@gitroom/frontend/components/launches/helpers/media.settings.component'; const Polonto = dynamic( () => import('@gitroom/frontend/components/launches/polonto') ); @@ -550,46 +551,108 @@ export const MultiMediaComponent: FC<{ setMediaModal(true); }, []); + const mediaSettings = useMediaSettings(); + const t = useT(); return ( <> -
+
{modal && } {mediaModal && !!user?.tier?.ai && ( )} -
-
- +
+ + + {!!currentMedia && ( + + onChange({ target: { name: 'upload', value } }) + } + className="flex gap-[10px] sortable-container" + animation={200} + swap={true} + handle=".dragging" + > + {currentMedia.map((media, index) => ( + +
+
+ :: +
+ +
+
mediaSettings(media)} + className="absolute top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] bg-black/80 rounded-[10px] opacity-0 group-hover:opacity-100 transition-opacity z-[100]" + > + + + +
+ {media?.path?.indexOf('mp4') > -1 ? ( + + ) : ( + + )} +
+
+ x +
+
+
+ ))} +
+ )} +
+
+
{error}
diff --git a/apps/frontend/src/components/media/new.uploader.tsx b/apps/frontend/src/components/media/new.uploader.tsx index 93038db3..6260f695 100644 --- a/apps/frontend/src/components/media/new.uploader.tsx +++ b/apps/frontend/src/components/media/new.uploader.tsx @@ -54,6 +54,7 @@ export function MultipartFileUploader({ /> ); } + export function useUppyUploader(props: { // @ts-ignore onUploadSuccess: (result: UploadResult) => void; @@ -120,67 +121,6 @@ export function useUppyUploader(props: { }); }); - // required thumbnails - uppy2.addPreProcessor(async (fileIDs) => { - return new Promise(async (resolve, reject) => { - const findVideos = uppy2 - .getFiles() - .filter((f) => fileIDs.includes(f.id)) - .filter((f) => f.type?.startsWith('video/')); - - if (findVideos.length === 0) { - resolve(true); - return; - } - - for (const currentVideo of findVideos) { - const resolvedVideo = await new Promise((resolve, reject) => { - const video = document.createElement('video'); - const url = URL.createObjectURL(currentVideo.data); - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - video.addEventListener('loadedmetadata', () => { - const duration = video.duration; - const randomTime = Math.random() * duration; - - video.currentTime = randomTime; - - video.addEventListener('seeked', function capture() { - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - - canvas.toBlob((blob) => { - resolve(blob); - }); - // Cleanup - video.removeEventListener('seeked', capture); - URL.revokeObjectURL(url); - }); - }); - - video.src = url; - }); - - uppy2.addFile({ - name: `thumbnail-${Date.now()}.jpg`, - type: 'image/jpeg', - data: resolvedVideo, - meta: { - thumbnail: true, - videoId: currentVideo.id, - }, - isRemote: false, - }); - } - - // add the new thumbnail to uppy seperate from the file - - resolve(true); - }); - }); - const { plugin, options } = getUppyUploadPlugin( transloadit.length > 0 ? 'transloadit' : storageProvider, fetch, diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index ef4a81fa..de833050 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -28,15 +28,10 @@ import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data'; import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core'; -import { - LinkedinCompany, - LinkedinCompanyPop, - ShowLinkedinCompany, -} from '@gitroom/frontend/components/launches/helpers/linkedin.component'; -import { DropEvent, FileRejection, useDropzone } from 'react-dropzone'; +import { LinkedinCompanyPop } from '@gitroom/frontend/components/launches/helpers/linkedin.component'; +import { useDropzone } from 'react-dropzone'; import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader'; -import { UploadResult } from '@uppy/core'; -import { Dashboard, ProgressBar } from '@uppy/react'; +import { Dashboard } from '@uppy/react'; export const EditorWrapper: FC<{ totalPosts: number; value: string; @@ -261,102 +256,108 @@ export const EditorWrapper: FC<{ return null; } - return items.map((g, index) => ( -
- {!canEdit && !isCreateSet && ( -
{ - if (index !== 0) { - return; - } + return ( +
+ {items.map((g, index) => ( +
+ {!canEdit && !isCreateSet && ( +
{ + if (index !== 0) { + return; + } - setLoaded(false); - addRemoveInternal(current); - }} - className="select-none cursor-pointer absolute w-full h-full left-0 top-0 bg-red-600/10 z-[100]" - > - {index === 0 && ( -
- Edit + setLoaded(false); + addRemoveInternal(current); + }} + className="select-none cursor-pointer absolute w-full h-full left-0 top-0 bg-red-600/10 z-[100]" + > + {index === 0 && ( +
+ Edit +
+ )}
)} -
- )} -
-
- -
-
- - {index === 0 && - current !== 'global' && - canEdit && - !existingData.integration && ( - - - - )} - {items.length > 1 && ( - - +
+ - +
+
+ + {index === 0 && + current !== 'global' && + canEdit && + !existingData.integration && ( + + + + )} + {items.length > 1 && ( + + + + )} +
+
+ + {canEdit ? ( + + ) : ( +
)}
-
- - {canEdit ? ( - - ) :
} + ))}
- )); + ); }; export const Editor: FC<{ @@ -475,6 +476,14 @@ export const Editor: FC<{
+ {validateChars && props.value.length < 6 && ( +
+ {t( + 'the_post_should_be_at_least_6_characters_long', + 'The post should be at least 6 characters long' + )} +
+ )}
- {validateChars && props.value.length < 6 && ( -
- {t( - 'the_post_should_be_at_least_6_characters_long', - 'The post should be at least 6 characters long' - )} -
- )}
-
+
{setImages && (
+ +
+ + {/* Hidden file input */} + + + {mode === 'frame' ? ( + <> +
+
+ + {isLoaded && ( + <> +
+ +
+ {formatTime(currentTime)} + {formatTime(duration)} +
+
+ +
+ +
+ + )} + + ) : ( +
+
+ + + +
+

Upload a custom thumbnail image

+

+ PNG, JPG, JPEG up to 10MB +

+
+
+ + +
+ )} + + +
+ ); }; export const MediaComponentInner: FC<{ onClose: () => void; - onSelect: (tag: string) => void; + onSelect: (media: { + id: string; + name: string; + path: string; + thumbnail: string; + alt: string; + }) => void; media: - | { id: string; name: string; path: string; thumbnail: string } + | { id: string; name: string; path: string; thumbnail: string; alt: string } | undefined; }> = (props) => { const { onClose, onSelect, media } = props; + const setActivateExitButton = useLaunchStore((e) => e.setActivateExitButton); + const newFetch = useFetch(); + const [newThumbnail, setNewThumbnail] = useState(null); + const [isEditingThumbnail, setIsEditingThumbnail] = useState(false); + const [altText, setAltText] = useState(media?.alt || ''); + const [loading, setLoading] = useState(false); + const [thumbnail, setThumbnail] = useState(props.media?.thumbnail || null); + + useEffect(() => { + setActivateExitButton(false); + return () => { + setActivateExitButton(true); + }; + }, []); + + const save = useCallback(async () => { + setLoading(true); + let path = thumbnail || ''; + if (newThumbnail) { + const blob = await (await fetch(newThumbnail)).blob(); + const formData = new FormData(); + formData.append('file', blob, 'media.jpg'); + formData.append('preventSave', 'true'); + const data = await ( + await newFetch('/media/upload-simple', { + method: 'POST', + body: formData, + }) + ).json(); + path = data.path; + } + + const media = await ( + await newFetch('/media/information', { + method: 'POST', + body: JSON.stringify({ + id: props.media.id, + alt: altText, + thumbnail: path, + }), + }) + ).json(); + + onSelect(media); + onClose(); + }, [altText, newThumbnail, thumbnail]); return ( -
-
-
-
- -
- -
-
- {media?.path.indexOf('mp4') > -1 && } + + + + +
+
+
+ + setAltText(e.target.value)} + placeholder="Describe the image/video content..." + className="w-full px-3 py-2 bg-fifth border border-tableBorder rounded-lg text-textColor placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-forth focus:border-transparent" + /> +
+ {media?.path.indexOf('mp4') > -1 && ( + <> + {/* Alt Text Input */} +
+ {!isEditingThumbnail ? ( +
+ {/* Show existing thumbnail if it exists */} + {(newThumbnail || thumbnail) && ( +
+ + Current Thumbnail: + + Current thumbnail +
+ )} + + {/* Action Buttons */} +
+ + {(thumbnail || newThumbnail) && ( + + )} +
+
+ ) : ( +
+ {/* Back button */} +
+ +
+ + {/* Thumbnail Editor */} + { + // Convert blob to base64 or handle as needed + const reader = new FileReader(); + reader.onload = () => { + // You can handle the result here - for now just call onSelect with the blob URL + const url = URL.createObjectURL(blob); + setNewThumbnail(url); + setIsEditingThumbnail(false); + }; + reader.readAsDataURL(blob); + }} + media={media} + altText={altText} + onAltTextChange={setAltText} + /> +
+ )} +
+ + )} + + {!isEditingThumbnail && ( +
+ + +
+ )} +
diff --git a/apps/frontend/src/components/launches/polonto.tsx b/apps/frontend/src/components/launches/polonto.tsx index 89c5bd1e..ffd73225 100644 --- a/apps/frontend/src/components/launches/polonto.tsx +++ b/apps/frontend/src/components/launches/polonto.tsx @@ -21,6 +21,7 @@ import { PictureGeneratorSection } from '@gitroom/frontend/components/launches/p import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { loadVars } from '@gitroom/react/helpers/variable.context'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; const store = createStore({ get key() { return loadVars().plontoKey; @@ -75,6 +76,15 @@ const Polonto: FC<{ height?: number; }> = (props) => { const { setMedia, type, closeModal } = props; + + const setActivateExitButton = useLaunchStore((e) => e.setActivateExitButton); + useEffect(() => { + setActivateExitButton(false); + return () => { + setActivateExitButton(true); + }; + }, []); + const user = useUser(); const features = useMemo(() => { return [ diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index 494d1970..bbc4ccbe 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -31,6 +31,7 @@ import { useT } from '@gitroom/react/translation/get.transation.service.client'; import { ThirdPartyMedia } from '@gitroom/frontend/components/third-parties/third-party.media'; import { ReactSortable } from 'react-sortablejs'; import { useMediaSettings } from '@gitroom/frontend/components/launches/helpers/media.settings.component'; +import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; const Polonto = dynamic( () => import('@gitroom/frontend/components/launches/polonto') ); @@ -159,15 +160,25 @@ export const MediaBox: FC<{ }> = (props) => { const { setMedia, type, closeModal } = props; const [mediaList, setListMedia] = useState([]); + const setActivateExitButton = useLaunchStore((e) => e.setActivateExitButton); const fetch = useFetch(); const mediaDirectory = useMediaDirectory(); const [page, setPage] = useState(0); const [pages, setPages] = useState(0); const [selectedMedia, setSelectedMedia] = useState([]); const ref = useRef(null); + + useEffect(() => { + setActivateExitButton(false); + return () => { + setActivateExitButton(true); + }; + }, []); + const loadMedia = useCallback(async () => { return (await fetch(`/media?page=${page + 1}`)).json(); }, [page]); + const setNewMedia = useCallback( (media: Media) => () => { setSelectedMedia( @@ -185,13 +196,7 @@ export const MediaBox: FC<{ }, [selectedMedia] ); - const removeMedia = useCallback( - (media: Media) => () => { - setSelectedMedia(selectedMedia.filter((f) => f.id !== media.id)); - setListMedia(mediaList.filter((f) => f.id !== media.id)); - }, - [selectedMedia] - ); + const addNewMedia = useCallback( (media: Media[]) => () => { setSelectedMedia((currentMedia) => [...currentMedia, ...media]); @@ -481,6 +486,8 @@ export const MultiMediaComponent: FC<{ value?: Array<{ id: string; path: string; + alt?: string; + thumbnail?: string; }>; }; }) => void; @@ -610,7 +617,20 @@ export const MultiMediaComponent: FC<{
mediaSettings(media)} + onClick={async () => { + const data: any = await mediaSettings(media); + console.log( + value?.map((p) => (p.id === data.id ? data : p)) + ); + onChange({ + target: { + name: 'upload', + value: value?.map((p) => + p.id === data.id ? data : p + ), + }, + }); + }} className="absolute top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] bg-black/80 rounded-[10px] opacity-0 group-hover:opacity-100 transition-opacity z-[100]" >
+
+ {setImages && ( + { + setImages(value.target.value); + }} + onOpen={() => {}} + onClose={() => {}} + /> + )} +
-
- {setImages && ( - { - setImages(value.target.value); - }} - onOpen={() => {}} - onClose={() => {}} - /> - )} -
{(props?.totalChars || 0) > 0 && (
= (props) => { integrations, setSelectedIntegrations, locked, + activateExitButton, } = useLaunchStore( useShallow((state) => ({ hide: state.hide, @@ -73,6 +74,7 @@ export const ManageModal: FC = (props) => { integrations: state.integrations, setSelectedIntegrations: state.setSelectedIntegrations, locked: state.locked, + activateExitButton: state.activateExitButton })) ); @@ -96,6 +98,10 @@ export const ManageModal: FC = (props) => { }, [existingData, mutate, modal]); const askClose = useCallback(async () => { + if (!activateExitButton) { + return; + } + if ( await deleteDialog( t( @@ -111,7 +117,7 @@ export const ManageModal: FC = (props) => { } modal.closeAll(); } - }, []); + }, [activateExitButton]); const changeCustomer = useCallback( (customer: string) => { diff --git a/apps/frontend/src/components/new-launch/store.ts b/apps/frontend/src/components/new-launch/store.ts index d7b0b491..d00b22a7 100644 --- a/apps/frontend/src/components/new-launch/store.ts +++ b/apps/frontend/src/components/new-launch/store.ts @@ -30,6 +30,7 @@ interface StoreState { repeater?: number; isCreateSet: boolean; totalChars: number; + activateExitButton: boolean; tags: { label: string; value: string }[]; tab: 0 | 1; current: string; @@ -114,9 +115,11 @@ interface StoreState { media: { id: string; path: string }[] ) => void; setPostComment: (postComment: PostComment) => void; + setActivateExitButton?: (activateExitButton: boolean) => void; } const initialState = { + activateExitButton: true, date: dayjs(), postComment: PostComment.ALL, tags: [] as { label: string; value: string }[], @@ -498,4 +501,8 @@ export const useLaunchStore = create()((set) => ({ set((state) => ({ postComment, })), + setActivateExitButton: (activateExitButton: boolean) => + set((state) => ({ + activateExitButton, + })), })); diff --git a/apps/frontend/src/components/third-parties/third-party.media.tsx b/apps/frontend/src/components/third-parties/third-party.media.tsx index e7bfc6b8..f96bbda9 100644 --- a/apps/frontend/src/components/third-parties/third-party.media.tsx +++ b/apps/frontend/src/components/third-parties/third-party.media.tsx @@ -7,6 +7,7 @@ import React, { createContext, FC, useCallback, + useEffect, useMemo, useState, } from 'react'; @@ -15,6 +16,7 @@ import useSWR from 'swr'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import './providers/heygen.provider'; import { thirdPartyList } from '@gitroom/frontend/components/third-parties/third-party.wrapper'; +import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; const ThirdPartyContext = createContext({ id: '', @@ -57,6 +59,14 @@ export const ThirdPartyPopup: FC<{ const { closeModal, thirdParties, allData, onChange } = props; const [thirdParty, setThirdParty] = useState(null); + const setActivateExitButton = useLaunchStore((e) => e.setActivateExitButton); + useEffect(() => { + setActivateExitButton(false); + return () => { + setActivateExitButton(true); + }; + }, []); + const Component = useMemo(() => { if (!thirdParty) { return EmptyComponent; diff --git a/libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts b/libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts index bf19bcd9..648dba43 100644 --- a/libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts @@ -1,12 +1,12 @@ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; import { Injectable } from '@nestjs/common'; +import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto'; @Injectable() export class MediaRepository { constructor(private _media: PrismaRepository<'media'>) {} saveFile(org: string, fileName: string, filePath: string) { - const file = fileName.split('.'); return this._media.model.media.create({ data: { organization: { @@ -16,15 +16,13 @@ export class MediaRepository { }, name: fileName, path: filePath, - ...(fileName.indexOf('mp4') > -1 - ? { thumbnail: `${file[0]}.thumbnail.jpg` } - : {}), }, select: { id: true, name: true, path: true, thumbnail: true, + alt: true, }, }); } @@ -49,6 +47,26 @@ export class MediaRepository { }); } + saveMediaInformation(org: string, data: SaveMediaInformationDto) { + return this._media.model.media.update({ + where: { + id: data.id, + organizationId: org, + }, + data: { + alt: data.alt, + thumbnail: data.thumbnail, + }, + select: { + id: true, + name: true, + alt: true, + thumbnail: true, + path: true, + }, + }); + } + async getMedia(org: string, page: number) { const pageNum = (page || 1) - 1; const query = { @@ -75,6 +93,7 @@ export class MediaRepository { name: true, path: true, thumbnail: true, + alt: true, }, skip: pageNum * 28, take: 28, diff --git a/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts index 921ebc11..496504f9 100644 --- a/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/media/media.service.ts @@ -3,6 +3,7 @@ import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { Organization } from '@prisma/client'; +import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto'; @Injectable() export class MediaService { @@ -44,4 +45,8 @@ export class MediaService { getMedia(org: string, page: number) { return this._mediaRepository.getMedia(org, page); } + + saveMediaInformation(org: string, data: SaveMediaInformationDto) { + return this._mediaRepository.saveMediaInformation(org, data); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index b09684ca..52f67514 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -203,6 +203,7 @@ model Media { organization Organization @relation(fields: [organizationId], references: [id]) organizationId String thumbnail String? + alt String? fileSize Int @default(0) type String @default("image") createdAt DateTime @default(now()) diff --git a/libraries/nestjs-libraries/src/dtos/media/save.media.information.dto.ts b/libraries/nestjs-libraries/src/dtos/media/save.media.information.dto.ts new file mode 100644 index 00000000..0cf4d58c --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/media/save.media.information.dto.ts @@ -0,0 +1,13 @@ +import { IsOptional, IsString, IsUrl, ValidateIf } from 'class-validator'; + +export class SaveMediaInformationDto { + @IsString() + id: string; + + @IsString() + alt: string; + + @IsUrl() + @ValidateIf((o) => !!o.thumbnail) + thumbnail: string; +}