shuffle code around
Build / build (push) Successful in 2m1s Details
Test / test (push) Successful in 37s Details

This commit is contained in:
Ash Keel 2024-04-25 22:34:24 +02:00
parent a1fab34a70
commit d276b734bf
No known key found for this signature in database
GPG Key ID: 53A9E9A6035DD109
20 changed files with 576 additions and 517 deletions

View File

@ -31,20 +31,20 @@ import { initializeServerInfo } from '~/store/server/reducer';
import LogViewer from './components/LogViewer';
import Sidebar, { RouteSection } from './components/Sidebar';
import Scrollbar from './components/utils/Scrollbar';
import TwitchChatCommandsPage from './pages/ChatCommands';
import TwitchChatTimersPage from './pages/ChatTimers';
import ChatAlertsPage from './pages/ChatAlerts';
import TwitchChatCommandsPage from './pages/twitch/ChatCommands';
import TwitchChatTimersPage from './pages/twitch/ChatTimers';
import ChatAlertsPage from './pages/twitch/ChatAlerts';
import Dashboard from './pages/Dashboard';
import DebugPage from './pages/Debug';
import LoyaltyConfigPage from './pages/LoyaltyConfig';
import LoyaltyQueuePage from './pages/LoyaltyQueue';
import LoyaltyRewardsPage from './pages/LoyaltyRewards';
import DebugPage from './pages/system/Debug';
import LoyaltyConfigPage from './pages/loyalty/LoyaltyConfig';
import LoyaltyQueuePage from './pages/loyalty/LoyaltyQueue';
import LoyaltyRewardsPage from './pages/loyalty/Rewards/Page';
import OnboardingPage from './pages/Onboarding';
import ServerSettingsPage from './pages/ServerSettings';
import StrimertulPage from './pages/Strimertul';
import TwitchSettingsPage from './pages/TwitchSettings/Page';
import UISettingsPage from './pages/UISettingsPage';
import ExtensionsPage from './pages/Extensions';
import ServerSettingsPage from './pages/system/ServerSettings';
import StrimertulPage from './pages/system/Strimertul';
import TwitchSettingsPage from './pages/twitch/TwitchSettings/Page';
import UISettingsPage from './pages/system/UISettingsPage';
import ExtensionsPage from './pages/system/Extensions';
import { getTheme, styled } from './theme';
import Loading from './components/Loading';
import InteractiveAuthDialog from './components/InteractiveAuthDialog';

View File

