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": {
|
||||
"version": "0.1.2",
|
||||
"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": {
|
||||
"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",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@radix-ui/react-icons": "^1.0.3",
|
||||
"@radix-ui/react-label": "^0.1.3",
|
||||
"@radix-ui/react-tabs": "^0.1.4",
|
||||
"@radix-ui/react-toolbar": "^0.1.4",
|
||||
"@reduxjs/toolkit": "^1.5.1",
|
||||
"@stitches/react": "^1.2.6",
|
||||
"@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.",
|
||||
"every": "every",
|
||||
"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": {
|
||||
|
@ -199,5 +205,15 @@
|
|||
"hours": "hours",
|
||||
"minutes": "minutes",
|
||||
"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 ChatAlertsPage from './pages/ChatAlerts';
|
||||
import LoyaltyConfigPage from './pages/LoyaltyConfig';
|
||||
import LoyaltyQueuePage from './pages/LoyaltyQueue';
|
||||
|
||||
const LoadingDiv = styled('div', {
|
||||
display: 'flex',
|
||||
|
@ -192,6 +193,7 @@ export default function App(): JSX.Element {
|
|||
/>
|
||||
<Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} />
|
||||
<Route path="/loyalty/settings" element={<LoyaltyConfigPage />} />
|
||||
<Route path="/loyalty/users" element={<LoyaltyQueuePage />} />
|
||||
</Routes>
|
||||
</PageWrapper>
|
||||
</PageContent>
|
||||
|
|
|
@ -57,6 +57,7 @@ function Interval({
|
|||
type="number"
|
||||
border="none"
|
||||
required={required}
|
||||
disabled={!active}
|
||||
css={{
|
||||
maxWidth: '5rem',
|
||||
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: {
|
||||
enabled: {},
|
||||
disabled: {
|
||||
borderLeftColor: '$red7',
|
||||
borderLeftColor: '$red6',
|
||||
backgroundColor: '$gray3',
|
||||
color: '$gray10',
|
||||
},
|
||||
|
@ -68,7 +68,7 @@ const CommandName = styled('span', {
|
|||
status: {
|
||||
enabled: {},
|
||||
disabled: {
|
||||
color: '$gray10',
|
||||
color: '$gray9',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -56,7 +56,6 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
|||
</FlexRow>
|
||||
</Field>
|
||||
</PageHeader>
|
||||
{active && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -75,7 +74,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
|||
id="currency"
|
||||
placeholder={t('pages.loyalty-settings.currency-placeholder')}
|
||||
value={config?.currency ?? ''}
|
||||
disabled={busy}
|
||||
disabled={!active || busy}
|
||||
required={true}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
|
@ -106,7 +105,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
|||
placeholder={'0'}
|
||||
css={{ maxWidth: '5rem' }}
|
||||
value={config?.points?.amount ?? '0'}
|
||||
disabled={busy}
|
||||
disabled={!active || busy}
|
||||
required={true}
|
||||
onChange={(e) => {
|
||||
const intNum = parseInt(e.target.value, 10);
|
||||
|
@ -139,7 +138,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
|||
}),
|
||||
);
|
||||
}}
|
||||
active={!busy}
|
||||
active={active && !busy}
|
||||
min={5}
|
||||
required={true}
|
||||
/>
|
||||
|
@ -155,7 +154,7 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
|||
id="bonus"
|
||||
placeholder={'0'}
|
||||
value={config?.points?.activity_bonus ?? '0'}
|
||||
disabled={busy}
|
||||
disabled={!active || busy}
|
||||
required={true}
|
||||
onChange={(e) => {
|
||||
const intNum = parseInt(e.target.value, 10);
|
||||
|
@ -173,14 +172,11 @@ export default function LoyaltySettingsPage(): React.ReactElement {
|
|||
);
|
||||
}}
|
||||
/>
|
||||
<FieldNote>
|
||||
{t('pages.loyalty-settings.bonus-points-hint')}
|
||||
</FieldNote>
|
||||
<FieldNote>{t('pages.loyalty-settings.bonus-points-hint')}</FieldNote>
|
||||
</Field>
|
||||
|
||||
<SaveButton type="submit" status={status} />
|
||||
</form>
|
||||
)}
|
||||
</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 './tabs';
|
||||
export * from './theme';
|
||||
export * from './toolbar';
|
||||
export * from './utils';
|
||||
|
|
|
@ -39,6 +39,12 @@ export const { styled, theme } = createStitches({
|
|||
},
|
||||
borderRadius: {
|
||||
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