'use client'; import React, { FC, Fragment, MouseEventHandler, useCallback, useEffect, useMemo, useState, } from 'react'; import dayjs from 'dayjs'; import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; import clsx from 'clsx'; import MDEditor, { commands } from '@uiw/react-md-editor'; import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload'; import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; import { useModals } from '@mantine/modals'; import { useHideTopEditor } from '@gitroom/frontend/components/launches/helpers/use.hide.top.editor'; import { Button } from '@gitroom/react/form/button'; // @ts-ignore import useKeypress from 'react-use-keypress'; import { getValues, resetValues, } from '@gitroom/frontend/components/launches/helpers/use.values'; 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 { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component'; 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'; import { ProvidersOptions } from '@gitroom/frontend/components/launches/providers.options'; import { v4 as uuidv4 } from 'uuid'; import useSWR, { useSWRConfig } from 'swr'; import { useToaster } from '@gitroom/react/toaster/toaster'; import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector'; import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow'; import { DatePicker } from '@gitroom/frontend/components/launches/helpers/date.picker'; import { arrayMoveImmutable } from 'array-move'; import { Information, PostToOrganization, } from '@gitroom/frontend/components/launches/post.to.organization'; import { Submitted } from '@gitroom/frontend/components/launches/submitted'; import { capitalize } from 'lodash'; export const AddEditModal: FC<{ date: dayjs.Dayjs; integrations: Integrations[]; reopenModal: () => void; }> = (props) => { const { date, integrations, reopenModal } = props; const [dateState, setDateState] = useState(date); const { mutate } = useSWRConfig(); // hook to open a new modal const modal = useModals(); // selected integrations to allow edit const [selectedIntegrations, setSelectedIntegrations] = useState< Integrations[] >([]); // value of each editor const [value, setValue] = useState< Array<{ content: string; id?: string; image?: Array<{ id: string; path: string }>; }> >([{ content: '' }]); const fetch = useFetch(); const updateOrder = useCallback(() => { modal.closeAll(); reopenModal(); }, [reopenModal, modal]); // prevent the window exit by mistake usePreventWindowUnload(true); // hook to move the settings in the right place to fix missing fields const moveToIntegration = useMoveToIntegration(); // hook to test if the top editor should be hidden const showHide = useHideTopEditor(); const [showError, setShowError] = useState(false); // are we in edit mode? const existingData = useExistingData(); // Post for const [postFor, setPostFor] = useState(); const expend = useExpend(); const toaster = useToaster(); // if it's edit just set the current integration useEffect(() => { if (existingData.integration) { setSelectedIntegrations([ integrations.find((p) => p.id === existingData.integration)!, ]); } }, [existingData.integration]); // if the user exit the popup we reset the global variable with all the values useEffect(() => { return () => { resetValues(); }; }, []); // Change the value of the global editor const changeValue = useCallback( (index: number) => (newValue: string) => { return setValue((prev) => { prev[index].content = newValue; return [...prev]; }); }, [value] ); const changeImage = useCallback( (index: number) => (newValue: { target: { name: string; value?: Array<{ id: string; path: string }> }; }) => { return setValue((prev) => { return prev.map((p, i) => { if (i === index) { return { ...p, image: newValue.target.value }; } return p; }); }); }, [] ); // Add another editor const addValue = useCallback( (index: number) => () => { setValue((prev) => { return prev.reduce((acc, p, i) => { acc.push(p); if (i === index) { acc.push({ content: '' }); } return acc; }, [] as Array<{ content: string }>); }); }, [] ); const changePosition = useCallback( (index: number) => (type: 'up' | 'down') => { if (type === 'up' && index !== 0) { setValue((prev) => { return arrayMoveImmutable(prev, index, index - 1); }); } else if (type === 'down') { setValue((prev) => { return arrayMoveImmutable(prev, index, index + 1); }); } }, [] ); // Delete post const deletePost = useCallback( (index: number) => async () => { if ( !(await deleteDialog( 'Are you sure you want to delete this post?', 'Yes, delete it!' )) ) { return; } setValue((prev) => { prev.splice(index, 1); return [...prev]; }); }, [value] ); // override the close modal to ask the user if he is sure to close const askClose = useCallback(async () => { if ( await deleteDialog( 'Are you sure you want to close this modal? (all data will be lost)', 'Yes, close it!' ) ) { modal.closeAll(); } }, []); // sometimes it's easier to click escape to close useKeypress('Escape', askClose); const postNow = useCallback( ((e) => { e.stopPropagation(); e.preventDefault(); return schedule('now')(); }) as MouseEventHandler, [] ); // function to send to the server and save const schedule = useCallback( (type: 'draft' | 'now' | 'schedule' | 'delete') => async () => { if (type === 'delete') { if ( !(await deleteDialog( 'Are you sure you want to delete this post?', 'Yes, delete it!' )) ) { return; } await fetch(`/posts/${existingData.group}`, { method: 'DELETE', }); mutate('/posts'); modal.closeAll(); return; } const values = getValues(); const allKeys = Object.keys(values).map((v) => ({ integration: integrations.find((p) => p.id === v), value: values[v].posts, valid: values[v].isValid, group: existingData?.group, trigger: values[v].trigger, settings: values[v].settings(), firstCommentRequirements: values[v].firstCommentRequirements, maximumMediaRequirements: values[v].maximumMediaRequirements, minimumMediaRequirements: values[v].minimumMediaRequirements, })); for (const key of allKeys) { // @ts-ignore const images = key?.value[0].image; if ( (images?.length || 0) > (key.maximumMediaRequirements || 0) || (images?.length || 0) < (key.minimumMediaRequirements || 0) ) { toaster.show( `The amount of ${capitalize(key?.integration?.identifier)} media attached supposed to be ${ key.maximumMediaRequirements === key.minimumMediaRequirements ? key.minimumMediaRequirements : `between ${key.minimumMediaRequirements} to ${key.maximumMediaRequirements}` }`, 'warning' ); return; } if ( key.firstCommentRequirements && !images?.every((p: any) => key.firstCommentRequirements === 'video' ? p.name.includes('mp4') : !p.name.includes('mp4') ) ) { toaster.show( `${capitalize(key?.integration?.identifier?.toUpperCase())} media should be a ${ key.firstCommentRequirements === 'video' ? 'video' : 'image' }`, 'warning' ); return; } if (key.value.some((p) => !p.content || p.content.length < 6)) { setShowError(true); return; } if (!key.valid) { await key.trigger(); moveToIntegration(key?.integration?.id!); return; } } await fetch('/posts', { method: 'POST', body: JSON.stringify({ ...(postFor ? { order: postFor.id } : {}), type, date: dateState.utc().format('YYYY-MM-DDTHH:mm:ss'), posts: allKeys, }), }); existingData.group = uuidv4(); mutate('/posts'); toaster.show( !existingData.integration ? 'Added successfully' : 'Updated successfully' ); modal.closeAll(); }, [postFor, dateState, value, integrations, existingData] ); const getPostsMarketplace = useCallback(async () => { return ( await fetch(`/posts/marketplace/${existingData?.posts?.[0]?.id}`) ).json(); }, []); const { data } = useSWR( `/posts/marketplace/${existingData?.posts?.[0]?.id}`, getPostsMarketplace ); const canSendForPublication = useMemo(() => { if (!postFor) { return true; } return selectedIntegrations.every((integration) => { const find = postFor.missing.find( (p) => p.integration.integration.id === integration.id ); if (!find) { return false; } return find.missing !== 0; }); }, [data, postFor, selectedIntegrations]); return ( <>
{!existingData.integration && ( )}
{!existingData.integration && !showHide.hideTopEditor ? ( <>
You are in global editing mode
{value.map((p, index) => (
1 ? 150 : 250} commands={[ ...commands .getCommands() .filter((f) => f.name !== 'image'), newImage, postSelector(dateState), ]} value={p.content} preview="edit" // @ts-ignore onChange={changeValue(index)} /> {showError && (!p.content || p.content.length < 6) && (
The post should be at least 6 characters long
)}
{value.length > 1 && (
Delete Post
)}
))} ) : null}
{!!existingData.integration && ( )}
{!!selectedIntegrations.length && (
)}
); };