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:
parent
5a455951b7
commit
8f147c85a9
6 changed files with 184 additions and 19 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
‹
|
||||||
</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
|
›
|
||||||
</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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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: '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Reference in a new issue