mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
Re-add pagelist and WIP LoyaltyQueue
This commit is contained in:
parent
7e65c72bbb
commit
dd15e65251
12 changed files with 591 additions and 105 deletions
92
frontend/package-lock.json
generated
92
frontend/package-lock.json
generated
|
@ -750,6 +750,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@radix-ui/react-separator": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-4T19/TwF/fp/5x8H2KjNre1htv5iJkXcXzr4QJnqEYiE/wkPwWlW/vcuTWKIY8AXDp+tAgGw0UPrajV7loU+TA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-primitive": "0.1.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-slot": "0.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@radix-ui/react-slot": {
|
"@radix-ui/react-slot": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz",
|
||||||
|
@ -794,6 +814,78 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@radix-ui/react-toggle": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-ig2LsivpVOxbPoBpNy/6dgB5WNm0mkPMaBBgwo7yufDhlUExdp+bRCqkcWkN9EV42SEbd4etNEIrLAqppfgq3g==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "0.1.0",
|
||||||
|
"@radix-ui/react-primitive": "0.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "0.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-slot": "0.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-toggle-group": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-pzs3Cc5I9JKnBdCBCB4xfhbQpWNKY9dYwKVxcYLzGmaUsVPUiAq2AYr/Ht3LebhIxWYHkggjX8EOTeO6ExMr8Q==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "0.1.0",
|
||||||
|
"@radix-ui/react-context": "0.1.1",
|
||||||
|
"@radix-ui/react-primitive": "0.1.3",
|
||||||
|
"@radix-ui/react-roving-focus": "0.1.4",
|
||||||
|
"@radix-ui/react-toggle": "0.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "0.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-slot": "0.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radix-ui/react-toolbar": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-0+Qoq/dkQCwNGvOXAopWDz1HGR85HdkFrNGK2WOAQ5goktgeqRgd0QBixOOsLP+vorBBMZSGxkNocSZpZg8yoA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "0.1.0",
|
||||||
|
"@radix-ui/react-context": "0.1.1",
|
||||||
|
"@radix-ui/react-primitive": "0.1.3",
|
||||||
|
"@radix-ui/react-roving-focus": "0.1.4",
|
||||||
|
"@radix-ui/react-separator": "0.1.3",
|
||||||
|
"@radix-ui/react-toggle-group": "0.1.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-slot": "0.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@radix-ui/react-use-body-pointer-events": {
|
"@radix-ui/react-use-body-pointer-events": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.0.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@radix-ui/react-icons": "^1.0.3",
|
"@radix-ui/react-icons": "^1.0.3",
|
||||||
"@radix-ui/react-label": "^0.1.3",
|
"@radix-ui/react-label": "^0.1.3",
|
||||||
"@radix-ui/react-tabs": "^0.1.4",
|
"@radix-ui/react-tabs": "^0.1.4",
|
||||||
|
"@radix-ui/react-toolbar": "^0.1.4",
|
||||||
"@reduxjs/toolkit": "^1.5.1",
|
"@reduxjs/toolkit": "^1.5.1",
|
||||||
"@stitches/react": "^1.2.6",
|
"@stitches/react": "^1.2.6",
|
||||||
"@strimertul/kilovolt-client": "^6.2.0",
|
"@strimertul/kilovolt-client": "^6.2.0",
|
||||||
|
|
|
@ -174,6 +174,12 @@
|
||||||
"note": "Note: Unlike platform-native systems (eg. Twitch channel points), this relies on chat activity rather than actual viewing status.",
|
"note": "Note: Unlike platform-native systems (eg. Twitch channel points), this relies on chat activity rather than actual viewing status.",
|
||||||
"every": "every",
|
"every": "every",
|
||||||
"reward": "How often to give {{currency}}"
|
"reward": "How often to give {{currency}}"
|
||||||
|
},
|
||||||
|
"loyalty-queue": {
|
||||||
|
"title": "Points and redeems",
|
||||||
|
"subtitle": "User leaderboard and pending reward redeems",
|
||||||
|
"queue-tab": "Redeem queue",
|
||||||
|
"users-tab": "Manage points"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form-actions": {
|
"form-actions": {
|
||||||
|
@ -199,5 +205,15 @@
|
||||||
"hours": "hours",
|
"hours": "hours",
|
||||||
"minutes": "minutes",
|
"minutes": "minutes",
|
||||||
"seconds": "seconds"
|
"seconds": "seconds"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"items-per-page": "Items per page",
|
||||||
|
"page": "Page {{page}}",
|
||||||
|
"gotopage": "Go to page {{page}}",
|
||||||
|
"title": "pagination",
|
||||||
|
"previous": "Previous page",
|
||||||
|
"next": "Next page",
|
||||||
|
"gotolast": "Go to last page",
|
||||||
|
"gotofirst": "Go to first page"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import TwitchBotTimersPage from './pages/BotTimers';
|
||||||
import AuthDialog from './pages/AuthDialog';
|
import AuthDialog from './pages/AuthDialog';
|
||||||
import ChatAlertsPage from './pages/ChatAlerts';
|
import ChatAlertsPage from './pages/ChatAlerts';
|
||||||
import LoyaltyConfigPage from './pages/LoyaltyConfig';
|
import LoyaltyConfigPage from './pages/LoyaltyConfig';
|
||||||
|
import LoyaltyQueuePage from './pages/LoyaltyQueue';
|
||||||
|
|
||||||
const LoadingDiv = styled('div', {
|
const LoadingDiv = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -192,6 +193,7 @@ export default function App(): JSX.Element {
|
||||||
/>
|
/>
|
||||||
<Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} />
|
<Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} />
|
||||||
<Route path="/loyalty/settings" element={<LoyaltyConfigPage />} />
|
<Route path="/loyalty/settings" element={<LoyaltyConfigPage />} />
|
||||||
|
<Route path="/loyalty/users" element={<LoyaltyQueuePage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
|
@ -57,6 +57,7 @@ function Interval({
|
||||||
type="number"
|
type="number"
|
||||||
border="none"
|
border="none"
|
||||||
required={required}
|
required={required}
|
||||||
|
disabled={!active}
|
||||||
css={{
|
css={{
|
||||||
maxWidth: '5rem',
|
maxWidth: '5rem',
|
||||||
borderRightWidth: '1px',
|
borderRightWidth: '1px',
|
||||||
|
|
167
frontend/src/ui/components/PageList.tsx
Normal file
167
frontend/src/ui/components/PageList.tsx
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { styled, Toolbar, ToolbarButton, ToolbarComboBox } from '../theme';
|
||||||
|
|
||||||
|
export interface PageListProps {
|
||||||
|
current: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
itemsPerPage: number;
|
||||||
|
onSelectChange: (itemsPerPage: number) => void;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolbarSection = styled('section', {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '0.3rem',
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageList({
|
||||||
|
current,
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
itemsPerPage,
|
||||||
|
onSelectChange,
|
||||||
|
onPageChange,
|
||||||
|
}: PageListProps): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Toolbar
|
||||||
|
role="navigation"
|
||||||
|
aria-label={t('pagination.title')}
|
||||||
|
css={{
|
||||||
|
'@medium': {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToolbarSection
|
||||||
|
css={{
|
||||||
|
'@mobile': { flex: 1 },
|
||||||
|
'@medium': { flex: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToolbarButton
|
||||||
|
aria-label={t('pagination.previous')}
|
||||||
|
title={t('pagination.previous')}
|
||||||
|
disabled={current <= min}
|
||||||
|
onClick={() => onPageChange(current - 1)}
|
||||||
|
css={{
|
||||||
|
'@mobile': { flex: 1 },
|
||||||
|
'@medium': { flex: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
aria-label={t('pagination.next')}
|
||||||
|
title={t('pagination.next')}
|
||||||
|
disabled={current >= max}
|
||||||
|
onClick={() => onPageChange(current + 1)}
|
||||||
|
css={{
|
||||||
|
'@mobile': { flex: 1 },
|
||||||
|
'@medium': { flex: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarComboBox
|
||||||
|
title={t('pagination.items-per-page')}
|
||||||
|
aria-label={t('pagination.items-per-page')}
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={(ev) => onSelectChange(Number(ev.target.value))}
|
||||||
|
css={{
|
||||||
|
textAlign: 'center',
|
||||||
|
'@mobile': { flex: 1 },
|
||||||
|
'@medium': { flex: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={15}>15</option>
|
||||||
|
<option value={30}>30</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
<option value={100}>100</option>
|
||||||
|
</ToolbarComboBox>
|
||||||
|
</ToolbarSection>
|
||||||
|
<ToolbarSection>
|
||||||
|
{current > min ? (
|
||||||
|
<ToolbarButton
|
||||||
|
className="button pagination-link"
|
||||||
|
aria-label={t('pagination.gotofirst')}
|
||||||
|
title={t('pagination.gotofirst')}
|
||||||
|
onClick={() => onPageChange(min)}
|
||||||
|
>
|
||||||
|
{min}
|
||||||
|
</ToolbarButton>
|
||||||
|
) : null}
|
||||||
|
{current > min + 2 ? (
|
||||||
|
<span className="pagination-ellipsis">…</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{current > min + 1 ? (
|
||||||
|
<ToolbarButton
|
||||||
|
className="button pagination-link"
|
||||||
|
aria-label={t('pagination.gotopage', {
|
||||||
|
page: current - 1,
|
||||||
|
})}
|
||||||
|
title={t('pagination.gotopage', {
|
||||||
|
page: current - 1,
|
||||||
|
})}
|
||||||
|
onClick={() => onPageChange(current - 1)}
|
||||||
|
>
|
||||||
|
{current - 1}
|
||||||
|
</ToolbarButton>
|
||||||
|
) : null}
|
||||||
|
<ToolbarButton
|
||||||
|
disabled={true}
|
||||||
|
className="pagination-link is-current"
|
||||||
|
aria-label={t('pagination.page', {
|
||||||
|
page: current,
|
||||||
|
})}
|
||||||
|
title={t('pagination.page', {
|
||||||
|
page: current,
|
||||||
|
})}
|
||||||
|
aria-current="page"
|
||||||
|
css={{
|
||||||
|
border: '1px solid $teal7',
|
||||||
|
cursor: 'inherit',
|
||||||
|
background: '$teal7',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{current}
|
||||||
|
</ToolbarButton>
|
||||||
|
{current < max ? (
|
||||||
|
<ToolbarButton
|
||||||
|
className="button pagination-link"
|
||||||
|
aria-label={t('pagination.gotopage', {
|
||||||
|
page: current + 1,
|
||||||
|
})}
|
||||||
|
title={t('pagination.gotopage', {
|
||||||
|
page: current + 1,
|
||||||
|
})}
|
||||||
|
onClick={() => onPageChange(current + 1)}
|
||||||
|
>
|
||||||
|
{current + 1}
|
||||||
|
</ToolbarButton>
|
||||||
|
) : null}
|
||||||
|
{current < max - 2 ? (
|
||||||
|
<span className="pagination-ellipsis">…</span>
|
||||||
|
) : null}
|
||||||
|
{current < max - 1 ? (
|
||||||
|
<ToolbarButton
|
||||||
|
className="button pagination-link"
|
||||||
|
aria-label={t('pagination.gotolast')}
|
||||||
|
title={t('pagination.gotolast')}
|
||||||
|
onClick={() => onPageChange(max)}
|
||||||
|
>
|
||||||
|
{max}
|
||||||
|
</ToolbarButton>
|
||||||
|
) : null}
|
||||||
|
</ToolbarSection>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(PageList);
|
|
@ -48,7 +48,7 @@ const CommandItemContainer = styled('article', {
|
||||||
status: {
|
status: {
|
||||||
enabled: {},
|
enabled: {},
|
||||||
disabled: {
|
disabled: {
|
||||||
borderLeftColor: '$red7',
|
borderLeftColor: '$red6',
|
||||||
backgroundColor: '$gray3',
|
backgroundColor: '$gray3',
|
||||||
color: '$gray10',
|
color: '$gray10',
|
||||||
},
|
},
|
||||||
|
@ -68,7 +68,7 @@ const CommandName = styled('span', {
|
||||||
status: {
|
status: {
|
||||||
enabled: {},
|
enabled: {},
|
||||||
disabled: {
|
disabled: {
|
||||||
color: '$gray10',
|
color: '$gray9',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,7 +56,6 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</Field>
|
</Field>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{active && (
|
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -75,7 +74,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
id="currency"
|
id="currency"
|
||||||
placeholder={t('pages.loyalty-settings.currency-placeholder')}
|
placeholder={t('pages.loyalty-settings.currency-placeholder')}
|
||||||
value={config?.currency ?? ''}
|
value={config?.currency ?? ''}
|
||||||
disabled={busy}
|
disabled={!active || busy}
|
||||||
required={true}
|
required={true}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -106,7 +105,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
placeholder={'0'}
|
placeholder={'0'}
|
||||||
css={{ maxWidth: '5rem' }}
|
css={{ maxWidth: '5rem' }}
|
||||||
value={config?.points?.amount ?? '0'}
|
value={config?.points?.amount ?? '0'}
|
||||||
disabled={busy}
|
disabled={!active || busy}
|
||||||
required={true}
|
required={true}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const intNum = parseInt(e.target.value, 10);
|
const intNum = parseInt(e.target.value, 10);
|
||||||
|
@ -139,7 +138,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
active={!busy}
|
active={active && !busy}
|
||||||
min={5}
|
min={5}
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
|
@ -155,7 +154,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
id="bonus"
|
id="bonus"
|
||||||
placeholder={'0'}
|
placeholder={'0'}
|
||||||
value={config?.points?.activity_bonus ?? '0'}
|
value={config?.points?.activity_bonus ?? '0'}
|
||||||
disabled={busy}
|
disabled={!active || busy}
|
||||||
required={true}
|
required={true}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const intNum = parseInt(e.target.value, 10);
|
const intNum = parseInt(e.target.value, 10);
|
||||||
|
@ -173,14 +172,11 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FieldNote>
|
<FieldNote>{t('pages.loyalty-settings.bonus-points-hint')}</FieldNote>
|
||||||
{t('pages.loyalty-settings.bonus-points-hint')}
|
|
||||||
</FieldNote>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<SaveButton type="submit" status={status} />
|
<SaveButton type="submit" status={status} />
|
||||||
</form>
|
</form>
|
||||||
)}
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
145
frontend/src/ui/pages/LoyaltyQueue.tsx
Normal file
145
frontend/src/ui/pages/LoyaltyQueue.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useModule, useUserPoints } from '../../lib/react-utils';
|
||||||
|
import { modules } from '../../store/api/reducer';
|
||||||
|
import PageList from '../components/PageList';
|
||||||
|
import {
|
||||||
|
PageContainer,
|
||||||
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
TabButton,
|
||||||
|
TabContainer,
|
||||||
|
TabContent,
|
||||||
|
TabList,
|
||||||
|
TextBlock,
|
||||||
|
} from '../theme';
|
||||||
|
|
||||||
|
interface UserSortingOrder {
|
||||||
|
key: 'user' | 'points';
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
function RewardQueue() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [queue, setQueue] = useModule(modules.loyaltyRedeemQueue);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// Big hack but this is required or refunds break
|
||||||
|
useUserPoints();
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserList() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const users = useUserPoints();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [entriesPerPage, setEntriesPerPage] = useState(15);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [usernameFilter, setUsernameFilter] = useState('');
|
||||||
|
const [sorting, setSorting] = useState<UserSortingOrder>({
|
||||||
|
key: 'points',
|
||||||
|
order: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeSort = (key: 'user' | 'points') => {
|
||||||
|
if (sorting.key === key) {
|
||||||
|
// Same key, swap sorting order
|
||||||
|
setSorting({
|
||||||
|
...sorting,
|
||||||
|
order: sorting.order === 'asc' ? 'desc' : 'asc',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Different key, change to sort that key
|
||||||
|
setSorting({ ...sorting, key, order: 'asc' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawEntries = Object.entries(users ?? []);
|
||||||
|
const filtered = rawEntries.filter(([user]) => user.includes(usernameFilter));
|
||||||
|
|
||||||
|
const sortedEntries = filtered;
|
||||||
|
switch (sorting.key) {
|
||||||
|
case 'user':
|
||||||
|
if (sorting.order === 'asc') {
|
||||||
|
sortedEntries.sort(([userA], [userB]) => (userA > userB ? 1 : -1));
|
||||||
|
} else {
|
||||||
|
sortedEntries.sort(([userA], [userB]) => (userA < userB ? 1 : -1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'points':
|
||||||
|
if (sorting.order === 'asc') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
sortedEntries.sort(([_a, a], [_b, b]) => a.points - b.points);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
sortedEntries.sort(([_a, a], [_b, b]) => b.points - a.points);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// unreacheable
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = page * entriesPerPage;
|
||||||
|
const paged = sortedEntries.slice(offset, offset + entriesPerPage);
|
||||||
|
const totalPages = Math.floor(sortedEntries.length / entriesPerPage);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageList
|
||||||
|
current={page + 1}
|
||||||
|
min={1}
|
||||||
|
max={totalPages + 1}
|
||||||
|
itemsPerPage={entriesPerPage}
|
||||||
|
onSelectChange={(em) => setEntriesPerPage(em)}
|
||||||
|
onPageChange={(p) => setPage(p - 1)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{paged.map(([user, { points }]) => (
|
||||||
|
<article key={user}>
|
||||||
|
{user} - {points}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<PageList
|
||||||
|
current={page + 1}
|
||||||
|
min={1}
|
||||||
|
max={totalPages + 1}
|
||||||
|
itemsPerPage={entriesPerPage}
|
||||||
|
onSelectChange={(em) => setEntriesPerPage(em)}
|
||||||
|
onPageChange={(p) => setPage(p - 1)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoyaltyQueuePage(): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>{t('pages.loyalty-queue.title')}</PageTitle>
|
||||||
|
<TextBlock>{t('pages.loyalty-queue.subtitle')}</TextBlock>
|
||||||
|
</PageHeader>
|
||||||
|
<TabContainer defaultValue="users">
|
||||||
|
<TabList>
|
||||||
|
<TabButton value="queue">
|
||||||
|
{t('pages.loyalty-queue.queue-tab')}
|
||||||
|
</TabButton>
|
||||||
|
<TabButton value="users">
|
||||||
|
{t('pages.loyalty-queue.users-tab')}
|
||||||
|
</TabButton>
|
||||||
|
</TabList>
|
||||||
|
<TabContent value="queue">
|
||||||
|
<RewardQueue />
|
||||||
|
</TabContent>
|
||||||
|
<TabContent value="users">
|
||||||
|
<UserList />
|
||||||
|
</TabContent>
|
||||||
|
</TabContainer>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,4 +4,5 @@ export * from './forms';
|
||||||
export * from './pages';
|
export * from './pages';
|
||||||
export * from './tabs';
|
export * from './tabs';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
|
export * from './toolbar';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
|
@ -39,6 +39,12 @@ export const { styled, theme } = createStitches({
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
form: '0.3rem',
|
form: '0.3rem',
|
||||||
|
toolbar: '0.5rem',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
media: {
|
||||||
|
mobile: '(min-width: 640px)',
|
||||||
|
medium: '(min-width: 768px)',
|
||||||
|
wide: '(min-width: 1024px)',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
59
frontend/src/ui/theme/toolbar.ts
Normal file
59
frontend/src/ui/theme/toolbar.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
|
||||||
|
import { styled, theme } from './theme';
|
||||||
|
|
||||||
|
export const Toolbar = styled(ToolbarPrimitive.Root, {
|
||||||
|
display: 'flex',
|
||||||
|
padding: '0.4rem',
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
width: '100%',
|
||||||
|
minWidth: 'max-content',
|
||||||
|
borderRadius: theme.borderRadius.toolbar,
|
||||||
|
backgroundColor: '$gray2',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemStyles = {
|
||||||
|
all: 'unset',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
color: '$gray12',
|
||||||
|
padding: '0.6rem 0.8rem',
|
||||||
|
borderRadius: theme.borderRadius.form,
|
||||||
|
display: 'flex',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
lineHeight: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolbarButton = styled(ToolbarPrimitive.Button, {
|
||||||
|
...itemStyles,
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: '$gray4',
|
||||||
|
border: '1px solid $gray6',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ToolbarComboBox = styled('select', {
|
||||||
|
flex: '0 0 auto',
|
||||||
|
color: '$gray12',
|
||||||
|
display: 'inline-flex',
|
||||||
|
lineHeight: 1,
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
margin: 0,
|
||||||
|
fontWeight: '300',
|
||||||
|
border: '1px solid $gray6',
|
||||||
|
padding: '0.5rem 0.25rem',
|
||||||
|
borderRadius: theme.borderRadius.form,
|
||||||
|
backgroundColor: '$gray2',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '$teal7',
|
||||||
|
},
|
||||||
|
'&:focus': {
|
||||||
|
borderColor: '$teal7',
|
||||||
|
backgroundColor: '$gray3',
|
||||||
|
},
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: '$gray4',
|
||||||
|
borderColor: '$gray5',
|
||||||
|
color: '$gray8',
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in a new issue