- 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>
110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
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>
|
||
)
|
||
}
|