diff --git a/REDESIGN_PLAN.md b/REDESIGN_PLAN.md
index 1080c2e..a7dd052 100644
--- a/REDESIGN_PLAN.md
+++ b/REDESIGN_PLAN.md
@@ -22,353 +22,7 @@ Success: #22c55e
Error: #ef4444
Text: #e2e8f0
Text Muted: #64748b
-```
-
----
-
-## Фаза 1: Базовая инфраструктура
-
-### 1.1 Обновление Tailwind Config
-- [ ] Новая цветовая палитра (neon colors)
-- [ ] Кастомные анимации:
- - `glitch` - glitch эффект для текста
- - `glow-pulse` - пульсация свечения
- - `float` - плавное парение
- - `slide-in-left/right/up/down` - слайды
- - `scale-in` - появление с масштабом
- - `shimmer` - блик на элементах
-- [ ] Кастомные backdrop-blur классы
-- [ ] Градиентные утилиты
-
-### 1.2 Глобальные стили (index.css)
-- [ ] CSS переменные для цветов
-- [ ] Glitch keyframes анимация
-- [ ] Noise/grain overlay
-- [ ] Glow эффекты (box-shadow неон)
-- [ ] Custom scrollbar (неоновый)
-- [ ] Selection стили (выделение текста)
-
-### 1.3 Новые UI компоненты
-- [ ] `GlitchText` - текст с glitch эффектом
-- [ ] `NeonButton` - кнопка с неоновым свечением
-- [ ] `GlassCard` - карточка с glassmorphism
-- [ ] `AnimatedCounter` - анимированные числа
-- [ ] `ProgressBar` - неоновый прогресс бар
-- [ ] `Badge` - бейджи со свечением
-- [ ] `Skeleton` - скелетоны загрузки
-- [ ] `Tooltip` - тултипы
-- [ ] `Tabs` - табы с анимацией
-- [ ] `Modal` - переработанная модалка
-
----
-
-## Фаза 2: Layout и навигация
-
-### 2.1 Новый Header
-- [ ] Sticky header с blur при скролле
-- [ ] Логотип с glitch эффектом при hover
-- [ ] Анимированная навигация (underline slide)
-- [ ] Notification bell с badge
-- [ ] User dropdown с аватаром
-- [ ] Mobile hamburger menu с slide-in
-
-### 2.2 Sidebar (новый компонент)
-- [ ] Collapsible sidebar для desktop
-- [ ] Иконки с tooltip
-- [ ] Active state с неоновой подсветкой
-- [ ] Quick stats внизу
-
-### 2.3 Footer
-- [ ] Минималистичный footer
-- [ ] Social links
-- [ ] Version info
-
----
-
-## Фаза 3: Страницы
-
-### 3.1 HomePage (полный редизайн)
-```
-┌─────────────────────────────────────────────┐
-│ HERO SECTION │
-│ ┌─────────────────────────────────────┐ │
-│ │ Animated background (particles/ │ │
-│ │ geometric shapes) │ │
-│ │ │ │
-│ │ GAME MARATHON │ │
-│ │ │ │
-│ │ Tagline with typing effect │ │
-│ │ │ │
-│ │ [Start Playing] [Watch Demo] │ │
-│ └─────────────────────────────────────┘ │
-├─────────────────────────────────────────────┤
-│ FEATURES SECTION (3 glass cards) │
-│ ┌───────┐ ┌───────┐ ┌───────┐ │
-│ │ Icon │ │ Icon │ │ Icon │ hover: │
-│ │ Title │ │ Title │ │ Title │ lift + │
-│ │ Desc │ │ Desc │ │ Desc │ glow │
-│ └───────┘ └───────┘ └───────┘ │
-├─────────────────────────────────────────────┤
-│ HOW IT WORKS (timeline style) │
-│ ○───────○───────○───────○ │
-│ 1 2 3 4 │
-│ Create Add Spin Win │
-├─────────────────────────────────────────────┤
-│ LIVE STATS (animated counters) │
-│ [ 1,234 Marathons ] [ 5,678 Challenges ] │
-├─────────────────────────────────────────────┤
-│ CTA SECTION │
-│ Ready to compete? [Join Now] │
-└─────────────────────────────────────────────┘
-```
-
-### 3.2 Login/Register Pages
-- [ ] Centered card с glassmorphism
-- [ ] Animated background (subtle)
-- [ ] Form inputs с glow при focus
-- [ ] Password strength indicator
-- [ ] Social login buttons (future)
-- [ ] Smooth transitions между login/register
-
-### 3.3 MarathonsPage (Dashboard)
-```
-┌─────────────────────────────────────────────┐
-│ Header: "My Marathons" + Create button │
-├─────────────────────────────────────────────┤
-│ Quick Stats Bar │
-│ [Active: 2] [Completed: 5] [Total Wins: 3]│
-├─────────────────────────────────────────────┤
-│ Filters/Tabs: All | Active | Completed │
-├─────────────────────────────────────────────┤
-│ Marathon Cards Grid (2-3 columns) │
-│ ┌──────────────────┐ ┌──────────────────┐ │
-│ │ Cover image/ │ │ │ │
-│ │ gradient │ │ │ │
-│ │ ──────────── │ │ │ │
-│ │ Title │ │ │ │
-│ │ Status badge │ │ │ │
-│ │ Participants │ │ │ │
-│ │ Progress bar │ │ │ │
-│ └──────────────────┘ └──────────────────┘ │
-├─────────────────────────────────────────────┤
-│ Join by Code (expandable section) │
-└─────────────────────────────────────────────┘
-```
-
-### 3.4 MarathonPage (Detail)
-```
-┌─────────────────────────────────────────────┐
-│ Hero Banner │
-│ ┌─────────────────────────────────────┐ │
-│ │ Background gradient + pattern │ │
-│ │ Marathon Title (large) │ │
-│ │ Status | Dates | Participants │ │
-│ │ [Play] [Leaderboard] [Settings] │ │
-│ └─────────────────────────────────────┘ │
-├─────────────────────────────────────────────┤
-│ Event Banner (if active) - animated │
-├────────────────────┬────────────────────────┤
-│ Main Content │ Sidebar │
-│ ┌──────────────┐ │ ┌──────────────────┐ │
-│ │ Your Stats │ │ │ Activity Feed │ │
-│ │ Points/Streak│ │ │ (scrollable) │ │
-│ └──────────────┘ │ │ │ │
-│ ┌──────────────┐ │ │ │ │
-│ │ Quick Actions│ │ │ │ │
-│ └──────────────┘ │ │ │ │
-│ ┌──────────────┐ │ │ │ │
-│ │ Games List │ │ │ │ │
-│ │ (collapsible)│ │ │ │ │
-│ └──────────────┘ │ └──────────────────┘ │
-└────────────────────┴────────────────────────┘
-```
-
-### 3.5 PlayPage (Game Screen) - ГЛАВНАЯ СТРАНИЦА
-```
-┌─────────────────────────────────────────────┐
-│ Top Bar: Points | Streak | Event Timer │
-├─────────────────────────────────────────────┤
-│ ┌─────────────────────────────────────┐ │
-│ │ SPIN WHEEL │ │
-│ │ (redesigned, neon style) │ │
-│ │ ┌─────────┐ │ │
-│ │ │ WHEEL │ │ │
-│ │ │ │ │ │
-│ │ └─────────┘ │ │
-│ │ [SPIN BUTTON] │ │
-│ └─────────────────────────────────────┘ │
-├─────────────────────────────────────────────┤
-│ Active Challenge Card (если есть) │
-│ ┌─────────────────────────────────────┐ │
-│ │ Game: [Title] | Difficulty: [★★★] │ │
-│ │ ─────────────────────────────────── │ │
-│ │ Challenge Title │ │
-│ │ Description... │ │
-│ │ ─────────────────────────────────── │ │
-│ │ Points: 100 | Time: ~2h │ │
-│ │ ─────────────────────────────────── │ │
-│ │ Proof Upload Area │ │
-│ │ [File] [URL] [Comment] │ │
-│ │ ─────────────────────────────────── │ │
-│ │ [Complete ✓] [Skip ✗] │ │
-│ └─────────────────────────────────────┘ │
-├─────────────────────────────────────────────┤
-│ Mini Leaderboard (top 3 + you) │
-└─────────────────────────────────────────────┘
-```
-
-### 3.6 LeaderboardPage
-- [ ] Animated podium для top 3
-- [ ] Table с hover эффектами
-- [ ] Highlight для текущего пользователя
-- [ ] Streak fire animation
-- [ ] Sorting/filtering
-
-### 3.7 ProfilePage
-```
-┌─────────────────────────────────────────────┐
-│ Profile Header │
-│ ┌─────────────────────────────────────┐ │
-│ │ Avatar (large, glow border) │ │
-│ │ Nickname [Edit] │ │
-│ │ Member since: Date │ │
-│ └─────────────────────────────────────┘ │
-├─────────────────────────────────────────────┤
-│ Stats Cards (animated counters) │
-│ [Marathons] [Wins] [Challenges] [Points] │
-├─────────────────────────────────────────────┤
-│ Achievements Section (future) │
-├─────────────────────────────────────────────┤
-│ Telegram Connection Card │
-├─────────────────────────────────────────────┤
-│ Security Section (password change) │
-└─────────────────────────────────────────────┘
-```
-
-### 3.8 LobbyPage
-- [ ] Step-by-step wizard UI
-- [ ] Game cards grid с preview
-- [ ] Challenge preview с difficulty badges
-- [ ] AI generation progress animation
-- [ ] Launch countdown
-
----
-
-## Фаза 4: Специальные компоненты
-
-### 4.1 SpinWheel (полный редизайн)
-- [ ] 3D perspective эффект
-- [ ] Неоновые сегменты с названиями игр
-- [ ] Particle effects при кручении
-- [ ] Glow trail за указателем
-- [ ] Sound effects (optional)
-- [ ] Confetti при выигрыше
-
-### 4.2 EventBanner (переработка)
-- [ ] Pulsating glow border
-- [ ] Countdown timer с flip animation
-- [ ] Event-specific icons/colors
-- [ ] Dismiss animation
-
-### 4.3 ActivityFeed (переработка)
-- [ ] Timeline style
-- [ ] Avatar circles
-- [ ] Type-specific icons
-- [ ] Hover для деталей
-- [ ] New items animation (slide-in)
-
-### 4.4 Challenge Cards
-- [ ] Difficulty stars/badges
-- [ ] Progress indicator
-- [ ] Expandable details
-- [ ] Proof preview thumbnail
-
----
-
-## Фаза 5: Анимации и эффекты
-
-### 5.1 Page Transitions
-- [ ] Framer Motion page transitions
-- [ ] Fade + slide between routes
-- [ ] Loading skeleton screens
-
-### 5.2 Micro-interactions
-- [ ] Button press effects
-- [ ] Input focus glow
-- [ ] Success checkmark animation
-- [ ] Error shake animation
-- [ ] Loading spinners (custom)
-
-### 5.3 Background Effects
-- [ ] Animated gradient mesh
-- [ ] Floating particles (optional)
-- [ ] Grid pattern overlay
-- [ ] Noise texture
-
-### 5.4 Special Effects
-- [ ] Glitch text на заголовках
-- [ ] Neon glow на важных элементах
-- [ ] Shimmer effect на loading
-- [ ] Confetti на achievements
-
----
-
-## Фаза 6: Responsive и Polish
-
-### 6.1 Mobile Optimization
-- [ ] Touch-friendly targets
-- [ ] Swipe gestures
-- [ ] Bottom navigation (mobile)
-- [ ] Collapsible sections
-
-### 6.2 Accessibility
-- [ ] Keyboard navigation
-- [ ] Focus indicators
-- [ ] Screen reader support
-- [ ] Reduced motion option
-
-### 6.3 Performance
-- [ ] Lazy loading images
-- [ ] Code splitting
-- [ ] Animation optimization
-- [ ] Bundle size check
-
----
-
-## Порядок реализации
-
-### Sprint 1: Фундамент (2-3 дня)
-1. Tailwind config + colors
-2. Global CSS + animations
-3. Base UI components (Button, Card, Input)
-4. GlitchText component
-5. Updated Layout/Header
-
-### Sprint 2: Core Pages (3-4 дня)
-1. HomePage с hero
-2. Login/Register
-3. MarathonsPage dashboard
-4. Profile page
-
-### Sprint 3: Game Flow (3-4 дня)
-1. MarathonPage detail
-2. SpinWheel redesign
-3. PlayPage
-4. LeaderboardPage
-
-### Sprint 4: Polish (2-3 дня)
-1. LobbyPage
-2. Event components
-3. Activity feed
-4. Animations & transitions
-
-### Sprint 5: Finalization (1-2 дня)
-1. Mobile testing
-2. Performance optimization
-3. Bug fixes
-4. Final polish
-
----
+```А ты
## Референсы для вдохновления
diff --git a/backend/alembic/versions/018_seed_static_content.py b/backend/alembic/versions/018_seed_static_content.py
new file mode 100644
index 0000000..f648f2e
--- /dev/null
+++ b/backend/alembic/versions/018_seed_static_content.py
@@ -0,0 +1,346 @@
+"""Seed static content
+
+Revision ID: 018_seed_static_content
+Revises: 017_admin_logs_nullable_admin_id
+Create Date: 2024-12-20
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '018_seed_static_content'
+down_revision: Union[str, None] = '017_admin_logs_nullable_admin_id'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+STATIC_CONTENT_DATA = [
+ {
+ 'key': 'terms_of_service',
+ 'title': 'Пользовательское соглашение',
+ 'content': '''
Настоящее Пользовательское соглашение (далее — «Соглашение») регулирует отношения между администрацией интернет-сервиса «Игровой Марафон» (далее — «Сервис», «Платформа», «Мы») и физическим лицом, использующим Сервис (далее — «Пользователь», «Вы»).
+
+Дата вступления в силу: с момента регистрации на Платформе.
+Используя Сервис, Вы подтверждаете, что полностью ознакомились с условиями настоящего Соглашения и принимаете их в полном объёме.
+
+
+
+1. Общие положения
+
+1.1. Сервис «Игровой Марафон» представляет собой онлайн-платформу для организации и проведения игровых марафонов — соревнований, в рамках которых участники выполняют игровые задания (челленджи) и получают очки за их успешное выполнение.
+
+1.2. Сервис предоставляет Пользователям следующие возможности:
+
+Создание и участие в игровых марафонах
+Получение случайных игровых заданий различной сложности
+Отслеживание прогресса и статистики участников
+Участие в специальных игровых событиях
+Получение уведомлений через интеграцию с Telegram
+
+
+1.3. Сервис предоставляется на условиях «как есть» (as is). Администрация не гарантирует, что Сервис будет соответствовать ожиданиям Пользователя или работать бесперебойно.
+
+
+
+2. Регистрация и учётная запись
+
+2.1. Для доступа к функционалу Сервиса необходима регистрация учётной записи. При регистрации Пользователь обязуется предоставить достоверные данные.
+
+2.2. Пользователь несёт полную ответственность за:
+
+Сохранность своих учётных данных (логина и пароля)
+Все действия, совершённые с использованием его учётной записи
+Своевременное уведомление Администрации о несанкционированном доступе к аккаунту
+
+
+2.3. Каждый Пользователь имеет право на одну учётную запись. Создание дополнительных аккаунтов (мультиаккаунтинг) запрещено и влечёт блокировку всех связанных учётных записей.
+
+2.4. Пользователь вправе в любой момент удалить свою учётную запись, обратившись к Администрации. При удалении аккаунта все связанные данные будут безвозвратно удалены.
+
+
+
+3. Правила использования Сервиса
+
+3.1. При использовании Сервиса запрещается:
+
+Использовать читы, эксплойты, модификации и любое стороннее программное обеспечение, дающее нечестное преимущество при выполнении игровых заданий
+Предоставлять ложные доказательства выполнения заданий (поддельные скриншоты, видео, достижения)
+Передавать доступ к учётной записи третьим лицам
+Оскорблять, унижать или преследовать других участников
+Распространять спам, рекламу или вредоносный контент
+Нарушать работу Сервиса техническими средствами
+Использовать Сервис для деятельности, нарушающей законодательство
+
+
+3.2. Правила проведения марафонов:
+
+Участники обязаны честно выполнять полученные задания
+Доказательства выполнения должны быть подлинными и соответствовать требованиям задания
+Отказ от задания (дроп) влечёт штрафные санкции согласно правилам конкретного марафона
+Споры по заданиям разрешаются через систему диспутов с участием других участников марафона
+
+
+3.3. Организаторы марафонов несут ответственность за соблюдение правил в рамках своих мероприятий и имеют право устанавливать дополнительные правила, не противоречащие настоящему Соглашению.
+
+
+
+4. Система очков и рейтинг
+
+4.1. За выполнение заданий Пользователи получают очки, количество которых зависит от сложности задания и активных игровых событий.
+
+4.2. Очки используются исключительно для формирования рейтинга участников в рамках марафонов и не имеют денежного эквивалента.
+
+4.3. Администрация оставляет за собой право корректировать начисленные очки в случае выявления нарушений или технических ошибок.
+
+
+
+5. Ответственность сторон
+
+5.1. Администрация не несёт ответственности за:
+
+Временную недоступность Сервиса по техническим причинам
+Потерю данных вследствие технических сбоев
+Действия третьих лиц, получивших доступ к учётной записи Пользователя
+Контент, размещаемый Пользователями
+Качество интернет-соединения Пользователя
+
+
+5.2. Пользователь несёт ответственность за соблюдение условий настоящего Соглашения и применимого законодательства.
+
+
+
+6. Санкции за нарушения
+
+6.1. За нарушение условий настоящего Соглашения Администрация вправе применить следующие санкции:
+
+Предупреждение — за незначительные нарушения
+Временная блокировка — ограничение доступа к Сервису на определённый срок
+Постоянная блокировка — бессрочное ограничение доступа за грубые или повторные нарушения
+
+
+6.2. Решение о применении санкций принимается Администрацией единолично и является окончательным. Администрация не обязана объяснять причины принятого решения.
+
+6.3. Обход блокировки путём создания новых учётных записей влечёт блокировку всех выявленных аккаунтов.
+
+
+
+7. Интеллектуальная собственность
+
+7.1. Все элементы Сервиса (дизайн, код, тексты, логотипы) являются объектами интеллектуальной собственности Администрации и защищены применимым законодательством.
+
+7.2. Использование материалов Сервиса без письменного разрешения Администрации запрещено.
+
+
+
+8. Изменение условий Соглашения
+
+8.1. Администрация вправе в одностороннем порядке изменять условия настоящего Соглашения.
+
+8.2. Актуальная редакция Соглашения размещается на данной странице с указанием даты последнего обновления.
+
+8.3. Продолжение использования Сервиса после внесения изменений означает согласие Пользователя с новой редакцией Соглашения.
+
+
+
+9. Заключительные положения
+
+9.1. Настоящее Соглашение регулируется законодательством Российской Федерации.
+
+9.2. Все споры, возникающие в связи с использованием Сервиса, подлежат разрешению путём переговоров. При недостижении согласия споры разрешаются в судебном порядке по месту нахождения Администрации.
+
+9.3. Признание судом недействительности какого-либо положения настоящего Соглашения не влечёт недействительности остальных положений.
+
+9.4. По всем вопросам, связанным с использованием Сервиса, Вы можете обратиться к Администрации через Telegram-бота или иные доступные каналы связи.
'''
+ },
+ {
+ 'key': 'privacy_policy',
+ 'title': 'Политика конфиденциальности',
+ 'content': '''Настоящая Политика конфиденциальности (далее — «Политика») описывает, как интернет-сервис «Игровой Марафон» (далее — «Сервис», «Мы») собирает, использует, хранит и защищает персональные данные пользователей (далее — «Пользователь», «Вы»).
+
+Используя Сервис, Вы даёте согласие на обработку Ваших персональных данных в соответствии с условиями настоящей Политики.
+
+
+
+1. Собираемые данные
+
+1.1. Данные, предоставляемые Пользователем:
+
+Регистрационные данные: логин, пароль (в зашифрованном виде), никнейм
+Данные профиля: аватар (при загрузке)
+Данные интеграции Telegram: Telegram ID, имя пользователя, имя и фамилия (при привязке бота)
+
+
+1.2. Данные, собираемые автоматически:
+
+Данные об активности: участие в марафонах, выполненные задания, заработанные очки, статистика
+Технические данные: IP-адрес, тип браузера, время доступа (для обеспечения безопасности)
+Данные сессии: информация для поддержания авторизации
+
+
+
+
+2. Цели обработки данных
+
+2.1. Мы обрабатываем Ваши персональные данные для следующих целей:
+
+Предоставление услуг:
+
+Идентификация и аутентификация Пользователя
+Обеспечение участия в марафонах и игровых событиях
+Ведение статистики и формирование рейтингов
+Отображение профиля Пользователя другим участникам
+
+
+Коммуникация:
+
+Отправка уведомлений о событиях марафонов через Telegram-бота
+Информирование о новых заданиях и результатах
+Ответы на обращения Пользователей
+
+
+Безопасность:
+
+Защита от несанкционированного доступа
+Выявление и предотвращение нарушений
+Ведение журнала административных действий
+
+
+
+
+3. Правовые основания обработки
+
+3.1. Обработка персональных данных осуществляется на следующих основаниях:
+
+Согласие Пользователя — при регистрации и использовании Сервиса
+Исполнение договора — Пользовательского соглашения
+Законный интерес — обеспечение безопасности Сервиса
+
+
+
+
+4. Хранение и защита данных
+
+4.1. Меры безопасности:
+
+Пароли хранятся в зашифрованном виде с использованием алгоритма bcrypt
+Передача данных осуществляется по защищённому протоколу HTTPS
+Доступ к базе данных ограничен и контролируется
+Административные действия логируются и требуют двухфакторной аутентификации
+
+
+4.2. Срок хранения:
+
+Данные учётной записи хранятся до момента её удаления Пользователем
+Данные об активности в марафонах хранятся бессрочно для ведения статистики
+Технические логи хранятся в течение 12 месяцев
+
+
+
+
+5. Передача данных третьим лицам
+
+5.1. Мы не продаём, не сдаём в аренду и не передаём Ваши персональные данные третьим лицам в коммерческих целях.
+
+5.2. Данные могут быть переданы:
+
+Telegram — для обеспечения работы уведомлений (только Telegram ID)
+Правоохранительным органам — по законному запросу в соответствии с применимым законодательством
+
+
+5.3. Публично доступная информация:
+Следующие данные видны другим Пользователям Сервиса:
+
+Никнейм
+Аватар
+Статистика участия в марафонах
+Позиция в рейтингах
+
+
+
+
+6. Права Пользователя
+
+6.1. Вы имеете право:
+
+Получить доступ к своим персональным данным
+Исправить неточные или неполные данные в настройках профиля
+Удалить свою учётную запись и связанные данные
+Отозвать согласие на обработку данных (путём удаления аккаунта)
+Отключить интеграцию с Telegram в любой момент
+
+
+6.2. Для реализации своих прав обратитесь к Администрации через доступные каналы связи.
+
+
+
+7. Файлы cookie и локальное хранилище
+
+7.1. Сервис использует локальное хранилище браузера (localStorage, sessionStorage) для:
+
+Хранения токена авторизации
+Сохранения пользовательских настроек интерфейса
+Запоминания закрытых информационных баннеров
+
+
+7.2. Вы можете очистить локальное хранилище в настройках браузера, однако это приведёт к выходу из учётной записи.
+
+
+
+8. Обработка данных несовершеннолетних
+
+8.1. Сервис не предназначен для лиц младше 14 лет. Мы сознательно не собираем персональные данные детей.
+
+8.2. Если Вам стало известно, что ребёнок предоставил нам персональные данные, пожалуйста, свяжитесь с Администрацией для их удаления.
+
+
+
+9. Изменение Политики
+
+9.1. Мы оставляем за собой право изменять настоящую Политику. Актуальная редакция всегда доступна на данной странице.
+
+9.2. О существенных изменениях мы уведомим Пользователей через Telegram-бота или баннер на сайте.
+
+9.3. Продолжение использования Сервиса после внесения изменений означает согласие с обновлённой Политикой.
+
+
+
+10. Контактная информация
+
+10.1. По вопросам, связанным с обработкой персональных данных, Вы можете обратиться к Администрации через:
+
+Telegram-бота Сервиса
+Форму обратной связи (при наличии)
+
+
+10.2. Мы обязуемся рассмотреть Ваше обращение в разумные сроки и предоставить ответ.
'''
+ },
+ {
+ 'key': 'telegram_bot_info',
+ 'title': 'Привяжите Telegram-бота',
+ 'content': 'Получайте уведомления о событиях марафона, новых заданиях и результатах прямо в Telegram'
+ },
+ {
+ 'key': 'announcement',
+ 'title': 'Добро пожаловать!',
+ 'content': 'Мы рады приветствовать вас в «Игровом Марафоне»! Создайте свой первый марафон или присоединитесь к существующему по коду приглашения.'
+ }
+]
+
+
+def upgrade() -> None:
+ for item in STATIC_CONTENT_DATA:
+ # Use ON CONFLICT to avoid duplicates
+ op.execute(f"""
+ INSERT INTO static_content (key, title, content, created_at, updated_at)
+ VALUES ('{item['key']}', '{item['title'].replace("'", "''")}', '{item['content'].replace("'", "''")}', NOW(), NOW())
+ ON CONFLICT (key) DO NOTHING
+ """)
+
+
+def downgrade() -> None:
+ keys = [f"'{item['key']}'" for item in STATIC_CONTENT_DATA]
+ op.execute(f"DELETE FROM static_content WHERE key IN ({', '.join(keys)})")
diff --git a/backend/app/api/v1/admin.py b/backend/app/api/v1/admin.py
index f5aa817..130e045 100644
--- a/backend/app/api/v1/admin.py
+++ b/backend/app/api/v1/admin.py
@@ -758,6 +758,37 @@ async def create_content(
return content
+@router.delete("/content/{key}", response_model=MessageResponse)
+async def delete_content(
+ key: str,
+ request: Request,
+ current_user: CurrentUser,
+ db: DbSession,
+):
+ """Delete static content. Admin only."""
+ require_admin_with_2fa(current_user)
+
+ result = await db.execute(
+ select(StaticContent).where(StaticContent.key == key)
+ )
+ content = result.scalar_one_or_none()
+ if not content:
+ raise HTTPException(status_code=404, detail="Content not found")
+
+ await db.delete(content)
+ await db.commit()
+
+ # Log action
+ await log_admin_action(
+ db, current_user.id, AdminActionType.CONTENT_UPDATE.value,
+ "static_content", content.id,
+ {"action": "delete", "key": key},
+ request.client.host if request.client else None
+ )
+
+ return {"message": f"Content '{key}' deleted successfully"}
+
+
# ============ Dashboard ============
@router.get("/dashboard", response_model=DashboardStats)
async def get_dashboard(current_user: CurrentUser, db: DbSession):
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1cb4718..407cda1 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -21,6 +21,7 @@ import { InvitePage } from '@/pages/InvitePage'
import { AssignmentDetailPage } from '@/pages/AssignmentDetailPage'
import { ProfilePage } from '@/pages/ProfilePage'
import { UserProfilePage } from '@/pages/UserProfilePage'
+import { StaticContentPage } from '@/pages/StaticContentPage'
import { NotFoundPage } from '@/pages/NotFoundPage'
import { TeapotPage } from '@/pages/TeapotPage'
import { ServerErrorPage } from '@/pages/ServerErrorPage'
@@ -89,6 +90,11 @@ function App() {
{/* Public invite page */}
} />
+ {/* Public static content pages */}
+ } />
+ } />
+ } />
+
('/admin/content', { key, title, content })
return response.data
},
+
+ deleteContent: async (key: string): Promise => {
+ await client.delete(`/admin/content/${key}`)
+ },
}
// Public content API (no auth required)
diff --git a/frontend/src/components/AnnouncementBanner.tsx b/frontend/src/components/AnnouncementBanner.tsx
new file mode 100644
index 0000000..55059b6
--- /dev/null
+++ b/frontend/src/components/AnnouncementBanner.tsx
@@ -0,0 +1,78 @@
+import { useState, useEffect } from 'react'
+import { contentApi } from '@/api/admin'
+import { Megaphone, X } from 'lucide-react'
+
+const STORAGE_KEY = 'announcement_dismissed'
+
+export function AnnouncementBanner() {
+ const [content, setContent] = useState(null)
+ const [title, setTitle] = useState(null)
+ const [updatedAt, setUpdatedAt] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+
+ useEffect(() => {
+ const loadAnnouncement = async () => {
+ try {
+ const data = await contentApi.getPublicContent('announcement')
+ // Check if this announcement was already dismissed (by updated_at)
+ const dismissedAt = localStorage.getItem(STORAGE_KEY)
+ if (dismissedAt === data.updated_at) {
+ setContent(null)
+ } else {
+ setContent(data.content)
+ setTitle(data.title)
+ setUpdatedAt(data.updated_at)
+ }
+ } catch {
+ // No announcement or error - don't show
+ setContent(null)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadAnnouncement()
+ }, [])
+
+ const handleDismiss = () => {
+ if (updatedAt) {
+ // Store the updated_at to know which announcement was dismissed
+ // When admin updates announcement, updated_at changes and banner shows again
+ localStorage.setItem(STORAGE_KEY, updatedAt)
+ setContent(null)
+ }
+ }
+
+ if (isLoading || !content) {
+ return null
+ }
+
+ return (
+
+ {/* Close button */}
+
+
+
+
+ {/* Content */}
+
+
+
+
+
+ {title && (
+
{title}
+ )}
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/TelegramBotBanner.tsx b/frontend/src/components/TelegramBotBanner.tsx
index a39a94b..63ca42b 100644
--- a/frontend/src/components/TelegramBotBanner.tsx
+++ b/frontend/src/components/TelegramBotBanner.tsx
@@ -1,16 +1,36 @@
import { Link } from 'react-router-dom'
import { useAuthStore } from '@/store/auth'
+import { contentApi } from '@/api/admin'
import { NeonButton } from '@/components/ui'
import { Bot, Bell, X } from 'lucide-react'
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
const STORAGE_KEY = 'telegram_banner_dismissed'
+// Default content if not configured in admin
+const DEFAULT_TITLE = 'Привяжите Telegram-бота'
+const DEFAULT_DESCRIPTION = 'Получайте уведомления о событиях марафона, новых заданиях и результатах прямо в Telegram'
+
export function TelegramBotBanner() {
const user = useAuthStore((state) => state.user)
const [dismissed, setDismissed] = useState(() => {
return sessionStorage.getItem(STORAGE_KEY) === 'true'
})
+ const [title, setTitle] = useState(DEFAULT_TITLE)
+ const [description, setDescription] = useState(DEFAULT_DESCRIPTION)
+
+ useEffect(() => {
+ const loadContent = async () => {
+ try {
+ const data = await contentApi.getPublicContent('telegram_bot_info')
+ if (data.title) setTitle(data.title)
+ if (data.content) setDescription(data.content)
+ } catch {
+ // Use defaults if content not found
+ }
+ }
+ loadContent()
+ }, [])
const handleDismiss = () => {
sessionStorage.setItem(STORAGE_KEY, 'true')
@@ -49,10 +69,10 @@ export function TelegramBotBanner() {
- Привяжите Telegram-бота
+ {title}
- Получайте уведомления о событиях марафона, новых заданиях и результатах прямо в Telegram
+ {description}
diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx
index d5a5b71..b2f3959 100644
--- a/frontend/src/components/layout/Layout.tsx
+++ b/frontend/src/components/layout/Layout.tsx
@@ -234,7 +234,13 @@ export function Layout() {
Игровой Марафон © {new Date().getFullYear()}
-
+
+
+ Правила
+
+
+ Конфиденциальность
+
v1.0
diff --git a/frontend/src/pages/MarathonsPage.tsx b/frontend/src/pages/MarathonsPage.tsx
index 50170ae..0e6658f 100644
--- a/frontend/src/pages/MarathonsPage.tsx
+++ b/frontend/src/pages/MarathonsPage.tsx
@@ -5,6 +5,7 @@ import type { MarathonListItem } from '@/types'
import { NeonButton, GlassCard, StatsCard } from '@/components/ui'
import { Plus, Users, Calendar, Loader2, Trophy, Gamepad2, ChevronRight, Hash, Sparkles } from 'lucide-react'
import { TelegramBotBanner } from '@/components/TelegramBotBanner'
+import { AnnouncementBanner } from '@/components/AnnouncementBanner'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
@@ -146,6 +147,11 @@ export function MarathonsPage() {
)}
+ {/* Announcement Banner */}
+
+
{/* Telegram Bot Banner */}
diff --git a/frontend/src/pages/StaticContentPage.tsx b/frontend/src/pages/StaticContentPage.tsx
new file mode 100644
index 0000000..40a7030
--- /dev/null
+++ b/frontend/src/pages/StaticContentPage.tsx
@@ -0,0 +1,107 @@
+import { useState, useEffect } from 'react'
+import { useParams, useLocation, Link } from 'react-router-dom'
+import { contentApi } from '@/api/admin'
+import type { StaticContent } from '@/types'
+import { GlassCard } from '@/components/ui'
+import { ArrowLeft, Loader2, FileText } from 'lucide-react'
+
+// Map routes to content keys
+const ROUTE_KEY_MAP: Record
= {
+ '/terms': 'terms_of_service',
+ '/privacy': 'privacy_policy',
+}
+
+export function StaticContentPage() {
+ const { key: paramKey } = useParams<{ key: string }>()
+ const location = useLocation()
+ const [content, setContent] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ // Determine content key from route or param
+ const contentKey = ROUTE_KEY_MAP[location.pathname] || paramKey
+
+ useEffect(() => {
+ if (!contentKey) return
+
+ const loadContent = async () => {
+ setIsLoading(true)
+ setError(null)
+ try {
+ const data = await contentApi.getPublicContent(contentKey)
+ setContent(data)
+ } catch {
+ setError('Контент не найден')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadContent()
+ }, [contentKey])
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (error || !content) {
+ return (
+
+
+
+
+
+ Страница не найдена
+ Запрашиваемый контент не существует
+
+
+ На главную
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ На главную
+
+
+
+ {content.title}
+
+
+ Последнее обновление: {new Date(content.updated_at).toLocaleDateString('ru-RU', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric'
+ })}
+
+
+
+ )
+}
diff --git a/frontend/src/pages/admin/AdminContentPage.tsx b/frontend/src/pages/admin/AdminContentPage.tsx
index a34f3cf..dc1904c 100644
--- a/frontend/src/pages/admin/AdminContentPage.tsx
+++ b/frontend/src/pages/admin/AdminContentPage.tsx
@@ -3,7 +3,8 @@ import { adminApi } from '@/api'
import type { StaticContent } from '@/types'
import { useToast } from '@/store/toast'
import { NeonButton } from '@/components/ui'
-import { FileText, Plus, Pencil, X, Save, Code } from 'lucide-react'
+import { FileText, Plus, Pencil, X, Save, Code, Trash2 } from 'lucide-react'
+import { useConfirm } from '@/store/confirm'
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleString('ru-RU', {
@@ -28,6 +29,7 @@ export function AdminContentPage() {
const [formContent, setFormContent] = useState('')
const toast = useToast()
+ const confirm = useConfirm()
useEffect(() => {
loadContents()
@@ -101,6 +103,30 @@ export function AdminContentPage() {
}
}
+ const handleDelete = async (content: StaticContent) => {
+ const confirmed = await confirm({
+ title: 'Удалить контент?',
+ message: `Вы уверены, что хотите удалить "${content.title}"? Это действие нельзя отменить.`,
+ confirmText: 'Удалить',
+ cancelText: 'Отмена',
+ variant: 'danger',
+ })
+
+ if (!confirmed) return
+
+ try {
+ await adminApi.deleteContent(content.key)
+ setContents(contents.filter(c => c.id !== content.id))
+ if (editing?.id === content.id) {
+ handleCancel()
+ }
+ toast.success('Контент удалён')
+ } catch (err) {
+ console.error('Failed to delete content:', err)
+ toast.error('Ошибка удаления')
+ }
+ }
+
if (loading) {
return (
@@ -155,15 +181,28 @@ export function AdminContentPage() {
{content.content.replace(/<[^>]*>/g, '').slice(0, 100)}...
- {
- e.stopPropagation()
- handleEdit(content)
- }}
- className="p-2 text-gray-400 hover:text-accent-400 hover:bg-accent-500/10 rounded-lg transition-colors ml-3"
- >
-
-
+
+
{
+ e.stopPropagation()
+ handleEdit(content)
+ }}
+ className="p-2 text-gray-400 hover:text-accent-400 hover:bg-accent-500/10 rounded-lg transition-colors"
+ title="Редактировать"
+ >
+
+
+
{
+ e.stopPropagation()
+ handleDelete(content)
+ }}
+ className="p-2 text-gray-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
+ title="Удалить"
+ >
+
+
+
Обновлено: {formatDate(content.updated_at)}