1539 lines
88 KiB
HTML
1539 lines
88 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="cs">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>NaughtyBulldogs — Hodnocení streamerů</title>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
:root{--bg:#f5f5f3;--card:#fff;--border:#e5e5e2;--text:#1a1a1a;--muted:#888;--purple:#7F77DD;--purple-d:#534AB7;--purple-l:#EEEDFE;--green:#1D9E75;--orange:#EF9F27;--red:#D85A30;--red-l:#FAECE7;--red-d:#993C1D}
|
||
@media(prefers-color-scheme:dark){:root{--bg:#1a1a1a;--card:#242424;--border:#333;--text:#f0f0ee;--muted:#666;--purple-l:#2a2840}}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:16px;color:var(--text);background:var(--bg);min-height:100vh}
|
||
input,select,textarea{font-family:inherit;font-size:15px;border:0.5px solid var(--border);border-radius:8px;padding:8px 12px;width:100%;background:var(--card);color:var(--text);outline:none}
|
||
input:focus,select:focus,textarea:focus{border-color:#aaa}
|
||
button{font-family:inherit;cursor:pointer}
|
||
.btn{padding:9px 18px;border-radius:8px;border:0.5px solid var(--border);background:var(--card);color:var(--text);font-size:15px;transition:background .15s}
|
||
.btn:hover{background:var(--bg)}
|
||
.btn-primary{background:var(--purple);border-color:var(--purple-d);color:#fff}
|
||
.btn-primary:hover{background:var(--purple-d)}
|
||
.btn-sm{padding:7px 14px;font-size:14px}
|
||
.btn-ghost{border:none;background:none;color:var(--muted);font-size:15px;padding:7px 13px;border-radius:8px}
|
||
.btn-ghost:hover{background:var(--bg);color:var(--text)}
|
||
.btn-danger{background:#fff0ee;border-color:#f09595;color:#a32d2d}
|
||
.badge-live{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;font-size:13px;font-weight:500;background:var(--red-l);color:var(--red-d);border:0.5px solid var(--red)}
|
||
.badge-off{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;font-size:13px;font-weight:500;background:var(--bg);color:var(--muted);border:0.5px solid var(--border)}
|
||
.dot{width:7px;height:7px;border-radius:50%;background:var(--red);animation:pulse 1.4s infinite;flex-shrink:0}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||
nav{background:var(--card);border-bottom:0.5px solid var(--border);padding:0 1.5rem;display:flex;align-items:center;gap:10px;height:52px;position:sticky;top:0;z-index:100}
|
||
.logo{font-size:16px;font-weight:600;flex:1}
|
||
.ntab{padding:7px 15px;border-radius:8px;border:none;background:none;font-size:15px;color:var(--muted);cursor:pointer;transition:background .15s;font-family:inherit}
|
||
.ntab.on{background:var(--bg);color:var(--text);font-weight:500}
|
||
.ndiv{width:0.5px;height:22px;background:var(--border)}
|
||
main{padding:1.5rem;max-width:1100px;margin:0 auto}
|
||
.live-bar{background:var(--card);border:0.5px solid var(--border);border-radius:12px;padding:1rem 1.25rem;margin-bottom:1.25rem}
|
||
.live-chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:.75rem}
|
||
.lchip{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:20px;background:var(--red-l);border:0.5px solid var(--red);font-size:14px;text-decoration:none;color:var(--red-d)}
|
||
.lchip:hover{opacity:.85}
|
||
.suggest-bar{background:var(--bg);border:0.5px solid var(--border);border-radius:10px;padding:.75rem 1.25rem;display:flex;align-items:center;gap:10px;margin-bottom:1.25rem;flex-wrap:wrap}
|
||
.suggest-bar p{font-size:14px;color:var(--muted);flex:1}
|
||
.sbar{display:flex;gap:10px;margin-bottom:1.25rem}
|
||
.sbar input{flex:1}
|
||
.frow{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1.25rem;align-items:center}
|
||
.fchip{padding:6px 14px;border-radius:20px;border:0.5px solid var(--border);background:var(--card);font-size:14px;cursor:pointer;color:var(--muted);transition:all .15s;font-family:inherit}
|
||
.fchip.on{border-color:var(--purple);color:var(--purple-d);background:var(--purple-l)}
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
|
||
.card{border-radius:12px;border:0.5px solid var(--border);background:var(--card);overflow:hidden;transition:border-color .15s}
|
||
.card:hover{border-color:#bbb}
|
||
.card-pub{cursor:pointer}
|
||
.ch{padding:1rem 1.25rem;display:flex;align-items:flex-start;gap:12px}
|
||
.ava{width:46px;height:46px;border-radius:50%;background:var(--purple-l);display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:500;color:var(--purple-d);flex-shrink:0;text-transform:uppercase}
|
||
.cn{font-size:16px;font-weight:500}
|
||
.cs{font-size:13px;color:var(--muted)}
|
||
.cb{padding:0 1.25rem .75rem;border-top:0.5px solid var(--border)}
|
||
.srow{display:flex;flex-wrap:wrap;gap:4px;padding:8px 0}
|
||
.si{display:flex;flex-direction:column;align-items:center;gap:2px;flex:1;min-width:46px}
|
||
.si .sl{font-size:11px;color:var(--muted);text-align:center;line-height:1.2}
|
||
.sn{font-size:18px;font-weight:500}
|
||
.cf{padding:8px 1.25rem;border-top:0.5px solid var(--border);display:flex;gap:8px;align-items:center}
|
||
.ts{font-size:21px;font-weight:500}
|
||
.lrow{display:flex;gap:6px;flex-wrap:wrap;margin-top:5px}
|
||
.lb{padding:4px 10px;border-radius:6px;border:0.5px solid var(--border);font-size:12px;text-decoration:none;color:var(--muted);background:var(--bg)}
|
||
.lb:hover{color:var(--text)}
|
||
.tag{display:inline-block;padding:2px 8px;border-radius:6px;font-size:12px;font-weight:500;background:var(--purple-l);color:var(--purple-d)}
|
||
.mbg{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:200;display:flex;align-items:flex-start;justify-content:center;padding:1rem;overflow-y:auto}
|
||
.modal{background:var(--card);border-radius:16px;width:100%;max-width:680px;margin:auto;border:0.5px solid var(--border)}
|
||
.modal-sm{max-width:400px}
|
||
.mh{padding:1.25rem 1.5rem;border-bottom:0.5px solid var(--border);display:flex;align-items:center;gap:12px}
|
||
.mb{padding:1.5rem;max-height:72vh;overflow-y:auto}
|
||
.mf{padding:1rem 1.5rem;border-top:0.5px solid var(--border);display:flex;gap:8px;justify-content:flex-end}
|
||
.fg{margin-bottom:1rem}
|
||
.fl{font-size:14px;color:var(--muted);margin-bottom:5px;display:block}
|
||
.fgr{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||
.st{font-size:12px;font-weight:500;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:1.25rem 0 .6rem;border-bottom:0.5px solid var(--border);padding-bottom:5px}
|
||
.ri{display:flex;align-items:center;gap:12px}
|
||
.ri input[type=range]{flex:1}
|
||
.rv{min-width:24px;text-align:right;font-weight:500;font-size:16px}
|
||
.bw{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
||
.bl{font-size:13px;color:var(--muted);width:110px;flex-shrink:0}
|
||
.bb{flex:1;height:6px;background:var(--bg);border-radius:3px;overflow:hidden}
|
||
.bf{height:100%;border-radius:3px}
|
||
.bv{font-size:13px;font-weight:500;min-width:32px;text-align:right}
|
||
.tv{display:flex;border:0.5px solid var(--border);border-radius:8px;overflow:hidden}
|
||
.tv button{border:none;padding:6px 14px;font-size:14px;cursor:pointer;background:none;color:var(--muted);font-family:inherit}
|
||
.tv button.on{background:var(--bg);color:var(--text);font-weight:500}
|
||
table{width:100%;border-collapse:collapse;font-size:14px}
|
||
th{text-align:left;padding:8px 9px;font-size:12px;font-weight:500;color:var(--muted);border-bottom:0.5px solid var(--border)}
|
||
td{padding:8px 9px;border-bottom:0.5px solid var(--border)}
|
||
tr:last-child td{border-bottom:none}
|
||
.adchip{display:inline-flex;align-items:center;padding:4px 11px;border-radius:20px;background:var(--purple-l);border:0.5px solid #AFA9EC;font-size:13px;color:var(--purple-d)}
|
||
.empty{text-align:center;padding:3rem;color:var(--muted);font-size:16px}
|
||
.cbadge{font-size:13px;color:var(--muted)}
|
||
.spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-right:6px}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.toast{position:fixed;bottom:1.5rem;right:1.5rem;background:var(--card);border:0.5px solid var(--border);border-radius:10px;padding:.75rem 1.25rem;font-size:14px;z-index:300;box-shadow:0 4px 16px rgba(0,0,0,.12);animation:fadeIn .2s ease}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||
.comment{padding:.75rem 0;border-bottom:0.5px solid var(--border)}
|
||
.comment:last-child{border-bottom:none}
|
||
.comment-head{display:flex;align-items:center;gap:8px;margin-bottom:4px}
|
||
.comment-author{font-size:14px;font-weight:500}
|
||
.comment-time{font-size:12px;color:var(--muted)}
|
||
.comment-admin-badge{font-size:11px;font-weight:500;padding:1px 6px;border-radius:4px;background:var(--purple-l);color:var(--purple-d)}
|
||
.comment-body{font-size:15px;line-height:1.6;color:var(--text)}
|
||
.comment-del{margin-left:auto;border:none;background:none;color:var(--muted);cursor:pointer;font-size:16px;padding:0 4px;line-height:1}
|
||
.comment-del:hover{color:var(--red)}
|
||
.comment-form{display:flex;flex-direction:column;gap:8px;margin-top:1rem}
|
||
.comment-form input,.comment-form textarea{font-size:14px}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="R"></div>
|
||
<script>
|
||
const CATS=[
|
||
{k:'s', l:'Statistiky'}, {k:'g', l:'Grafika'}, {k:'a', l:'Alerty'},
|
||
{k:'v', l:'Vychytávky'}, {k:'n', l:'Náhled'}, {k:'ns',l:'Nastavení'},
|
||
{k:'o', l:'Odlišnost'}
|
||
];
|
||
const MAX=70;
|
||
|
||
// ── State ──────────────────────────────────────────────────────────
|
||
let ST={
|
||
admin:false, isMod:false, banned:false, db:[], loading:true,
|
||
tab:'pub', search:'', fil:'all', av:'cards',
|
||
admSection:'streamers', // 'streamers' | 'users' | 'teams' | 'settings'
|
||
loginM:false, editM:false, editId:null,
|
||
detailId:null, sugM:false, newM:false,
|
||
comments:{}, communityRatings:{},
|
||
oauthUser:null, myGroups:[],
|
||
settings:{auth_enabled:'false', auth_twitch_enabled:'true'},
|
||
pendingUsers:[], currentMods:[], allUsers:[],
|
||
raterGroups:[],
|
||
teamM:false, teamId:null, // open team management modal for a group
|
||
rateM:false, rateStreamerId:null, myRating:null,
|
||
activeTeamId:null, // currently-selected team in rater view (admin: any; member: from myGroups)
|
||
teamRatings:{}, // map streamer_id -> rating, fetched per active team
|
||
detailCRTab:'overview', // 'overview' | group_id (number) — which community-rating tab is open in detail
|
||
detailCmtFilter:'all', // 'all' | group_id — filter comments by team
|
||
};
|
||
|
||
// ── API helpers ────────────────────────────────────────────────────
|
||
async function api(method, path, body){
|
||
const opts={method, headers:{'Content-Type':'application/json'},credentials:'include'};
|
||
if(body) opts.body=JSON.stringify(body);
|
||
const r=await fetch(path,opts);
|
||
const j=await r.json();
|
||
if(!r.ok) throw new Error(j.error||'API error');
|
||
return j;
|
||
}
|
||
async function GET(p){return api('GET',p);}
|
||
async function POST(p,b){return api('POST',p,b);}
|
||
async function PUT(p,b){return api('PUT',p,b);}
|
||
async function DEL(p){return api('DELETE',p);}
|
||
|
||
// ── Boot ───────────────────────────────────────────────────────────
|
||
async function boot(){
|
||
// Check for auth errors from OAuth redirect
|
||
const params=new URLSearchParams(location.search);
|
||
if(params.get('auth_error')){
|
||
toast('Přihlášení se nezdařilo: '+params.get('auth_error'),false);
|
||
history.replaceState({},'','/');
|
||
}
|
||
|
||
try{
|
||
const [sess, streamers, settings, me]=await Promise.all([
|
||
GET('/api/auth?action=check'),
|
||
GET('/api/streamers'),
|
||
GET('/api/settings'),
|
||
GET('/api/oauth?action=me'),
|
||
]);
|
||
ST.admin = sess.admin;
|
||
ST.db = streamers;
|
||
ST.settings = settings;
|
||
ST.oauthUser= me.user;
|
||
ST.isMod = sess.admin || me.is_mod;
|
||
ST.banned = me.banned || false;
|
||
if(ST.isMod){
|
||
const all=await GET('/api/streamers?all=1');
|
||
ST.db=all;
|
||
}
|
||
// Load rater groups for OAuth users (also admins, so they could see their own team membership)
|
||
if(ST.oauthUser){
|
||
try{ ST.myGroups = await GET('/api/rater_groups?my=1') || []; }catch(e){ST.myGroups=[];}
|
||
}
|
||
}catch(e){console.error(e);}
|
||
ST.loading=false;
|
||
re();
|
||
setInterval(autoLiveRefresh, 3*60*1000);
|
||
}
|
||
|
||
async function autoLiveRefresh(){
|
||
try{
|
||
const rows=await GET('/api/live');
|
||
let changed=false;
|
||
rows.forEach(r=>{
|
||
const s=ST.db.find(x=>x.id===r.id);
|
||
if(s&&(s.status!==r.status||s.game!==r.game||s.title!==r.title)){
|
||
s.status=r.status;s.game=r.game;s.title=r.title;changed=true;
|
||
}
|
||
});
|
||
if(changed)re();
|
||
}catch(e){console.error('Live refresh failed:',e);}
|
||
}
|
||
|
||
async function loadComments(sid){
|
||
try{
|
||
const list=await GET('/api/comments?streamer_id='+sid);
|
||
ST.comments[sid]=list;
|
||
// Re-render just the comments section without full re-render
|
||
const el=document.getElementById('comments-list');
|
||
if(el)el.innerHTML=renderCommentsList(sid);
|
||
const btn=document.getElementById('comments-load');
|
||
if(btn)btn.style.display='none';
|
||
}catch(e){console.error('Failed to load comments:',e);}
|
||
}
|
||
|
||
function renderCommentsList(sid){
|
||
const all=ST.comments[sid]||[];
|
||
if(!all.length)return`<div style="font-size:14px;color:var(--muted);padding:.5rem 0">Zatím žádné komentáře</div>`;
|
||
// Build set of team ids present in comments (for filter chips)
|
||
const presentTeamIds=new Set();
|
||
all.forEach(c=>(c.team_ids||[]).forEach(t=>presentTeamIds.add(t)));
|
||
const teamLookup={};
|
||
(ST.raterGroups||[]).concat(ST.myGroups||[]).forEach(g=>{teamLookup[g.id]=g.name;});
|
||
// Also pull team names from the current streamer's community ratings (works for guests)
|
||
(ST.communityRatings[sid]?.ratings||[]).forEach(r=>{teamLookup[r.group_id]=r.group_name;});
|
||
// Filter
|
||
const f=ST.detailCmtFilter||'all';
|
||
const list = f==='all' ? all : all.filter(c=>(c.team_ids||[]).includes(+f));
|
||
|
||
// Filter chips (only if we have any team-affiliated comments)
|
||
let chips='';
|
||
if(presentTeamIds.size){
|
||
chips=`<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:.6rem;align-items:center">
|
||
<span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;margin-right:4px">Filtr:</span>
|
||
<button class="fchip ${f==='all'?'on':''}" data-cmtfilter="all" style="font-size:12px;padding:3px 10px">Všechny (${all.length})</button>
|
||
${[...presentTeamIds].map(tid=>{
|
||
const cnt=all.filter(c=>(c.team_ids||[]).includes(tid)).length;
|
||
const nm=teamLookup[tid]||`Tým #${tid}`;
|
||
return `<button class="fchip ${f==tid?'on':''}" data-cmtfilter="${tid}" style="font-size:12px;padding:3px 10px">${escHtml(nm)} (${cnt})</button>`;
|
||
}).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
const items = list.length===0
|
||
? `<div style="font-size:14px;color:var(--muted);padding:.5rem 0">Žádné komentáře pro tento filtr</div>`
|
||
: list.map(c=>{
|
||
const tIds=c.team_ids||[];
|
||
const teamBadges = tIds.map(tid=>{
|
||
const nm=teamLookup[tid]||`Tým #${tid}`;
|
||
return `<span style="font-size:10px;padding:1px 6px;background:var(--purple-l);color:var(--purple-d);border-radius:8px">${escHtml(nm)}</span>`;
|
||
}).join('');
|
||
return `<div class="comment">
|
||
<div class="comment-head">
|
||
${c.user_avatar?`<img src="${c.user_avatar}" style="width:22px;height:22px;border-radius:50%;object-fit:cover" />`:''}
|
||
<span class="comment-author">${escHtml(c.user_display||c.author)}</span>
|
||
${c.user_provider?`<span style="font-size:11px;color:var(--muted);padding:1px 6px;background:var(--bg);border-radius:8px;border:0.5px solid var(--border)">${c.user_provider}</span>`:''}
|
||
${c.is_admin?'<span class="comment-admin-badge">crew</span>':''}
|
||
${teamBadges}
|
||
<span class="comment-time">${formatDate(c.created_at)}</span>
|
||
${ST.admin?`<button class="comment-del" data-cid="${c.id}" data-sid="${sid}">×</button>`:''}
|
||
</div>
|
||
<div class="comment-body">${escHtml(c.body)}</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
return chips + items;
|
||
}
|
||
|
||
function escHtml(s){return(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
function formatDate(iso){
|
||
const d=new Date(iso);
|
||
return d.toLocaleDateString('cs-CZ',{day:'numeric',month:'numeric',year:'numeric'})+' '+d.toLocaleTimeString('cs-CZ',{hour:'2-digit',minute:'2-digit'});
|
||
}
|
||
|
||
// ── Utility ────────────────────────────────────────────────────────
|
||
function tot(r){return CATS.reduce((a,c)=>a+(+r[c.k]||0),0);}
|
||
function gc(v,m){const p=v/m;return p>=.7?'#1D9E75':p>=.4?'#EF9F27':'#D85A30';}
|
||
function av(n){return(n||'?').slice(0,2).toUpperCase();}
|
||
function toast(msg,ok=true){
|
||
const t=document.createElement('div');
|
||
t.className='toast';t.style.borderColor=ok?'#1D9E75':'#D85A30';
|
||
t.textContent=msg;document.body.appendChild(t);
|
||
setTimeout(()=>t.remove(),3000);
|
||
}
|
||
|
||
// ── Render ─────────────────────────────────────────────────────────
|
||
function re(){document.getElementById('R').innerHTML=html();bind();}
|
||
|
||
function html(){
|
||
if(ST.loading) return `<nav><div class="logo">🐾 NaughtyBulldogs</div></nav><main><div class="empty"><span class="spinner"></span>Načítám…</div></main>`;
|
||
|
||
// Ban screen
|
||
if(ST.banned) return `
|
||
<nav><div class="logo">🐾 NaughtyBulldogs — Hodnocení streamerů</div></nav>
|
||
<main style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||
<div style="text-align:center;max-width:480px;padding:2rem">
|
||
<div style="font-size:48px;margin-bottom:1rem">🚫</div>
|
||
<div style="font-size:20px;font-weight:500;margin-bottom:.75rem">Váš účet byl zabanován</div>
|
||
<div style="font-size:15px;color:var(--muted);line-height:1.7;margin-bottom:1.5rem">
|
||
Pro odbanování nás kontaktujte přes:<br>
|
||
<strong>Instagram</strong> · <strong>Discord</strong> · nebo jiným způsobem přes<br>
|
||
<strong>NaughtyBulldogs</strong>
|
||
</div>
|
||
<a href="/api/oauth?action=logout" class="btn" style="text-decoration:none">Odhlásit se</a>
|
||
</div>
|
||
</main>`;
|
||
|
||
const liveEv=ST.db.filter(x=>x.status==='live'&&x.evaluated);
|
||
const u=ST.oauthUser;
|
||
return `<nav>
|
||
<div class="logo">🐾 NaughtyBulldogs — Hodnocení streamerů</div>
|
||
<button class="ntab ${ST.tab==='pub'?'on':''}" data-tab="pub">Přehled</button>
|
||
${(ST.admin||ST.myGroups?.length)?`<button class="ntab ${ST.tab==='rate'?'on':''}" data-tab="rate">Hodnotit</button>`:''}
|
||
${ST.isMod&&!ST.admin?`<button class="ntab ${ST.tab==='mod'?'on':''}" data-tab="mod">Hodnocení</button>`:''}
|
||
${ST.admin?`<button class="ntab ${ST.tab==='adm'?'on':''}" data-tab="adm">Správa</button>`:''}
|
||
<div class="ndiv"></div>
|
||
${u?`
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
${u.avatar?`<img src="${u.avatar}" style="width:28px;height:28px;border-radius:50%;object-fit:cover" />`:''}
|
||
<span style="font-size:14px;font-weight:500">${u.display_name}</span>
|
||
<span style="font-size:11px;color:var(--muted);padding:2px 7px;background:var(--bg);border-radius:10px;border:0.5px solid var(--border)">${u.provider}</span>
|
||
${ST.isMod&&!ST.admin?`<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:#EAF3DE;color:#3B6D11;border:0.5px solid #639922">mod</span>`:''}
|
||
<a class="btn-ghost btn-sm" href="/api/oauth?action=logout" style="text-decoration:none">Odhlásit</a>
|
||
</div>
|
||
`:`
|
||
${ST.settings.auth_twitch_enabled==='true'?`<a class="btn btn-sm" href="/api/oauth?provider=twitch" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z"/></svg> Twitch</a>`:``}
|
||
${ST.settings.auth_kick_enabled==='true'?`<a class="btn btn-sm" href="/api/oauth?provider=kick" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px;background:#53fc18;border-color:#3dd10f;color:#000"><span style="font-weight:700">K</span> Kick</a>`:``}
|
||
`}
|
||
${ST.admin?`<span class="adchip">⚙ Admin</span><button class="btn-ghost btn-sm" id="lo">Odhlásit</button>`:`<button class="btn-ghost btn-sm" id="li">Admin</button>`}
|
||
</nav>
|
||
<main>
|
||
${liveEv.length?`<div class="live-bar">
|
||
<div style="display:flex;align-items:center;gap:8px"><div class="dot"></div><span style="font-size:14px;font-weight:500">Právě live (${liveEv.length})</span></div>
|
||
<div class="live-chips">${liveEv.map(s=>`<a class="lchip" href="https://${s.platform==='kick'?'kick.com':'twitch.tv'}/${s.platform==='kick'?s.kick:s.name}" target="_blank"><div class="dot" style="width:6px;height:6px"></div><strong>${s.name}</strong>${s.game?`<span style="opacity:.6;font-size:11px">· ${s.game}</span>`:''}</a>`).join('')}</div>
|
||
</div>`:''}
|
||
${ST.tab==='adm'&&ST.admin?admView():ST.tab==='mod'&&ST.isMod&&!ST.admin?modView():ST.tab==='rate'&&(ST.admin||ST.myGroups?.length)?raterView():pubView()}
|
||
</main>
|
||
${ST.loginM?loginModal():''}
|
||
${ST.editM?editModal():''}
|
||
${ST.detailId?detailModal():''}
|
||
${ST.sugM?sugModal():''}
|
||
${ST.newM?newModal():''}
|
||
${ST.teamM?teamModal():''}
|
||
${ST.rateM?communityRateModal():''}`;
|
||
}
|
||
|
||
// ── Public view ────────────────────────────────────────────────────
|
||
function pubList(){
|
||
let l=ST.db.filter(x=>x.evaluated);
|
||
if(ST.search){const q=ST.search.toLowerCase();l=l.filter(x=>x.name.toLowerCase().includes(q));}
|
||
if(ST.fil==='live')l=l.filter(x=>x.status==='live');
|
||
else if(ST.fil==='twitch')l=l.filter(x=>x.platform==='twitch');
|
||
else if(ST.fil==='kick')l=l.filter(x=>x.platform==='kick');
|
||
return l;
|
||
}
|
||
function pubView(){
|
||
const list=pubList(),tot_ev=ST.db.filter(x=>x.evaluated).length;
|
||
return `<div>
|
||
<div class="suggest-bar"><p>Znáte zajímavého českého streamera? Navrhněte ho k hodnocení.</p><button class="btn btn-sm" id="sugbtn">+ Navrhnout streamera</button></div>
|
||
<div class="sbar"><input id="si" placeholder="Hledat ohodnoceného streamera…" value="${ST.search}"></div>
|
||
<div class="frow">
|
||
<span style="font-size:12px;color:var(--muted)">Filtr:</span>
|
||
${[['all','Všichni'],['live','🔴 Live'],['twitch','Twitch'],['kick','Kick']].map(([f,l])=>`<button class="fchip ${ST.fil===f?'on':''}" data-f="${f}">${l}</button>`).join('')}
|
||
<span class="cbadge">${list.length} z ${tot_ev}</span>
|
||
</div>
|
||
${list.length?`<div class="grid">${list.map(pubCard).join('')}</div>`:`<div class="empty">${ST.search?'Žádný streamer nenalezen':'Zatím žádní ohodnocení streamers'}</div>`}
|
||
</div>`;
|
||
}
|
||
function pubCard(s){
|
||
const t=tot(s.r),c=gc(t,MAX);
|
||
const cr=ST.communityRatings[s.id];
|
||
const crAvg=cr?.avg;
|
||
return `<div class="card card-pub" data-det="${s.id}">
|
||
<div class="ch"><div class="ava">${av(s.name)}</div>
|
||
<div style="flex:1;min-width:0"><div class="cn">${s.name}</div><div class="cs">${s.platform==='kick'?'Kick':'Twitch'}${s.game?` · ${s.game}`:''}</div></div>
|
||
${s.status==='live'?`<span class="badge-live"><div class="dot" style="width:6px;height:6px"></div>LIVE</span>`:`<span class="badge-off">Offline</span>`}
|
||
</div>
|
||
<div class="cb"><div class="srow">${CATS.map(c=>ms(c.l,s.r[c.k],10)).join('')}</div></div>
|
||
<div class="cf">
|
||
<div><div style="font-size:11px;color:var(--muted)">Celkem</div><div class="ts" style="color:${c}">${t}<span style="font-size:12px;opacity:.4">/${MAX}</span></div></div>
|
||
<div style="margin-left:auto;text-align:right"><div style="font-size:11px;color:var(--muted)">Odměna</div><div style="font-size:15px;font-weight:500">${s.reward?s.reward+' Kč':'—'}</div></div>
|
||
</div>
|
||
${crAvg?`<div style="padding:6px 1.25rem;border-top:0.5px solid var(--border);background:var(--bg);display:flex;align-items:center;gap:8px">
|
||
<span style="font-size:11px;color:var(--muted)">Komunita (${crAvg.count}×)</span>
|
||
<span style="font-size:14px;font-weight:500;color:${gc(crAvg.total,MAX)}">${crAvg.total}/${MAX}</span>
|
||
</div>`:''}
|
||
</div>`;
|
||
}
|
||
function ms(lbl,v,m){return `<div class="si"><div class="sn" style="color:${gc(v,m)}">${v}</div><div class="sl">${lbl}</div></div>`;}
|
||
|
||
// ── Admin view ─────────────────────────────────────────────────────
|
||
function admList(){
|
||
let l=[...ST.db];
|
||
if(ST.search){const q=ST.search.toLowerCase();l=l.filter(x=>x.name.toLowerCase().includes(q));}
|
||
if(ST.fil==='live')l=l.filter(x=>x.status==='live');
|
||
else if(ST.fil==='ev')l=l.filter(x=>x.evaluated);
|
||
else if(ST.fil==='pend')l=l.filter(x=>!x.evaluated);
|
||
else if(ST.fil==='sug')l=l.filter(x=>x.added_by==='viewer');
|
||
return l;
|
||
}
|
||
function admView(){
|
||
const sec=ST.admSection||'streamers';
|
||
return `<div>
|
||
<div style="display:flex;border-bottom:0.5px solid var(--border);margin-bottom:1.25rem;gap:4px;flex-wrap:wrap">
|
||
<button class="ntab ${sec==='streamers'?'on':''}" data-asec="streamers" style="border-radius:0">Streameři</button>
|
||
<button class="ntab ${sec==='users'?'on':''}" data-asec="users" style="border-radius:0">Uživatelé${ST.allUsers?.length?` <span style="font-size:11px;color:var(--muted)">(${ST.allUsers.length})</span>`:''}</button>
|
||
<button class="ntab ${sec==='teams'?'on':''}" data-asec="teams" style="border-radius:0">Týmy${ST.raterGroups?.length?` <span style="font-size:11px;color:var(--muted)">(${ST.raterGroups.length})</span>`:''}</button>
|
||
<button class="ntab ${sec==='settings'?'on':''}" data-asec="settings" style="border-radius:0">Nastavení</button>
|
||
</div>
|
||
${sec==='streamers'?admStreamers():sec==='users'?admUsers():sec==='teams'?admTeams():admSettings()}
|
||
</div>`;
|
||
}
|
||
|
||
function admStreamers(){
|
||
const list=admList(),pend=ST.db.filter(x=>!x.evaluated).length,sug=ST.db.filter(x=>x.added_by==='viewer').length;
|
||
return `<div>
|
||
<div style="display:flex;gap:10px;margin-bottom:1.25rem;align-items:center;flex-wrap:wrap">
|
||
<button class="btn btn-primary btn-sm" id="newbtn">+ Přidat streamera</button>
|
||
<button class="btn btn-sm" id="rfbtn">↻ Live refresh</button>
|
||
${pend?`<span style="font-size:13px;color:var(--muted)">${pend} čeká</span>`:''}
|
||
${sug?`<span style="font-size:13px;color:var(--purple-d)">${sug} návrhů</span>`:''}
|
||
<div style="margin-left:auto"><div class="tv"><button class="${ST.av==='cards'?'on':''}" data-av="cards">Karty</button><button class="${ST.av==='table'?'on':''}" data-av="table">Tabulka</button></div></div>
|
||
</div>
|
||
<div class="sbar"><input id="si" placeholder="Hledat streamera…" value="${ST.search}"></div>
|
||
<div class="frow">
|
||
${[['all','Všichni'],['live','🔴 Live'],['ev','Ohodnocení'],['pend','Čekají'],['sug','Návrhy']].map(([f,l])=>`<button class="fchip ${ST.fil===f?'on':''}" data-f="${f}">${l}</button>`).join('')}
|
||
<span class="cbadge">${list.length}</span>
|
||
</div>
|
||
${ST.av==='table'?admTable(list):admCards(list)}
|
||
</div>`;
|
||
}
|
||
|
||
function admUsers(){
|
||
const mods=ST.currentMods||[];
|
||
const users=ST.allUsers||[];
|
||
return `<div>
|
||
<div class="st">Přidat moderátora</div>
|
||
<div style="display:flex;gap:8px;align-items:flex-end;margin-bottom:.5rem;flex-wrap:wrap">
|
||
<div style="flex:1;min-width:200px"><div class="fl">Uživatelské jméno</div><input id="mod-login" placeholder="twitch_username"></div>
|
||
<div style="width:130px"><div class="fl">Platforma</div><select id="mod-provider"><option value="twitch">Twitch</option><option value="kick">Kick</option></select></div>
|
||
<button class="btn btn-primary btn-sm" id="mod-add">Přidat</button>
|
||
</div>
|
||
<div id="mod-err" style="color:var(--red);font-size:13px;display:none;margin-bottom:.5rem"></div>
|
||
|
||
<div class="st" style="margin-top:1.5rem">Aktuální moderátoři (${mods.length})</div>
|
||
${mods.length===0?`<div style="font-size:14px;color:var(--muted);padding:.4rem 0">Žádní moderátoři</div>`:''}
|
||
${mods.map(m=>`<div style="display:flex;align-items:center;gap:10px;padding:.6rem 0;border-bottom:0.5px solid var(--border)">
|
||
${m.avatar?`<img src="${m.avatar}" style="width:32px;height:32px;border-radius:50%" />`:`<div class="ava" style="width:32px;height:32px;font-size:12px">${av(m.display_name)}</div>`}
|
||
<div style="flex:1"><div style="font-size:14px;font-weight:500">${m.display_name}</div><div style="font-size:12px;color:var(--muted)">${m.provider} · ${m.login}</div></div>
|
||
<button class="btn btn-sm btn-danger" data-delmod="${m.mod_id}">Odebrat</button>
|
||
</div>`).join('')}
|
||
|
||
<div class="st" style="margin-top:1.5rem">Všichni přihlášení uživatelé (${users.length})</div>
|
||
${users.length===0?`<div style="font-size:14px;color:var(--muted);padding:.4rem 0">Žádní uživatelé</div>`:''}
|
||
${users.map(u=>`<div style="display:flex;align-items:center;gap:10px;padding:.6rem 0;border-bottom:0.5px solid var(--border);${u.banned?'opacity:.5':''}">
|
||
${u.avatar?`<img src="${u.avatar}" style="width:32px;height:32px;border-radius:50%" />`:`<div class="ava" style="width:32px;height:32px;font-size:12px">${av(u.display_name)}</div>`}
|
||
<div style="flex:1">
|
||
<div style="font-size:14px;font-weight:500">${u.display_name} ${u.is_mod?'<span style="font-size:11px;padding:1px 7px;background:#EAF3DE;color:#3B6D11;border-radius:8px">mod</span>':''} ${u.banned?'<span style="font-size:11px;padding:1px 7px;background:#FAECE7;color:#993C1D;border-radius:8px">ban</span>':''}</div>
|
||
<div style="font-size:12px;color:var(--muted)">${u.provider} · ${u.login} · naposledy ${formatDate(u.last_seen)}</div>
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||
${u.is_mod
|
||
?`<button class="btn btn-sm" data-revmod="${u.id}">Odebrat mod</button>`
|
||
:`<button class="btn btn-sm btn-primary" data-givemod="${u.id}" data-login="${u.login}" data-provider="${u.provider}" ${u.banned?'disabled':''}>+Mod</button>`}
|
||
${u.banned
|
||
?`<button class="btn btn-sm" data-unban="${u.id}">Odbanovat</button>`
|
||
:`<button class="btn btn-sm btn-danger" data-ban="${u.id}">Ban</button>`}
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
function admTeams(){
|
||
const groups=ST.raterGroups||[];
|
||
// Streamers already linked to a team — exclude from create dropdown
|
||
const linkedSids=new Set(groups.map(g=>g.streamer_id).filter(Boolean));
|
||
const availableStreamers=ST.db.filter(s=>!linkedSids.has(s.id));
|
||
return `<div>
|
||
<div style="font-size:13px;color:var(--muted);margin-bottom:1rem">Tým hodnotitelů reprezentuje konkrétního streamera — každý jeho člen může hodnotit ostatní streamery (ne sebe). Průměr týmu se zobrazí jako komunitní hodnocení.</div>
|
||
<div class="st">Vytvořit tým</div>
|
||
<div style="display:grid;gap:8px;grid-template-columns:1fr 1fr auto;margin-bottom:.5rem;align-items:end">
|
||
<div><div class="fl">Název týmu</div><input id="rg-name" placeholder="např. NaughtyBulldogs"></div>
|
||
<div><div class="fl">Streamer (volitelný)</div>
|
||
<select id="rg-streamer">
|
||
<option value="">— bez vazby na streamera —</option>
|
||
${availableStreamers.map(s=>`<option value="${s.id}">${escHtml(s.name)} (${s.platform})</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" id="rg-create">Vytvořit</button>
|
||
</div>
|
||
<div id="rg-err" style="color:var(--red);font-size:13px;display:none;margin-bottom:.5rem"></div>
|
||
|
||
<div class="st" style="margin-top:1.5rem">Týmy (${groups.length})</div>
|
||
${groups.length===0?`<div style="font-size:14px;color:var(--muted);padding:.4rem 0">Žádné týmy</div>`:''}
|
||
${groups.map(g=>`<div style="border:0.5px solid var(--border);border-radius:10px;padding:.85rem 1rem;margin-bottom:.6rem;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<div style="flex:1;min-width:160px">
|
||
<div style="font-size:15px;font-weight:500">${escHtml(g.name)}${g.streamer_name?` <span style="font-size:12px;color:var(--purple-d);background:var(--purple-l);padding:1px 8px;border-radius:8px;font-weight:400">→ ${escHtml(g.streamer_name)}</span>`:''}</div>
|
||
<div style="font-size:12px;color:var(--muted)">${g.members.length} členů · ${g.ratings_count} hodnocení</div>
|
||
</div>
|
||
<button class="btn btn-sm" data-edteam="${g.id}" title="Upravit vazbu">🔗</button>
|
||
<button class="btn btn-sm" data-team="${g.id}">👥 Spravovat</button>
|
||
<button class="btn btn-sm btn-danger" data-delrg="${g.id}">×</button>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
function admSettings(){
|
||
const s=ST.settings;
|
||
return `<div style="max-width:560px">
|
||
<div class="st">Komentáře — autorizace</div>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:.5rem 0;border-bottom:0.5px solid var(--border)">
|
||
<div>
|
||
<div style="font-size:15px;font-weight:500">Vyžadovat přihlášení pro komentáře</div>
|
||
<div style="font-size:13px;color:var(--muted)">Pokud vypnuto, stačí zadat přezdívku</div>
|
||
</div>
|
||
<input type="checkbox" id="set-auth" ${s.auth_enabled==='true'?'checked':''} style="width:18px;height:18px;cursor:pointer">
|
||
</div>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:.5rem 0;border-bottom:0.5px solid var(--border)">
|
||
<div>
|
||
<div style="font-size:15px;font-weight:500">Povolit přihlášení přes Twitch</div>
|
||
<div style="font-size:13px;color:var(--muted)">Zobrazí tlačítko "Přihlásit přes Twitch"</div>
|
||
</div>
|
||
<input type="checkbox" id="set-twitch" ${s.auth_twitch_enabled==='true'?'checked':''} style="width:18px;height:18px;cursor:pointer">
|
||
</div>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:.5rem 0">
|
||
<div>
|
||
<div style="font-size:15px;font-weight:500">Povolit přihlášení přes Kick</div>
|
||
<div style="font-size:13px;color:var(--muted)">Vyžaduje Kick OAuth app na kick.com/settings/developer</div>
|
||
</div>
|
||
<input type="checkbox" id="set-kick" ${s.auth_kick_enabled==='true'?'checked':''} style="width:18px;height:18px;cursor:pointer">
|
||
</div>
|
||
<div style="margin-top:1.25rem;display:flex;gap:8px">
|
||
<button class="btn btn-primary btn-sm" id="sett-save">Uložit nastavení</button>
|
||
<span id="sett-msg" style="font-size:13px;color:var(--muted);align-self:center"></span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
function admCards(list){
|
||
if(!list.length)return`<div class="empty">Žádný streamer</div>`;
|
||
return `<div class="grid">${list.map(admCard).join('')}</div>`;
|
||
}
|
||
function admCard(s){
|
||
const t=tot(s.r),c=gc(t,MAX);
|
||
return `<div class="card">
|
||
<div class="ch"><div class="ava">${av(s.name)}</div>
|
||
<div style="flex:1;min-width:0">
|
||
<div class="cn">${s.name} ${s.added_by==='viewer'?'<span class="tag">návrh</span>':''}</div>
|
||
<div class="cs">${s.platform==='kick'?'Kick':'Twitch'}${s.game?` · ${s.game}`:''}</div>
|
||
<div class="lrow">
|
||
${s.platform==='kick'
|
||
?`<a class="lb" href="https://kick.com/${s.kick||s.name}" target="_blank">Kick</a>
|
||
<a class="lb" href="https://streamscharts.com/channels/${s.kick||s.name}?platform=kick" target="_blank">StreamsCharts</a>
|
||
<a class="lb" href="https://streamelements.com/${s.kick||s.name}" target="_blank">StreamElements</a>`
|
||
:`<a class="lb" href="https://twitch.tv/${s.name}" target="_blank">Twitch</a>
|
||
|
||
<a class="lb" href="https://streamscharts.com/channels/${s.name}" target="_blank">StreamsCharts</a>
|
||
<a class="lb" href="https://streamelements.com/${s.name}" target="_blank">StreamElements</a>
|
||
${s.kick?`<a class="lb" href="https://kick.com/${s.kick}" target="_blank">Kick</a>`:''}`}
|
||
</div>
|
||
</div>
|
||
${s.status==='live'?`<span class="badge-live"><div class="dot" style="width:6px;height:6px"></div>LIVE</span>`:`<span class="badge-off">Offline</span>`}
|
||
</div>
|
||
${s.evaluated?`<div class="cb"><div class="srow">${CATS.map(c=>ms(c.l,s.r[c.k],10)).join('')}</div></div>
|
||
<div class="cf">
|
||
<div><div style="font-size:11px;color:var(--muted)">Skóre</div><div class="ts" style="color:${c}">${t}<span style="font-size:12px;opacity:.4">/${MAX}</span></div></div>
|
||
<div style="margin-left:auto;text-align:right"><div style="font-size:11px;color:var(--muted)">Odměna</div><div style="font-size:15px;font-weight:500">${s.reward?s.reward+' Kč':'—'}</div></div>
|
||
<div style="display:flex;gap:5px;margin-left:10px"><button class="btn btn-sm" data-ed="${s.id}">Upravit</button><button class="btn btn-sm" data-rst="${s.id}" title="Resetovat hodnocení" style="border-color:#f09595;color:#a32d2d">↺</button><button class="btn btn-sm" data-lock="${s.id}" data-locked="${s.community_locked}" title="${s.community_locked?'Odemknout':'Zamknout'} komunitní hodnocení" style="border-color:#aaa">${s.community_locked?'🔒':'🔓'}</button><button class="btn btn-sm btn-danger" data-dl="${s.id}">×</button></div>
|
||
</div>`:`<div class="cf" style="justify-content:space-between">
|
||
<span style="font-size:13px;color:var(--muted)">Čeká na hodnocení</span>
|
||
<div style="display:flex;gap:5px"><button class="btn btn-sm btn-primary" data-ed="${s.id}">+ Hodnotit</button><button class="btn btn-sm btn-danger" data-dl="${s.id}">×</button></div>
|
||
</div>`}
|
||
</div>`;
|
||
}
|
||
function admTable(list){
|
||
return `<div style="overflow-x:auto"><table>
|
||
<thead><tr><th>Streamer</th>${CATS.map(c=>`<th>${c.l.slice(0,5)}.</th>`).join('')}<th>Celkem</th><th>Odměna</th><th></th></tr></thead>
|
||
<tbody>${list.map(s=>{const t=tot(s.r);return`<tr>
|
||
<td><strong>${s.name}</strong>${s.added_by==='viewer'?' <span class="tag" style="font-size:10px">návrh</span>':''}<br><span style="font-size:11px;color:var(--muted)">${s.status==='live'?'🔴 live':'offline'}</span></td>
|
||
${s.evaluated?CATS.map(c=>`<td style="color:${gc(s.r[c.k],10)};font-weight:500">${s.r[c.k]}</td>`).join(''):`<td colspan="${CATS.length}" style="color:var(--muted)">—</td>`}
|
||
<td style="font-weight:500;color:${gc(t,MAX)}">${s.evaluated?`${t}/${MAX}`:'—'}</td>
|
||
<td>${s.reward?s.reward+' Kč':'—'}</td>
|
||
<td><div style="display:flex;gap:4px"><button class="btn btn-sm" data-ed="${s.id}">Upravit</button><button class="btn btn-sm" data-rst="${s.id}" title="Reset" style="border-color:#f09595;color:#a32d2d">↺</button><button class="btn btn-sm btn-danger" data-dl="${s.id}">×</button></div></td>
|
||
</tr>`;}).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
// ── Modals ─────────────────────────────────────────────────────────
|
||
function loginModal(){return `<div class="mbg" id="lmbg">
|
||
<div class="modal modal-sm">
|
||
<div class="mh"><div style="flex:1"><div style="font-size:16px;font-weight:500">Admin přihlášení</div><div style="font-size:13px;color:var(--muted)">Správa hodnocení a streamerů</div></div><button class="btn-ghost" id="lmx">✕</button></div>
|
||
<div class="mb"><div class="fg"><div class="fl">Heslo</div><input id="lpw" type="password" placeholder="Admin heslo"></div><div id="lerr" style="color:var(--red);font-size:13px;display:none;margin-top:-6px">Nesprávné heslo</div></div>
|
||
<div class="mf"><button class="btn" id="lmcancel">Zrušit</button><button class="btn btn-primary" id="lmok">Přihlásit</button></div>
|
||
</div></div>`;}
|
||
|
||
function editModal(){
|
||
const s=ST.editId?ST.db.find(x=>x.id===ST.editId):null;
|
||
const r=s?{...s.r}:{s:0,g:0,a:0,v:0,n:0,ns:0,o:0};
|
||
return `<div class="mbg" id="embg">
|
||
<div class="modal">
|
||
<div class="mh"><div class="ava" style="width:38px;height:38px;font-size:13px">${av(s?s.name:'?')}</div>
|
||
<div style="flex:1"><div style="font-size:16px;font-weight:500">${s?s.name:'—'}</div><div style="font-size:13px;color:var(--muted)">Hodnocení (0–10 bodů per kategorie)</div></div>
|
||
<button class="btn-ghost" id="emx">✕</button></div>
|
||
<div class="mb"><form id="ef">
|
||
<div class="st">Platforma & Status</div>
|
||
<div class="fgr">
|
||
${s&&s.platform==='kick'
|
||
? `<div class="fg"><div class="fl" style="color:var(--muted);font-size:12px">Kick streamer — jméno nelze změnit zde</div><input name="kick" value="${s?s.kick||s.name:''}" type="hidden"><div style="font-size:14px;padding:7px 0;color:var(--muted)">${s?s.kick||s.name:''}</div></div>`
|
||
: `<div class="fg"><div class="fl">Kick jméno (pokud má i Kick kanál)</div><input name="kick" value="${s?s.kick||'':''}" placeholder="kick_username"></div>`}
|
||
<div class="fg"><div class="fl">Status</div><select name="status"><option value="offline" ${!s||s.status==='offline'?'selected':''}>Offline</option><option value="live" ${s&&s.status==='live'?'selected':''}>LIVE</option></select></div>
|
||
</div>
|
||
<div class="fgr">
|
||
<div class="fg"><div class="fl">Hra</div><input name="game" value="${s?s.game||'':''}" placeholder="Just Chatting…"></div>
|
||
<div class="fg"><div class="fl">Titulek</div><input name="title" value="${s?s.title||'':''}" placeholder="Titulek streamu"></div>
|
||
</div>
|
||
<div class="st">Hodnocení</div>
|
||
${CATS.map(c=>sl(c.k,`${c.l} (0–10)`,r[c.k]||0,10)).join('')}
|
||
<div class="st">Finanční odměna & Poznámky</div>
|
||
<div class="fg">
|
||
<div class="fl">Odměna (automaticky dle skóre: 30–130 Kč)</div>
|
||
<div style="display:flex;align-items:center;gap:16px;padding:8px 0">
|
||
<div style="font-size:28px;font-weight:600;color:var(--purple)" id="auto-reward">${autoReward(r)} Kč</div>
|
||
<div style="font-size:13px;color:var(--muted)">= 30 + (skóre / 70) × 100, zaokrouhleno</div>
|
||
</div>
|
||
<input type="hidden" name="reward" id="rw" value="${autoReward(r)}">
|
||
</div>
|
||
<div class="fg"><div class="fl">Poznámky</div><textarea name="notes" rows="3" style="resize:vertical">${s?s.notes||'':''}</textarea></div>
|
||
</form></div>
|
||
<div class="mf"><button class="btn" id="emcancel">Zrušit</button><button class="btn btn-primary" id="emsave">Uložit</button></div>
|
||
</div></div>`;
|
||
}
|
||
function sl(nm,lbl,val,max){const c=gc(val,max);return `<div class="fg"><div class="fl">${lbl}</div><div class="ri"><input type="range" name="${nm}" min="0" max="${max}" step="1" value="${val}" id="r${nm}"><div class="rv" id="rv${nm}" style="color:${c}">${val}</div></div></div>`;}
|
||
|
||
function detailModal(){
|
||
const s=ST.db.find(x=>x.id===ST.detailId);if(!s)return'';
|
||
const t=tot(s.r),c=gc(t,MAX);
|
||
const cr=ST.communityRatings[s.id];
|
||
const crAvg=cr?.avg;
|
||
const crRatings=cr?.ratings||[];
|
||
const authOn=ST.settings.auth_enabled==='true';
|
||
const u=ST.oauthUser;
|
||
return `<div class="mbg" id="dmbg">
|
||
<div class="modal">
|
||
<div class="mh">
|
||
<div class="ava" style="width:48px;height:48px;font-size:16px">${av(s.name)}</div>
|
||
<div style="flex:1">
|
||
<div style="font-size:17px;font-weight:500">${s.name}</div>
|
||
<div style="font-size:13px;color:var(--muted)">${s.platform==='kick'?'Kick':'Twitch'} · ${s.status==='live'?'<span style="color:var(--red)">🔴 LIVE</span>':'Offline'}</div>
|
||
<div class="lrow">
|
||
${s.platform==='kick'
|
||
?`<a class="lb" href="https://kick.com/${s.kick||s.name}" target="_blank">Kick</a>
|
||
<a class="lb" href="https://streamscharts.com/channels/${s.kick||s.name}?platform=kick" target="_blank">StreamsCharts</a>
|
||
<a class="lb" href="https://streamelements.com/${s.kick||s.name}" target="_blank">StreamElements</a>`
|
||
:`<a class="lb" href="https://twitch.tv/${s.name}" target="_blank">Twitch</a>
|
||
<a class="lb" href="https://streamscharts.com/channels/${s.name}" target="_blank">StreamsCharts</a>
|
||
<a class="lb" href="https://streamelements.com/${s.name}" target="_blank">StreamElements</a>
|
||
${s.kick?`<a class="lb" href="https://kick.com/${s.kick}" target="_blank">Kick</a>`:''}`}
|
||
</div>
|
||
</div>
|
||
<button class="btn-ghost" id="dmx">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<!-- Official rating -->
|
||
<div style="font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);margin-bottom:.5rem">Oficiální hodnocení</div>
|
||
<div style="display:flex;align-items:flex-end;gap:2rem;margin-bottom:1.5rem">
|
||
<div><div style="font-size:12px;color:var(--muted)">Celkové skóre</div><div style="font-size:40px;font-weight:600;color:${c};line-height:1">${t}<span style="font-size:17px;opacity:.4">/${MAX}</span></div></div>
|
||
${s.reward?`<div><div style="font-size:12px;color:var(--muted)">Finanční odměna</div><div style="font-size:26px;font-weight:500">${s.reward} Kč</div></div>`:''}
|
||
</div>
|
||
${CATS.map(cat=>{const v=s.r[cat.k]||0;return`<div class="bw"><div class="bl">${cat.l}</div><div class="bb"><div class="bf" style="width:${Math.round(v/10*100)}%;background:${gc(v,10)}"></div></div><div class="bv" style="color:${gc(v,10)}">${v}/10</div></div>`;}).join('')}
|
||
${s.notes?`<div style="margin-top:1rem;padding-top:1rem;border-top:0.5px solid var(--border)"><div style="font-size:12px;color:var(--muted);margin-bottom:.5rem">Poznámky</div><div style="font-size:14px;line-height:1.6;color:var(--muted)">${s.notes}</div></div>`:''}
|
||
|
||
<!-- Community ratings -->
|
||
<div style="margin-top:1.5rem;padding-top:1rem;border-top:0.5px solid var(--border)">
|
||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:.75rem">
|
||
<div style="font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)">Hodnocení komunit${crAvg?` (${crAvg.count})`:''}</div>
|
||
${s.community_locked?`<span style="font-size:11px;padding:2px 8px;border-radius:8px;background:#FAECE7;color:#993C1D;border:0.5px solid var(--red)">🔒 Uzamčeno</span>`:''}
|
||
</div>
|
||
${crAvg?(()=>{
|
||
// Validate active CR tab — fall back to overview if it points to a removed team
|
||
let curTab=ST.detailCRTab;
|
||
if(curTab!=='overview' && !crRatings.find(r=>r.group_id===curTab)) curTab='overview';
|
||
return `
|
||
<div style="display:flex;gap:4px;border-bottom:0.5px solid var(--border);margin-bottom:1rem;flex-wrap:wrap;overflow-x:auto">
|
||
<button class="ntab ${curTab==='overview'?'on':''}" data-crtab="overview" style="border-radius:0;padding:.5rem .75rem;font-size:13px">Průměr</button>
|
||
${crRatings.map(r=>{const rt=CATS.reduce((a,c)=>a+(+r[c.k]||0),0);return `<button class="ntab ${curTab===r.group_id?'on':''}" data-crtab="${r.group_id}" style="border-radius:0;padding:.5rem .75rem;font-size:13px">${escHtml(r.group_name)} <span style="opacity:.6;margin-left:4px">${rt}/${MAX}</span></button>`;}).join('')}
|
||
</div>
|
||
${curTab==='overview'?`
|
||
<div style="display:flex;align-items:center;gap:1.5rem;margin-bottom:1rem">
|
||
<div><div style="font-size:12px;color:var(--muted)">Průměr ze všech týmů</div><div style="font-size:28px;font-weight:600;color:${gc(crAvg.total,MAX)};line-height:1">${crAvg.total}<span style="font-size:14px;opacity:.4">/${MAX}</span></div></div>
|
||
</div>
|
||
${CATS.map(cat=>{const v=crAvg[cat.k]||0;return`<div class="bw"><div class="bl">${cat.l}</div><div class="bb"><div class="bf" style="width:${Math.round(v/10*100)}%;background:${gc(v,10)};opacity:.7"></div></div><div class="bv" style="color:${gc(v,10)}">${v}/10</div></div>`;}).join('')}
|
||
`:(()=>{
|
||
const r=crRatings.find(x=>x.group_id===curTab);if(!r)return '';
|
||
const rt=CATS.reduce((a,c)=>a+(+r[c.k]||0),0);
|
||
return `
|
||
<div style="display:flex;align-items:center;gap:1.5rem;margin-bottom:1rem">
|
||
<div><div style="font-size:12px;color:var(--muted)">Hodnocení od týmu <strong>${escHtml(r.group_name)}</strong></div><div style="font-size:28px;font-weight:600;color:${gc(rt,MAX)};line-height:1">${rt}<span style="font-size:14px;opacity:.4">/${MAX}</span></div></div>
|
||
${ST.admin?`<button class="btn btn-sm btn-danger" data-delcr="${r.id}" style="margin-left:auto">× Smazat</button>`:''}
|
||
</div>
|
||
${CATS.map(cat=>{
|
||
const v=r[cat.k]||0;
|
||
const _key={s:'statistiky',g:'grafika',a:'alerty',v:'vychytavky',n:'nahled',ns:'nastaveni',o:'odlisnost'}[cat.k];
|
||
const v2=r[_key]??v;
|
||
return `<div class="bw"><div class="bl">${cat.l}</div><div class="bb"><div class="bf" style="width:${Math.round(v2/10*100)}%;background:${gc(v2,10)};opacity:.85"></div></div><div class="bv" style="color:${gc(v2,10)}">${v2}/10</div></div>`;
|
||
}).join('')}
|
||
${r.notes?`<div style="margin-top:.85rem;padding:.7rem .9rem;background:var(--purple-l);border-left:3px solid var(--purple-d);border-radius:6px"><div style="font-size:11px;color:var(--purple-d);font-weight:500;text-transform:uppercase;letter-spacing:.4px;margin-bottom:.3rem">Poznámka týmu</div><div style="font-size:14px;line-height:1.5;white-space:pre-wrap">${escHtml(r.notes)}</div></div>`:''}
|
||
`;
|
||
})()}
|
||
`;})():`<div style="font-size:14px;color:var(--muted)">Zatím žádná komunitní hodnocení</div>`}
|
||
</div>
|
||
|
||
<!-- Comments -->
|
||
<div style="margin-top:1.5rem;padding-top:1rem;border-top:0.5px solid var(--border)">
|
||
<div style="font-size:13px;font-weight:500;margin-bottom:.75rem">Komentáře</div>
|
||
<div id="comments-list">
|
||
${ST.comments[s.id]!=null?renderCommentsList(s.id):`<button class="btn btn-sm" id="comments-load" data-sid="${s.id}">Načíst komentáře</button>`}
|
||
</div>
|
||
<div class="comment-form" id="comment-form">
|
||
${authOn&&!u&&!ST.admin?`<div style="font-size:14px;color:var(--muted);padding:.5rem 0">
|
||
Pro komentování je vyžadováno přihlášení.
|
||
${ST.settings.auth_twitch_enabled==='true'?`<a class="btn btn-sm btn-primary" href="/api/oauth?provider=twitch" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px;margin-left:8px"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z"/></svg> Twitch</a>`:``}
|
||
${ST.settings.auth_kick_enabled==='true'?`<a class="btn btn-sm" href="/api/oauth?provider=kick" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px;margin-left:4px;background:#53fc18;border-color:#3dd10f;color:#000"><span style="font-weight:700">K</span> Kick</a>`:``}
|
||
</div>`:u?`<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||
${u.avatar?`<img src="${u.avatar}" style="width:24px;height:24px;border-radius:50%" />`:''}
|
||
<span style="font-size:14px;font-weight:500">${u.display_name}</span>
|
||
<span style="font-size:11px;color:var(--muted)">${u.provider}</span>
|
||
</div>
|
||
<textarea id="c-body" rows="2" placeholder="Napište komentář…" style="resize:vertical"></textarea>
|
||
<div><button class="btn btn-sm btn-primary" id="c-submit" data-sid="${s.id}">Odeslat</button></div>
|
||
`:`<input id="c-author" placeholder="Vaše přezdívka (nepovinné)" style="max-width:220px">
|
||
<textarea id="c-body" rows="2" placeholder="Napište komentář…" style="resize:vertical"></textarea>
|
||
<div><button class="btn btn-sm btn-primary" id="c-submit" data-sid="${s.id}">Odeslat</button></div>`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
function modView(){
|
||
// Moderator view — same as admin cards but without add/delete/settings
|
||
const list=[...ST.db];
|
||
const filtered=list.filter(s=>{
|
||
if(ST.search){const q=ST.search.toLowerCase();if(!s.name.toLowerCase().includes(q))return false;}
|
||
if(ST.fil==='live')return s.status==='live';
|
||
if(ST.fil==='ev')return s.evaluated;
|
||
if(ST.fil==='pend')return !s.evaluated;
|
||
return true;
|
||
});
|
||
return `<div>
|
||
<div style="padding:.6rem 1rem;background:#EAF3DE;border:0.5px solid #639922;border-radius:10px;margin-bottom:1.25rem;font-size:14px;color:#3B6D11">
|
||
Přihlášen jako moderátor — můžeš hodnotit streamery a spravovat komentáře.
|
||
</div>
|
||
<div class="sbar"><input id="si" placeholder="Hledat streamera…" value="${ST.search}"></div>
|
||
<div class="frow">
|
||
${[['all','Všichni'],['live','🔴 Live'],['ev','Ohodnocení'],['pend','Čekají']].map(([f,l])=>`<button class="fchip ${ST.fil===f?'on':''}" data-f="${f}">${l}</button>`).join('')}
|
||
<span class="cbadge">${filtered.length} streamerů</span>
|
||
</div>
|
||
<div class="grid">${filtered.map(s=>modCard(s)).join('')}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function modCard(s){
|
||
const t=tot(s.r),c=gc(t,MAX);
|
||
return `<div class="card" style="cursor:default">
|
||
<div class="ch"><div class="ava">${av(s.name)}</div>
|
||
<div style="flex:1;min-width:0">
|
||
<div class="cn">${s.name}</div>
|
||
<div class="cs">${s.platform==='kick'?'Kick':'Twitch'}${s.game?` · ${s.game}`:''}</div>
|
||
<div class="lrow">
|
||
${s.platform==='kick'
|
||
?`<a class="lb" href="https://kick.com/${s.kick||s.name}" target="_blank">Kick</a>
|
||
<a class="lb" href="https://streamscharts.com/channels/${s.kick||s.name}?platform=kick" target="_blank">StreamsCharts</a>
|
||
<a class="lb" href="https://streamelements.com/${s.kick||s.name}" target="_blank">StreamElements</a>`
|
||
:`<a class="lb" href="https://twitch.tv/${s.name}" target="_blank">Twitch</a>
|
||
<a class="lb" href="https://streamscharts.com/channels/${s.name}" target="_blank">StreamsCharts</a>
|
||
<a class="lb" href="https://streamelements.com/${s.name}" target="_blank">StreamElements</a>
|
||
${s.kick?`<a class="lb" href="https://kick.com/${s.kick}" target="_blank">Kick</a>`:''}`}
|
||
</div>
|
||
</div>
|
||
${s.status==='live'?`<span class="badge-live"><div class="dot" style="width:6px;height:6px"></div>LIVE</span>`:`<span class="badge-off">Offline</span>`}
|
||
</div>
|
||
${s.evaluated?`<div class="cb"><div class="srow">${CATS.map(c=>ms(c.l,s.r[c.k],10)).join('')}</div></div>
|
||
<div class="cf">
|
||
<div><div style="font-size:11px;color:var(--muted)">Skóre</div><div class="ts" style="color:${c}">${t}<span style="font-size:12px;opacity:.4">/${MAX}</span></div></div>
|
||
<div style="margin-left:auto;text-align:right"><div style="font-size:11px;color:var(--muted)">Odměna</div><div style="font-size:15px;font-weight:500">${s.reward?s.reward+' Kč':'—'}</div></div>
|
||
<div style="margin-left:10px"><button class="btn btn-sm" data-ed="${s.id}">Upravit</button></div>
|
||
</div>`:`<div class="cf" style="justify-content:space-between">
|
||
<span style="font-size:13px;color:var(--muted)">Čeká na hodnocení</span>
|
||
<button class="btn btn-sm btn-primary" data-ed="${s.id}">+ Hodnotit</button>
|
||
</div>`}
|
||
</div>`;
|
||
}
|
||
|
||
// ── Rater view (members + admin) ──────────────────────────────────
|
||
function raterView(){
|
||
// Available teams to rate as
|
||
// - admin: all teams (ST.raterGroups)
|
||
// - member: only teams they belong to (ST.myGroups)
|
||
const teams = ST.admin ? (ST.raterGroups||[]) : (ST.myGroups||[]);
|
||
|
||
if(!teams.length){
|
||
return `<div class="empty">
|
||
${ST.admin
|
||
? 'Zatím nejsou žádné týmy. Vytvoř tým ve Správa → Týmy.'
|
||
: 'Nejste členem žádného týmu. Požádejte admina o přidání.'}
|
||
</div>`;
|
||
}
|
||
|
||
// Default-pick a team if none selected (or selected one no longer exists)
|
||
if(!ST.activeTeamId || !teams.find(t=>t.id===ST.activeTeamId)){
|
||
ST.activeTeamId = teams[0].id;
|
||
}
|
||
const team = teams.find(t=>t.id===ST.activeTeamId);
|
||
const linkedSid = team?.streamer_id || null;
|
||
|
||
// Show all streamers, exclude the team's own streamer
|
||
const list = ST.db.filter(s=>{
|
||
if(linkedSid && s.id===linkedSid) return false;
|
||
if(ST.search){const q=ST.search.toLowerCase();if(!s.name.toLowerCase().includes(q))return false;}
|
||
return true;
|
||
});
|
||
|
||
const teamPicker = teams.length>1
|
||
? `<select id="team-picker" style="max-width:260px">
|
||
${teams.map(t=>`<option value="${t.id}" ${t.id===ST.activeTeamId?'selected':''}>${escHtml(t.name)}${t.streamer_id?' · '+escHtml(t.streamer_name||'#'+t.streamer_id):''}</option>`).join('')}
|
||
</select>`
|
||
: `<strong>${escHtml(team.name)}</strong>${team.streamer_id?` · <span style="opacity:.7">${escHtml(team.streamer_name||'')}</span>`:''}`;
|
||
|
||
return `<div>
|
||
<div style="padding:.7rem 1rem;background:#EAF3DE;border:0.5px solid #639922;border-radius:10px;margin-bottom:1rem;font-size:14px;color:#3B6D11;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<span>${ST.admin?'Hodnotíš jako tým:':'Hodnotíš za tým:'}</span>
|
||
${teamPicker}
|
||
${linkedSid?`<span style="font-size:12px;opacity:.7">(svého streamera nelze hodnotit)</span>`:''}
|
||
</div>
|
||
<div class="sbar"><input id="si" placeholder="Hledat streamera…" value="${ST.search}"></div>
|
||
${list.length===0?`<div class="empty">Žádný streamer k hodnocení.</div>`:`
|
||
<div class="grid">${list.map(s=>{
|
||
const myR=ST.teamRatings?.[s.id];
|
||
const myTotal=myR?CATS.reduce((a,c)=>{
|
||
const map={s:'statistiky',g:'grafika',a:'alerty',v:'vychytavky',n:'nahled',ns:'nastaveni',o:'odlisnost'};
|
||
return a+(+myR[map[c.k]]||0);
|
||
},0):null;
|
||
return `<div class="card" style="cursor:default">
|
||
<div class="ch"><div class="ava">${av(s.name)}</div>
|
||
<div style="flex:1;min-width:0"><div class="cn">${s.name}${!s.evaluated?' <span class="tag" style="font-size:10px">bez ofic. hodnocení</span>':''}</div><div class="cs">${s.platform==='kick'?'Kick':'Twitch'}${s.game?` · ${s.game}`:''}</div></div>
|
||
${s.status==='live'?`<span class="badge-live"><div class="dot" style="width:6px;height:6px"></div>LIVE</span>`:`<span class="badge-off">Offline</span>`}
|
||
</div>
|
||
<div class="cf" style="justify-content:space-between">
|
||
<div>
|
||
${myTotal!==null?`<div style="font-size:11px;color:var(--muted)">Hodnocení týmu</div><div style="font-size:18px;font-weight:500;color:${gc(myTotal,MAX)}">${myTotal}/${MAX}</div>`:`<span style="font-size:13px;color:var(--muted)">Zatím nehodnoceno</span>`}
|
||
</div>
|
||
<div style="display:flex;gap:6px;align-items:center">
|
||
${s.community_locked&&!ST.admin?`<span style="font-size:12px;color:var(--red)">🔒 Uzamčeno</span>`:`<button class="btn btn-sm btn-primary" data-rate="${s.id}">${myTotal!==null?'Upravit':'+ Hodnotit'}</button>`}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>`}
|
||
</div>`;
|
||
}
|
||
|
||
function communityRateModal(){
|
||
const sid=ST.rateStreamerId;
|
||
const s=ST.db.find(x=>x.id===sid);
|
||
if(!s)return'';
|
||
const teams = ST.admin ? (ST.raterGroups||[]) : (ST.myGroups||[]);
|
||
const team = teams.find(t=>t.id===ST.activeTeamId);
|
||
const myR=ST.myRating;
|
||
const r=myR?{s:myR.statistiky,g:myR.grafika,a:myR.alerty,v:myR.vychytavky,n:myR.nahled,ns:myR.nastaveni,o:myR.odlisnost}:{s:0,g:0,a:0,v:0,n:0,ns:0,o:0};
|
||
return `<div class="mbg" id="crate-bg">
|
||
<div class="modal">
|
||
<div class="mh">
|
||
<div class="ava" style="width:38px;height:38px;font-size:13px">${av(s.name)}</div>
|
||
<div style="flex:1"><div style="font-size:16px;font-weight:500">${s.name}</div><div style="font-size:13px;color:var(--muted)">Hodnocení za tým <strong>${escHtml(team?.name||'?')}</strong>${ST.admin?' <span style="opacity:.6">(admin)</span>':''}</div></div>
|
||
<button class="btn-ghost" id="crate-close">✕</button>
|
||
</div>
|
||
<div class="mb"><form id="crf">
|
||
<div class="st">Hodnocení (0–10 bodů)</div>
|
||
${CATS.map(c=>sl(c.k,`${c.l} (0–10)`,r[c.k]||0,10)).join('')}
|
||
<div class="fg" style="margin-top:1rem"><div class="fl">Poznámky (nepovinné)</div>
|
||
<textarea name="notes" rows="2" style="resize:vertical">${myR?.notes||''}</textarea>
|
||
</div>
|
||
</form></div>
|
||
<div class="mf">
|
||
<button class="btn" id="crate-cancel">Zrušit</button>
|
||
<button class="btn btn-primary" id="crate-save">Uložit hodnocení</button>
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
|
||
function teamModal(){
|
||
const g=(ST.raterGroups||[]).find(x=>x.id===ST.teamId);
|
||
if(!g)return'';
|
||
return `<div class="mbg" id="team-bg">
|
||
<div class="modal" style="max-width:640px">
|
||
<div class="mh">
|
||
<div style="flex:1">
|
||
<div style="font-size:16px;font-weight:500">👥 ${escHtml(g.name)}${g.streamer_name?` <span style="font-size:12px;color:var(--purple-d);background:var(--purple-l);padding:1px 8px;border-radius:8px;font-weight:400">→ ${escHtml(g.streamer_name)}</span>`:''}</div>
|
||
<div style="font-size:13px;color:var(--muted)">${g.members.length} členů · ${g.ratings_count} hodnocení</div>
|
||
</div>
|
||
<button class="btn-ghost" id="team-close">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<div class="st">Přidat / promote člena</div>
|
||
<div style="display:grid;gap:6px;grid-template-columns:1fr 100px 110px auto;margin-bottom:.4rem;align-items:center">
|
||
<input id="tm-login" placeholder="twitch_username">
|
||
<select id="tm-provider"><option value="twitch">Twitch</option><option value="kick">Kick</option></select>
|
||
<select id="tm-role"><option value="rater">Rater</option><option value="owner">Owner (streamer)</option></select>
|
||
<button class="btn btn-primary btn-sm" id="tm-add">+ Přidat</button>
|
||
</div>
|
||
<div style="font-size:11px;color:var(--muted);margin-bottom:.4rem">
|
||
<strong>Owner</strong> = sám streamer (zástupce týmu, jen jeden). <strong>Rater</strong> = člen týmu, který hodnotí ostatní streamery.
|
||
</div>
|
||
<div id="tm-err" style="color:var(--red);font-size:12px;display:none;margin-bottom:.4rem"></div>
|
||
|
||
<div class="st" style="margin-top:1.25rem">Členové (${g.members.length})</div>
|
||
${g.members.length===0?`<div style="font-size:14px;color:var(--muted);padding:.4rem 0">Zatím nikdo</div>`:''}
|
||
${g.members.map(m=>{
|
||
const isOwner=m.role==='owner';
|
||
return `<div style="display:flex;align-items:center;gap:10px;padding:.5rem 0;border-bottom:0.5px solid var(--border)">
|
||
${m.avatar?`<img src="${m.avatar}" style="width:28px;height:28px;border-radius:50%" />`:`<div class="ava" style="width:28px;height:28px;font-size:11px">${av(m.display_name)}</div>`}
|
||
<div style="flex:1">
|
||
<div style="font-size:14px;font-weight:500">${m.display_name}
|
||
${isOwner
|
||
?'<span style="font-size:10px;padding:1px 7px;background:var(--purple-l);color:var(--purple-d);border-radius:8px;margin-left:6px">OWNER</span>'
|
||
:'<span style="font-size:10px;padding:1px 7px;background:#EAF3DE;color:#3B6D11;border-radius:8px;margin-left:6px">rater</span>'}
|
||
</div>
|
||
<div style="font-size:12px;color:var(--muted)">${m.provider} · ${m.login}</div>
|
||
</div>
|
||
<button class="btn btn-sm btn-danger" data-delmember="${m.member_id}" style="padding:4px 10px">×</button>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
<div class="mf">
|
||
<button class="btn" id="team-cancel">Zavřít</button>
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
|
||
function sugModal(){return `<div class="mbg" id="sgbg">
|
||
<div class="modal modal-sm">
|
||
<div class="mh"><div style="flex:1"><div style="font-size:16px;font-weight:500">Navrhnout streamera</div><div style="font-size:13px;color:var(--muted)">Váš návrh zobrazíme jako podklad</div></div><button class="btn-ghost" id="sgx">✕</button></div>
|
||
<div class="mb">
|
||
<div class="fg"><div class="fl">Twitch / Kick jméno *</div><input id="sgn" placeholder="username"></div>
|
||
<div class="fg"><div class="fl">Platforma</div><select id="sgp"><option value="twitch">Twitch</option><option value="kick">Kick</option></select></div>
|
||
<div class="fg"><div class="fl">Vaše přezdívka</div><input id="sgs" placeholder="nepovinné"></div>
|
||
<div id="sgerr" style="color:var(--red);font-size:13px;display:none">Vyplňte jméno streamera</div>
|
||
</div>
|
||
<div class="mf"><button class="btn" id="sgcancel">Zrušit</button><button class="btn btn-primary" id="sgok">Odeslat návrh</button></div>
|
||
</div></div>`;}
|
||
function newModal(){return `<div class="mbg" id="nmbg">
|
||
<div class="modal modal-sm">
|
||
<div class="mh"><div style="flex:1"><div style="font-size:16px;font-weight:500">Přidat streamera</div></div><button class="btn-ghost" id="nmx">✕</button></div>
|
||
<div class="mb">
|
||
<div class="fg"><div class="fl">Platforma</div><select id="nmp" onchange="
|
||
const isTwitch=this.value==='twitch';
|
||
document.getElementById('nm-name-label').textContent=isTwitch?'Twitch jméno *':'Kick jméno *';
|
||
document.getElementById('nmn').placeholder=isTwitch?'twitch_username':'kick_username';
|
||
document.getElementById('nm-kick-row').style.display=isTwitch?'block':'none';
|
||
"><option value="twitch">Twitch</option><option value="kick">Kick</option></select></div>
|
||
<div class="fg"><div class="fl" id="nm-name-label">Twitch jméno *</div><input id="nmn" placeholder="twitch_username"></div>
|
||
<div class="fg" id="nm-kick-row"><div class="fl">Kick jméno (pokud se liší od Twitch)</div><input id="nmk" placeholder="nepovinné"></div>
|
||
<div id="nmerr" style="color:var(--red);font-size:13px;display:none">Jméno povinné nebo již existuje</div>
|
||
</div>
|
||
<div class="mf"><button class="btn" id="nmcancel">Zrušit</button><button class="btn btn-primary" id="nmok">Přidat</button></div>
|
||
</div></div>`;}
|
||
|
||
// ── Event binding ──────────────────────────────────────────────────
|
||
function on(id,fn){document.getElementById(id)?.addEventListener('click',fn);}
|
||
function qa(sel,fn){document.querySelectorAll(sel).forEach(fn);}
|
||
|
||
function bind(){
|
||
qa('[data-tab]',el=>el.addEventListener('click',async()=>{
|
||
const t=el.dataset.tab;
|
||
ST.tab=t;ST.fil='all';ST.search='';
|
||
if(t==='rate'){
|
||
try{
|
||
if(ST.admin && !ST.raterGroups?.length){
|
||
ST.raterGroups=await GET('/api/rater_groups');
|
||
}
|
||
// Decide active team
|
||
const teams = ST.admin ? (ST.raterGroups||[]) : (ST.myGroups||[]);
|
||
if(teams.length && (!ST.activeTeamId || !teams.find(t2=>t2.id===ST.activeTeamId))){
|
||
ST.activeTeamId = teams[0].id;
|
||
}
|
||
// Load this team's ratings map
|
||
if(ST.activeTeamId){
|
||
try{ST.teamRatings=await GET('/api/community_ratings?team_ratings=1&group_id='+ST.activeTeamId);}
|
||
catch(e){ST.teamRatings={};}
|
||
}
|
||
}catch(e){toast('Chyba načítání: '+e.message,false);}
|
||
}
|
||
re();
|
||
}));
|
||
// Team picker change (rater view) — switch active team, refetch its ratings
|
||
const tp=document.getElementById('team-picker');
|
||
if(tp)tp.addEventListener('change',async()=>{
|
||
ST.activeTeamId=+tp.value;
|
||
try{ST.teamRatings=await GET('/api/community_ratings?team_ratings=1&group_id='+ST.activeTeamId);}
|
||
catch(e){ST.teamRatings={};}
|
||
re();
|
||
});
|
||
qa('[data-f]',el=>el.addEventListener('click',()=>{ST.fil=el.dataset.f;re();}));
|
||
qa('[data-av]',el=>el.addEventListener('click',()=>{ST.av=el.dataset.av;re();}));
|
||
// Admin section navigation — lazy-load data on first visit
|
||
qa('[data-asec]',el=>el.addEventListener('click',async()=>{
|
||
const sec=el.dataset.asec;
|
||
ST.admSection=sec;
|
||
try{
|
||
if(sec==='users'){
|
||
const [mods,users]=await Promise.all([GET('/api/moderators'),GET('/api/moderators?users=1')]);
|
||
ST.currentMods=mods;ST.allUsers=users;
|
||
}else if(sec==='teams'){
|
||
ST.raterGroups=await GET('/api/rater_groups');
|
||
}
|
||
}catch(e){toast('Chyba načítání: '+e.message,false);}
|
||
re();
|
||
}));
|
||
const si=document.getElementById('si');
|
||
if(si)si.addEventListener('input',e=>{ST.search=e.target.value;re();});
|
||
on('li',()=>{ST.loginM=true;re();setTimeout(()=>document.getElementById('lpw')?.focus(),30);});
|
||
on('lo',async()=>{await POST('/api/auth?action=logout');ST.admin=false;ST.tab='pub';ST.db=await GET('/api/streamers');re();});
|
||
on('sugbtn',()=>{ST.sugM=true;re();});
|
||
on('newbtn',()=>{ST.newM=true;re();});
|
||
on('rfbtn',async()=>{
|
||
try{
|
||
const r=await GET('/api/live?refresh=1');
|
||
ST.db=await GET('/api/streamers?all=1');
|
||
toast(`Live refresh hotov — ${r.results?.filter(x=>x.status==='live').length||0} live`);
|
||
re();
|
||
}catch(e){toast('Chyba při live refresh: '+e.message,false);}
|
||
});
|
||
// Open team management modal for a single team
|
||
qa('[data-team]',el=>el.addEventListener('click',e=>{
|
||
e.stopPropagation();
|
||
ST.teamId=+el.dataset.team;ST.teamM=true;re();
|
||
}));
|
||
qa('[data-ed]',el=>el.addEventListener('click',e=>{e.stopPropagation();ST.editId=+el.dataset.ed;ST.editM=true;re();bsliders();}));
|
||
qa('[data-dl]',el=>el.addEventListener('click',async e=>{
|
||
e.stopPropagation();
|
||
if(!confirm('Smazat streamera?'))return;
|
||
try{await DEL('/api/streamers?id='+el.dataset.dl);ST.db=ST.db.filter(x=>x.id!=el.dataset.dl);toast('Smazáno');re();}
|
||
catch(e){toast('Chyba: '+e.message,false);}
|
||
}));
|
||
qa('[data-rst]',el=>el.addEventListener('click',async e=>{
|
||
e.stopPropagation();
|
||
const s=ST.db.find(x=>x.id===+el.dataset.rst);if(!s)return;
|
||
const wipeCommunity=confirm(
|
||
'Resetovat oficiální hodnocení streamera "'+s.name+'"?\n\n'+
|
||
'OK = také smaže VŠECHNA komunitní hodnocení tohoto streamera.\n'+
|
||
'Zrušit = jen oficiální skóre, komunitní zůstanou.'
|
||
);
|
||
if(!confirm('Pokračovat? Tato akce je nevratná.'))return;
|
||
try{
|
||
await PUT('/api/streamers?id='+s.id,{
|
||
kick:s.kick||'',status:s.status,game:s.game||'',title:s.title||'',
|
||
r:{s:0,g:0,a:0,v:0,n:0,ns:0,o:0},reward:0,notes:''
|
||
});
|
||
Object.assign(s,{r:{s:0,g:0,a:0,v:0,n:0,ns:0,o:0},reward:0,evaluated:false});
|
||
if(wipeCommunity){
|
||
await DEL('/api/community_ratings?wipe_streamer='+s.id);
|
||
// Refresh teams (counts changed) and ratings cache
|
||
if(ST.raterGroups?.length)ST.raterGroups=await GET('/api/rater_groups');
|
||
delete ST.communityRatings[s.id];
|
||
}
|
||
toast(wipeCommunity?'Resetováno (vč. komunitních hodnocení)':'Oficiální hodnocení resetováno');re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
}));
|
||
// Lock/unlock community ratings
|
||
qa('[data-lock]',el=>el.addEventListener('click',async e=>{
|
||
e.stopPropagation();
|
||
const sid=+el.dataset.lock;
|
||
const locked=el.dataset.locked==='true';
|
||
try{
|
||
await PUT('/api/streamers?id='+sid+'&lock=1',{community_locked:!locked});
|
||
const s=ST.db.find(x=>x.id===sid);
|
||
if(s)s.community_locked=!locked;
|
||
toast(locked?'Hodnocení odemčeno':'Hodnocení uzamčeno');re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
}));
|
||
// Open rate modal
|
||
qa('[data-rate]',el=>el.addEventListener('click',async e=>{
|
||
e.stopPropagation();
|
||
const sid=+el.dataset.rate;
|
||
ST.rateStreamerId=sid;
|
||
// Pre-fill from active team's ratings map (loaded on tab/team switch)
|
||
ST.myRating = ST.teamRatings?.[sid] || null;
|
||
ST.rateM=true;re();setTimeout(()=>bcrSliders(),30);
|
||
}));
|
||
// Delete community rating (admin)
|
||
qa('[data-delcr]',el=>el.addEventListener('click',async e=>{
|
||
e.stopPropagation();
|
||
if(!confirm('Smazat toto komunitní hodnocení?'))return;
|
||
try{
|
||
await DEL('/api/community_ratings?id='+el.dataset.delcr);
|
||
// Reload community ratings for current detail
|
||
if(ST.detailId)ST.communityRatings[ST.detailId]=await GET('/api/community_ratings?streamer_id='+ST.detailId);
|
||
toast('Hodnocení smazáno');re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
}));
|
||
// Detail community-rating tab switch (Průměr / per-team)
|
||
qa('[data-crtab]',el=>el.addEventListener('click',e=>{
|
||
e.stopPropagation();
|
||
const v=el.dataset.crtab;
|
||
ST.detailCRTab = (v==='overview')?'overview':+v;
|
||
re();
|
||
}));
|
||
// Detail comment filter (all / per-team)
|
||
qa('[data-cmtfilter]',el=>el.addEventListener('click',e=>{
|
||
e.stopPropagation();
|
||
ST.detailCmtFilter = el.dataset.cmtfilter;
|
||
re();
|
||
}));
|
||
qa('[data-det]',el=>el.addEventListener('click',async()=>{
|
||
const sid=+el.dataset.det;
|
||
ST.detailId=sid;
|
||
// Reset detail-modal local state when opening a new streamer
|
||
ST.detailCRTab='overview';
|
||
ST.detailCmtFilter='all';
|
||
// Make sure team list is loaded for admins (used for badges in renderCommentsList)
|
||
if(ST.admin && !ST.raterGroups?.length){
|
||
try{ST.raterGroups=await GET('/api/rater_groups');}catch(e){}
|
||
}
|
||
// Load community ratings for this streamer
|
||
try{ST.communityRatings[sid]=await GET('/api/community_ratings?streamer_id='+sid);}catch(e){}
|
||
re();
|
||
}));
|
||
bLogin();bEdit();bDetail();bSug();bNew();
|
||
bAdmUsers();bAdmTeams();bAdmSettings();bTeam();bCommunityRate();
|
||
}
|
||
|
||
function bLogin(){
|
||
if(!ST.loginM)return;
|
||
const cl=()=>{ST.loginM=false;re();};
|
||
on('lmx',cl);on('lmcancel',cl);
|
||
on('lmbg',e=>{if(e.target.id==='lmbg')cl();});
|
||
const doL=async()=>{
|
||
const pw=document.getElementById('lpw')?.value;
|
||
try{
|
||
await POST('/api/auth?action=login',{password:pw});
|
||
ST.admin=true;ST.loginM=false;
|
||
ST.db=await GET('/api/streamers?all=1');
|
||
re();
|
||
}catch(e){const err=document.getElementById('lerr');if(err)err.style.display='block';}
|
||
};
|
||
on('lmok',doL);
|
||
document.getElementById('lpw')?.addEventListener('keydown',e=>{if(e.key==='Enter')doL();});
|
||
}
|
||
function bEdit(){
|
||
if(!ST.editM)return;
|
||
const cl=()=>{ST.editM=false;ST.editId=null;re();};
|
||
on('emx',cl);on('emcancel',cl);
|
||
on('embg',e=>{if(e.target.id==='embg')cl();});
|
||
on('emsave',async()=>{
|
||
const f=document.getElementById('ef');if(!f)return;
|
||
const fd=new FormData(f);
|
||
const s=ST.db.find(x=>x.id===ST.editId);if(!s)return;
|
||
const body={
|
||
kick:fd.get('kick')||'', status:fd.get('status'),
|
||
game:fd.get('game')||'', title:fd.get('title')||'',
|
||
r:Object.fromEntries(CATS.map(c=>[c.k,+(fd.get(c.k)||0)])),
|
||
reward:Math.round(+(document.getElementById('rw')?.value||0)),
|
||
notes:fd.get('notes')||''
|
||
};
|
||
try{
|
||
await PUT('/api/streamers?id='+ST.editId,body);
|
||
// Update local state
|
||
Object.assign(s,{kick:body.kick,status:body.status,game:body.game,title:body.title,r:body.r,reward:body.reward,notes:body.notes,evaluated:Object.values(body.r).some(v=>v>0)});
|
||
toast('Uloženo');ST.editM=false;ST.editId=null;re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
bsliders();
|
||
}
|
||
// Auto reward: 30 Kč base + up to 100 Kč based on score (0-70), rounded to whole crowns
|
||
function autoReward(r){
|
||
const score=CATS.reduce((a,c)=>a+(+r[c.k]||0),0);
|
||
return Math.round(30+(score/MAX)*100);
|
||
}
|
||
|
||
function bsliders(){
|
||
CATS.forEach(c=>{
|
||
const i=document.getElementById('r'+c.k),v=document.getElementById('rv'+c.k);
|
||
if(i&&v)i.addEventListener('input',()=>{
|
||
v.textContent=i.value;
|
||
v.style.color=gc(+i.value,10);
|
||
updateAutoReward();
|
||
});
|
||
});
|
||
updateAutoReward();
|
||
}
|
||
|
||
function updateAutoReward(){
|
||
const r={};
|
||
CATS.forEach(c=>{
|
||
const i=document.getElementById('r'+c.k);
|
||
r[c.k]=i?+i.value:0;
|
||
});
|
||
const reward=autoReward(r);
|
||
const el=document.getElementById('auto-reward');
|
||
const hw=document.getElementById('rw');
|
||
if(el)el.textContent=reward+' Kč';
|
||
if(hw)hw.value=reward;
|
||
}
|
||
function bDetail(){
|
||
if(!ST.detailId)return;
|
||
on('dmx',()=>{ST.detailId=null;re();});
|
||
on('dmbg',e=>{if(e.target.id==='dmbg'){ST.detailId=null;re();}});
|
||
|
||
// Load comments button
|
||
on('comments-load',()=>loadComments(ST.detailId));
|
||
|
||
// Auto-load if already cached
|
||
if(ST.comments[ST.detailId]==null){
|
||
loadComments(ST.detailId);
|
||
}
|
||
|
||
// Submit comment
|
||
on('c-submit',async()=>{
|
||
const sid=ST.detailId;
|
||
const body=document.getElementById('c-body')?.value.trim();
|
||
const author=document.getElementById('c-author')?.value.trim()||'Anonym';
|
||
if(!body||body.length<2){toast('Komentář je příliš krátký',false);return;}
|
||
try{
|
||
const c=await POST('/api/comments',{streamer_id:sid,author,body});
|
||
if(!ST.comments[sid])ST.comments[sid]=[];
|
||
ST.comments[sid].push(c);
|
||
const el=document.getElementById('comments-list');
|
||
if(el)el.innerHTML=renderCommentsList(sid);
|
||
const bodyEl = document.getElementById('c-body');
|
||
const authorEl = document.getElementById('c-author');
|
||
if(bodyEl) bodyEl.value='';
|
||
if(authorEl) authorEl.value='';
|
||
toast('Komentář přidán');
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
|
||
// Delete comment (admin)
|
||
document.querySelectorAll('[data-cid]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
const cid=+btn.dataset.cid;
|
||
const sid=+btn.dataset.sid;
|
||
if(!confirm('Smazat komentář?'))return;
|
||
try{
|
||
await DEL('/api/comments?id='+cid);
|
||
ST.comments[sid]=(ST.comments[sid]||[]).filter(c=>c.id!==cid);
|
||
const el=document.getElementById('comments-list');
|
||
if(el)el.innerHTML=renderCommentsList(sid);
|
||
toast('Smazáno');
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
}
|
||
function bSug(){
|
||
if(!ST.sugM)return;
|
||
const cl=()=>{ST.sugM=false;re();};
|
||
on('sgx',cl);on('sgcancel',cl);
|
||
on('sgbg',e=>{if(e.target.id==='sgbg')cl();});
|
||
on('sgok',async()=>{
|
||
const name=document.getElementById('sgn').value.trim().toLowerCase();
|
||
const err=document.getElementById('sgerr');
|
||
if(!name){err.style.display='block';return;}
|
||
try{
|
||
await POST('/api/streamers',{name,platform:document.getElementById('sgp').value,submitter:document.getElementById('sgs').value.trim()});
|
||
ST.sugM=false;toast(`Díky! Návrh na "${name}" byl přidán.`);re();
|
||
}catch(e){err.textContent=e.message;err.style.display='block';}
|
||
});
|
||
}
|
||
function bNew(){
|
||
if(!ST.newM)return;
|
||
const cl=()=>{ST.newM=false;re();};
|
||
on('nmx',cl);on('nmcancel',cl);
|
||
on('nmbg',e=>{if(e.target.id==='nmbg')cl();});
|
||
on('nmok',async()=>{
|
||
const name=document.getElementById('nmn').value.trim().toLowerCase();
|
||
const platform=document.getElementById('nmp').value;
|
||
const err=document.getElementById('nmerr');
|
||
if(!name){err.style.display='block';return;}
|
||
// For kick: the main name field IS the kick name, twitch name field unused
|
||
// For twitch: name is twitch name, optional kick field is secondary
|
||
const kick_name = platform==='kick' ? name : (document.getElementById('nmk').value.trim().toLowerCase()||'');
|
||
const streamer_name = name;
|
||
try{
|
||
const res=await POST('/api/streamers?admin=1',{name:streamer_name,platform,kick_name});
|
||
ST.db.push({id:res.id,name:streamer_name,platform,kick:kick_name,status:'offline',game:'',title:'',added_by:'admin',evaluated:false,r:{s:0,g:0,a:0,v:0,n:0,ns:0,o:0},reward:0,notes:''});
|
||
ST.newM=false;toast('Streamer přidán');re();
|
||
}catch(e){err.textContent=e.message;err.style.display='block';}
|
||
});
|
||
}
|
||
|
||
// ── Admin: Users section (inline) ─────────────────────────────────
|
||
async function reloadUsers(){
|
||
const [mods,users]=await Promise.all([GET('/api/moderators'),GET('/api/moderators?users=1')]);
|
||
ST.currentMods=mods;ST.allUsers=users;
|
||
}
|
||
|
||
function bAdmUsers(){
|
||
if(!ST.admin||ST.tab!=='adm'||ST.admSection!=='users')return;
|
||
|
||
// Add mod by username
|
||
on('mod-add',async()=>{
|
||
const login=document.getElementById('mod-login')?.value.trim().toLowerCase();
|
||
const provider=document.getElementById('mod-provider')?.value;
|
||
const err=document.getElementById('mod-err');
|
||
if(!login){err.textContent='Zadej uživatelské jméno';err.style.display='block';return;}
|
||
try{
|
||
const res=await POST('/api/moderators',{login,provider});
|
||
toast(`${res.display_name} přidán jako moderátor`);
|
||
await reloadUsers();re();
|
||
}catch(e){err.textContent=e.message;err.style.display='block';}
|
||
});
|
||
|
||
// Remove mod
|
||
document.querySelectorAll('[data-delmod]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
if(!confirm('Odebrat moderátora?'))return;
|
||
try{
|
||
await DEL('/api/moderators?id='+btn.dataset.delmod);
|
||
toast('Moderátor odebrán');
|
||
await reloadUsers();re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
|
||
// Give mod from users list
|
||
document.querySelectorAll('[data-givemod]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
try{
|
||
const res=await POST('/api/moderators',{login:btn.dataset.login,provider:btn.dataset.provider});
|
||
toast(`${res.display_name} přidán jako moderátor`);
|
||
await reloadUsers();re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
|
||
// Revoke mod from users list (by user_id → find mod_id)
|
||
document.querySelectorAll('[data-revmod]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
if(!confirm('Odebrat mod oprávnění?'))return;
|
||
try{
|
||
const mod=ST.currentMods.find(m=>m.id==btn.dataset.revmod);
|
||
if(mod) await DEL('/api/moderators?id='+mod.mod_id);
|
||
toast('Mod odebrán');
|
||
await reloadUsers();re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
|
||
// Ban
|
||
document.querySelectorAll('[data-ban]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
if(!confirm('Zabanovat uživatele?'))return;
|
||
try{
|
||
await POST('/api/moderators?ban=1',{user_id:+btn.dataset.ban});
|
||
toast('Uživatel zabanován');
|
||
await reloadUsers();re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
|
||
// Unban
|
||
document.querySelectorAll('[data-unban]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
try{
|
||
await DEL('/api/moderators?unban='+btn.dataset.unban);
|
||
toast('Uživatel odbanován');
|
||
await reloadUsers();re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Admin: Teams section (inline list) ────────────────────────────
|
||
function bAdmTeams(){
|
||
if(!ST.admin||ST.tab!=='adm'||ST.admSection!=='teams')return;
|
||
|
||
// Create team — reads name + optional streamer_id
|
||
on('rg-create',async()=>{
|
||
const name=document.getElementById('rg-name')?.value.trim();
|
||
const sidVal=document.getElementById('rg-streamer')?.value;
|
||
const streamer_id=sidVal?+sidVal:null;
|
||
const err=document.getElementById('rg-err');
|
||
if(!name){err.textContent='Zadej název týmu';err.style.display='block';return;}
|
||
try{
|
||
const payload={name};
|
||
if(streamer_id)payload.streamer_id=streamer_id;
|
||
await POST('/api/rater_groups',payload);
|
||
ST.raterGroups=await GET('/api/rater_groups');
|
||
// Refresh streamers so team_id gets reflected
|
||
try{ST.db=await GET('/api/streamers?all=1');}catch(e){}
|
||
if(err){err.style.display='none';err.textContent='';}
|
||
toast('Tým vytvořen');re();
|
||
}catch(e){if(err){err.textContent=e.message;err.style.display='block';}}
|
||
});
|
||
|
||
// Edit team binding — rename and/or change linked streamer via prompt
|
||
document.querySelectorAll('[data-edteam]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
const gid=+btn.dataset.edteam;
|
||
const g=ST.raterGroups.find(x=>x.id===gid);
|
||
if(!g)return;
|
||
const newName=prompt('Nový název týmu (prázdné = ponechat):', g.name);
|
||
if(newName===null)return;
|
||
|
||
// Build a list of streamers to choose from (current linked + unlinked ones)
|
||
const linkedSids=new Set(ST.raterGroups.map(x=>x.streamer_id).filter(Boolean));
|
||
const choices=ST.db.filter(s=>!linkedSids.has(s.id)||s.id===g.streamer_id);
|
||
const numbered=choices.map((s,i)=>`${i+1}) ${s.name}`).join('\n');
|
||
const cur=g.streamer_id?choices.findIndex(s=>s.id===g.streamer_id)+1:0;
|
||
const ans=prompt(
|
||
`Vazba na streamera — zadej číslo (0 = bez vazby, prázdné = ponechat):\n\n0) — bez vazby —\n${numbered}`,
|
||
String(cur)
|
||
);
|
||
if(ans===null)return;
|
||
|
||
const payload={};
|
||
if(newName.trim()&&newName.trim()!==g.name) payload.name=newName.trim();
|
||
if(ans.trim()!==''){
|
||
const idx=parseInt(ans,10);
|
||
if(idx===0) payload.streamer_id=null;
|
||
else if(idx>=1&&idx<=choices.length) payload.streamer_id=choices[idx-1].id;
|
||
else { toast('Neplatné číslo',false); return; }
|
||
}
|
||
if(!Object.keys(payload).length){ toast('Nic nezměněno'); return; }
|
||
|
||
try{
|
||
await PUT('/api/rater_groups?id='+gid,payload);
|
||
ST.raterGroups=await GET('/api/rater_groups');
|
||
try{ST.db=await GET('/api/streamers?all=1');}catch(e){}
|
||
toast('Tým upraven');re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
|
||
// Delete team
|
||
document.querySelectorAll('[data-delrg]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
if(!confirm('Smazat tým a všechna jeho hodnocení?'))return;
|
||
try{
|
||
await DEL('/api/rater_groups?id='+btn.dataset.delrg);
|
||
ST.raterGroups=await GET('/api/rater_groups');
|
||
toast('Tým smazán');re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Admin: Settings section (inline) ──────────────────────────────
|
||
function bAdmSettings(){
|
||
if(!ST.admin||ST.tab!=='adm'||ST.admSection!=='settings')return;
|
||
on('sett-save',async()=>{
|
||
const msg=document.getElementById('sett-msg');
|
||
try{
|
||
await PUT('/api/settings',{
|
||
auth_enabled: document.getElementById('set-auth').checked,
|
||
auth_twitch_enabled: document.getElementById('set-twitch').checked,
|
||
auth_kick_enabled: document.getElementById('set-kick').checked,
|
||
});
|
||
ST.settings=await GET('/api/settings');
|
||
if(msg){msg.textContent='✓ Uloženo';msg.style.color='var(--green)';setTimeout(()=>{if(msg)msg.textContent='';},2500);}
|
||
toast('Nastavení uloženo');
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
}
|
||
|
||
// ── Team management modal (single team) ───────────────────────────
|
||
function bTeam(){
|
||
if(!ST.teamM)return;
|
||
const cl=()=>{ST.teamM=false;ST.teamId=null;re();};
|
||
on('team-close',cl);on('team-cancel',cl);
|
||
on('team-bg',e=>{if(e.target.id==='team-bg')cl();});
|
||
|
||
// Add member to this team
|
||
on('tm-add',async()=>{
|
||
const login=document.getElementById('tm-login')?.value.trim().toLowerCase();
|
||
const provider=document.getElementById('tm-provider')?.value;
|
||
const role=document.getElementById('tm-role')?.value||'rater';
|
||
const err=document.getElementById('tm-err');
|
||
if(!login){err.textContent='Zadej jméno';err.style.display='block';return;}
|
||
try{
|
||
const res=await POST('/api/rater_groups?members=1',{group_id:ST.teamId,login,provider,role});
|
||
ST.raterGroups=await GET('/api/rater_groups');
|
||
toast(`${res.display_name} přidán jako ${res.role==='owner'?'owner':'rater'}`);
|
||
if(err)err.style.display='none';re();
|
||
}catch(e){err.textContent=e.message;err.style.display='block';}
|
||
});
|
||
|
||
// Remove member
|
||
document.querySelectorAll('[data-delmember]').forEach(btn=>{
|
||
btn.addEventListener('click',async()=>{
|
||
try{
|
||
await DEL('/api/rater_groups?member='+btn.dataset.delmember);
|
||
ST.raterGroups=await GET('/api/rater_groups');
|
||
toast('Člen odebrán');re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
});
|
||
}
|
||
|
||
function bCommunityRate(){
|
||
if(!ST.rateM)return;
|
||
const cl=()=>{ST.rateM=false;ST.rateStreamerId=null;ST.myRating=null;re();};
|
||
on('crate-close',cl);on('crate-cancel',cl);
|
||
on('crate-bg',e=>{if(e.target.id==='crate-bg')cl();});
|
||
|
||
on('crate-save',async()=>{
|
||
const form=document.getElementById('crf');if(!form)return;
|
||
const fd=new FormData(form);
|
||
const r={};
|
||
CATS.forEach(c=>{r[c.k]=+(fd.get(c.k)||0);});
|
||
try{
|
||
const payload={r,notes:fd.get('notes')||''};
|
||
if(ST.activeTeamId)payload.group_id=ST.activeTeamId;
|
||
await PUT('/api/community_ratings?streamer_id='+ST.rateStreamerId,payload);
|
||
// Reload community ratings (used elsewhere) and the team's ratings map
|
||
try{ST.communityRatings[ST.rateStreamerId]=await GET('/api/community_ratings?streamer_id='+ST.rateStreamerId);}catch(e){}
|
||
if(ST.activeTeamId){
|
||
try{ST.teamRatings=await GET('/api/community_ratings?team_ratings=1&group_id='+ST.activeTeamId);}catch(e){}
|
||
}
|
||
// Refresh admin team list so ratings_count badge stays in sync
|
||
if(ST.admin){
|
||
try{ST.raterGroups=await GET('/api/rater_groups');}catch(e){}
|
||
}
|
||
toast('Hodnocení uloženo');
|
||
ST.rateM=false;ST.rateStreamerId=null;ST.myRating=null;re();
|
||
}catch(e){toast('Chyba: '+e.message,false);}
|
||
});
|
||
|
||
bcrSliders();
|
||
}
|
||
|
||
function bcrSliders(){
|
||
CATS.forEach(c=>{
|
||
const i=document.getElementById('r'+c.k),v=document.getElementById('rv'+c.k);
|
||
if(i&&v)i.addEventListener('input',()=>{v.textContent=i.value;v.style.color=gc(+i.value,10);});
|
||
});
|
||
}
|
||
|
||
boot();
|
||
</script>
|
||
</body>
|
||
</html>
|