import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Box, Button, Container, FormControl, FormLabel, Input, Tab, TabList, TabPanel, TabPanels, Tabs, Heading, Text, useToast, VStack, HStack, Link as ChakraLink, Select, Avatar, Badge, Progress, useColorModeValue, SimpleGrid, Image, IconButton, } from '@chakra-ui/react'; import { useAuth } from '../contexts/AuthContext'; import api from '../services/api'; import { getLeaderboard, LeaderboardItem, getProfile, EngagementProfile, getRewards, RewardItem, redeemReward, patchAvatar, patchProfile, getMyTransactions, PointsTx, getAchievements } from '../services/engagement'; import { getMyWinnings } from '../services/sweepstakes'; import { Upload, RefreshCw, Pencil, Gift } from 'lucide-react'; const SemiAdminPage: React.FC = () => { const { user, updateUser } = useAuth(); const splitName = (full?: string) => { const v = String(full || '').trim(); if (!v) return { fn: '', ln: '' }; const parts = v.split(/\s+/); if (parts.length === 1) return { fn: parts[0], ln: '' }; return { fn: parts[0], ln: parts.slice(1).join(' ') }; }; const init = splitName(user?.name); const [firstName, setFirstName] = useState(init.fn); const [lastName, setLastName] = useState(init.ln); const [isSaving, setIsSaving] = useState(false); const [prefsToken, setPrefsToken] = useState(''); const toast = useToast(); useEffect(() => { const s = splitName(user?.name); setFirstName(s.fn); setLastName(s.ln); }, [user?.name]); useEffect(() => { (async () => { try { const res = await api.get('/newsletter/token/me'); setPrefsToken(res.data?.token || ''); } catch {} })(); }, []); const loadProfile = async () => { setLoadingProf(true); try { const p = await getProfile(); setProf(p); setUsernameEdit(p.username || ''); } finally { setLoadingProf(false); } }; const loadRewards = async () => { setLoadingRewards(true); try { setRewards(await getRewards()); } finally { setLoadingRewards(false); } }; useEffect(() => { loadProfile(); loadRewards(); }, []); useEffect(() => { (async () => { setTxLoading(true); try { const items = await getMyTransactions({ limit: 100 }); setTxItems(items); } finally { setTxLoading(false); } })(); (async () => { setAchLoading(true); try { const res = await getAchievements(); setAchItems(res.achievements || []); } finally { setAchLoading(false); } })(); }, []); useEffect(() => { const onRefresh = () => { loadProfile().catch(()=>{}); }; window.addEventListener('engagement:refresh', onRefresh as any); return () => window.removeEventListener('engagement:refresh', onRefresh as any); }, []); const handleSave = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); try { const res = await api.put('/me', { first_name: firstName, last_name: lastName }); const updated = res.data?.user; if (updated) { const n = `${updated.first_name || firstName} ${updated.last_name || lastName}`.trim(); updateUser({ name: n }); } toast({ title: 'Uloženo', description: 'Osobní údaje byly aktualizovány.', status: 'success', duration: 3000 }); } catch (err: any) { toast({ title: 'Chyba', description: err?.response?.data?.error || 'Nelze uložit změny', status: 'error' }); } finally { setIsSaving(false); } }; const prefsUrl = prefsToken ? `/newsletter/preferences?token=${encodeURIComponent(prefsToken)}` : ''; const [metric, setMetric] = useState<'points'|'level'|'xp'>('points'); const [leaders, setLeaders] = useState([]); const [loadingLb, setLoadingLb] = useState(false); const [txLoading, setTxLoading] = useState(false); const [txItems, setTxItems] = useState([]); const [achLoading, setAchLoading] = useState(false); const [achItems, setAchItems] = useState([]); const [winsLoading, setWinsLoading] = useState(false); const [wins, setWins] = useState>([]); useEffect(() => { (async () => { try { setWinsLoading(true); const res = await getMyWinnings(); setWins((res.items || []).map((w:any) => ({ id: w.id, prize_name: w.prize_name, claim_status: w.claim_status, created_at: w.created_at }))); } catch { setWins([]); } finally { setWinsLoading(false); } })(); }, []); // Engagement profile const [prof, setProf] = useState(null); const [loadingProf, setLoadingProf] = useState(true); const [rewards, setRewards] = useState([]); const [loadingRewards, setLoadingRewards] = useState(false); const [usernameEdit, setUsernameEdit] = useState(''); const [usernameEditing, setUsernameEditing] = useState(false); const fileRef = useRef(null); const cardBg = useColorModeValue('white', 'gray.800'); const border = useColorModeValue('gray.200', 'gray.700'); useEffect(() => { let mounted = true; (async () => { try { setLoadingLb(true); const res = await getLeaderboard(metric, 20); if (mounted) setLeaders(res.items || []); } catch { if (mounted) setLeaders([]); } finally { if (mounted) setLoadingLb(false); } })(); return () => { mounted = false; }; }, [metric]); // XP thresholds helpers const levelInfo = useMemo(() => { if (!prof) return { level: 1, xp: 0, currentBase: 0, nextBase: 100, pct: 0 }; const L = Math.max(1, Number(prof.level || 1)); const xp = Number(prof.xp || 0); // total needed to reach level L: 100 + 200 + ... + 100*(L-1) = 50*(L-1)*L const totalToL = 50 * (L - 1) * L; const nextInc = 100 * L; const totalToNext = totalToL + nextInc; const inLevel = Math.max(0, xp - totalToL); const pct = Math.max(0, Math.min(100, Math.floor((inLevel / Math.max(1, nextInc)) * 100))); return { level: L, xp, currentBase: totalToL, nextBase: totalToNext, pct, inLevel, nextInc }; }, [prof]); const baseNameColor = useColorModeValue('gray.800', 'gray.100'); const nameColor = useMemo(() => { const L = levelInfo.level; if (L >= 20) return 'yellow.400'; // gold if (L >= 15) return 'purple.400'; // epic if (L >= 10) return 'blue.400'; // rare if (L >= 5) return 'teal.400'; // uncommon return baseNameColor; }, [levelInfo.level, baseNameColor]); const triggerUpload = () => { if (!prof?.avatar_upload_unlocked) { toast({ status: 'info', title: 'Odemkněte nahrání avataru', description: 'V obchodě níže můžete odemknout možnost nahrát vlastní profilový obrázek.', duration: 3500 }); const el = document.getElementById('rewards-store'); if (el) el.scrollIntoView({ behavior: 'smooth' }); return; } fileRef.current?.click(); }; const onFileSelected = async (e: React.ChangeEvent) => { const f = e.target.files?.[0]; if (!f) return; try { const fd = new FormData(); fd.append('file', f); const res = await api.post('/upload', fd, { headers: { 'Content-Type': 'multipart/form-data' } }); const url = res.data?.url || res.data?.absolute_url; if (!url) throw new Error('Upload selhal'); await patchAvatar({ avatar_url: url }); toast({ status: 'success', title: 'Avatar aktualizován' }); await loadProfile(); } catch (err: any) { const msg = err?.response?.data?.error || 'Nahrání selhalo'; toast({ status: 'error', title: 'Chyba', description: msg }); } finally { if (fileRef.current) fileRef.current.value = ''; } }; const randomizeAvatar = async () => { const seed = Math.random().toString(36).slice(2, 10); const url = `https://api.dicebear.com/7.x/pixel-art/svg?radius=50&seed=${encodeURIComponent(seed)}`; await patchAvatar({ avatar_url: url }); await loadProfile(); toast({ status: 'success', title: 'Náhodný avatar nastaven' }); }; const saveUsername = async () => { const v = usernameEdit.trim(); if (!v) { toast({ status: 'warning', title: 'Uživatelské jméno je prázdné' }); return; } try { setUsernameEditing(true); await patchProfile({ username: v }); toast({ status: 'success', title: 'Uživatelské jméno uloženo' }); await loadProfile(); } catch (e: any) { toast({ status: 'error', title: 'Chyba', description: e?.response?.data?.error || 'Nelze uložit' }); } finally { setUsernameEditing(false); } }; return ( Fan zóna {/* Profile header */} {/* Upload icon (left) */} } size="sm" variant="solid" colorScheme="blue" position="absolute" left="-10px" top="50%" transform="translateY(-50%)" onClick={triggerUpload} /> {/* Level badge (right) */} Lv {levelInfo.level} {/* Randomize (bottom) */} } size="xs" variant="ghost" position="absolute" bottom="-6px" right="50%" transform="translateX(50%)" onClick={randomizeAvatar} /> {/* Username */} {!usernameEditing && ( {(prof?.username || '').trim() || 'Nastavte uživatelské jméno'} )} } onClick={() => setUsernameEditing((v)=>!v)} /> {usernameEditing && ( setUsernameEdit(e.target.value)} placeholder="uživatelské-jméno" maxW="260px" /> )} {/* Full name */} {`${firstName || ''} ${lastName || ''}`.trim() || '—'} {/* XP progress */} {levelInfo.inLevel || 0} / {levelInfo.nextInc} XP do další úrovně Lv {levelInfo.level} {/* Points */} Aktuální body: {prof?.points ?? 0} {/* Store */} Obchod s odměnami {loadingRewards ? ( Načítám… ) : ( {rewards.map((r) => ( {r.name} {r.image_url && {r.name}} Cena: {r.cost_points} bodů ))} )} Jak získat body • Napište smysluplný komentář (+5) • Hlasujte v anketě (+3, 1× denně) • Přihlaste se k newsletteru (+12) Osobní údaje Newsletter Žebříčky Historie bodů Úspěchy Výhry Jméno setFirstName(e.target.value)} placeholder="Jméno" /> Příjmení setLastName(e.target.value)} placeholder="Příjmení" /> Spravujte předvolby newsletteru nebo se odhlaste. {prefsUrl ? ( ) : ( Načítám odkaz na nastavení… )} Žebříčky {loadingLb && Načítám…} {!loadingLb && leaders.length === 0 && ( Žádná data k zobrazení. )} {!loadingLb && leaders.map((it) => { const value = metric === 'points' ? it.points : (metric === 'level' ? it.level : it.xp); const max = Math.max(...leaders.map(l => metric === 'points' ? l.points : (metric === 'level' ? l.level : l.xp)), 1); const pct = Math.max(2, Math.floor((Number(value) / Number(max)) * 100)); const name = (it.username || '').trim() || `${it.first_name || ''} ${it.last_name || ''}`.trim() || `#${it.user_id}`; return ( {it.rank} {name} {value} ); })} {txLoading ? ( Načítám… ) : ( Čas Delta Důvod Meta {txItems.map((t) => ( {t.created_at ? new Date(t.created_at).toLocaleString() : '-'} = 0 ? 'green' : 'red'}>{t.delta >= 0 ? `+${t.delta}` : t.delta} {t.reason} {t.meta ? JSON.stringify(t.meta) : '-'} ))} {txItems.length === 0 && ( Žádné transakce. )} )} {achLoading ? ( Načítám… ) : ( {achItems.map((a: any) => ( {a.title} {a.achieved ? Splněno : Nesplněno} {a.description} {a.points} bodů {a.achieved_at && {new Date(a.achieved_at).toLocaleString()}} ))} {achItems.length === 0 && ( Žádné úspěchy k zobrazení. )} )} ); }; export default SemiAdminPage;