mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
i18n is done hooray!
This commit is contained in:
parent
5bcb55f0a2
commit
de7c23f14b
8 changed files with 236 additions and 119 deletions
|
@ -18,7 +18,11 @@
|
|||
"system": {
|
||||
"menu-header": "Navigation",
|
||||
"loading": "Loading…",
|
||||
"connection-lost": "Connection to server was lost, retrying..."
|
||||
"connection-lost": "Connection to server was lost, retrying...",
|
||||
"pagination": {
|
||||
"page": "Page {{page}}",
|
||||
"gotopage": "Go to page {{page}}"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"read-key": "Read key",
|
||||
|
@ -28,7 +32,14 @@
|
|||
"fix-json-btn": "Fix JSON"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"create": "Create",
|
||||
"cancel": "Cancel",
|
||||
"ok": "OK"
|
||||
},
|
||||
"http": {
|
||||
"header": "Web server configuration",
|
||||
|
@ -43,8 +54,90 @@
|
|||
"enable": "Enable back-end (stulbe) integration",
|
||||
"endpoint": "Stulbe Endpoint",
|
||||
"username": "User name",
|
||||
"username-placeholder": "myUserName",
|
||||
"authkey": "Authorization key",
|
||||
"authkey-placeholder": "key goes here",
|
||||
"username-placeholder": "myUserName"
|
||||
"authkey-placeholder": "key goes here"
|
||||
},
|
||||
"loyalty": {
|
||||
"goals": {
|
||||
"reached": "Reached!",
|
||||
"contributors": "Contributors:",
|
||||
"no-contributors": "No one has contributed yet :(",
|
||||
"id": "Goal ID",
|
||||
"id-placeholder": "goal_id_here",
|
||||
"id-help": "Choose a simple name that can be referenced by other software. It will be auto-generated from the goal name if you leave it blank.",
|
||||
"err-goalid-dup": "There is already a goal with this ID! Please choose a different one.",
|
||||
"name": "Name",
|
||||
"name-placeholder": "My dream goal",
|
||||
"icon": "Icon",
|
||||
"icon-placeholder": "Image URL",
|
||||
"description": "Description",
|
||||
"description-placeholder": "What's gonna happen when we reach this goal?",
|
||||
"header": "Community goals",
|
||||
"new": "New goal",
|
||||
"search": "Search by name",
|
||||
"modify": "Modify goal"
|
||||
},
|
||||
"points": "Points",
|
||||
"points-fallback": "points",
|
||||
"queue": {
|
||||
"header": "Redemption queue",
|
||||
"search": "Search by username",
|
||||
"reward-name": "Reward name",
|
||||
"request": "Request",
|
||||
"accept": "Accept",
|
||||
"refund": "Refund",
|
||||
"err-not-available": "Redemption queue is not available (loyalty disabled or no one has redeemed anything yet)"
|
||||
},
|
||||
"rewards": {
|
||||
"test": "Test",
|
||||
"cooldown": "Cooldown",
|
||||
"required-info": "Required info",
|
||||
"id": "Reward ID",
|
||||
"id-placeholder": "reward_id_here",
|
||||
"err-rewid-dup": "There is already a reward with this ID! Please choose a different one.",
|
||||
"id-help": "Choose a simple name that can be referenced by other software. It will be auto-generated from the reward name if you leave it blank.",
|
||||
"name": "Name",
|
||||
"name-placeholder": "My awesome reward",
|
||||
"icon": "Icon",
|
||||
"icon-placeholder": "Image URL",
|
||||
"description": "Description",
|
||||
"description-placeholder": "What's so cool about this reward?",
|
||||
"cost": "Cost",
|
||||
"requires-extra-info": "Requires viewer-specified details",
|
||||
"extra-info": "Required info",
|
||||
"extra-info-placeholder": "What extra detail to ask the viewer for",
|
||||
"header": "Loyalty rewards",
|
||||
"new-reward": "New reward",
|
||||
"search": "Search by name",
|
||||
"modify-reward": "Modify reward"
|
||||
},
|
||||
"config": {
|
||||
"header": "Loyalty system configuration",
|
||||
"enable": "Enable loyalty points",
|
||||
"err-twitchbot-disabled": "(Twitch bot must be enabled for this!)",
|
||||
"currency-name": "Currency name",
|
||||
"bonus-points": "Bonus points for active users",
|
||||
"points-every": "every",
|
||||
"give-points": "Give",
|
||||
"point-reward-frequency": "How often to give {{points}}"
|
||||
},
|
||||
"userlist": {
|
||||
"give-points": "Give points to user",
|
||||
"modify-balance": "Modify balance",
|
||||
"give-button": "Give",
|
||||
"userlist-header": "All viewers with {{currency}}",
|
||||
"err-not-available": "Viewer list is not available (loyalty disabled or no one has points)"
|
||||
}
|
||||
},
|
||||
"form-common": {
|
||||
"required": "Required",
|
||||
"date": "Date",
|
||||
"username": "Username",
|
||||
"time": {
|
||||
"seconds": "seconds",
|
||||
"minutes": "minutes",
|
||||
"hours": "hours"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface ModalProps {
|
||||
title: string;
|
||||
|
@ -28,6 +29,7 @@ function Modal({
|
|||
bgDismiss,
|
||||
children,
|
||||
}: React.PropsWithChildren<ModalProps>): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={`modal ${active ? 'is-active' : ''}`}>
|
||||
<div
|
||||
|
@ -52,14 +54,14 @@ function Modal({
|
|||
disabled={!confirmEnabled}
|
||||
onClick={() => onConfirm()}
|
||||
>
|
||||
{confirmName ?? 'OK'}
|
||||
{confirmName ?? t('actions.ok')}
|
||||
</button>
|
||||
{showCancel ? (
|
||||
<button
|
||||
className={`button ${cancelClass ?? ''}`}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{cancelName ?? 'Cancel'}
|
||||
{cancelName ?? t('actions.cancel')}
|
||||
</button>
|
||||
) : null}
|
||||
</footer>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface PageListProps {
|
||||
current: number;
|
||||
|
@ -17,6 +18,7 @@ function PageList({
|
|||
onSelectChange,
|
||||
onPageChange,
|
||||
}: PageListProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<nav
|
||||
className="pagination is-small"
|
||||
|
@ -52,7 +54,7 @@ function PageList({
|
|||
<li>
|
||||
<button
|
||||
className="button pagination-link"
|
||||
aria-label={`Goto page ${min}`}
|
||||
aria-label={t('system.pagination.gotopage', { page: min })}
|
||||
onClick={() => onPageChange(min)}
|
||||
>
|
||||
{min}
|
||||
|
@ -69,7 +71,9 @@ function PageList({
|
|||
<li>
|
||||
<button
|
||||
className="button pagination-link"
|
||||
aria-label={`Goto page ${current - 1}`}
|
||||
aria-label={t('system.pagination.gotopage', {
|
||||
page: current - 1,
|
||||
})}
|
||||
onClick={() => onPageChange(current - 1)}
|
||||
>
|
||||
{current - 1}
|
||||
|
@ -79,7 +83,9 @@ function PageList({
|
|||
<li>
|
||||
<button
|
||||
className="pagination-link is-current"
|
||||
aria-label={`Page ${current}`}
|
||||
aria-label={t('system.pagination.page', {
|
||||
page: current,
|
||||
})}
|
||||
aria-current="page"
|
||||
>
|
||||
{current}
|
||||
|
@ -89,7 +95,9 @@ function PageList({
|
|||
<li>
|
||||
<button
|
||||
className="button pagination-link"
|
||||
aria-label={`Goto page ${current + 1}`}
|
||||
aria-label={t('system.pagination.gotopage', {
|
||||
page: current + 1,
|
||||
})}
|
||||
onClick={() => onPageChange(current + 1)}
|
||||
>
|
||||
{current + 1}
|
||||
|
@ -105,7 +113,9 @@ function PageList({
|
|||
<li>
|
||||
<button
|
||||
className="button pagination-link"
|
||||
aria-label={`Goto page ${max}`}
|
||||
aria-label={t('system.pagination.gotopage', {
|
||||
page: max,
|
||||
})}
|
||||
onClick={() => onPageChange(max)}
|
||||
>
|
||||
{max}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { RouteComponentProps } from '@reach/router';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { RootState } from '../../../store';
|
||||
|
@ -13,9 +14,11 @@ interface GoalItemProps {
|
|||
onDelete: () => void;
|
||||
}
|
||||
function GoalItem({ item, onToggleState, onEdit, onDelete }: GoalItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const currency = useSelector(
|
||||
(state: RootState) =>
|
||||
state.api.moduleConfigs?.loyaltyConfig?.currency ?? 'points',
|
||||
state.api.moduleConfigs?.loyaltyConfig?.currency ??
|
||||
t('loyalty.points-fallback'),
|
||||
);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const placeholder = 'https://bulma.io/images/placeholders/128x128.png';
|
||||
|
@ -46,7 +49,7 @@ function GoalItem({ item, onToggleState, onEdit, onDelete }: GoalItemProps) {
|
|||
}}
|
||||
>
|
||||
{item.contributed >= item.total ? (
|
||||
<span className="goal-reached">Reached!</span>
|
||||
<span className="goal-reached">{t('loyalty.goals.reached')}</span>
|
||||
) : (
|
||||
<>
|
||||
{item.contributed} / {item.total} {currency}
|
||||
|
@ -69,11 +72,11 @@ function GoalItem({ item, onToggleState, onEdit, onDelete }: GoalItemProps) {
|
|||
<div className="contributors" style={{ marginTop: '1rem' }}>
|
||||
{contributors.length > 0 ? (
|
||||
<>
|
||||
<b>Contributors:</b>
|
||||
<b>{t('loyalty.goals.contributors')}</b>
|
||||
<table className="table is-striped is-narrow">
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Points</th>
|
||||
<th>{t('form-common.username')}</th>
|
||||
<th>{t('loyalty.points')}</th>
|
||||
</tr>
|
||||
{contributors.map(([user, points]) => (
|
||||
<tr>
|
||||
|
@ -89,18 +92,18 @@ function GoalItem({ item, onToggleState, onEdit, onDelete }: GoalItemProps) {
|
|||
</table>
|
||||
</>
|
||||
) : (
|
||||
<b>No one has contributed yet :(</b>
|
||||
<b>{t('loyalty.goals.no-contributors')}</b>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<a className="button is-small" onClick={onToggleState}>
|
||||
{item.enabled ? 'Disable' : 'Enable'}
|
||||
{item.enabled ? t('actions.disable') : t('actions.enable')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onEdit}>
|
||||
Edit
|
||||
{t('actions.edit')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onDelete}>
|
||||
Delete
|
||||
{t('actions.delete')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -126,6 +129,8 @@ function GoalModal({
|
|||
title,
|
||||
confirmText,
|
||||
}: GoalModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loyaltyConfig] = useModule(modules.loyaltyConfig);
|
||||
const [goals] = useModule(modules.loyaltyGoals);
|
||||
|
||||
|
@ -175,7 +180,7 @@ function GoalModal({
|
|||
>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Goal ID</label>
|
||||
<label className="label">{t('loyalty.goals.id')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -183,29 +188,24 @@ function GoalModal({
|
|||
<input
|
||||
className={idInvalid ? 'input is-danger' : 'input'}
|
||||
type="text"
|
||||
placeholder="goal_id_here"
|
||||
placeholder={t('loyalty.goals.id-placeholder')}
|
||||
value={slug}
|
||||
onChange={(ev) => setIDex(ev.target.value)}
|
||||
/>
|
||||
</p>
|
||||
{idInvalid ? (
|
||||
<p className="help is-danger">
|
||||
There is already a goal with this ID! Please choose a different
|
||||
one.
|
||||
{t('loyalty.goals.err-goalid-dup')}
|
||||
</p>
|
||||
) : (
|
||||
<p className="help">
|
||||
Choose a simple name that can be referenced by other software.
|
||||
It will be auto-generated from the goal name if you leave it
|
||||
blank.
|
||||
</p>
|
||||
<p className="help">{t('loyalty.goals.id-help')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Name</label>
|
||||
<label className="label">{t('loyalty.goals.name')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -214,7 +214,7 @@ function GoalModal({
|
|||
disabled={!active}
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="My dream goal"
|
||||
placeholder={t('loyalty.goals.name-placeholder')}
|
||||
value={name ?? ''}
|
||||
onChange={(ev) => setName(ev.target.value)}
|
||||
/>
|
||||
|
@ -224,7 +224,7 @@ function GoalModal({
|
|||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Icon</label>
|
||||
<label className="label">{t('loyalty.goals.icon')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -232,7 +232,7 @@ function GoalModal({
|
|||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Image URL"
|
||||
placeholder={t('loyalty.goals.icon-placeholder')}
|
||||
value={image ?? ''}
|
||||
onChange={(ev) => setImage(ev.target.value)}
|
||||
/>
|
||||
|
@ -242,14 +242,14 @@ function GoalModal({
|
|||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Description</label>
|
||||
<label className="label">{t('loyalty.goals.description')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<p className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
placeholder="What's gonna happen when we reach this goal?"
|
||||
placeholder={t('loyalty.goals.description-placeholder')}
|
||||
onChange={(ev) => setDescription(ev.target.value)}
|
||||
value={description}
|
||||
></textarea>
|
||||
|
@ -259,7 +259,7 @@ function GoalModal({
|
|||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Required</label>
|
||||
<label className="label">{t('form-common.required')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field has-addons">
|
||||
|
@ -289,6 +289,7 @@ export default function LoyaltyGoalsPage(
|
|||
const [goals, setGoals] = useModule(modules.loyaltyGoals);
|
||||
const [moduleConfig] = useModule(modules.moduleConfig);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const twitchActive = moduleConfig?.twitch ?? false;
|
||||
|
@ -336,7 +337,7 @@ export default function LoyaltyGoalsPage(
|
|||
|
||||
return (
|
||||
<>
|
||||
<h1 className="title is-4">Community goals</h1>
|
||||
<h1 className="title is-4">{t('loyalty.goals.header')}</h1>
|
||||
|
||||
<div className="field is-grouped">
|
||||
<p className="control">
|
||||
|
@ -345,7 +346,7 @@ export default function LoyaltyGoalsPage(
|
|||
disabled={!active}
|
||||
onClick={() => setCreateModal(true)}
|
||||
>
|
||||
New goal
|
||||
{t('loyalty.goals.new')}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
|
@ -353,7 +354,7 @@ export default function LoyaltyGoalsPage(
|
|||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Search by name"
|
||||
placeholder={t('loyalty.goals.search')}
|
||||
value={goalFilter}
|
||||
onChange={(ev) => setGoalFilter(ev.target.value)}
|
||||
/>
|
||||
|
@ -361,16 +362,16 @@ export default function LoyaltyGoalsPage(
|
|||
</div>
|
||||
|
||||
<GoalModal
|
||||
title="New goal"
|
||||
confirmText="Create"
|
||||
title={t('loyalty.goals.new')}
|
||||
confirmText={t('actions.create')}
|
||||
active={createModal}
|
||||
onConfirm={createGoal}
|
||||
onClose={() => setCreateModal(false)}
|
||||
/>
|
||||
{showModifyGoal ? (
|
||||
<GoalModal
|
||||
title="Modify goal"
|
||||
confirmText="Edit"
|
||||
title={t('loyalty.goals.modify')}
|
||||
confirmText={t('actions.edit')}
|
||||
active={true}
|
||||
onConfirm={(goal) => modifyGoal(showModifyGoal.id, goal)}
|
||||
initialData={showModifyGoal}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useModule, useUserPoints } from '../../../lib/react-utils';
|
||||
import {
|
||||
LoyaltyRedeem,
|
||||
|
@ -32,6 +33,7 @@ export default function LoyaltyRedeemQueuePage(
|
|||
const [entriesPerPage, setEntriesPerPage] = useState(15);
|
||||
const [page, setPage] = useState(0);
|
||||
const [usernameFilter, setUsernameFilter] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const changeSort = (key: 'user' | 'when') => {
|
||||
|
@ -105,14 +107,14 @@ export default function LoyaltyRedeemQueuePage(
|
|||
|
||||
return (
|
||||
<>
|
||||
<h1 className="title is-4">Redemption queue</h1>
|
||||
<h1 className="title is-4">{t('loyalty.queue.header')}</h1>
|
||||
{redemptions ? (
|
||||
<>
|
||||
<div className="field">
|
||||
<input
|
||||
className="input is-small"
|
||||
type="text"
|
||||
placeholder="Search by username"
|
||||
placeholder={t('loyalty.queue.search')}
|
||||
value={usernameFilter}
|
||||
onChange={(ev) =>
|
||||
setUsernameFilter(ev.target.value.toLowerCase())
|
||||
|
@ -132,7 +134,7 @@ export default function LoyaltyRedeemQueuePage(
|
|||
<tr>
|
||||
<th style={{ width: '20%' }}>
|
||||
<span className="sortable" onClick={() => changeSort('when')}>
|
||||
Date
|
||||
{t('form-common.date')}
|
||||
{sorting.key === 'when' ? (
|
||||
<span className="sort-icon">
|
||||
{sorting.order === 'asc' ? '▴' : '▾'}
|
||||
|
@ -142,7 +144,7 @@ export default function LoyaltyRedeemQueuePage(
|
|||
</th>
|
||||
<th>
|
||||
<span className="sortable" onClick={() => changeSort('user')}>
|
||||
Username
|
||||
{t('form-common.username')}
|
||||
{sorting.key === 'user' ? (
|
||||
<span className="sort-icon">
|
||||
{sorting.order === 'asc' ? '▴' : '▾'}
|
||||
|
@ -150,8 +152,8 @@ export default function LoyaltyRedeemQueuePage(
|
|||
) : null}
|
||||
</span>
|
||||
</th>
|
||||
<th>Reward name</th>
|
||||
<th>Request</th>
|
||||
<th>{t('loyalty.queue.reward-name')}</th>
|
||||
<th>{t('loyalty.queue.request')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -168,11 +170,15 @@ export default function LoyaltyRedeemQueuePage(
|
|||
<td>{redemption.reward.name}</td>
|
||||
<td>{redemption.request_text}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<a onClick={() => acceptRedeem(redemption)}>Accept</a>
|
||||
<a onClick={() => acceptRedeem(redemption)}>
|
||||
{t('loyalty.queue.accept')}
|
||||
</a>
|
||||
{redemption.username !== '@PLATFORM' ? (
|
||||
<>
|
||||
{' 🞄 '}
|
||||
<a onClick={() => refundRedeem(redemption)}>Refund</a>
|
||||
<a onClick={() => refundRedeem(redemption)}>
|
||||
{t('loyalty.queue.refund')}
|
||||
</a>
|
||||
</>
|
||||
) : null}
|
||||
</td>
|
||||
|
@ -190,10 +196,7 @@ export default function LoyaltyRedeemQueuePage(
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Redemption queue is not available (loyalty disabled or no one has
|
||||
redeemed anything yet)
|
||||
</p>
|
||||
<p>{t('loyalty.queue.err-not-available')}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { RouteComponentProps } from '@reach/router';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import prettyTime from 'pretty-ms';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { RootState } from '../../../store';
|
||||
import {
|
||||
|
@ -26,9 +27,11 @@ function RewardItem({
|
|||
onDelete,
|
||||
onTest,
|
||||
}: RewardItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const currency = useSelector(
|
||||
(state: RootState) =>
|
||||
state.api.moduleConfigs?.loyaltyConfig?.currency ?? 'points',
|
||||
state.api.moduleConfigs?.loyaltyConfig?.currency ??
|
||||
t('loyalty.points-fallback'),
|
||||
);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const placeholder = 'https://bulma.io/images/placeholders/128x128.png';
|
||||
|
@ -75,26 +78,27 @@ function RewardItem({
|
|||
{item.description}
|
||||
{item.cooldown > 0 ? (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<b>Cooldown:</b> {prettyTime(item.cooldown * 1000)}
|
||||
<b>{t('loyalty.rewards.cooldown')}:</b>{' '}
|
||||
{prettyTime(item.cooldown * 1000)}
|
||||
</div>
|
||||
) : null}
|
||||
{item.required_info ? (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<b>Required info:</b> {item.required_info}
|
||||
<b>{t('loyalty.rewards.required-info')}:</b> {item.required_info}
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<a className="button is-small" onClick={onTest}>
|
||||
Test
|
||||
{t('loyalty.rewards.test')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onToggleState}>
|
||||
{item.enabled ? 'Disable' : 'Enable'}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onEdit}>
|
||||
Edit
|
||||
{t('actions.edit')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onDelete}>
|
||||
Delete
|
||||
{t('actions.delete')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -120,6 +124,7 @@ function RewardModal({
|
|||
title,
|
||||
confirmText,
|
||||
}: RewardModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const currency = useSelector(
|
||||
(state: RootState) =>
|
||||
state.api.moduleConfigs?.loyaltyConfig?.currency ?? 'points',
|
||||
|
@ -185,7 +190,7 @@ function RewardModal({
|
|||
>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Reward ID</label>
|
||||
<label className="label">{t('loyalty.rewards.id')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -193,29 +198,24 @@ function RewardModal({
|
|||
<input
|
||||
className={idInvalid ? 'input is-danger' : 'input'}
|
||||
type="text"
|
||||
placeholder="reward_id_here"
|
||||
placeholder={t('loyalty.rewards.id-placeholder')}
|
||||
value={slug}
|
||||
onChange={(ev) => setIDex(ev.target.value)}
|
||||
/>
|
||||
</p>
|
||||
{idInvalid ? (
|
||||
<p className="help is-danger">
|
||||
There is already a reward with this ID! Please choose a
|
||||
different one.
|
||||
{t('loyalty.rewards.err-rewid-dup')}
|
||||
</p>
|
||||
) : (
|
||||
<p className="help">
|
||||
Choose a simple name that can be referenced by other software.
|
||||
It will be auto-generated from the reward name if you leave it
|
||||
blank.
|
||||
</p>
|
||||
<p className="help">{t('loyalty.rewards.id-help')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Name</label>
|
||||
<label className="label">{t('loyalty.rewards.name')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -224,7 +224,7 @@ function RewardModal({
|
|||
disabled={!active}
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="My awesome reward"
|
||||
placeholder={t('loyalty.rewards.name-placeholder')}
|
||||
value={name ?? ''}
|
||||
onChange={(ev) => setName(ev.target.value)}
|
||||
/>
|
||||
|
@ -234,7 +234,7 @@ function RewardModal({
|
|||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Icon</label>
|
||||
<label className="label">{t('loyalty.rewards.icon')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -242,7 +242,7 @@ function RewardModal({
|
|||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Image URL"
|
||||
placeholder={t('loyalty.rewards.icon-placeholder')}
|
||||
value={image ?? ''}
|
||||
onChange={(ev) => setImage(ev.target.value)}
|
||||
/>
|
||||
|
@ -252,14 +252,14 @@ function RewardModal({
|
|||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Description</label>
|
||||
<label className="label">{t('loyalty.rewards.description')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<p className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
placeholder="What's so cool about this reward?"
|
||||
placeholder={t('loyalty.rewards.description-placeholder')}
|
||||
onChange={(ev) => setDescription(ev.target.value)}
|
||||
value={description}
|
||||
></textarea>
|
||||
|
@ -269,7 +269,7 @@ function RewardModal({
|
|||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Cost</label>
|
||||
<label className="label">{t('loyalty.rewards.cost')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field has-addons">
|
||||
|
@ -298,7 +298,7 @@ function RewardModal({
|
|||
checked={extraRequired}
|
||||
onChange={(ev) => setExtraRequired(ev.target.checked)}
|
||||
/>{' '}
|
||||
Requires viewer-specified details
|
||||
{t('loyalty.rewards.requires-extra-info')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -307,7 +307,7 @@ function RewardModal({
|
|||
<>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Required info</label>
|
||||
<label className="label">{t('loyalty.rewards.extra-info')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -316,7 +316,7 @@ function RewardModal({
|
|||
disabled={!active}
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="What extra detail to ask the viewer for"
|
||||
placeholder={t('loyalty.rewards.extra-info-placeholder')}
|
||||
value={extraDetails ?? ''}
|
||||
onChange={(ev) => setExtraDetails(ev.target.value)}
|
||||
/>
|
||||
|
@ -328,7 +328,7 @@ function RewardModal({
|
|||
) : null}
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Cooldown</label>
|
||||
<label className="label">{t('loyalty.rewards.cooldown')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field has-addons">
|
||||
|
@ -361,9 +361,9 @@ function RewardModal({
|
|||
setTempCooldownMult(intMult);
|
||||
}}
|
||||
>
|
||||
<option value="1">seconds</option>
|
||||
<option value="60">minutes</option>
|
||||
<option value="3600">hours</option>
|
||||
<option value="1">{t('form-common.time.seconds')}</option>
|
||||
<option value="60">{t('form-common.time.minutes')}</option>
|
||||
<option value="3600">{t('form-common.time.hours')}</option>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
|
@ -382,6 +382,7 @@ export default function LoyaltyRewardsPage(
|
|||
const [moduleConfig] = useModule(modules.moduleConfig);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const twitchActive = moduleConfig?.twitch ?? false;
|
||||
const loyaltyEnabled = moduleConfig?.loyalty ?? false;
|
||||
|
@ -440,7 +441,7 @@ export default function LoyaltyRewardsPage(
|
|||
|
||||
return (
|
||||
<>
|
||||
<h1 className="title is-4">Loyalty rewards</h1>
|
||||
<h1 className="title is-4">{t('loyalty.rewards.header')}</h1>
|
||||
|
||||
<div className="field is-grouped">
|
||||
<p className="control">
|
||||
|
@ -449,7 +450,7 @@ export default function LoyaltyRewardsPage(
|
|||
disabled={!active}
|
||||
onClick={() => setCreateModal(true)}
|
||||
>
|
||||
New reward
|
||||
{t('loyalty.rewards.new-reward')}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
|
@ -457,7 +458,7 @@ export default function LoyaltyRewardsPage(
|
|||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Search by name"
|
||||
placeholder={t('loyalty.rewards.search')}
|
||||
value={rewardFilter}
|
||||
onChange={(ev) => setRewardFilter(ev.target.value)}
|
||||
/>
|
||||
|
@ -465,16 +466,16 @@ export default function LoyaltyRewardsPage(
|
|||
</div>
|
||||
|
||||
<RewardModal
|
||||
title="New reward"
|
||||
confirmText="Create"
|
||||
title={t('loyalty.rewards.new-reward')}
|
||||
confirmText={t('actions.create')}
|
||||
active={createModal}
|
||||
onConfirm={createReward}
|
||||
onClose={() => setCreateModal(false)}
|
||||
/>
|
||||
{showModifyReward ? (
|
||||
<RewardModal
|
||||
title="Modify reward"
|
||||
confirmText="Edit"
|
||||
title={t('loyalty.rewards.modify-reward')}
|
||||
confirmText={t('actions.edit')}
|
||||
active={true}
|
||||
onConfirm={(reward) => modifyReward(showModifyReward.id, reward)}
|
||||
initialData={showModifyReward}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { RouteComponentProps } from '@reach/router';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { getInterval } from '../../../lib/time-utils';
|
||||
|
@ -14,6 +15,7 @@ export default function LoyaltySettingPage(
|
|||
const [twitchConfig] = useModule(modules.twitchConfig);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const twitchActive = moduleConfig?.twitch ?? false;
|
||||
const twitchBotActive = twitchConfig?.enable_bot ?? false;
|
||||
|
@ -52,7 +54,7 @@ export default function LoyaltySettingPage(
|
|||
|
||||
return (
|
||||
<>
|
||||
<h1 className="title is-4">Loyalty system configuration</h1>
|
||||
<h1 className="title is-4">{t('loyalty.config.header')}</h1>
|
||||
<div className="field">
|
||||
<label className="checkbox">
|
||||
<input
|
||||
|
@ -67,21 +69,21 @@ export default function LoyaltySettingPage(
|
|||
}),
|
||||
)
|
||||
}
|
||||
/>{' '}
|
||||
Enable loyalty points{' '}
|
||||
/>
|
||||
{` ${t('loyalty.config.enable')} `}
|
||||
{twitchActive && twitchBotActive
|
||||
? ''
|
||||
: '(Twitch bot must be enabled for this!)'}
|
||||
: t('loyalty.config.err-twitchbot-disabled')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Currency name</label>
|
||||
<label className="label">{t('loyalty.config.currency-name')}</label>
|
||||
<p className="control">
|
||||
<input
|
||||
disabled={!active}
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="points"
|
||||
placeholder={t('loyalty.points-fallback')}
|
||||
value={loyaltyConfig?.currency ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
|
@ -95,11 +97,13 @@ export default function LoyaltySettingPage(
|
|||
</p>
|
||||
</div>
|
||||
<label className="label">
|
||||
How often to give {loyaltyConfig?.currency || 'points'}
|
||||
{t('loyalty.config.point-reward-frequency', {
|
||||
points: loyaltyConfig?.currency || t('loyalty.points-fallback'),
|
||||
})}
|
||||
</label>
|
||||
<div className="field has-addons" style={{ marginBottom: 0 }}>
|
||||
<p className="control">
|
||||
<a className="button is-static">Give</a>
|
||||
<a className="button is-static">{t('loyalty.config.give-points')}</a>
|
||||
</p>
|
||||
<p className="control">
|
||||
<input
|
||||
|
@ -126,7 +130,7 @@ export default function LoyaltySettingPage(
|
|||
/>
|
||||
</p>
|
||||
<p className="control">
|
||||
<a className="button is-static">every</a>
|
||||
<a className="button is-static">{t('loyalty.config.points-every')}</a>
|
||||
</p>
|
||||
<p className="control">
|
||||
<input
|
||||
|
@ -157,15 +161,15 @@ export default function LoyaltySettingPage(
|
|||
setTempIntervalMult(intMult);
|
||||
}}
|
||||
>
|
||||
<option value="1">seconds</option>
|
||||
<option value="60">minutes</option>
|
||||
<option value="3600">hours</option>
|
||||
<option value="1">{t('form-common.time.seconds')}</option>
|
||||
<option value="60">{t('form-common.time.minutes')}</option>
|
||||
<option value="3600">{t('form-common.time.hours')}</option>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Bonus points for active users</label>
|
||||
<label className="label">{t('loyalty.config.bonus-points')}</label>
|
||||
<p className="control">
|
||||
<input
|
||||
disabled={!active}
|
||||
|
@ -197,7 +201,7 @@ export default function LoyaltySettingPage(
|
|||
dispatch(setLoyaltyConfig(loyaltyConfig));
|
||||
}}
|
||||
>
|
||||
Save
|
||||
{t('actions.save')}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PageList from '../../components/PageList';
|
||||
import { useModule, useUserPoints } from '../../../lib/react-utils';
|
||||
import { RootState } from '../../../store';
|
||||
|
@ -33,6 +34,7 @@ function UserModal({
|
|||
title,
|
||||
confirmText,
|
||||
}: UserModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const currency = useSelector(
|
||||
(state: RootState) =>
|
||||
state.api.moduleConfigs?.loyaltyConfig?.currency ?? 'points',
|
||||
|
@ -69,7 +71,7 @@ function UserModal({
|
|||
>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Username</label>
|
||||
<label className="label">{t('form-common.username')}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
|
@ -120,8 +122,9 @@ export default function LoyaltyUserListPage(
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
props: RouteComponentProps<unknown>,
|
||||
): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [loyaltyConfig] = useModule(modules.loyaltyConfig);
|
||||
const currency = loyaltyConfig?.currency ?? 'points';
|
||||
const currency = loyaltyConfig?.currency ?? t('loyalty.points-fallback');
|
||||
const users = useUserPoints();
|
||||
const dispatch = useDispatch();
|
||||
const [sorting, setSorting] = useState<SortingOrder>({
|
||||
|
@ -190,8 +193,8 @@ export default function LoyaltyUserListPage(
|
|||
return (
|
||||
<>
|
||||
<UserModal
|
||||
title="Give points to user"
|
||||
confirmText="Give"
|
||||
title={t('loyalty.userlist.give-points')}
|
||||
confirmText={t('loyalty.userlist.give-button')}
|
||||
active={createModal}
|
||||
onConfirm={(entry) => assignPoints(entry)}
|
||||
initialData={{ user: '', entry: { points: 0 } }}
|
||||
|
@ -199,22 +202,24 @@ export default function LoyaltyUserListPage(
|
|||
/>
|
||||
{editModal ? (
|
||||
<UserModal
|
||||
title="Modify balance"
|
||||
confirmText="Edit"
|
||||
title={t('loyalty.userlist.modify-balance')}
|
||||
confirmText={t('actions.edit')}
|
||||
active={true}
|
||||
onConfirm={(entry) => modifyUser(entry)}
|
||||
initialData={editModal}
|
||||
onClose={() => setEditModal(null)}
|
||||
/>
|
||||
) : null}
|
||||
<h1 className="title is-4">All viewers with {currency}</h1>
|
||||
<h1 className="title is-4">
|
||||
{t('loyalty.userlist.userlist-header', { currency })}
|
||||
</h1>
|
||||
{users ? (
|
||||
<>
|
||||
<div className="field">
|
||||
<input
|
||||
className="input is-small"
|
||||
type="text"
|
||||
placeholder="Search by username"
|
||||
placeholder={t('loyalty.queue.search')}
|
||||
value={usernameFilter}
|
||||
onChange={(ev) =>
|
||||
setUsernameFilter(ev.target.value.toLowerCase())
|
||||
|
@ -223,7 +228,7 @@ export default function LoyaltyUserListPage(
|
|||
</div>
|
||||
<div className="field">
|
||||
<a className="button is-small" onClick={() => setCreateModal(true)}>
|
||||
Give points to user
|
||||
{t('loyalty.userlist.give-points')}
|
||||
</a>
|
||||
</div>
|
||||
<PageList
|
||||
|
@ -239,7 +244,7 @@ export default function LoyaltyUserListPage(
|
|||
<tr>
|
||||
<th>
|
||||
<span className="sortable" onClick={() => changeSort('user')}>
|
||||
Username
|
||||
{t('form-common.username')}
|
||||
{sorting.key === 'user' ? (
|
||||
<span className="sort-icon">
|
||||
{sorting.order === 'asc' ? '▴' : '▾'}
|
||||
|
@ -279,7 +284,7 @@ export default function LoyaltyUserListPage(
|
|||
})
|
||||
}
|
||||
>
|
||||
Edit
|
||||
{t('actions.edit')}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -296,9 +301,7 @@ export default function LoyaltyUserListPage(
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Viewer list is not available (loyalty disabled or no one has points)
|
||||
</p>
|
||||
<p>{t('loyalty.userlist.err-not-available')}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue