2022-01-02 10:46:07 +00:00
|
|
|
import { PlusIcon } from '@radix-ui/react-icons';
|
2022-01-07 08:41:53 +00:00
|
|
|
import React, { useState } from 'react';
|
2022-01-02 10:46:07 +00:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2022-01-07 08:41:53 +00:00
|
|
|
import { useDispatch } from 'react-redux';
|
|
|
|
import { useModule } from '../../lib/react-utils';
|
|
|
|
import { modules } from '../../store/api/reducer';
|
|
|
|
import {
|
|
|
|
accessLevels,
|
|
|
|
AccessLevelType,
|
|
|
|
TwitchBotCustomCommand,
|
|
|
|
} from '../../store/api/types';
|
|
|
|
import AlertContent from '../components/AlertContent';
|
|
|
|
import DialogContent from '../components/DialogContent';
|
2022-01-02 10:46:07 +00:00
|
|
|
import {
|
|
|
|
Button,
|
2022-01-07 08:41:53 +00:00
|
|
|
ComboBox,
|
|
|
|
Dialog,
|
|
|
|
DialogActions,
|
|
|
|
DialogClose,
|
|
|
|
Field,
|
|
|
|
FieldNote,
|
2022-01-02 10:46:07 +00:00
|
|
|
FlexRow,
|
|
|
|
InputBox,
|
2022-01-07 08:41:53 +00:00
|
|
|
Label,
|
2022-01-10 10:19:54 +00:00
|
|
|
MultiButton,
|
2022-01-02 10:46:07 +00:00
|
|
|
PageContainer,
|
|
|
|
PageHeader,
|
|
|
|
PageTitle,
|
2022-01-07 08:41:53 +00:00
|
|
|
styled,
|
|
|
|
Textarea,
|
2022-01-02 10:46:07 +00:00
|
|
|
TextBlock,
|
|
|
|
} from '../theme';
|
2022-01-07 08:41:53 +00:00
|
|
|
import { Alert, AlertTrigger } from '../theme/alert';
|
|
|
|
|
|
|
|
const CommandList = styled('div', { marginTop: '1rem' });
|
|
|
|
const CommandItemContainer = styled('article', {
|
|
|
|
backgroundColor: '$gray2',
|
|
|
|
margin: '0.5rem 0',
|
|
|
|
padding: '0.5rem',
|
|
|
|
borderLeft: '5px solid $teal8',
|
|
|
|
borderRadius: '0.25rem',
|
|
|
|
borderBottom: '1px solid $gray4',
|
|
|
|
transition: 'all 50ms',
|
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: '$gray3',
|
|
|
|
},
|
|
|
|
variants: {
|
|
|
|
status: {
|
|
|
|
enabled: {},
|
|
|
|
disabled: {
|
2022-01-12 12:20:30 +00:00
|
|
|
borderLeftColor: '$red6',
|
2022-01-07 08:41:53 +00:00
|
|
|
backgroundColor: '$gray3',
|
|
|
|
color: '$gray10',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const CommandHeader = styled('header', {
|
|
|
|
display: 'flex',
|
|
|
|
gap: '0.5rem',
|
|
|
|
alignItems: 'center',
|
|
|
|
marginBottom: '0.4rem',
|
|
|
|
});
|
|
|
|
const CommandName = styled('span', {
|
|
|
|
color: '$teal10',
|
|
|
|
fontWeight: 'bold',
|
|
|
|
variants: {
|
|
|
|
status: {
|
|
|
|
enabled: {},
|
|
|
|
disabled: {
|
2022-01-12 12:20:30 +00:00
|
|
|
color: '$gray9',
|
2022-01-07 08:41:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const CommandDescription = styled('span', {
|
|
|
|
flex: 1,
|
|
|
|
});
|
|
|
|
const CommandActions = styled('div', {
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'center',
|
|
|
|
gap: '0.25rem',
|
|
|
|
});
|
|
|
|
const CommandText = styled('div', {
|
|
|
|
fontFamily: 'Space Mono',
|
|
|
|
fontSize: '10pt',
|
|
|
|
margin: '-0.5rem',
|
|
|
|
marginTop: '0',
|
|
|
|
padding: '0.5rem',
|
|
|
|
backgroundColor: '$gray4',
|
|
|
|
lineHeight: '1.2rem',
|
|
|
|
});
|
|
|
|
const ACLIndicator = styled('span', {
|
|
|
|
fontFamily: 'Space Mono',
|
|
|
|
fontSize: '10pt',
|
|
|
|
marginRight: '0.5rem',
|
|
|
|
});
|
|
|
|
|
|
|
|
interface CommandItemProps {
|
|
|
|
name: string;
|
|
|
|
item: TwitchBotCustomCommand;
|
|
|
|
onToggle?: () => void;
|
|
|
|
onEdit?: () => void;
|
|
|
|
onDelete?: () => void;
|
|
|
|
}
|
|
|
|
|
2022-01-10 10:19:54 +00:00
|
|
|
function CommandItem({
|
2022-01-07 08:41:53 +00:00
|
|
|
name,
|
|
|
|
item,
|
|
|
|
onToggle,
|
|
|
|
onEdit,
|
|
|
|
onDelete,
|
|
|
|
}: CommandItemProps): React.ReactElement {
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
|
|
return (
|
|
|
|
<CommandItemContainer status={item.enabled ? 'enabled' : 'disabled'}>
|
|
|
|
<CommandHeader>
|
|
|
|
<CommandName status={item.enabled ? 'enabled' : 'disabled'}>
|
|
|
|
{name}
|
|
|
|
</CommandName>
|
|
|
|
<CommandDescription>{item.description}</CommandDescription>
|
|
|
|
<CommandActions>
|
|
|
|
{item.access_level !== 'everyone' && (
|
|
|
|
<ACLIndicator>
|
|
|
|
{t(`pages.botcommands.acl.${item.access_level}`)}
|
|
|
|
{item.access_level !== 'streamer' && '+'}
|
|
|
|
</ACLIndicator>
|
|
|
|
)}
|
2022-01-10 10:19:54 +00:00
|
|
|
<MultiButton>
|
|
|
|
<Button
|
|
|
|
styling="multi"
|
|
|
|
size="small"
|
|
|
|
onClick={() => (onToggle ? onToggle() : null)}
|
|
|
|
>
|
|
|
|
{t(item.enabled ? 'form-actions.disable' : 'form-actions.enable')}
|
|
|
|
</Button>
|
|
|
|
<Button
|
|
|
|
styling="multi"
|
|
|
|
size="small"
|
|
|
|
onClick={() => (onEdit ? onEdit() : null)}
|
|
|
|
>
|
|
|
|
{t('form-actions.edit')}
|
|
|
|
</Button>
|
|
|
|
<Alert>
|
|
|
|
<AlertTrigger asChild>
|
|
|
|
<Button styling="multi" size="small">
|
|
|
|
{t('form-actions.delete')}
|
|
|
|
</Button>
|
|
|
|
</AlertTrigger>
|
|
|
|
<AlertContent
|
|
|
|
variation="danger"
|
|
|
|
title={t('pages.botcommands.remove-command-title', { name })}
|
|
|
|
description="This cannot be undone"
|
|
|
|
actionText="Delete"
|
|
|
|
actionButtonProps={{ variation: 'danger' }}
|
|
|
|
showCancel={true}
|
|
|
|
onAction={() => (onDelete ? onDelete() : null)}
|
|
|
|
/>
|
|
|
|
</Alert>
|
|
|
|
</MultiButton>
|
2022-01-07 08:41:53 +00:00
|
|
|
</CommandActions>
|
|
|
|
</CommandHeader>
|
|
|
|
<CommandText>{item.response}</CommandText>
|
|
|
|
</CommandItemContainer>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
type DialogPrompt =
|
|
|
|
| { kind: 'new' }
|
|
|
|
| { kind: 'edit'; name: string; item: TwitchBotCustomCommand };
|
|
|
|
|
|
|
|
function CommandDialog({
|
|
|
|
kind,
|
|
|
|
name,
|
|
|
|
item,
|
|
|
|
onSubmit,
|
|
|
|
}: {
|
|
|
|
kind: 'new' | 'edit';
|
|
|
|
name?: string;
|
|
|
|
item?: TwitchBotCustomCommand;
|
|
|
|
onSubmit?: (name: string, item: TwitchBotCustomCommand) => void;
|
|
|
|
}) {
|
|
|
|
const [commandName, setCommandName] = useState(name ?? '');
|
|
|
|
const [description, setDescription] = useState(item?.description ?? '');
|
|
|
|
const [response, setResponse] = useState(item?.response ?? '');
|
|
|
|
const [accessLevel, setAccessLevel] = useState(
|
|
|
|
item?.access_level ?? 'everyone',
|
|
|
|
);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
|
|
return (
|
|
|
|
<DialogContent title={t(`pages.botcommands.command-header-${kind}`)}>
|
|
|
|
<form
|
|
|
|
onSubmit={(e) => {
|
2022-01-10 10:39:50 +00:00
|
|
|
if (!(e.target as HTMLFormElement).checkValidity()) {
|
|
|
|
return;
|
|
|
|
}
|
2022-01-07 08:41:53 +00:00
|
|
|
e.preventDefault();
|
|
|
|
if (onSubmit) {
|
|
|
|
onSubmit(commandName, {
|
|
|
|
...item,
|
|
|
|
description,
|
|
|
|
response,
|
|
|
|
access_level: accessLevel as AccessLevelType,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Field spacing="narrow" size="fullWidth">
|
|
|
|
<Label htmlFor="command-name">
|
|
|
|
{t('pages.botcommands.command-name')}
|
|
|
|
</Label>
|
|
|
|
<InputBox
|
|
|
|
id="command-name"
|
|
|
|
value={commandName}
|
2022-01-10 10:39:50 +00:00
|
|
|
required={true}
|
2022-01-07 08:41:53 +00:00
|
|
|
onChange={(e) => setCommandName(e.target.value)}
|
|
|
|
placeholder={t('pages.botcommands.command-name-placeholder')}
|
|
|
|
/>
|
|
|
|
</Field>
|
|
|
|
<Field spacing="narrow" size="fullWidth">
|
|
|
|
<Label htmlFor="command-description">
|
|
|
|
{t('pages.botcommands.command-desc')}
|
|
|
|
</Label>
|
|
|
|
<InputBox
|
|
|
|
id="command-description"
|
|
|
|
value={description}
|
|
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
|
|
placeholder={t('pages.botcommands.command-desc-placeholder')}
|
|
|
|
/>
|
|
|
|
</Field>
|
|
|
|
<Field spacing="narrow" size="fullWidth">
|
|
|
|
<Label htmlFor="command-response">
|
|
|
|
{t('pages.botcommands.command-response')}
|
|
|
|
</Label>
|
|
|
|
<Textarea
|
|
|
|
value={response}
|
2022-01-10 10:39:50 +00:00
|
|
|
required={true}
|
2022-01-07 08:41:53 +00:00
|
|
|
onChange={(e) => setResponse(e.target.value)}
|
|
|
|
id="command-response"
|
|
|
|
placeholder={t('pages.botcommands.command-response-placeholder')}
|
|
|
|
>
|
|
|
|
{item?.response}
|
|
|
|
</Textarea>
|
|
|
|
</Field>
|
|
|
|
<Field spacing="narrow" size="fullWidth">
|
|
|
|
<Label htmlFor="command-acl">
|
|
|
|
{t('pages.botcommands.command-acl')}
|
|
|
|
</Label>
|
|
|
|
<ComboBox
|
|
|
|
id="command-acl"
|
|
|
|
value={accessLevel}
|
|
|
|
onChange={(e) => setAccessLevel(e.target.value as AccessLevelType)}
|
|
|
|
>
|
|
|
|
{accessLevels.map((level) => (
|
|
|
|
<option key={level} value={level}>
|
|
|
|
{t(`pages.botcommands.acl.${level}`)}
|
|
|
|
</option>
|
|
|
|
))}
|
|
|
|
</ComboBox>
|
|
|
|
<FieldNote>{t('pages.botcommands.command-acl-help')}</FieldNote>
|
|
|
|
</Field>
|
|
|
|
<DialogActions>
|
|
|
|
<Button variation="primary">
|
|
|
|
{t(`pages.botcommands.command-action-${kind}`)}
|
|
|
|
</Button>
|
|
|
|
<DialogClose asChild>
|
|
|
|
<Button type="button">{t('form-actions.cancel')}</Button>
|
|
|
|
</DialogClose>
|
|
|
|
</DialogActions>
|
|
|
|
</form>
|
|
|
|
</DialogContent>
|
|
|
|
);
|
|
|
|
}
|
2022-01-02 10:46:07 +00:00
|
|
|
|
|
|
|
export default function TwitchBotCommandsPage(): React.ReactElement {
|
2022-01-10 10:19:54 +00:00
|
|
|
const [commands, setCommands] = useModule(modules.twitchBotCommands);
|
|
|
|
const [filter, setFilter] = useState('');
|
2022-01-07 08:41:53 +00:00
|
|
|
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
2022-01-02 10:46:07 +00:00
|
|
|
const { t } = useTranslation();
|
2022-01-07 08:41:53 +00:00
|
|
|
const dispatch = useDispatch();
|
|
|
|
|
2022-01-10 10:19:54 +00:00
|
|
|
const filterLC = filter.toLowerCase();
|
2022-01-07 08:41:53 +00:00
|
|
|
|
|
|
|
const setCommand = (newName: string, data: TwitchBotCustomCommand): void => {
|
|
|
|
switch (activeDialog.kind) {
|
|
|
|
case 'new':
|
|
|
|
dispatch(
|
2022-01-10 10:19:54 +00:00
|
|
|
setCommands({
|
|
|
|
...commands,
|
2022-01-07 08:41:53 +00:00
|
|
|
[newName]: {
|
|
|
|
...data,
|
|
|
|
enabled: true,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case 'edit': {
|
|
|
|
const oldName = activeDialog.name;
|
|
|
|
dispatch(
|
2022-01-10 10:19:54 +00:00
|
|
|
setCommands({
|
|
|
|
...commands,
|
2022-01-07 08:41:53 +00:00
|
|
|
[oldName]: undefined,
|
|
|
|
[newName]: data,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setActiveDialog(null);
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteCommand = (cmd: string): void => {
|
|
|
|
dispatch(
|
2022-01-10 10:19:54 +00:00
|
|
|
setCommands({
|
|
|
|
...commands,
|
2022-01-07 08:41:53 +00:00
|
|
|
[cmd]: undefined,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const toggleCommand = (cmd: string): void => {
|
|
|
|
dispatch(
|
2022-01-10 10:19:54 +00:00
|
|
|
setCommands({
|
|
|
|
...commands,
|
2022-01-07 08:41:53 +00:00
|
|
|
[cmd]: {
|
2022-01-10 10:19:54 +00:00
|
|
|
...commands[cmd],
|
|
|
|
enabled: !commands[cmd].enabled,
|
2022-01-07 08:41:53 +00:00
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
};
|
2022-01-02 10:46:07 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<PageContainer>
|
|
|
|
<PageHeader>
|
|
|
|
<PageTitle>{t('pages.botcommands.title')}</PageTitle>
|
|
|
|
<TextBlock>{t('pages.botcommands.desc')}</TextBlock>
|
|
|
|
</PageHeader>
|
|
|
|
|
|
|
|
<FlexRow spacing="1" align="left">
|
2022-01-07 08:41:53 +00:00
|
|
|
<Button
|
|
|
|
variation="primary"
|
|
|
|
onClick={() => setActiveDialog({ kind: 'new' })}
|
|
|
|
>
|
2022-01-02 10:46:07 +00:00
|
|
|
<PlusIcon /> {t('pages.botcommands.add-button')}
|
|
|
|
</Button>
|
2022-01-07 08:41:53 +00:00
|
|
|
|
2022-01-02 10:46:07 +00:00
|
|
|
<InputBox
|
|
|
|
css={{ flex: 1 }}
|
|
|
|
placeholder={t('pages.botcommands.search-placeholder')}
|
2022-01-10 10:19:54 +00:00
|
|
|
value={filter}
|
|
|
|
onChange={(e) => setFilter(e.target.value)}
|
2022-01-02 10:46:07 +00:00
|
|
|
/>
|
|
|
|
</FlexRow>
|
2022-01-07 08:41:53 +00:00
|
|
|
<CommandList>
|
2022-01-10 10:19:54 +00:00
|
|
|
{Object.keys(commands ?? {})
|
|
|
|
?.filter((cmd) => cmd.toLowerCase().includes(filterLC))
|
2022-01-07 08:41:53 +00:00
|
|
|
.sort()
|
|
|
|
.map((cmd) => (
|
|
|
|
<CommandItem
|
|
|
|
key={cmd}
|
|
|
|
name={cmd}
|
2022-01-10 10:19:54 +00:00
|
|
|
item={commands[cmd]}
|
2022-01-07 08:41:53 +00:00
|
|
|
onToggle={() => toggleCommand(cmd)}
|
|
|
|
onEdit={() =>
|
|
|
|
setActiveDialog({
|
|
|
|
kind: 'edit',
|
|
|
|
name: cmd,
|
2022-01-10 10:19:54 +00:00
|
|
|
item: commands[cmd],
|
2022-01-07 08:41:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
onDelete={() => deleteCommand(cmd)}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</CommandList>
|
|
|
|
|
|
|
|
<Dialog
|
|
|
|
open={!!activeDialog}
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
if (!open) {
|
|
|
|
// Reset dialog status on dialog close
|
|
|
|
setActiveDialog(null);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{activeDialog && (
|
|
|
|
<CommandDialog
|
|
|
|
{...activeDialog}
|
|
|
|
onSubmit={(name, data) => setCommand(name, data)}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</Dialog>
|
2022-01-02 10:46:07 +00:00
|
|
|
</PageContainer>
|
|
|
|
);
|
|
|
|
}
|