-
onClose() : null}
- />
-
-
- {title}
- {showCancel ? (
-
-
-
-
-
- );
-}
-
-export default React.memo(Modal);
diff --git a/frontend/src/ui/components/PageList.tsx b/frontend/src/ui/components/PageList.tsx
deleted file mode 100644
index 4afc6b1..0000000
--- a/frontend/src/ui/components/PageList.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-
-export interface PageListProps {
- current: number;
- max: number;
- min: number;
- itemsPerPage: number;
- onSelectChange: (itemsPerPage: number) => void;
- onPageChange: (page: number) => void;
-}
-
-function PageList({
- current,
- max,
- min,
- itemsPerPage,
- onSelectChange,
- onPageChange,
-}: PageListProps): React.ReactElement {
- const { t } = useTranslation();
- return (
-
- );
-}
-
-export default React.memo(PageList);
diff --git a/frontend/src/ui/components/TabbedView.tsx b/frontend/src/ui/components/TabbedView.tsx
deleted file mode 100644
index 567b1ed..0000000
--- a/frontend/src/ui/components/TabbedView.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React, { useEffect } from 'react';
-
-function TabbedView({
- children,
-}: // eslint-disable-next-line @typescript-eslint/ban-types
-React.PropsWithChildren<{}>): React.ReactElement {
- const [activeTab, setActiveTab] = React.useState(null);
-
- const tabs = React.Children.map(children, (elem, i) => {
- const id =
- (typeof elem === 'object' && 'props' in elem
- ? elem.props['data-name']
- : null) ?? `TAB#${i}`;
- return {
- id,
- tabContent: elem,
- };
- });
-
- useEffect(() => {
- if (activeTab === null) {
- setActiveTab(tabs[0].id);
- }
- }, [children, activeTab]);
-
- if (activeTab === null) {
- return
;
- }
-
- const active = tabs.find((elem) => elem.id === activeTab);
- return (
- <>
-
-
{active.tabContent}
- >
- );
-}
-
-export default React.memo(TabbedView);
diff --git a/frontend/src/ui/pages/Debug.tsx b/frontend/src/ui/pages/Debug.tsx
deleted file mode 100644
index 895d49f..0000000
--- a/frontend/src/ui/pages/Debug.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useSelector } from 'react-redux';
-import { RootState } from '../../store';
-
-export default function DebugPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- params: RouteComponentProps
,
-): React.ReactElement {
- const { t } = useTranslation();
- const api = useSelector((state: RootState) => state.api.client);
- const [readKey, setReadKey] = useState('');
- const [readValue, setReadValue] = useState('');
- const [writeKey, setWriteKey] = useState('');
- const [writeValue, setWriteValue] = useState('');
- const [writeErrorMsg, setWriteErrorMsg] = useState(null);
-
- const performRead = async () => {
- const value = await api.getKey(readKey);
- setReadValue(value);
- };
- const performWrite = async () => {
- const result = await api.putKey(writeKey, writeValue);
- console.log(result);
- };
- const fixJSON = () => {
- try {
- setWriteValue(JSON.stringify(JSON.parse(writeValue)));
- setWriteErrorMsg(null);
- } catch (e) {
- setWriteErrorMsg(e.message);
- }
- };
- const dumpKeys = async () => {
- console.log(await api.keyList());
- };
- const dumpAll = async () => {
- console.log(await api.getKeysByPrefix(''));
- };
-
- return (
-
-
- {t('debug.friendly-greeting')}
-
-
-
-
-
-
-
-
-
-
-
-
-
- setReadKey(ev.target.value)}
- placeholder="some-bucket/some-key"
- />
-
-
-
-
-
-
-
-
-
-
-
- setWriteKey(ev.target.value)}
- placeholder="some-bucket/some-key"
- />
-
-
-
-
-
- {' '}
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/ui/pages/HTTP.tsx b/frontend/src/ui/pages/HTTP.tsx
deleted file mode 100644
index 0288c79..0000000
--- a/frontend/src/ui/pages/HTTP.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useModule } from '../../lib/react-utils';
-import apiReducer, { modules } from '../../store/api/reducer';
-import Field from '../components/Field';
-
-export default function HTTPPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- params: RouteComponentProps,
-): React.ReactElement {
- const { t } = useTranslation();
- const [httpConfig, setHTTPConfig] = useModule(modules.httpConfig);
- const dispatch = useDispatch();
-
- const busy = httpConfig === null;
- const active = httpConfig?.enable_static_server ?? false;
-
- return (
- <>
- {t('http.header')}
-
-
-
- dispatch(
- apiReducer.actions.httpConfigChanged({
- ...httpConfig,
- bind: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
-
- dispatch(
- apiReducer.actions.httpConfigChanged({
- ...httpConfig,
- kv_password: ev.target.value,
- }),
- )
- }
- />
-
- Leave empty to disable authentication
-
-
-
-
- {active && (
-
-
-
- dispatch(
- apiReducer.actions.httpConfigChanged({
- ...httpConfig,
- path: ev.target.value,
- }),
- )
- }
- />
-
-
- )}
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/Home.tsx b/frontend/src/ui/pages/Home.tsx
deleted file mode 100644
index 01fc738..0000000
--- a/frontend/src/ui/pages/Home.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-
-export default function Home(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- params: RouteComponentProps,
-): React.ReactElement {
- return Work in progress!!
;
-}
diff --git a/frontend/src/ui/pages/loyalty/Goals.tsx b/frontend/src/ui/pages/loyalty/Goals.tsx
deleted file mode 100644
index e49f5d9..0000000
--- a/frontend/src/ui/pages/loyalty/Goals.tsx
+++ /dev/null
@@ -1,384 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useSelector, useDispatch } from 'react-redux';
-import { useModule } from '../../../lib/react-utils';
-import { RootState } from '../../../store';
-import { modules } from '../../../store/api/reducer';
-import Modal from '../../components/Modal';
-import { LoyaltyGoal } from '../../../store/api/types';
-import Field from '../../components/Field';
-
-interface GoalItemProps {
- item: LoyaltyGoal;
- onToggleState: () => void;
- onEdit: () => void;
- onDelete: () => void;
-}
-function GoalItem({ item, onToggleState, onEdit, onDelete }: GoalItemProps) {
- const { t } = useTranslation();
- const currency = useSelector(
- (state: RootState) =>
- state.api.moduleConfigs?.loyaltyConfig?.currency ??
- t('loyalty.points-fallback'),
- );
- const [expanded, setExpanded] = useState(false);
- const placeholder = 'https://bulma.io/images/placeholders/128x128.png';
- const contributors = Object.entries(item.contributors ?? {}).sort(
- ([, pointsA], [, pointsB]) => pointsB - pointsA,
- );
-
- return (
-
-
- {expanded ? (
-
- {item.description}
-
- {contributors.length > 0 ? (
- <>
-
{t('loyalty.goals.contributors')}
-
-
- {t('form-common.username')} |
- {t('loyalty.points')} |
-
- {contributors.map(([user, points]) => (
-
- {user} |
-
- {points}{' '}
-
- ({Math.round((points / item.total) * 10000) / 100}%)
-
- |
-
- ))}
-
- >
- ) : (
-
{t('loyalty.goals.no-contributors')}
- )}
-
-
-
- ) : null}
-
- );
-}
-
-interface GoalModalProps {
- active: boolean;
- onConfirm: (r: LoyaltyGoal) => void;
- onClose: () => void;
- initialData?: LoyaltyGoal;
- title: string;
- confirmText: string;
-}
-
-function GoalModal({
- active,
- onConfirm,
- onClose,
- initialData,
- title,
- confirmText,
-}: GoalModalProps) {
- const { t } = useTranslation();
-
- const [loyaltyConfig] = useModule(modules.loyaltyConfig);
- const [goals] = useModule(modules.loyaltyGoals);
-
- const [id, setID] = useState(initialData?.id ?? '');
- const [name, setName] = useState(initialData?.name ?? '');
- const [image, setImage] = useState(initialData?.image ?? '');
- const [description, setDescription] = useState(
- initialData?.description ?? '',
- );
- const [total, setTotal] = useState(initialData?.total ?? 0);
-
- const setIDex = (newID) =>
- setID(newID.toLowerCase().replace(/[^a-zA-Z0-9]/gi, '-'));
-
- const slug = id || name?.toLowerCase().replace(/[^a-zA-Z0-9]/gi, '-') || '';
- const idExists = goals?.some((goal) => goal.id === slug) ?? false;
- const idInvalid = slug !== initialData?.id && idExists;
-
- const validForm = idInvalid === false && name !== '' && total >= 0;
-
- const confirm = () => {
- if (onConfirm) {
- onConfirm({
- id: slug,
- name,
- description,
- total,
- enabled: initialData?.enabled ?? false,
- image,
- contributed: initialData?.contributed ?? 0,
- contributors: initialData?.contributors ?? {},
- });
- }
- };
-
- return (
- confirm()}
- onClose={() => onClose()}
- >
-
-
-
-
- setIDex(ev.target.value)}
- />
-
- {idInvalid ? (
-
- {t('loyalty.goals.err-goalid-dup')}
-
- ) : (
-
{t('loyalty.goals.id-help')}
- )}
-
-
-
-
-
-
-
- setName(ev.target.value)}
- />
-
-
-
-
-
-
-
-
- setImage(ev.target.value)}
- />
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default function LoyaltyGoalsPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const [goals, setGoals] = useModule(modules.loyaltyGoals);
- const [twitchConfig] = useModule(modules.twitchConfig);
- const [loyaltyConfig] = useModule(modules.loyaltyConfig);
-
- const { t } = useTranslation();
- const dispatch = useDispatch();
-
- const twitchActive = twitchConfig?.enabled ?? false;
- const loyaltyEnabled = loyaltyConfig?.enabled ?? false;
- const active = twitchActive && loyaltyEnabled;
-
- const [goalFilter, setGoalFilter] = useState('');
- const goalFilterLC = goalFilter.toLowerCase();
-
- const [createModal, setCreateModal] = useState(false);
- const [showModifyGoal, setShowModifyGoal] = useState(null);
-
- const createGoal = (newGoal: LoyaltyGoal) => {
- dispatch(setGoals([...(goals ?? []), newGoal]));
- setCreateModal(false);
- };
-
- const toggleGoal = (goalID: string) => {
- dispatch(
- setGoals(
- goals.map((entry) =>
- entry.id === goalID
- ? {
- ...entry,
- enabled: !entry.enabled,
- }
- : entry,
- ),
- ),
- );
- };
-
- const modifyGoal = (originalGoalID: string, goal: LoyaltyGoal) => {
- dispatch(
- setGoals(
- goals.map((entry) => (entry.id === originalGoalID ? goal : entry)),
- ),
- );
- setShowModifyGoal(null);
- };
-
- const deleteGoal = (goalID: string) => {
- dispatch(setGoals(goals.filter((entry) => entry.id !== goalID)));
- };
-
- return (
- <>
- {t('loyalty.goals.header')}
-
-
-
-
-
-
-
- setGoalFilter(ev.target.value)}
- />
-
-
-
- setCreateModal(false)}
- />
- {showModifyGoal ? (
- modifyGoal(showModifyGoal.id, goal)}
- initialData={showModifyGoal}
- onClose={() => setShowModifyGoal(null)}
- />
- ) : null}
-
- {goals
- ?.filter((goal) => goal.name.toLowerCase().includes(goalFilterLC))
- .map((goal) => (
- deleteGoal(goal.id)}
- onEdit={() => setShowModifyGoal(goal)}
- onToggleState={() => toggleGoal(goal.id)}
- />
- ))}
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/loyalty/Main.tsx b/frontend/src/ui/pages/loyalty/Main.tsx
deleted file mode 100644
index 4c3b89e..0000000
--- a/frontend/src/ui/pages/loyalty/Main.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-
-export default function LoyaltyPage({
- children,
-}: RouteComponentProps>): React.ReactElement {
- return <>{children}>;
-}
diff --git a/frontend/src/ui/pages/loyalty/Queue.tsx b/frontend/src/ui/pages/loyalty/Queue.tsx
deleted file mode 100644
index 5eee98e..0000000
--- a/frontend/src/ui/pages/loyalty/Queue.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import React, { useState } from 'react';
-import { useDispatch } from 'react-redux';
-import { RouteComponentProps } from '@reach/router';
-import { useTranslation } from 'react-i18next';
-import { useModule, useUserPoints } from '../../../lib/react-utils';
-import {
- modules,
- removeRedeem,
- setUserPoints,
-} from '../../../store/api/reducer';
-import PageList from '../../components/PageList';
-import { LoyaltyRedeem } from '../../../store/api/types';
-
-interface SortingOrder {
- key: 'user' | 'when';
- order: 'asc' | 'desc';
-}
-
-export default function LoyaltyRedeemQueuePage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const [redemptions] = useModule(modules.loyaltyRedeemQueue);
-
- // Big hack but this is required or refunds break
- useUserPoints();
-
- const [sorting, setSorting] = useState({
- key: 'when',
- order: 'desc',
- });
-
- const [entriesPerPage, setEntriesPerPage] = useState(15);
- const [page, setPage] = useState(0);
- const [usernameFilter, setUsernameFilter] = useState('');
- const { t } = useTranslation();
- const dispatch = useDispatch();
-
- const changeSort = (key: 'user' | 'when') => {
- if (sorting.key === key) {
- // Same key, swap sorting order
- setSorting({
- ...sorting,
- order: sorting.order === 'asc' ? 'desc' : 'asc',
- });
- } else {
- // Different key, change to sort that key
- setSorting({ ...sorting, key, order: 'asc' });
- }
- };
-
- const filtered =
- redemptions?.filter(({ username }) => username.includes(usernameFilter)) ??
- [];
-
- const sortedEntries = filtered;
- switch (sorting.key) {
- case 'user':
- if (sorting.order === 'asc') {
- sortedEntries.sort((a, b) => (a.username > b.username ? 1 : -1));
- } else {
- sortedEntries.sort((a, b) => (a.username < b.username ? 1 : -1));
- }
- break;
- case 'when':
- if (sorting.order === 'asc') {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- sortedEntries.sort(
- (a, b) =>
- Date.parse(a.when.toString()) - Date.parse(b.when.toString()),
- );
- } else {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- sortedEntries.sort(
- (a, b) =>
- Date.parse(b.when.toString()) - Date.parse(a.when.toString()),
- );
- }
- break;
- default:
- // unreacheable
- }
-
- const paged = sortedEntries.slice(
- page * entriesPerPage,
- (page + 1) * entriesPerPage,
- );
- const totalPages = Math.floor(sortedEntries.length / entriesPerPage);
-
- const acceptRedeem = (redeem: LoyaltyRedeem) => {
- // Just take the redeem off the list
- dispatch(removeRedeem(redeem));
- };
-
- const refundRedeem = (redeem: LoyaltyRedeem) => {
- // Give points back to the viewer
- dispatch(
- setUserPoints({
- user: redeem.username,
- points: redeem.reward.price,
- relative: true,
- }),
- );
- // Take the redeem off the list
- dispatch(removeRedeem(redeem));
- };
-
- return (
- <>
- {t('loyalty.queue.header')}
- {redemptions ? (
- <>
-
-
- setUsernameFilter(ev.target.value.toLowerCase())
- }
- />
-
- setEntriesPerPage(em)}
- onPageChange={(p) => setPage(p - 1)}
- />
-
-
-
-
- changeSort('when')}>
- {t('form-common.date')}
- {sorting.key === 'when' ? (
-
- {sorting.order === 'asc' ? '▴' : '▾'}
-
- ) : null}
-
- |
-
- changeSort('user')}>
- {t('form-common.username')}
- {sorting.key === 'user' ? (
-
- {sorting.order === 'asc' ? '▴' : '▾'}
-
- ) : null}
-
- |
- {t('loyalty.queue.reward-name')} |
- {t('loyalty.queue.request')} |
- |
-
-
-
-
- {paged.map((redemption) => (
-
- {new Date(redemption.when).toLocaleString()} |
-
- {redemption.display_name} ({redemption.username})
- |
- {redemption.reward.name} |
- {redemption.request_text} |
-
- acceptRedeem(redemption)}>
- {t('loyalty.queue.accept')}
-
- {redemption.username !== '@PLATFORM' ? (
- <>
- {' 🞄 '}
- refundRedeem(redemption)}>
- {t('loyalty.queue.refund')}
-
- >
- ) : null}
- |
-
- ))}
-
-
- setEntriesPerPage(em)}
- onPageChange={(p) => setPage(p - 1)}
- />
- >
- ) : (
- {t('loyalty.queue.err-not-available')}
- )}
- >
- );
-}
diff --git a/frontend/src/ui/pages/loyalty/Rewards.tsx b/frontend/src/ui/pages/loyalty/Rewards.tsx
deleted file mode 100644
index 4664457..0000000
--- a/frontend/src/ui/pages/loyalty/Rewards.tsx
+++ /dev/null
@@ -1,440 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React, { useState } from 'react';
-import { useSelector, useDispatch } from 'react-redux';
-import prettyTime from 'pretty-ms';
-import { useTranslation } from 'react-i18next';
-import { useModule } from '../../../lib/react-utils';
-import { RootState } from '../../../store';
-import { createRedeem, modules } from '../../../store/api/reducer';
-import Modal from '../../components/Modal';
-import { LoyaltyReward } from '../../../store/api/types';
-import Field from '../../components/Field';
-import Interval from '../../components/Interval';
-
-interface RewardItemProps {
- item: LoyaltyReward;
- onToggleState: () => void;
- onEdit: () => void;
- onDelete: () => void;
- onTest: () => void;
-}
-function RewardItem({
- item,
- onToggleState,
- onEdit,
- onDelete,
- onTest,
-}: RewardItemProps) {
- const { t } = useTranslation();
- const currency = useSelector(
- (state: RootState) =>
- state.api.moduleConfigs?.loyaltyConfig?.currency ??
- t('loyalty.points-fallback'),
- );
- const [expanded, setExpanded] = useState(false);
- const placeholder = 'https://bulma.io/images/placeholders/128x128.png';
-
- return (
-
-
- {expanded ? (
-
- {item.description}
- {item.cooldown > 0 ? (
-
- {t('loyalty.rewards.cooldown')}:{' '}
- {prettyTime(item.cooldown * 1000)}
-
- ) : null}
- {item.required_info ? (
-
- {t('loyalty.rewards.required-info')}: {item.required_info}
-
- ) : null}
-
-
- ) : null}
-
- );
-}
-
-interface RewardModalProps {
- active: boolean;
- onConfirm: (r: LoyaltyReward) => void;
- onClose: () => void;
- initialData?: LoyaltyReward;
- title: string;
- confirmText: string;
-}
-
-function RewardModal({
- active,
- onConfirm,
- onClose,
- initialData,
- title,
- confirmText,
-}: RewardModalProps) {
- const { t } = useTranslation();
- const currency = useSelector(
- (state: RootState) =>
- state.api.moduleConfigs?.loyaltyConfig?.currency ?? 'points',
- );
- const [rewards] = useModule(modules.loyaltyRewards);
-
- const [id, setID] = useState(initialData?.id ?? '');
- const [name, setName] = useState(initialData?.name ?? '');
- const [image, setImage] = useState(initialData?.image ?? '');
- const [description, setDescription] = useState(
- initialData?.description ?? '',
- );
- const [price, setPrice] = useState(initialData?.price ?? 0);
- const [extraDetails, setExtraDetails] = useState(
- initialData?.required_info ?? '',
- );
- const [extraRequired, setExtraRequired] = useState(extraDetails !== '');
-
- const [cooldown, setCooldown] = useState(initialData?.cooldown ?? 0);
-
- const setIDex = (newID) =>
- setID(newID.toLowerCase().replace(/[^a-zA-Z0-9]/gi, '-'));
-
- const slug = id || name?.toLowerCase().replace(/[^a-zA-Z0-9]/gi, '-') || '';
- const idExists = rewards?.some((reward) => reward.id === slug) ?? false;
- const idInvalid = slug !== initialData?.id && idExists;
-
- const validForm = idInvalid === false && name !== '' && price >= 0;
-
- const confirm = () => {
- if (onConfirm) {
- onConfirm({
- id: slug,
- name,
- description,
- price,
- enabled: initialData?.enabled ?? false,
- image,
- required_info: extraRequired ? extraDetails : undefined,
- cooldown,
- });
- }
- };
-
- return (
- confirm()}
- onClose={() => onClose()}
- >
-
-
-
-
- setIDex(ev.target.value)}
- />
-
- {idInvalid ? (
-
- {t('loyalty.rewards.err-rewid-dup')}
-
- ) : (
-
{t('loyalty.rewards.id-help')}
- )}
-
-
-
-
-
-
-
- setName(ev.target.value)}
- />
-
-
-
-
-
-
-
-
- setImage(ev.target.value)}
- />
-
-
-
-
-
-
-
-
-
-
-
- setPrice(parseInt(ev.target.value, 10))}
- />
-
-
- {currency}
-
-
-
-
-
-
-
-
-
-
-
-
- {extraRequired ? (
- <>
-
-
-
-
- setExtraDetails(ev.target.value)}
- />
-
-
-
-
- >
- ) : null}
-
-
-
-
- );
-}
-
-export default function LoyaltyRewardsPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const [rewards, setRewards] = useModule(modules.loyaltyRewards);
- const [twitchConfig] = useModule(modules.twitchConfig);
- const [loyaltyConfig] = useModule(modules.loyaltyConfig);
-
- const dispatch = useDispatch();
- const { t } = useTranslation();
-
- const twitchActive = twitchConfig?.enabled ?? false;
- const loyaltyEnabled = loyaltyConfig?.enabled ?? false;
- const active = twitchActive && loyaltyEnabled;
-
- const [rewardFilter, setRewardFilter] = useState('');
- const rewardFilterLC = rewardFilter.toLowerCase();
-
- const [createModal, setCreateModal] = useState(false);
- const [showModifyReward, setShowModifyReward] = useState(null);
-
- const createReward = (newReward: LoyaltyReward) => {
- dispatch(setRewards([...(rewards ?? []), newReward]));
- setCreateModal(false);
- };
-
- const toggleReward = (rewardID: string) => {
- dispatch(
- setRewards(
- rewards.map((entry) =>
- entry.id === rewardID
- ? {
- ...entry,
- enabled: !entry.enabled,
- }
- : entry,
- ),
- ),
- );
- };
-
- const modifyReward = (originRewardID: string, reward: LoyaltyReward) => {
- dispatch(
- setRewards(
- rewards.map((entry) => (entry.id === originRewardID ? reward : entry)),
- ),
- );
- setShowModifyReward(null);
- };
-
- const deleteReward = (rewardID: string) => {
- dispatch(setRewards(rewards.filter((entry) => entry.id !== rewardID)));
- };
-
- const testRedeem = (reward: LoyaltyReward) => {
- dispatch(
- createRedeem({
- username: '@PLATFORM',
- display_name: 'me :3',
- when: new Date(),
- reward,
- request_text: '',
- }),
- );
- };
-
- return (
- <>
- {t('loyalty.rewards.header')}
-
-
-
-
-
-
-
- setRewardFilter(ev.target.value)}
- />
-
-
-
- setCreateModal(false)}
- />
- {showModifyReward ? (
- modifyReward(showModifyReward.id, reward)}
- initialData={showModifyReward}
- onClose={() => setShowModifyReward(null)}
- />
- ) : null}
-
- {rewards
- ?.filter((reward) =>
- reward.name.toLowerCase().includes(rewardFilterLC),
- )
- .map((reward) => (
- deleteReward(reward.id)}
- onEdit={() => setShowModifyReward(reward)}
- onToggleState={() => toggleReward(reward.id)}
- onTest={() => testRedeem(reward)}
- />
- ))}
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/loyalty/Settings.tsx b/frontend/src/ui/pages/loyalty/Settings.tsx
deleted file mode 100644
index e0e8f3b..0000000
--- a/frontend/src/ui/pages/loyalty/Settings.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React, { useState, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useModule } from '../../../lib/react-utils';
-import apiReducer, { modules } from '../../../store/api/reducer';
-import Field from '../../components/Field';
-import Interval from '../../components/Interval';
-
-export default function LoyaltySettingPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const [loyaltyConfig, setLoyaltyConfig] = useModule(modules.loyaltyConfig);
- const [twitchConfig] = useModule(modules.twitchConfig);
-
- const dispatch = useDispatch();
- const { t } = useTranslation();
-
- const twitchActive = twitchConfig?.enabled ?? false;
- const twitchBotActive = twitchConfig?.enable_bot ?? false;
- const loyaltyEnabled = loyaltyConfig?.enabled ?? false;
- const active = twitchActive && twitchBotActive && loyaltyEnabled;
-
- const [interval, setInterval] = useState(
- loyaltyConfig?.points?.interval ?? 0,
- );
-
- useEffect(() => {
- dispatch(
- apiReducer.actions.loyaltyConfigChanged({
- ...loyaltyConfig,
- points: {
- ...loyaltyConfig?.points,
- interval,
- },
- }),
- );
- }, [interval]);
-
- return (
- <>
- {t('loyalty.config.header')}
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.loyaltyConfigChanged({
- ...loyaltyConfig,
- currency: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
-
-
-
- {
- const bonus = parseInt(ev.target.value, 10);
- if (Number.isNaN(bonus)) {
- return;
- }
- dispatch(
- apiReducer.actions.loyaltyConfigChanged({
- ...loyaltyConfig,
- points: {
- ...loyaltyConfig?.points,
- activity_bonus: bonus,
- },
- }),
- );
- }}
- />
-
-
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/loyalty/UserList.tsx b/frontend/src/ui/pages/loyalty/UserList.tsx
deleted file mode 100644
index 55a9b50..0000000
--- a/frontend/src/ui/pages/loyalty/UserList.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-import React, { useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { RouteComponentProps } from '@reach/router';
-import { useTranslation } from 'react-i18next';
-import PageList from '../../components/PageList';
-import { useModule, useUserPoints } from '../../../lib/react-utils';
-import { RootState } from '../../../store';
-import { modules, setUserPoints } from '../../../store/api/reducer';
-import Modal from '../../components/Modal';
-import { LoyaltyPointsEntry } from '../../../store/api/types';
-import Field from '../../components/Field';
-
-interface UserData {
- user: string;
- entry: LoyaltyPointsEntry;
-}
-
-interface UserModalProps {
- active: boolean;
- onConfirm: (r: UserData) => void;
- onClose: () => void;
- initialData?: UserData;
- title: string;
- confirmText: string;
-}
-
-function UserModal({
- active,
- onConfirm,
- onClose,
- initialData,
- title,
- confirmText,
-}: UserModalProps) {
- const { t } = useTranslation();
- const currency = useSelector(
- (state: RootState) =>
- state.api.moduleConfigs?.loyaltyConfig?.currency ?? 'points',
- );
-
- const [user, setUser] = useState(initialData.user);
- const [entry, setEntry] = useState(initialData.entry);
- const userEditable = initialData.user === '';
-
- const nameValid = user !== '';
- const pointsValid = Number.isFinite(entry.points);
- const validForm = nameValid && pointsValid;
-
- const confirm = () => {
- if (onConfirm) {
- onConfirm({
- user,
- entry,
- });
- }
- };
-
- return (
- confirm()}
- onClose={() => onClose()}
- >
-
-
-
-
- setUser(ev.target.value)}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- setEntry({ ...entry, points: parseInt(ev.target.value, 10) })
- }
- />
-
-
-
-
-
- );
-}
-
-interface SortingOrder {
- key: 'user' | 'points';
- order: 'asc' | 'desc';
-}
-export default function LoyaltyUserListPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const { t } = useTranslation();
- const [loyaltyConfig] = useModule(modules.loyaltyConfig);
- const currency = loyaltyConfig?.currency ?? t('loyalty.points-fallback');
- const users = useUserPoints();
- const dispatch = useDispatch();
- const [sorting, setSorting] = useState({
- key: 'points',
- order: 'desc',
- });
-
- const [entriesPerPage, setEntriesPerPage] = useState(15);
- const [page, setPage] = useState(0);
- const [usernameFilter, setUsernameFilter] = useState('');
- const [editModal, setEditModal] = useState(null);
- const [createModal, setCreateModal] = useState(false);
-
- const changeSort = (key: 'user' | 'points') => {
- if (sorting.key === key) {
- // Same key, swap sorting order
- setSorting({
- ...sorting,
- order: sorting.order === 'asc' ? 'desc' : 'asc',
- });
- } else {
- // Different key, change to sort that key
- setSorting({ ...sorting, key, order: 'asc' });
- }
- };
-
- const rawEntries = Object.entries(users ?? []);
- const filtered = rawEntries.filter(([user]) => user.includes(usernameFilter));
-
- const sortedEntries = filtered;
- switch (sorting.key) {
- case 'user':
- if (sorting.order === 'asc') {
- sortedEntries.sort(([userA], [userB]) => (userA > userB ? 1 : -1));
- } else {
- sortedEntries.sort(([userA], [userB]) => (userA < userB ? 1 : -1));
- }
- break;
- case 'points':
- if (sorting.order === 'asc') {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- sortedEntries.sort(([_a, a], [_b, b]) => a.points - b.points);
- } else {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- sortedEntries.sort(([_a, a], [_b, b]) => b.points - a.points);
- }
- break;
- default:
- // unreacheable
- }
-
- const offset = page * entriesPerPage;
- const paged = sortedEntries.slice(offset, offset + entriesPerPage);
- const totalPages = Math.floor(sortedEntries.length / entriesPerPage);
-
- const modifyUser = ({ entry, user }: UserData) => {
- dispatch(setUserPoints({ user, points: entry.points, relative: false }));
- setEditModal(null);
- };
- const assignPoints = ({ entry, user }: UserData) => {
- console.log(user, entry);
- dispatch(setUserPoints({ user, points: entry.points, relative: true }));
- setCreateModal(false);
- };
-
- return (
- <>
- assignPoints(entry)}
- initialData={{ user: '', entry: { points: 0 } }}
- onClose={() => setCreateModal(false)}
- />
- {editModal ? (
- modifyUser(entry)}
- initialData={editModal}
- onClose={() => setEditModal(null)}
- />
- ) : null}
-
- {t('loyalty.userlist.userlist-header', { currency })}
-
- {users ? (
- <>
-
-
- setUsernameFilter(ev.target.value.toLowerCase())
- }
- />
-
-
- setEntriesPerPage(em)}
- onPageChange={(p) => setPage(p - 1)}
- />
-
-
-
-
- changeSort('user')}>
- {t('form-common.username')}
- {sorting.key === 'user' ? (
-
- {sorting.order === 'asc' ? '▴' : '▾'}
-
- ) : null}
-
- |
-
- changeSort('points')}
- style={{ textTransform: 'capitalize' }}
- >
- {currency}
- {sorting.key === 'points' ? (
-
- {sorting.order === 'asc' ? '▴' : '▾'}
-
- ) : null}
-
- |
- |
-
-
-
-
- {paged.map(([user, p]) => (
-
- {user} |
- {p.points} |
-
-
- setEditModal({
- user,
- entry: p,
- })
- }
- >
- {t('actions.edit')}
-
- |
-
- ))}
-
-
- setEntriesPerPage(em)}
- onPageChange={(p) => setPage(p - 1)}
- />
- >
- ) : (
- {t('loyalty.userlist.err-not-available')}
- )}
- >
- );
-}
diff --git a/frontend/src/ui/pages/stulbe/Config.tsx b/frontend/src/ui/pages/stulbe/Config.tsx
deleted file mode 100644
index ffb94f5..0000000
--- a/frontend/src/ui/pages/stulbe/Config.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useModule } from '../../../lib/react-utils';
-import Stulbe from '../../../lib/stulbe-lib';
-import apiReducer, { modules } from '../../../store/api/reducer';
-import Field from '../../components/Field';
-
-export default function StulbeConfigPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- params: RouteComponentProps,
-): React.ReactElement {
- const { t } = useTranslation();
- const [stulbeConfig, setStulbeConfig] = useModule(modules.stulbeConfig);
- const [testResult, setTestResult] = useState(null);
- const dispatch = useDispatch();
-
- const busy = stulbeConfig === null;
- const active = stulbeConfig?.enabled ?? false;
-
- const test = async () => {
- try {
- const client = new Stulbe(stulbeConfig.endpoint);
- await client.auth(stulbeConfig.username, stulbeConfig.auth_key);
- setTestResult(t('backend.config.auth-success-message'));
- } catch (e) {
- setTestResult(e.message);
- }
- };
-
- return (
- <>
- {t('backend.config.header')}
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.stulbeConfigChanged({
- ...stulbeConfig,
- endpoint: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
-
- dispatch(
- apiReducer.actions.stulbeConfigChanged({
- ...stulbeConfig,
- username: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
-
- dispatch(
- apiReducer.actions.stulbeConfigChanged({
- ...stulbeConfig,
- auth_key: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
- {testResult ? (
-
- {testResult}
-
- ) : null}
- >
- );
-}
diff --git a/frontend/src/ui/pages/stulbe/Main.tsx b/frontend/src/ui/pages/stulbe/Main.tsx
deleted file mode 100644
index 01d4cad..0000000
--- a/frontend/src/ui/pages/stulbe/Main.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-
-export default function StulbePage({
- children,
-}: RouteComponentProps>): React.ReactElement {
- return <>{children}>;
-}
diff --git a/frontend/src/ui/pages/stulbe/Webhook.tsx b/frontend/src/ui/pages/stulbe/Webhook.tsx
deleted file mode 100644
index 47e846b..0000000
--- a/frontend/src/ui/pages/stulbe/Webhook.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-/* eslint-disable camelcase */
-import { RouteComponentProps } from '@reach/router';
-import React, { useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useSelector } from 'react-redux';
-import eventsubTests from '../../../data/eventsub-tests';
-import { useModule } from '../../../lib/react-utils';
-import Stulbe from '../../../lib/stulbe-lib';
-import { modules } from '../../../store/api/reducer';
-import { RootState } from '../../../store';
-
-interface UserData {
- id: string;
- login: string;
- display_name: string;
- profile_image_url: string;
-}
-
-interface SyncError {
- ok: false;
- error: string;
-}
-
-const eventSubTestFn = {
- 'channel.update': (send) => {
- send(eventsubTests['channel.update']);
- },
- 'channel.follow': (send) => {
- send(eventsubTests['channel.follow']);
- },
- 'channel.subscribe': (send) => {
- send(eventsubTests['channel.subscribe']);
- },
- 'channel.subscription.gift': (send) => {
- send(eventsubTests['channel.subscription.gift']);
- setTimeout(() => {
- send(eventsubTests['channel.subscribe']);
- }, 2000);
- },
- 'channel.subscription.message': (send) => {
- send(eventsubTests['channel.subscribe']);
- setTimeout(() => {
- send(eventsubTests['channel.subscription.message']);
- }, 2000);
- },
- 'channel.cheer': (send) => {
- send(eventsubTests['channel.cheer']);
- },
- 'channel.raid': (send) => {
- send(eventsubTests['channel.raid']);
- },
-};
-
-export default function StulbeWebhooksPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const { t } = useTranslation();
- const kv = useSelector((state: RootState) => state.api.client);
- const [stulbeConfig] = useModule(modules.stulbeConfig);
- const [userStatus, setUserStatus] = useState(null);
- const [client, setClient] = useState(null);
-
- const getUserInfo = async () => {
- try {
- const res = (await client.makeRequest(
- 'GET',
- 'api/twitch/user',
- )) as UserData;
- setUserStatus(res);
- } catch (e) {
- setUserStatus({ ok: false, error: e.message });
- }
- };
- const startAuthFlow = async () => {
- const res = (await client.makeRequest('POST', 'api/twitch/authorize')) as {
- auth_url: string;
- };
- const win = window.open(
- res.auth_url,
- '_blank',
- 'height=800,width=520,scrollbars=yes,status=yes',
- );
- // Hack, have to poll because no events are reliable for this
- const iv = setInterval(() => {
- if (win.closed) {
- clearInterval(iv);
- setUserStatus(null);
- getUserInfo();
- }
- }, 1000);
- };
-
- const sendFakeEvent = async (event: keyof typeof eventSubTestFn) => {
- eventSubTestFn[event]((data) => {
- kv.putJSON('stulbe/ev/webhook', {
- ...data,
- subscription: {
- ...data.subscription,
- created_at: new Date().toISOString(),
- },
- });
- });
- };
-
- // eslint-disable-next-line consistent-return
- useEffect(() => {
- if (client) {
- // Get user info
- getUserInfo();
- } else if (
- stulbeConfig &&
- stulbeConfig.enabled &&
- stulbeConfig.endpoint &&
- stulbeConfig.auth_key &&
- stulbeConfig.username
- ) {
- const tryAuth = async () => {
- // Try authenticating
- const stulbeClient = new Stulbe(stulbeConfig.endpoint);
- await stulbeClient.auth(stulbeConfig.username, stulbeConfig.auth_key);
- setClient(stulbeClient);
- };
- tryAuth();
- }
- }, [stulbeConfig, client]);
-
- if (!stulbeConfig.enabled) {
- return (
- <>
- {t('backend.webhook.err-not-enabled')}
- >
- );
- }
-
- let userBlock = {t('backend.webhook.loading')};
- if (userStatus !== null) {
- if ('id' in userStatus) {
- userBlock = (
- <>
-
-
{t('backend.config.authenticated')}
-
-
{userStatus.display_name}
-
- >
- );
- } else {
- userBlock = t('backend.webhook.err-no-user');
- }
- }
-
- return (
- <>
- {t('backend.webhook.header')}
-
-
- {t('backend.webhook.current-status')}
-
-
{userBlock}
-
-
-
{t('backend.webhook.auth-message')}
-
-
-
- {t('backend.webhook.fake-header')}
-
- {Object.keys(eventSubTestFn).map((ev: keyof typeof eventsubTests) => (
-
- ))}
- >
- );
-}
diff --git a/frontend/src/ui/pages/twitch/APISettings.tsx b/frontend/src/ui/pages/twitch/APISettings.tsx
deleted file mode 100644
index d338742..0000000
--- a/frontend/src/ui/pages/twitch/APISettings.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-import { Trans, useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useModule } from '../../../lib/react-utils';
-import apiReducer, { modules } from '../../../store/api/reducer';
-import Field from '../../components/Field';
-
-export default function TwitchBotSettingsPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- params: RouteComponentProps,
-): React.ReactElement {
- const { t } = useTranslation();
- const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
- const dispatch = useDispatch();
-
- const busy = twitchConfig === null;
- const active = twitchConfig?.enabled ?? false;
-
- return (
- <>
- {t('twitch.config.header')}
-
-
-
-
-
{t('twitch.config.apiguide-1')}
-
- {'- '}
-
- {'Go to '}
-
- https://dev.twitch.tv/console/apps/create
-
-
-
-
- {'- '}
- {t('twitch.config.apiguide-3')}
-
-
- - OAuth Redirect URLs
- - http://localhost:4337/oauth
- - Category
- - Broadcasting Suite
-
- {'- '}
-
- Once created, create a New Secret, then copy both fields below!
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchConfigChanged({
- ...twitchConfig,
- api_client_id: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchConfigChanged({
- ...twitchConfig,
- api_client_secret: ev.target.value,
- }),
- )
- }
- />
-
-
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/twitch/Alerts.tsx b/frontend/src/ui/pages/twitch/Alerts.tsx
deleted file mode 100644
index 7fc0949..0000000
--- a/frontend/src/ui/pages/twitch/Alerts.tsx
+++ /dev/null
@@ -1,419 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useModule } from '../../../lib/react-utils';
-import apiReducer, { modules } from '../../../store/api/reducer';
-import Field from '../../components/Field';
-import MessageArray from '../../components/MessageArray';
-import TabbedView from '../../components/TabbedView';
-
-export default function TwitchBotAlertsPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const [twitchConfig] = useModule(modules.twitchConfig);
- const [stulbeConfig] = useModule(modules.stulbeConfig);
- const [twitchBotAlerts, setTwitchBotAlerts] = useModule(
- modules.twitchBotAlerts,
- );
- const { t } = useTranslation();
- const dispatch = useDispatch();
-
- const botActive = twitchConfig?.enable_bot ?? false;
- const stulbeActive = stulbeConfig?.enabled ?? false;
-
- if (!botActive) {
- return (
- <>
- {t('twitch.bot-alerts.header')}
- {t('twitch.bot-alerts.err-twitchbot-disabled')}
- >
- );
- }
-
- if (!stulbeActive) {
- return (
- <>
- {t('twitch.bot-alerts.header')}
- {t('twitch.bot-alerts.err-stulbe-disabled')}
- >
- );
- }
-
- return (
- <>
- {t('twitch.bot-alerts.header')}
-
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotAlertsChanged({
- ...twitchBotAlerts,
- follow: {
- ...twitchBotAlerts.follow,
- messages,
- },
- }),
- )
- }
- />
-
-
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotAlertsChanged({
- ...twitchBotAlerts,
- subscription: {
- ...twitchBotAlerts.subscription,
- messages,
- },
- }),
- )
- }
- />
-
-
-
-
- {t('twitch.bot-alerts.variation-header')}
-
- {twitchBotAlerts?.subscription?.variations?.map((variation, i) => (
-
-
-
-
-
-
-
-
-
- {variation.min_streak ? (
-
-
- dispatch(
- apiReducer.actions.twitchBotAlertsChanged({
- ...twitchBotAlerts,
- subscription: {
- ...twitchBotAlerts.subscription,
- variations:
- // Replace messages in nth variation
- twitchBotAlerts?.subscription?.variations.map(
- (v, j) =>
- j === i
- ? {
- ...v,
- min_streak: ev.target.value,
- }
- : v,
- ),
- },
- }),
- )
- }
- />
-
- ) : null}
-
-
- dispatch(
- apiReducer.actions.twitchBotAlertsChanged({
- ...twitchBotAlerts,
- subscription: {
- ...twitchBotAlerts.subscription,
- variations:
- // Replace messages in nth variation
- twitchBotAlerts?.subscription?.variations.map(
- (v, j) => (j === i ? { ...v, messages } : v),
- ),
- },
- }),
- )
- }
- />
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotAlertsChanged({
- ...twitchBotAlerts,
- gift_sub: {
- ...twitchBotAlerts.gift_sub,
- messages,
- },
- }),
- )
- }
- />
-
-
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotAlertsChanged({
- ...twitchBotAlerts,
- raid: {
- ...twitchBotAlerts.raid,
- messages,
- },
- }),
- )
- }
- />
-
-
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotAlertsChanged({
- ...twitchBotAlerts,
- cheer: {
- ...twitchBotAlerts.cheer,
- messages,
- },
- }),
- )
- }
- />
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/twitch/BotSettings.tsx b/frontend/src/ui/pages/twitch/BotSettings.tsx
deleted file mode 100644
index 5695a9a..0000000
--- a/frontend/src/ui/pages/twitch/BotSettings.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-import { Trans, useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useModule } from '../../../lib/react-utils';
-import apiReducer, { modules } from '../../../store/api/reducer';
-import Field from '../../components/Field';
-
-export default function TwitchBotSettingsPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- params: RouteComponentProps,
-): React.ReactElement {
- const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
- const [twitchBotConfig, setTwitchBotConfig] = useModule(
- modules.twitchBotConfig,
- );
- const { t } = useTranslation();
- const dispatch = useDispatch();
-
- const busy = twitchConfig === null;
- const twitchActive = twitchConfig?.enabled ?? false;
- const botActive = twitchConfig?.enable_bot ?? false;
- const active = twitchActive && botActive;
-
- return (
- <>
- {t('twitch.bot.header')}
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotConfigChanged({
- ...twitchBotConfig,
- channel: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotConfigChanged({
- ...twitchBotConfig,
- username: ev.target.value,
- }),
- )
- }
- />
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotConfigChanged({
- ...twitchBotConfig,
- oauth: ev.target.value,
- }),
- )
- }
- />
-
-
-
- {
- 'You can get this by logging in with the bot account and going here: '
- }
-
- https://twitchapps.com/tmi/
-
-
-
-
-
-
-
-
-
-
-
-
- dispatch(
- apiReducer.actions.twitchBotConfigChanged({
- ...twitchBotConfig,
- chat_history: parseInt(ev.target.value, 10) ?? 0,
- }),
- )
- }
- />
-
-
- {t('twitch.bot.suf-messages')}
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/twitch/Commands.tsx b/frontend/src/ui/pages/twitch/Commands.tsx
deleted file mode 100644
index 8cc53b9..0000000
--- a/frontend/src/ui/pages/twitch/Commands.tsx
+++ /dev/null
@@ -1,331 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useModule } from '../../../lib/react-utils';
-import { modules } from '../../../store/api/reducer';
-import Modal from '../../components/Modal';
-import {
- AccessLevelType,
- TwitchBotCustomCommand,
-} from '../../../store/api/types';
-import Field from '../../components/Field';
-
-interface CommandItemProps {
- name: string;
- item: TwitchBotCustomCommand;
- onToggleState: () => void;
- onEdit: () => void;
- onDelete: () => void;
-}
-function CommandItem({
- name,
- item,
- onToggleState,
- onEdit,
- onDelete,
-}: CommandItemProps) {
- const { t } = useTranslation();
- const [expanded, setExpanded] = useState(false);
-
- return (
-
-
- {expanded ? (
-
- {t('twitch.commands.response')}:{' '}
-
{item.response}
-
-
- ) : null}
-
- );
-}
-
-interface CommandModalProps {
- active: boolean;
- onConfirm: (newName: string, r: TwitchBotCustomCommand) => void;
- onClose: () => void;
- initialData?: TwitchBotCustomCommand;
- initialName?: string;
- title: string;
- confirmText: string;
-}
-
-function CommandModal({
- active,
- onConfirm,
- onClose,
- initialName,
- initialData,
- title,
- confirmText,
-}: CommandModalProps) {
- const [name, setName] = useState(initialName ?? '');
- const [description, setDescription] = useState(
- initialData?.description ?? '',
- );
- const [accessLevel, setAccessLevel] = useState(
- initialData?.access_level ?? 'everyone',
- );
- const [response, setResponse] = useState(initialData?.response ?? '');
-
- const { t } = useTranslation();
- const slugify = (str: string) =>
- str.toLowerCase().replace(/[^a-zA-Z0-9!.-_@:;'"<>]/gi, '-');
- const validForm = name !== '' && response !== '';
-
- const confirm = () => {
- if (onConfirm) {
- onConfirm(name, {
- description,
- response,
- enabled: initialData?.enabled ?? false,
- access_level: accessLevel,
- });
- }
- };
-
- return (
- confirm()}
- onClose={() => onClose()}
- >
-
-
-
-
- setName(slugify(ev.target.value))}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{t('twitch.commands.access-level-help')}
-
-
-
-
- );
-}
-
-export default function TwitchBotCommandsPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const [commands, setCommands] = useModule(modules.twitchBotCommands);
- const dispatch = useDispatch();
- const { t } = useTranslation();
-
- const [createModal, setCreateModal] = useState(false);
- const [showModifyCommand, setShowModifyCommand] = useState(null);
- const [commandFilter, setCommandFilter] = useState('');
- const commandFilterLC = commandFilter.toLowerCase();
-
- const createCommand = (cmd: string, data: TwitchBotCustomCommand): void => {
- dispatch(
- setCommands({
- ...commands,
- [cmd]: data,
- }),
- );
- setCreateModal(false);
- };
-
- const modifyCommand = (
- oldName: string,
- newName: string,
- data: TwitchBotCustomCommand,
- ): void => {
- dispatch(
- setCommands({
- ...commands,
- [oldName]: undefined,
- [newName]: {
- ...commands[oldName],
- ...data,
- },
- }),
- );
- setShowModifyCommand(null);
- };
-
- const deleteCommand = (cmd: string): void => {
- dispatch(
- setCommands({
- ...commands,
- [cmd]: undefined,
- }),
- );
- };
-
- const toggleCommand = (cmd: string): void => {
- dispatch(
- setCommands({
- ...commands,
- [cmd]: {
- ...commands[cmd],
- enabled: !commands[cmd].enabled,
- },
- }),
- );
- };
-
- return (
- <>
- {t('twitch.commands.header')}
-
-
-
-
-
-
- setCommandFilter(ev.target.value)}
- />
-
-
-
- setCreateModal(false)}
- />
- {showModifyCommand ? (
-
- modifyCommand(showModifyCommand, newName, cmdData)
- }
- initialName={showModifyCommand}
- initialData={showModifyCommand ? commands[showModifyCommand] : null}
- onClose={() => setShowModifyCommand(null)}
- />
- ) : null}
-
- {Object.keys(commands ?? {})
- ?.filter((cmd) => cmd.toLowerCase().includes(commandFilterLC))
- .map((cmd) => (
- deleteCommand(cmd)}
- onEdit={() => setShowModifyCommand(cmd)}
- onToggleState={() => toggleCommand(cmd)}
- />
- ))}
-
- >
- );
-}
diff --git a/frontend/src/ui/pages/twitch/Main.tsx b/frontend/src/ui/pages/twitch/Main.tsx
deleted file mode 100644
index 1ccf17d..0000000
--- a/frontend/src/ui/pages/twitch/Main.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React from 'react';
-
-export default function TwitchBotPage({
- children,
-}: RouteComponentProps>): React.ReactElement {
- return <>{children}>;
-}
diff --git a/frontend/src/ui/pages/twitch/Timers.tsx b/frontend/src/ui/pages/twitch/Timers.tsx
deleted file mode 100644
index 0f194a3..0000000
--- a/frontend/src/ui/pages/twitch/Timers.tsx
+++ /dev/null
@@ -1,343 +0,0 @@
-import { RouteComponentProps } from '@reach/router';
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import prettyTime from 'pretty-ms';
-import { useModule } from '../../../lib/react-utils';
-import { modules } from '../../../store/api/reducer';
-import Modal from '../../components/Modal';
-import { TwitchBotTimer } from '../../../store/api/types';
-import Field from '../../components/Field';
-import Interval, { hours, minutes } from '../../components/Interval';
-import MessageArray from '../../components/MessageArray';
-
-interface TimerItemProps {
- item: TwitchBotTimer;
- onToggleState: () => void;
- onEdit: () => void;
- onDelete: () => void;
-}
-function TimerItem({ item, onToggleState, onEdit, onDelete }: TimerItemProps) {
- const { t } = useTranslation();
- const [expanded, setExpanded] = useState(false);
-
- return (
-
-
-
- {item.enabled ? (
- <>
- {item.name}
(
- {t('twitch.timers.condition-text', {
- time: prettyTime(item.minimum_delay * 1000),
- messages: item.minimum_chat_activity,
- })}
- )
- >
- ) : (
-
- {item.name}
-
- )}
-
- setExpanded(!expanded)}
- >
-
- ❯
-
-
-
- {expanded ? (
-
- {t('twitch.timers.messages')}:{' '}
- {item.messages.map((message, index) => (
-
{message}
- ))}
-
-
- ) : null}
-
- );
-}
-
-interface TimerModalProps {
- active: boolean;
- onConfirm: (newName: string, r: TwitchBotTimer) => void;
- onClose: () => void;
- initialData?: TwitchBotTimer;
- initialName?: string;
- title: string;
- confirmText: string;
-}
-
-function TimerModal({
- active,
- onConfirm,
- onClose,
- initialName,
- initialData,
- title,
- confirmText,
-}: TimerModalProps) {
- const [name, setName] = useState(initialName ?? '');
- const [messages, setMessages] = useState(initialData?.messages ?? ['']);
- const [minDelay, setMinDelay] = useState(initialData?.minimum_delay ?? 300);
- const [minActivity, setMinActivity] = useState(
- initialData?.minimum_chat_activity ?? 5,
- );
-
- const { t } = useTranslation();
- const validForm =
- name !== '' && messages.length > 0 && messages.every((msg) => msg !== '');
-
- const confirm = () => {
- if (onConfirm) {
- onConfirm(name, {
- name,
- messages,
- minimum_chat_activity: minActivity,
- minimum_delay: minDelay,
- enabled: initialData?.enabled ?? false,
- });
- }
- };
-
- return (
- confirm()}
- onClose={() => onClose()}
- >
-
-
-
-
- setName(ev.target.value)}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- setMessages(changed)}
- />
-
-
-
- );
-}
-
-export default function TwitchBotTimersPage(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- props: RouteComponentProps,
-): React.ReactElement {
- const [twitchConfig] = useModule(modules.twitchConfig);
- const [timerConfig, setTimerConfig] = useModule(modules.twitchBotTimers);
- const dispatch = useDispatch();
- const { t } = useTranslation();
-
- const [createModal, setCreateModal] = useState(false);
- const [showModifyTimer, setShowModifyTimer] = useState(null);
- const [timerFilter, setTimerFilter] = useState('');
- const timerFilterLC = timerFilter.toLowerCase();
-
- const botActive = twitchConfig?.enable_bot ?? false;
-
- const createTimer = (name: string, data: TwitchBotTimer): void => {
- dispatch(
- setTimerConfig({
- ...timerConfig,
- timers: {
- ...timerConfig.timers,
- [name]: data,
- },
- }),
- );
- setCreateModal(false);
- };
-
- const modifyTimer = (
- oldName: string,
- newName: string,
- data: TwitchBotTimer,
- ): void => {
- dispatch(
- setTimerConfig({
- ...timerConfig,
- timers: {
- ...timerConfig.timers,
- [oldName]: undefined,
- [newName]: {
- ...timerConfig.timers[oldName],
- ...data,
- },
- },
- }),
- );
- setShowModifyTimer(null);
- };
-
- const deleteTimer = (cmd: string): void => {
- dispatch(
- setTimerConfig({
- ...timerConfig,
- timers: {
- ...timerConfig.timers,
- [cmd]: undefined,
- },
- }),
- );
- };
-
- const toggleTimer = (cmd: string): void => {
- dispatch(
- setTimerConfig({
- ...timerConfig,
- timers: {
- ...timerConfig.timers,
- [cmd]: {
- ...timerConfig.timers[cmd],
- enabled: !timerConfig.timers[cmd].enabled,
- },
- },
- }),
- );
- };
-
- if (!botActive) {
- return (
- <>
- {t('twitch.timers.header')}
- {t('twitch.timers.err-twitchbot-disabled')}
- >
- );
- }
-
- return (
- <>
- {t('twitch.timers.header')}
-
-
-
-
-
-
- setTimerFilter(ev.target.value)}
- />
-
-
-
- setCreateModal(false)}
- />
- {showModifyTimer ? (
-
- modifyTimer(showModifyTimer, newName, cmdData)
- }
- initialName={showModifyTimer}
- initialData={
- showModifyTimer ? timerConfig.timers[showModifyTimer] : null
- }
- onClose={() => setShowModifyTimer(null)}
- />
- ) : null}
-
- {Object.keys(timerConfig?.timers ?? {})
- ?.filter((cmd) => cmd.toLowerCase().includes(timerFilterLC))
- .map((timer) => (
- deleteTimer(timer)}
- onEdit={() => setShowModifyTimer(timer)}
- onToggleState={() => toggleTimer(timer)}
- />
- ))}
-
- >
- );
-}