import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useModule } from '../../lib/react-utils'; import { useAppDispatch } from '../../store'; import { modules } from '../../store/api/reducer'; import { LoyaltyGoal, LoyaltyReward } from '../../store/api/types'; import AlertContent from '../components/AlertContent'; import DialogContent from '../components/DialogContent'; import Interval from '../components/Interval'; import { Button, Checkbox, CheckboxIndicator, ControlledInputBox, Dialog, DialogActions, Field, FieldNote, FlexRow, InputBox, Label, MultiButton, PageContainer, PageHeader, PageTitle, styled, TabButton, TabContainer, TabContent, TabList, Textarea, TextBlock, } from '../theme'; import { Alert, AlertTrigger } from '../theme/alert'; const RewardList = styled('div', { marginTop: '1rem' }); const GoalList = styled('div', { marginTop: '1rem' }); const RewardItemContainer = styled('article', { backgroundColor: '$gray2', margin: '0.5rem 0', padding: '0.5rem', borderLeft: '5px solid $teal8', borderRadius: '0.25rem', borderBottom: '1px solid $gray4', transition: 'all 50ms', '&:hover': { backgroundColor: '$gray3', }, variants: { status: { enabled: {}, disabled: { borderLeftColor: '$red6', backgroundColor: '$gray3', color: '$gray10', }, }, }, }); const RewardHeader = styled('header', { display: 'flex', gap: '0.5rem', alignItems: 'center', marginBottom: '0.4rem', }); const RewardName = styled('span', { color: '$teal12', flex: 1, fontWeight: 'bold', variants: { status: { enabled: {}, disabled: { color: '$gray9', }, }, }, }); const RewardDescription = styled('span', { flex: 1, fontSize: '0.9rem', color: '$gray11', }); const RewardActions = styled('div', { display: 'flex', alignItems: 'center', gap: '0.25rem', }); const RewardID = styled('code', { fontFamily: 'Space Mono', color: '$teal11', }); const RewardCost = styled('div', { fontSize: '0.9rem', marginRight: '0.5rem', }); const RewardIcon = styled('div', { width: '32px', height: '32px', backgroundColor: '$gray4', borderRadius: '0.25rem', display: 'flex', alignItems: 'center', }); const NoneText = styled('div', { color: '$gray9', fontSize: '1.2em', textAlign: 'center', fontStyle: 'italic', paddingTop: '1rem', }); interface RewardItemProps { name: string; item: LoyaltyReward; currency: string; onToggle?: () => void; onEdit?: () => void; onDelete?: () => void; } function RewardItem({ name, item, currency, onToggle, onEdit, onDelete, }: RewardItemProps): React.ReactElement { const { t } = useTranslation(); return ( {item.image && ( )} {item.name} ({name}) {item.price} {currency} (onDelete ? onDelete() : null)} /> {item.description} ); } interface GoalItemProps { name: string; item: LoyaltyGoal; currency: string; onToggle?: () => void; onEdit?: () => void; onDelete?: () => void; } function GoalItem({ name, item, currency, onToggle, onEdit, onDelete, }: GoalItemProps): React.ReactElement { const { t } = useTranslation(); return ( {item.image && ( )} {item.name} ({name}) {item.contributed} / {item.total} {currency} ( {Math.round((item.contributed / item.total) * 100)}%) (onDelete ? onDelete() : null)} /> {item.description} ); } function RewardsPage() { const { t } = useTranslation(); const dispatch = useAppDispatch(); const [cursorPosition, setCursorPosition] = useState(0); const [config] = useModule(modules.loyaltyConfig); const [rewards, setRewards] = useModule(modules.loyaltyRewards); const [filter, setFilter] = useState(''); const [dialogReward, setDialogReward] = useState<{ open: boolean; new: boolean; reward: LoyaltyReward; }>({ open: false, new: false, reward: null }); const [requiredInfo, setRequiredInfo] = useState({ enabled: false, text: '', }); const filterLC = filter.toLowerCase(); const deleteReward = (id: string) => { void dispatch(setRewards(rewards?.filter((r) => r.id !== id) ?? [])); }; const toggleReward = (id: string) => { void dispatch( setRewards( rewards?.map((r) => { if (r.id === id) { return { ...r, enabled: !r.enabled, }; } return r; }) ?? [], ), ); }; return ( <> setDialogReward({ ...dialogReward, open: state }) } >
{ e.preventDefault(); if (!(e.target as HTMLFormElement).checkValidity()) { return; } const { reward } = dialogReward; if (requiredInfo.enabled) { reward.required_info = requiredInfo.text; } const index = rewards?.findIndex((r) => r.id === reward.id); if (index >= 0) { const newRewards = rewards.slice(0); newRewards[index] = reward; void dispatch(setRewards(newRewards)); } else { void dispatch(setRewards([...(rewards ?? []), reward])); } setDialogReward({ ...dialogReward, open: false }); }} > { e.target.selectionStart = cursorPosition; }} onChange={(e) => { setCursorPosition(e.target.selectionStart); setDialogReward({ ...dialogReward, reward: { ...dialogReward?.reward, id: e.target.value ?.toLowerCase() .replace(/[^a-zA-Z0-9]/gi, '-') ?? '', }, }); if ( dialogReward.new && rewards.find((r) => r.id === e.target.value) ) { (e.target as HTMLInputElement).setCustomValidity( t('pages.loyalty-rewards.id-already-in-use'), ); } else { (e.target as HTMLInputElement).setCustomValidity(''); } }} /> {t('pages.loyalty-rewards.reward-id-hint')} { setDialogReward({ ...dialogReward, reward: { ...dialogReward?.reward, name: e.target.value, }, }); }} /> {t('pages.loyalty-rewards.reward-name-hint')} { setDialogReward({ ...dialogReward, reward: { ...dialogReward?.reward, image: e.target.value, }, }); }} /> { setDialogReward({ ...dialogReward, reward: { ...dialogReward?.reward, price: parseInt(e.target.value, 10), }, }); }} /> { setDialogReward({ ...dialogReward, reward: { ...dialogReward?.reward, cooldown, }, }); }} /> { setRequiredInfo({ ...requiredInfo, enabled: !!e, }); }} > {requiredInfo.enabled && } { setRequiredInfo({ ...requiredInfo, text: e.target.value }); }} />
setFilter(e.target.value)} /> {rewards ? ( rewards ?.filter( (r) => r.name.toLowerCase().includes(filterLC) || r.id.toLowerCase().includes(filterLC) || r.description.toLowerCase().includes(filterLC), ) .map((r) => ( setDialogReward({ open: true, new: false, reward: r, }) } onDelete={() => deleteReward(r.id)} onToggle={() => toggleReward(r.id)} /> )) ) : ( {t('pages.loyalty-rewards.no-rewards')} )} ); } function GoalsPage() { const { t } = useTranslation(); const dispatch = useAppDispatch(); const [config] = useModule(modules.loyaltyConfig); const [goals, setGoals] = useModule(modules.loyaltyGoals); const [filter, setFilter] = useState(''); const [dialogGoal, setDialogGoal] = useState<{ open: boolean; new: boolean; goal: LoyaltyGoal; }>({ open: false, new: false, goal: null }); const [_requiredInfo, setRequiredInfo] = useState({ enabled: false, text: '', }); const filterLC = filter.toLowerCase(); const deleteGoal = (id: string): void => { void dispatch(setGoals(goals?.filter((r) => r.id !== id) ?? [])); }; const toggleGoal = (id: string): void => { void dispatch( setGoals( goals?.map((r) => { if (r.id === id) { return { ...r, enabled: !r.enabled, }; } return r; }) ?? [], ), ); }; return ( <> setDialogGoal({ ...dialogGoal, open: state })} >
{ e.preventDefault(); if (!(e.target as HTMLFormElement).checkValidity()) { return; } const { goal } = dialogGoal; const index = goals?.findIndex((g) => g.id === goal.id); if (index >= 0) { const newGoals = goals.slice(0); newGoals[index] = goal; void dispatch(setGoals(newGoals)); } else { void dispatch(setGoals([...(goals ?? []), goal])); } setDialogGoal({ ...dialogGoal, open: false }); }} > { setDialogGoal({ ...dialogGoal, goal: { ...dialogGoal?.goal, id: e.target.value ?.toLowerCase() .replace(/[^a-zA-Z0-9]/gi, '-') ?? '', }, }); if ( dialogGoal.new && goals.find((r) => r.id === e.target.value) ) { (e.target as HTMLInputElement).setCustomValidity( t('pages.loyalty-rewards.id-already-in-use'), ); } else { (e.target as HTMLInputElement).setCustomValidity(''); } }} /> {t('pages.loyalty-rewards.goal-id-hint')} { setDialogGoal({ ...dialogGoal, goal: { ...dialogGoal?.goal, name: e.target.value, }, }); }} /> {t('pages.loyalty-rewards.goal-name-hint')} { setDialogGoal({ ...dialogGoal, goal: { ...dialogGoal?.goal, image: e.target.value, }, }); }} /> { setDialogGoal({ ...dialogGoal, goal: { ...dialogGoal?.goal, total: parseInt(e.target.value, 10), }, }); }} />
setFilter(e.target.value)} /> {goals ? ( goals ?.filter( (r) => r.name.toLowerCase().includes(filterLC) || r.id.toLowerCase().includes(filterLC) || r.description.toLowerCase().includes(filterLC), ) .map((r) => ( setDialogGoal({ open: true, new: false, goal: r, }) } onDelete={() => deleteGoal(r.id)} onToggle={() => toggleGoal(r.id)} /> )) ) : ( {t('pages.loyalty-rewards.no-goals')} )} ); } export default function LoyaltyRewardsPage(): React.ReactElement { const { t } = useTranslation(); return ( {t('pages.loyalty-rewards.title')} {t('pages.loyalty-rewards.subtitle')} {t('pages.loyalty-rewards.rewards-tab')} {t('pages.loyalty-rewards.goals-tab')} ); }