diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4963cc9..f69f39b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 2d1e777..dc1bf63 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index fa7aa4e..6b421c1 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -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( diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index 5ba8366..09ba0a6 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -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 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 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": { diff --git a/frontend/src/style.css b/frontend/src/style.css deleted file mode 100644 index ccf0f7d..0000000 --- a/frontend/src/style.css +++ /dev/null @@ -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); -} diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index 13b65f0..3145bc0 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -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 ( @@ -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 ; } - const Container = styled('main', { - display: 'flex', - flexDirection: 'row', - minHeight: '100vh', - }); - return ( - - } /> - } /> - } /> - + + + } /> + } /> + } /> + + ); diff --git a/frontend/src/ui/components/Sidebar.tsx b/frontend/src/ui/components/Sidebar.tsx index eaf96ac..550dfba 100644 --- a/frontend/src/ui/components/Sidebar.tsx +++ b/frontend/src/ui/components/Sidebar.tsx @@ -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 ( -
- {APPNAME} - - - {version && !dev ? version : t('debug.dev-build')} - - {!dev && lastVersion && !version.startsWith(lastVersion.name) && ( - UPDATE AVAILABLE - )} -
- {sections.map(({ title: sectionTitle, links }) => ( - - {t(sectionTitle)} - {links.map((route) => ( - - ))} - - ))} + +
+ {APPNAME} + + {version && !dev ? version : t('debug.dev-build')} + + {!dev && lastVersion && !version.startsWith(lastVersion.name) && ( + + {t('menu.messages.update-available')} + + )} +
+ {sections.map(({ title: sectionTitle, links }) => ( + + {t(sectionTitle)} + {links.map((route) => ( + + ))} + + ))} +
); } diff --git a/frontend/src/ui/pages/BackendIntegration.tsx b/frontend/src/ui/pages/BackendIntegration.tsx index 26d86c4..8e4f3c6 100644 --- a/frontend/src/ui/pages/BackendIntegration.tsx +++ b/frontend/src/ui/pages/BackendIntegration.tsx @@ -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(null); + const [client, setClient] = useState(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

{t('pages.stulbe.err-not-enabled')}

; + } + + let userBlock = {t('pages.stulbe.loading-data')}; + if (userStatus !== null) { + if ('id' in userStatus) { + userBlock = ( + <> + +

{t('pages.stulbe.authenticated-as')}

+ + {userStatus.display_name} +
+ + ); + } else { + userBlock = t('pages.stulbe.err-no-user'); + } + } + return ( + <> +

{t('pages.stulbe.auth-message')}

+ + {t('pages.stulbe.current-status')} + {userBlock} + {t('pages.stulbe.sim-events')} + + {Object.keys(eventSubTestFn).map((ev: keyof typeof eventsubTests) => ( + + ))} + + + ); +} + +function BackendConfiguration() { const [stulbeConfig, setStulbeConfig, loadStatus] = useModule( modules.stulbeConfig, ); @@ -38,91 +220,109 @@ export default function BackendIntegrationPage(): React.ReactElement { } }; + return ( +
{ + dispatch(setStulbeConfig(stulbeConfig)); + ev.preventDefault(); + }} + > + + + + dispatch( + apiReducer.actions.stulbeConfigChanged({ + ...stulbeConfig, + enabled: e.target.value.length > 0, + endpoint: e.target.value, + }), + ) + } + /> + + + + + dispatch( + apiReducer.actions.stulbeConfigChanged({ + ...stulbeConfig, + username: e.target.value, + }), + ) + } + /> + + + + + dispatch( + apiReducer.actions.stulbeConfigChanged({ + ...stulbeConfig, + auth_key: e.target.value, + }), + ) + } + /> + + + + + +
+ ); +} + +export default function BackendIntegrationPage(): React.ReactElement { + const { t } = useTranslation(); + return ( {t('pages.stulbe.title')}

- {'Optional back-end integration (using '} - stulbe or any - Kilovolt compatible endpoint) for syncing keys and obtaining webhook - events + {' '} + stulbe

-
{ - dispatch(setStulbeConfig(stulbeConfig)); - ev.preventDefault(); - }} - > - - - - dispatch( - apiReducer.actions.stulbeConfigChanged({ - ...stulbeConfig, - enabled: e.target.value.length > 0, - endpoint: e.target.value, - }), - ) - } - /> - - - - - dispatch( - apiReducer.actions.stulbeConfigChanged({ - ...stulbeConfig, - username: e.target.value, - }), - ) - } - /> - - - - - dispatch( - apiReducer.actions.stulbeConfigChanged({ - ...stulbeConfig, - auth_key: e.target.value, - }), - ) - } - /> - - - - - -
+ + + + + {t('pages.stulbe.configuration')} + + + {t('pages.stulbe.twitch-events')} + + + + + + + + +
); } diff --git a/frontend/src/ui/brand.ts b/frontend/src/ui/theme/brand.ts similarity index 100% rename from frontend/src/ui/brand.ts rename to frontend/src/ui/theme/brand.ts diff --git a/frontend/src/ui/theme.ts b/frontend/src/ui/theme/forms.ts similarity index 71% rename from frontend/src/ui/theme.ts rename to frontend/src/ui/theme/forms.ts index aa1df63..7145822 100644 --- a/frontend/src/ui/theme.ts +++ b/frontend/src/ui/theme/forms.ts @@ -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 }; diff --git a/frontend/src/ui/theme/index.ts b/frontend/src/ui/theme/index.ts new file mode 100644 index 0000000..7496d13 --- /dev/null +++ b/frontend/src/ui/theme/index.ts @@ -0,0 +1,5 @@ +export * from './theme'; +export * from './brand'; +export * from './forms'; +export * from './pages'; +export * from './tabs'; diff --git a/frontend/src/ui/theme/pages.ts b/frontend/src/ui/theme/pages.ts new file mode 100644 index 0000000..5b25ef6 --- /dev/null +++ b/frontend/src/ui/theme/pages.ts @@ -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', + }, + }, + }, +}); diff --git a/frontend/src/ui/theme/tabs.ts b/frontend/src/ui/theme/tabs.ts new file mode 100644 index 0000000..5ce4eea --- /dev/null +++ b/frontend/src/ui/theme/tabs.ts @@ -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', +}); diff --git a/frontend/src/ui/theme/theme.ts b/frontend/src/ui/theme/theme.ts new file mode 100644 index 0000000..27068bf --- /dev/null +++ b/frontend/src/ui/theme/theme.ts @@ -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, + }, + }, +});