From d7586e9bb9ee007162f359927383349e954c90df Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Fri, 16 Dec 2022 11:10:43 +0100 Subject: [PATCH] feat: add UI config object, wip ui settings page --- frontend/package-lock.json | 41 ++++++++++++++++ frontend/package.json | 1 + frontend/package.json.md5 | 2 +- frontend/src/locale/en/translation.json | 15 +++++- frontend/src/locale/it/translation.json | 3 ++ frontend/src/locale/languages.ts | 34 +++++++++++++ frontend/src/locale/setup.ts | 13 +---- frontend/src/store/api/reducer.ts | 9 ++++ frontend/src/store/api/types.ts | 6 +++ frontend/src/store/index.ts | 7 ++- frontend/src/ui/App.tsx | 36 ++++++++------ .../{utils => forms}/ControlledInput.tsx | 0 .../ui/components/{ => forms}/Interval.tsx | 2 +- .../ui/components/{ => forms}/MultiInput.tsx | 2 +- .../{utils => forms}/PasswordField.tsx | 0 .../src/ui/components/forms/RadioGroup.tsx | 31 ++++++++++++ .../{utils => forms}/SaveButton.tsx | 0 frontend/src/ui/pages/BotTimers.tsx | 4 +- frontend/src/ui/pages/ChatAlerts.tsx | 4 +- frontend/src/ui/pages/LoyaltyConfig.tsx | 4 +- frontend/src/ui/pages/LoyaltyRewards.tsx | 2 +- frontend/src/ui/pages/ServerSettings.tsx | 2 +- frontend/src/ui/pages/TwitchSettings.tsx | 2 +- frontend/src/ui/pages/UISettingsPage.tsx | 48 +++++++++++++++++++ frontend/src/ui/theme/forms.ts | 4 +- 25 files changed, 230 insertions(+), 42 deletions(-) create mode 100644 frontend/src/locale/languages.ts rename frontend/src/ui/components/{utils => forms}/ControlledInput.tsx (100%) rename frontend/src/ui/components/{ => forms}/Interval.tsx (97%) rename frontend/src/ui/components/{ => forms}/MultiInput.tsx (97%) rename frontend/src/ui/components/{utils => forms}/PasswordField.tsx (100%) create mode 100644 frontend/src/ui/components/forms/RadioGroup.tsx rename frontend/src/ui/components/{utils => forms}/SaveButton.tsx (100%) create mode 100644 frontend/src/ui/pages/UISettingsPage.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9053c50..1b8e71a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-icons": "^1.1.1", "@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-tabs": "^1.0.1", "@radix-ui/react-toggle": "^1.0.1", @@ -953,6 +954,28 @@ "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": { "version": "1.0.1", "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-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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b464137..25d5403 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-icons": "^1.1.1", "@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-tabs": "^1.0.1", "@radix-ui/react-toggle": "^1.0.1", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index e70c688..8d6ba43 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -b120abf6c619728b1d1a33e9dda709c0 \ No newline at end of file +1e088af20465ec91bc3495c250e91b1a \ No newline at end of file diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index bedbd65..b1724cf 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -1,4 +1,7 @@ { + "$meta": { + "language-name": "English" + }, "menu": { "sections": { "monitor": "Monitor", @@ -11,7 +14,8 @@ "dashboard": "Dashboard" }, "strimertul": { - "settings": "Server settings" + "settings": "Server settings", + "ui-config": "User interface" }, "twitch": { "configuration": "Configuration", @@ -253,7 +257,14 @@ "not-live": "Offline / Not streaming" }, "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": { diff --git a/frontend/src/locale/it/translation.json b/frontend/src/locale/it/translation.json index 8a939be..4ede5c3 100644 --- a/frontend/src/locale/it/translation.json +++ b/frontend/src/locale/it/translation.json @@ -1,4 +1,7 @@ { + "$meta": { + "language-name": "Italiano" + }, "form-actions": { "save": "Salva", "saving": "Sto salvando...", diff --git a/frontend/src/locale/languages.ts b/frontend/src/locale/languages.ts new file mode 100644 index 0000000..e56a4e1 --- /dev/null +++ b/frontend/src/locale/languages.ts @@ -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( + (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; diff --git a/frontend/src/locale/setup.ts b/frontend/src/locale/setup.ts index 22998b7..6ad65e9 100644 --- a/frontend/src/locale/setup.ts +++ b/frontend/src/locale/setup.ts @@ -1,19 +1,10 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import { APPNAME } from '~/ui/theme'; - -import en from './en/translation.json'; -import it from './it/translation.json'; +import { resources } from './languages'; void i18n.use(initReactI18next).init({ - resources: { - en: { - translation: en, - }, - it: { - translation: it, - }, - }, + resources, lng: navigator.language, fallbackLng: 'en', interpolation: { diff --git a/frontend/src/store/api/reducer.ts b/frontend/src/store/api/reducer.ts index 8cb21b1..48ebb40 100644 --- a/frontend/src/store/api/reducer.ts +++ b/frontend/src/store/api/reducer.ts @@ -230,6 +230,14 @@ export const modules = { 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( @@ -280,6 +288,7 @@ const initialState: APIState = { twitchBotConfig: null, loyaltyConfig: null, }, + uiConfig: null, requestStatus: {}, }; diff --git a/frontend/src/store/api/types.ts b/frontend/src/store/api/types.ts index 6628a70..87e1006 100644 --- a/frontend/src/store/api/types.ts +++ b/frontend/src/store/api/types.ts @@ -143,6 +143,11 @@ export interface LoyaltyRedeem { request_text: string; } +export interface UISettings { + onboardingDone: boolean; + language: string; +} + export enum ConnectionStatus { NotConnected, AuthenticationNeeded, @@ -176,5 +181,6 @@ export interface APIState { twitchBotConfig: TwitchBotConfig; loyaltyConfig: LoyaltyConfig; }; + uiConfig: UISettings; requestStatus: Record; } diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 9c9c7f2..e5aa00c 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -1,6 +1,7 @@ import { configureStore } from '@reduxjs/toolkit'; -import { useDispatch } from 'react-redux'; +import { EqualityFn, useDispatch, useSelector } from 'react-redux'; import thunkMiddleware from 'redux-thunk'; + import apiReducer from './api/reducer'; import loggingReducer from './logging/reducer'; @@ -19,5 +20,9 @@ const store = configureStore({ export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: ( + selector: (state: RootState) => Selected, + equalityFn?: EqualityFn | undefined, +) => Selected = useSelector; export default store; diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index 03487b0..c440727 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -1,9 +1,10 @@ import { ChatBubbleIcon, + CodeIcon, DashboardIcon, FrameIcon, - GearIcon, MixerHorizontalIcon, + MixIcon, StarIcon, TableIcon, TimerIcon, @@ -11,7 +12,6 @@ import { import { EventsOff, EventsOn } from '@wailsapp/runtime/runtime'; import { t } from 'i18next'; import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { @@ -21,14 +21,16 @@ import { } from '@wailsapp/go/main/App'; import { main } from '@wailsapp/go/models'; -import { RootState, useAppDispatch } from '~/store'; +import { useAppDispatch, useAppSelector } from '~/store'; import { createWSClient, useAuthBypass } from '~/store/api/reducer'; import { ConnectionStatus } from '~/store/api/types'; import loggingReducer from '~/store/logging/reducer'; // @ts-expect-error Asset import import spinner from '~/assets/icon-loading.svg'; +import LogViewer from './components/LogViewer'; import Sidebar, { RouteSection } from './components/Sidebar'; +import Scrollbar from './components/utils/Scrollbar'; import AuthDialog from './pages/AuthDialog'; import TwitchBotCommandsPage from './pages/BotCommands'; import TwitchBotTimersPage from './pages/BotTimers'; @@ -38,13 +40,12 @@ import DebugPage from './pages/Debug'; import LoyaltyConfigPage from './pages/LoyaltyConfig'; import LoyaltyQueuePage from './pages/LoyaltyQueue'; import LoyaltyRewardsPage from './pages/LoyaltyRewards'; +import OnboardingPage from './pages/Onboarding'; import ServerSettingsPage from './pages/ServerSettings'; import StrimertulPage from './pages/Strimertul'; import TwitchSettingsPage from './pages/TwitchSettings'; +import UISettingsPage from './pages/UISettingsPage'; import { styled } from './theme'; -import Scrollbar from './components/utils/Scrollbar'; -import LogViewer from './components/LogViewer'; -import OnboardingPage from './pages/Onboarding'; const LoadingDiv = styled('div', { display: 'flex', @@ -87,7 +88,12 @@ const sections: RouteSection[] = [ { title: 'menu.pages.strimertul.settings', url: '/http', - icon: , + icon: , + }, + { + title: 'menu.pages.strimertul.ui-config', + url: '/ui-config', + icon: , }, ], }, @@ -160,10 +166,9 @@ const PageWrapper = styled('div', { export default function App(): JSX.Element { const [ready, setReady] = useState(false); - const client = useSelector((state: RootState) => state.api.client); - const connected = useSelector( - (state: RootState) => state.api.connectionStatus, - ); + const client = useAppSelector((state) => state.api.client); + const uiConfig = useAppSelector((state) => state.api.uiConfig); + const connected = useAppSelector((state) => state.api.connectionStatus); const dispatch = useAppDispatch(); const location = useLocation(); const navigate = useNavigate(); @@ -212,10 +217,12 @@ export default function App(): JSX.Element { } }, [ready, connected]); - // TODO: Only do this when + const onboardingDone = uiConfig?.onboardingDone; useEffect(() => { - navigate('/setup'); - }, []); + if (!onboardingDone) { + navigate('/setup'); + } + }, [ready, onboardingDone]); if (connected === ConnectionStatus.NotConnected) { return ; @@ -244,6 +251,7 @@ export default function App(): JSX.Element { } /> } /> } /> + } /> } /> + {props.values.map(({ id, label }) => ( +
+ + + + +
+ ))} + + ); +} diff --git a/frontend/src/ui/components/utils/SaveButton.tsx b/frontend/src/ui/components/forms/SaveButton.tsx similarity index 100% rename from frontend/src/ui/components/utils/SaveButton.tsx rename to frontend/src/ui/components/forms/SaveButton.tsx diff --git a/frontend/src/ui/pages/BotTimers.tsx b/frontend/src/ui/pages/BotTimers.tsx index f638873..e3b6087 100644 --- a/frontend/src/ui/pages/BotTimers.tsx +++ b/frontend/src/ui/pages/BotTimers.tsx @@ -8,8 +8,8 @@ import { modules } from '~/store/api/reducer'; import { TwitchBotTimer } from '~/store/api/types'; import AlertContent from '../components/AlertContent'; import DialogContent from '../components/DialogContent'; -import Interval, { hours, minutes } from '../components/Interval'; -import MultiInput from '../components/MultiInput'; +import Interval, { hours, minutes } from '../components/forms/Interval'; +import MultiInput from '../components/forms/MultiInput'; import { Button, Dialog, diff --git a/frontend/src/ui/pages/ChatAlerts.tsx b/frontend/src/ui/pages/ChatAlerts.tsx index 234f6b0..82afb73 100644 --- a/frontend/src/ui/pages/ChatAlerts.tsx +++ b/frontend/src/ui/pages/ChatAlerts.tsx @@ -4,7 +4,7 @@ import { CheckIcon } from '@radix-ui/react-icons'; import { useModule, useStatus } from '~/lib/react-utils'; import apiReducer, { modules } from '~/store/api/reducer'; import { useAppDispatch } from '~/store'; -import MultiInput from '../components/MultiInput'; +import MultiInput from '../components/forms/MultiInput'; import { Checkbox, CheckboxIndicator, @@ -20,7 +20,7 @@ import { TabList, TextBlock, } from '../theme'; -import SaveButton from '../components/utils/SaveButton'; +import SaveButton from '../components/forms/SaveButton'; export default function ChatAlertsPage(): React.ReactElement { const { t } = useTranslation(); diff --git a/frontend/src/ui/pages/LoyaltyConfig.tsx b/frontend/src/ui/pages/LoyaltyConfig.tsx index 6dd3761..ec75df3 100644 --- a/frontend/src/ui/pages/LoyaltyConfig.tsx +++ b/frontend/src/ui/pages/LoyaltyConfig.tsx @@ -17,8 +17,8 @@ import { InputBox, FieldNote, } from '../theme'; -import SaveButton from '../components/utils/SaveButton'; -import Interval from '../components/Interval'; +import SaveButton from '../components/forms/SaveButton'; +import Interval from '../components/forms/Interval'; export default function LoyaltySettingsPage(): React.ReactElement { const { t } = useTranslation(); diff --git a/frontend/src/ui/pages/LoyaltyRewards.tsx b/frontend/src/ui/pages/LoyaltyRewards.tsx index ebd1585..847bda5 100644 --- a/frontend/src/ui/pages/LoyaltyRewards.tsx +++ b/frontend/src/ui/pages/LoyaltyRewards.tsx @@ -7,7 +7,7 @@ import { modules } from '~/store/api/reducer'; import { LoyaltyGoal, LoyaltyReward } from '~/store/api/types'; import AlertContent from '../components/AlertContent'; import DialogContent from '../components/DialogContent'; -import Interval from '../components/Interval'; +import Interval from '../components/forms/Interval'; import { Button, Checkbox, diff --git a/frontend/src/ui/pages/ServerSettings.tsx b/frontend/src/ui/pages/ServerSettings.tsx index 9dc5877..8ed2b15 100644 --- a/frontend/src/ui/pages/ServerSettings.tsx +++ b/frontend/src/ui/pages/ServerSettings.tsx @@ -5,7 +5,7 @@ import { useAppDispatch } from '~/store'; import apiReducer, { modules } from '~/store/api/reducer'; import AlertContent from '../components/AlertContent'; import RevealLink from '../components/utils/RevealLink'; -import SaveButton from '../components/utils/SaveButton'; +import SaveButton from '../components/forms/SaveButton'; import { Field, FieldNote, diff --git a/frontend/src/ui/pages/TwitchSettings.tsx b/frontend/src/ui/pages/TwitchSettings.tsx index 599280a..ce3a4ec 100644 --- a/frontend/src/ui/pages/TwitchSettings.tsx +++ b/frontend/src/ui/pages/TwitchSettings.tsx @@ -12,7 +12,7 @@ import apiReducer, { modules } from '~/store/api/reducer'; import BrowserLink from '../components/BrowserLink'; import DefinitionTable from '../components/DefinitionTable'; import RevealLink from '../components/utils/RevealLink'; -import SaveButton from '../components/utils/SaveButton'; +import SaveButton from '../components/forms/SaveButton'; import { Button, ButtonGroup, diff --git a/frontend/src/ui/pages/UISettingsPage.tsx b/frontend/src/ui/pages/UISettingsPage.tsx new file mode 100644 index 0000000..5ea85dd --- /dev/null +++ b/frontend/src/ui/pages/UISettingsPage.tsx @@ -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 ( + + + {t('pages.uiconfig.title')} + + + + ({ + id: lang.code, + label: ( + + {lang.name}{' '} + {lang.keys < maxKeys ? ( + + Partial translation ( + {((lang.keys / maxKeys) * 100).toFixed(1)}% - {lang.keys}/ + {maxKeys}) + + ) : null} + + ), + }))} + /> + + + ); +} diff --git a/frontend/src/ui/theme/forms.ts b/frontend/src/ui/theme/forms.ts index 4ce7bd5..1bfb5e9 100644 --- a/frontend/src/ui/theme/forms.ts +++ b/frontend/src/ui/theme/forms.ts @@ -2,8 +2,8 @@ import * as UnstyledLabel from '@radix-ui/react-label'; import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; import * as ToggleGroup from '@radix-ui/react-toggle-group'; import { styled, theme } from './theme'; -import ControlledInput from '../components/utils/ControlledInput'; -import PasswordField from '../components/utils/PasswordField'; +import ControlledInput from '../components/forms/ControlledInput'; +import PasswordField from '../components/forms/PasswordField'; export const Field = styled('fieldset', { all: 'unset',