postiz/apps/frontend/src/components/autopost/autopost.tsx

442 lines
14 KiB
TypeScript

import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { Button } from '@gitroom/react/form/button';
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { Input } from '@gitroom/react/form/input';
import { FormProvider, useForm } from 'react-hook-form';
import { array, boolean, object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { Select } from '@gitroom/react/form/select';
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
import { useToaster } from '@gitroom/react/toaster/toaster';
import clsx from 'clsx';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { CopilotTextarea } from '@copilotkit/react-textarea';
import { Slider } from '@gitroom/react/form/slider';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const Autopost: FC = () => {
const fetch = useFetch();
const t = useT();
const modal = useModals();
const toaster = useToaster();
const list = useCallback(async () => {
return (await fetch('/autopost')).json();
}, []);
const { data, mutate } = useSWR('autopost', list);
const addWebhook = useCallback(
(data?: any) => () => {
modal.openModal({
title: data ? 'Edit Autopost' : 'Add Autopost',
withCloseButton: true,
children: <AddOrEditWebhook data={data} reload={mutate} />,
});
},
[]
);
const deleteHook = useCallback(
(data: any) => async () => {
if (
await deleteDialog(
t(
'are_you_sure_you_want_to_delete',
`Are you sure you want to delete ${data.name}?`,
{ name: data.name }
)
)
) {
await fetch(`/autopost/${data.id}`, {
method: 'DELETE',
});
mutate();
toaster.show('Webhook deleted successfully', 'success');
}
},
[]
);
const changeActive = useCallback(
(data: any) => async (ac: 'on' | 'off') => {
await fetch(`/autopost/${data.id}/active`, {
body: JSON.stringify({
active: ac === 'on',
}),
method: 'POST',
});
mutate();
},
[mutate]
);
return (
<div className="flex flex-col">
<h3 className="text-[20px]">{t('autopost', 'Autopost')}</h3>
<div className="text-customColor18 mt-[4px]">
{t(
'autopost_can_automatically_posts_your_rss_new_items_to_social_media',
'Autopost can automatically posts your RSS new items to social media'
)}
</div>
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
<div className="flex flex-col w-full">
{!!data?.length && (
<div className="grid grid-cols-[1fr,1fr,1fr,1fr,1fr] w-full gap-y-[10px]">
<div>{t('title', 'Title')}</div>
<div>{t('url', 'URL')}</div>
<div>{t('edit', 'Edit')}</div>
<div>{t('delete', 'Delete')}</div>
<div>{t('active', 'Active')}</div>
{data?.map((p: any) => (
<Fragment key={p.id}>
<div className="flex flex-col justify-center">{p.title}</div>
<div className="flex flex-col justify-center">{p.url}</div>
<div className="flex flex-col justify-center">
<div>
<Button onClick={addWebhook(p)}>
{t('edit', 'Edit')}
</Button>
</div>
</div>
<div className="flex flex-col justify-center">
<div>
<Button onClick={deleteHook(p)}>
{t('delete', 'Delete')}
</Button>
</div>
</div>
<div>
<Slider
value={p.active ? 'on' : 'off'}
onChange={changeActive(p)}
fill={true}
/>
</div>
</Fragment>
))}
</div>
)}
<div>
<Button
onClick={addWebhook()}
className={clsx((data?.length || 0) > 0 && 'my-[16px]')}
>
{t('add_an_autopost', 'Add an autopost')}
</Button>
</div>
</div>
</div>
</div>
);
};
const details = object().shape({
title: string().required(),
content: string(),
onSlot: boolean().required(),
syncLast: boolean().required(),
url: string().url().required(),
active: boolean().required(),
addPicture: boolean().required(),
generateContent: boolean().required(),
integrations: array().of(
object().shape({
id: string().required(),
})
),
});
const options = [
{
label: 'All integrations',
value: 'all',
},
{
label: 'Specific integrations',
value: 'specific',
},
];
const optionsChoose = [
{
label: 'Yes',
value: true,
},
{
label: 'No',
value: false,
},
];
const postImmediately = [
{
label: 'Post on the next available slot',
value: true,
},
{
label: 'Post Immediately',
value: false,
},
];
export const AddOrEditWebhook: FC<{
data?: any;
reload: () => void;
}> = (props) => {
const { data, reload } = props;
const fetch = useFetch();
const [allIntegrations, setAllIntegrations] = useState(
(JSON.parse(data?.integrations || '[]')?.length || 0) > 0
? options[1]
: options[0]
);
const modal = useModals();
const toast = useToaster();
const [valid, setValid] = useState(data?.url || '');
const [lastUrl, setLastUrl] = useState(data?.lastUrl || '');
const form = useForm({
resolver: yupResolver(details),
values: {
title: data?.title || '',
content: data?.content || '',
onSlot: data?.onSlot || false,
syncLast: data?.syncLast || false,
url: data?.url || '',
// eslint-disable-next-line no-prototype-builtins
active: data?.hasOwnProperty?.('active') ? data?.active : true,
addPicture: data?.addPicture || false,
// eslint-disable-next-line no-prototype-builtins
generateContent: data?.hasOwnProperty?.('generateContent')
? data?.generateContent
: true,
integrations: JSON.parse(data?.integrations || '[]') || [],
},
});
const generateContent = form.watch('generateContent');
const content = form.watch('content');
const url = form.watch('url');
const syncLast = form.watch('syncLast');
const integrations = form.watch('integrations');
const integration = useCallback(async () => {
return (await fetch('/integrations/list')).json();
}, []);
const changeIntegration = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const findValue = options.find(
(option) => option.value === e.target.value
)!;
setAllIntegrations(findValue);
if (findValue.value === 'all') {
form.setValue('integrations', []);
}
},
[]
);
const { data: dataList, isLoading } = useSWR('integrations', integration, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
revalidateOnMount: true,
refreshWhenHidden: false,
refreshWhenOffline: false,
});
const callBack = useCallback(
async (values: any) => {
await fetch(data?.id ? `/autopost/${data?.id}` : '/autopost', {
method: data?.id ? 'PUT' : 'POST',
body: JSON.stringify({
...(data?.id
? {
id: data.id,
}
: {}),
...values,
...(!syncLast
? {
lastUrl,
}
: {
lastUrl: '',
}),
}),
});
toast.show(
data?.id
? 'Autopost updated successfully'
: 'Autopost added successfully',
'success'
);
modal.closeAll();
reload();
},
[data, integrations, lastUrl, syncLast]
);
const sendTest = useCallback(async () => {
const url = form.getValues('url');
try {
const { success, url: newUrl } = await (
await fetch(`/autopost/send?url=${encodeURIComponent(url)}`, {
method: 'POST',
headers: {
contentType: 'application/json',
},
})
).json();
if (!success) {
setValid('');
toast.show('Could not use this RSS feed', 'warning');
return;
}
toast.show('RSS valid!', 'success');
setValid(url);
setLastUrl(newUrl);
} catch (e: any) {
/** empty **/
}
}, []);
const t = useT();
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(callBack)}>
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 pt-0">
<div>
<Input
label="Title"
translationKey="label_title"
{...form.register('title')}
/>
<Input
label="URL"
translationKey="label_url"
{...form.register('url')}
/>
<Select
label="Should we sync the current last post?"
translationKey="label_should_sync_last_post"
{...form.register('syncLast', {
setValueAs: (value) => {
return value === 'true' || value === true;
},
})}
>
{optionsChoose.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</Select>
<Select
label="When should we post it?"
translationKey="label_when_post"
{...form.register('onSlot', {
setValueAs: (value) => value === 'true' || value === true,
})}
>
{postImmediately.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</Select>
<Select
label="Autogenerate content"
translationKey="label_autogenerate_content"
{...form.register('generateContent', {
setValueAs: (value) => value === 'true' || value === true,
})}
>
{optionsChoose.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</Select>
{!generateContent && (
<>
<div className={`text-[14px] mb-[6px]`}>
{t('post_content', 'Post content')}
</div>
<CopilotTextarea
disableBranding={true}
className={clsx(
'!min-h-40 !max-h-80 p-2 overflow-x-hidden scrollbar scrollbar-thumb-[#612AD5] bg-customColor2 outline-none mb-[16px] border-fifth border rounded-[4px]'
)}
value={content}
onChange={(e) => {
form.setValue('content', e.target.value);
}}
placeholder="Write your post..."
autosuggestionsConfig={{
textareaPurpose: `Assist me in writing social media post`,
chatApiConfigs: {},
}}
/>
</>
)}
<Select
label="Generate Picture?"
translationKey="label_generate_picture"
{...form.register('addPicture', {
setValueAs: (value) => value === 'true' || value === true,
})}
>
{optionsChoose.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</Select>
<Select
value={allIntegrations.value}
name="integrations"
label="Integrations"
translationKey="label_integrations"
disableForm={true}
onChange={changeIntegration}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
{allIntegrations.value === 'specific' && dataList && !isLoading && (
<PickPlatforms
integrations={dataList.integrations}
selectedIntegrations={integrations as any[]}
onChange={(e) => form.setValue('integrations', e)}
singleSelect={false}
toolTip={true}
isMain={true}
/>
)}
<div className="flex gap-[10px]">
{valid === url && (syncLast || !!lastUrl) && (
<Button
type="submit"
className="mt-[24px]"
disabled={
valid !== url ||
!form.formState.isValid ||
(allIntegrations.value === 'specific' &&
!integrations?.length)
}
>
{t('save', 'Save')}
</Button>
)}
<Button
type="button"
className="mt-[24px]"
onClick={sendTest}
disabled={
!form.formState.isValid ||
(allIntegrations.value === 'specific' &&
!integrations?.length)
}
>
{t('send_test', 'Send Test')}
</Button>
</div>
</div>
</div>
</form>
</FormProvider>
);
};