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

Update frontend with new deps and linting

This commit is contained in:
Ash Keel 2022-11-18 15:46:50 +01:00
parent 05cda342fd
commit 94d57b9a70
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
32 changed files with 6944 additions and 2034 deletions

1
driver.interface.go Normal file
View file

@ -0,0 +1 @@
package main

View file

@ -1,23 +1,39 @@
module.exports = { module.exports = {
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'import'], plugins: ['@typescript-eslint', 'import'],
extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended', 'prettier'], extends: [
'airbnb-base',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier',
],
root: true,
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.cjs'],
},
rules: { rules: {
'no-console': 0, 'no-console': 0,
'import/extensions': 0, 'import/extensions': 0,
'no-use-before-define': 'off', 'no-use-before-define': 'off',
'no-shadow': 'off', 'no-shadow': 'off',
'no-unused-vars': 'off',
'no-void': ['error', { allowAsStatement: true }],
'@typescript-eslint/no-use-before-define': ['error'], '@typescript-eslint/no-use-before-define': ['error'],
'@typescript-eslint/no-shadow': ['error'], '@typescript-eslint/no-shadow': ['error'],
'default-case': 'off', 'default-case': 'off',
'consistent-return': 'off', 'consistent-return': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '^_' }],
'@typescript-eslint/no-unsafe-return': ['error'], '@typescript-eslint/no-unsafe-return': ['error'],
'@typescript-eslint/switch-exhaustiveness-check': ['error'], '@typescript-eslint/switch-exhaustiveness-check': ['error'],
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
}, },
settings: { settings: {
'import/resolver': { 'import/resolver': {
node: { node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'], extensions: ['.js', '.jsx', '.ts', '.tsx'],
moduleDirectory: ['node_modules', 'src/'],
}, },
typescript: {}, typescript: {},
}, },
@ -25,5 +41,5 @@ module.exports = {
env: { env: {
browser: true, browser: true,
}, },
ignorePatterns: ['OLD/*'], ignorePatterns: ['OLD/*', 'wailsjs/*', 'dist/*'],
}; };

File diff suppressed because it is too large Load diff

View file

@ -4,39 +4,39 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@billjs/event-emitter": "^1.0.3", "@billjs/event-emitter": "^1.0.3",
"@fontsource/space-mono": "^4.5.0", "@fontsource/space-mono": "^4.5.10",
"@radix-ui/colors": "^0.1.8", "@radix-ui/colors": "^0.1.8",
"@radix-ui/react-alert-dialog": "^0.1.5", "@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^0.1.4", "@radix-ui/react-checkbox": "^1.0.1",
"@radix-ui/react-dialog": "^0.1.5", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-icons": "^1.0.3", "@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-label": "^0.1.3", "@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-tabs": "^0.1.4", "@radix-ui/react-tabs": "^1.0.1",
"@radix-ui/react-toolbar": "^0.1.4", "@radix-ui/react-toolbar": "^1.0.1",
"@reduxjs/toolkit": "^1.5.1", "@redux-devtools/extension": "^3.2.3",
"@stitches/react": "^1.2.6", "@reduxjs/toolkit": "^1.9.0",
"@strimertul/kilovolt-client": "^6.2.0", "@stitches/react": "^1.2.8",
"@types/node": "^15.0.2", "@strimertul/kilovolt-client": "^6.4.0",
"@types/react": "^17.0.5", "@types/node": "^18.11.9",
"@types/react-dom": "^17.0.4", "@types/react": "^18.0.25",
"@vitejs/plugin-react": "^1.0.9", "@types/react-dom": "^18.0.9",
"i18next": "^20.6.1", "@vitejs/plugin-react": "^2.2.0",
"i18next": "^22.0.6",
"inter-ui": "^3.19.3", "inter-ui": "^3.19.3",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"overlayscrollbars": "^1.13.1", "overlayscrollbars": "^2.0.1",
"overlayscrollbars-react": "^0.2.3", "overlayscrollbars-react": "^0.5.0",
"postcss-import": "^14.0.2", "postcss-import": "^15.0.0",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-i18next": "^11.12.0", "react-i18next": "^12.0.0",
"react-redux": "^7.2.4", "react-redux": "^8.0.5",
"react-router-dom": "^6.1.1", "react-router-dom": "^6.4.3",
"react-toastify": "^8.1.0", "react-toastify": "^9.1.1",
"redux-devtools-extension": "^2.13.9", "redux-thunk": "^2.4.2",
"redux-thunk": "^2.3.0", "sass": "^1.56.1",
"sass": "^1.32.12", "typescript": "^4.9.3",
"typescript": "^4.2.4", "vite": "^3.2.4"
"vite": "^2.6.14"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -47,15 +47,15 @@
"last 1 Chrome version" "last 1 Chrome version"
], ],
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^5.43.0",
"eslint": "^7.26.0", "eslint": "^8.27.0",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^2.4.0", "eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.3.0", "prettier": "^2.7.1",
"rimraf": "^3.0.2" "rimraf": "^3.0.2"
} }
} }

View file

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import 'inter-ui/inter.css'; import 'inter-ui/inter.css';
import '@fontsource/space-mono/index.css'; import '@fontsource/space-mono/index.css';
import 'normalize.css/normalize.css'; import 'normalize.css/normalize.css';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import 'overlayscrollbars/css/OverlayScrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import './locale/setup'; import './locale/setup';
@ -19,9 +19,9 @@ globalStyles();
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<BrowserRouter> <HashRouter>
<App /> <App />
</BrowserRouter> </HashRouter>
</Provider>, </Provider>,
document.getElementById('main'), document.getElementById('main'),
); );

View file

