mirror of https://git.sr.ht/~ashkeel/strimertul
shuffle code around
This commit is contained in:
parent
a1fab34a70
commit
d276b734bf
|
@ -31,20 +31,20 @@ import { initializeServerInfo } from '~/store/server/reducer';
|
||||||
import LogViewer from './components/LogViewer';
|
import LogViewer from './components/LogViewer';
|
||||||
import Sidebar, { RouteSection } from './components/Sidebar';
|
import Sidebar, { RouteSection } from './components/Sidebar';
|
||||||
import Scrollbar from './components/utils/Scrollbar';
|
import Scrollbar from './components/utils/Scrollbar';
|
||||||
import TwitchChatCommandsPage from './pages/ChatCommands';
|
import TwitchChatCommandsPage from './pages/twitch/ChatCommands';
|
||||||
import TwitchChatTimersPage from './pages/ChatTimers';
|
import TwitchChatTimersPage from './pages/twitch/ChatTimers';
|
||||||
import ChatAlertsPage from './pages/ChatAlerts';
|
import ChatAlertsPage from './pages/twitch/ChatAlerts';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import DebugPage from './pages/Debug';
|
import DebugPage from './pages/system/Debug';
|
||||||
import LoyaltyConfigPage from './pages/LoyaltyConfig';
|
import LoyaltyConfigPage from './pages/loyalty/LoyaltyConfig';
|
||||||
import LoyaltyQueuePage from './pages/LoyaltyQueue';
|
import LoyaltyQueuePage from './pages/loyalty/LoyaltyQueue';
|
||||||
import LoyaltyRewardsPage from './pages/LoyaltyRewards';
|
import LoyaltyRewardsPage from './pages/loyalty/Rewards/Page';
|
||||||
import OnboardingPage from './pages/Onboarding';
|
import OnboardingPage from './pages/Onboarding';
|
||||||
import ServerSettingsPage from './pages/ServerSettings';
|
import ServerSettingsPage from './pages/system/ServerSettings';
|
||||||
import StrimertulPage from './pages/Strimertul';
|
import StrimertulPage from './pages/system/Strimertul';
|
||||||
import TwitchSettingsPage from './pages/TwitchSettings/Page';
|
import TwitchSettingsPage from './pages/twitch/TwitchSettings/Page';
|
||||||
import UISettingsPage from './pages/UISettingsPage';
|
import UISettingsPage from './pages/system/UISettingsPage';
|
||||||
import ExtensionsPage from './pages/Extensions';
|
import ExtensionsPage from './pages/system/Extensions';
|
||||||
import { getTheme, styled } from './theme';
|
import { getTheme, styled } from './theme';
|
||||||
import Loading from './components/Loading';
|
import Loading from './components/Loading';
|
||||||
import InteractiveAuthDialog from './components/InteractiveAuthDialog';
|
import InteractiveAuthDialog from './components/InteractiveAuthDialog';
|
||||||
|
|
|
@ -3,7 +3,11 @@ import {
|
||||||
DiscordLogoIcon,
|
DiscordLogoIcon,
|
||||||
EnvelopeClosedIcon,
|
EnvelopeClosedIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} 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 = (
|
export const Channels = (
|
||||||
<ChannelList>
|
<ChannelList>
|
||||||
|
|
|
@ -16,9 +16,9 @@ import {
|
||||||
CheckboxIndicator,
|
CheckboxIndicator,
|
||||||
InputBox,
|
InputBox,
|
||||||
FieldNote,
|
FieldNote,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
import SaveButton from '../components/forms/SaveButton';
|
import SaveButton from '../../components/forms/SaveButton';
|
||||||
import Interval from '../components/forms/Interval';
|
import Interval from '../../components/forms/Interval';
|
||||||
|
|
||||||
export default function LoyaltySettingsPage(): React.ReactElement {
|
export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
|
@ -5,8 +5,8 @@ import { SortFunction } from '~/lib/types';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import { modules, removeRedeem, setUserPoints } from '~/store/api/reducer';
|
import { modules, removeRedeem, setUserPoints } from '~/store/api/reducer';
|
||||||
import { LoyaltyRedeem } from '~/store/api/types';
|
import { LoyaltyRedeem } from '~/store/api/types';
|
||||||
import { DataTable } from '../components/DataTable';
|
import { DataTable } from '../../components/DataTable';
|
||||||
import DialogContent from '../components/DialogContent';
|
import DialogContent from '../../components/DialogContent';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -24,8 +24,8 @@ import {
|
||||||
TabContent,
|
TabContent,
|
||||||
TabList,
|
TabList,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
import { TableCell, TableRow } from '../theme/table';
|
import { TableCell, TableRow } from '../../theme/table';
|
||||||
|
|
||||||
function RewardQueueRow({ data }: { data: LoyaltyRedeem & { date: Date } }) {
|
function RewardQueueRow({ data }: { data: LoyaltyRedeem & { date: Date } }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useModule } from '~/lib/react';
|
import { useModule } from '~/lib/react';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import { modules } from '~/store/api/reducer';
|
import { modules } from '~/store/api/reducer';
|
||||||
import { LoyaltyGoal, LoyaltyReward } from '~/store/api/types';
|
import { LoyaltyReward } from '~/store/api/types';
|
||||||
import AlertContent from '../components/AlertContent';
|
import AlertContent from '../../../components/AlertContent';
|
||||||
import DialogContent from '../components/DialogContent';
|
import DialogContent from '../../../components/DialogContent';
|
||||||
import Interval from '../components/forms/Interval';
|
import Interval from '../../../components/forms/Interval';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
@ -22,88 +22,22 @@ import {
|
||||||
Label,
|
Label,
|
||||||
MultiButton,
|
MultiButton,
|
||||||
NoneText,
|
NoneText,
|
||||||
PageContainer,
|
|
||||||
PageHeader,
|
|
||||||
PageTitle,
|
|
||||||
styled,
|
styled,
|
||||||
TabButton,
|
|
||||||
TabContainer,
|
|
||||||
TabContent,
|
|
||||||
TabList,
|
|
||||||
Textarea,
|
Textarea,
|
||||||
TextBlock,
|
} from '../../../theme';
|
||||||
} from '../theme';
|
import { Alert, AlertTrigger } from '../../../theme/alert';
|
||||||
import { Alert, AlertTrigger } from '../theme/alert';
|
import {
|
||||||
|
RewardItemContainer,
|
||||||
|
RewardHeader,
|
||||||
|
RewardIcon,
|
||||||
|
RewardName,
|
||||||
|
RewardID,
|
||||||
|
RewardCost,
|
||||||
|
RewardActions,
|
||||||
|
RewardDescription,
|
||||||
|
} from './theme';
|
||||||
|
|
||||||
const RewardList = styled('div', { marginTop: '1rem' });
|
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 {
|
interface RewardItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -184,87 +118,7 @@ function RewardItem({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GoalItemProps {
|
export function RewardsTab() {
|
||||||
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() {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [cursorPosition, setCursorPosition] = useState(0);
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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',
|
||||||
|
});
|
|
@ -14,7 +14,7 @@ import {
|
||||||
PageTitle,
|
PageTitle,
|
||||||
styled,
|
styled,
|
||||||
Textarea,
|
Textarea,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
|
|
||||||
const Disclaimer = styled('div', {
|
const Disclaimer = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
|
@ -28,9 +28,9 @@ import extensionsReducer, {
|
||||||
} from '~/store/extensions/reducer';
|
} from '~/store/extensions/reducer';
|
||||||
import { useModule } from '~/lib/react';
|
import { useModule } from '~/lib/react';
|
||||||
import { modules } from '~/store/api/reducer';
|
import { modules } from '~/store/api/reducer';
|
||||||
import AlertContent from '../components/AlertContent';
|
import AlertContent from '../../components/AlertContent';
|
||||||
import DialogContent from '../components/DialogContent';
|
import DialogContent from '../../components/DialogContent';
|
||||||
import Loading from '../components/Loading';
|
import Loading from '../../components/Loading';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ComboBox,
|
ComboBox,
|
||||||
|
@ -51,8 +51,8 @@ import {
|
||||||
TabContainer,
|
TabContainer,
|
||||||
TabContent,
|
TabContent,
|
||||||
TabList,
|
TabList,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
import { Alert, AlertTrigger } from '../theme/alert';
|
import { Alert, AlertTrigger } from '../../theme/alert';
|
||||||
|
|
||||||
const ExtensionRow = styled('article', {
|
const ExtensionRow = styled('article', {
|
||||||
marginBottom: '0.4rem',
|
marginBottom: '0.4rem',
|
|
@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useModule, useStatus } from '~/lib/react';
|
import { useModule, useStatus } from '~/lib/react';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import apiReducer, { modules } from '~/store/api/reducer';
|
import apiReducer, { modules } from '~/store/api/reducer';
|
||||||
import AlertContent from '../components/AlertContent';
|
import AlertContent from '../../components/AlertContent';
|
||||||
import RevealLink from '../components/utils/RevealLink';
|
import RevealLink from '../../components/utils/RevealLink';
|
||||||
import SaveButton from '../components/forms/SaveButton';
|
import SaveButton from '../../components/forms/SaveButton';
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldNote,
|
FieldNote,
|
||||||
|
@ -15,8 +15,8 @@ import {
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
PasswordInputBox,
|
PasswordInputBox,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
import { Alert } from '../theme/alert';
|
import { Alert } from '../../theme/alert';
|
||||||
|
|
||||||
export default function ServerSettingsPage(): React.ReactElement {
|
export default function ServerSettingsPage(): React.ReactElement {
|
||||||
const [serverConfig, setServerConfig, loadStatus] = useModule(
|
const [serverConfig, setServerConfig, loadStatus] = useModule(
|
|
@ -6,9 +6,9 @@ import { useNavigate } from 'react-router-dom';
|
||||||
// @ts-expect-error Asset import
|
// @ts-expect-error Asset import
|
||||||
import logo from '~/assets/icon-logo.svg';
|
import logo from '~/assets/icon-logo.svg';
|
||||||
|
|
||||||
import { APPNAME, PageContainer, PageHeader, styled } from '../theme';
|
import { APPNAME, PageContainer, PageHeader, styled } from '../../theme';
|
||||||
import BrowserLink from '../components/BrowserLink';
|
import BrowserLink from '../../components/BrowserLink';
|
||||||
import Channels from '../components/utils/Channels';
|
import Channels from '../../components/utils/Channels';
|
||||||
|
|
||||||
const gradientAnimation = keyframes({
|
const gradientAnimation = keyframes({
|
||||||
'0%': {
|
'0%': {
|
|
@ -4,7 +4,7 @@ import { useModule } from '~/lib/react';
|
||||||
import { languages } from '~/locale/languages';
|
import { languages } from '~/locale/languages';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import { modules } from '~/store/api/reducer';
|
import { modules } from '~/store/api/reducer';
|
||||||
import RadioGroup from '../components/forms/RadioGroup';
|
import RadioGroup from '../../components/forms/RadioGroup';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Field,
|
Field,
|
||||||
|
@ -14,7 +14,7 @@ import {
|
||||||
PageTitle,
|
PageTitle,
|
||||||
styled,
|
styled,
|
||||||
themes,
|
themes,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
|
|
||||||
const PartialWarning = styled('small', {
|
const PartialWarning = styled('small', {
|
||||||
color: '$yellow11',
|
color: '$yellow11',
|
|
@ -4,7 +4,7 @@ import { CheckIcon } from '@radix-ui/react-icons';
|
||||||
import { useModule, useStatus } from '~/lib/react';
|
import { useModule, useStatus } from '~/lib/react';
|
||||||
import apiReducer, { modules } from '~/store/api/reducer';
|
import apiReducer, { modules } from '~/store/api/reducer';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import MultiInput from '../components/forms/MultiInput';
|
import MultiInput from '../../components/forms/MultiInput';
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxIndicator,
|
CheckboxIndicator,
|
||||||
|
@ -19,8 +19,8 @@ import {
|
||||||
TabContent,
|
TabContent,
|
||||||
TabList,
|
TabList,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
import SaveButton from '../components/forms/SaveButton';
|
import SaveButton from '../../components/forms/SaveButton';
|
||||||
|
|
||||||
export default function ChatAlertsPage(): React.ReactElement {
|
export default function ChatAlertsPage(): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
|
@ -11,8 +11,8 @@ import {
|
||||||
TwitchChatCustomCommand,
|
TwitchChatCustomCommand,
|
||||||
} from '~/store/api/types';
|
} from '~/store/api/types';
|
||||||
import { TestCommandTemplate } from '@wailsapp/go/main/App';
|
import { TestCommandTemplate } from '@wailsapp/go/main/App';
|
||||||
import AlertContent from '../components/AlertContent';
|
import AlertContent from '../../components/AlertContent';
|
||||||
import DialogContent from '../components/DialogContent';
|
import DialogContent from '../../components/DialogContent';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ComboBox,
|
ComboBox,
|
||||||
|
@ -34,8 +34,8 @@ import {
|
||||||
styled,
|
styled,
|
||||||
Textarea,
|
Textarea,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
import { Alert, AlertTrigger } from '../theme/alert';
|
import { Alert, AlertTrigger } from '../../theme/alert';
|
||||||
|
|
||||||
const CommandList = styled('div', { marginTop: '1rem' });
|
const CommandList = styled('div', { marginTop: '1rem' });
|
||||||
const CommandItemContainer = styled('article', {
|
const CommandItemContainer = styled('article', {
|
|
@ -6,11 +6,11 @@ import { useModule } from '~/lib/react';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import { modules } from '~/store/api/reducer';
|
import { modules } from '~/store/api/reducer';
|
||||||
import { TwitchChatTimer } from '~/store/api/types';
|
import { TwitchChatTimer } from '~/store/api/types';
|
||||||
import AlertContent from '../components/AlertContent';
|
import AlertContent from '../../components/AlertContent';
|
||||||
import DialogContent from '../components/DialogContent';
|
import DialogContent from '../../components/DialogContent';
|
||||||
import Interval from '../components/forms/Interval';
|
import Interval from '../../components/forms/Interval';
|
||||||
import { hours, minutes } from '../components/forms/units';
|
import { hours, minutes } from '../../components/forms/units';
|
||||||
import MultiInput from '../components/forms/MultiInput';
|
import MultiInput from '../../components/forms/MultiInput';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -27,8 +27,8 @@ import {
|
||||||
PageTitle,
|
PageTitle,
|
||||||
styled,
|
styled,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../theme';
|
} from '../../theme';
|
||||||
import { Alert, AlertTrigger } from '../theme/alert';
|
import { Alert, AlertTrigger } from '../../theme/alert';
|
||||||
|
|
||||||
const TimerList = styled('div', { marginTop: '1rem' });
|
const TimerList = styled('div', { marginTop: '1rem' });
|
||||||
const TimerItemContainer = styled('article', {
|
const TimerItemContainer = styled('article', {
|
|
@ -18,7 +18,7 @@ import {
|
||||||
TabContent,
|
TabContent,
|
||||||
TabList,
|
TabList,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../../theme';
|
} from '../../../theme';
|
||||||
import TwitchAPISettings from './TwitchAPISettings';
|
import TwitchAPISettings from './TwitchAPISettings';
|
||||||
import TwitchEventSubSettings from './TwitchEventSubSettings';
|
import TwitchEventSubSettings from './TwitchEventSubSettings';
|
||||||
import TwitchChatSettings from './TwitchChatSettings';
|
import TwitchChatSettings from './TwitchChatSettings';
|
|
@ -4,10 +4,10 @@ import { useModule, useStatus } from '~/lib/react';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
import apiReducer, { modules } from '~/store/api/reducer';
|
import apiReducer, { modules } from '~/store/api/reducer';
|
||||||
import { checkTwitchKeys } from '~/lib/twitch';
|
import { checkTwitchKeys } from '~/lib/twitch';
|
||||||
import BrowserLink from '../../components/BrowserLink';
|
import BrowserLink from '../../../components/BrowserLink';
|
||||||
import DefinitionTable from '../../components/DefinitionTable';
|
import DefinitionTable from '../../../components/DefinitionTable';
|
||||||
import RevealLink from '../../components/utils/RevealLink';
|
import RevealLink from '../../../components/utils/RevealLink';
|
||||||
import SaveButton from '../../components/forms/SaveButton';
|
import SaveButton from '../../../components/forms/SaveButton';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
|
@ -18,9 +18,9 @@ import {
|
||||||
SectionHeader,
|
SectionHeader,
|
||||||
styled,
|
styled,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../../theme';
|
} from '../../../theme';
|
||||||
import AlertContent from '../../components/AlertContent';
|
import AlertContent from '../../../components/AlertContent';
|
||||||
import { Alert } from '../../theme/alert';
|
import { Alert } from '../../../theme/alert';
|
||||||
|
|
||||||
const StepList = styled('ul', {
|
const StepList = styled('ul', {
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
|
@ -5,7 +5,7 @@ import apiReducer, { modules } from '~/store/api/reducer';
|
||||||
import { startAuthFlow } from '~/lib/twitch';
|
import { startAuthFlow } from '~/lib/twitch';
|
||||||
import TwitchUserBlock from '~/ui/components/TwitchUserBlock';
|
import TwitchUserBlock from '~/ui/components/TwitchUserBlock';
|
||||||
import { ExternalLinkIcon } from '@radix-ui/react-icons';
|
import { ExternalLinkIcon } from '@radix-ui/react-icons';
|
||||||
import SaveButton from '../../components/forms/SaveButton';
|
import SaveButton from '../../../components/forms/SaveButton';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Field,
|
Field,
|
||||||
|
@ -14,7 +14,7 @@ import {
|
||||||
Label,
|
Label,
|
||||||
SectionHeader,
|
SectionHeader,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../../theme';
|
} from '../../../theme';
|
||||||
|
|
||||||
export default function TwitchChatSettings() {
|
export default function TwitchChatSettings() {
|
||||||
const [chatConfig, setChatConfig, loadStatus] = useModule(
|
const [chatConfig, setChatConfig, loadStatus] = useModule(
|
|
@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
import eventsubTests from '~/data/eventsub-tests';
|
import eventsubTests from '~/data/eventsub-tests';
|
||||||
import { useAppSelector } from '~/store';
|
import { useAppSelector } from '~/store';
|
||||||
import { startAuthFlow } from '~/lib/twitch';
|
import { startAuthFlow } from '~/lib/twitch';
|
||||||
import { Button, ButtonGroup, SectionHeader, TextBlock } from '../../theme';
|
import { Button, ButtonGroup, SectionHeader, TextBlock } from '../../../theme';
|
||||||
import TwitchUserBlock from '../../components/TwitchUserBlock';
|
import TwitchUserBlock from '../../../components/TwitchUserBlock';
|
||||||
|
|
||||||
export default function TwitchEventSubSettings() {
|
export default function TwitchEventSubSettings() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
Loading…
Reference in New Issue