@ -3,7 +3,11 @@ import {
DiscordLogoIcon,
EnvelopeClosedIcon,
} from '@radix-ui/react-icons';
import { ChannelList, Channel, ChannelLink } from '~/ui/pages/Strimertul';
import {
ChannelList,
Channel,
ChannelLink,
} from '~/ui/pages/system/Strimertul';
export const Channels = (
<ChannelList>

View File

@ -16,9 +16,9 @@ import {
CheckboxIndicator,
InputBox,
FieldNote,
} from '../theme';
import SaveButton from '../components/forms/SaveButton';
import Interval from '../components/forms/Interval';
} from '../../theme';
import SaveButton from '../../components/forms/SaveButton';
import Interval from '../../components/forms/Interval';
export default function LoyaltySettingsPage(): React.ReactElement {
const { t } = useTranslation();

View File

@ -5,8 +5,8 @@ import { SortFunction } from '~/lib/types';
import { useAppDispatch } from '~/store';
import { modules, removeRedeem, setUserPoints } from '~/store/api/reducer';
import { LoyaltyRedeem } from '~/store/api/types';
import { DataTable } from '../components/DataTable';
import DialogContent from '../components/DialogContent';
import { DataTable } from '../../components/DataTable';
import DialogContent from '../../components/DialogContent';
import {
Button,
Dialog,
@ -24,8 +24,8 @@ import {
TabContent,
TabList,
TextBlock,
} from '../theme';
import { TableCell, TableRow } from '../theme/table';
} from '../../theme';
import { TableCell, TableRow } from '../../theme/table';
function RewardQueueRow({ data }: { data: LoyaltyRedeem & { date: Date } }) {
const dispatch = useAppDispatch();

View File

@ -0,0 +1,380 @@
import { PlusIcon } from '@radix-ui/react-icons';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useModule } from '~/lib/react';
import { useAppDispatch } from '~/store';
import { modules } from '~/store/api/reducer';
import { LoyaltyGoal } from '~/store/api/types';
import AlertContent from '../../../components/AlertContent';
import DialogContent from '../../../components/DialogContent';
import {
Button,
ControlledInputBox,
Dialog,
DialogActions,
Field,
FieldNote,
FlexRow,
InputBox,
Label,
MultiButton,
NoneText,
styled,
Textarea,
} from '../../../theme';
import { Alert, AlertTrigger } from '../../../theme/alert';
import {
RewardActions,
RewardCost,
RewardDescription,
RewardHeader,
RewardID,
RewardIcon,
RewardItemContainer,
RewardName,
} from './theme';
const GoalList = styled('div', { marginTop: '1rem' });
interface GoalItemProps {
name: string;
item: LoyaltyGoal;
currency: string;
onToggle?: () => void;
onEdit?: () => void;
onDelete?: () => void;
}
function GoalItem({
name,
item,
currency,
onToggle,
onEdit,
onDelete,
}: GoalItemProps): React.ReactElement {
const { t } = useTranslation();
return (
<RewardItemContainer status={item.enabled ? 'enabled' : 'disabled'}>
<RewardHeader>
<RewardIcon>
{item.image && (
<img
src={item.image}
style={{ width: '32px', borderRadius: '0.25rem' }}
/>
)}
</RewardIcon>
<RewardName status={item.enabled ? 'enabled' : 'disabled'}>
{item.name} (<RewardID>{name}</RewardID>)
</RewardName>
<RewardCost>
{item.contributed} / {item.total} {currency} (
{Math.round((item.contributed / item.total) * 100)}%)
</RewardCost>
<RewardActions>
<MultiButton>
<Button
styling="multi"
size="small"
onClick={() => (onToggle ? onToggle() : null)}
>
{item.enabled
? t('form-actions.disable')
: t('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.loyalty-rewards.remove-reward-title', {
name: item.name,
})}
description={t('form-actions.warning-delete')}
actionText={t('form-actions.delete')}
actionButtonProps={{ variation: 'danger' }}
showCancel={true}
onAction={() => (onDelete ? onDelete() : null)}
/>
</Alert>
</MultiButton>
</RewardActions>
</RewardHeader>
<RewardDescription>{item.description}</RewardDescription>
</RewardItemContainer>
);
}
export function GoalsTab() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [config] = useModule(modules.loyaltyConfig);
const [goals, setGoals] = useModule(modules.loyaltyGoals);
const [filter, setFilter] = useState('');
const [dialogGoal, setDialogGoal] = useState<{
open: boolean;
new: boolean;
goal: LoyaltyGoal;
}>({ open: false, new: false, goal: null });
const filterLC = filter.toLowerCase();
const deleteGoal = (id: string): void => {
void dispatch(setGoals(goals?.filter((r) => r.id !== id) ?? []));
};
const toggleGoal = (id: string): void => {
void dispatch(
setGoals(
goals?.map((r) => {
if (r.id === id) {
return {
...r,
enabled: !r.enabled,
};
}
return r;
}) ?? [],
),
);
};
return (
<>
<Dialog
open={dialogGoal.open}
onOpenChange={(state) => setDialogGoal({ ...dialogGoal, open: state })}
>
<DialogContent
title={
dialogGoal.new
? t('pages.loyalty-rewards.create-goal')
: t('pages.loyalty-rewards.edit-goal')
}
closeButton={true}
>
<form
onSubmit={(e) => {
e.preventDefault();
if (!(e.target as HTMLFormElement).checkValidity()) {
return;
}
const { goal } = dialogGoal;
const index = goals?.findIndex((g) => g.id === goal.id);
if (index >= 0) {
const newGoals = goals.slice(0);
newGoals[index] = goal;
void dispatch(setGoals(newGoals));
} else {
void dispatch(setGoals([...(goals ?? []), goal]));
}
setDialogGoal({ ...dialogGoal, open: false });
}}
>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-id">
{t('pages.loyalty-rewards.goal-id')}
</Label>
<ControlledInputBox
id="goal-id"
type="text"
required
disabled={!dialogGoal.new}
value={dialogGoal?.goal?.id}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
id:
e.target.value
?.toLowerCase()
.replace(/[^a-z0-9]/gi, '-') ?? '',
},
});
if (
dialogGoal.new &&
goals.find((r) => r.id === e.target.value)
) {
(e.target as HTMLInputElement).setCustomValidity(
t('pages.loyalty-rewards.id-already-in-use'),
);
} else {
(e.target as HTMLInputElement).setCustomValidity('');
}
}}
/>
<FieldNote>{t('pages.loyalty-rewards.goal-id-hint')}</FieldNote>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-name">
{t('pages.loyalty-rewards.goal-name')}
</Label>
<InputBox
id="goal-name"
type="text"
required
value={dialogGoal?.goal?.name ?? ''}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
name: e.target.value,
},
});
}}
/>
<FieldNote>{t('pages.loyalty-rewards.goal-name-hint')}</FieldNote>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-icon">
{t('pages.loyalty-rewards.goal-icon')}
</Label>
<InputBox
id="goal-icon"
type="text"
value={dialogGoal?.goal?.image ?? ''}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
image: e.target.value,
},
});
}}
/>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-desc">
{t('pages.loyalty-rewards.goal-desc')}
</Label>
<Textarea
id="goal-desc"
value={dialogGoal?.goal?.description ?? ''}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
description: e.target.value,
},
});
}}
>
{dialogGoal?.goal?.description ?? ''}
</Textarea>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-cost">
{t('pages.loyalty-rewards.goal-cost')}
</Label>
<InputBox
id="goal-cost"
type="number"
required
defaultValue={dialogGoal?.goal?.total}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
total: parseInt(e.target.value, 10),
},
});
}}
/>
</Field>
<DialogActions>
<Button variation="primary" type="submit">
{dialogGoal.new
? t('form-actions.create')
: t('form-actions.edit')}
</Button>
<Button
type="button"
onClick={() => setDialogGoal({ ...dialogGoal, open: false })}
>
{t('form-actions.cancel')}
</Button>
</DialogActions>
</form>
</DialogContent>
</Dialog>
<Field size="fullWidth" spacing="none">
<FlexRow css={{ flex: 1, alignItems: 'stretch' }} spacing="1">
<Button
variation="primary"
onClick={() => {
setDialogGoal({
open: true,
new: true,
goal: {
id: '',
enabled: true,
name: '',
description: '',
image: '',
total: 0,
contributed: 0,
contributors: {},
},
});
}}
>
<PlusIcon /> {t('pages.loyalty-rewards.create-goal')}
</Button>
<InputBox
css={{ flex: 1 }}
placeholder={t('pages.loyalty-rewards.goal-filter')}
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</FlexRow>
</Field>
<GoalList>
{goals && goals.length > 0 ? (
goals
?.filter(
(r) =>
r.name.toLowerCase().includes(filterLC) ||
r.id.toLowerCase().includes(filterLC) ||
r.description.toLowerCase().includes(filterLC),
)
.map((r) => (
<GoalItem
key={r.id}
name={r.id}
item={r}
currency={(
config?.currency || t('pages.loyalty-queue.points')
).toLowerCase()}
onEdit={() =>
setDialogGoal({
open: true,
new: false,
goal: r,
})
}
onDelete={() => deleteGoal(r.id)}
onToggle={() => toggleGoal(r.id)}
/>
))
) : (
<NoneText>{t('pages.loyalty-rewards.no-goals')}</NoneText>
)}
</GoalList>
</>
);
}

