1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-20 02:00:49 +00:00

Table data!

This commit is contained in:
Ash Keel 2022-01-14 01:41:23 +01:00
parent 3ce419ad9c
commit 01e8f96e6e
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
7 changed files with 240 additions and 69 deletions

View file

@ -179,7 +179,10 @@
"title": "Points and redeems", "title": "Points and redeems",
"subtitle": "User leaderboard and pending reward redeems", "subtitle": "User leaderboard and pending reward redeems",
"queue-tab": "Redeem queue", "queue-tab": "Redeem queue",
"users-tab": "Manage points" "users-tab": "Manage points",
"username": "Viewer",
"points": "Points",
"username-filter": "Search by username"
} }
}, },
"form-actions": { "form-actions": {

View file

@ -0,0 +1,126 @@
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import React, { useState } from 'react';
import { styled } from '../theme';
import { Table, TableHeader } from '../theme/table';
import PageList from './PageList';
interface SortingOrder<T> {
key: keyof T;
order: 'asc' | 'desc';
}
export interface DataTableProps<T> {
data: T[];
columns: ({
title: string;
} & React.HTMLAttributes<HTMLTableCellElement> &
(
| {
sortable: true;
key: Extract<keyof T, string>;
}
| {
sortable: false;
key: string;
}
))[];
defaultSort: SortingOrder<T>;
view: (data: T) => React.ReactNode;
sort: (key: keyof T) => (a: T, b: T) => number;
}
const Sortable = styled('div', {
display: 'flex',
gap: '0.3rem',
alignItems: 'center',
cursor: 'pointer',
});
/**
* DataTable is a component that displays a list of data in a table format with sorting and pagination.
*/
export function DataTable<T>({
data,
columns,
defaultSort,
sort,
view,
}: DataTableProps<T>): React.ReactElement {
const [entriesPerPage, setEntriesPerPage] = useState(15);
const [page, setPage] = useState(0);
const [sorting, setSorting] = useState<SortingOrder<T>>(defaultSort);
const changeSort = (key: keyof T) => {
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 sortedEntries = data.slice(0);
const sortFn = sort(sorting.key);
if (sortFn) {
sortedEntries.sort((a, b) => {
const result = sortFn(a, b);
switch (sorting.order) {
case 'asc':
return result;
case 'desc':
return -result;
}
});
}
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)}
/>
<Table>
<thead>
{columns.map((props) => (
<TableHeader {...props} key={props.key}>
{props.sortable ? (
<Sortable onClick={() => changeSort(props.key)}>
{props.title}
{sorting.key === props.key &&
(sorting.order === 'asc' ? (
<ChevronUpIcon />
) : (
<ChevronDownIcon />
))}
</Sortable>
) : (
props.title
)}
</TableHeader>
))}
</thead>
<tbody>{paged.map(view)}</tbody>
</Table>
<PageList
current={page + 1}
min={1}
max={totalPages + 1}
itemsPerPage={entriesPerPage}
onSelectChange={(em) => setEntriesPerPage(em)}
onPageChange={(p) => setPage(p - 1)}
/>
</>
);
}

View file

@ -86,6 +86,7 @@ function PageList({
</ToolbarComboBox> </ToolbarComboBox>
</ToolbarSection> </ToolbarSection>
<ToolbarSection> <ToolbarSection>
<div style={{ padding: '0 0.25rem' }}>{t('pagination.page')}</div>
{current > min ? ( {current > min ? (
<ToolbarButton <ToolbarButton
className="button pagination-link" className="button pagination-link"

View file

@ -3,8 +3,11 @@ import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useModule, useUserPoints } from '../../lib/react-utils'; import { useModule, useUserPoints } from '../../lib/react-utils';
import { modules } from '../../store/api/reducer'; import { modules } from '../../store/api/reducer';
import PageList from '../components/PageList'; import { DataTable } from '../components/DataTable';
import { import {
Button,
Field,
InputBox,
PageContainer, PageContainer,
PageHeader, PageHeader,
PageTitle, PageTitle,
@ -14,6 +17,7 @@ import {
TabList, TabList,
TextBlock, TextBlock,
} from '../theme'; } from '../theme';
import { TableCell, TableRow } from '../theme/table';
interface UserSortingOrder { interface UserSortingOrder {
key: 'user' | 'points'; key: 'user' | 'points';
@ -36,80 +40,75 @@ function UserList() {
const users = useUserPoints(); const users = useUserPoints();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [entriesPerPage, setEntriesPerPage] = useState(15); const [config] = useModule(modules.loyaltyConfig);
const [page, setPage] = useState(0);
const [usernameFilter, setUsernameFilter] = useState(''); const [usernameFilter, setUsernameFilter] = useState('');
const [sorting, setSorting] = useState<UserSortingOrder>({ const filtered = Object.entries(users ?? [])
key: 'points', .filter(([user]) => user.includes(usernameFilter))
order: 'desc', .map(([username, data]) => ({
}); username,
...data,
}));
type UserEntry = typeof filtered[0];
const changeSort = (key: 'user' | 'points') => { type SortFn = (a: UserEntry, b: UserEntry) => number;
if (sorting.key === key) {
// Same key, swap sorting order const sortfn = (key: keyof UserEntry): SortFn => {
setSorting({ switch (key) {
...sorting, case 'username': {
order: sorting.order === 'asc' ? 'desc' : 'asc', return (a, b) => a.username.localeCompare(b.username);
}); }
} else { case 'points': {
// Different key, change to sort that key return (a: UserEntry, b: UserEntry) => a.points - b.points;
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 ( return (
<> <>
<PageList <Field size="fullWidth" spacing="none">
current={page + 1} <InputBox
min={1} placeholder={t('pages.loyalty-queue.username-filter')}
max={totalPages + 1} value={usernameFilter}
itemsPerPage={entriesPerPage} onChange={(e) => setUsernameFilter(e.target.value)}
onSelectChange={(em) => setEntriesPerPage(em)} />
onPageChange={(p) => setPage(p - 1)} </Field>
/> <DataTable
sort={sortfn}
{paged.map(([user, { points }]) => ( data={filtered}
<article key={user}> columns={[
{user} - {points} {
</article> key: 'username',
))} title: t('pages.loyalty-queue.username'),
sortable: true,
<PageList style: {
current={page + 1} width: '100%',
min={1} textAlign: 'left',
max={totalPages + 1} },
itemsPerPage={entriesPerPage} },
onSelectChange={(em) => setEntriesPerPage(em)} {
onPageChange={(p) => setPage(p - 1)} key: 'points',
title: config?.currency || t('pages.loyalty-queue.points'),
sortable: true,
style: {
textTransform: 'capitalize',
},
},
{
key: 'actions',
title: '',
sortable: false,
},
]}
defaultSort={{ key: 'points', order: 'desc' }}
view={({ username, points }) => (
<TableRow key="username">
<TableCell css={{ width: '100%' }}>{username}</TableCell>
<TableCell>{points}</TableCell>
<TableCell>
<Button size="small">Edit</Button>
</TableCell>
</TableRow>
)}
/> />
</> </>
); );

View file

@ -15,6 +15,9 @@ export const Field = styled('fieldset', {
narrow: { narrow: {
marginBottom: '1rem', marginBottom: '1rem',
}, },
none: {
marginBottom: 0,
},
}, },
size: { size: {
fullWidth: { fullWidth: {
@ -109,6 +112,7 @@ export const MultiButton = styled('div', {
export const Button = styled('button', { export const Button = styled('button', {
all: 'unset', all: 'unset',
cursor: 'pointer', cursor: 'pointer',
userSelect: 'none',
color: '$gray12', color: '$gray12',
fontWeight: '300', fontWeight: '300',
padding: '0.5rem 1rem', padding: '0.5rem 1rem',

View file

@ -0,0 +1,37 @@
import { theme, styled } from './theme';
export const Table = styled('table', {
border: '1px solid $gray3',
borderCollapse: 'collapse',
});
export const TableRow = styled('tr', {
all: 'unset',
display: 'table-row',
padding: '0.5rem',
verticalAlign: 'middle',
textAlign: 'left',
backgroundColor: '$gray1',
'&:nth-child(even)': {
backgroundColor: '$gray2',
},
});
export const TableHeader = styled('th', {
all: 'unset',
display: 'table-cell',
padding: '0.25rem 0.5rem',
height: '2rem',
verticalAlign: 'middle',
textAlign: 'left',
backgroundColor: '$gray3',
fontWeight: 'bold',
});
export const TableCell = styled('td', {
all: 'unset',
display: 'table-cell',
padding: '0.25rem 0.5rem',
verticalAlign: 'middle',
textAlign: 'left',
});

View file

@ -23,6 +23,7 @@ const itemStyles = {
lineHeight: 1, lineHeight: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
userSelect: 'none',
}; };
export const ToolbarButton = styled(ToolbarPrimitive.Button, { export const ToolbarButton = styled(ToolbarPrimitive.Button, {