mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-18 01:50:50 +00:00
feat: add UI config object, wip ui settings page
This commit is contained in:
parent
1c5db13b0d
commit
d7586e9bb9
25 changed files with 230 additions and 42 deletions
41
frontend/package-lock.json
generated
41
frontend/package-lock.json
generated
|
@ -16,6 +16,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.0.2",
|
"@radix-ui/react-dialog": "^1.0.2",
|
||||||
"@radix-ui/react-icons": "^1.1.1",
|
"@radix-ui/react-icons": "^1.1.1",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.2",
|
"@radix-ui/react-scroll-area": "^1.0.2",
|
||||||
"@radix-ui/react-tabs": "^1.0.1",
|
"@radix-ui/react-tabs": "^1.0.1",
|
||||||
"@radix-ui/react-toggle": "^1.0.1",
|
"@radix-ui/react-toggle": "^1.0.1",
|
||||||
|
@ -953,6 +954,28 @@
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-7rrkZCXu0Q7oC0MxCm497X1DdV/tI78oNIGXA8sDbCkboiTkuLSe728zCCpRYHw+9PifHIx86nsbITPEq5yijg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.0",
|
||||||
|
"@radix-ui/react-context": "1.0.0",
|
||||||
|
"@radix-ui/react-direction": "1.0.0",
|
||||||
|
"@radix-ui/react-presence": "1.0.0",
|
||||||
|
"@radix-ui/react-primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.0.0",
|
||||||
|
"@radix-ui/react-use-size": "1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-roving-focus": {
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
||||||
|
@ -5817,6 +5840,24 @@
|
||||||
"@radix-ui/react-slot": "1.0.1"
|
"@radix-ui/react-slot": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@radix-ui/react-radio-group": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-7rrkZCXu0Q7oC0MxCm497X1DdV/tI78oNIGXA8sDbCkboiTkuLSe728zCCpRYHw+9PifHIx86nsbITPEq5yijg==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.0",
|
||||||
|
"@radix-ui/react-context": "1.0.0",
|
||||||
|
"@radix-ui/react-direction": "1.0.0",
|
||||||
|
"@radix-ui/react-presence": "1.0.0",
|
||||||
|
"@radix-ui/react-primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.0.0",
|
||||||
|
"@radix-ui/react-use-size": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@radix-ui/react-roving-focus": {
|
"@radix-ui/react-roving-focus": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.0.2",
|
"@radix-ui/react-dialog": "^1.0.2",
|
||||||
"@radix-ui/react-icons": "^1.1.1",
|
"@radix-ui/react-icons": "^1.1.1",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.2",
|
"@radix-ui/react-scroll-area": "^1.0.2",
|
||||||
"@radix-ui/react-tabs": "^1.0.1",
|
"@radix-ui/react-tabs": "^1.0.1",
|
||||||
"@radix-ui/react-toggle": "^1.0.1",
|
"@radix-ui/react-toggle": "^1.0.1",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
b120abf6c619728b1d1a33e9dda709c0
|
1e088af20465ec91bc3495c250e91b1a
|
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"$meta": {
|
||||||
|
"language-name": "English"
|
||||||
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"sections": {
|
"sections": {
|
||||||
"monitor": "Monitor",
|
"monitor": "Monitor",
|
||||||
|
@ -11,7 +14,8 @@
|
||||||
"dashboard": "Dashboard"
|
"dashboard": "Dashboard"
|
||||||
},
|
},
|
||||||
"strimertul": {
|
"strimertul": {
|
||||||
"settings": "Server settings"
|
"settings": "Server settings",
|
||||||
|
"ui-config": "User interface"
|
||||||
},
|
},
|
||||||
"twitch": {
|
"twitch": {
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
|
@ -253,7 +257,14 @@
|
||||||
"not-live": "Offline / Not streaming"
|
"not-live": "Offline / Not streaming"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"welcome-header": "Welcome to {{APPNAME}}"
|
"welcome-header": "Welcome to {{APPNAME}}",
|
||||||
|
"welcome-p1": "It looks like this is the first time you started {{APPNAME}}. You can click the \"Get started\" button at the bottom to set things up one by one, or just skip everything and configure things manually later.",
|
||||||
|
"welcome-continue-button": "Get started",
|
||||||
|
"skip-button": "Skip onboarding"
|
||||||
|
},
|
||||||
|
"uiconfig": {
|
||||||
|
"title": "User interface settings",
|
||||||
|
"language": "Language"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form-actions": {
|
"form-actions": {
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"$meta": {
|
||||||
|
"language-name": "Italiano"
|
||||||
|
},
|
||||||
"form-actions": {
|
"form-actions": {
|
||||||
"save": "Salva",
|
"save": "Salva",
|
||||||
"saving": "Sto salvando...",
|
"saving": "Sto salvando...",
|
||||||
|
|
34
frontend/src/locale/languages.ts
Normal file
34
frontend/src/locale/languages.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { ResourceKey } from 'i18next';
|
||||||
|
import en from './en/translation.json';
|
||||||
|
import it from './it/translation.json';
|
||||||
|
|
||||||
|
function countKeys(res: ResourceKey): number {
|
||||||
|
if (typeof res === 'string') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Object.values(res).reduce<number>(
|
||||||
|
(acc: number, k: ResourceKey) => acc + countKeys(k),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LanguageMeta {
|
||||||
|
'language-name': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resources = {
|
||||||
|
en: {
|
||||||
|
translation: en,
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
translation: it,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const languages = Object.entries(resources).map(([code, lang]) => ({
|
||||||
|
code,
|
||||||
|
name: (lang.translation.$meta as LanguageMeta)['language-name'] || code,
|
||||||
|
keys: countKeys(lang),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default resources;
|
|
@ -1,19 +1,10 @@
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import { APPNAME } from '~/ui/theme';
|
import { APPNAME } from '~/ui/theme';
|
||||||
|
import { resources } from './languages';
|
||||||
import en from './en/translation.json';
|
|
||||||
import it from './it/translation.json';
|
|
||||||
|
|
||||||
void i18n.use(initReactI18next).init({
|
void i18n.use(initReactI18next).init({
|
||||||
resources: {
|
resources,
|
||||||
en: {
|
|
||||||
translation: en,
|
|
||||||
},
|
|
||||||
it: {
|
|
||||||
translation: it,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lng: navigator.language,
|
lng: navigator.language,
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
interpolation: {
|
interpolation: {
|
||||||
|
|
|
@ -230,6 +230,14 @@ export const modules = {
|
||||||
state.loyalty.redeemQueue = payload;
|
state.loyalty.redeemQueue = payload;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
uiConfig: makeModule(
|
||||||
|
'ui/settings',
|
||||||
|
(state) => state.uiConfig,
|
||||||
|
(state, { payload }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
state.uiConfig = payload;
|
||||||
|
},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRedeem = createAsyncThunk(
|
export const createRedeem = createAsyncThunk(
|
||||||
|
@ -280,6 +288,7 @@ const initialState: APIState = {
|
||||||
twitchBotConfig: null,
|
twitchBotConfig: null,
|
||||||
loyaltyConfig: null,
|
loyaltyConfig: null,
|
||||||
},
|
},
|
||||||
|
uiConfig: null,
|
||||||
requestStatus: {},
|
requestStatus: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -143,6 +143,11 @@ export interface LoyaltyRedeem {
|
||||||
request_text: string;
|
request_text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UISettings {
|
||||||
|
onboardingDone: boolean;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export enum ConnectionStatus {
|
export enum ConnectionStatus {
|
||||||
NotConnected,
|
NotConnected,
|
||||||
AuthenticationNeeded,
|
AuthenticationNeeded,
|
||||||
|
@ -176,5 +181,6 @@ export interface APIState {
|
||||||
twitchBotConfig: TwitchBotConfig;
|
twitchBotConfig: TwitchBotConfig;
|
||||||
loyaltyConfig: LoyaltyConfig;
|
loyaltyConfig: LoyaltyConfig;
|
||||||
};
|
};
|
||||||
|
uiConfig: UISettings;
|
||||||
requestStatus: Record<string, RequestStatus>;
|
requestStatus: Record<string, RequestStatus>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { useDispatch } from 'react-redux';
|
import { EqualityFn, useDispatch, useSelector } from 'react-redux';
|
||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
|
|
||||||
import apiReducer from './api/reducer';
|
import apiReducer from './api/reducer';
|
||||||
import loggingReducer from './logging/reducer';
|
import loggingReducer from './logging/reducer';
|
||||||
|
|
||||||
|
@ -19,5 +20,9 @@ const store = configureStore({
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
export type AppDispatch = typeof store.dispatch;
|
export type AppDispatch = typeof store.dispatch;
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||||
|
export const useAppSelector: <Selected = unknown>(
|
||||||
|
selector: (state: RootState) => Selected,
|
||||||
|
equalityFn?: EqualityFn<Selected> | undefined,
|
||||||
|
) => Selected = useSelector;
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import {
|
import {
|
||||||
ChatBubbleIcon,
|
ChatBubbleIcon,
|
||||||
|
CodeIcon,
|
||||||
DashboardIcon,
|
DashboardIcon,
|
||||||
FrameIcon,
|
FrameIcon,
|
||||||
GearIcon,
|
|
||||||
MixerHorizontalIcon,
|
MixerHorizontalIcon,
|
||||||
|
MixIcon,
|
||||||
StarIcon,
|
StarIcon,
|
||||||
TableIcon,
|
TableIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
|
@ -11,7 +12,6 @@ import {
|
||||||
import { EventsOff, EventsOn } from '@wailsapp/runtime/runtime';
|
import { EventsOff, EventsOn } from '@wailsapp/runtime/runtime';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -21,14 +21,16 @@ import {
|
||||||
} from '@wailsapp/go/main/App';
|
} from '@wailsapp/go/main/App';
|
||||||
import { main } from '@wailsapp/go/models';
|
import { main } from '@wailsapp/go/models';
|
||||||
|
|
||||||
import { RootState, useAppDispatch } from '~/store';
|
import { useAppDispatch, useAppSelector } from '~/store';
|
||||||
import { createWSClient, useAuthBypass } from '~/store/api/reducer';
|
import { createWSClient, useAuthBypass } from '~/store/api/reducer';
|
||||||
import { ConnectionStatus } from '~/store/api/types';
|
import { ConnectionStatus } from '~/store/api/types';
|
||||||
import loggingReducer from '~/store/logging/reducer';
|
import loggingReducer from '~/store/logging/reducer';
|
||||||
// @ts-expect-error Asset import
|
// @ts-expect-error Asset import
|
||||||
import spinner from '~/assets/icon-loading.svg';
|
import spinner from '~/assets/icon-loading.svg';
|
||||||
|
|
||||||
|
import LogViewer from './components/LogViewer';
|
||||||
import Sidebar, { RouteSection } from './components/Sidebar';
|
import Sidebar, { RouteSection } from './components/Sidebar';
|
||||||
|
import Scrollbar from './components/utils/Scrollbar';
|
||||||
import AuthDialog from './pages/AuthDialog';
|
import AuthDialog from './pages/AuthDialog';
|
||||||
import TwitchBotCommandsPage from './pages/BotCommands';
|
import TwitchBotCommandsPage from './pages/BotCommands';
|
||||||
import TwitchBotTimersPage from './pages/BotTimers';
|
import TwitchBotTimersPage from './pages/BotTimers';
|
||||||
|
@ -38,13 +40,12 @@ import DebugPage from './pages/Debug';
|
||||||
import LoyaltyConfigPage from './pages/LoyaltyConfig';
|
import LoyaltyConfigPage from './pages/LoyaltyConfig';
|
||||||
import LoyaltyQueuePage from './pages/LoyaltyQueue';
|
import LoyaltyQueuePage from './pages/LoyaltyQueue';
|
||||||
import LoyaltyRewardsPage from './pages/LoyaltyRewards';
|
import LoyaltyRewardsPage from './pages/LoyaltyRewards';
|
||||||
|
import OnboardingPage from './pages/Onboarding';
|
||||||
import ServerSettingsPage from './pages/ServerSettings';
|
import ServerSettingsPage from './pages/ServerSettings';
|
||||||
import StrimertulPage from './pages/Strimertul';
|
import StrimertulPage from './pages/Strimertul';
|
||||||
import TwitchSettingsPage from './pages/TwitchSettings';
|
import TwitchSettingsPage from './pages/TwitchSettings';
|
||||||
|
import UISettingsPage from './pages/UISettingsPage';
|
||||||
import { styled } from './theme';
|
import { styled } from './theme';
|
||||||
import Scrollbar from './components/utils/Scrollbar';
|
|
||||||
import LogViewer from './components/LogViewer';
|
|
||||||
import OnboardingPage from './pages/Onboarding';
|
|
||||||
|
|
||||||
const LoadingDiv = styled('div', {
|
const LoadingDiv = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -87,7 +88,12 @@ const sections: RouteSection[] = [
|
||||||
{
|
{
|
||||||
title: 'menu.pages.strimertul.settings',
|
title: 'menu.pages.strimertul.settings',
|
||||||
url: '/http',
|
url: '/http',
|
||||||
icon: <GearIcon />,
|
icon: <CodeIcon />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'menu.pages.strimertul.ui-config',
|
||||||
|
url: '/ui-config',
|
||||||
|
icon: <MixIcon />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -160,10 +166,9 @@ const PageWrapper = styled('div', {
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
export default function App(): JSX.Element {
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
const client = useSelector((state: RootState) => state.api.client);
|
const client = useAppSelector((state) => state.api.client);
|
||||||
const connected = useSelector(
|
const uiConfig = useAppSelector((state) => state.api.uiConfig);
|
||||||
(state: RootState) => state.api.connectionStatus,
|
const connected = useAppSelector((state) => state.api.connectionStatus);
|
||||||
);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -212,10 +217,12 @@ export default function App(): JSX.Element {
|
||||||
}
|
}
|
||||||
}, [ready, connected]);
|
}, [ready, connected]);
|
||||||
|
|
||||||
// TODO: Only do this when
|
const onboardingDone = uiConfig?.onboardingDone;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigate('/setup');
|
if (!onboardingDone) {
|
||||||
}, []);
|
navigate('/setup');
|
||||||
|
}
|
||||||
|
}, [ready, onboardingDone]);
|
||||||
|
|
||||||
if (connected === ConnectionStatus.NotConnected) {
|
if (connected === ConnectionStatus.NotConnected) {
|
||||||
return <Loading message={t('special.loading')} />;
|
return <Loading message={t('special.loading')} />;
|
||||||
|
@ -244,6 +251,7 @@ export default function App(): JSX.Element {
|
||||||
<Route path="/about" element={<StrimertulPage />} />
|
<Route path="/about" element={<StrimertulPage />} />
|
||||||
<Route path="/debug" element={<DebugPage />} />
|
<Route path="/debug" element={<DebugPage />} />
|
||||||
<Route path="/http" element={<ServerSettingsPage />} />
|
<Route path="/http" element={<ServerSettingsPage />} />
|
||||||
|
<Route path="/ui-config" element={<UISettingsPage />} />
|
||||||
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
|
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/twitch/bot/commands"
|
path="/twitch/bot/commands"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getInterval } from '~/lib/time-utils';
|
import { getInterval } from '~/lib/time-utils';
|
||||||
import { ComboBox, FlexRow, InputBox } from '../theme';
|
import { ComboBox, FlexRow, InputBox } from '../../theme';
|
||||||
|
|
||||||
export interface TimeUnit {
|
export interface TimeUnit {
|
||||||
multiplier: number;
|
multiplier: number;
|
|
@ -1,7 +1,7 @@
|
||||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, FlexRow, Textarea } from '../theme';
|
import { Button, FlexRow, Textarea } from '../../theme';
|
||||||
|
|
||||||
export interface MultiInputProps {
|
export interface MultiInputProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
31
frontend/src/ui/components/forms/RadioGroup.tsx
Normal file
31
frontend/src/ui/components/forms/RadioGroup.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import { Root, Item, Indicator } from '@radix-ui/react-radio-group';
|
||||||
|
|
||||||
|
export interface RadioGroupProps {
|
||||||
|
label: string;
|
||||||
|
selected?: string;
|
||||||
|
values: {
|
||||||
|
id: string;
|
||||||
|
label: string | ReactElement;
|
||||||
|
}[];
|
||||||
|
default?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RadioGroup(props: RadioGroupProps) {
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
defaultValue={props.default}
|
||||||
|
value={props.selected}
|
||||||
|
aria-label={props.label}
|
||||||
|
>
|
||||||
|
{props.values.map(({ id, label }) => (
|
||||||
|
<div key={id} style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Item value="default" id={`r${id}`}>
|
||||||
|
<Indicator />
|
||||||
|
</Item>
|
||||||
|
<label htmlFor={`r${id}`}>{label}</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ 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';
|
||||||
import DialogContent from '../components/DialogContent';
|
import DialogContent from '../components/DialogContent';
|
||||||
import Interval, { hours, minutes } from '../components/Interval';
|
import Interval, { hours, minutes } from '../components/forms/Interval';
|
||||||
import MultiInput from '../components/MultiInput';
|
import MultiInput from '../components/forms/MultiInput';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import MultiInput from '../components/MultiInput';
|
import MultiInput from '../components/forms/MultiInput';
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxIndicator,
|
CheckboxIndicator,
|
||||||
|
@ -20,7 +20,7 @@ import {
|
||||||
TabList,
|
TabList,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../theme';
|
} from '../theme';
|
||||||
import SaveButton from '../components/utils/SaveButton';
|
import SaveButton from '../components/forms/SaveButton';
|
||||||
|
|
||||||
export default function ChatAlertsPage(): React.ReactElement {
|
export default function ChatAlertsPage(): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
|
@ -17,8 +17,8 @@ import {
|
||||||
InputBox,
|
InputBox,
|
||||||
FieldNote,
|
FieldNote,
|
||||||
} from '../theme';
|
} from '../theme';
|
||||||
import SaveButton from '../components/utils/SaveButton';
|
import SaveButton from '../components/forms/SaveButton';
|
||||||
import Interval from '../components/Interval';
|
import Interval from '../components/forms/Interval';
|
||||||
|
|
||||||
export default function LoyaltySettingsPage(): React.ReactElement {
|
export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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';
|
||||||
import DialogContent from '../components/DialogContent';
|
import DialogContent from '../components/DialogContent';
|
||||||
import Interval from '../components/Interval';
|
import Interval from '../components/forms/Interval';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useAppDispatch } from '~/store';
|
||||||
import apiReducer, { modules } from '~/store/api/reducer';
|
import apiReducer, { modules } from '~/store/api/reducer';
|
||||||
import AlertContent from '../components/AlertContent';
|
import AlertContent from '../components/AlertContent';
|
||||||
import RevealLink from '../components/utils/RevealLink';
|
import RevealLink from '../components/utils/RevealLink';
|
||||||
import SaveButton from '../components/utils/SaveButton';
|
import SaveButton from '../components/forms/SaveButton';
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldNote,
|
FieldNote,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import apiReducer, { modules } from '~/store/api/reducer';
|
||||||
import BrowserLink from '../components/BrowserLink';
|
import BrowserLink from '../components/BrowserLink';
|
||||||
import DefinitionTable from '../components/DefinitionTable';
|
import DefinitionTable from '../components/DefinitionTable';
|
||||||
import RevealLink from '../components/utils/RevealLink';
|
import RevealLink from '../components/utils/RevealLink';
|
||||||
import SaveButton from '../components/utils/SaveButton';
|
import SaveButton from '../components/forms/SaveButton';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
|
|
48
frontend/src/ui/pages/UISettingsPage.tsx
Normal file
48
frontend/src/ui/pages/UISettingsPage.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useModule } from '~/lib/react-utils';
|
||||||
|
import { languages } from '~/locale/languages';
|
||||||
|
import { modules } from '~/store/api/reducer';
|
||||||
|
import RadioGroup from '../components/forms/RadioGroup';
|
||||||
|
import { Field, Label, PageContainer, PageHeader, PageTitle } from '../theme';
|
||||||
|
|
||||||
|
export default function UISettingsPage(): React.ReactElement {
|
||||||
|
const [uiConfig, setUiConfig] = useModule(modules.uiConfig);
|
||||||
|
const [t, i18n] = useTranslation();
|
||||||
|
|
||||||
|
const maxKeys = languages.reduce(
|
||||||
|
(current, it) => Math.max(current, it.keys),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader css={{ paddingBottom: '1rem' }}>
|
||||||
|
<PageTitle>{t('pages.uiconfig.title')}</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<Field size="fullWidth">
|
||||||
|
<Label htmlFor="bind">{t('pages.uiconfig.language')}</Label>
|
||||||
|
<RadioGroup
|
||||||
|
label={t('pages.uiconfig.language')}
|
||||||
|
default={i18n.resolvedLanguage}
|
||||||
|
selected={uiConfig?.language ?? i18n.resolvedLanguage}
|
||||||
|
values={languages.map((lang) => ({
|
||||||
|
id: lang.code,
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
{lang.name}{' '}
|
||||||
|
{lang.keys < maxKeys ? (
|
||||||
|
<small>
|
||||||
|
Partial translation (
|
||||||
|
{((lang.keys / maxKeys) * 100).toFixed(1)}% - {lang.keys}/
|
||||||
|
{maxKeys})
|
||||||
|
</small>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ import * as UnstyledLabel from '@radix-ui/react-label';
|
||||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
import * as ToggleGroup from '@radix-ui/react-toggle-group';
|
import * as ToggleGroup from '@radix-ui/react-toggle-group';
|
||||||
import { styled, theme } from './theme';
|
import { styled, theme } from './theme';
|
||||||
import ControlledInput from '../components/utils/ControlledInput';
|
import ControlledInput from '../components/forms/ControlledInput';
|
||||||
import PasswordField from '../components/utils/PasswordField';
|
import PasswordField from '../components/forms/PasswordField';
|
||||||
|
|
||||||
export const Field = styled('fieldset', {
|
export const Field = styled('fieldset', {
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
|
|
Loading…
Reference in a new issue