@ -1,13 +1,22 @@
import { ActionCreatorWithOptionalPayload, AsyncThunk } from '@reduxjs/toolkit'; import {
ActionCreatorWithOptionalPayload,
AsyncThunk,
Draft,
} from '@reduxjs/toolkit';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
KilovoltMessage, KilovoltMessage,
SubscriptionHandler, SubscriptionHandler,
} from '@strimertul/kilovolt-client'; } from '@strimertul/kilovolt-client';
import { RootState } from '../store'; import { RootState, useAppDispatch } from '../store';
import apiReducer, { getUserPoints } from '../store/api/reducer'; import apiReducer, { getUserPoints } from '../store/api/reducer';
import { APIState, LoyaltyStorage, RequestStatus } from '../store/api/types'; import {
APIState,
LoyaltyPointsEntry,
LoyaltyStorage,
RequestStatus,
} from '../store/api/types';
interface LoadStatus { interface LoadStatus {
load: RequestStatus; load: RequestStatus;
@ -20,9 +29,9 @@ export function useLiveKeyRaw(key: string) {
useEffect(() => { useEffect(() => {
const subscriber: SubscriptionHandler = (v) => setData(v); const subscriber: SubscriptionHandler = (v) => setData(v);
client.subscribeKey(key, subscriber); void client.subscribeKey(key, subscriber);
return () => { return () => {
client.unsubscribeKey(key, subscriber); void client.unsubscribeKey(key, subscriber);
}; };
}, []); }, []);
@ -31,7 +40,7 @@ export function useLiveKeyRaw(key: string) {
export function useLiveKey<T>(key: string): T { export function useLiveKey<T>(key: string): T {
const data = useLiveKeyRaw(key); const data = useLiveKeyRaw(key);
return data ? JSON.parse(data) : null; return data ? (JSON.parse(data) as T) : null;
} }
export function useModule<T>({ export function useModule<T>({
@ -42,7 +51,7 @@ export function useModule<T>({
asyncSetter, asyncSetter,
}: { }: {
key: string; key: string;
selector: (state: APIState) => T; selector: (state: Draft<APIState>) => T;
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
getter: AsyncThunk<T, void, {}>; getter: AsyncThunk<T, void, {}>;
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
@ -62,15 +71,15 @@ export function useModule<T>({
const saveStatus = useSelector( const saveStatus = useSelector(
(state: RootState) => state.api.requestStatus[`save-${key}`], (state: RootState) => state.api.requestStatus[`save-${key}`],
); );
const dispatch = useDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
dispatch(getter()); void dispatch(getter());
const subscriber = (newValue) => { const subscriber: SubscriptionHandler = (newValue) => {
dispatch(asyncSetter(JSON.parse(newValue) as T)); void dispatch(asyncSetter(JSON.parse(newValue) as T));
}; };
client.subscribeKey(key, subscriber); void client.subscribeKey(key, subscriber);
return () => { return () => {
client.unsubscribeKey(key, subscriber); void client.unsubscribeKey(key, subscriber);
dispatch( dispatch(
apiReducer.actions.requestKeysRemoved([`save-${key}`, `load-${key}`]), apiReducer.actions.requestKeysRemoved([`save-${key}`, `load-${key}`]),
); );
@ -93,7 +102,7 @@ export function useStatus(
const [localStatus, setlocalStatus] = useState(status); const [localStatus, setlocalStatus] = useState(status);
const maxTime = Date.now() - interval; const maxTime = Date.now() - interval;
useEffect(() => { useEffect(() => {
const remaining = status?.updated.getTime() - maxTime; const remaining = status ? status.updated.getTime() - maxTime : null;
if (remaining) { if (remaining) {
setTimeout(() => { setTimeout(() => {
setlocalStatus(null); setlocalStatus(null);
@ -109,17 +118,19 @@ export function useUserPoints(): LoyaltyStorage {
const prefix = 'loyalty/points/'; const prefix = 'loyalty/points/';
const client = useSelector((state: RootState) => state.api.client); const client = useSelector((state: RootState) => state.api.client);
const data = useSelector((state: RootState) => state.api.loyalty.users); const data = useSelector((state: RootState) => state.api.loyalty.users);
const dispatch = useDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
dispatch(getUserPoints()); void dispatch(getUserPoints());
const subscriber: SubscriptionHandler = (newValue, key) => { const subscriber: SubscriptionHandler = (newValue, key) => {
const user = key.substring(prefix.length); const user = key.substring(prefix.length);
const entry = JSON.parse(newValue); const entry = JSON.parse(newValue) as LoyaltyPointsEntry;
dispatch(apiReducer.actions.loyaltyUserPointsChanged({ user, entry })); void dispatch(
apiReducer.actions.loyaltyUserPointsChanged({ user, entry }),
);
}; };
client.subscribePrefix(prefix, subscriber); void client.subscribePrefix(prefix, subscriber);
return () => { return () => {
client.unsubscribePrefix(prefix, subscriber); void client.unsubscribePrefix(prefix, subscriber);
}; };
}, []); }, []);
return data; return data;

View file

@ -2,6 +2,15 @@ export interface StulbeOptions {
controller: AbortController; controller: AbortController;
} }
type stulbeAuthResult =
| {
ok: true;
token: string;
}
| {
error: string;
};
export default class Stulbe { export default class Stulbe {
private token: string; private token: string;
@ -12,7 +21,7 @@ export default class Stulbe {
) {} ) {}
public async auth(user: string, key: string): Promise<boolean> { public async auth(user: string, key: string): Promise<boolean> {
const res = await ( const res: stulbeAuthResult = (await (
await fetch(`${this.endpoint}/api/auth`, { await fetch(`${this.endpoint}/api/auth`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -24,23 +33,23 @@ export default class Stulbe {
}), }),
signal: this.options?.controller.signal, signal: this.options?.controller.signal,
}) })
).json(); ).json()) as stulbeAuthResult;
if (!res.ok) { if ('error' in res) {
throw new Error(res.error); throw new Error(res.error);
} }
this.token = res.token; this.token = res.token;
return res.ok; return res.ok;
} }
public async makeRequest<T>( public async makeRequest<T, B extends BodyInit | URLSearchParams>(
method: string, method: string,
path: string, path: string,
body?: any, body?: B,
): Promise<T> { ): Promise<T> {
if (!this.token) { if (!this.token) {
throw new Error('not authenticated'); throw new Error('not authenticated');
} }
const res = await ( const res = (await (
await fetch(`${this.endpoint}/${path}`, { await fetch(`${this.endpoint}/${path}`, {
method, method,
headers: { headers: {
@ -50,7 +59,7 @@ export default class Stulbe {
body, body,
signal: this.options?.controller.signal, signal: this.options?.controller.signal,
}) })
).json(); ).json()) as T;
return res; return res;
} }
} }

View file

@ -193,7 +193,8 @@
"no-redeems": "No pending redeems", "no-redeems": "No pending redeems",
"no-users": "No viewers found", "no-users": "No viewers found",
"refund": "Refund", "refund": "Refund",
"accept": "Accept" "accept": "Accept",
"hardcoded": "My Random hardcoded text"
}, },
"debug": { "debug": {
"dismiss-warning": "I am not afraid! ...well ok maybe a little", "dismiss-warning": "I am not afraid! ...well ok maybe a little",

View file

@ -3,7 +3,7 @@ import { initReactI18next } from 'react-i18next';
import en from './en/translation.json'; import en from './en/translation.json';
i18n.use(initReactI18next).init({ void i18n.use(initReactI18next).init({
resources: { resources: {
en: { en: {
translation: en, translation: en,

View file

@ -1,14 +1,16 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { import {
AnyAction,
AsyncThunk, AsyncThunk,
CaseReducer, CaseReducer,
createAction, createAction,
createAsyncThunk, createAsyncThunk,
createSlice, createSlice,
Dispatch,
PayloadAction, PayloadAction,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import KilovoltWS from '@strimertul/kilovolt-client'; import KilovoltWS from '@strimertul/kilovolt-client';
import { kvError } from '@strimertul/kilovolt-client/lib/messages'; import type { kvError } from '@strimertul/kilovolt-client/types/messages';
import { import {
APIState, APIState,
ConnectionStatus, ConnectionStatus,
@ -17,8 +19,13 @@ import {
LoyaltyStorage, LoyaltyStorage,
} from './types'; } from './types';
interface AppThunkAPI {
dispatch: Dispatch;
getState: () => unknown;
}
function makeGetterThunk<T>(key: string) { function makeGetterThunk<T>(key: string) {
return async (_: void, { getState }) => { return async (_: void, { getState }: AppThunkAPI) => {
const { api } = getState() as { api: APIState }; const { api } = getState() as { api: APIState };
return api.client.getJSON<T>(key); return api.client.getJSON<T>(key);
}; };
@ -29,13 +36,15 @@ function makeSetterThunk<T>(
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
getter: AsyncThunk<T, void, {}>, getter: AsyncThunk<T, void, {}>,
) { ) {
return async (data: T, { getState, dispatch }) => { return async (data: T, { getState, dispatch }: AppThunkAPI) => {
const { api } = getState() as { api: APIState }; const { api } = getState() as { api: APIState };
const result = await api.client.putJSON(key, data); const result = await api.client.putJSON(key, data);
if ('ok' in result) { if ('ok' in result) {
if (result.ok) { if (result.ok) {
// Re-load value from KV // Re-load value from KV
dispatch(getter()); // Need to do type fuckery to avoid cyclic redundancy
// (unless there's a better way that I'm missing)
void dispatch(getter() as unknown as AnyAction);
} }
} }
return result; return result;
@ -84,10 +93,10 @@ export const createWSClient = createAsyncThunk(
async (options: { address: string; password?: string }, { dispatch }) => { async (options: { address: string; password?: string }, { dispatch }) => {
const client = new KilovoltWS(options.address, options.password); const client = new KilovoltWS(options.address, options.password);
client.on('error', (err) => { client.on('error', (err) => {
dispatch(kvErrorReceived(err.data)); void dispatch(kvErrorReceived(err.data as kvError));
}); });
await client.wait(); await client.wait();
dispatch(setupClientReconnect(client)); await dispatch(setupClientReconnect(client));
return client; return client;
}, },
); );
@ -99,9 +108,9 @@ export const getUserPoints = createAsyncThunk(
const keys = await api.client.getKeysByPrefix(loyaltyPointsPrefix); const keys = await api.client.getKeysByPrefix(loyaltyPointsPrefix);
const userpoints: LoyaltyStorage = {}; const userpoints: LoyaltyStorage = {};
Object.entries(keys).forEach(([k, v]) => { Object.entries(keys).forEach(([k, v]) => {
userpoints[k.substr(loyaltyPointsPrefix.length)] = JSON.parse( userpoints[k.substring(loyaltyPointsPrefix.length)] = JSON.parse(
v as string, v,
); ) as LoyaltyPointsEntry;
}); });
return userpoints; return userpoints;
}, },
@ -131,6 +140,7 @@ export const modules = {
'http/config', 'http/config',
(state) => state.moduleConfigs?.httpConfig, (state) => state.moduleConfigs?.httpConfig,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.moduleConfigs.httpConfig = payload; state.moduleConfigs.httpConfig = payload;
}, },
), ),
@ -138,6 +148,7 @@ export const modules = {
'twitch/config', 'twitch/config',
(state) => state.moduleConfigs?.twitchConfig, (state) => state.moduleConfigs?.twitchConfig,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.moduleConfigs.twitchConfig = payload; state.moduleConfigs.twitchConfig = payload;
}, },
), ),
@ -145,6 +156,7 @@ export const modules = {
'twitch/bot-config', 'twitch/bot-config',
(state) => state.moduleConfigs?.twitchBotConfig, (state) => state.moduleConfigs?.twitchBotConfig,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.moduleConfigs.twitchBotConfig = payload; state.moduleConfigs.twitchBotConfig = payload;
}, },
), ),
@ -152,6 +164,7 @@ export const modules = {
'twitch/bot-custom-commands', 'twitch/bot-custom-commands',
(state) => state.twitchBot?.commands, (state) => state.twitchBot?.commands,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.twitchBot.commands = payload; state.twitchBot.commands = payload;
}, },
), ),
@ -159,6 +172,7 @@ export const modules = {
'twitch/bot-modules/timers/config', 'twitch/bot-modules/timers/config',
(state) => state.twitchBot?.timers, (state) => state.twitchBot?.timers,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.twitchBot.timers = payload; state.twitchBot.timers = payload;
}, },
), ),
@ -166,6 +180,7 @@ export const modules = {
'twitch/bot-modules/alerts/config', 'twitch/bot-modules/alerts/config',
(state) => state.twitchBot?.alerts, (state) => state.twitchBot?.alerts,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.twitchBot.alerts = payload; state.twitchBot.alerts = payload;
}, },
), ),
@ -173,6 +188,7 @@ export const modules = {
'stulbe/config', 'stulbe/config',
(state) => state.moduleConfigs?.stulbeConfig, (state) => state.moduleConfigs?.stulbeConfig,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.moduleConfigs.stulbeConfig = payload; state.moduleConfigs.stulbeConfig = payload;
}, },
), ),
@ -180,6 +196,7 @@ export const modules = {
'loyalty/config', 'loyalty/config',
(state) => state.moduleConfigs?.loyaltyConfig, (state) => state.moduleConfigs?.loyaltyConfig,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.moduleConfigs.loyaltyConfig = payload; state.moduleConfigs.loyaltyConfig = payload;
}, },
), ),
@ -187,6 +204,7 @@ export const modules = {
loyaltyRewardsKey, loyaltyRewardsKey,
(state) => state.loyalty.rewards, (state) => state.loyalty.rewards,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.loyalty.rewards = payload; state.loyalty.rewards = payload;
}, },
), ),
@ -194,6 +212,7 @@ export const modules = {
'loyalty/goals', 'loyalty/goals',
(state) => state.loyalty.goals, (state) => state.loyalty.goals,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.loyalty.goals = payload; state.loyalty.goals = payload;
}, },
), ),
@ -201,6 +220,7 @@ export const modules = {
'loyalty/redeem-queue', 'loyalty/redeem-queue',
(state) => state.loyalty.redeemQueue, (state) => state.loyalty.redeemQueue,
(state, { payload }) => { (state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.loyalty.redeemQueue = payload; state.loyalty.redeemQueue = payload;
}, },
), ),
@ -344,9 +364,9 @@ const apiReducer = createSlice({
setupClientReconnect = createAsyncThunk( setupClientReconnect = createAsyncThunk(
'api/setupClientReconnect', 'api/setupClientReconnect',
async (client: KilovoltWS, { dispatch }) => { (client: KilovoltWS, { dispatch }) => {
client.on('close', () => { client.on('close', () => {
setTimeout(async () => { setTimeout(() => {
console.info('Attempting reconnection'); console.info('Attempting reconnection');
client.reconnect(); client.reconnect();
}, 5000); }, 5000);
@ -366,7 +386,7 @@ setupClientReconnect = createAsyncThunk(
kvErrorReceived = createAsyncThunk( kvErrorReceived = createAsyncThunk(
'api/kvErrorReceived', 'api/kvErrorReceived',
async (error: kvError, { dispatch }) => { (error: kvError, { dispatch }) => {
switch (error.error) { switch (error.error) {
case 'authentication required': case 'authentication required':
case 'authentication failed': case 'authentication failed':

View file

@ -1,7 +1,7 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import KilovoltWS from '@strimertul/kilovolt-client'; import KilovoltWS from '@strimertul/kilovolt-client';
import { kvError } from '@strimertul/kilovolt-client/lib/messages'; import type { kvError } from '@strimertul/kilovolt-client/types/messages';
interface HTTPConfig { interface HTTPConfig {
bind: string; bind: string;

View file

@ -1,4 +1,5 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';
import thunkMiddleware from 'redux-thunk'; import thunkMiddleware from 'redux-thunk';
import apiReducer from './api/reducer'; import apiReducer from './api/reducer';
@ -14,5 +15,7 @@ const store = configureStore({
}); });
export type RootState = ReturnType<typeof store.getState>; export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export default store; export default store;

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
ChatBubbleIcon, ChatBubbleIcon,
DashboardIcon, DashboardIcon,
@ -17,7 +17,7 @@ import { ToastContainer } from 'react-toastify';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import Sidebar, { RouteSection } from './components/Sidebar'; import Sidebar, { RouteSection } from './components/Sidebar';
import ServerSettingsPage from './pages/ServerSettings'; import ServerSettingsPage from './pages/ServerSettings';
import { RootState } from '../store'; import { RootState, useAppDispatch } from '../store';
import { createWSClient } from '../store/api/reducer'; import { createWSClient } from '../store/api/reducer';
import { ConnectionStatus } from '../store/api/types'; import { ConnectionStatus } from '../store/api/types';
import { styled } from './theme'; import { styled } from './theme';
@ -50,7 +50,7 @@ const Spinner = styled('img', {
function Loading() { function Loading() {
return ( return (
<LoadingDiv> <LoadingDiv>
<Spinner src={spinner} alt="Loading..." /> <Spinner src={spinner as string} alt="Loading..." />
</LoadingDiv> </LoadingDiv>
); );
} }
@ -152,17 +152,17 @@ export default function App(): JSX.Element {
const connected = useSelector( const connected = useSelector(
(state: RootState) => state.api.connectionStatus, (state: RootState) => state.api.connectionStatus,
); );
const dispatch = useDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
if (!client) { if (!client) {
dispatch( void dispatch(
createWSClient({ createWSClient({
address: address:
process.env.NODE_ENV === 'development' process.env.NODE_ENV === 'development'
? 'ws://localhost:4337/ws' ? 'ws://localhost:4337/ws'
: `ws://${window.location.host}/ws`, : `ws://${window.location.host}/ws`,
password: localStorage.password, password: localStorage.password as string,
}), }),
); );
} }

View file

@ -75,6 +75,7 @@ export function DataTable<T>({
case 'desc': case 'desc':
return -result; return -result;
} }
return 0;
}); });
} }

View file

@ -155,8 +155,9 @@ export default function Sidebar({
const matchApp = useMatch({ path: resolved.pathname, end: true }); const matchApp = useMatch({ path: resolved.pathname, end: true });
const client = useSelector((state: RootState) => state.api.client); const client = useSelector((state: RootState) => state.api.client);
const [version, setVersion] = useState<string>(null); const [version, setVersion] = useState<string>(null);
const [lastVersion, setLastVersion] = const [lastVersion, setLastVersion] = useState<{ url: string; name: string }>(
useState<{ url: string; name: string }>(null); null,
);
const dev = version && version.startsWith('v0.0.0'); const dev = version && version.startsWith('v0.0.0');
async function fetchVersion() { async function fetchVersion() {
@ -174,7 +175,7 @@ export default function Sidebar({
}, },
}, },
); );
const data = await req.json(); const data = (await req.json()) as { html_url: string; name: string };
setLastVersion({ setLastVersion({
url: data.html_url, url: data.html_url,
name: data.name, name: data.name,
@ -186,12 +187,12 @@ export default function Sidebar({
} }
useEffect(() => { useEffect(() => {
fetchLastVersion(); void fetchLastVersion();
}, []); }, []);
useEffect(() => { useEffect(() => {
if (client) { if (client) {
fetchVersion(); void fetchVersion();
} }
}, [client]); }, [client]);
@ -205,7 +206,7 @@ export default function Sidebar({
<AppLink to={'/about'} status={matchApp ? 'active' : 'default'}> <AppLink to={'/about'} status={matchApp ? 'active' : 'default'}>
<AppName> <AppName>
<img <img
src={logo} src={logo as string}
style={{ height: '28px', marginBottom: '-2px' }} style={{ height: '28px', marginBottom: '-2px' }}
/> />
{APPNAME} {APPNAME}

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { useModule, useStatus } from '../../lib/react-utils'; import { useModule, useStatus } from '../../lib/react-utils';
@ -25,7 +25,7 @@ import {
TextBlock, TextBlock,
} from '../theme'; } from '../theme';
import eventsubTests from '../../data/eventsub-tests'; import eventsubTests from '../../data/eventsub-tests';
import { RootState } from '../../store'; import { RootState, useAppDispatch } from '../../store';
interface UserData { interface UserData {
id: string; id: string;
@ -41,36 +41,6 @@ interface SyncError {
error: string; 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']);
},
};
const TwitchUser = styled('div', { const TwitchUser = styled('div', {
display: 'flex', display: 'flex',
gap: '0.8rem', gap: '0.8rem',
@ -84,6 +54,11 @@ const TwitchPic = styled('img', {
}); });
const TwitchName = styled('p', { fontWeight: 'bold' }); const TwitchName = styled('p', { fontWeight: 'bold' });
interface authChallengeRequest {
// eslint-disable-next-line camelcase
auth_url: string;
}
function WebhookIntegration() { function WebhookIntegration() {
const { t } = useTranslation(); const { t } = useTranslation();
const [stulbeConfig] = useModule(modules.stulbeConfig); const [stulbeConfig] = useModule(modules.stulbeConfig);
@ -93,21 +68,21 @@ function WebhookIntegration() {
const getUserInfo = async () => { const getUserInfo = async () => {
try { try {
const res = (await client.makeRequest( const res = await client.makeRequest<UserData, null>(
'GET', 'GET',
'api/twitch/user', 'api/twitch/user',
)) as UserData; );
setUserStatus(res); setUserStatus(res);
} catch (e) { } catch (e) {
setUserStatus({ ok: false, error: e.message }); setUserStatus({ ok: false, error: (e as Error).message });
} }
}; };
const startAuthFlow = async () => { const startAuthFlow = async () => {
const res = (await client.makeRequest('POST', 'api/twitch/authorize')) as { const res = await client.makeRequest<authChallengeRequest, null>(
// eslint-disable-next-line camelcase 'POST',
auth_url: string; 'api/twitch/authorize',
}; );
const win = window.open( const win = window.open(
res.auth_url, res.auth_url,
'_blank', '_blank',
@ -118,28 +93,27 @@ function WebhookIntegration() {
if (win.closed) { if (win.closed) {
clearInterval(iv); clearInterval(iv);
setUserStatus(null); setUserStatus(null);
getUserInfo(); void getUserInfo();
} }
}, 1000); }, 1000);
}; };
const sendFakeEvent = async (event: keyof typeof eventSubTestFn) => { const sendFakeEvent = async (event: keyof typeof eventsubTests) => {
eventSubTestFn[event]((data) => { const data = eventsubTests[event];
kv.putJSON('stulbe/ev/webhook', { await kv.putJSON('stulbe/ev/webhook', {
...data, ...data,
subscription: { subscription: {
...data.subscription, ...data.subscription,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}, },
}); });
});
}; };
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
useEffect(() => { useEffect(() => {
if (client) { if (client) {
// Get user info // Get user info
getUserInfo(); void getUserInfo();
} else if ( } else if (
stulbeConfig && stulbeConfig &&
stulbeConfig.enabled && stulbeConfig.enabled &&
@ -153,7 +127,7 @@ function WebhookIntegration() {
await stulbeClient.auth(stulbeConfig.username, stulbeConfig.auth_key); await stulbeClient.auth(stulbeConfig.username, stulbeConfig.auth_key);
setClient(stulbeClient); setClient(stulbeClient);
}; };
tryAuth(); void tryAuth();
} }
}, [stulbeConfig, client]); }, [stulbeConfig, client]);
@ -177,21 +151,32 @@ function WebhookIntegration() {
</> </>
); );
} else { } else {
userBlock = t('pages.stulbe.err-no-user'); userBlock = <span>{t('pages.stulbe.err-no-user')}</span>;
} }
} }
return ( return (
<> <>
<p>{t('pages.stulbe.auth-message')}</p> <p>{t('pages.stulbe.auth-message')}</p>
<Button variation="primary" onClick={startAuthFlow} disabled={!client}> <Button
variation="primary"
onClick={() => {
void startAuthFlow();
}}
disabled={!client}
>
<ExternalLinkIcon /> {t('pages.stulbe.auth-button')} <ExternalLinkIcon /> {t('pages.stulbe.auth-button')}
</Button> </Button>
<SectionHeader>{t('pages.stulbe.current-status')}</SectionHeader> <SectionHeader>{t('pages.stulbe.current-status')}</SectionHeader>
{userBlock} {userBlock}
<SectionHeader>{t('pages.stulbe.sim-events')}</SectionHeader> <SectionHeader>{t('pages.stulbe.sim-events')}</SectionHeader>
<ButtonGroup> <ButtonGroup>
{Object.keys(eventSubTestFn).map((ev: keyof typeof eventsubTests) => ( {Object.keys(eventsubTests).map((ev: keyof typeof eventsubTests) => (
<Button key={ev} onClick={() => sendFakeEvent(ev)}> <Button
key={ev}
onClick={() => {
void sendFakeEvent(ev);
}}
>
{t(`pages.stulbe.sim.${ev}`, { defaultValue: ev })} {t(`pages.stulbe.sim.${ev}`, { defaultValue: ev })}
</Button> </Button>
))} ))}
@ -205,7 +190,7 @@ function BackendConfiguration() {
modules.stulbeConfig, modules.stulbeConfig,
); );
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const status = useStatus(loadStatus.save); const status = useStatus(loadStatus.save);
const active = stulbeConfig?.enabled ?? false; const active = stulbeConfig?.enabled ?? false;
const busy = const busy =
@ -217,14 +202,14 @@ function BackendConfiguration() {
await client.auth(stulbeConfig.username, stulbeConfig.auth_key); await client.auth(stulbeConfig.username, stulbeConfig.auth_key);
toast.success(t('pages.stulbe.test-success')); toast.success(t('pages.stulbe.test-success'));
} catch (e) { } catch (e) {
toast.error(e.message); toast.error((e as Error).message);
} }
}; };
return ( return (
<form <form
onSubmit={(ev) => { onSubmit={(ev) => {
dispatch(setStulbeConfig(stulbeConfig)); void dispatch(setStulbeConfig(stulbeConfig));
ev.preventDefault(); ev.preventDefault();
}} }}
> >
@ -236,15 +221,15 @@ function BackendConfiguration() {
placeholder={t('pages.stulbe.bind-placeholder')} placeholder={t('pages.stulbe.bind-placeholder')}
value={stulbeConfig?.endpoint ?? ''} value={stulbeConfig?.endpoint ?? ''}
disabled={busy} disabled={busy}
onChange={(e) => onChange={(e) => {
dispatch( void dispatch(
apiReducer.actions.stulbeConfigChanged({ apiReducer.actions.stulbeConfigChanged({
...stulbeConfig, ...stulbeConfig,
enabled: e.target.value.length > 0, enabled: e.target.value.length > 0,
endpoint: e.target.value, endpoint: e.target.value,
}), }),
) );
} }}
/> />
</Field> </Field>
<Field size="fullWidth"> <Field size="fullWidth">
@ -255,14 +240,14 @@ function BackendConfiguration() {
value={stulbeConfig?.username ?? ''} value={stulbeConfig?.username ?? ''}
required={true} required={true}
disabled={!active || busy} disabled={!active || busy}
onChange={(e) => onChange={(e) => {
dispatch( void dispatch(
apiReducer.actions.stulbeConfigChanged({ apiReducer.actions.stulbeConfigChanged({
...stulbeConfig, ...stulbeConfig,
username: e.target.value, username: e.target.value,
}), }),
) );
} }}
/> />
</Field> </Field>
<Field size="fullWidth"> <Field size="fullWidth">
@ -273,19 +258,25 @@ function BackendConfiguration() {
value={stulbeConfig?.auth_key ?? ''} value={stulbeConfig?.auth_key ?? ''}
disabled={!active || busy} disabled={!active || busy}
required={true} required={true}
onChange={(e) => onChange={(e) => {
dispatch( void dispatch(
apiReducer.actions.stulbeConfigChanged({ apiReducer.actions.stulbeConfigChanged({
...stulbeConfig, ...stulbeConfig,
auth_key: e.target.value, auth_key: e.target.value,
}), }),
) );
} }}
/> />
</Field> </Field>
<ButtonGroup> <ButtonGroup>
<SaveButton status={status} /> <SaveButton status={status} />
<Button type="button" disabled={!active || busy} onClick={() => test()}> <Button
type="button"
disabled={!active || busy}
onClick={() => {
void test();
}}
>
{t('pages.stulbe.test-button')} {t('pages.stulbe.test-button')}
</Button> </Button>
</ButtonGroup> </ButtonGroup>

View file

@ -1,8 +1,8 @@
import { PlusIcon } from '@radix-ui/react-icons'; import { PlusIcon } from '@radix-ui/react-icons';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule } from '../../lib/react-utils'; import { useModule } from '../../lib/react-utils';
import { useAppDispatch } from '../../store';
import { modules } from '../../store/api/reducer'; import { modules } from '../../store/api/reducer';
import { import {
accessLevels, accessLevels,
@ -213,7 +213,7 @@ function CommandDialog({
...item, ...item,
description, description,
response, response,
access_level: accessLevel as AccessLevelType, access_level: accessLevel,
}); });
} }
}} }}
@ -290,14 +290,14 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null); const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const filterLC = filter.toLowerCase(); const filterLC = filter.toLowerCase();
const setCommand = (newName: string, data: TwitchBotCustomCommand): void => { const setCommand = (newName: string, data: TwitchBotCustomCommand): void => {
switch (activeDialog.kind) { switch (activeDialog.kind) {
case 'new': case 'new':
dispatch( void dispatch(
setCommands({ setCommands({
...commands, ...commands,
[newName]: { [newName]: {
@ -309,7 +309,7 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
break; break;
case 'edit': { case 'edit': {
const oldName = activeDialog.name; const oldName = activeDialog.name;
dispatch( void dispatch(
setCommands({ setCommands({
...commands, ...commands,
[oldName]: undefined, [oldName]: undefined,
@ -323,7 +323,7 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
}; };
const deleteCommand = (cmd: string): void => { const deleteCommand = (cmd: string): void => {
dispatch( void dispatch(
setCommands({ setCommands({
...commands, ...commands,
[cmd]: undefined, [cmd]: undefined,
@ -332,7 +332,7 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
}; };
const toggleCommand = (cmd: string): void => { const toggleCommand = (cmd: string): void => {
dispatch( void dispatch(
setCommands({ setCommands({
...commands, ...commands,
[cmd]: { [cmd]: {

View file

@ -1,8 +1,9 @@
import { PlusIcon } from '@radix-ui/react-icons'; import { PlusIcon } from '@radix-ui/react-icons';
import { TFunction } from 'i18next';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { TFunction, useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule } from '../../lib/react-utils'; import { useModule } from '../../lib/react-utils';
import { useAppDispatch } from '../../store';
import { modules } from '../../store/api/reducer'; import { modules } from '../../store/api/reducer';
import { TwitchBotTimer } from '../../store/api/types'; import { TwitchBotTimer } from '../../store/api/types';
import AlertContent from '../components/AlertContent'; import AlertContent from '../components/AlertContent';
@ -303,14 +304,14 @@ export default function TwitchBotTimersPage(): React.ReactElement {
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null); const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const filterLC = filter.toLowerCase(); const filterLC = filter.toLowerCase();
const setTimer = (newName: string, data: TwitchBotTimer): void => { const setTimer = (newName: string, data: TwitchBotTimer): void => {
switch (activeDialog.kind) { switch (activeDialog.kind) {
case 'new': case 'new':
dispatch( void dispatch(
setTimerConfig({ setTimerConfig({
...timerConfig, ...timerConfig,
timers: { timers: {
@ -325,7 +326,7 @@ export default function TwitchBotTimersPage(): React.ReactElement {
break; break;
case 'edit': { case 'edit': {
const oldName = activeDialog.name; const oldName = activeDialog.name;
dispatch( void dispatch(
setTimerConfig({ setTimerConfig({
...timerConfig, ...timerConfig,
timers: { timers: {
@ -342,7 +343,7 @@ export default function TwitchBotTimersPage(): React.ReactElement {
}; };
const deleteTimer = (cmd: string): void => { const deleteTimer = (cmd: string): void => {
dispatch( void dispatch(
setTimerConfig({ setTimerConfig({
...timerConfig, ...timerConfig,
timers: { timers: {
@ -354,7 +355,7 @@ export default function TwitchBotTimersPage(): React.ReactElement {
}; };
const toggleTimer = (cmd: string): void => { const toggleTimer = (cmd: string): void => {
dispatch( void dispatch(
setTimerConfig({ setTimerConfig({
...timerConfig, ...timerConfig,
timers: { timers: {
@ -391,7 +392,7 @@ export default function TwitchBotTimersPage(): React.ReactElement {
/> />
</FlexRow> </FlexRow>
<TimerList> <TimerList>
{!!timerConfig?.timers ? ( {timerConfig?.timers ? (
Object.keys(timerConfig?.timers ?? {}) Object.keys(timerConfig?.timers ?? {})
?.filter((cmd) => cmd.toLowerCase().includes(filterLC)) ?.filter((cmd) => cmd.toLowerCase().includes(filterLC))
.sort() .sort()

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
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 apiReducer, { modules } from '../../store/api/reducer'; import apiReducer, { modules } from '../../store/api/reducer';
@ -21,10 +20,11 @@ import {
TextBlock, TextBlock,
} from '../theme'; } from '../theme';
import SaveButton from '../components/utils/SaveButton'; import SaveButton from '../components/utils/SaveButton';
import { useAppDispatch } from '../../store';
export default function ChatAlertsPage(): React.ReactElement { export default function ChatAlertsPage(): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const [alerts, setAlerts, loadStatus] = useModule(modules.twitchBotAlerts); const [alerts, setAlerts, loadStatus] = useModule(modules.twitchBotAlerts);
const status = useStatus(loadStatus.save); const status = useStatus(loadStatus.save);
@ -32,7 +32,7 @@ export default function ChatAlertsPage(): React.ReactElement {
<PageContainer> <PageContainer>
<form <form
onSubmit={(ev) => { onSubmit={(ev) => {
dispatch(setAlerts(alerts)); void dispatch(setAlerts(alerts));
ev.preventDefault(); ev.preventDefault();
}} }}
> >
@ -61,8 +61,8 @@ export default function ChatAlertsPage(): React.ReactElement {
<FlexRow spacing={1} align="left"> <FlexRow spacing={1} align="left">
<Checkbox <Checkbox
checked={alerts?.follow?.enabled ?? false} checked={alerts?.follow?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
follow: { follow: {
@ -70,8 +70,8 @@ export default function ChatAlertsPage(): React.ReactElement {
enabled: !!ev, enabled: !!ev,
}, },
}), }),
) );
} }}
id="follow-enabled" id="follow-enabled"
> >
<CheckboxIndicator> <CheckboxIndicator>
@ -107,8 +107,8 @@ export default function ChatAlertsPage(): React.ReactElement {
<FlexRow spacing={1} align="left"> <FlexRow spacing={1} align="left">
<Checkbox <Checkbox
checked={alerts?.subscription?.enabled ?? false} checked={alerts?.subscription?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
subscription: { subscription: {
@ -116,8 +116,8 @@ export default function ChatAlertsPage(): React.ReactElement {
enabled: !!ev, enabled: !!ev,
}, },
}), }),
) );
} }}
id="subscription-enabled" id="subscription-enabled"
> >
<CheckboxIndicator> <CheckboxIndicator>
@ -138,7 +138,7 @@ export default function ChatAlertsPage(): React.ReactElement {
disabled={!alerts?.subscription?.enabled ?? true} disabled={!alerts?.subscription?.enabled ?? true}
required={alerts?.subscription?.enabled ?? false} required={alerts?.subscription?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
subscription: { ...alerts.subscription, messages }, subscription: { ...alerts.subscription, messages },
@ -154,8 +154,8 @@ export default function ChatAlertsPage(): React.ReactElement {
<FlexRow spacing={1} align="left"> <FlexRow spacing={1} align="left">
<Checkbox <Checkbox
checked={alerts?.gift_sub?.enabled ?? false} checked={alerts?.gift_sub?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
gift_sub: { gift_sub: {
@ -163,8 +163,8 @@ export default function ChatAlertsPage(): React.ReactElement {
enabled: !!ev, enabled: !!ev,
}, },
}), }),
) );
} }}
id="gift_sub-enabled" id="gift_sub-enabled"
> >
<CheckboxIndicator> <CheckboxIndicator>
@ -185,7 +185,7 @@ export default function ChatAlertsPage(): React.ReactElement {
disabled={!alerts?.gift_sub?.enabled ?? true} disabled={!alerts?.gift_sub?.enabled ?? true}
required={alerts?.gift_sub?.enabled ?? false} required={alerts?.gift_sub?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
gift_sub: { ...alerts.gift_sub, messages }, gift_sub: { ...alerts.gift_sub, messages },
@ -201,8 +201,8 @@ export default function ChatAlertsPage(): React.ReactElement {
<FlexRow spacing={1} align="left"> <FlexRow spacing={1} align="left">
<Checkbox <Checkbox
checked={alerts?.raid?.enabled ?? false} checked={alerts?.raid?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
raid: { raid: {
@ -210,8 +210,8 @@ export default function ChatAlertsPage(): React.ReactElement {
enabled: !!ev, enabled: !!ev,
}, },
}), }),
) );
} }}
id="raid-enabled" id="raid-enabled"
> >
<CheckboxIndicator> <CheckboxIndicator>
@ -232,7 +232,7 @@ export default function ChatAlertsPage(): React.ReactElement {
disabled={!alerts?.raid?.enabled ?? true} disabled={!alerts?.raid?.enabled ?? true}
required={alerts?.raid?.enabled ?? false} required={alerts?.raid?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
raid: { ...alerts.raid, messages }, raid: { ...alerts.raid, messages },
@ -248,8 +248,8 @@ export default function ChatAlertsPage(): React.ReactElement {
<FlexRow spacing={1} align="left"> <FlexRow spacing={1} align="left">
<Checkbox <Checkbox
checked={alerts?.cheer?.enabled ?? false} checked={alerts?.cheer?.enabled ?? false}
onCheckedChange={(ev) => onCheckedChange={(ev) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
cheer: { cheer: {
@ -257,8 +257,8 @@ export default function ChatAlertsPage(): React.ReactElement {
enabled: !!ev, enabled: !!ev,
}, },
}), }),
) );
} }}
id="raid-enabled" id="raid-enabled"
> >
<CheckboxIndicator> <CheckboxIndicator>
@ -279,7 +279,7 @@ export default function ChatAlertsPage(): React.ReactElement {
disabled={!alerts?.cheer?.enabled ?? true} disabled={!alerts?.cheer?.enabled ?? true}
required={alerts?.cheer?.enabled ?? false} required={alerts?.cheer?.enabled ?? false}
onChange={(messages) => { onChange={(messages) => {
dispatch( void dispatch(
apiReducer.actions.twitchBotAlertsChanged({ apiReducer.actions.twitchBotAlertsChanged({
...alerts, ...alerts,
cheer: { ...alerts.cheer, messages }, cheer: { ...alerts.cheer, messages },

View file

@ -1,4 +1,4 @@
import { CircleIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { CircleIcon } from '@radix-ui/react-icons';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PageContainer, SectionHeader, styled } from '../theme'; import { PageContainer, SectionHeader, styled } from '../theme';
@ -67,7 +67,7 @@ const Darken = styled('a', {
function TwitchSection() { function TwitchSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const twitchInfo = useLiveKey<StreamInfo[]>('twitch/stream-info'); const twitchInfo = useLiveKey<StreamInfo[]>('twitch/stream-info');
//const twitchActivity = useLiveKey<StreamInfo[]>('twitch/chat-activity'); // const twitchActivity = useLiveKey<StreamInfo[]>('twitch/chat-activity');
return ( return (
<> <>

View file

@ -40,7 +40,7 @@ export default function DebugPage(): React.ReactElement {
const [readValue, setReadValue] = useState(''); const [readValue, setReadValue] = useState('');
const [writeKey, setWriteKey] = useState(''); const [writeKey, setWriteKey] = useState('');
const [writeValue, setWriteValue] = useState(''); const [writeValue, setWriteValue] = useState('');
const [writeErrorMsg, setWriteErrorMsg] = useState(null); const [writeErrorMsg, setWriteErrorMsg] = useState<string>(null);
const api = useSelector((state: RootState) => state.api.client); const api = useSelector((state: RootState) => state.api.client);
const performRead = async () => { const performRead = async () => {
@ -54,9 +54,11 @@ export default function DebugPage(): React.ReactElement {
try { try {
setWriteValue(JSON.stringify(JSON.parse(writeValue))); setWriteValue(JSON.stringify(JSON.parse(writeValue)));
setWriteErrorMsg(null); setWriteErrorMsg(null);
} catch (e) { } catch (e: unknown) {
if (e instanceof Error) {
setWriteErrorMsg(e.message); setWriteErrorMsg(e.message);
} }
}
}; };
const dumpKeys = async () => { const dumpKeys = async () => {
console.log(await api.keyList()); console.log(await api.keyList());
@ -86,10 +88,20 @@ export default function DebugPage(): React.ReactElement {
<Field size="fullWidth"> <Field size="fullWidth">
<Label htmlFor="read-key">{t('pages.debug.console-ops')}</Label> <Label htmlFor="read-key">{t('pages.debug.console-ops')}</Label>
<FlexRow align="left" spacing="1"> <FlexRow align="left" spacing="1">
<Button type="button" onClick={() => dumpKeys()}> <Button
type="button"
onClick={() => {
void dumpKeys();
}}
>
{t('pages.debug.dump-keys')} {t('pages.debug.dump-keys')}
</Button> </Button>
<Button type="button" onClick={() => dumpAll()}> <Button
type="button"
onClick={() => {
void dumpAll();
}}
>
{t('pages.debug.dump-all')} {t('pages.debug.dump-all')}
</Button> </Button>
</FlexRow> </FlexRow>
@ -98,7 +110,7 @@ export default function DebugPage(): React.ReactElement {
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
if ((e.target as HTMLFormElement).checkValidity()) { if ((e.target as HTMLFormElement).checkValidity()) {
performRead(); void performRead();
} }
}} }}
> >
@ -124,7 +136,7 @@ export default function DebugPage(): React.ReactElement {
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
if ((e.target as HTMLFormElement).checkValidity()) { if ((e.target as HTMLFormElement).checkValidity()) {
performWrite(); void performWrite();
} }
}} }}
> >

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { CheckIcon } from '@radix-ui/react-icons'; import { CheckIcon } from '@radix-ui/react-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule, useStatus } from '../../lib/react-utils'; import { useModule, useStatus } from '../../lib/react-utils';
import apiReducer, { modules } from '../../store/api/reducer'; import apiReducer, { modules } from '../../store/api/reducer';
import { import {
@ -19,11 +18,12 @@ import {
} from '../theme'; } from '../theme';
import SaveButton from '../components/utils/SaveButton'; import SaveButton from '../components/utils/SaveButton';
import Interval from '../components/Interval'; import Interval from '../components/Interval';
import { useAppDispatch } from '../../store';
export default function LoyaltySettingsPage(): React.ReactElement { export default function LoyaltySettingsPage(): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
const [config, setConfig, loadStatus] = useModule(modules.loyaltyConfig); const [config, setConfig, loadStatus] = useModule(modules.loyaltyConfig);
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const status = useStatus(loadStatus.save); const status = useStatus(loadStatus.save);
const busy = const busy =
loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending'; loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending';
@ -40,14 +40,14 @@ export default function LoyaltySettingsPage(): React.ReactElement {
<FlexRow spacing={1}> <FlexRow spacing={1}>
<Checkbox <Checkbox
checked={active} checked={active}
onCheckedChange={(ev) => onCheckedChange={(ev) => {
dispatch( void dispatch(
setConfig({ setConfig({
...config, ...config,
enabled: !!ev, enabled: !!ev,
}), }),
) );
} }}
id="enable" id="enable"
> >
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator> <CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
@ -62,7 +62,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
if (!(e.target as HTMLFormElement).checkValidity()) { if (!(e.target as HTMLFormElement).checkValidity()) {
return; return;
} }
dispatch(setConfig(config)); void dispatch(setConfig(config));
}} }}
> >
<Field size="fullWidth"> <Field size="fullWidth">
@ -76,14 +76,14 @@ export default function LoyaltySettingsPage(): React.ReactElement {
value={config?.currency ?? ''} value={config?.currency ?? ''}
disabled={!active || busy} disabled={!active || busy}
required={true} required={true}
onChange={(e) => onChange={(e) => {
dispatch( void dispatch(
apiReducer.actions.loyaltyConfigChanged({ apiReducer.actions.loyaltyConfigChanged({
...config, ...config,
currency: e.target.value, currency: e.target.value,
}), }),
) );
} }}
/> />
<FieldNote> <FieldNote>
{t('pages.loyalty-settings.currency-name-hint')} {t('pages.loyalty-settings.currency-name-hint')}
@ -112,7 +112,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
if (Number.isNaN(intNum)) { if (Number.isNaN(intNum)) {
return; return;
} }
dispatch( void dispatch(
apiReducer.actions.loyaltyConfigChanged({ apiReducer.actions.loyaltyConfigChanged({
...config, ...config,
points: { points: {
@ -128,7 +128,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
id="timer-interval" id="timer-interval"
value={config?.points?.interval ?? 120} value={config?.points?.interval ?? 120}
onChange={(interval) => { onChange={(interval) => {
dispatch( void dispatch(
apiReducer.actions.loyaltyConfigChanged({ apiReducer.actions.loyaltyConfigChanged({
...(config ?? {}), ...(config ?? {}),
points: { points: {
@ -161,7 +161,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
if (Number.isNaN(intNum)) { if (Number.isNaN(intNum)) {
return; return;
} }
dispatch( void dispatch(
apiReducer.actions.loyaltyConfigChanged({ apiReducer.actions.loyaltyConfigChanged({
...config, ...config,
points: { points: {

View file

@ -1,8 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule, useUserPoints } from '../../lib/react-utils'; import { useModule, useUserPoints } from '../../lib/react-utils';
import { SortFunction } from '../../lib/type-utils'; import { SortFunction } from '../../lib/type-utils';
import { useAppDispatch } from '../../store';
import { modules, removeRedeem, setUserPoints } from '../../store/api/reducer'; import { modules, removeRedeem, setUserPoints } from '../../store/api/reducer';
import { DataTable } from '../components/DataTable'; import { DataTable } from '../components/DataTable';
import DialogContent from '../components/DialogContent'; import DialogContent from '../components/DialogContent';
@ -28,7 +28,7 @@ import { TableCell, TableRow } from '../theme/table';
function RewardQueue() { function RewardQueue() {
const { t } = useTranslation(); const { t } = useTranslation();
const [queue] = useModule(modules.loyaltyRedeemQueue); const [queue] = useModule(modules.loyaltyRedeemQueue);
const dispatch = useDispatch(); const dispatch = useAppDispatch();
// Big hack but this is required or refunds break // Big hack but this is required or refunds break
useUserPoints(); useUserPoints();
@ -42,11 +42,13 @@ function RewardQueue() {
return a.display_name?.localeCompare(b.display_name); return a.display_name?.localeCompare(b.display_name);
} }
case 'when': { case 'when': {
return a.date?.getTime() - b.date?.getTime(); return a.date && b.date ? a.date.getTime() - b.date.getTime() : 0;
} }
case 'reward': { case 'reward': {
return a.reward?.name?.localeCompare(b.reward.name); return a.reward?.name?.localeCompare(b.reward.name);
} }
default:
return 0;
} }
}; };
@ -96,7 +98,7 @@ function RewardQueue() {
]} ]}
defaultSort={{ key: 'when', order: 'desc' }} defaultSort={{ key: 'when', order: 'desc' }}
view={(entry) => ( view={(entry) => (
<TableRow key={entry.when + entry.username}> <TableRow key={`${entry.when.toString()}${entry.username}`}>
<TableCell css={{ width: '22%', fontSize: '0.8rem' }}> <TableCell css={{ width: '22%', fontSize: '0.8rem' }}>
{entry.date.toLocaleString()} {entry.date.toLocaleString()}
</TableCell> </TableCell>
@ -108,7 +110,7 @@ function RewardQueue() {
<Button <Button
size="small" size="small"
onClick={() => { onClick={() => {
dispatch(removeRedeem(entry)); void dispatch(removeRedeem(entry));
}} }}
> >
{t('pages.loyalty-queue.accept')} {t('pages.loyalty-queue.accept')}
@ -117,7 +119,7 @@ function RewardQueue() {
size="small" size="small"
onClick={() => { onClick={() => {
// Give points back to the viewer // Give points back to the viewer
dispatch( void dispatch(
setUserPoints({ setUserPoints({
user: entry.username, user: entry.username,
points: entry.reward.price, points: entry.reward.price,
@ -125,7 +127,7 @@ function RewardQueue() {
}), }),
); );
// Take the redeem off the list // Take the redeem off the list
dispatch(removeRedeem(entry)); void dispatch(removeRedeem(entry));
}} }}
> >
{t('pages.loyalty-queue.refund')} {t('pages.loyalty-queue.refund')}
@ -143,7 +145,7 @@ function RewardQueue() {
function UserList() { function UserList() {
const { t } = useTranslation(); const { t } = useTranslation();
const users = useUserPoints(); const users = useUserPoints();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const [currentEntry, setCurrentEntry] = useState<UserEntry>(null); const [currentEntry, setCurrentEntry] = useState<UserEntry>(null);
const [givePointDialog, setGivePointDialog] = useState({ const [givePointDialog, setGivePointDialog] = useState({
@ -188,7 +190,7 @@ function UserList() {
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
if ((e.target as HTMLFormElement).checkValidity()) { if ((e.target as HTMLFormElement).checkValidity()) {
dispatch( void dispatch(
setUserPoints({ setUserPoints({
...givePointDialog, ...givePointDialog,
relative: true, relative: true,
@ -225,7 +227,7 @@ function UserList() {
onChange={(e) => onChange={(e) =>
setGivePointDialog({ setGivePointDialog({
...givePointDialog, ...givePointDialog,
points: parseInt(e.target.value), points: parseInt(e.target.value, 10),
}) })
} }
/> />
@ -258,7 +260,7 @@ function UserList() {
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
if ((e.target as HTMLFormElement).checkValidity()) { if ((e.target as HTMLFormElement).checkValidity()) {
dispatch( void dispatch(
setUserPoints({ setUserPoints({
user: currentEntry.username, user: currentEntry.username,
points: currentEntry.points, points: currentEntry.points,
@ -290,7 +292,7 @@ function UserList() {
onChange={(e) => onChange={(e) =>
setCurrentEntry({ setCurrentEntry({
...currentEntry, ...currentEntry,
points: parseInt(e.target.value), points: parseInt(e.target.value, 10),
}) })
} }
/> />

View file

@ -1,8 +1,8 @@
import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'; import { CheckIcon, PlusIcon } from '@radix-ui/react-icons';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule } from '../../lib/react-utils'; import { useModule } from '../../lib/react-utils';
import { useAppDispatch } from '../../store';
import { modules } from '../../store/api/reducer'; import { modules } from '../../store/api/reducer';
import { LoyaltyGoal, LoyaltyReward } from '../../store/api/types'; import { LoyaltyGoal, LoyaltyReward } from '../../store/api/types';
import AlertContent from '../components/AlertContent'; import AlertContent from '../components/AlertContent';
@ -267,7 +267,7 @@ function GoalItem({
function RewardsPage() { function RewardsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const [config] = useModule(modules.loyaltyConfig); const [config] = useModule(modules.loyaltyConfig);
const [rewards, setRewards] = useModule(modules.loyaltyRewards); const [rewards, setRewards] = useModule(modules.loyaltyRewards);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@ -282,12 +282,12 @@ function RewardsPage() {
}); });
const filterLC = filter.toLowerCase(); const filterLC = filter.toLowerCase();
const deleteReward = (id: string): void => { const deleteReward = (id: string) => {
dispatch(setRewards(rewards?.filter((r) => r.id !== id) ?? [])); void dispatch(setRewards(rewards?.filter((r) => r.id !== id) ?? []));
}; };
const toggleReward = (id: string): void => { const toggleReward = (id: string) => {
dispatch( void dispatch(
setRewards( setRewards(
rewards?.map((r) => { rewards?.map((r) => {
if (r.id === id) { if (r.id === id) {
@ -324,17 +324,17 @@ function RewardsPage() {
if (!(e.target as HTMLFormElement).checkValidity()) { if (!(e.target as HTMLFormElement).checkValidity()) {
return; return;
} }
const reward = dialogReward.reward; const { reward } = dialogReward;
if (requiredInfo.enabled) { if (requiredInfo.enabled) {
reward.required_info = requiredInfo.text; reward.required_info = requiredInfo.text;
} }
const index = rewards?.findIndex((t) => t.id == reward.id); const index = rewards?.findIndex((r) => r.id === reward.id);
if (index >= 0) { if (index >= 0) {
const newRewards = rewards.slice(0); const newRewards = rewards.slice(0);
newRewards[index] = reward; newRewards[index] = reward;
dispatch(setRewards(newRewards)); void dispatch(setRewards(newRewards));
} else { } else {
dispatch(setRewards([...(rewards ?? []), reward])); void dispatch(setRewards([...(rewards ?? []), reward]));
} }
setDialogReward({ ...dialogReward, open: false }); setDialogReward({ ...dialogReward, open: false });
}} }}
@ -450,7 +450,7 @@ function RewardsPage() {
...dialogReward, ...dialogReward,
reward: { reward: {
...dialogReward?.reward, ...dialogReward?.reward,
price: parseInt(e.target.value), price: parseInt(e.target.value, 10),
}, },
}); });
}} }}
@ -600,7 +600,7 @@ function RewardsPage() {
function GoalsPage() { function GoalsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const [config] = useModule(modules.loyaltyConfig); const [config] = useModule(modules.loyaltyConfig);
const [goals, setGoals] = useModule(modules.loyaltyGoals); const [goals, setGoals] = useModule(modules.loyaltyGoals);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@ -609,18 +609,18 @@ function GoalsPage() {
new: boolean; new: boolean;
goal: LoyaltyGoal; goal: LoyaltyGoal;
}>({ open: false, new: false, goal: null }); }>({ open: false, new: false, goal: null });
const [requiredInfo, setRequiredInfo] = useState({ const [_requiredInfo, setRequiredInfo] = useState({
enabled: false, enabled: false,
text: '', text: '',
}); });
const filterLC = filter.toLowerCase(); const filterLC = filter.toLowerCase();
const deleteGoal = (id: string): void => { const deleteGoal = (id: string): void => {
dispatch(setGoals(goals?.filter((r) => r.id !== id) ?? [])); void dispatch(setGoals(goals?.filter((r) => r.id !== id) ?? []));
}; };
const toggleGoal = (id: string): void => { const toggleGoal = (id: string): void => {
dispatch( void dispatch(
setGoals( setGoals(
goals?.map((r) => { goals?.map((r) => {
if (r.id === id) { if (r.id === id) {
@ -655,14 +655,14 @@ function GoalsPage() {
if (!(e.target as HTMLFormElement).checkValidity()) { if (!(e.target as HTMLFormElement).checkValidity()) {
return; return;
} }
const goal = dialogGoal.goal; const { goal } = dialogGoal;
const index = goals?.findIndex((t) => t.id == goal.id); const index = goals?.findIndex((g) => g.id === goal.id);
if (index >= 0) { if (index >= 0) {
const newGoals = goals.slice(0); const newGoals = goals.slice(0);
newGoals[index] = goal; newGoals[index] = goal;
dispatch(setGoals(newGoals)); void dispatch(setGoals(newGoals));
} else { } else {
dispatch(setGoals([...(goals ?? []), goal])); void dispatch(setGoals([...(goals ?? []), goal]));
} }
setDialogGoal({ ...dialogGoal, open: false }); setDialogGoal({ ...dialogGoal, open: false });
}} }}
@ -776,7 +776,7 @@ function GoalsPage() {
...dialogGoal, ...dialogGoal,
goal: { goal: {
...dialogGoal?.goal, ...dialogGoal?.goal,
total: parseInt(e.target.value), total: parseInt(e.target.value, 10),
}, },
}); });
}} }}

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule, useStatus } from '../../lib/react-utils'; import { useModule, useStatus } from '../../lib/react-utils';
import { useAppDispatch } from '../../store';
import apiReducer, { modules } from '../../store/api/reducer'; import apiReducer, { modules } from '../../store/api/reducer';
import SaveButton from '../components/utils/SaveButton'; import SaveButton from '../components/utils/SaveButton';
import { import {
@ -19,7 +19,7 @@ export default function ServerSettingsPage(): React.ReactElement {
modules.httpConfig, modules.httpConfig,
); );
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const status = useStatus(loadStatus.save); const status = useStatus(loadStatus.save);
const busy = const busy =
loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending'; loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending';
@ -31,7 +31,7 @@ export default function ServerSettingsPage(): React.ReactElement {
</PageHeader> </PageHeader>
<form <form
onSubmit={(ev) => { onSubmit={(ev) => {
dispatch(setServerConfig(serverConfig)); void dispatch(setServerConfig(serverConfig));
ev.preventDefault(); ev.preventDefault();
}} }}
> >

View file

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { keyframes } from '@stitches/react'; import { keyframes } from '@stitches/react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { GitHubLogoIcon, TwitterLogoIcon } from '@radix-ui/react-icons';
import { APPNAME, PageContainer, PageHeader, styled } from '../theme'; import { APPNAME, PageContainer, PageHeader, styled } from '../theme';
// @ts-expect-error Asset import // @ts-expect-error Asset import
import logo from '../../assets/icon-logo.svg'; import logo from '../../assets/icon-logo.svg';
import { GitHubLogoIcon, TwitterLogoIcon } from '@radix-ui/react-icons';
const gradientAnimation = keyframes({ const gradientAnimation = keyframes({
'0%': { '0%': {
@ -22,10 +22,10 @@ const gradientAnimation = keyframes({
const LogoPic = styled('div', { const LogoPic = styled('div', {
minHeight: '170px', minHeight: '170px',
width: '270px', width: '270px',
maskImage: `url(${logo})`, maskImage: `url(${logo as string})`,
maskRepeat: 'no-repeat', maskRepeat: 'no-repeat',
maskPosition: 'center', maskPosition: 'center',
animation: `${gradientAnimation} 12s ease infinite`, animation: `${gradientAnimation()} 12s ease infinite`,
backgroundSize: '400% 400%', backgroundSize: '400% 400%',
backgroundImage: `linear-gradient( backgroundImage: `linear-gradient(
45deg, 45deg,

View file

@ -1,8 +1,8 @@
import { CheckIcon } from '@radix-ui/react-icons'; import { CheckIcon } from '@radix-ui/react-icons';
import React from 'react'; import React from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule, useStatus } from '../../lib/react-utils'; import { useModule, useStatus } from '../../lib/react-utils';
import { useAppDispatch } from '../../store';
import apiReducer, { modules } from '../../store/api/reducer'; import apiReducer, { modules } from '../../store/api/reducer';
import DefinitionTable from '../components/DefinitionTable'; import DefinitionTable from '../components/DefinitionTable';
import SaveButton from '../components/utils/SaveButton'; import SaveButton from '../components/utils/SaveButton';
@ -48,15 +48,15 @@ function TwitchBotSettings() {
); );
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
const status = useStatus(loadStatus.save); const status = useStatus(loadStatus.save);
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const active = twitchConfig?.enable_bot ?? false; const active = twitchConfig?.enable_bot ?? false;
return ( return (
<form <form
onSubmit={(ev) => { onSubmit={(ev) => {
dispatch(setTwitchConfig(twitchConfig)); void dispatch(setTwitchConfig(twitchConfig));
dispatch(setBotConfig(botConfig)); void dispatch(setBotConfig(botConfig));
ev.preventDefault(); ev.preventDefault();
}} }}
> >
@ -189,12 +189,12 @@ function TwitchAPISettings() {
modules.twitchConfig, modules.twitchConfig,
); );
const status = useStatus(loadStatus.save); const status = useStatus(loadStatus.save);
const dispatch = useDispatch(); const dispatch = useAppDispatch();
return ( return (
<form <form
onSubmit={(ev) => { onSubmit={(ev) => {
dispatch(setTwitchConfig(twitchConfig)); void dispatch(setTwitchConfig(twitchConfig));
ev.preventDefault(); ev.preventDefault();
}} }}
> >
@ -281,7 +281,7 @@ function TwitchAPISettings() {
export default function TwitchSettingsPage(): React.ReactElement { export default function TwitchSettingsPage(): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const active = twitchConfig?.enabled ?? false; const active = twitchConfig?.enabled ?? false;
@ -294,14 +294,14 @@ export default function TwitchSettingsPage(): React.ReactElement {
<FlexRow spacing={1}> <FlexRow spacing={1}>
<Checkbox <Checkbox
checked={active} checked={active}
onCheckedChange={(ev) => onCheckedChange={(ev) => {
dispatch( void dispatch(
setTwitchConfig({ setTwitchConfig({
...twitchConfig, ...twitchConfig,
enabled: !!ev, enabled: !!ev,
}), }),
) );
} }}
id="enable" id="enable"
> >
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator> <CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>

View file

@ -22,7 +22,7 @@ export const AlertOverlay = styled(AlertDialogPrimitive.Overlay, {
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,
'@media (prefers-reduced-motion: no-preference)': { '@media (prefers-reduced-motion: no-preference)': {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`, animation: `${overlayShow()} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
}, },
}); });
@ -40,7 +40,7 @@ export const AlertContainer = styled(AlertDialogPrimitive.Content, {
maxHeight: '85vh', maxHeight: '85vh',
padding: '1rem', padding: '1rem',
'@media (prefers-reduced-motion: no-preference)': { '@media (prefers-reduced-motion: no-preference)': {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`, animation: `${contentShow()} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
}, },
border: '2px solid $teal8', border: '2px solid $teal8',
'&:focus': { outline: 'none' }, '&:focus': { outline: 'none' },

View file

@ -21,7 +21,7 @@ export const DialogOverlay = styled(DialogPrimitive.Overlay, {
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,
'@media (prefers-reduced-motion: no-preference)': { '@media (prefers-reduced-motion: no-preference)': {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`, animation: `${overlayShow()} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
}, },
}); });
@ -40,7 +40,7 @@ export const DialogContainer = styled(DialogPrimitive.Content, {
padding: '1rem', padding: '1rem',
overflow: 'auto', overflow: 'auto',
'@media (prefers-reduced-motion: no-preference)': { '@media (prefers-reduced-motion: no-preference)': {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`, animation: `${contentShow()} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
}, },
'&:focus': { outline: 'none' }, '&:focus': { outline: 'none' },
}); });

View file

@ -1,4 +1,4 @@
import { theme, styled } from './theme'; import { styled } from './theme';
export const Table = styled('table', { export const Table = styled('table', {
borderCollapse: 'collapse', borderCollapse: 'collapse',

4
frontend/wailsjs/go/main/App.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function GetHubPassword(arg1:number):Promise<void>;

View file

@ -0,0 +1,7 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function GetHubPassword(arg1) {
return window['go']['main']['App']['GetHubPassword'](arg1);
}