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

User point editing let's gooo

This commit is contained in:
Ash Keel 2021-05-18 13:30:08 +02:00
parent 5a455951b7
commit 8f147c85a9
No known key found for this signature in database
GPG key ID: CF2CC050478BD7E5
6 changed files with 184 additions and 19 deletions

View file

@ -68,7 +68,7 @@ interface LoyaltyConfig {
banlist: string[]; banlist: string[];
} }
interface LoyaltyPointsEntry { export interface LoyaltyPointsEntry {
points: number; points: number;
} }
@ -100,6 +100,7 @@ export interface LoyaltyRedeem {
display_name: string; display_name: string;
when: Date; when: Date;
reward: LoyaltyReward; reward: LoyaltyReward;
request_text: string;
} }
export interface APIState { export interface APIState {

View file

@ -4,6 +4,8 @@ export interface PageListProps {
current: number; current: number;
max: number; max: number;
min: number; min: number;
itemsPerPage: number;
onSelectChange: (itemsPerPage: number) => void;
onPageChange: (page: number) => void; onPageChange: (page: number) => void;
} }
@ -11,11 +13,13 @@ export default function PageList({
current, current,
max, max,
min, min,
itemsPerPage,
onSelectChange,
onPageChange, onPageChange,
}: PageListProps): React.ReactElement { }: PageListProps): React.ReactElement {
return ( return (
<nav <nav
className="pagination is-centered is-small" className="pagination is-small"
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
> >
@ -24,15 +28,25 @@ export default function PageList({
disabled={current <= min} disabled={current <= min}
onClick={() => onPageChange(current - 1)} onClick={() => onPageChange(current - 1)}
> >
Previous &lsaquo;
</button> </button>
<button <button
className="button pagination-next" className="button pagination-next"
disabled={current >= max} disabled={current >= max}
onClick={() => onPageChange(current + 1)} onClick={() => onPageChange(current + 1)}
> >
Next page &rsaquo;
</button> </button>
<select
className="pagination-next"
value={itemsPerPage}
onChange={(ev) => onSelectChange(Number(ev.target.value))}
>
<option value={15}>15</option>
<option value={30}>30</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
<ul className="pagination-list"> <ul className="pagination-list">
{current > min ? ( {current > min ? (
<li> <li>

View file

@ -21,11 +21,9 @@ export default function TabbedView({
{tabs.map(({ route, name }) => ( {tabs.map(({ route, name }) => (
<li key={route}> <li key={route}>
<Link <Link
getProps={({ isCurrent }) => { getProps={({ isCurrent }) => ({
return {
className: isCurrent ? 'is-active' : '', className: isCurrent ? 'is-active' : '',
}; })}
}}
to={route} to={route}
> >
{name} {name}

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import { useModule } from '../../../lib/react-utils'; import { useModule, useUserPoints } from '../../../lib/react-utils';
import { import {
LoyaltyRedeem, LoyaltyRedeem,
modules, modules,
@ -21,6 +21,9 @@ export default function LoyaltyRedeemQueuePage(
): React.ReactElement { ): React.ReactElement {
const [redemptions] = useModule(modules.loyaltyRedeemQueue); const [redemptions] = useModule(modules.loyaltyRedeemQueue);
// Big hack but this is required or refunds break
useUserPoints();
const [sorting, setSorting] = useState<SortingOrder>({ const [sorting, setSorting] = useState<SortingOrder>({
key: 'when', key: 'when',
order: 'desc', order: 'desc',
@ -120,6 +123,8 @@ export default function LoyaltyRedeemQueuePage(
current={page + 1} current={page + 1}
min={1} min={1}
max={totalPages + 1} max={totalPages + 1}
itemsPerPage={entriesPerPage}
onSelectChange={(em) => setEntriesPerPage(em)}
onPageChange={(p) => setPage(p - 1)} onPageChange={(p) => setPage(p - 1)}
/> />
<table className="table is-striped is-fullwidth"> <table className="table is-striped is-fullwidth">
@ -146,6 +151,7 @@ export default function LoyaltyRedeemQueuePage(
</span> </span>
</th> </th>
<th>Reward name</th> <th>Reward name</th>
<th>Request</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -160,6 +166,7 @@ export default function LoyaltyRedeemQueuePage(
{redemption.display_name} ({redemption.username}) {redemption.display_name} ({redemption.username})
</td> </td>
<td>{redemption.reward.name}</td> <td>{redemption.reward.name}</td>
<td>{redemption.request_text}</td>
<td style={{ textAlign: 'right' }}> <td style={{ textAlign: 'right' }}>
<a onClick={() => acceptRedeem(redemption)}>Accept</a> <a onClick={() => acceptRedeem(redemption)}>Accept</a>
{redemption.username !== '@PLATFORM' ? ( {redemption.username !== '@PLATFORM' ? (
@ -177,6 +184,8 @@ export default function LoyaltyRedeemQueuePage(
current={page + 1} current={page + 1}
min={1} min={1}
max={totalPages + 1} max={totalPages + 1}
itemsPerPage={entriesPerPage}
onSelectChange={(em) => setEntriesPerPage(em)}
onPageChange={(p) => setPage(p - 1)} onPageChange={(p) => setPage(p - 1)}
/> />
</> </>

View file

@ -374,6 +374,7 @@ export default function LoyaltyRewardsPage(
display_name: 'me :3', display_name: 'me :3',
when: new Date(), when: new Date(),
reward, reward,
request_text: '',
}), }),
); );
}; };

View file

@ -1,7 +1,116 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import PageList from '../../components/PageList'; import PageList from '../../components/PageList';
import { useUserPoints } from '../../../lib/react-utils'; import { useModule, useUserPoints } from '../../../lib/react-utils';
import { RootState } from '../../../store';
import {
LoyaltyPointsEntry,
modules,
setUserPoints,
} from '../../../store/api/reducer';
import Modal from '../../components/Modal';
interface UserData {
user: string;
entry: LoyaltyPointsEntry;
}
interface UserModalProps {
active: boolean;
onConfirm: (r: UserData) => void;
onClose: () => void;
initialData?: UserData;
title: string;
confirmText: string;
}
function UserModal({
active,
onConfirm,
onClose,
initialData,
title,
confirmText,
}: UserModalProps) {
const currency = useSelector(
(state: RootState) =>
state.api.moduleConfigs?.loyaltyConfig?.currency ?? 'points',
);
const [user, setUser] = useState(initialData.user);
const [entry, setEntry] = useState(initialData.entry);
const userEditable = initialData.user !== '';
const nameValid = user !== '';
const pointsValid = Number.isFinite(entry.points);
const validForm = nameValid && pointsValid;
const confirm = () => {
if (onConfirm) {
onConfirm({
user,
entry,
});
}
};
return (
<Modal
active={active}
title={title}
showCancel={true}
bgDismiss={true}
confirmName={confirmText}
confirmClass="is-success"
confirmEnabled={validForm}
onConfirm={() => confirm()}
onClose={() => onClose()}
>
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label">Username</label>
</div>
<div className="field-body">
<div className="field">
<p className="control">
<input
disabled={!active || !userEditable}
className={!nameValid ? 'input is-danger' : 'input'}
type="text"
placeholder="Username"
value={user ?? ''}
onChange={(ev) => setUser(ev.target.value)}
/>
</p>
</div>
</div>
</div>
<div className="field is-horizontal">
<div className="field-label is-normal">
<label className="label" style={{ textTransform: 'capitalize' }}>
{currency}
</label>
</div>
<div className="field-body">
<div className="field has-addons">
<p className="control">
<input
className="input"
type="number"
placeholder="#"
value={entry.points ?? ''}
onChange={(ev) =>
setEntry({ ...entry, points: parseInt(ev.target.value, 10) })
}
/>
</p>
</div>
</div>
</div>
</Modal>
);
}
interface SortingOrder { interface SortingOrder {
key: 'user' | 'points'; key: 'user' | 'points';
@ -11,7 +120,10 @@ export default function LoyaltyUserListPage(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
props: RouteComponentProps<unknown>, props: RouteComponentProps<unknown>,
): React.ReactElement { ): React.ReactElement {
const [loyaltyConfig] = useModule(modules.loyaltyConfig);
const currency = loyaltyConfig?.currency ?? 'points';
const users = useUserPoints(); const users = useUserPoints();
const dispatch = useDispatch();
const [sorting, setSorting] = useState<SortingOrder>({ const [sorting, setSorting] = useState<SortingOrder>({
key: 'points', key: 'points',
order: 'desc', order: 'desc',
@ -20,6 +132,7 @@ export default function LoyaltyUserListPage(
const [entriesPerPage, setEntriesPerPage] = useState(15); const [entriesPerPage, setEntriesPerPage] = useState(15);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [usernameFilter, setUsernameFilter] = useState(''); const [usernameFilter, setUsernameFilter] = useState('');
const [editModal, setEditModal] = useState<UserData>(null);
const changeSort = (key: 'user' | 'points') => { const changeSort = (key: 'user' | 'points') => {
if (sorting.key === key) { if (sorting.key === key) {
@ -59,15 +172,28 @@ export default function LoyaltyUserListPage(
// unreacheable // unreacheable
} }
const paged = sortedEntries.slice( const offset = page * entriesPerPage;
page * entriesPerPage, const paged = sortedEntries.slice(offset, offset + entriesPerPage);
(page + 1) * entriesPerPage,
);
const totalPages = Math.floor(sortedEntries.length / entriesPerPage); const totalPages = Math.floor(sortedEntries.length / entriesPerPage);
const modifyUser = ({ entry, user }: UserData) => {
dispatch(setUserPoints({ user, points: entry.points, relative: false }));
setEditModal(null);
};
return ( return (
<> <>
<h1 className="title is-4">All viewers with points</h1> {editModal ? (
<UserModal
title="Modify balance"
confirmText="Edit"
active={true}
onConfirm={(entry) => modifyUser(entry)}
initialData={editModal}
onClose={() => setEditModal(null)}
/>
) : null}
<h1 className="title is-4">All viewers with {currency}</h1>
{users ? ( {users ? (
<> <>
<div className="field"> <div className="field">
@ -85,6 +211,8 @@ export default function LoyaltyUserListPage(
current={page + 1} current={page + 1}
min={1} min={1}
max={totalPages + 1} max={totalPages + 1}
itemsPerPage={entriesPerPage}
onSelectChange={(em) => setEntriesPerPage(em)}
onPageChange={(p) => setPage(p - 1)} onPageChange={(p) => setPage(p - 1)}
/> />
<table className="table is-striped is-fullwidth"> <table className="table is-striped is-fullwidth">
@ -104,8 +232,9 @@ export default function LoyaltyUserListPage(
<span <span
className="sortable" className="sortable"
onClick={() => changeSort('points')} onClick={() => changeSort('points')}
style={{ textTransform: 'capitalize' }}
> >
Points {currency}
{sorting.key === 'points' ? ( {sorting.key === 'points' ? (
<span className="sort-icon"> <span className="sort-icon">
{sorting.order === 'asc' ? '▴' : '▾'} {sorting.order === 'asc' ? '▴' : '▾'}
@ -113,7 +242,7 @@ export default function LoyaltyUserListPage(
) : null} ) : null}
</span> </span>
</th> </th>
<th></th> <th style={{ width: '10%' }}></th>
</tr> </tr>
</thead> </thead>
@ -122,7 +251,18 @@ export default function LoyaltyUserListPage(
<tr key={user}> <tr key={user}>
<td>{user}</td> <td>{user}</td>
<td>{p.points}</td> <td>{p.points}</td>
<td></td> <td style={{ textAlign: 'right', paddingRight: '1rem' }}>
<a
onClick={() =>
setEditModal({
user,
entry: p,
})
}
>
Edit
</a>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -131,6 +271,8 @@ export default function LoyaltyUserListPage(
current={page + 1} current={page + 1}
min={1} min={1}
max={totalPages + 1} max={totalPages + 1}
itemsPerPage={entriesPerPage}
onSelectChange={(em) => setEntriesPerPage(em)}
onPageChange={(p) => setPage(p - 1)} onPageChange={(p) => setPage(p - 1)}
/> />
</> </>