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:
parent
2703c39df8
commit
5f59763372
14 changed files with 612 additions and 197 deletions
142
frontend/package-lock.json
generated
142
frontend/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -26,7 +26,6 @@ import { styled } from './theme';
|
|||
import spinner from '../assets/icon-loading.svg';
|
||||
import BackendIntegrationPage from './pages/BackendIntegration';
|
||||
|
||||
function Loading() {
|
||||
const LoadingDiv = styled('div', {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
|
@ -38,6 +37,7 @@ function Loading() {
|
|||
maxWidth: '100px',
|
||||
});
|
||||
|
||||
function Loading() {
|
||||
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} />
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -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,14 +163,19 @@ export default function Sidebar({
|
|||
|
||||
return (
|
||||
<Container>
|
||||
<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}>UPDATE AVAILABLE</UpdateButton>
|
||||
<UpdateButton href={lastVersion.url}>
|
||||
{t('menu.messages.update-available')}
|
||||
</UpdateButton>
|
||||
)}
|
||||
</Header>
|
||||
{sections.map(({ title: sectionTitle, links }) => (
|
||||
|
@ -183,6 +186,7 @@ export default function Sidebar({
|
|||
))}
|
||||
</MenuSection>
|
||||
))}
|
||||
</OverlayScrollbarsComponent>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
@ -39,18 +221,6 @@ export default function BackendIntegrationPage(): React.ReactElement {
|
|||
};
|
||||
|
||||
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
|
||||
</Trans>
|
||||
</p>
|
||||
</PageHeader>
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
dispatch(setStulbeConfig(stulbeConfig));
|
||||
|
@ -114,15 +284,45 @@ export default function BackendIntegrationPage(): React.ReactElement {
|
|||
</Field>
|
||||
<ButtonGroup>
|
||||
<SaveButton status={status} />
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!active || busy}
|
||||
onClick={() => test()}
|
||||
>
|
||||
<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">
|
||||
{' '}
|
||||
<a href="https://github.com/strimertul/stulbe/">stulbe</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 };
|
5
frontend/src/ui/theme/index.ts
Normal file
5
frontend/src/ui/theme/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export * from './theme';
|
||||
export * from './brand';
|
||||
export * from './forms';
|
||||
export * from './pages';
|
||||
export * from './tabs';
|
30
frontend/src/ui/theme/pages.ts
Normal file
30
frontend/src/ui/theme/pages.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
22
frontend/src/ui/theme/tabs.ts
Normal file
22
frontend/src/ui/theme/tabs.ts
Normal 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',
|
||||
});
|
38
frontend/src/ui/theme/theme.ts
Normal file
38
frontend/src/ui/theme/theme.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue