mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
feat: banner modal (#5132)
https://linear.app/unleash/issue/2-1548/ui-create-banner-newedit-modal Adds a new banner modal (and form) that allows admins to create and edit banners. Also adds a new `FormSwitch` common component that may be helpful in different situations where we need a switch on a form. <img width="1263" alt="image" src="https://github.com/Unleash/unleash/assets/14320932/1b89db9b-9003-413c-8829-c37d245e2487">
This commit is contained in:
parent
898c1b4bc7
commit
3ca22c7c5c
343
frontend/src/component/admin/banners/BannerModal/BannerForm.tsx
Normal file
343
frontend/src/component/admin/banners/BannerModal/BannerForm.tsx
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { Banner } from 'component/banners/Banner/Banner';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { FormSwitch } from 'component/common/FormSwitch/FormSwitch';
|
||||||
|
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
||||||
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
|
import Input from 'component/common/Input/Input';
|
||||||
|
import { BannerVariant } from 'interfaces/banner';
|
||||||
|
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
|
||||||
|
const StyledForm = styled('form')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(4),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledFieldGroup = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledInput = styled(Input)(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: theme.spacing(50),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTooltip = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(0.5),
|
||||||
|
gap: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: theme.spacing(50),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const VARIANT_OPTIONS = [
|
||||||
|
{ key: 'info', label: 'Information' },
|
||||||
|
{ key: 'warning', label: 'Warning' },
|
||||||
|
{ key: 'error', label: 'Error' },
|
||||||
|
{ key: 'success', label: 'Success' },
|
||||||
|
];
|
||||||
|
|
||||||
|
type IconOption = 'Default' | 'Custom' | 'None';
|
||||||
|
type LinkOption = 'Link' | 'Dialog' | 'None';
|
||||||
|
|
||||||
|
interface IBannerFormProps {
|
||||||
|
enabled: boolean;
|
||||||
|
message: string;
|
||||||
|
variant: BannerVariant;
|
||||||
|
sticky: boolean;
|
||||||
|
icon: string;
|
||||||
|
link: string;
|
||||||
|
linkText: string;
|
||||||
|
dialogTitle: string;
|
||||||
|
dialog: string;
|
||||||
|
setEnabled: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setMessage: Dispatch<SetStateAction<string>>;
|
||||||
|
setVariant: Dispatch<SetStateAction<BannerVariant>>;
|
||||||
|
setSticky: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIcon: Dispatch<SetStateAction<string>>;
|
||||||
|
setLink: Dispatch<SetStateAction<string>>;
|
||||||
|
setLinkText: Dispatch<SetStateAction<string>>;
|
||||||
|
setDialogTitle: Dispatch<SetStateAction<string>>;
|
||||||
|
setDialog: Dispatch<SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BannerForm = ({
|
||||||
|
enabled,
|
||||||
|
message,
|
||||||
|
variant,
|
||||||
|
sticky,
|
||||||
|
icon,
|
||||||
|
link,
|
||||||
|
linkText,
|
||||||
|
dialogTitle,
|
||||||
|
dialog,
|
||||||
|
setEnabled,
|
||||||
|
setMessage,
|
||||||
|
setVariant,
|
||||||
|
setSticky,
|
||||||
|
setIcon,
|
||||||
|
setLink,
|
||||||
|
setLinkText,
|
||||||
|
setDialogTitle,
|
||||||
|
setDialog,
|
||||||
|
}: IBannerFormProps) => {
|
||||||
|
const [iconOption, setIconOption] = useState<IconOption>(
|
||||||
|
icon === '' ? 'Default' : icon === 'none' ? 'None' : 'Custom',
|
||||||
|
);
|
||||||
|
const [linkOption, setLinkOption] = useState<LinkOption>(
|
||||||
|
link === '' ? 'None' : link === 'dialog' ? 'Dialog' : 'Link',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<StyledInputDescription>Preview:</StyledInputDescription>
|
||||||
|
<Banner
|
||||||
|
banner={{
|
||||||
|
message:
|
||||||
|
message ||
|
||||||
|
'*No message set. Please enter a message below.*',
|
||||||
|
variant,
|
||||||
|
sticky: false,
|
||||||
|
icon,
|
||||||
|
link,
|
||||||
|
linkText,
|
||||||
|
dialogTitle,
|
||||||
|
dialog,
|
||||||
|
}}
|
||||||
|
inline
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What is your banner message?
|
||||||
|
<HelpIcon
|
||||||
|
tooltip={
|
||||||
|
<StyledTooltip>
|
||||||
|
<p>Markdown is supported.</p>
|
||||||
|
</StyledTooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledInput
|
||||||
|
autoFocus
|
||||||
|
label='Banner message'
|
||||||
|
value={message}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setMessage(e.target.value)
|
||||||
|
}
|
||||||
|
autoComplete='off'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What type of banner is it?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledSelect
|
||||||
|
size='small'
|
||||||
|
value={variant}
|
||||||
|
onChange={(variant) => setVariant(variant as BannerVariant)}
|
||||||
|
options={VARIANT_OPTIONS}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What icon should be displayed on the banner?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledSelect
|
||||||
|
size='small'
|
||||||
|
value={iconOption}
|
||||||
|
onChange={(iconOption) => {
|
||||||
|
setIconOption(iconOption as IconOption);
|
||||||
|
if (iconOption === 'None') {
|
||||||
|
setIcon('none');
|
||||||
|
} else {
|
||||||
|
setIcon('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={['Default', 'Custom', 'None'].map((option) => ({
|
||||||
|
key: option,
|
||||||
|
label: option,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={iconOption === 'Custom'}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What custom icon should be displayed?
|
||||||
|
<HelpIcon
|
||||||
|
tooltip={
|
||||||
|
<StyledTooltip>
|
||||||
|
<p>
|
||||||
|
Choose an icon from{' '}
|
||||||
|
<a
|
||||||
|
href='https://fonts.google.com/icons'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
Material Symbols
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For example, if you want to
|
||||||
|
display the "Rocket Launch"
|
||||||
|
icon, you can enter
|
||||||
|
"rocket_launch" in the field
|
||||||
|
below.
|
||||||
|
</p>
|
||||||
|
</StyledTooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledInput
|
||||||
|
label='Banner icon'
|
||||||
|
value={icon}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setIcon(e.target.value)
|
||||||
|
}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What action should be available in the banner?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledSelect
|
||||||
|
size='small'
|
||||||
|
value={linkOption}
|
||||||
|
onChange={(linkOption) => {
|
||||||
|
setLinkOption(linkOption as LinkOption);
|
||||||
|
if (linkOption === 'Dialog') {
|
||||||
|
setLink('dialog');
|
||||||
|
} else {
|
||||||
|
setLink('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={['None', 'Link', 'Dialog'].map((option) => ({
|
||||||
|
key: option,
|
||||||
|
label: option,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={linkOption === 'Link'}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What URL should be opened?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledInput
|
||||||
|
label='URL'
|
||||||
|
value={link}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setLink(e.target.value)
|
||||||
|
}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!linkText) setLinkText(link);
|
||||||
|
}}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={linkOption !== 'None'}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What is the action text?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledInput
|
||||||
|
label='Action text'
|
||||||
|
value={linkText}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setLinkText(e.target.value)
|
||||||
|
}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={linkOption === 'Dialog'}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What is the dialog title?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledInput
|
||||||
|
label='Dialog title'
|
||||||
|
value={dialogTitle}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setDialogTitle(e.target.value)
|
||||||
|
}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
<StyledInputDescription>
|
||||||
|
What is the dialog content?
|
||||||
|
<HelpIcon
|
||||||
|
tooltip={
|
||||||
|
<StyledTooltip>
|
||||||
|
<p>Markdown is supported.</p>
|
||||||
|
</StyledTooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledInputDescription>
|
||||||
|
<StyledInput
|
||||||
|
label='Dialog content'
|
||||||
|
multiline
|
||||||
|
minRows={4}
|
||||||
|
value={dialog}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setDialog(e.target.value)
|
||||||
|
}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<StyledInputDescription>
|
||||||
|
Is the banner sticky on the screen when scrolling?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<FormSwitch
|
||||||
|
checked={sticky}
|
||||||
|
setChecked={setSticky}
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'start',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<StyledInputDescription>
|
||||||
|
Is the banner currently visible to all users?
|
||||||
|
</StyledInputDescription>
|
||||||
|
<FormSwitch
|
||||||
|
checked={enabled}
|
||||||
|
setChecked={setEnabled}
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'start',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
};
|
173
frontend/src/component/admin/banners/BannerModal/BannerModal.tsx
Normal file
173
frontend/src/component/admin/banners/BannerModal/BannerModal.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { Button, styled } from '@mui/material';
|
||||||
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { FormEvent, useEffect, useState } from 'react';
|
||||||
|
import { BannerVariant, IInternalBanner } from 'interfaces/banner';
|
||||||
|
import { useBanners } from 'hooks/api/getters/useBanners/useBanners';
|
||||||
|
import {
|
||||||
|
AddOrUpdateBanner,
|
||||||
|
useBannersApi,
|
||||||
|
} from 'hooks/api/actions/useMessageBannersApi/useMessageBannersApi';
|
||||||
|
import { BannerForm } from './BannerForm';
|
||||||
|
|
||||||
|
const StyledForm = styled('form')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||||
|
marginTop: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
paddingTop: theme.spacing(4),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IBannerModalProps {
|
||||||
|
banner?: IInternalBanner;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BannerModal = ({ banner, open, setOpen }: IBannerModalProps) => {
|
||||||
|
const { refetch } = useBanners();
|
||||||
|
const { addBanner, updateBanner, loading } = useBannersApi();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [variant, setVariant] = useState<BannerVariant>('info');
|
||||||
|
const [sticky, setSticky] = useState(false);
|
||||||
|
const [icon, setIcon] = useState('');
|
||||||
|
const [link, setLink] = useState('');
|
||||||
|
const [linkText, setLinkText] = useState('');
|
||||||
|
const [dialogTitle, setDialogTitle] = useState('');
|
||||||
|
const [dialog, setDialog] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEnabled(banner?.enabled ?? true);
|
||||||
|
setMessage(banner?.message || '');
|
||||||
|
setVariant(banner?.variant || 'info');
|
||||||
|
setSticky(banner?.sticky || false);
|
||||||
|
setIcon(banner?.icon || '');
|
||||||
|
setLink(banner?.link || '');
|
||||||
|
setLinkText(banner?.linkText || '');
|
||||||
|
setDialogTitle(banner?.dialogTitle || '');
|
||||||
|
setDialog(banner?.dialog || '');
|
||||||
|
}, [open, banner]);
|
||||||
|
|
||||||
|
const editing = banner !== undefined;
|
||||||
|
const title = editing ? 'Edit banner' : 'New banner';
|
||||||
|
const isValid = message.length;
|
||||||
|
|
||||||
|
const payload: AddOrUpdateBanner = {
|
||||||
|
message,
|
||||||
|
variant,
|
||||||
|
icon,
|
||||||
|
link,
|
||||||
|
linkText,
|
||||||
|
dialogTitle,
|
||||||
|
dialog,
|
||||||
|
sticky,
|
||||||
|
enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatApiCode = () => {
|
||||||
|
return `curl --location --request ${editing ? 'PUT' : 'POST'} '${
|
||||||
|
uiConfig.unleashUrl
|
||||||
|
}/api/admin/banners${editing ? `/${banner.id}` : ''}' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editing) {
|
||||||
|
await updateBanner(banner.id, payload);
|
||||||
|
} else {
|
||||||
|
await addBanner(payload);
|
||||||
|
}
|
||||||
|
setToastData({
|
||||||
|
title: `Banner ${editing ? 'updated' : 'added'} successfully`,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarModal
|
||||||
|
open={open}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
label={title}
|
||||||
|
>
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
modal
|
||||||
|
title={title}
|
||||||
|
description='Banners allow you to display messages to other users inside your Unleash instance.'
|
||||||
|
documentationLink='https://docs.getunleash.io/reference/banners'
|
||||||
|
documentationLinkLabel='Banners documentation'
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<StyledForm onSubmit={onSubmit}>
|
||||||
|
<BannerForm
|
||||||
|
enabled={enabled}
|
||||||
|
message={message}
|
||||||
|
variant={variant}
|
||||||
|
sticky={sticky}
|
||||||
|
icon={icon}
|
||||||
|
link={link}
|
||||||
|
linkText={linkText}
|
||||||
|
dialogTitle={dialogTitle}
|
||||||
|
dialog={dialog}
|
||||||
|
setEnabled={setEnabled}
|
||||||
|
setMessage={setMessage}
|
||||||
|
setVariant={setVariant}
|
||||||
|
setSticky={setSticky}
|
||||||
|
setIcon={setIcon}
|
||||||
|
setLink={setLink}
|
||||||
|
setLinkText={setLinkText}
|
||||||
|
setDialogTitle={setDialogTitle}
|
||||||
|
setDialog={setDialog}
|
||||||
|
/>
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
disabled={!isValid}
|
||||||
|
>
|
||||||
|
{editing ? 'Save' : 'Add'} banner
|
||||||
|
</Button>
|
||||||
|
<StyledCancelButton
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</StyledCancelButton>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
</StyledForm>
|
||||||
|
</FormTemplate>
|
||||||
|
</SidebarModal>
|
||||||
|
);
|
||||||
|
};
|
@ -23,6 +23,7 @@ import { BannersActionsCell } from './BannersActionsCell';
|
|||||||
import { BannerDeleteDialog } from './BannerDeleteDialog';
|
import { BannerDeleteDialog } from './BannerDeleteDialog';
|
||||||
import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
|
import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
|
||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
|
import { BannerModal } from '../BannerModal/BannerModal';
|
||||||
|
|
||||||
export const BannersTable = () => {
|
export const BannersTable = () => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
@ -234,11 +235,11 @@ export const BannersTable = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* <BannerModal
|
<BannerModal
|
||||||
banner={selectedBanner}
|
banner={selectedBanner}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
setOpen={setModalOpen}
|
setOpen={setModalOpen}
|
||||||
/> */}
|
/>
|
||||||
<BannerDeleteDialog
|
<BannerDeleteDialog
|
||||||
banner={selectedBanner}
|
banner={selectedBanner}
|
||||||
open={deleteOpen}
|
open={deleteOpen}
|
||||||
|
@ -22,7 +22,13 @@ const StyledBar = styled('aside', {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: theme.spacing(1),
|
padding: theme.spacing(1),
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
borderBottom: inline ? 'none' : '1px solid',
|
...(inline
|
||||||
|
? {
|
||||||
|
border: '1px solid',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
}),
|
||||||
borderColor: theme.palette[variant].border,
|
borderColor: theme.palette[variant].border,
|
||||||
background: theme.palette[variant].light,
|
background: theme.palette[variant].light,
|
||||||
color: theme.palette[variant].dark,
|
color: theme.palette[variant].dark,
|
||||||
|
45
frontend/src/component/common/FormSwitch/FormSwitch.tsx
Normal file
45
frontend/src/component/common/FormSwitch/FormSwitch.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Box, BoxProps, FormControlLabel, Switch, styled } from '@mui/material';
|
||||||
|
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||||
|
|
||||||
|
const StyledContainer = styled(Box)({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledSwitchSpan = styled('span')(({ theme }) => ({
|
||||||
|
marginLeft: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IFormSwitchProps extends BoxProps {
|
||||||
|
checked: boolean;
|
||||||
|
setChecked: Dispatch<SetStateAction<boolean>>;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormSwitch = ({
|
||||||
|
checked,
|
||||||
|
setChecked,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: IFormSwitchProps) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer {...props}>
|
||||||
|
{children}
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => setChecked(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<StyledSwitchSpan>
|
||||||
|
{checked ? 'Enabled' : 'Disabled'}
|
||||||
|
</StyledSwitchSpan>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -3,7 +3,7 @@ import useAPI from '../useApi/useApi';
|
|||||||
|
|
||||||
const ENDPOINT = 'api/admin/banners';
|
const ENDPOINT = 'api/admin/banners';
|
||||||
|
|
||||||
type AddOrUpdateBanner = Omit<IInternalBanner, 'id' | 'createdAt'>;
|
export type AddOrUpdateBanner = Omit<IInternalBanner, 'id' | 'createdAt'>;
|
||||||
|
|
||||||
export const useBannersApi = () => {
|
export const useBannersApi = () => {
|
||||||
const { loading, makeRequest, createRequest, errors } = useAPI({
|
const { loading, makeRequest, createRequest, errors } = useAPI({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export type BannerVariant = 'warning' | 'info' | 'error' | 'success';
|
export type BannerVariant = 'info' | 'warning' | 'error' | 'success';
|
||||||
|
|
||||||
export interface IBanner {
|
export interface IBanner {
|
||||||
message: string;
|
message: string;
|
||||||
@ -12,7 +12,7 @@ export interface IBanner {
|
|||||||
dialog?: string;
|
dialog?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInternalBanner extends IBanner {
|
export interface IInternalBanner extends Omit<IBanner, 'plausibleEvent'> {
|
||||||
id: number;
|
id: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user