mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-18 01:50:50 +00:00
Update frontend with new deps and linting
This commit is contained in:
parent
05cda342fd
commit
94d57b9a70
32 changed files with 6944 additions and 2034 deletions
1
driver.interface.go
Normal file
1
driver.interface.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package main
|
|
@ -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/*'],
|
||||||
};
|
};
|
8308
frontend/package-lock.json
generated
8308
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ export function DataTable<T>({
|
||||||
case 'desc':
|
case 'desc':
|
||||||
return -result;
|
return -result;
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]: {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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' },
|
||||||
|
|
|
@ -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' },
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
4
frontend/wailsjs/go/main/App.d.ts
vendored
Normal 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>;
|
7
frontend/wailsjs/go/main/App.js
Normal file
7
frontend/wailsjs/go/main/App.js
Normal 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);
|
||||||
|
}
|
Loading…
Reference in a new issue