Files
game-marathon/frontend/src/pages/widget/CurrentWidget.tsx
mamonov.ep 146ed5e489 Add OBS widgets for streamers
- Add widget token authentication system
- Create leaderboard, current assignment, and progress widgets
- Support dark, light, and neon themes
- Add widget settings modal for URL generation
- Fix avatar loading through backend API proxy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 19:16:50 +03:00

110 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { widgetsApi } from '@/api/widgets'
import type { WidgetCurrentData } from '@/types'
import '@/styles/widget.css'
const DIFFICULTY_LABELS: Record<string, string> = {
easy: 'Легко',
medium: 'Средне',
hard: 'Сложно',
}
export default function CurrentWidget() {
const [searchParams] = useSearchParams()
const [data, setData] = useState<WidgetCurrentData | null>(null)
const [error, setError] = useState<string | null>(null)
const marathonId = searchParams.get('marathon')
const token = searchParams.get('token')
const theme = searchParams.get('theme') || 'dark'
const transparent = searchParams.get('transparent') === 'true'
useEffect(() => {
if (!marathonId || !token) {
setError('Missing marathon or token parameter')
return
}
const fetchData = async () => {
try {
const result = await widgetsApi.getCurrent(parseInt(marathonId), token)
setData(result)
setError(null)
} catch {
setError('Failed to load data')
}
}
fetchData()
const interval = setInterval(fetchData, 30000)
return () => clearInterval(interval)
}, [marathonId, token])
if (error) {
return (
<div className={`widget widget-theme-${theme} ${transparent ? 'widget-transparent' : ''}`}>
<div className="widget-error">{error}</div>
</div>
)
}
if (!data) {
return (
<div className={`widget widget-theme-${theme} ${transparent ? 'widget-transparent' : ''}`}>
<div className="widget-loading">Loading...</div>
</div>
)
}
if (!data.has_assignment) {
return (
<div className={`widget widget-theme-${theme} ${transparent ? 'widget-transparent' : ''}`}>
<div className="widget-current widget-no-assignment">
<div className="widget-waiting">Ожидание спина...</div>
</div>
</div>
)
}
return (
<div className={`widget widget-theme-${theme} ${transparent ? 'widget-transparent' : ''}`}>
<div className="widget-current">
<div className="widget-current-header">
{data.game_cover_url && (
<img src={data.game_cover_url} alt="" className="widget-game-cover" />
)}
<div className="widget-current-info">
<div className="widget-game-title">{data.game_title}</div>
<div className="widget-assignment-type">
{data.assignment_type === 'playthrough' ? 'Прохождение' : 'Челлендж'}
</div>
</div>
</div>
<div className="widget-challenge">
<div className="widget-challenge-title">{data.challenge_title}</div>
{data.challenge_description && (
<div className="widget-challenge-desc">{data.challenge_description}</div>
)}
</div>
<div className="widget-current-footer">
<span className="widget-points-badge">+{data.points} очков</span>
{data.difficulty && (
<span className={`widget-difficulty widget-difficulty-${data.difficulty}`}>
{DIFFICULTY_LABELS[data.difficulty] || data.difficulty}
</span>
)}
</div>
{data.assignment_type === 'playthrough' && data.bonus_total !== null && data.bonus_total > 0 && (
<div className="widget-bonus-progress">
Бонусы: {data.bonus_completed || 0} / {data.bonus_total}
</div>
)}
</div>
</div>
)
}