1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00

Overlay scroll and webhook page

This commit is contained in:
Ash Keel 2021-12-16 17:01:24 +01:00
parent 2703c39df8
commit 5f59763372
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
14 changed files with 612 additions and 197 deletions

View file

@ -425,6 +425,37 @@
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
},
"@radix-ui/primitive": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-0.1.0.tgz",
"integrity": "sha512-tqxZKybwN5Fa3VzZry4G6mXAAb9aAqKmPtnVbZpL0vsBwvOHTBwsjHVPXylocYLwEtBY9SCe665bYnNB515uoA==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-collection": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-0.1.3.tgz",
"integrity": "sha512-tMBY65l87tj77fMX44EBjm5p8clR6swkcNFr0/dDVdEPC0Vf3fwkv62dezCnZyrRBpkOgZPDOp2kO73hYlCfXw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-primitive": "0.1.3",
"@radix-ui/react-slot": "0.1.2"
},
"dependencies": {
"@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-compose-refs": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz",
@ -476,6 +507,42 @@
"@radix-ui/react-slot": "0.1.2"
}
},
"@radix-ui/react-roving-focus": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-0.1.4.tgz",
"integrity": "sha512-zaixcAxRcWQliUSx6l9rdfJhvcbuY7Tb4Emb7H4DWCTx1kenXH8+n9mwa8gaSIJLLSSSMzBpQATlpFw9xv/bJQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-collection": "0.1.3",
"@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-use-callback-ref": "0.1.0",
"@radix-ui/react-use-controllable-state": "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-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-slot": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz",
@ -485,6 +552,58 @@
"@radix-ui/react-compose-refs": "0.1.0"
}
},
"@radix-ui/react-tabs": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-0.1.4.tgz",
"integrity": "sha512-5UK1j3vcFQTNlsFjgxLHFw2eGLJ5EYL40/YHWWkH8fK6UC9+JSfVSVZQYImoZOeUVWgHJSEBaNdbikncjnUsKw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "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-roving-focus": "0.1.4",
"@radix-ui/react-use-callback-ref": "0.1.0",
"@radix-ui/react-use-controllable-state": "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-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-use-callback-ref": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.1.0.tgz",
"integrity": "sha512-Va041McOFFl+aV+sejvl0BS2aeHx86ND9X/rVFmEFQKTXCp6xgUK0NGUAGcgBlIjnJSbMYPGEk1xKSSlVcN2Aw==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-controllable-state": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz",
"integrity": "sha512-zv7CX/PgsRl46a52Tl45TwqwVJdmqnlQEQhaYMz/yBOD2sx2gCkCFSoF/z9mpnYWmS6DTLNTg5lIps3fV6EnXg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "0.1.0"
}
},
"@radix-ui/react-use-layout-effect": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz",
@ -567,6 +686,14 @@
"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",
@ -2270,6 +2397,16 @@
"word-wrap": "^1.2.3"
}
},
"overlayscrollbars": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.1.tgz",
"integrity": "sha512-gIQfzgGgu1wy80EB4/6DaJGHMEGmizq27xHIESrzXq0Y/J0Ay1P3DWk6tuVmEPIZH15zaBlxeEJOqdJKmowHCQ=="
},
"overlayscrollbars-react": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.2.3.tgz",
"integrity": "sha512-eN/JsEtJvPulOXOZXIdo1H90eriUWcgj4TwSdOcchk2M4uY2/BpsHlZ2+0viZMLXTcNQNJz+/4m47NugSBg+0g=="
},
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
@ -2461,6 +2598,11 @@
"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",

View file

@ -6,6 +6,7 @@
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-icons": "^1.0.3",
"@radix-ui/react-label": "^0.1.3",
"@radix-ui/react-tabs": "^0.1.4",
"@reduxjs/toolkit": "^1.5.1",
"@stitches/react": "^1.2.6",
"@strimertul/kilovolt-client": "^6.2.0",
@ -16,6 +17,8 @@
"i18next": "^20.6.1",
"inter-ui": "^3.19.3",
"normalize.css": "^8.0.1",
"overlayscrollbars": "^1.13.1",
"overlayscrollbars-react": "^0.2.3",
"postcss-import": "^14.0.2",
"pretty-ms": "^7.0.1",
"react": "^17.0.2",

