feat: добавлен режим "Каникулы" и улучшения админ-панели

- Добавлен режим "Каникулы" который полностью заменяет главную страницу:
  * Карточка с эмодзи 🎉 и праздничным сообщением
  * Поддержка произвольного текста в формате Markdown
  * Карточка центрируется по вертикали при отсутствии текста

- Улучшения админ-панели:
  * Переключатель режима "Каникулы"
  * Редактор текста с подсказками по форматированию Markdown
  * Исправлена проблема с обновлением настроек (сохранение существующих значений)
  * Исправлена проблема с debug опциями в production (не блокируют обновление обычных настроек)

- Оптимизация загрузки:
  * Проверка режима каникул перед загрузкой групп
  * Динамическая загрузка ReactMarkdown только при необходимости
  * Кеш настроек сбрасывается на главной странице для актуальности

- Добавлен скрипт для сброса пароля администратора (scripts/reset-admin-password.js)

- Установлена библиотека react-markdown для рендеринга Markdown контента
This commit is contained in:
kilyabin
2025-12-04 23:22:42 +04:00
parent e46a2419c3
commit 3f74709513
9 changed files with 1562 additions and 23 deletions

View File

@@ -117,6 +117,8 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
newPassword: '',
confirmPassword: ''
})
const [showVacationModeEditDialog, setShowVacationModeEditDialog] = React.useState(false)
const [vacationModeContent, setVacationModeContent] = React.useState<string>(settings.vacationModeContent || '')
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
const id = Date.now().toString()
@@ -531,6 +533,39 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
disabled={loading}
/>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-3">
<div>
<div className="font-semibold">Режим "Каникулы"</div>
<div className="text-sm text-muted-foreground">
Включить режим каникул (заменяет главную страницу)
</div>
</div>
<ToggleSwitch
checked={settings.vacationModeEnabled ?? false}
onChange={(checked) => handleUpdateSettings({ ...settings, vacationModeEnabled: checked })}
disabled={loading}
/>
</div>
<div className="flex items-center justify-between">
<div>
<div className="font-semibold">Редактировать текст каникул</div>
<div className="text-sm text-muted-foreground">
Настроить текст, отображаемый в режиме каникул
</div>
</div>
<Button
variant="outline"
onClick={() => {
setVacationModeContent(settings.vacationModeContent || '')
setShowVacationModeEditDialog(true)
}}
disabled={loading}
>
Редактировать
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
@@ -996,6 +1031,79 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
</DialogContent>
</Dialog>
{/* Диалог редактирования текста каникул */}
<Dialog open={showVacationModeEditDialog} onOpenChange={setShowVacationModeEditDialog}>
<DialogContent className="max-w-3xl max-h-[90vh]">
<DialogHeader>
<DialogTitle>Редактирование текста режима Каникулы</DialogTitle>
<DialogDescription>
Отредактируйте текст, который будет отображаться в режиме каникул. Поддерживается форматирование Markdown.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="vacation-mode-content">Текст (Markdown)</Label>
<textarea
id="vacation-mode-content"
value={vacationModeContent}
onChange={(e) => setVacationModeContent(e.target.value)}
className="flex min-h-[300px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono"
placeholder="Введите текст в формате Markdown..."
/>
<div className="text-xs text-muted-foreground space-y-1">
<p className="font-semibold">Подсказки по форматированию:</p>
<ul className="list-disc list-inside space-y-0.5">
<li><code className="bg-muted px-1 py-0.5 rounded"># Заголовок</code> - заголовок первого уровня</li>
<li><code className="bg-muted px-1 py-0.5 rounded">## Подзаголовок</code> - заголовок второго уровня</li>
<li><code className="bg-muted px-1 py-0.5 rounded">**жирный**</code> - <strong>жирный текст</strong></li>
<li><code className="bg-muted px-1 py-0.5 rounded">*курсив*</code> - <em>курсивный текст</em></li>
<li><code className="bg-muted px-1 py-0.5 rounded">[текст](url)</code> - ссылка</li>
<li><code className="bg-muted px-1 py-0.5 rounded">- элемент</code> - маркированный список</li>
</ul>
</div>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setShowVacationModeEditDialog(false)
setVacationModeContent(settings.vacationModeContent || '')
}}
>
Отмена
</Button>
<Button
type="button"
onClick={async () => {
setLoading(true)
setError(null)
try {
const updatedSettings = {
...settings,
vacationModeContent: vacationModeContent
}
await handleUpdateSettings(updatedSettings)
setShowVacationModeEditDialog(false)
showToast('Текст каникул успешно сохранен', 'success')
} catch (err) {
const errorMessage = 'Ошибка при сохранении текста'
setError(errorMessage)
showToast(errorMessage, 'error')
} finally {
setLoading(false)
}
}}
disabled={loading}
>
{loading ? 'Сохранение...' : 'Сохранить'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Toast уведомления */}
<ToastContainer toasts={toasts} onClose={removeToast} />
</>