mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
Auth dialog, chat timers, some reusable components
This commit is contained in:
parent
64366a7903
commit
0b7f7fe069
14 changed files with 843 additions and 86 deletions
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
|
@ -2708,11 +2708,6 @@
|
||||||
"error-ex": "^1.2.0"
|
"error-ex": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"parse-ms": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
|
|
||||||
},
|
|
||||||
"path-exists": {
|
"path-exists": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||||
|
@ -2812,14 +2807,6 @@
|
||||||
"fast-diff": "^1.1.2"
|
"fast-diff": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pretty-ms": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
|
|
||||||
"requires": {
|
|
||||||
"parse-ms": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"progress": {
|
"progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
"overlayscrollbars": "^1.13.1",
|
"overlayscrollbars": "^1.13.1",
|
||||||
"overlayscrollbars-react": "^0.2.3",
|
"overlayscrollbars-react": "^0.2.3",
|
||||||
"postcss-import": "^14.0.2",
|
"postcss-import": "^14.0.2",
|
||||||
"pretty-ms": "^7.0.1",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^11.12.0",
|
"react-i18next": "^11.12.0",
|
||||||
|
|
|
@ -125,6 +125,31 @@
|
||||||
"streamer": "Streamer only"
|
"streamer": "Streamer only"
|
||||||
},
|
},
|
||||||
"remove-command-title": "Remove command {{name}}?"
|
"remove-command-title": "Remove command {{name}}?"
|
||||||
|
},
|
||||||
|
"bottimers": {
|
||||||
|
"title": "Bot timers",
|
||||||
|
"desc": "Define reminders such as checking out your social media or ongoing events",
|
||||||
|
"add-button": "New timer",
|
||||||
|
"search-placeholder": "Search timer by name",
|
||||||
|
"timer-header-new": "New timer",
|
||||||
|
"timer-header-edit": "Edit timer",
|
||||||
|
"timer-name": "Timer name",
|
||||||
|
"timer-name-placeholder": "my-timer",
|
||||||
|
"timer-action-new": "Create",
|
||||||
|
"timer-action-edit": "Edit",
|
||||||
|
"remove-timer-title": "Remove timer {{name}}?",
|
||||||
|
"timer-parameters": "every {{time}}, ≥ {{messages}} messages in the last {{interval}}",
|
||||||
|
"timer-interval": "Minimul interval",
|
||||||
|
"timer-activity": "Minimul chat activity (0 to disable)",
|
||||||
|
"timer-activity-desc": "messages in the last 5 minutes",
|
||||||
|
"timer-messages": "Messages"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"title": "Authentication required",
|
||||||
|
"desc": "The installation of strimertül you are trying to reach is protected with a password. Please write the password below to access the control panel.",
|
||||||
|
"no-pwd-note": " If the database has no password (for example, it was recently changed from having one to none), leave the field empty.",
|
||||||
|
"password": "Password",
|
||||||
|
"submit": "Authenticate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form-actions": {
|
"form-actions": {
|
||||||
|
@ -137,9 +162,18 @@
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"ok": "OK"
|
"ok": "OK",
|
||||||
|
"add": "Add"
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"dev-build": "Development build"
|
"dev-build": "Development build"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"x-hours": "{{time}} hrs",
|
||||||
|
"x-minutes": "{{time}} min",
|
||||||
|
"x-seconds": "{{time}} sec",
|
||||||
|
"hours": "hours",
|
||||||
|
"minutes": "minutes",
|
||||||
|
"seconds": "seconds"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
ChatBubbleIcon,
|
ChatBubbleIcon,
|
||||||
|
@ -20,13 +20,27 @@ import ServerSettingsPage from './pages/ServerSettings';
|
||||||
import { RootState } from '../store';
|
import { RootState } from '../store';
|
||||||
import { createWSClient } from '../store/api/reducer';
|
import { createWSClient } from '../store/api/reducer';
|
||||||
import { ConnectionStatus } from '../store/api/types';
|
import { ConnectionStatus } from '../store/api/types';
|
||||||
import { styled } from './theme';
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContainer,
|
||||||
|
FlexRow,
|
||||||
|
InputBox,
|
||||||
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
styled,
|
||||||
|
TextBlock,
|
||||||
|
} from './theme';
|
||||||
|
|
||||||
// @ts-expect-error Asset import
|
// @ts-expect-error Asset import
|
||||||
import spinner from '../assets/icon-loading.svg';
|
import spinner from '../assets/icon-loading.svg';
|
||||||
import BackendIntegrationPage from './pages/BackendIntegration';
|
import BackendIntegrationPage from './pages/BackendIntegration';
|
||||||
import TwitchSettingsPage from './pages/TwitchSettings';
|
import TwitchSettingsPage from './pages/TwitchSettings';
|
||||||
import TwitchBotCommandsPage from './pages/BotCommands';
|
import TwitchBotCommandsPage from './pages/BotCommands';
|
||||||
|
import TwitchBotTimersPage from './pages/BotTimers';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import DialogContent from './components/DialogContent';
|
||||||
|
import AuthDialog from './pages/AuthDialog';
|
||||||
|
|
||||||
const LoadingDiv = styled('div', {
|
const LoadingDiv = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -46,18 +60,6 @@ function Loading() {
|
||||||
</LoadingDiv>
|
</LoadingDiv>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthDialog() {
|
|
||||||
const AuthWrapper = styled('div', {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
minHeight: '100vh',
|
|
||||||
});
|
|
||||||
|
|
||||||
return <AuthWrapper></AuthWrapper>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sections: RouteSection[] = [
|
const sections: RouteSection[] = [
|
||||||
{
|
{
|
||||||
title: 'menu.sections.monitor',
|
title: 'menu.sections.monitor',
|
||||||
|
@ -194,6 +196,10 @@ export default function App(): JSX.Element {
|
||||||
path="/twitch/bot/commands"
|
path="/twitch/bot/commands"
|
||||||
element={<TwitchBotCommandsPage />}
|
element={<TwitchBotCommandsPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/twitch/bot/timers"
|
||||||
|
element={<TwitchBotTimersPage />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
88
frontend/src/ui/components/Interval.tsx
Normal file
88
frontend/src/ui/components/Interval.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { getInterval } from '../../lib/time-utils';
|
||||||
|
import { ComboBox, FlexRow, InputBox } from '../theme';
|
||||||
|
|
||||||
|
export interface TimeUnit {
|
||||||
|
multiplier: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const seconds = { multiplier: 1, unit: 'time.seconds' };
|
||||||
|
export const minutes = { multiplier: 60, unit: 'time.minutes' };
|
||||||
|
export const hours = { multiplier: 3600, unit: 'time.hours' };
|
||||||
|
|
||||||
|
export interface IntervalProps {
|
||||||
|
active: boolean;
|
||||||
|
value: number;
|
||||||
|
id?: string;
|
||||||
|
min?: number;
|
||||||
|
units?: TimeUnit[];
|
||||||
|
onChange?: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Interval({ id, active, value, min, units, onChange }: IntervalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const timeUnits = units ?? [seconds, minutes, hours];
|
||||||
|
|
||||||
|
const [numInitialValue, multInitialValue] = getInterval(value);
|
||||||
|
const [num, setNum] = useState(numInitialValue);
|
||||||
|
const [mult, setMult] = useState(multInitialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const total = num * mult;
|
||||||
|
if (min && total < min) {
|
||||||
|
const [minNum, minMult] = getInterval(min);
|
||||||
|
setNum(minNum);
|
||||||
|
setMult(minMult);
|
||||||
|
}
|
||||||
|
onChange(Math.max(min ?? 0, total));
|
||||||
|
}, [num, mult]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FlexRow align="left" border="form">
|
||||||
|
<InputBox
|
||||||
|
id={id}
|
||||||
|
type="number"
|
||||||
|
border="none"
|
||||||
|
css={{
|
||||||
|
maxWidth: '5rem',
|
||||||
|
borderRightWidth: '1px',
|
||||||
|
borderRadius: '$borderRadius$form 0 0 $borderRadius$form',
|
||||||
|
}}
|
||||||
|
value={num ?? ''}
|
||||||
|
onChange={(ev) => {
|
||||||
|
const intNum = parseInt(ev.target.value, 10);
|
||||||
|
if (Number.isNaN(intNum)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNum(intNum);
|
||||||
|
}}
|
||||||
|
placeholder="#"
|
||||||
|
/>
|
||||||
|
<ComboBox
|
||||||
|
border="none"
|
||||||
|
value={mult.toString() ?? ''}
|
||||||
|
disabled={!active}
|
||||||
|
onChange={(ev) => {
|
||||||
|
const intMult = parseInt(ev.target.value, 10);
|
||||||
|
if (Number.isNaN(intMult)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMult(intMult);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timeUnits.map((unit) => (
|
||||||
|
<option key={unit.unit} value={unit.multiplier.toString()}>
|
||||||
|
{t(unit.unit)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</ComboBox>
|
||||||
|
</FlexRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Interval);
|
78
frontend/src/ui/components/MultiInput.tsx
Normal file
78
frontend/src/ui/components/MultiInput.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, FlexRow, InputBox, Textarea } from '../theme';
|
||||||
|
|
||||||
|
export interface MessageArrayProps {
|
||||||
|
placeholder?: string;
|
||||||
|
value: string[];
|
||||||
|
onChange: (value: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessageArray({ value, placeholder, onChange }: MessageArrayProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{value.map((message, index) => (
|
||||||
|
<FlexRow key={message + index} css={{ marginTop: '0.5rem', flex: 1 }}>
|
||||||
|
<FlexRow border="form" css={{ flex: 1, alignItems: 'stretch' }}>
|
||||||
|
<Textarea
|
||||||
|
border="none"
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(ev) => {
|
||||||
|
const newMessages = [...value];
|
||||||
|
newMessages[index] = ev.target.value;
|
||||||
|
onChange(newMessages);
|
||||||
|
}}
|
||||||
|
className={message !== '' ? 'input' : 'input is-danger'}
|
||||||
|
css={
|
||||||
|
value.length > 1
|
||||||
|
? {
|
||||||
|
borderRadius: '$borderRadius$form 0 0 $borderRadius$form',
|
||||||
|
flex: 1,
|
||||||
|
}
|
||||||
|
: { flex: 1 }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</Textarea>
|
||||||
|
{value.length > 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variation="danger"
|
||||||
|
styling="form"
|
||||||
|
onClick={() => {
|
||||||
|
const newMessages = [...value];
|
||||||
|
newMessages.splice(index, 1);
|
||||||
|
onChange(newMessages.length > 0 ? newMessages : ['']);
|
||||||
|
}}
|
||||||
|
css={{
|
||||||
|
margin: '-1px',
|
||||||
|
borderRadius: '0 $borderRadius$form $borderRadius$form 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Cross2Icon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FlexRow>
|
||||||
|
</FlexRow>
|
||||||
|
))}
|
||||||
|
<FlexRow align="left">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
styling="link"
|
||||||
|
type="button"
|
||||||
|
css={{ marginTop: '0.5rem' }}
|
||||||
|
onClick={() => {
|
||||||
|
onChange([...value, '']);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('form-actions.add')}
|
||||||
|
</Button>
|
||||||
|
</FlexRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(MessageArray);
|
|
@ -38,10 +38,12 @@ const Header = styled('div', {
|
||||||
const AppName = styled('h1', {
|
const AppName = styled('h1', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
gap: '0.2rem',
|
gap: '0.2rem',
|
||||||
fontSize: '1.4rem',
|
fontSize: '1.4rem',
|
||||||
margin: '0.5rem 0 0.5rem 0',
|
margin: '0.5rem 0 0.5rem 0',
|
||||||
fontWeight: 300,
|
fontWeight: 300,
|
||||||
|
paddingRight: '0.5rem',
|
||||||
});
|
});
|
||||||
|
|
||||||
const VersionLabel = styled('div', {
|
const VersionLabel = styled('div', {
|
||||||
|
@ -49,7 +51,7 @@ const VersionLabel = styled('div', {
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '$teal8',
|
color: '$teal8',
|
||||||
paddingLeft: '12px',
|
textAlign: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
const UpdateButton = styled('a', {
|
const UpdateButton = styled('a', {
|
||||||
|
|
80
frontend/src/ui/pages/AuthDialog.tsx
Normal file
80
frontend/src/ui/pages/AuthDialog.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { styled } from '@stitches/react';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, InputBox, TextBlock } from '../theme';
|
||||||
|
|
||||||
|
const AuthWrapper = styled('div', {
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
maxWidth: '600px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '90vw',
|
||||||
|
margin: '0 auto',
|
||||||
|
});
|
||||||
|
const AuthTitle = styled('div', {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '$teal12',
|
||||||
|
fontSize: '15pt',
|
||||||
|
borderBottom: '1px solid $teal6',
|
||||||
|
padding: '1rem 1.5rem',
|
||||||
|
margin: '0',
|
||||||
|
lineHeight: '1.25',
|
||||||
|
});
|
||||||
|
const AuthDialogContainer = styled('form', {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: '0',
|
||||||
|
backgroundColor: '$gray2',
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
boxShadow:
|
||||||
|
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||||
|
});
|
||||||
|
const Content = styled('div', {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: '1.5rem',
|
||||||
|
gap: '1rem',
|
||||||
|
});
|
||||||
|
const Actions = styled('div', {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
borderTop: '1px solid $gray6',
|
||||||
|
padding: '1rem 1.5rem',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function AuthDialog(): React.ReactElement {
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthWrapper>
|
||||||
|
<AuthDialogContainer
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
localStorage.setItem('password', password);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AuthTitle>{t('pages.auth.title')}</AuthTitle>
|
||||||
|
<Content>
|
||||||
|
<TextBlock spacing="none">{t('pages.auth.desc')}</TextBlock>
|
||||||
|
<TextBlock spacing="none">{t('pages.auth.no-pwd-note')}</TextBlock>
|
||||||
|
</Content>
|
||||||
|
<Actions>
|
||||||
|
<InputBox
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
css={{ flex: 1 }}
|
||||||
|
placeholder={t('pages.auth.password')}
|
||||||
|
/>
|
||||||
|
<Button variation="primary" type="submit">
|
||||||
|
{t('pages.auth.submit')}
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</AuthDialogContainer>
|
||||||
|
</AuthWrapper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import {
|
||||||
FlexRow,
|
FlexRow,
|
||||||
InputBox,
|
InputBox,
|
||||||
Label,
|
Label,
|
||||||
|
MultiButton,
|
||||||
PageContainer,
|
PageContainer,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
|
@ -103,7 +104,7 @@ interface CommandItemProps {
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandItemEl({
|
function CommandItem({
|
||||||
name,
|
name,
|
||||||
item,
|
item,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
@ -126,36 +127,38 @@ function CommandItemEl({
|
||||||
{item.access_level !== 'streamer' && '+'}
|
{item.access_level !== 'streamer' && '+'}
|
||||||
</ACLIndicator>
|
</ACLIndicator>
|
||||||
)}
|
)}
|
||||||
<Button
|
<MultiButton>
|
||||||
styling="link"
|
<Button
|
||||||
size="small"
|
styling="multi"
|
||||||
onClick={() => (onToggle ? onToggle() : null)}
|
size="small"
|
||||||
>
|
onClick={() => (onToggle ? onToggle() : null)}
|
||||||
{t(item.enabled ? 'form-actions.disable' : 'form-actions.enable')}
|
>
|
||||||
</Button>
|
{t(item.enabled ? 'form-actions.disable' : 'form-actions.enable')}
|
||||||
<Button
|
</Button>
|
||||||
styling="link"
|
<Button
|
||||||
size="small"
|
styling="multi"
|
||||||
onClick={() => (onEdit ? onEdit() : null)}
|
size="small"
|
||||||
>
|
onClick={() => (onEdit ? onEdit() : null)}
|
||||||
{t('form-actions.edit')}
|
>
|
||||||
</Button>
|
{t('form-actions.edit')}
|
||||||
<Alert>
|
</Button>
|
||||||
<AlertTrigger asChild>
|
<Alert>
|
||||||
<Button styling="link" size="small">
|
<AlertTrigger asChild>
|
||||||
{t('form-actions.delete')}
|
<Button styling="multi" size="small">
|
||||||
</Button>
|
{t('form-actions.delete')}
|
||||||
</AlertTrigger>
|
</Button>
|
||||||
<AlertContent
|
</AlertTrigger>
|
||||||
variation="danger"
|
<AlertContent
|
||||||
title={t('pages.botcommands.remove-command-title', { name })}
|
variation="danger"
|
||||||
description="This cannot be undone"
|
title={t('pages.botcommands.remove-command-title', { name })}
|
||||||
actionText="Delete"
|
description="This cannot be undone"
|
||||||
actionButtonProps={{ variation: 'danger' }}
|
actionText="Delete"
|
||||||
showCancel={true}
|
actionButtonProps={{ variation: 'danger' }}
|
||||||
onAction={() => (onDelete ? onDelete() : null)}
|
showCancel={true}
|
||||||
/>
|
onAction={() => (onDelete ? onDelete() : null)}
|
||||||
</Alert>
|
/>
|
||||||
|
</Alert>
|
||||||
|
</MultiButton>
|
||||||
</CommandActions>
|
</CommandActions>
|
||||||
</CommandHeader>
|
</CommandHeader>
|
||||||
<CommandText>{item.response}</CommandText>
|
<CommandText>{item.response}</CommandText>
|
||||||
|
@ -163,8 +166,6 @@ function CommandItemEl({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandItem = React.memo(CommandItemEl);
|
|
||||||
|
|
||||||
type DialogPrompt =
|
type DialogPrompt =
|
||||||
| { kind: 'new' }
|
| { kind: 'new' }
|
||||||
| { kind: 'edit'; name: string; item: TwitchBotCustomCommand };
|
| { kind: 'edit'; name: string; item: TwitchBotCustomCommand };
|
||||||
|
@ -269,20 +270,20 @@ function CommandDialog({
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TwitchBotCommandsPage(): React.ReactElement {
|
export default function TwitchBotCommandsPage(): React.ReactElement {
|
||||||
const [botCommands, setBotCommands] = useModule(modules.twitchBotCommands);
|
const [commands, setCommands] = useModule(modules.twitchBotCommands);
|
||||||
const [commandFilter, setCommandFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const commandFilterLC = commandFilter.toLowerCase();
|
const filterLC = filter.toLowerCase();
|
||||||
|
|
||||||
const setCommand = (newName: string, data: TwitchBotCustomCommand): void => {
|
const setCommand = (newName: string, data: TwitchBotCustomCommand): void => {
|
||||||
switch (activeDialog.kind) {
|
switch (activeDialog.kind) {
|
||||||
case 'new':
|
case 'new':
|
||||||
dispatch(
|
dispatch(
|
||||||
setBotCommands({
|
setCommands({
|
||||||
...botCommands,
|
...commands,
|
||||||
[newName]: {
|
[newName]: {
|
||||||
...data,
|
...data,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -293,8 +294,8 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
|
||||||
case 'edit': {
|
case 'edit': {
|
||||||
const oldName = activeDialog.name;
|
const oldName = activeDialog.name;
|
||||||
dispatch(
|
dispatch(
|
||||||
setBotCommands({
|
setCommands({
|
||||||
...botCommands,
|
...commands,
|
||||||
[oldName]: undefined,
|
[oldName]: undefined,
|
||||||
[newName]: data,
|
[newName]: data,
|
||||||
}),
|
}),
|
||||||
|
@ -307,8 +308,8 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
|
||||||
|
|
||||||
const deleteCommand = (cmd: string): void => {
|
const deleteCommand = (cmd: string): void => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setBotCommands({
|
setCommands({
|
||||||
...botCommands,
|
...commands,
|
||||||
[cmd]: undefined,
|
[cmd]: undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -316,11 +317,11 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
|
||||||
|
|
||||||
const toggleCommand = (cmd: string): void => {
|
const toggleCommand = (cmd: string): void => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setBotCommands({
|
setCommands({
|
||||||
...botCommands,
|
...commands,
|
||||||
[cmd]: {
|
[cmd]: {
|
||||||
...botCommands[cmd],
|
...commands[cmd],
|
||||||
enabled: !botCommands[cmd].enabled,
|
enabled: !commands[cmd].enabled,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -344,25 +345,25 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
|
||||||
<InputBox
|
<InputBox
|
||||||
css={{ flex: 1 }}
|
css={{ flex: 1 }}
|
||||||
placeholder={t('pages.botcommands.search-placeholder')}
|
placeholder={t('pages.botcommands.search-placeholder')}
|
||||||
value={commandFilter}
|
value={filter}
|
||||||
onChange={(e) => setCommandFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
{Object.keys(botCommands ?? {})
|
{Object.keys(commands ?? {})
|
||||||
?.filter((cmd) => cmd.toLowerCase().includes(commandFilterLC))
|
?.filter((cmd) => cmd.toLowerCase().includes(filterLC))
|
||||||
.sort()
|
.sort()
|
||||||
.map((cmd) => (
|
.map((cmd) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={cmd}
|
key={cmd}
|
||||||
name={cmd}
|
name={cmd}
|
||||||
item={botCommands[cmd]}
|
item={commands[cmd]}
|
||||||
onToggle={() => toggleCommand(cmd)}
|
onToggle={() => toggleCommand(cmd)}
|
||||||
onEdit={() =>
|
onEdit={() =>
|
||||||
setActiveDialog({
|
setActiveDialog({
|
||||||
kind: 'edit',
|
kind: 'edit',
|
||||||
name: cmd,
|
name: cmd,
|
||||||
item: botCommands[cmd],
|
item: commands[cmd],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onDelete={() => deleteCommand(cmd)}
|
onDelete={() => deleteCommand(cmd)}
|
||||||
|
|
417
frontend/src/ui/pages/BotTimers.tsx
Normal file
417
frontend/src/ui/pages/BotTimers.tsx
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
import { PlusIcon } from '@radix-ui/react-icons';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { TFunction, useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useModule } from '../../lib/react-utils';
|
||||||
|
import { modules } from '../../store/api/reducer';
|
||||||
|
import { TwitchBotTimer } from '../../store/api/types';
|
||||||
|
import AlertContent from '../components/AlertContent';
|
||||||
|
import DialogContent from '../components/DialogContent';
|
||||||
|
import Interval, { hours, minutes } from '../components/Interval';
|
||||||
|
import MultiInput from '../components/MultiInput';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogClose,
|
||||||
|
Field,
|
||||||
|
FlexRow,
|
||||||
|
InputBox,
|
||||||
|
Label,
|
||||||
|
MultiButton,
|
||||||
|
PageContainer,
|
||||||
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
styled,
|
||||||
|
TextBlock,
|
||||||
|
} from '../theme';
|
||||||
|
import { Alert, AlertTrigger } from '../theme/alert';
|
||||||
|
|
||||||
|
const TimerList = styled('div', { marginTop: '1rem' });
|
||||||
|
const TimerItemContainer = styled('article', {
|
||||||
|
backgroundColor: '$gray2',
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderLeft: '5px solid $teal8',
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
borderBottom: '1px solid $gray4',
|
||||||
|
transition: 'all 50ms',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '$gray3',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
status: {
|
||||||
|
enabled: {},
|
||||||
|
disabled: {
|
||||||
|
borderLeftColor: '$red7',
|
||||||
|
backgroundColor: '$gray3',
|
||||||
|
color: '$gray10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const TimerHeader = styled('header', {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
});
|
||||||
|
const TimerName = styled('span', {
|
||||||
|
color: '$teal10',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
variants: {
|
||||||
|
status: {
|
||||||
|
enabled: {},
|
||||||
|
disabled: {
|
||||||
|
color: '$gray10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const TimerDescription = styled('span', {
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
const TimerActions = styled('div', {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.25rem',
|
||||||
|
});
|
||||||
|
const TimerText = styled('div', {
|
||||||
|
fontFamily: 'Space Mono',
|
||||||
|
fontSize: '10pt',
|
||||||
|
margin: '0 -0.5rem',
|
||||||
|
marginTop: '0',
|
||||||
|
marginBottom: '0.3rem',
|
||||||
|
padding: '0.5rem',
|
||||||
|
backgroundColor: '$gray4',
|
||||||
|
lineHeight: '1.2rem',
|
||||||
|
'&:last-child': {
|
||||||
|
marginBottom: '-0.5rem',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function humanTime(t: TFunction<'translation'>, secs: number): string {
|
||||||
|
const mins = Math.floor(secs / 60);
|
||||||
|
const hrs = Math.floor(mins / 60);
|
||||||
|
|
||||||
|
if (hrs > 0) {
|
||||||
|
return t('time.x-hours', { time: hrs });
|
||||||
|
}
|
||||||
|
if (mins > 0) {
|
||||||
|
return t('time.x-minutes', { time: mins });
|
||||||
|
}
|
||||||
|
return t('time.x-seconds', { time: secs });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimerItemProps {
|
||||||
|
name: string;
|
||||||
|
item: TwitchBotTimer;
|
||||||
|
onToggle?: () => void;
|
||||||
|
onEdit?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimerItem({
|
||||||
|
name,
|
||||||
|
item,
|
||||||
|
onToggle,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}: TimerItemProps): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimerItemContainer status={item.enabled ? 'enabled' : 'disabled'}>
|
||||||
|
<TimerHeader>
|
||||||
|
<TimerName status={item.enabled ? 'enabled' : 'disabled'}>
|
||||||
|
{name}
|
||||||
|
</TimerName>
|
||||||
|
<TimerDescription>
|
||||||
|
(
|
||||||
|
{t('pages.bottimers.timer-parameters', {
|
||||||
|
time: humanTime(t, item.minimum_delay),
|
||||||
|
messages: item.minimum_chat_activity,
|
||||||
|
interval: humanTime(t, 300),
|
||||||
|
})}
|
||||||
|
)
|
||||||
|
</TimerDescription>
|
||||||
|
<TimerActions>
|
||||||
|
<MultiButton>
|
||||||
|
<Button
|
||||||
|
styling="multi"
|
||||||
|
size="small"
|
||||||
|
onClick={() => (onToggle ? onToggle() : null)}
|
||||||
|
>
|
||||||
|
{t(item.enabled ? 'form-actions.disable' : 'form-actions.enable')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
styling="multi"
|
||||||
|
size="small"
|
||||||
|
onClick={() => (onEdit ? onEdit() : null)}
|
||||||
|
>
|
||||||
|
{t('form-actions.edit')}
|
||||||
|
</Button>
|
||||||
|
<Alert>
|
||||||
|
<AlertTrigger asChild>
|
||||||
|
<Button styling="multi" size="small">
|
||||||
|
{t('form-actions.delete')}
|
||||||
|
</Button>
|
||||||
|
</AlertTrigger>
|
||||||
|
<AlertContent
|
||||||
|
variation="danger"
|
||||||
|
title={t('pages.bottimers.remove-timer-title', { name })}
|
||||||
|
description="This cannot be undone"
|
||||||
|
actionText="Delete"
|
||||||
|
actionButtonProps={{ variation: 'danger' }}
|
||||||
|
showCancel={true}
|
||||||
|
onAction={() => (onDelete ? onDelete() : null)}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
</MultiButton>
|
||||||
|
</TimerActions>
|
||||||
|
</TimerHeader>
|
||||||
|
{item.messages?.map((message, index) => (
|
||||||
|
<TimerText key={index}>{message}</TimerText>
|
||||||
|
))}
|
||||||
|
</TimerItemContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialogPrompt =
|
||||||
|
| { kind: 'new' }
|
||||||
|
| { kind: 'edit'; name: string; item: TwitchBotTimer };
|
||||||
|
|
||||||
|
function TimerDialog({
|
||||||
|
kind,
|
||||||
|
name,
|
||||||
|
item,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
kind: 'new' | 'edit';
|
||||||
|
name?: string;
|
||||||
|
item?: TwitchBotTimer;
|
||||||
|
onSubmit?: (name: string, item: TwitchBotTimer) => void;
|
||||||
|
}) {
|
||||||
|
const [timerName, setName] = useState(name ?? '');
|
||||||
|
const [messages, setMessages] = useState(item?.messages ?? ['']);
|
||||||
|
const [minDelay, setMinDelay] = useState(item?.minimum_delay ?? 300);
|
||||||
|
const [minActivity, setMinActivity] = useState(
|
||||||
|
item?.minimum_chat_activity ?? 5,
|
||||||
|
);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent title={t(`pages.bottimers.timer-header-${kind}`)}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onSubmit) {
|
||||||
|
onSubmit(timerName, {
|
||||||
|
...item,
|
||||||
|
messages,
|
||||||
|
minimum_delay: minDelay,
|
||||||
|
minimum_chat_activity: minActivity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field spacing="narrow" size="fullWidth">
|
||||||
|
<Label htmlFor="timer-name">{t('pages.bottimers.timer-name')}</Label>
|
||||||
|
<InputBox
|
||||||
|
id="timer-name"
|
||||||
|
value={timerName}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder={t('pages.bottimers.timer-name-placeholder')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field spacing="narrow" size="fullWidth">
|
||||||
|
<Label htmlFor="timer-interval">
|
||||||
|
{t('pages.bottimers.timer-interval')}
|
||||||
|
</Label>
|
||||||
|
<FlexRow align="left">
|
||||||
|
<Interval
|
||||||
|
id="timer-interval"
|
||||||
|
value={minDelay}
|
||||||
|
onChange={setMinDelay}
|
||||||
|
active={true}
|
||||||
|
min={60}
|
||||||
|
units={[minutes, hours]}
|
||||||
|
/>
|
||||||
|
</FlexRow>
|
||||||
|
</Field>
|
||||||
|
<Field spacing="narrow" size="fullWidth">
|
||||||
|
<Label htmlFor="timer-activity">
|
||||||
|
{t('pages.bottimers.timer-activity')}
|
||||||
|
</Label>
|
||||||
|
<FlexRow align="left" spacing={1}>
|
||||||
|
<InputBox
|
||||||
|
id="timer-activity"
|
||||||
|
value={minActivity}
|
||||||
|
type="number"
|
||||||
|
css={{
|
||||||
|
width: '5rem',
|
||||||
|
}}
|
||||||
|
onChange={(ev) => {
|
||||||
|
const intNum = parseInt(ev.target.value, 10);
|
||||||
|
if (Number.isNaN(intNum)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMinActivity(intNum);
|
||||||
|
}}
|
||||||
|
placeholder="#"
|
||||||
|
/>
|
||||||
|
<span>{t('pages.bottimers.timer-activity-desc')}</span>
|
||||||
|
</FlexRow>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field spacing="narrow" size="fullWidth">
|
||||||
|
<Label>{t('pages.bottimers.timer-messages')}</Label>
|
||||||
|
<MultiInput value={messages} onChange={setMessages} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button variation="primary">
|
||||||
|
{t(`pages.bottimers.timer-action-${kind}`)}
|
||||||
|
</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button">{t('form-actions.cancel')}</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TwitchBotTimersPage(): React.ReactElement {
|
||||||
|
const [timerConfig, setTimerConfig] = useModule(modules.twitchBotTimers);
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const filterLC = filter.toLowerCase();
|
||||||
|
|
||||||
|
const setTimer = (newName: string, data: TwitchBotTimer): void => {
|
||||||
|
switch (activeDialog.kind) {
|
||||||
|
case 'new':
|
||||||
|
dispatch(
|
||||||
|
setTimerConfig({
|
||||||
|
...timerConfig,
|
||||||
|
timers: {
|
||||||
|
...timerConfig.timers,
|
||||||
|
[newName]: {
|
||||||
|
...data,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'edit': {
|
||||||
|
const oldName = activeDialog.name;
|
||||||
|
dispatch(
|
||||||
|
setTimerConfig({
|
||||||
|
...timerConfig,
|
||||||
|
timers: {
|
||||||
|
...timerConfig.timers,
|
||||||
|
[oldName]: undefined,
|
||||||
|
[newName]: data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveDialog(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTimer = (cmd: string): void => {
|
||||||
|
dispatch(
|
||||||
|
setTimerConfig({
|
||||||
|
...timerConfig,
|
||||||
|
timers: {
|
||||||
|
...timerConfig.timers,
|
||||||
|
[cmd]: undefined,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTimer = (cmd: string): void => {
|
||||||
|
dispatch(
|
||||||
|
setTimerConfig({
|
||||||
|
...timerConfig,
|
||||||
|
timers: {
|
||||||
|
...timerConfig.timers,
|
||||||
|
[cmd]: {
|
||||||
|
...timerConfig.timers[cmd],
|
||||||
|
enabled: !timerConfig.timers[cmd].enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>{t('pages.bottimers.title')}</PageTitle>
|
||||||
|
<TextBlock>{t('pages.bottimers.desc')}</TextBlock>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<FlexRow spacing="1" align="left">
|
||||||
|
<Button
|
||||||
|
variation="primary"
|
||||||
|
onClick={() => setActiveDialog({ kind: 'new' })}
|
||||||
|
>
|
||||||
|
<PlusIcon /> {t('pages.bottimers.add-button')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<InputBox
|
||||||
|
css={{ flex: 1 }}
|
||||||
|
placeholder={t('pages.bottimers.search-placeholder')}
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FlexRow>
|
||||||
|
<TimerList>
|
||||||
|
{Object.keys(timerConfig?.timers ?? {})
|
||||||
|
?.filter((cmd) => cmd.toLowerCase().includes(filterLC))
|
||||||
|
.sort()
|
||||||
|
.map((cmd) => (
|
||||||
|
<TimerItem
|
||||||
|
key={cmd}
|
||||||
|
name={cmd}
|
||||||
|
item={timerConfig.timers[cmd]}
|
||||||
|
onToggle={() => toggleTimer(cmd)}
|
||||||
|
onEdit={() =>
|
||||||
|
setActiveDialog({
|
||||||
|
kind: 'edit',
|
||||||
|
name: cmd,
|
||||||
|
item: timerConfig.timers[cmd],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onDelete={() => deleteTimer(cmd)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TimerList>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={!!activeDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
// Reset dialog status on dialog close
|
||||||
|
setActiveDialog(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeDialog && (
|
||||||
|
<TimerDialog
|
||||||
|
{...activeDialog}
|
||||||
|
onSubmit={(name, data) => setTimer(name, data)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import * as UnstyledLabel from '@radix-ui/react-label';
|
import * as UnstyledLabel from '@radix-ui/react-label';
|
||||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
import { styled } from './theme';
|
import { styled } from './theme';
|
||||||
|
import { theme } from '.';
|
||||||
|
|
||||||
export const Field = styled('fieldset', {
|
export const Field = styled('fieldset', {
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
|
@ -45,7 +46,7 @@ export const InputBox = styled('input', {
|
||||||
fontWeight: '300',
|
fontWeight: '300',
|
||||||
border: '1px solid $gray6',
|
border: '1px solid $gray6',
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
borderRadius: '0.3rem',
|
borderRadius: theme.borderRadius.form,
|
||||||
backgroundColor: '$gray2',
|
backgroundColor: '$gray2',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
borderColor: '$teal7',
|
borderColor: '$teal7',
|
||||||
|
@ -59,6 +60,13 @@ export const InputBox = styled('input', {
|
||||||
borderColor: '$gray5',
|
borderColor: '$gray5',
|
||||||
color: '$gray8',
|
color: '$gray8',
|
||||||
},
|
},
|
||||||
|
variants: {
|
||||||
|
border: {
|
||||||
|
none: {
|
||||||
|
borderWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Textarea = styled('textarea', {
|
export const Textarea = styled('textarea', {
|
||||||
|
@ -66,7 +74,7 @@ export const Textarea = styled('textarea', {
|
||||||
fontWeight: '300',
|
fontWeight: '300',
|
||||||
border: '1px solid $gray6',
|
border: '1px solid $gray6',
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
borderRadius: '0.3rem',
|
borderRadius: theme.borderRadius.form,
|
||||||
backgroundColor: '$gray2',
|
backgroundColor: '$gray2',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
borderColor: '$teal7',
|
borderColor: '$teal7',
|
||||||
|
@ -80,6 +88,13 @@ export const Textarea = styled('textarea', {
|
||||||
borderColor: '$gray5',
|
borderColor: '$gray5',
|
||||||
color: '$gray8',
|
color: '$gray8',
|
||||||
},
|
},
|
||||||
|
variants: {
|
||||||
|
border: {
|
||||||
|
none: {
|
||||||
|
borderWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ButtonGroup = styled('div', {
|
export const ButtonGroup = styled('div', {
|
||||||
|
@ -87,12 +102,17 @@ export const ButtonGroup = styled('div', {
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MultiButton = styled('div', {
|
||||||
|
display: 'flex',
|
||||||
|
});
|
||||||
|
|
||||||
export const Button = styled('button', {
|
export const Button = styled('button', {
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
color: '$gray12',
|
||||||
fontWeight: '300',
|
fontWeight: '300',
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.5rem 1rem',
|
||||||
borderRadius: '0.3rem',
|
borderRadius: theme.borderRadius.form,
|
||||||
fontSize: '1.1rem',
|
fontSize: '1.1rem',
|
||||||
border: '1px solid $gray6',
|
border: '1px solid $gray6',
|
||||||
backgroundColor: '$gray4',
|
backgroundColor: '$gray4',
|
||||||
|
@ -108,13 +128,34 @@ export const Button = styled('button', {
|
||||||
},
|
},
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
variants: {
|
variants: {
|
||||||
|
border: {
|
||||||
|
none: {
|
||||||
|
borderWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
styling: {
|
styling: {
|
||||||
|
form: {
|
||||||
|
padding: '0.65rem',
|
||||||
|
},
|
||||||
link: {
|
link: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: '$teal11',
|
color: '$teal11',
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
|
multi: {
|
||||||
|
borderRadius: '0',
|
||||||
|
margin: '0 -1px',
|
||||||
|
'&:first-child': {
|
||||||
|
borderRadius: `$borderRadius$form 0 0 $borderRadius$form`,
|
||||||
|
},
|
||||||
|
'&:last-child': {
|
||||||
|
borderRadius: `0 $borderRadius$form $borderRadius$form 0`,
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
zIndex: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
small: {
|
small: {
|
||||||
|
@ -177,7 +218,7 @@ export const ComboBox = styled('select', {
|
||||||
fontWeight: '300',
|
fontWeight: '300',
|
||||||
border: '1px solid $gray6',
|
border: '1px solid $gray6',
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
borderRadius: '0.3rem',
|
borderRadius: theme.borderRadius.form,
|
||||||
backgroundColor: '$gray2',
|
backgroundColor: '$gray2',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
borderColor: '$teal7',
|
borderColor: '$teal7',
|
||||||
|
@ -191,6 +232,13 @@ export const ComboBox = styled('select', {
|
||||||
borderColor: '$gray5',
|
borderColor: '$gray5',
|
||||||
color: '$gray8',
|
color: '$gray8',
|
||||||
},
|
},
|
||||||
|
variants: {
|
||||||
|
border: {
|
||||||
|
none: {
|
||||||
|
borderWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Checkbox = styled(CheckboxPrimitive.Root, {
|
export const Checkbox = styled(CheckboxPrimitive.Root, {
|
||||||
|
|
|
@ -31,4 +31,11 @@ export const SectionHeader = styled('h2', {
|
||||||
|
|
||||||
export const TextBlock = styled('p', {
|
export const TextBlock = styled('p', {
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
none: {
|
||||||
|
margin: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,5 +37,8 @@ export const { styled, theme } = createStitches({
|
||||||
...redDark,
|
...redDark,
|
||||||
...blackA,
|
...blackA,
|
||||||
},
|
},
|
||||||
|
borderRadius: {
|
||||||
|
form: '0.3rem',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { theme } from '.';
|
||||||
import { styled } from './theme';
|
import { styled } from './theme';
|
||||||
|
|
||||||
export const FlexRow = styled('div', {
|
export const FlexRow = styled('div', {
|
||||||
|
@ -6,6 +7,12 @@ export const FlexRow = styled('div', {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
variants: {
|
variants: {
|
||||||
|
border: {
|
||||||
|
form: {
|
||||||
|
border: '1px solid $gray6',
|
||||||
|
borderRadius: theme.borderRadius.form,
|
||||||
|
},
|
||||||
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
'1': {
|
'1': {
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
|
|
Loading…
Reference in a new issue