mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-18 01:50:50 +00:00
User point editing let's gooo
This commit is contained in:
parent
5a455951b7
commit
8f147c85a9
6 changed files with 184 additions and 19 deletions
|
@ -68,7 +68,7 @@ interface LoyaltyConfig {
|
|||
banlist: string[];
|
||||
}
|
||||
|
||||
interface LoyaltyPointsEntry {
|
||||
export interface LoyaltyPointsEntry {
|
||||
points: number;
|
||||
}
|
||||
|
||||
|
@ -100,6 +100,7 @@ export interface LoyaltyRedeem {
|
|||
display_name: string;
|
||||
when: Date;
|
||||
reward: LoyaltyReward;
|
||||
request_text: string;
|
||||
}
|
||||
|
||||
export interface APIState {
|
||||
|
|
|
@ -4,6 +4,8 @@ export interface PageListProps {
|
|||
current: number;
|
||||
max: number;
|
||||
min: number;
|
||||
itemsPerPage: number;
|
||||
onSelectChange: (itemsPerPage: number) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
|
@ -11,11 +13,13 @@ export default function PageList({
|
|||
current,
|
||||
max,
|
||||
min,
|
||||
itemsPerPage,
|
||||
onSelectChange,
|
||||
onPageChange,
|
||||
}: PageListProps): React.ReactElement {
|
||||
return (
|
||||
<nav
|
||||
className="pagination is-centered is-small"
|
||||
className="pagination is-small"
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
>
|
||||
|
@ -24,15 +28,25 @@ export default function PageList({
|
|||
disabled={current <= min}
|
||||
onClick={() => onPageChange(current - 1)}
|
||||
>
|
||||
Previous
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
className="button pagination-next"
|
||||
disabled={current >= max}
|
||||
onClick={() => onPageChange(current + 1)}
|
||||
>
|
||||
Next page
|
||||
›
|
||||
</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">
|
||||
{current > min ? (
|
||||
<li>
|
||||
|
|
|
@ -21,11 +21,9 @@ export default function TabbedView({
|
|||
{tabs.map(({ route, name }) => (
|
||||
<li key={route}>
|
||||
<Link
|
||||
getProps={({ isCurrent }) => {
|
||||
return {
|
||||
className: isCurrent ? 'is-active' : '',
|
||||
};
|
||||
}}
|
||||
getProps={({ isCurrent }) => ({
|
||||
className: isCurrent ? 'is-active' : '',
|
||||
})}
|
||||
to={route}
|
||||
>
|
||||
{name}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { useModule, useUserPoints } from '../../../lib/react-utils';
|
||||
import {
|
||||
LoyaltyRedeem,
|
||||
modules,
|
||||
|
@ -21,6 +21,9 @@ export default function LoyaltyRedeemQueuePage(
|
|||
): React.ReactElement {
|
||||
const [redemptions] = useModule(modules.loyaltyRedeemQueue);
|
||||
|
||||
// Big hack but this is required or refunds break
|
||||
useUserPoints();
|
||||
|
||||
const [sorting, setSorting] = useState<SortingOrder>({
|
||||
key: 'when',
|
||||
order: 'desc',
|
||||
|
@ -120,6 +123,8 @@ export default function LoyaltyRedeemQueuePage(
|
|||
current={page + 1}
|
||||
min={1}
|
||||
max={totalPages + 1}
|
||||
itemsPerPage={entriesPerPage}
|
||||
onSelectChange={(em) => setEntriesPerPage(em)}
|
||||
onPageChange={(p) => setPage(p - 1)}
|
||||
/>
|
||||
<table className="table is-striped is-fullwidth">
|
||||
|
@ -146,6 +151,7 @@ export default function LoyaltyRedeemQueuePage(
|
|||
</span>
|
||||
</th>
|
||||
<th>Reward name</th>
|
||||
<th>Request</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -160,6 +166,7 @@ export default function LoyaltyRedeemQueuePage(
|
|||
{redemption.display_name} ({redemption.username})
|
||||
</td>
|
||||
<td>{redemption.reward.name}</td>
|
||||
<td>{redemption.request_text}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<a onClick={() => acceptRedeem(redemption)}>Accept</a>
|
||||
{redemption.username !== '@PLATFORM' ? (
|
||||
|
@ -177,6 +184,8 @@ export default function LoyaltyRedeemQueuePage(
|
|||
current={page + 1}
|
||||
min={1}
|
||||
max={totalPages + 1}
|
||||
itemsPerPage={entriesPerPage}
|
||||
onSelectChange={(em) => setEntriesPerPage(em)}
|
||||
onPageChange={(p) => setPage(p - 1)}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -374,6 +374,7 @@ export default function LoyaltyRewardsPage(
|
|||
display_name: 'me :3',
|
||||
when: new Date(),
|
||||
reward,
|
||||
request_text: '',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,116 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
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 {
|
||||
key: 'user' | 'points';
|
||||
|
@ -11,7 +120,10 @@ export default function LoyaltyUserListPage(
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
props: RouteComponentProps<unknown>,
|
||||
): React.ReactElement {
|
||||
const [loyaltyConfig] = useModule(modules.loyaltyConfig);
|
||||
const currency = loyaltyConfig?.currency ?? 'points';
|
||||
const users = useUserPoints();
|
||||
const dispatch = useDispatch();
|
||||
const [sorting, setSorting] = useState<SortingOrder>({
|
||||
key: 'points',
|
||||
order: 'desc',
|
||||
|
@ -20,6 +132,7 @@ export default function LoyaltyUserListPage(
|
|||
const [entriesPerPage, setEntriesPerPage] = useState(15);
|
||||
const [page, setPage] = useState(0);
|
||||
const [usernameFilter, setUsernameFilter] = useState('');
|
||||
const [editModal, setEditModal] = useState<UserData>(null);
|
||||
|
||||
const changeSort = (key: 'user' | 'points') => {
|
||||
if (sorting.key === key) {
|
||||
|
@ -59,15 +172,28 @@ export default function LoyaltyUserListPage(
|
|||
// unreacheable
|
||||
}
|
||||
|
||||
const paged = sortedEntries.slice(
|
||||
page * entriesPerPage,
|
||||
(page + 1) * entriesPerPage,
|
||||
);
|
||||
const offset = page * entriesPerPage;
|
||||
const paged = sortedEntries.slice(offset, offset + entriesPerPage);
|
||||
const totalPages = Math.floor(sortedEntries.length / entriesPerPage);
|
||||
|
||||
const modifyUser = ({ entry, user }: UserData) => {
|
||||
dispatch(setUserPoints({ user, points: entry.points, relative: false }));
|
||||
setEditModal(null);
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<>
|
||||
<div className="field">
|
||||
|
@ -85,6 +211,8 @@ export default function LoyaltyUserListPage(
|
|||
current={page + 1}
|
||||
min={1}
|
||||
max={totalPages + 1}
|
||||
itemsPerPage={entriesPerPage}
|
||||
onSelectChange={(em) => setEntriesPerPage(em)}
|
||||
onPageChange={(p) => setPage(p - 1)}
|
||||
/>
|
||||
<table className="table is-striped is-fullwidth">
|
||||
|
@ -104,8 +232,9 @@ export default function LoyaltyUserListPage(
|
|||
<span
|
||||
className="sortable"
|
||||
onClick={() => changeSort('points')}
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
Points
|
||||
{currency}
|
||||
{sorting.key === 'points' ? (
|
||||
<span className="sort-icon">
|
||||
{sorting.order === 'asc' ? '▴' : '▾'}
|
||||
|
@ -113,7 +242,7 @@ export default function LoyaltyUserListPage(
|
|||
) : null}
|
||||
</span>
|
||||
</th>
|
||||
<th></th>
|
||||
<th style={{ width: '10%' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
@ -122,7 +251,18 @@ export default function LoyaltyUserListPage(
|
|||
<tr key={user}>
|
||||
<td>{user}</td>
|
||||
<td>{p.points}</td>
|
||||
<td></td>
|
||||
<td style={{ textAlign: 'right', paddingRight: '1rem' }}>
|
||||
<a
|
||||
onClick={() =>
|
||||
setEditModal({
|
||||
user,
|
||||
entry: p,
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -131,6 +271,8 @@ export default function LoyaltyUserListPage(
|
|||
current={page + 1}
|
||||
min={1}
|
||||
max={totalPages + 1}
|
||||
itemsPerPage={entriesPerPage}
|
||||
onSelectChange={(em) => setEntriesPerPage(em)}
|
||||
onPageChange={(p) => setPage(p - 1)}
|
||||
/>
|
||||
</>
|
||||
|
|
Loading…
Reference in a new issue