View file

@ -6,12 +6,15 @@ import { BrowserRouter } from 'react-router-dom';
import 'inter-ui/inter.css';
import 'normalize.css/normalize.css';
import 'react-toastify/dist/ReactToastify.css';
import 'overlayscrollbars/css/OverlayScrollbars.css';
import './locale/setup';
import './style.css';
import store from './store';
import App from './ui/App';
import { globalStyles } from './ui/theme';
globalStyles();
ReactDOM.render(
<Provider store={store}>

View file

@ -25,6 +25,9 @@
"points": "Points and redeems",
"rewards": "Rewards and goals"
}
},
"messages": {
"update-available": "UPDATE AVAILABLE"
}
},
"pages": {
@ -47,8 +50,28 @@
"endpoint": "Back-end endpoint",
"auth-key": "Authorization key",
"username": "User name",
"subtitle": "Optional back-end integration (using <1>stulbe</1> or any Kilovolt compatible endpoint) for syncing keys and obtaining webhook events",
"bind-placeholder": "HTTP endpoint, leave empty to disable integration"
"subtitle": "Optional back-end integration (using <1>stulbe</1> or any Kilovolt compatible endpoint) for syncing keys and obtaining webhook events.",
"bind-placeholder": "HTTP endpoint, leave empty to disable integration",
"configuration": "Configuration",
"twitch-events": "Twitch events",
"err-not-enabled": "Please configure the back-end before accessing this page",
"loading-data": "Querying user data from backend…",
"authenticated-as": "Authenticated as",
"profile-picture": "Profile picture",
"err-no-user": "No twitch user is currently associated (and therefore webhooks are disabled!)",
"sim": {
"channel.update": "Channel update",
"channel.follow": "New follow",
"channel.subscribe": "New sub",
"channel.subscription.gift": "Gift sub",
"channel.subscription.message": "Re-sub with message",
"channel.cheer": "Cheer",
"channel.raid": "Raid"
},
"sim-events": "Send test event",
"auth-button": "Authenticate as Twitch",
"auth-message": "Click the following button to authenticate the back-end with your Twitch account:",
"current-status": "Current status"
}
},
"form-actions": {

View file

@ -1,28 +0,0 @@
@import '@radix-ui/colors/grayDark.css';
@import '@radix-ui/colors/tealDark.css';
body,
html {
margin: 0;
padding: 0;
}
body {
background-color: var(--gray1);
color: var(--teal12);
}
html {
font-family: 'Inter', 'system-ui';
}
@supports (font-variation-settings: normal) {
html {
font-family: 'Inter var', 'system-ui';
}
}
a,
a:visited {
color: var(--teal11);
}

View file

@ -26,18 +26,18 @@ import { styled } from './theme';
import spinner from '../assets/icon-loading.svg';
import BackendIntegrationPage from './pages/BackendIntegration';
const LoadingDiv = styled('div', {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
});
const Spinner = styled('img', {
maxWidth: '100px',
});
function Loading() {
const LoadingDiv = styled('div', {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
});
const Spinner = styled('img', {
maxWidth: '100px',
});
return (
<LoadingDiv>
<Spinner src={spinner} alt="Loading..." />
@ -129,6 +129,19 @@ const sections: RouteSection[] = [
},
];
const Container = styled('div', {
display: 'flex',
flexDirection: 'row',
minHeight: '100vh',
});
const PageContent = styled('main', {
display: 'flex',
flexDirection: 'row',
flex: 1,
overflow: 'auto',
});
export default function App(): JSX.Element {
const client = useSelector((state: RootState) => state.api.client);
const connected = useSelector(
@ -158,20 +171,16 @@ export default function App(): JSX.Element {
return <AuthDialog />;
}
const Container = styled('main', {
display: 'flex',
flexDirection: 'row',
minHeight: '100vh',
});
return (
<Container>
<Sidebar sections={sections} />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/http" element={<ServerSettingsPage />} />
<Route path="/backend" element={<BackendIntegrationPage />} />
</Routes>
<PageContent>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/http" element={<ServerSettingsPage />} />
<Route path="/backend" element={<BackendIntegrationPage />} />
</Routes>
</PageContent>
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
</Container>
);

View file

@ -3,8 +3,9 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { RootState } from '../../store';
import { APPNAME, APPREPO } from '../brand';
import { APPNAME, APPREPO } from '../theme';
export interface RouteSection {
title: string;
@ -22,11 +23,8 @@ interface SidebarProps {
}
const Container = styled('section', {
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
background: '$gray1',
maxWidth: '220px',
flexShrink: 0,
borderRight: '1px solid $gray6',
});
@ -77,7 +75,7 @@ const MenuHeader = styled('header', {
color: '$teal9',
});
const MenuLink = styled(Link, {
color: '$teal13',
color: '$teal13 !important',
display: 'flex',
alignItems: 'center',
textDecoration: 'none',
@ -88,7 +86,7 @@ const MenuLink = styled(Link, {
variants: {
status: {
selected: {
color: '$teal13',
color: '$teal13 !important',
backgroundColor: '$teal5',
},
clickable: {
@ -165,24 +163,30 @@ export default function Sidebar({
return (
<Container>
<Header>
<AppName>{APPNAME}</AppName>
<VersionLabel>
{version && !dev ? version : t('debug.dev-build')}
</VersionLabel>
{!dev && lastVersion && !version.startsWith(lastVersion.name) && (
<UpdateButton href={lastVersion.url}>UPDATE AVAILABLE</UpdateButton>
)}
</Header>
{sections.map(({ title: sectionTitle, links }) => (
<MenuSection key={sectionTitle}>
<MenuHeader>{t(sectionTitle)}</MenuHeader>
{links.map((route) => (
<SidebarLink route={route} key={`${route.title}-${route.url}`} />
))}
</MenuSection>
))}
<OverlayScrollbarsComponent
style={{ maxHeight: '100vh' }}
options={{ scrollbars: { autoHide: 'scroll' } }}
>
<Header>
<AppName>{APPNAME}</AppName>
<VersionLabel>
{version && !dev ? version : t('debug.dev-build')}
</VersionLabel>
{!dev && lastVersion && !version.startsWith(lastVersion.name) && (
<UpdateButton href={lastVersion.url}>
{t('menu.messages.update-available')}
</UpdateButton>
)}
</Header>
{sections.map(({ title: sectionTitle, links }) => (
<MenuSection key={sectionTitle}>
<MenuHeader>{t(sectionTitle)}</MenuHeader>
{links.map((route) => (
<SidebarLink route={route} key={`${route.title}-${route.url}`} />
))}
</MenuSection>
))}
</OverlayScrollbarsComponent>
</Container>
);
}

View file

@ -1,7 +1,9 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
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';
import apiReducer, { modules } from '../../store/api/reducer';
@ -15,9 +17,189 @@ import {
PageContainer,
PageHeader,
PageTitle,
SectionHeader,
styled,
TabButton,
TabContent,
TabList,
} from '../theme';
import eventsubTests from '../../data/eventsub-tests';
import { RootState } from '../../store';
export default function BackendIntegrationPage(): React.ReactElement {
interface UserData {
id: string;
login: string;
// eslint-disable-next-line camelcase
display_name: string;
// eslint-disable-next-line camelcase
profile_image_url: string;
}
interface SyncError {
ok: false;
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', {
display: 'flex',
gap: '0.8rem',
alignItems: 'center',
fontSize: '14pt',
fontWeight: '300',
});
const TwitchPic = styled('img', {
width: '48px',
borderRadius: '50%',
});
const TwitchName = styled('p', { fontWeight: 'bold' });
function WebhookIntegration() {
const { t } = useTranslation();
const [stulbeConfig] = useModule(modules.stulbeConfig);
const kv = useSelector((state: RootState) => state.api.client);
const [userStatus, setUserStatus] = useState<UserData | SyncError>(null);
const [client, setClient] = useState<Stulbe>(null);
const getUserInfo = async () => {
try {
const res = (await client.makeRequest(
'GET',
'api/twitch/user',
)) as UserData;
setUserStatus(res);
} catch (e) {
setUserStatus({ ok: false, error: e.message });
}
};
const startAuthFlow = async () => {
const res = (await client.makeRequest('POST', 'api/twitch/authorize')) as {
// eslint-disable-next-line camelcase
auth_url: string;
};
const win = window.open(
res.auth_url,
'_blank',
'height=800,width=520,scrollbars=yes,status=yes',
);
// Hack, have to poll because no events are reliable for this
const iv = setInterval(() => {
if (win.closed) {
clearInterval(iv);
setUserStatus(null);
getUserInfo();
}
}, 1000);
};
const sendFakeEvent = async (event: keyof typeof eventSubTestFn) => {
eventSubTestFn[event]((data) => {
kv.putJSON('stulbe/ev/webhook', {
...data,
subscription: {
...data.subscription,
created_at: new Date().toISOString(),
},
});
});
};
// eslint-disable-next-line consistent-return
useEffect(() => {
if (client) {
// Get user info
getUserInfo();
} else if (
stulbeConfig &&
stulbeConfig.enabled &&
stulbeConfig.endpoint &&
stulbeConfig.auth_key &&
stulbeConfig.username
) {
const tryAuth = async () => {
// Try authenticating
const stulbeClient = new Stulbe(stulbeConfig.endpoint);
await stulbeClient.auth(stulbeConfig.username, stulbeConfig.auth_key);
setClient(stulbeClient);
};
tryAuth();
}
}, [stulbeConfig, client]);
if (!stulbeConfig || !stulbeConfig.enabled) {
return <h1>{t('pages.stulbe.err-not-enabled')}</h1>;
}
let userBlock = <i>{t('pages.stulbe.loading-data')}</i>;
if (userStatus !== null) {
if ('id' in userStatus) {
userBlock = (
<>
<TwitchUser>
<p>{t('pages.stulbe.authenticated-as')}</p>
<TwitchPic
src={userStatus.profile_image_url}
alt={t('pages.stulbe.profile-picture')}
/>
<TwitchName>{userStatus.display_name}</TwitchName>
</TwitchUser>
</>
);
} else {
userBlock = t('pages.stulbe.err-no-user');
}
}
return (
<>
<p>{t('pages.stulbe.auth-message')}</p>
<Button onClick={startAuthFlow} disabled={!client}>
<ExternalLinkIcon /> {t('pages.stulbe.auth-button')}
</Button>
<SectionHeader>{t('pages.stulbe.current-status')}</SectionHeader>
{userBlock}
<SectionHeader>{t('pages.stulbe.sim-events')}</SectionHeader>
<ButtonGroup>
{Object.keys(eventSubTestFn).map((ev: keyof typeof eventsubTests) => (
<Button onClick={() => sendFakeEvent(ev)}>
{t(`pages.stulbe.sim.${ev}`, { defaultValue: ev })}
</Button>
))}
</ButtonGroup>
</>
);
}
function BackendConfiguration() {
const [stulbeConfig, setStulbeConfig, loadStatus] = useModule(
modules.stulbeConfig,
);
@ -38,91 +220,109 @@ export default function BackendIntegrationPage(): React.ReactElement {
}
};
return (
<form
onSubmit={(ev) => {
dispatch(setStulbeConfig(stulbeConfig));
ev.preventDefault();
}}
>
<Field size="fullWidth">
<Label htmlFor="endpoint">{t('pages.stulbe.endpoint')}</Label>
<InputBox
type="text"
id="endpoint"
placeholder={t('pages.stulbe.bind-placeholder')}
value={stulbeConfig?.endpoint ?? ''}
disabled={busy}
onChange={(e) =>
dispatch(
apiReducer.actions.stulbeConfigChanged({
...stulbeConfig,
enabled: e.target.value.length > 0,
endpoint: e.target.value,
}),
)
}
/>
</Field>
<Field size="fullWidth">
<Label htmlFor="username">{t('pages.stulbe.username')}</Label>
<InputBox
type="text"
id="username"
value={stulbeConfig?.username ?? ''}
required={true}
disabled={!active || busy}
onChange={(e) =>
dispatch(
apiReducer.actions.stulbeConfigChanged({
...stulbeConfig,
username: e.target.value,
}),
)
}
/>
</Field>
<Field size="fullWidth">
<Label htmlFor="password">{t('pages.stulbe.auth-key')}</Label>
<InputBox
type="password"
id="password"
value={stulbeConfig?.auth_key ?? ''}
disabled={!active || busy}
required={true}
onChange={(e) =>
dispatch(
apiReducer.actions.stulbeConfigChanged({
...stulbeConfig,
auth_key: e.target.value,
}),
)
}
/>
</Field>
<ButtonGroup>
<SaveButton status={status} />
<Button type="button" disabled={!active || busy} onClick={() => test()}>
{t('pages.stulbe.test-button')}
</Button>
</ButtonGroup>
</form>
);
}
export default function BackendIntegrationPage(): React.ReactElement {
const { t } = useTranslation();
return (
<PageContainer>
<PageHeader>
<PageTitle>{t('pages.stulbe.title')}</PageTitle>
<p>
<Trans i18nKey="pages.stulbe.subtitle">
{'Optional back-end integration (using '}
<a href="https://github.com/strimertul/stulbe/">stulbe</a> or any
Kilovolt compatible endpoint) for syncing keys and obtaining webhook
events
{' '}
<a href="https://github.com/strimertul/stulbe/">stulbe</a>
</Trans>
</p>
</PageHeader>
<form
onSubmit={(ev) => {
dispatch(setStulbeConfig(stulbeConfig));
ev.preventDefault();
}}
>
<Field size="fullWidth">
<Label htmlFor="endpoint">{t('pages.stulbe.endpoint')}</Label>
<InputBox
type="text"
id="endpoint"
placeholder={t('pages.stulbe.bind-placeholder')}
value={stulbeConfig?.endpoint ?? ''}
disabled={busy}
onChange={(e) =>
dispatch(
apiReducer.actions.stulbeConfigChanged({
...stulbeConfig,
enabled: e.target.value.length > 0,
endpoint: e.target.value,
}),
)
}
/>
</Field>
<Field size="fullWidth">
<Label htmlFor="username">{t('pages.stulbe.username')}</Label>
<InputBox
type="text"
id="username"
value={stulbeConfig?.username ?? ''}
required={true}
disabled={!active || busy}
onChange={(e) =>
dispatch(
apiReducer.actions.stulbeConfigChanged({
...stulbeConfig,
username: e.target.value,
}),
)
}
/>
</Field>
<Field size="fullWidth">
<Label htmlFor="password">{t('pages.stulbe.auth-key')}</Label>
<InputBox
type="password"
id="password"
value={stulbeConfig?.auth_key ?? ''}
disabled={!active || busy}
required={true}
onChange={(e) =>
dispatch(
apiReducer.actions.stulbeConfigChanged({
...stulbeConfig,
auth_key: e.target.value,
}),
)
}
/>
</Field>
<ButtonGroup>
<SaveButton status={status} />
<Button
type="button"
disabled={!active || busy}
onClick={() => test()}
>
{t('pages.stulbe.test-button')}
</Button>
</ButtonGroup>
</form>
<Tabs.Root defaultValue="configuration">
<TabList>
<TabButton value="configuration">
{t('pages.stulbe.configuration')}
</TabButton>
<TabButton value="webhook">
{t('pages.stulbe.twitch-events')}
</TabButton>
</TabList>
<TabContent value="configuration">
<BackendConfiguration />
</TabContent>
<TabContent value="webhook">
<WebhookIntegration />
</TabContent>
</Tabs.Root>
</PageContainer>
);
}

View file

@ -1,45 +1,11 @@
import {
grassDark,
grayDark,
redDark,
tealDark,
yellowDark,
} from '@radix-ui/colors';
import { createStitches } from '@stitches/react';
import * as UnstyledLabel from '@radix-ui/react-label';
import { styled } from './theme';
export const { styled, theme } = createStitches({
theme: {
colors: {
...grayDark,
...tealDark,
...yellowDark,
...grassDark,
...redDark,
},
},
});
export const PageContainer = styled('div', {
padding: '2rem',
maxWidth: '1000px',
width: '100%',
margin: '0 auto',
});
export const PageHeader = styled('header', {
marginBottom: '3rem',
});
export const PageTitle = styled('h1', {
fontSize: '25pt',
fontWeight: '600',
marginBottom: '0.5rem',
});
export const Field = styled('div', {
export const Field = styled('fieldset', {
all: 'unset',
marginBottom: '2rem',
display: 'flex',
justifyContent: 'flex-start',
variants: {
size: {
fullWidth: {
@ -129,5 +95,3 @@ export const Button = styled('button', {
},
},
});
export default { styled, theme };

View file

@ -0,0 +1,5 @@
export * from './theme';
export * from './brand';
export * from './forms';
export * from './pages';
export * from './tabs';

View file

@ -0,0 +1,30 @@
import { styled } from './theme';
export const PageContainer = styled('div', {
padding: '2rem',
maxWidth: '1000px',
width: '100%',
margin: '0 auto',
});
export const PageHeader = styled('header', {
marginBottom: '3rem',
});
export const PageTitle = styled('h1', {
fontSize: '25pt',
fontWeight: '600',
marginBottom: '0.5rem',
});
export const SectionHeader = styled('h2', {
fontSize: '18pt',
paddingTop: '1rem',
variants: {
spacing: {
none: {
paddingTop: '0',
},
},
},
});

View file

@ -0,0 +1,22 @@
import * as Tabs from '@radix-ui/react-tabs';
import { styled } from './theme';
export const TabList = styled(Tabs.List, {
borderBottom: '1px solid $gray6',
});
export const TabButton = styled(Tabs.Trigger, {
all: 'unset',
padding: '0.6rem 1.2rem',
borderBottom: 'none',
borderRadius: '0.2rem 0.2rem 0 0',
cursor: 'pointer',
'&[data-state="active"]': {
borderBottom: '2px solid $teal9',
},
marginBottom: '-1px',
});
export const TabContent = styled(Tabs.Content, {
paddingTop: '1.5rem',
});

View file

@ -0,0 +1,38 @@
import {
grayDark,
tealDark,
yellowDark,
grassDark,
redDark,
} from '@radix-ui/colors';
import { globalCss, createStitches } from '@stitches/react';
export const globalStyles = globalCss({
body: { margin: 0, padding: 0, backgroundColor: '$gray1', color: '$teal12' },
html: {
margin: 0,
padding: 0,
fontFamily: "'Intel', 'system-ui', sans-serif",
'@supports (font-variation-settings: normal)': {
fontFamily: "'Inter var', 'system-ui', sans-serif",
},
},
a: {
color: '$teal11',
'&:visited': {
color: '$teal11',
},
},
});
export const { styled, theme } = createStitches({
theme: {
colors: {
...grayDark,
...tealDark,
...yellowDark,
...grassDark,
...redDark,
},
},
});