From 382df926fa8d0e96f0c895ebe77c1e39fe18012f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 30 Jun 2025 23:10:11 +0700 Subject: [PATCH] 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 && (