Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | /**
* PresentationSettingsTab
* Story 10.8a: Moderator Presentation Page — Functional
*
* Tab component for admin page: configure about text and partner count
* displayed on the moderator presentation page.
*
* AC #8 (admin UI)
*/
import React, { useEffect, useState } from 'react';
import {
Alert,
Box,
Button,
CircularProgress,
Snackbar,
TextField,
Typography,
} from '@mui/material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
getPresentationSettings,
updatePresentationSettings,
} from '@/services/presentationService';
export const PresentationSettingsTab: React.FC = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { data: settings, isLoading } = useQuery({
queryKey: ['presentation-settings'],
queryFn: getPresentationSettings,
});
const [aboutText, setAboutText] = useState('');
const [partnerCount, setPartnerCount] = useState<number>(0);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarError, setSnackbarError] = useState(false);
// Sync local state when settings load
useEffect(() => {
if (settings) {
setAboutText(settings.aboutText ?? '');
setPartnerCount(settings.partnerCount ?? 0);
}
}, [settings]);
const mutation = useMutation({
mutationFn: () => updatePresentationSettings({ aboutText, partnerCount }),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['presentation-settings'] });
setSnackbarError(false);
setSnackbarOpen(true);
},
onError: () => {
setSnackbarError(true);
setSnackbarOpen(true);
},
});
if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<CircularProgress />
</Box>
);
}
return (
<Box sx={{ maxWidth: 700 }} data-testid="presentation-settings-tab">
<Typography variant="h6" gutterBottom>
{t('admin.presentationSettings.title', 'Presentation Settings')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t(
'admin.presentationSettings.subtitle',
'These values appear on the moderator presentation page (/present/:eventCode).'
)}
</Typography>
<TextField
label={t('admin.presentationSettings.aboutText', 'About Text')}
value={aboutText}
onChange={(e) => {
setAboutText(e.target.value);
}}
multiline
minRows={4}
fullWidth
sx={{ mb: 3 }}
inputProps={{ 'aria-label': 'about text' }}
/>
<TextField
label={t('admin.presentationSettings.partnerCount', 'Partner Count')}
type="number"
value={partnerCount}
onChange={(e) => {
setPartnerCount(Number(e.target.value));
}}
inputProps={{ min: 0, 'aria-label': 'partner count' }}
sx={{ mb: 3, width: 200 }}
/>
<Box>
<Button
variant="contained"
onClick={() => {
mutation.mutate();
}}
disabled={mutation.isPending}
startIcon={
mutation.isPending ? <CircularProgress size={16} color="inherit" /> : undefined
}
>
{t('admin.presentationSettings.save', 'Save')}
</Button>
</Box>
<Snackbar
open={snackbarOpen}
autoHideDuration={4000}
onClose={() => {
setSnackbarOpen(false);
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
severity={snackbarError ? 'error' : 'success'}
onClose={() => {
setSnackbarOpen(false);
}}
>
{snackbarError
? t('admin.presentationSettings.saveFailed', 'Save failed. Please try again.')
: t('admin.presentationSettings.saved', 'Settings saved.')}
</Alert>
</Snackbar>
</Box>
);
};
|