mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +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",
|
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
||||||
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
|
"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": {
|
"@radix-ui/react-compose-refs": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz",
|
"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-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": {
|
"@radix-ui/react-slot": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz",
|
"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-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": {
|
"@radix-ui/react-use-layout-effect": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz",
|
"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"
|
"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": {
|
"@types/react-dom": {
|
||||||
"version": "17.0.4",
|
"version": "17.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.4.tgz",
|
||||||
|
@ -2270,6 +2397,16 @@
|
||||||
"word-wrap": "^1.2.3"
|
"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": {
|
"p-limit": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
||||||
|
@ -2461,6 +2598,11 @@
|
||||||
"object-assign": "^4.1.1"
|
"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": {
|
"react-dom": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
"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/colors": "^0.1.8",
|
||||||
"@radix-ui/react-icons": "^1.0.3",
|
"@radix-ui/react-icons": "^1.0.3",
|
||||||
"@radix-ui/react-label": "^0.1.3",
|
"@radix-ui/react-label": "^0.1.3",
|
||||||
|
"@radix-ui/react-tabs": "^0.1.4",
|
||||||
"@reduxjs/toolkit": "^1.5.1",
|
"@reduxjs/toolkit": "^1.5.1",
|
||||||
"@stitches/react": "^1.2.6",
|
"@stitches/react": "^1.2.6",
|
||||||
"@strimertul/kilovolt-client": "^6.2.0",
|
"@strimertul/kilovolt-client": "^6.2.0",
|
||||||
|
@ -16,6 +17,8 @@
|
||||||
"i18next": "^20.6.1",
|
"i18next": "^20.6.1",
|
||||||
"inter-ui": "^3.19.3",
|
"inter-ui": "^3.19.3",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
"overlayscrollbars": "^1.13.1",
|
||||||
|
"overlayscrollbars-react": "^0.2.3",
|
||||||
"postcss-import": "^14.0.2",
|
"postcss-import": "^14.0.2",
|
||||||
"pretty-ms": "^7.0.1",
|
"pretty-ms": "^7.0.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|
|
@ -6,12 +6,15 @@ import { BrowserRouter } from 'react-router-dom';
|
||||||
import 'inter-ui/inter.css';
|
import 'inter-ui/inter.css';
|
||||||
import 'normalize.css/normalize.css';
|
import 'normalize.css/normalize.css';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import 'overlayscrollbars/css/OverlayScrollbars.css';
|
||||||
|
|
||||||
import './locale/setup';
|
import './locale/setup';
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import App from './ui/App';
|
import App from './ui/App';
|
||||||
|
import { globalStyles } from './ui/theme';
|
||||||
|
|
||||||
|
globalStyles();
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
|
|
@ -25,6 +25,9 @@
|
||||||
"points": "Points and redeems",
|
"points": "Points and redeems",
|
||||||
"rewards": "Rewards and goals"
|
"rewards": "Rewards and goals"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"update-available": "UPDATE AVAILABLE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
|
@ -47,8 +50,28 @@
|
||||||
"endpoint": "Back-end endpoint",
|
"endpoint": "Back-end endpoint",
|
||||||
"auth-key": "Authorization key",
|
"auth-key": "Authorization key",
|
||||||
"username": "User name",
|
"username": "User name",
|
||||||
"subtitle": "Optional back-end integration (using <1>stulbe</1> or any Kilovolt compatible endpoint) for syncing keys and obtaining webhook events",
|
"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"
|
"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": {
|
"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,18 +26,18 @@ import { styled } from './theme';
|
||||||
import spinner from '../assets/icon-loading.svg';
|
import spinner from '../assets/icon-loading.svg';
|
||||||
import BackendIntegrationPage from './pages/BackendIntegration';
|
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() {
|
function Loading() {
|
||||||
const LoadingDiv = styled('div', {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
minHeight: '100vh',
|
|
||||||
});
|
|
||||||
|
|
||||||
const Spinner = styled('img', {
|
|
||||||
maxWidth: '100px',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingDiv>
|
<LoadingDiv>
|
||||||
<Spinner src={spinner} alt="Loading..." />
|
<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 {
|
export default function App(): JSX.Element {
|
||||||
const client = useSelector((state: RootState) => state.api.client);
|
const client = useSelector((state: RootState) => state.api.client);
|
||||||
const connected = useSelector(
|
const connected = useSelector(
|
||||||
|
@ -158,20 +171,16 @@ export default function App(): JSX.Element {
|
||||||
return <AuthDialog />;
|
return <AuthDialog />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled('main', {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
minHeight: '100vh',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Sidebar sections={sections} />
|
<Sidebar sections={sections} />
|
||||||
<Routes>
|
<PageContent>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Routes>
|
||||||
<Route path="/http" element={<ServerSettingsPage />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/backend" element={<BackendIntegrationPage />} />
|
<Route path="/http" element={<ServerSettingsPage />} />
|
||||||
</Routes>
|
<Route path="/backend" element={<BackendIntegrationPage />} />
|
||||||
|
</Routes>
|
||||||
|
</PageContent>
|
||||||
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
|
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,8 +3,9 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
|
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import { RootState } from '../../store';
|
import { RootState } from '../../store';
|
||||||
import { APPNAME, APPREPO } from '../brand';
|
import { APPNAME, APPREPO } from '../theme';
|
||||||
|
|
||||||
export interface RouteSection {
|
export interface RouteSection {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -22,11 +23,8 @@ interface SidebarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled('section', {
|
const Container = styled('section', {
|
||||||
display: 'flex',
|
background: '$gray1',
|
||||||
flexDirection: 'column',
|
|
||||||
minHeight: '100vh',
|
|
||||||
maxWidth: '220px',
|
maxWidth: '220px',
|
||||||
flexShrink: 0,
|
|
||||||
borderRight: '1px solid $gray6',
|
borderRight: '1px solid $gray6',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -77,7 +75,7 @@ const MenuHeader = styled('header', {
|
||||||
color: '$teal9',
|
color: '$teal9',
|
||||||
});
|
});
|
||||||
const MenuLink = styled(Link, {
|
const MenuLink = styled(Link, {
|
||||||
color: '$teal13',
|
color: '$teal13 !important',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
|
@ -88,7 +86,7 @@ const MenuLink = styled(Link, {
|
||||||
variants: {
|
variants: {
|
||||||
status: {
|
status: {
|
||||||
selected: {
|
selected: {
|
||||||
color: '$teal13',
|
color: '$teal13 !important',
|
||||||
backgroundColor: '$teal5',
|
backgroundColor: '$teal5',
|
||||||
},
|
},
|
||||||
clickable: {
|
clickable: {
|
||||||
|
@ -165,24 +163,30 @@ export default function Sidebar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Header>
|
<OverlayScrollbarsComponent
|
||||||
<AppName>{APPNAME}</AppName>
|
style={{ maxHeight: '100vh' }}
|
||||||
|
options={{ scrollbars: { autoHide: 'scroll' } }}
|
||||||
<VersionLabel>
|
>
|
||||||
{version && !dev ? version : t('debug.dev-build')}
|
<Header>
|
||||||
</VersionLabel>
|
<AppName>{APPNAME}</AppName>
|
||||||
{!dev && lastVersion && !version.startsWith(lastVersion.name) && (
|
<VersionLabel>
|
||||||
<UpdateButton href={lastVersion.url}>UPDATE AVAILABLE</UpdateButton>
|
{version && !dev ? version : t('debug.dev-build')}
|
||||||
)}
|
</VersionLabel>
|
||||||
</Header>
|
{!dev && lastVersion && !version.startsWith(lastVersion.name) && (
|
||||||
{sections.map(({ title: sectionTitle, links }) => (
|
<UpdateButton href={lastVersion.url}>
|
||||||
<MenuSection key={sectionTitle}>
|
{t('menu.messages.update-available')}
|
||||||
<MenuHeader>{t(sectionTitle)}</MenuHeader>
|
</UpdateButton>
|
||||||
{links.map((route) => (
|
)}
|
||||||
<SidebarLink route={route} key={`${route.title}-${route.url}`} />
|
</Header>
|
||||||
))}
|
{sections.map(({ title: sectionTitle, links }) => (
|
||||||
</MenuSection>
|
<MenuSection key={sectionTitle}>
|
||||||
))}
|
<MenuHeader>{t(sectionTitle)}</MenuHeader>
|
||||||
|
{links.map((route) => (
|
||||||
|
<SidebarLink route={route} key={`${route.title}-${route.url}`} />
|
||||||
|
))}
|
||||||
|
</MenuSection>
|
||||||
|
))}
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
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 { useModule, useStatus } from '../../lib/react-utils';
|
||||||
import Stulbe from '../../lib/stulbe-lib';
|
import Stulbe from '../../lib/stulbe-lib';
|
||||||
import apiReducer, { modules } from '../../store/api/reducer';
|
import apiReducer, { modules } from '../../store/api/reducer';
|
||||||
|
@ -15,9 +17,189 @@ import {
|
||||||
PageContainer,
|
PageContainer,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
|
SectionHeader,
|
||||||
|
styled,
|
||||||
|
TabButton,
|
||||||
|
TabContent,
|
||||||
|
TabList,
|
||||||
} from '../theme';
|
} 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(
|
const [stulbeConfig, setStulbeConfig, loadStatus] = useModule(
|
||||||
modules.stulbeConfig,
|
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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>{t('pages.stulbe.title')}</PageTitle>
|
<PageTitle>{t('pages.stulbe.title')}</PageTitle>
|
||||||
<p>
|
<p>
|
||||||
<Trans i18nKey="pages.stulbe.subtitle">
|
<Trans i18nKey="pages.stulbe.subtitle">
|
||||||
{'Optional back-end integration (using '}
|
{' '}
|
||||||
<a href="https://github.com/strimertul/stulbe/">stulbe</a> or any
|
<a href="https://github.com/strimertul/stulbe/">stulbe</a>
|
||||||
Kilovolt compatible endpoint) for syncing keys and obtaining webhook
|
|
||||||
events
|
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<form
|
|
||||||
onSubmit={(ev) => {
|
<Tabs.Root defaultValue="configuration">
|
||||||
dispatch(setStulbeConfig(stulbeConfig));
|
<TabList>
|
||||||
ev.preventDefault();
|
<TabButton value="configuration">
|
||||||
}}
|
{t('pages.stulbe.configuration')}
|
||||||
>
|
</TabButton>
|
||||||
<Field size="fullWidth">
|
<TabButton value="webhook">
|
||||||
<Label htmlFor="endpoint">{t('pages.stulbe.endpoint')}</Label>
|
{t('pages.stulbe.twitch-events')}
|
||||||
<InputBox
|
</TabButton>
|
||||||
type="text"
|
</TabList>
|
||||||
id="endpoint"
|
<TabContent value="configuration">
|
||||||
placeholder={t('pages.stulbe.bind-placeholder')}
|
<BackendConfiguration />
|
||||||
value={stulbeConfig?.endpoint ?? ''}
|
</TabContent>
|
||||||
disabled={busy}
|
<TabContent value="webhook">
|
||||||
onChange={(e) =>
|
<WebhookIntegration />
|
||||||
dispatch(
|
</TabContent>
|
||||||
apiReducer.actions.stulbeConfigChanged({
|
</Tabs.Root>
|
||||||
...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>
|
|
||||||
</PageContainer>
|
</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 * as UnstyledLabel from '@radix-ui/react-label';
|
||||||
|
import { styled } from './theme';
|
||||||
|
|
||||||
export const { styled, theme } = createStitches({
|
export const Field = styled('fieldset', {
|
||||||
theme: {
|
all: 'unset',
|
||||||
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', {
|
|
||||||
marginBottom: '2rem',
|
marginBottom: '2rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
fullWidth: {
|
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