1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-20 02:00:49 +00:00

Loyalty system config page

This commit is contained in:
Ash Keel 2022-01-11 11:59:48 +01:00
parent 4a10599021
commit c7fc3579bc
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
5 changed files with 217 additions and 13 deletions

View file

@ -161,6 +161,19 @@
"gift_sub-enable": "Enable gifted subscription message", "gift_sub-enable": "Enable gifted subscription message",
"raid-enable": "Enable raid message", "raid-enable": "Enable raid message",
"cheer-enable": "Enable cheering message" "cheer-enable": "Enable cheering message"
},
"loyalty-settings": {
"title": "Loyalty system configuration",
"subtitle": "Loyalty system allowing viewers to accrue points and spend them on rewards and goals",
"enable": "Enable loyalty system",
"currency-placeholder": "points",
"currency-name": "Currency name",
"currency-name-hint": "This will be appended like this: \"user has X yourcurrency\" so choose a lowercase plural name",
"bonus-points": "Bonus points for active users",
"bonus-points-hint": "Extra amount of points awarded to people who have been chatting in the last set interval",
"note": "Note: Unlike platform-native systems (eg. Twitch channel points), this relies on chat activity rather than actual viewing status.",
"every": "every",
"reward": "How often to give {{currency}}"
} }
}, },
"form-actions": { "form-actions": {

View file

@ -30,6 +30,7 @@ import TwitchBotCommandsPage from './pages/BotCommands';
import TwitchBotTimersPage from './pages/BotTimers'; import TwitchBotTimersPage from './pages/BotTimers';
import AuthDialog from './pages/AuthDialog'; import AuthDialog from './pages/AuthDialog';
import ChatAlertsPage from './pages/ChatAlerts'; import ChatAlertsPage from './pages/ChatAlerts';
import LoyaltyConfigPage from './pages/LoyaltyConfig';
const LoadingDiv = styled('div', { const LoadingDiv = styled('div', {
display: 'flex', display: 'flex',
@ -190,6 +191,7 @@ export default function App(): JSX.Element {
element={<TwitchBotTimersPage />} element={<TwitchBotTimersPage />}
/> />
<Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} /> <Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} />
<Route path="/loyalty/settings" element={<LoyaltyConfigPage />} />
</Routes> </Routes>
</PageWrapper> </PageWrapper>
</PageContent> </PageContent>

View file

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { CheckIcon } from '@radix-ui/react-icons'; import { CheckIcon } from '@radix-ui/react-icons';
import { useModule, useStatus } from '../../lib/react-utils'; import { useModule, useStatus } from '../../lib/react-utils';
import { modules } from '../../store/api/reducer'; import apiReducer, { modules } from '../../store/api/reducer';
import MultiInput from '../components/MultiInput'; import MultiInput from '../components/MultiInput';
import { import {
Checkbox, Checkbox,
@ -20,11 +20,13 @@ import {
TabList, TabList,
TextBlock, TextBlock,
} from '../theme'; } from '../theme';
import SaveButton from '../components/utils/SaveButton';
export default function ChatAlertsPage(): React.ReactElement { export default function ChatAlertsPage(): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [alerts, setAlerts] = useModule(modules.twitchBotAlerts); const [alerts, setAlerts, loadStatus] = useModule(modules.twitchBotAlerts);
const status = useStatus(loadStatus.save);
return ( return (
<PageContainer> <PageContainer>
@ -53,7 +55,7 @@ export default function ChatAlertsPage(): React.ReactElement {
checked={alerts?.follow?.enabled ?? false} checked={alerts?.follow?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) =>
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
follow: { follow: {
...alerts.follow, ...alerts.follow,
@ -83,7 +85,7 @@ export default function ChatAlertsPage(): React.ReactElement {
required={alerts?.follow?.enabled ?? false} required={alerts?.follow?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
follow: { ...alerts.follow, messages }, follow: { ...alerts.follow, messages },
}), }),
@ -99,7 +101,7 @@ export default function ChatAlertsPage(): React.ReactElement {
checked={alerts?.subscription?.enabled ?? false} checked={alerts?.subscription?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) =>
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
subscription: { subscription: {
...alerts.subscription, ...alerts.subscription,
@ -129,7 +131,7 @@ export default function ChatAlertsPage(): React.ReactElement {
required={alerts?.subscription?.enabled ?? false} required={alerts?.subscription?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
subscription: { ...alerts.subscription, messages }, subscription: { ...alerts.subscription, messages },
}), }),
@ -146,7 +148,7 @@ export default function ChatAlertsPage(): React.ReactElement {
checked={alerts?.gift_sub?.enabled ?? false} checked={alerts?.gift_sub?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) =>
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
gift_sub: { gift_sub: {
...alerts.gift_sub, ...alerts.gift_sub,
@ -176,7 +178,7 @@ export default function ChatAlertsPage(): React.ReactElement {
required={alerts?.gift_sub?.enabled ?? false} required={alerts?.gift_sub?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
gift_sub: { ...alerts.gift_sub, messages }, gift_sub: { ...alerts.gift_sub, messages },
}), }),
@ -193,7 +195,7 @@ export default function ChatAlertsPage(): React.ReactElement {
checked={alerts?.raid?.enabled ?? false} checked={alerts?.raid?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) =>
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
raid: { raid: {
...alerts.raid, ...alerts.raid,
@ -223,7 +225,7 @@ export default function ChatAlertsPage(): React.ReactElement {
required={alerts?.raid?.enabled ?? false} required={alerts?.raid?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
raid: { ...alerts.raid, messages }, raid: { ...alerts.raid, messages },
}), }),
@ -240,7 +242,7 @@ export default function ChatAlertsPage(): React.ReactElement {
checked={alerts?.cheer?.enabled ?? false} checked={alerts?.cheer?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) =>
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
cheer: { cheer: {
...alerts.cheer, ...alerts.cheer,
@ -270,7 +272,7 @@ export default function ChatAlertsPage(): React.ReactElement {
required={alerts?.cheer?.enabled ?? false} required={alerts?.cheer?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( dispatch(
setAlerts({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
cheer: { ...alerts.cheer, messages }, cheer: { ...alerts.cheer, messages },
}), }),
@ -280,6 +282,7 @@ export default function ChatAlertsPage(): React.ReactElement {
</Field> </Field>
</TabContent> </TabContent>
</TabContainer> </TabContainer>
<SaveButton status={status} type="submit" />
</form> </form>
</PageContainer> </PageContainer>
); );

View file

@ -0,0 +1,186 @@
import React from 'react';
import { CheckIcon } from '@radix-ui/react-icons';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule, useStatus } from '../../lib/react-utils';
import apiReducer, { modules } from '../../store/api/reducer';
import {
PageContainer,
PageHeader,
PageTitle,
TextBlock,
Field,
FlexRow,
Label,
Checkbox,
CheckboxIndicator,
InputBox,
FieldNote,
} from '../theme';
import SaveButton from '../components/utils/SaveButton';
import Interval from '../components/Interval';
export default function LoyaltySettingsPage(): React.ReactElement {
const { t } = useTranslation();
const [config, setConfig, loadStatus] = useModule(modules.loyaltyConfig);
const dispatch = useDispatch();
const status = useStatus(loadStatus.save);
const busy =
loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending';
const active = config?.enabled ?? false;
return (
<PageContainer>
<PageHeader>
<PageTitle>{t('pages.loyalty-settings.title')}</PageTitle>
<TextBlock>{t('pages.loyalty-settings.subtitle')}</TextBlock>
<TextBlock>{t('pages.loyalty-settings.note')}</TextBlock>
<Field css={{ paddingTop: '1rem' }}>
<FlexRow spacing={1}>
<Checkbox
checked={active}
onCheckedChange={(ev) =>
dispatch(
setConfig({
...config,
enabled: !!ev,
}),
)
}
id="enable"
>
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
</Checkbox>
<Label htmlFor="enable">{t('pages.loyalty-settings.enable')}</Label>
</FlexRow>
</Field>
</PageHeader>
{active && (
<form
onSubmit={(e) => {
e.preventDefault();
if (!(e.target as HTMLFormElement).checkValidity()) {
return;
}
dispatch(setConfig(config));
}}
>
<Field size="fullWidth">
<Label htmlFor="currency">
{t('pages.loyalty-settings.currency-name')}
</Label>
<InputBox
type="text"
id="currency"
placeholder={t('pages.loyalty-settings.currency-placeholder')}
value={config?.currency ?? ''}
disabled={busy}
required={true}
onChange={(e) =>
dispatch(
apiReducer.actions.loyaltyConfigChanged({
...config,
currency: e.target.value,
}),
)
}
/>
<FieldNote>
{t('pages.loyalty-settings.currency-name-hint')}
</FieldNote>
</Field>
<Field size="fullWidth">
<Label htmlFor="reward">
{t('pages.loyalty-settings.reward', {
currency:
config?.currency ??
t('pages.loyalty-settings.currency-placeholder'),
})}
</Label>
<FlexRow align="left" spacing={1}>
<InputBox
type="number"
id="reward"
placeholder={'0'}
css={{ maxWidth: '5rem' }}
value={config?.points?.amount ?? '0'}
disabled={busy}
required={true}
onChange={(e) => {
const intNum = parseInt(e.target.value, 10);
if (Number.isNaN(intNum)) {
return;
}
dispatch(
apiReducer.actions.loyaltyConfigChanged({
...config,
points: {
...config.points,
amount: intNum,
},
}),
);
}}
/>
<div>{t('pages.loyalty-settings.every')}</div>
<Interval
id="timer-interval"
value={config?.points?.interval ?? 120}
onChange={(interval) => {
dispatch(
apiReducer.actions.loyaltyConfigChanged({
...(config ?? {}),
points: {
...config?.points,
interval,
},
}),
);
}}
active={!busy}
min={5}
required={true}
/>
</FlexRow>
</Field>
<Field size="fullWidth">
<Label htmlFor="bonus">
{t('pages.loyalty-settings.bonus-points')}
</Label>
<InputBox
type="number"
id="bonus"
placeholder={'0'}
value={config?.points?.activity_bonus ?? '0'}
disabled={busy}
required={true}
onChange={(e) => {
const intNum = parseInt(e.target.value, 10);
if (Number.isNaN(intNum)) {
return;
}
dispatch(
apiReducer.actions.loyaltyConfigChanged({
...config,
points: {
...config.points,
activity_bonus: intNum,
},
}),
);
}}
/>
<FieldNote>
{t('pages.loyalty-settings.bonus-points-hint')}
</FieldNote>
</Field>
<SaveButton type="submit" status={status} />
</form>
)}
</PageContainer>
);
}

View file

@ -330,7 +330,7 @@ export default function TwitchSettingsPage(): React.ReactElement {
<PageHeader> <PageHeader>
<PageTitle>{t('pages.twitch-settings.title')}</PageTitle> <PageTitle>{t('pages.twitch-settings.title')}</PageTitle>
<TextBlock>{t('pages.twitch-settings.subtitle')}</TextBlock> <TextBlock>{t('pages.twitch-settings.subtitle')}</TextBlock>
<Field> <Field css={{ paddingTop: '1rem' }}>
<FlexRow spacing={1}> <FlexRow spacing={1}>
<Checkbox <Checkbox
checked={active} checked={active}