View File

@ -0,0 +1,42 @@
import { useTranslation } from 'react-i18next';
import {
PageContainer,
PageHeader,
PageTitle,
TabButton,
TabContainer,
TabContent,
TabList,
TextBlock,
} from '../../../theme';
import { GoalsTab } from './GoalsTab';
import { RewardsTab } from './RewardsTab';
export default function LoyaltyRewardsPage(): React.ReactElement {
const { t } = useTranslation();
return (
<PageContainer>
<PageHeader>
<PageTitle>{t('pages.loyalty-rewards.title')}</PageTitle>
<TextBlock>{t('pages.loyalty-rewards.subtitle')}</TextBlock>
</PageHeader>
<TabContainer defaultValue="rewards">
<TabList>
<TabButton value="rewards">
{t('pages.loyalty-rewards.rewards-tab')}
</TabButton>
<TabButton value="goals">
{t('pages.loyalty-rewards.goals-tab')}
</TabButton>
</TabList>
<TabContent value="rewards">
<RewardsTab />
</TabContent>
<TabContent value="goals">
<GoalsTab />
</TabContent>
</TabContainer>
</PageContainer>
);
}

View File

@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next';
import { useModule } from '~/lib/react';
import { useAppDispatch } from '~/store';
import { modules } from '~/store/api/reducer';
import { LoyaltyGoal, LoyaltyReward } from '~/store/api/types';
import AlertContent from '../components/AlertContent';
import DialogContent from '../components/DialogContent';
import Interval from '../components/forms/Interval';
import { LoyaltyReward } from '~/store/api/types';
import AlertContent from '../../../components/AlertContent';
import DialogContent from '../../../components/DialogContent';
import Interval from '../../../components/forms/Interval';
import {
Button,
Checkbox,
@ -22,88 +22,22 @@ import {
Label,
MultiButton,
NoneText,
PageContainer,
PageHeader,
PageTitle,
styled,
TabButton,
TabContainer,
TabContent,
TabList,
Textarea,
TextBlock,
} from '../theme';
import { Alert, AlertTrigger } from '../theme/alert';
} from '../../../theme';
import { Alert, AlertTrigger } from '../../../theme/alert';
import {
RewardItemContainer,
RewardHeader,
RewardIcon,
RewardName,
RewardID,
RewardCost,
RewardActions,
RewardDescription,
} from './theme';
const RewardList = styled('div', { marginTop: '1rem' });
const GoalList = styled('div', { marginTop: '1rem' });
const RewardItemContainer = 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: '$red6',
backgroundColor: '$gray3',
color: '$gray10',
},
},
},
});
const RewardHeader = styled('header', {
display: 'flex',
gap: '0.5rem',
alignItems: 'center',
marginBottom: '0.4rem',
});
const RewardName = styled('span', {
color: '$gray12',
flex: 1,
fontWeight: 'bold',
variants: {
status: {
enabled: {},
disabled: {
color: '$gray9',
},
},
},
});
const RewardDescription = styled('span', {
flex: 1,
fontSize: '0.9rem',
color: '$gray11',
});
const RewardActions = styled('div', {
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
});
const RewardID = styled('code', {
fontFamily: 'Space Mono',
color: '$teal11',
});
const RewardCost = styled('div', {
fontSize: '0.9rem',
marginRight: '0.5rem',
});
const RewardIcon = styled('div', {
width: '32px',
height: '32px',
backgroundColor: '$gray4',
borderRadius: '0.25rem',
display: 'flex',
alignItems: 'center',
});
interface RewardItemProps {
name: string;
@ -184,87 +118,7 @@ function RewardItem({
);
}
interface GoalItemProps {
name: string;
item: LoyaltyGoal;
currency: string;
onToggle?: () => void;
onEdit?: () => void;
onDelete?: () => void;
}
function GoalItem({
name,
item,
currency,
onToggle,
onEdit,
onDelete,
}: GoalItemProps): React.ReactElement {
const { t } = useTranslation();
return (
<RewardItemContainer status={item.enabled ? 'enabled' : 'disabled'}>
<RewardHeader>
<RewardIcon>
{item.image && (
<img
src={item.image}
style={{ width: '32px', borderRadius: '0.25rem' }}
/>
)}
</RewardIcon>
<RewardName status={item.enabled ? 'enabled' : 'disabled'}>
{item.name} (<RewardID>{name}</RewardID>)
</RewardName>
<RewardCost>
{item.contributed} / {item.total} {currency} (
{Math.round((item.contributed / item.total) * 100)}%)
</RewardCost>
<RewardActions>
<MultiButton>
<Button
styling="multi"
size="small"
onClick={() => (onToggle ? onToggle() : null)}
>
{item.enabled
? t('form-actions.disable')
: t('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.loyalty-rewards.remove-reward-title', {
name: item.name,
})}
description={t('form-actions.warning-delete')}
actionText={t('form-actions.delete')}
actionButtonProps={{ variation: 'danger' }}
showCancel={true}
onAction={() => (onDelete ? onDelete() : null)}
/>
</Alert>
</MultiButton>
</RewardActions>
</RewardHeader>
<RewardDescription>{item.description}</RewardDescription>
</RewardItemContainer>
);
}
function RewardsPage() {
export function RewardsTab() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [cursorPosition, setCursorPosition] = useState(0);
@ -601,295 +455,3 @@ function RewardsPage() {
</>
);
}
function GoalsPage() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [config] = useModule(modules.loyaltyConfig);
const [goals, setGoals] = useModule(modules.loyaltyGoals);
const [filter, setFilter] = useState('');
const [dialogGoal, setDialogGoal] = useState<{
open: boolean;
new: boolean;
goal: LoyaltyGoal;
}>({ open: false, new: false, goal: null });
const filterLC = filter.toLowerCase();
const deleteGoal = (id: string): void => {
void dispatch(setGoals(goals?.filter((r) => r.id !== id) ?? []));
};
const toggleGoal = (id: string): void => {
void dispatch(
setGoals(
goals?.map((r) => {
if (r.id === id) {
return {
...r,
enabled: !r.enabled,
};
}
return r;
}) ?? [],
),
);
};
return (
<>
<Dialog
open={dialogGoal.open}
onOpenChange={(state) => setDialogGoal({ ...dialogGoal, open: state })}
>
<DialogContent
title={
dialogGoal.new
? t('pages.loyalty-rewards.create-goal')
: t('pages.loyalty-rewards.edit-goal')
}
closeButton={true}
>
<form
onSubmit={(e) => {
e.preventDefault();
if (!(e.target as HTMLFormElement).checkValidity()) {
return;
}
const { goal } = dialogGoal;
const index = goals?.findIndex((g) => g.id === goal.id);
if (index >= 0) {
const newGoals = goals.slice(0);
newGoals[index] = goal;
void dispatch(setGoals(newGoals));
} else {
void dispatch(setGoals([...(goals ?? []), goal]));
}
setDialogGoal({ ...dialogGoal, open: false });
}}
>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-id">
{t('pages.loyalty-rewards.goal-id')}
</Label>
<ControlledInputBox
id="goal-id"
type="text"
required
disabled={!dialogGoal.new}
value={dialogGoal?.goal?.id}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
id:
e.target.value
?.toLowerCase()
.replace(/[^a-z0-9]/gi, '-') ?? '',
},
});
if (
dialogGoal.new &&
goals.find((r) => r.id === e.target.value)
) {
(e.target as HTMLInputElement).setCustomValidity(
t('pages.loyalty-rewards.id-already-in-use'),
);
} else {
(e.target as HTMLInputElement).setCustomValidity('');
}
}}
/>
<FieldNote>{t('pages.loyalty-rewards.goal-id-hint')}</FieldNote>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-name">
{t('pages.loyalty-rewards.goal-name')}
</Label>
<InputBox
id="goal-name"
type="text"
required
value={dialogGoal?.goal?.name ?? ''}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
name: e.target.value,
},
});
}}
/>
<FieldNote>{t('pages.loyalty-rewards.goal-name-hint')}</FieldNote>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-icon">
{t('pages.loyalty-rewards.goal-icon')}
</Label>
<InputBox
id="goal-icon"
type="text"
value={dialogGoal?.goal?.image ?? ''}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
image: e.target.value,
},
});
}}
/>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-desc">
{t('pages.loyalty-rewards.goal-desc')}
</Label>
<Textarea
id="goal-desc"
value={dialogGoal?.goal?.description ?? ''}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
description: e.target.value,
},
});
}}
>
{dialogGoal?.goal?.description ?? ''}
</Textarea>
</Field>
<Field size="fullWidth" spacing="narrow">
<Label htmlFor="goal-cost">
{t('pages.loyalty-rewards.goal-cost')}
</Label>
<InputBox
id="goal-cost"
type="number"
required
defaultValue={dialogGoal?.goal?.total}
onChange={(e) => {
setDialogGoal({
...dialogGoal,
goal: {
...dialogGoal?.goal,
total: parseInt(e.target.value, 10),
},
});
}}
/>
</Field>
<DialogActions>
<Button variation="primary" type="submit">
{dialogGoal.new
? t('form-actions.create')
: t('form-actions.edit')}
</Button>
<Button
type="button"
onClick={() => setDialogGoal({ ...dialogGoal, open: false })}
>
{t('form-actions.cancel')}
</Button>
</DialogActions>
</form>
</DialogContent>
</Dialog>
<Field size="fullWidth" spacing="none">
<FlexRow css={{ flex: 1, alignItems: 'stretch' }} spacing="1">
<Button
variation="primary"
onClick={() => {
setDialogGoal({
open: true,
new: true,
goal: {
id: '',
enabled: true,
name: '',
description: '',
image: '',
total: 0,
contributed: 0,
contributors: {},
},
});
}}
>
<PlusIcon /> {t('pages.loyalty-rewards.create-goal')}
</Button>
<InputBox
css={{ flex: 1 }}
placeholder={t('pages.loyalty-rewards.goal-filter')}
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</FlexRow>
</Field>
<GoalList>
{goals && goals.length > 0 ? (
goals
?.filter(
(r) =>
r.name.toLowerCase().includes(filterLC) ||
r.id.toLowerCase().includes(filterLC) ||
r.description.toLowerCase().includes(filterLC),
)
.map((r) => (
<GoalItem
key={r.id}
name={r.id}
item={r}
currency={(
config?.currency || t('pages.loyalty-queue.points')
).toLowerCase()}
onEdit={() =>
setDialogGoal({
open: true,
new: false,
goal: r,
})
}
onDelete={() => deleteGoal(r.id)}
onToggle={() => toggleGoal(r.id)}
/>
))
) : (
<NoneText>{t('pages.loyalty-rewards.no-goals')}</NoneText>
)}
</GoalList>
</>
);
}
export default function LoyaltyRewardsPage(): React.ReactElement {
const { t } = useTranslation();
return (
<PageContainer>
<PageHeader>
<PageTitle>{t('pages.loyalty-rewards.title')}</PageTitle>
<TextBlock>{t('pages.loyalty-rewards.subtitle')}</TextBlock>
</PageHeader>
<TabContainer defaultValue="rewards">
<TabList>
<TabButton value="rewards">
{t('pages.loyalty-rewards.rewards-tab')}
</TabButton>
<TabButton value="goals">
{t('pages.loyalty-rewards.goals-tab')}
</TabButton>
</TabList>
<TabContent value="rewards">
<RewardsPage />
</TabContent>
<TabContent value="goals">
<GoalsPage />
</TabContent>
</TabContainer>
</PageContainer>
);
}

