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:
parent
4a10599021
commit
c7fc3579bc
5 changed files with 217 additions and 13 deletions
|
@ -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": {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
186
frontend/src/ui/pages/LoyaltyConfig.tsx
Normal file
186
frontend/src/ui/pages/LoyaltyConfig.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue