1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +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",
"subtitle": "User leaderboard and pending reward redeems",
"queue-tab": "Redeem queue",
"users-tab": "Manage points"
"users-tab": "Manage points",
"username": "Viewer",
"points": "Points",
"username-filter": "Search by username"
}
},
"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>
</ToolbarSection>
<ToolbarSection>
<div style={{ padding: '0 0.25rem' }}>{t('pagination.page')}</div>
{current > min ? (
<ToolbarButton
className="button pagination-link"

View file

@ -3,8 +3,11 @@ 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 { DataTable } from '../components/DataTable';
import {
Button,
Field,
InputBox,
PageContainer,
PageHeader,
PageTitle,
@ -14,6 +17,7 @@ import {
TabList,
TextBlock,
} from '../theme';
import { TableCell, TableRow } from '../theme/table';
interface UserSortingOrder {
key: 'user' | 'points';
@ -36,80 +40,75 @@ function UserList() {
const users = useUserPoints();
const dispatch = useDispatch();
const [entriesPerPage, setEntriesPerPage] = useState(15);
const [page, setPage] = useState(0);
const [config] = useModule(modules.loyaltyConfig);
const [usernameFilter, setUsernameFilter] = useState('');
const [sorting, setSorting] = useState<UserSortingOrder>({
key: 'points',
order: 'desc',
});
const filtered = Object.entries(users ?? [])
.filter(([user]) => user.includes(usernameFilter))
.map(([username, data]) => ({
username,
...data,
}));
type UserEntry = typeof filtered[0];
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' });
type SortFn = (a: UserEntry, b: UserEntry) => number;
const sortfn = (key: keyof UserEntry): SortFn => {
switch (key) {
case 'username': {
return (a, b) => a.username.localeCompare(b.username);
}
case 'points': {
return (a: UserEntry, b: UserEntry) => a.points - b.points;
}
}
};
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)}
<Field size="fullWidth" spacing="none">
<InputBox
placeholder={t('pages.loyalty-queue.username-filter')}
value={usernameFilter}
onChange={(e) => setUsernameFilter(e.target.value)}
/>
</Field>
<DataTable
sort={sortfn}
data={filtered}
columns={[
{
key: 'username',
title: t('pages.loyalty-queue.username'),
sortable: true,
style: {
width: '100%',
textAlign: 'left',
},
},
{
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: {
marginBottom: '1rem',
},
none: {
marginBottom: 0,
},
},
size: {
fullWidth: {
@ -109,6 +112,7 @@ export const MultiButton = styled('div', {
export const Button = styled('button', {
all: 'unset',
cursor: 'pointer',
userSelect: 'none',
color: '$gray12',
fontWeight: '300',
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,
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
};
export const ToolbarButton = styled(ToolbarPrimitive.Button, {