View File

@ -0,0 +1,71 @@
import { styled } from '~/ui/theme';
export const RewardItemContainer = 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: '$red6',
backgroundColor: '$gray3',
color: '$gray10',
},
},
},
});
export const RewardHeader = styled('header', {
display: 'flex',
gap: '0.5rem',
alignItems: 'center',
marginBottom: '0.4rem',
});
export const RewardName = styled('span', {
color: '$gray12',
flex: 1,
fontWeight: 'bold',
variants: {
status: {
enabled: {},
disabled: {
color: '$gray9',
},
},
},
});
export const RewardDescription = styled('span', {
flex: 1,
fontSize: '0.9rem',
color: '$gray11',
});
export const RewardActions = styled('div', {
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
});
export const RewardID = styled('code', {
fontFamily: 'Space Mono',
color: '$teal11',
});
export const RewardCost = styled('div', {
fontSize: '0.9rem',
marginRight: '0.5rem',
});
export const RewardIcon = styled('div', {
width: '32px',
height: '32px',
backgroundColor: '$gray4',
borderRadius: '0.25rem',
display: 'flex',
alignItems: 'center',
});

View File

@ -14,7 +14,7 @@ import {
PageTitle,
styled,
Textarea,
} from '../theme';
} from '../../theme';
const Disclaimer = styled('div', {
display: 'flex',

View File

@ -28,9 +28,9 @@ import extensionsReducer, {
} from '~/store/extensions/reducer';
import { useModule } from '~/lib/react';
import { modules } from '~/store/api/reducer';
import AlertContent from '../components/AlertContent';
import DialogContent from '../components/DialogContent';
import Loading from '../components/Loading';
import AlertContent from '../../components/AlertContent';
import DialogContent from '../../components/DialogContent';
import Loading from '../../components/Loading';
import {
Button,
ComboBox,
@ -51,8 +51,8 @@ import {
TabContainer,
TabContent,
TabList,
} from '../theme';
import { Alert, AlertTrigger } from '../theme/alert';
} from '../../theme';
import { Alert, AlertTrigger } from '../../theme/alert';
const ExtensionRow = styled('article', {
marginBottom: '0.4rem',

View File

@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next';
import { useModule, useStatus } from '~/lib/react';
import { useAppDispatch } from '~/store';
import apiReducer, { modules } from '~/store/api/reducer';
import AlertContent from '../components/AlertContent';
import RevealLink from '../components/utils/RevealLink';
import SaveButton from '../components/forms/SaveButton';
import AlertContent from '../../components/AlertContent';
import RevealLink from '../../components/utils/RevealLink';
import SaveButton from '../../components/forms/SaveButton';
import {
Field,
FieldNote,
@ -15,8 +15,8 @@ import {
PageHeader,
PageTitle,
PasswordInputBox,
} from '../theme';
import { Alert } from '../theme/alert';
} from '../../theme';
import { Alert } from '../../theme/alert';
export default function ServerSettingsPage(): React.ReactElement {
const [serverConfig, setServerConfig, loadStatus] = useModule(

View File

@ -6,9 +6,9 @@ import { useNavigate } from 'react-router-dom';
// @ts-expect-error Asset import
import logo from '~/assets/icon-logo.svg';
import { APPNAME, PageContainer, PageHeader, styled } from '../theme';
import BrowserLink from '../components/BrowserLink';
import Channels from '../components/utils/Channels';
import { APPNAME, PageContainer, PageHeader, styled } from '../../theme';
import BrowserLink from '../../components/BrowserLink';
import Channels from '../../components/utils/Channels';
const gradientAnimation = keyframes({
'0%': {

View File

@ -4,7 +4,7 @@ import { useModule } from '~/lib/react';
import { languages } from '~/locale/languages';
import { useAppDispatch } from '~/store';
import { modules } from '~/store/api/reducer';
import RadioGroup from '../components/forms/RadioGroup';
import RadioGroup from '../../components/forms/RadioGroup';
import {
Button,
Field,
@ -14,7 +14,7 @@ import {
PageTitle,
styled,
themes,
} from '../theme';
} from '../../theme';
const PartialWarning = styled('small', {
color: '$yellow11',

View File

@ -4,7 +4,7 @@ import { CheckIcon } from '@radix-ui/react-icons';
import { useModule, useStatus } from '~/lib/react';
import apiReducer, { modules } from '~/store/api/reducer';
import { useAppDispatch } from '~/store';
import MultiInput from '../components/forms/MultiInput';
import MultiInput from '../../components/forms/MultiInput';
import {
Checkbox,
CheckboxIndicator,
@ -19,8 +19,8 @@ import {
TabContent,
TabList,
TextBlock,
} from '../theme';
import SaveButton from '../components/forms/SaveButton';
} from '../../theme';
import SaveButton from '../../components/forms/SaveButton';
export default function ChatAlertsPage(): React.ReactElement {
const { t } = useTranslation();

View File

@ -11,8 +11,8 @@ import {
TwitchChatCustomCommand,
} from '~/store/api/types';
import { TestCommandTemplate } from '@wailsapp/go/main/App';
import AlertContent from '../components/AlertContent';
import DialogContent from '../components/DialogContent';
import AlertContent from '../../components/AlertContent';
import DialogContent from '../../components/DialogContent';
import {
Button,
ComboBox,
@ -34,8 +34,8 @@ import {
styled,
Textarea,
TextBlock,
} from '../theme';
import { Alert, AlertTrigger } from '../theme/alert';
} from '../../theme';
import { Alert, AlertTrigger } from '../../theme/alert';
const CommandList = styled('div', { marginTop: '1rem' });
const CommandItemContainer = styled('article', {

View File

@ -6,11 +6,11 @@ import { useModule } from '~/lib/react';
import { useAppDispatch } from '~/store';
import { modules } from '~/store/api/reducer';
import { TwitchChatTimer } from '~/store/api/types';
import AlertContent from '../components/AlertContent';
import DialogContent from '../components/DialogContent';
import Interval from '../components/forms/Interval';
import { hours, minutes } from '../components/forms/units';
import MultiInput from '../components/forms/MultiInput';
import AlertContent from '../../components/AlertContent';
import DialogContent from '../../components/DialogContent';
import Interval from '../../components/forms/Interval';
import { hours, minutes } from '../../components/forms/units';
import MultiInput from '../../components/forms/MultiInput';
import {
Button,
Dialog,
@ -27,8 +27,8 @@ import {
PageTitle,
styled,
TextBlock,
} from '../theme';
import { Alert, AlertTrigger } from '../theme/alert';
} from '../../theme';
import { Alert, AlertTrigger } from '../../theme/alert';
const TimerList = styled('div', { marginTop: '1rem' });
const TimerItemContainer = styled('article', {

View File

@ -18,7 +18,7 @@ import {
TabContent,
TabList,
TextBlock,
} from '../../theme';
} from '../../../theme';
import TwitchAPISettings from './TwitchAPISettings';
import TwitchEventSubSettings from './TwitchEventSubSettings';
import TwitchChatSettings from './TwitchChatSettings';

View File

@ -4,10 +4,10 @@ import { useModule, useStatus } from '~/lib/react';
import { useAppDispatch } from '~/store';
import apiReducer, { modules } from '~/store/api/reducer';
import { checkTwitchKeys } from '~/lib/twitch';
import BrowserLink from '../../components/BrowserLink';
import DefinitionTable from '../../components/DefinitionTable';
import RevealLink from '../../components/utils/RevealLink';
import SaveButton from '../../components/forms/SaveButton';
import BrowserLink from '../../../components/BrowserLink';
import DefinitionTable from '../../../components/DefinitionTable';
import RevealLink from '../../../components/utils/RevealLink';
import SaveButton from '../../../components/forms/SaveButton';
import {
Button,
ButtonGroup,
@ -18,9 +18,9 @@ import {
SectionHeader,
styled,
TextBlock,
} from '../../theme';
import AlertContent from '../../components/AlertContent';
import { Alert } from '../../theme/alert';
} from '../../../theme';
import AlertContent from '../../../components/AlertContent';
import { Alert } from '../../../theme/alert';
const StepList = styled('ul', {
lineHeight: '1.5',

View File

@ -5,7 +5,7 @@ import apiReducer, { modules } from '~/store/api/reducer';
import { startAuthFlow } from '~/lib/twitch';
import TwitchUserBlock from '~/ui/components/TwitchUserBlock';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import SaveButton from '../../components/forms/SaveButton';
import SaveButton from '../../../components/forms/SaveButton';
import {
Button,
Field,
@ -14,7 +14,7 @@ import {
Label,
SectionHeader,
TextBlock,
} from '../../theme';
} from '../../../theme';
export default function TwitchChatSettings() {
const [chatConfig, setChatConfig, loadStatus] = useModule(

View File

@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next';
import eventsubTests from '~/data/eventsub-tests';
import { useAppSelector } from '~/store';
import { startAuthFlow } from '~/lib/twitch';
import { Button, ButtonGroup, SectionHeader, TextBlock } from '../../theme';
import TwitchUserBlock from '../../components/TwitchUserBlock';
import { Button, ButtonGroup, SectionHeader, TextBlock } from '../../../theme';
import TwitchUserBlock from '../../../components/TwitchUserBlock';
export default function TwitchEventSubSettings() {
const { t } = useTranslation();