From c7fc3579bc26355a0d3836e8bd600f9b0b127881 Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Tue, 11 Jan 2022 11:59:48 +0100 Subject: [PATCH] Loyalty system config page --- frontend/src/locale/en/translation.json | 13 ++ frontend/src/ui/App.tsx | 2 + frontend/src/ui/pages/ChatAlerts.tsx | 27 ++-- frontend/src/ui/pages/LoyaltyConfig.tsx | 186 +++++++++++++++++++++++ frontend/src/ui/pages/TwitchSettings.tsx | 2 +- 5 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 frontend/src/ui/pages/LoyaltyConfig.tsx diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index db61600..60b350d 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -161,6 +161,19 @@ "gift_sub-enable": "Enable gifted subscription message", "raid-enable": "Enable raid 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": { diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index d106063..a034cb4 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -30,6 +30,7 @@ import TwitchBotCommandsPage from './pages/BotCommands'; import TwitchBotTimersPage from './pages/BotTimers'; import AuthDialog from './pages/AuthDialog'; import ChatAlertsPage from './pages/ChatAlerts'; +import LoyaltyConfigPage from './pages/LoyaltyConfig'; const LoadingDiv = styled('div', { display: 'flex', @@ -190,6 +191,7 @@ export default function App(): JSX.Element { element={} /> } /> + } /> diff --git a/frontend/src/ui/pages/ChatAlerts.tsx b/frontend/src/ui/pages/ChatAlerts.tsx index 6d15b7e..1e88377 100644 --- a/frontend/src/ui/pages/ChatAlerts.tsx +++ b/frontend/src/ui/pages/ChatAlerts.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { CheckIcon } from '@radix-ui/react-icons'; 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 { Checkbox, @@ -20,11 +20,13 @@ import { TabList, TextBlock, } from '../theme'; +import SaveButton from '../components/utils/SaveButton'; export default function ChatAlertsPage(): React.ReactElement { const { t } = useTranslation(); const dispatch = useDispatch(); - const [alerts, setAlerts] = useModule(modules.twitchBotAlerts); + const [alerts, setAlerts, loadStatus] = useModule(modules.twitchBotAlerts); + const status = useStatus(loadStatus.save); return ( @@ -53,7 +55,7 @@ export default function ChatAlertsPage(): React.ReactElement { checked={alerts?.follow?.enabled ?? false} onCheckedChange={(ev) => dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, follow: { ...alerts.follow, @@ -83,7 +85,7 @@ export default function ChatAlertsPage(): React.ReactElement { required={alerts?.follow?.enabled ?? false} onChange={(messages) => { dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, follow: { ...alerts.follow, messages }, }), @@ -99,7 +101,7 @@ export default function ChatAlertsPage(): React.ReactElement { checked={alerts?.subscription?.enabled ?? false} onCheckedChange={(ev) => dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, subscription: { ...alerts.subscription, @@ -129,7 +131,7 @@ export default function ChatAlertsPage(): React.ReactElement { required={alerts?.subscription?.enabled ?? false} onChange={(messages) => { dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, subscription: { ...alerts.subscription, messages }, }), @@ -146,7 +148,7 @@ export default function ChatAlertsPage(): React.ReactElement { checked={alerts?.gift_sub?.enabled ?? false} onCheckedChange={(ev) => dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, gift_sub: { ...alerts.gift_sub, @@ -176,7 +178,7 @@ export default function ChatAlertsPage(): React.ReactElement { required={alerts?.gift_sub?.enabled ?? false} onChange={(messages) => { dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, gift_sub: { ...alerts.gift_sub, messages }, }), @@ -193,7 +195,7 @@ export default function ChatAlertsPage(): React.ReactElement { checked={alerts?.raid?.enabled ?? false} onCheckedChange={(ev) => dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, raid: { ...alerts.raid, @@ -223,7 +225,7 @@ export default function ChatAlertsPage(): React.ReactElement { required={alerts?.raid?.enabled ?? false} onChange={(messages) => { dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, raid: { ...alerts.raid, messages }, }), @@ -240,7 +242,7 @@ export default function ChatAlertsPage(): React.ReactElement { checked={alerts?.cheer?.enabled ?? false} onCheckedChange={(ev) => dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, cheer: { ...alerts.cheer, @@ -270,7 +272,7 @@ export default function ChatAlertsPage(): React.ReactElement { required={alerts?.cheer?.enabled ?? false} onChange={(messages) => { dispatch( - setAlerts({ + apiReducer.actions.twitchBotAlertsChanged({ ...alerts, cheer: { ...alerts.cheer, messages }, }), @@ -280,6 +282,7 @@ export default function ChatAlertsPage(): React.ReactElement { + ); diff --git a/frontend/src/ui/pages/LoyaltyConfig.tsx b/frontend/src/ui/pages/LoyaltyConfig.tsx new file mode 100644 index 0000000..38ae791 --- /dev/null +++ b/frontend/src/ui/pages/LoyaltyConfig.tsx @@ -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 ( + + + {t('pages.loyalty-settings.title')} + {t('pages.loyalty-settings.subtitle')} + {t('pages.loyalty-settings.note')} + + + + dispatch( + setConfig({ + ...config, + enabled: !!ev, + }), + ) + } + id="enable" + > + {active && } + + + + + + {active && ( +
{ + e.preventDefault(); + if (!(e.target as HTMLFormElement).checkValidity()) { + return; + } + dispatch(setConfig(config)); + }} + > + + + + dispatch( + apiReducer.actions.loyaltyConfigChanged({ + ...config, + currency: e.target.value, + }), + ) + } + /> + + {t('pages.loyalty-settings.currency-name-hint')} + + + + + + + { + const intNum = parseInt(e.target.value, 10); + if (Number.isNaN(intNum)) { + return; + } + dispatch( + apiReducer.actions.loyaltyConfigChanged({ + ...config, + points: { + ...config.points, + amount: intNum, + }, + }), + ); + }} + /> +
{t('pages.loyalty-settings.every')}
+ { + dispatch( + apiReducer.actions.loyaltyConfigChanged({ + ...(config ?? {}), + points: { + ...config?.points, + interval, + }, + }), + ); + }} + active={!busy} + min={5} + required={true} + /> +
+
+ + + + { + const intNum = parseInt(e.target.value, 10); + if (Number.isNaN(intNum)) { + return; + } + dispatch( + apiReducer.actions.loyaltyConfigChanged({ + ...config, + points: { + ...config.points, + activity_bonus: intNum, + }, + }), + ); + }} + /> + + {t('pages.loyalty-settings.bonus-points-hint')} + + + + + + )} +
+ ); +} diff --git a/frontend/src/ui/pages/TwitchSettings.tsx b/frontend/src/ui/pages/TwitchSettings.tsx index cbaf46d..fddaa2a 100644 --- a/frontend/src/ui/pages/TwitchSettings.tsx +++ b/frontend/src/ui/pages/TwitchSettings.tsx @@ -330,7 +330,7 @@ export default function TwitchSettingsPage(): React.ReactElement { {t('pages.twitch-settings.title')} {t('pages.twitch-settings.subtitle')} - +