From 3256c4084186eb37e5d23108fe5a457ba1f1910d Mon Sep 17 00:00:00 2001 From: "mamonov.ep" Date: Fri, 9 Jan 2026 19:55:48 +0300 Subject: [PATCH] Add widget preview and combined widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add live preview iframe in widget settings modal - Create combined widget (all-in-one: leaderboard + current + progress) - Add widget type tabs for switching preview - Update documentation with completed tasks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/tz-obs-widget.md | 65 ++--- frontend/src/App.tsx | 2 + .../src/components/WidgetSettingsModal.tsx | 242 +++++++++++------- frontend/src/pages/widget/CombinedWidget.tsx | 158 ++++++++++++ frontend/src/styles/widget.css | 143 +++++++++++ 5 files changed, 487 insertions(+), 123 deletions(-) create mode 100644 frontend/src/pages/widget/CombinedWidget.tsx diff --git a/docs/tz-obs-widget.md b/docs/tz-obs-widget.md index 397611f..078a6d8 100644 --- a/docs/tz-obs-widget.md +++ b/docs/tz-obs-widget.md @@ -557,41 +557,42 @@ async def validate_widget_token(token: str, marathon_id: int, db: AsyncSession) ## План реализации -### Этап 1: Backend — модель и токены -- [ ] Создать модель `WidgetToken` -- [ ] Миграция для таблицы `widget_tokens` -- [ ] API создания токена (`POST /marathons/{id}/widget-token`) -- [ ] API отзыва токена (`DELETE /widget-tokens/{id}`) -- [ ] Валидация токена +### Этап 1: Backend — модель и токены ✅ +- [x] Создать модель `WidgetToken` +- [x] Миграция для таблицы `widget_tokens` +- [x] API создания токена (`POST /widgets/marathons/{id}/token`) +- [x] API отзыва токена (`DELETE /widgets/tokens/{id}`) +- [x] API регенерации токена (`POST /widgets/tokens/{id}/regenerate`) +- [x] Валидация токена -### Этап 2: Backend — API виджетов -- [ ] Эндпоинт `/widget/leaderboard` -- [ ] Эндпоинт `/widget/current` -- [ ] Эндпоинт `/widget/progress` -- [ ] Схемы ответов +### Этап 2: Backend — API виджетов ✅ +- [x] Эндпоинт `/widgets/data/leaderboard` +- [x] Эндпоинт `/widgets/data/current` +- [x] Эндпоинт `/widgets/data/progress` +- [x] Схемы ответов - [ ] Rate limiting -### Этап 3: Frontend — страницы виджетов -- [ ] Роутинг `/widget/*` -- [ ] Компонент `LeaderboardWidget` -- [ ] Компонент `CurrentWidget` -- [ ] Компонент `ProgressWidget` -- [ ] Polling обновлений +### Этап 3: Frontend — страницы виджетов ✅ +- [x] Роутинг `/widget/*` +- [x] Компонент `LeaderboardWidget` +- [x] Компонент `CurrentWidget` +- [x] Компонент `ProgressWidget` +- [x] Polling обновлений (30 сек) -### Этап 4: Frontend — темы и стили -- [ ] Базовые стили виджетов -- [ ] Тема Dark -- [ ] Тема Light -- [ ] Тема Neon -- [ ] Поддержка прозрачного фона -- [ ] Параметры кастомизации через URL +### Этап 4: Frontend — темы и стили ✅ +- [x] Базовые стили виджетов +- [x] Тема Dark +- [x] Тема Light +- [x] Тема Neon +- [x] Поддержка прозрачного фона +- [x] Параметры кастомизации через URL (theme, count, avatars, transparent) -### Этап 5: Frontend — страница настроек -- [ ] Страница генерации виджетов -- [ ] Форма настроек (тема, количество и т.д.) -- [ ] Копирование URL -- [ ] Превью виджетов -- [ ] Инструкция по добавлению в OBS +### Этап 5: Frontend — страница настроек ✅ +- [x] Модальное окно настройки виджетов (WidgetSettingsModal) +- [x] Форма настроек (тема, количество, аватарки, прозрачность) +- [x] Копирование URL +- [x] Превью виджетов (iframe) +- [x] Инструкция по добавлению в OBS ### Этап 6: Тестирование - [ ] Проверка в OBS Browser Source @@ -600,6 +601,10 @@ async def validate_widget_token(token: str, marathon_id: int, db: AsyncSession) - [ ] Тестирование на разных разрешениях - [ ] Проверка производительности (polling) +### Не реализовано (опционально) +- [x] Комбинированный виджет +- [ ] Rate limiting для API виджетов + --- ## Примеры виджетов diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2dbabed..b2a5bae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,7 @@ import { InventoryPage } from '@/pages/InventoryPage' import LeaderboardWidget from '@/pages/widget/LeaderboardWidget' import CurrentWidget from '@/pages/widget/CurrentWidget' import ProgressWidget from '@/pages/widget/ProgressWidget' +import CombinedWidget from '@/pages/widget/CombinedWidget' // Admin Pages import { @@ -95,6 +96,7 @@ function App() { } /> } /> } /> + } /> }> } /> diff --git a/frontend/src/components/WidgetSettingsModal.tsx b/frontend/src/components/WidgetSettingsModal.tsx index 3a15029..bcb1148 100644 --- a/frontend/src/components/WidgetSettingsModal.tsx +++ b/frontend/src/components/WidgetSettingsModal.tsx @@ -11,6 +11,8 @@ interface WidgetSettingsModalProps { type WidgetTheme = 'dark' | 'light' | 'neon' +type WidgetType = 'leaderboard' | 'current' | 'progress' | 'combined' + export function WidgetSettingsModal({ marathonId, isOpen, onClose }: WidgetSettingsModalProps) { const [token, setToken] = useState(null) const [loading, setLoading] = useState(false) @@ -18,6 +20,7 @@ export function WidgetSettingsModal({ marathonId, isOpen, onClose }: WidgetSetti const [count, setCount] = useState(5) const [showAvatars, setShowAvatars] = useState(true) const [transparent, setTransparent] = useState(false) + const [previewType, setPreviewType] = useState('leaderboard') const toast = useToast() useEffect(() => { @@ -52,7 +55,7 @@ export function WidgetSettingsModal({ marathonId, isOpen, onClose }: WidgetSetti } } - const buildWidgetUrl = (type: 'leaderboard' | 'current' | 'progress') => { + const buildWidgetUrl = (type: WidgetType) => { if (!token) return '' const baseUrl = window.location.origin const params = new URLSearchParams({ @@ -73,9 +76,16 @@ export function WidgetSettingsModal({ marathonId, isOpen, onClose }: WidgetSetti if (!isOpen) return null + const widgetNames: Record = { + leaderboard: 'Лидерборд', + current: 'Текущее задание', + progress: 'Прогресс', + combined: 'Всё в одном', + } + return (
-
+

Виджеты для OBS

@@ -90,121 +100,167 @@ export function WidgetSettingsModal({ marathonId, isOpen, onClose }: WidgetSetti
-
+
{loading ? (

Загрузка...

) : token ? ( - <> - {/* Settings */} +
+ {/* Left column - Preview */}
-

Настройки

+

Превью

-
-
- - -
- -
- - setCount(parseInt(e.target.value) || 5)} - className="w-full bg-dark-700 border border-dark-600 rounded-lg px-3 py-2" - /> -
+ {widgetNames[type]} + + ))}
-
- - - + {/* Preview iframe */} +
+