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/backend/src/api/routes/media.controller.ts b/apps/backend/src/api/routes/media.controller.ts index b86bfe0d..b3284f44 100644 --- a/apps/backend/src/api/routes/media.controller.ts +++ b/apps/backend/src/api/routes/media.controller.ts @@ -22,6 +22,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; +import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto'; @ApiTags('Media') @Controller('/media') @@ -95,16 +96,38 @@ export class MediaController { if (!name) { return false; } - return this._mediaService.saveFile(org.id, name, process.env.CLOUDFLARE_BUCKET_URL + '/' + name); + return this._mediaService.saveFile( + org.id, + name, + process.env.CLOUDFLARE_BUCKET_URL + '/' + name + ); + } + + @Post('/information') + saveMediaInformation( + @GetOrgFromRequest() org: Organization, + @Body() body: SaveMediaInformationDto + ) { + return this._mediaService.saveMediaInformation( + org.id, + body + ); } @Post('/upload-simple') @UseInterceptors(FileInterceptor('file')) async uploadSimple( @GetOrgFromRequest() org: Organization, - @UploadedFile('file') file: Express.Multer.File + @UploadedFile('file') file: Express.Multer.File, + @Body('preventSave') preventSave: string = 'false' ) { const getFile = await this.storage.uploadFile(file); + + if (preventSave === 'true') { + const { path } = getFile; + return { path }; + } + return this._mediaService.saveFile( org.id, getFile.originalname, 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/linkedin.component.tsx b/apps/frontend/src/components/launches/helpers/linkedin.component.tsx index 0dc5ecb8..e5a979d2 100644 --- a/apps/frontend/src/components/launches/helpers/linkedin.component.tsx +++ b/apps/frontend/src/components/launches/helpers/linkedin.component.tsx @@ -101,6 +101,13 @@ export const LinkedinCompany: FC<{ id: string; }> = (props) => { const { onClose, onSelect, id } = props; + const setActivateExitButton = useLaunchStore((e) => e.setActivateExitButton); + useEffect(() => { + setActivateExitButton(false); + return () => { + setActivateExitButton(true); + }; + }, []); const fetch = useFetch(); const [company, setCompany] = useState(null); const toast = useToaster(); @@ -136,6 +143,7 @@ export const LinkedinCompany: FC<{ + + + + {/* 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: (media: { + id: string; + name: string; + path: string; + thumbnail: string; + alt: string; + }) => void; + media: + | { 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 ( +
+
+
+
+
+ +
+ +
+
+
+ + 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/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/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/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..bbc4ccbe 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,8 @@ 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'; +import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; const Polonto = dynamic( () => import('@gitroom/frontend/components/launches/polonto') ); @@ -158,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( @@ -184,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]); @@ -480,6 +486,8 @@ export const MultiMediaComponent: FC<{ value?: Array<{ id: string; path: string; + alt?: string; + thumbnail?: string; }>; }; }) => void; @@ -550,46 +558,121 @@ 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) => ( + +
+
+ :: +
+ +
+
{ + 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]" + > + + + +
+ {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 f1474bec..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; @@ -69,7 +70,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 }, @@ -154,6 +155,7 @@ export function useUppyUploader(props: { return; } + console.log(result); if (transloadit.length > 0) { // @ts-ignore const allRes = result.transloadit[0].results; diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index ef4a81fa..5a7c92a4 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' + )} +
+ )}
+
+ {setImages && ( + { + setImages(value.target.value); + }} + onOpen={() => {}} + onClose={() => {}} + /> + )} +
- {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 && ( - { - setImages(value.target.value); - }} - onOpen={() => {}} - onClose={() => {}} - /> - )}
{(props?.totalChars || 0) > 0 && ( diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index 9dd978e5..ab308d70 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -60,6 +60,7 @@ export const ManageModal: FC = (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 b12e6f91..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; @@ -203,7 +213,7 @@ export const ThirdPartyMedia: FC<{