mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
Twitch settings done
This commit is contained in:
parent
5f59763372
commit
9fa27356b8
13 changed files with 602 additions and 26 deletions
88
frontend/package-lock.json
generated
88
frontend/package-lock.json
generated
|
@ -433,6 +433,55 @@
|
|||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-checkbox": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-0.1.4.tgz",
|
||||
"integrity": "sha512-UtiV0y4CNmcAdCqRaGGPxeET/asO44rfKxtBbMIxAD4BGPfSIe4+kkF0q624S5c7q07HXO0vhOYlSObR3Fj2bg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-label": "0.1.4",
|
||||
"@radix-ui/react-presence": "0.1.1",
|
||||
"@radix-ui/react-primitive": "0.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "0.1.0",
|
||||
"@radix-ui/react-use-previous": "0.1.0",
|
||||
"@radix-ui/react-use-size": "0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-id": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.4.tgz",
|
||||
"integrity": "sha512-/hq5m/D0ZfJWOS7TLF+G0l08KDRs87LBE46JkAvgKkg1fW4jkucx9At9D9vauIPSbdNmww5kXEp566hMlA8eXA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-label": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-0.1.4.tgz",
|
||||
"integrity": "sha512-I59IMdUhHixk6cG4D00UN+oFxTpur9cJQSOl+4EfSTJZs+x4PqDpM7p402/gb9sXJqylsUkDMHDdaPzLwPNf7g==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-id": "0.1.4",
|
||||
"@radix-ui/react-primitive": "0.1.3"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-collection": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-0.1.3.tgz",
|
||||
|
@ -498,6 +547,16 @@
|
|||
"@radix-ui/react-primitive": "0.1.2"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-presence": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-0.1.1.tgz",
|
||||
"integrity": "sha512-LsL+NcWDpFUAYCmXeH02o4pgqcSLpwxP84UIjCtpIKrsPe2vLuhcp79KC/jZJeXz+of2lUpMAxpM+eCpxFZtlg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.2.tgz",
|
||||
|
@ -612,6 +671,22 @@
|
|||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-previous": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-0.1.0.tgz",
|
||||
"integrity": "sha512-0fxNc33rYnCzDMPSiSnfS8YklnxQo8WqbAQXPAgIaaA1jRu2qFB916PL4qCIW+avcAAqFD38vWhqDqcVmBharA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-size": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-0.1.0.tgz",
|
||||
"integrity": "sha512-TcZAsR+BYI46w/RbaSFCRACl+Jh6mDqhu6GS2r0iuJpIVrj8atff7qtTjmMmfGtEDNEjhl7DxN3pr1nTS/oruQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.2.tgz",
|
||||
|
@ -686,14 +761,6 @@
|
|||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@types/react-custom-scroll": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-custom-scroll/-/react-custom-scroll-4.3.2.tgz",
|
||||
"integrity": "sha512-0gFAkoTihBzYcyoiw68qYIgTeHwPbCLbRMFftsVYlXLLvXx4y519If/+1r7UtOaF7aAXRRJOCYaHZj/HRyfc8Q==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "17.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.4.tgz",
|
||||
|
@ -2598,11 +2665,6 @@
|
|||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"react-custom-scroll": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-custom-scroll/-/react-custom-scroll-4.3.0.tgz",
|
||||
"integrity": "sha512-JHNWBAzyw3MgsCNcG6uXJQkCoVPmWxeEW01ABmUz4HsWxw3EQJ2GzrUa5y4ISI85V3q6O4zwCIZAVn10aM3clQ=="
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"dependencies": {
|
||||
"@billjs/event-emitter": "^1.0.3",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@radix-ui/react-checkbox": "^0.1.4",
|
||||
"@radix-ui/react-icons": "^1.0.3",
|
||||
"@radix-ui/react-label": "^0.1.3",
|
||||
"@radix-ui/react-tabs": "^0.1.4",
|
||||
|
|
|
@ -72,6 +72,33 @@
|
|||
"auth-button": "Authenticate as Twitch",
|
||||
"auth-message": "Click the following button to authenticate the back-end with your Twitch account:",
|
||||
"current-status": "Current status"
|
||||
},
|
||||
"twitch-settings": {
|
||||
"title": "Twitch configuration",
|
||||
"enable": "Enable Twitch integration",
|
||||
"apiguide-1": "You will need to create an application, here's how:",
|
||||
"apiguide-2": "Go to <1>https://dev.twitch.tv/console/apps/create</1>",
|
||||
"apiguide-3": "Use the following data for the required fields:",
|
||||
"oauth-redir-uri": "OAuth Redirect URLs",
|
||||
"apiguide-4": "Once made, create a <1>New Secret</1>, then copy both fields below and save!",
|
||||
"app-client-id": "App Client ID",
|
||||
"app-client-secret": "App Client Secret",
|
||||
"subtitle": "Twitch integration with streams including chat bot and API access. If you stream on Twitch, you definitely want this on.",
|
||||
"api-subheader": "Application info",
|
||||
"api-configuration": "API access",
|
||||
"bot-settings": "Bot settings",
|
||||
"enable-bot": "Enable Twitch bot",
|
||||
"bot-channel": "Twitch channel",
|
||||
"bot-username": "Twitch account username",
|
||||
"bot-oauth": "Authorization token",
|
||||
"bot-oauth-note": "You can get this by logging in with the bot account and going here: <1>https://twitchapps.com/tmi/</1>",
|
||||
"bot-info-header": "Bot account info",
|
||||
"bot-settings-copy": "A bot can interact with chat messages and provide extra events for the platform (chat events, some notifications) but requires access to a Twitch account. You can use your own or make a new one (if enabled on your main account, you can re-use the same email for your second account!)",
|
||||
"bot-chat-header": "Chat logging",
|
||||
"bot-chat-keys": "Enable chat logging",
|
||||
"bot-chat-history": "How many messages to keep in history",
|
||||
"bot-chat-history-suffix": "messages",
|
||||
"bot-chat-history-desc": "Chat logging allows for pages and modules to read messages from chat. It's recommended to keep chat logging enabled with a reasonable history size (eg. 6)"
|
||||
}
|
||||
},
|
||||
"form-actions": {
|
||||
|
|
|
@ -25,6 +25,7 @@ import { styled } from './theme';
|
|||
// @ts-expect-error Asset import
|
||||
import spinner from '../assets/icon-loading.svg';
|
||||
import BackendIntegrationPage from './pages/BackendIntegration';
|
||||
import TwitchSettingsPage from './pages/TwitchSettings';
|
||||
|
||||
const LoadingDiv = styled('div', {
|
||||
display: 'flex',
|
||||
|
@ -130,16 +131,23 @@ const sections: RouteSection[] = [
|
|||
];
|
||||
|
||||
const Container = styled('div', {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
minHeight: '100vh',
|
||||
overflow: 'hidden',
|
||||
height: '100vh',
|
||||
});
|
||||
|
||||
const PageContent = styled('main', {
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
const PageWrapper = styled('div', {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
|
@ -175,11 +183,14 @@ export default function App(): JSX.Element {
|
|||
<Container>
|
||||
<Sidebar sections={sections} />
|
||||
<PageContent>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/http" element={<ServerSettingsPage />} />
|
||||
<Route path="/backend" element={<BackendIntegrationPage />} />
|
||||
</Routes>
|
||||
<PageWrapper role="main">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/http" element={<ServerSettingsPage />} />
|
||||
<Route path="/backend" element={<BackendIntegrationPage />} />
|
||||
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
|
||||
</Routes>
|
||||
</PageWrapper>
|
||||
</PageContent>
|
||||
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
|
||||
</Container>
|
||||
|
|
40
frontend/src/ui/components/DefinitionTable.tsx
Normal file
40
frontend/src/ui/components/DefinitionTable.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import { styled } from '../theme';
|
||||
|
||||
const TableContainer = styled('table', {
|
||||
borderRadius: '3px',
|
||||
backgroundColor: '$gray2',
|
||||
padding: '0.3rem',
|
||||
margin: '0.5rem 0',
|
||||
});
|
||||
|
||||
const Term = styled('th', {
|
||||
padding: '0.3rem 0.5rem',
|
||||
textAlign: 'right',
|
||||
color: '$teal11',
|
||||
});
|
||||
|
||||
const Definition = styled('td', {
|
||||
padding: '0.3rem 0.5rem',
|
||||
});
|
||||
|
||||
interface DefinitionTableProps {
|
||||
entries: Record<string, string>;
|
||||
}
|
||||
|
||||
function DefinitionTable({ entries }: DefinitionTableProps) {
|
||||
return (
|
||||
<TableContainer>
|
||||
<tbody>
|
||||
{Object.entries(entries).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<Term>{key}</Term>
|
||||
<Definition>{value}</Definition>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DefinitionTable);
|
|
@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { ExternalLinkIcon } from '@radix-ui/react-icons';
|
||||
import { useModule, useStatus } from '../../lib/react-utils';
|
||||
import Stulbe from '../../lib/stulbe-lib';
|
||||
|
@ -20,8 +19,10 @@ import {
|
|||
SectionHeader,
|
||||
styled,
|
||||
TabButton,
|
||||
TabContainer,
|
||||
TabContent,
|
||||
TabList,
|
||||
TextBlock,
|
||||
} from '../theme';
|
||||
import eventsubTests from '../../data/eventsub-tests';
|
||||
import { RootState } from '../../store';
|
||||
|
@ -299,15 +300,15 @@ export default function BackendIntegrationPage(): React.ReactElement {
|
|||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitle>{t('pages.stulbe.title')}</PageTitle>
|
||||
<p>
|
||||
<TextBlock>
|
||||
<Trans i18nKey="pages.stulbe.subtitle">
|
||||
{' '}
|
||||
<a href="https://github.com/strimertul/stulbe/">stulbe</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</TextBlock>
|
||||
</PageHeader>
|
||||
|
||||
<Tabs.Root defaultValue="configuration">
|
||||
<TabContainer defaultValue="configuration">
|
||||
<TabList>
|
||||
<TabButton value="configuration">
|
||||
{t('pages.stulbe.configuration')}
|
||||
|
@ -322,7 +323,7 @@ export default function BackendIntegrationPage(): React.ReactElement {
|
|||
<TabContent value="webhook">
|
||||
<WebhookIntegration />
|
||||
</TabContent>
|
||||
</Tabs.Root>
|
||||
</TabContainer>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
374
frontend/src/ui/pages/TwitchSettings.tsx
Normal file
374
frontend/src/ui/pages/TwitchSettings.tsx
Normal file
|
@ -0,0 +1,374 @@
|
|||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useModule, useStatus } from '../../lib/react-utils';
|
||||
import apiReducer, { modules } from '../../store/api/reducer';
|
||||
import DefinitionTable from '../components/DefinitionTable';
|
||||
import SaveButton from '../components/utils/SaveButton';
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxIndicator,
|
||||
Field,
|
||||
FieldNote,
|
||||
FlexRow,
|
||||
InputBox,
|
||||
Label,
|
||||
PageContainer,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
SectionHeader,
|
||||
styled,
|
||||
TabButton,
|
||||
TabContainer,
|
||||
TabContent,
|
||||
TabList,
|
||||
TextBlock,
|
||||
} from '../theme';
|
||||
|
||||
const StepList = styled('ul', {
|
||||
lineHeight: '1.5',
|
||||
listStyleType: 'none',
|
||||
listStylePosition: 'outside',
|
||||
});
|
||||
const Step = styled('li', {
|
||||
marginBottom: '0.5rem',
|
||||
paddingLeft: '1rem',
|
||||
'&::marker': {
|
||||
color: '$teal11',
|
||||
content: '▧',
|
||||
display: 'inline-block',
|
||||
marginLeft: '-0.5rem',
|
||||
},
|
||||
});
|
||||
|
||||
function TwitchBotSettings() {
|
||||
const [botConfig, setBotConfig, loadStatus] = useModule(
|
||||
modules.twitchBotConfig,
|
||||
);
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const status = useStatus(loadStatus.save);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const active = twitchConfig?.enable_bot ?? false;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
dispatch(setTwitchConfig(twitchConfig));
|
||||
dispatch(setBotConfig(botConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<TextBlock>{t('pages.twitch-settings.bot-settings-copy')}</TextBlock>
|
||||
<Field>
|
||||
<FlexRow spacing={1}>
|
||||
<Checkbox
|
||||
checked={active}
|
||||
onCheckedChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
enable_bot: !!ev,
|
||||
}),
|
||||
)
|
||||
}
|
||||
id="enable-bot"
|
||||
>
|
||||
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label htmlFor="enable-bot">
|
||||
{t('pages.twitch-settings.enable-bot')}
|
||||
</Label>
|
||||
</FlexRow>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-channel">
|
||||
{t('pages.twitch-settings.bot-channel')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="bot-channel"
|
||||
required={active}
|
||||
disabled={!active || status?.type === 'pending'}
|
||||
value={botConfig?.channel ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
channel: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.bot-info-header')}
|
||||
</SectionHeader>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-username">
|
||||
{t('pages.twitch-settings.bot-username')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="bot-username"
|
||||
required={active}
|
||||
disabled={!active || status?.type === 'pending'}
|
||||
value={botConfig?.username ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
username: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-oauth">
|
||||
{t('pages.twitch-settings.bot-oauth')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="password"
|
||||
id="bot-oauth"
|
||||
required={active}
|
||||
disabled={!active || status?.type === 'pending'}
|
||||
value={botConfig?.oauth ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
oauth: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FieldNote>
|
||||
<Trans i18nKey="pages.twitch-settings.bot-oauth-note">
|
||||
{' '}
|
||||
<a href="https://twitchapps.com/tmi/">
|
||||
https://twitchapps.com/tmi/
|
||||
</a>
|
||||
</Trans>
|
||||
</FieldNote>
|
||||
</Field>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.bot-chat-header')}
|
||||
</SectionHeader>
|
||||
<TextBlock>{t('pages.twitch-settings.bot-chat-history-desc')}</TextBlock>
|
||||
<Field>
|
||||
<FlexRow spacing={1}>
|
||||
<Checkbox
|
||||
disabled={!active || status?.type === 'pending'}
|
||||
checked={botConfig?.chat_keys ?? false}
|
||||
onCheckedChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
chat_keys: !!ev,
|
||||
}),
|
||||
)
|
||||
}
|
||||
id="bot-chat-keys"
|
||||
>
|
||||
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label htmlFor="bot-chat-keys">
|
||||
{t('pages.twitch-settings.bot-chat-keys')}
|
||||
</Label>
|
||||
</FlexRow>
|
||||
</Field>
|
||||
<Field size="vertical">
|
||||
<Label htmlFor="bot-chat-history">
|
||||
{t('pages.twitch-settings.bot-chat-history')}
|
||||
</Label>
|
||||
<FlexRow
|
||||
css={{
|
||||
justifyContent: 'flex-start',
|
||||
gap: '0.8rem',
|
||||
backgroundColor: '$gray6',
|
||||
borderRadius: '5px',
|
||||
paddingRight: '0.8rem',
|
||||
}}
|
||||
>
|
||||
<InputBox
|
||||
type="number"
|
||||
id="bot-chat-history"
|
||||
required={active}
|
||||
disabled={!active || status?.type === 'pending'}
|
||||
value={botConfig?.chat_history ?? ''}
|
||||
css={{
|
||||
appearance: 'textfield',
|
||||
width: '4rem',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
chat_history: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
{t('pages.twitch-settings.bot-chat-history-suffix')}
|
||||
</FlexRow>
|
||||
</Field>
|
||||
<SaveButton status={status} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function TwitchAPISettings() {
|
||||
const { t } = useTranslation();
|
||||
const [httpConfig] = useModule(modules.httpConfig);
|
||||
const [twitchConfig, setTwitchConfig, loadStatus] = useModule(
|
||||
modules.twitchConfig,
|
||||
);
|
||||
const status = useStatus(loadStatus.save);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
dispatch(setTwitchConfig(twitchConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<SectionHeader spacing={'none'}>
|
||||
{t('pages.twitch-settings.api-subheader')}
|
||||
</SectionHeader>
|
||||
<TextBlock>{t('pages.twitch-settings.apiguide-1')}</TextBlock>
|
||||
<StepList>
|
||||
<Step>
|
||||
<Trans i18nKey="pages.twitch-settings.apiguide-2">
|
||||
{' '}
|
||||
<a href="https://dev.twitch.tv/console/apps/create">
|
||||
https://dev.twitch.tv/console/apps/create
|
||||
</a>
|
||||
</Trans>
|
||||
</Step>
|
||||
<Step>
|
||||
{t('pages.twitch-settings.apiguide-3')}
|
||||
|
||||
<DefinitionTable
|
||||
entries={{
|
||||
'OAuth Redirect URLs': `http://${
|
||||
httpConfig?.bind.indexOf(':') > 0
|
||||
? httpConfig.bind
|
||||
: `localhost${httpConfig?.bind ?? ':4337'}`
|
||||
}/oauth`,
|
||||
Category: 'Broadcasting Suite',
|
||||
}}
|
||||
/>
|
||||
</Step>
|
||||
<Step>
|
||||
<Trans i18nKey="pages.twitch-settings.apiguide-4">
|
||||
{'str1 '}
|
||||
<b>str2</b>
|
||||
</Trans>
|
||||
</Step>
|
||||
</StepList>
|
||||
<Field size="fullWidth" css={{ marginTop: '2rem' }}>
|
||||
<Label htmlFor="clientid">
|
||||
{t('pages.twitch-settings.app-client-id')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="clientid"
|
||||
placeholder={t('pages.twitch-settings.app-client-id')}
|
||||
required={true}
|
||||
value={twitchConfig?.api_client_id ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
api_client_id: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="clientsecret">
|
||||
{t('pages.twitch-settings.app-client-secret')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="password"
|
||||
id="clientsecret"
|
||||
placeholder={t('pages.twitch-settings.app-client-secret')}
|
||||
required={true}
|
||||
value={twitchConfig?.api_client_secret ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
api_client_secret: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<SaveButton status={status} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TwitchSettingsPage(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const active = twitchConfig?.enabled ?? false;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitle>{t('pages.twitch-settings.title')}</PageTitle>
|
||||
<TextBlock>{t('pages.twitch-settings.subtitle')}</TextBlock>
|
||||
<Field>
|
||||
<FlexRow spacing={1}>
|
||||
<Checkbox
|
||||
checked={active}
|
||||
onCheckedChange={(ev) =>
|
||||
dispatch(
|
||||
setTwitchConfig({
|
||||
...twitchConfig,
|
||||
enabled: !!ev,
|
||||
}),
|
||||
)
|
||||
}
|
||||
id="enable"
|
||||
>
|
||||
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label htmlFor="enable">{t('pages.twitch-settings.enable')}</Label>
|
||||
</FlexRow>
|
||||
</Field>
|
||||
</PageHeader>
|
||||
<div style={{ display: active ? '' : 'none' }}>
|
||||
<TabContainer defaultValue="api-config">
|
||||
<TabList>
|
||||
<TabButton value="api-config">
|
||||
{t('pages.twitch-settings.api-configuration')}
|
||||
</TabButton>
|
||||
<TabButton value="bot-settings">
|
||||
{t('pages.twitch-settings.bot-settings')}
|
||||
</TabButton>
|
||||
</TabList>
|
||||
<TabContent value="api-config">
|
||||
<TwitchAPISettings />
|
||||
</TabContent>
|
||||
<TabContent value="bot-settings">
|
||||
<TwitchBotSettings />
|
||||
</TabContent>
|
||||
</TabContainer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import * as UnstyledLabel from '@radix-ui/react-label';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { styled } from './theme';
|
||||
|
||||
export const Field = styled('fieldset', {
|
||||
|
@ -6,11 +7,17 @@ export const Field = styled('fieldset', {
|
|||
marginBottom: '2rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
variants: {
|
||||
size: {
|
||||
fullWidth: {
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
vertical: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -95,3 +102,33 @@ export const Button = styled('button', {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Checkbox = styled(CheckboxPrimitive.Root, {
|
||||
all: 'unset',
|
||||
width: 25,
|
||||
height: 25,
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid $teal6',
|
||||
backgroundColor: '$teal4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$teal5',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$teal6',
|
||||
},
|
||||
'&:disabled': {
|
||||
backgroundColor: '$gray4',
|
||||
borderColor: '$gray5',
|
||||
color: '$gray8',
|
||||
},
|
||||
});
|
||||
|
||||
export const CheckboxIndicator = styled(CheckboxPrimitive.Indicator, {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '$teal11',
|
||||
});
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from './brand';
|
|||
export * from './forms';
|
||||
export * from './pages';
|
||||
export * from './tabs';
|
||||
export * from './utils';
|
||||
|
|
|
@ -28,3 +28,7 @@ export const SectionHeader = styled('h2', {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const TextBlock = styled('p', {
|
||||
lineHeight: '1.5',
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { styled } from './theme';
|
||||
|
||||
export const TabContainer = styled(Tabs.Root, {});
|
||||
|
||||
export const TabList = styled(Tabs.List, {
|
||||
borderBottom: '1px solid $gray6',
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
import { globalCss, createStitches } from '@stitches/react';
|
||||
|
||||
export const globalStyles = globalCss({
|
||||
'*': { boxSizing: 'border-box' },
|
||||
body: { margin: 0, padding: 0, backgroundColor: '$gray1', color: '$teal12' },
|
||||
html: {
|
||||
margin: 0,
|
||||
|
|
15
frontend/src/ui/theme/utils.ts
Normal file
15
frontend/src/ui/theme/utils.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { styled } from './theme';
|
||||
|
||||
export const FlexRow = styled('div', {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
variants: {
|
||||
spacing: {
|
||||
'1': {
|
||||
gap: '0.5rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue