Files
fedi_start/accounts.html

1012 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CZ účty na Mastodonu Mamutovo</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap');
:root {
--bg: #0d0d0d;
--surface: #161616;
--surface2: #1e1e1e;
--surface3: #252525;
--accent: #00c896;
--accent2: #ff6b6b;
--accent3: #fbbf24;
--text: #f0f0f0;
--muted: #777;
--muted2: #555;
--border: #282828;
--radius: 10px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Syne', sans-serif;
min-height: 100vh;
}
/* NAV */
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
border-bottom: 1px solid var(--border);
background: rgba(13,13,13,0.9);
backdrop-filter: blur(10px);
position: sticky;
top: 0;
z-index: 50;
}
.nav-logo {
font-family: 'Space Mono', monospace;
font-size: 0.85rem;
color: var(--accent);
text-decoration: none;
font-weight: 700;
letter-spacing: 0.05em;
}
.nav-links {
display: flex;
gap: 1.5rem;
align-items: center;
}
.nav-links a {
font-family: 'Space Mono', monospace;
font-size: 0.75rem;
color: var(--muted);
text-decoration: none;
transition: color 0.15s;
}
.nav-links a:hover, .nav-links a.active { color: var(--text); }
.nav-updated {
font-family: 'Space Mono', monospace;
font-size: 0.68rem;
color: var(--muted2);
}
/* HEADER */
.header {
max-width: 1100px;
margin: 0 auto;
padding: 2.5rem 2rem 1.5rem;
}
h1 {
font-size: clamp(1.6rem, 4vw, 2.4rem);
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 0.4rem;
}
h1 span { color: var(--accent); }
.subtitle {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 1.8rem;
}
/* CONTROLS */
.controls {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.2rem;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
margin-bottom: 0.8rem;
}
.search-wrap {
flex: 1;
min-width: 200px;
position: relative;
}
.search-icon {
position: absolute;
left: 0.8rem;
top: 50%;
transform: translateY(-50%);
color: var(--muted2);
font-size: 0.85rem;
pointer-events: none;
}
input[type="text"] {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 7px;
padding: 0.55rem 0.9rem 0.55rem 2.2rem;
color: var(--text);
font-family: 'Syne', sans-serif;
font-size: 0.88rem;
outline: none;
transition: border-color 0.15s;
}
input[type="text"]:focus { border-color: var(--accent); }
input[type="text"]::placeholder { color: var(--muted2); }
select {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 7px;
padding: 0.55rem 2rem 0.55rem 0.9rem;
color: var(--text);
font-family: 'Syne', sans-serif;
font-size: 0.85rem;
outline: none;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23555' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.7rem center;
transition: border-color 0.15s;
}
select:focus { border-color: var(--accent); }
.filter-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
max-width: 1100px;
margin: 0 auto 1.2rem;
padding: 0 2rem;
}
.ftag {
font-family: 'Space Mono', monospace;
font-size: 0.72rem;
padding: 0.3rem 0.8rem;
border-radius: 999px;
border: 1px solid var(--border);
background: transparent;
color: var(--muted);
cursor: pointer;
transition: all 0.15s;
}
.ftag:hover, .ftag.active {
border-color: var(--accent);
color: var(--accent);
background: rgba(0,200,150,0.08);
}
/* STATS BAR */
.stats-bar {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1100px;
margin: 0 auto 1rem;
padding: 0 2rem;
}
.stats-count {
font-family: 'Space Mono', monospace;
font-size: 0.78rem;
color: var(--muted);
}
.stats-count strong { color: var(--text); }
.view-btns {
display: flex;
gap: 0.3rem;
}
.view-btn {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--muted);
transition: all 0.15s;
font-size: 0.8rem;
}
.view-btn.active, .view-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* DOWNLOAD BAR */
.dl-bar {
max-width: 1100px;
margin: 0 auto 1.5rem;
padding: 0 2rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-family: 'Space Mono', monospace;
font-size: 0.75rem;
font-weight: 700;
padding: 0.5rem 1rem;
border-radius: 7px;
border: none;
cursor: pointer;
text-decoration: none;
transition: all 0.15s;
}
.btn-accent {
background: var(--accent);
color: #000;
}
.btn-accent:hover { background: #00dfa8; transform: translateY(-1px); }
.btn-ghost {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
/* GRID */
.grid {
max-width: 1100px;
margin: 0 auto;
padding: 0 2rem 3rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.9rem;
}
.grid.list-view {
grid-template-columns: 1fr;
gap: 0.5rem;
}
/* CARD */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
transition: border-color 0.2s, transform 0.15s;
animation: fadeIn 0.3s ease both;
}
.card:hover {
border-color: var(--accent);
transform: translateY(-2px);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.card-header {
height: 60px;
background: linear-gradient(135deg, var(--surface2), var(--surface3));
position: relative;
}
.card-header-img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.4;
}
.card-avatar {
width: 52px;
height: 52px;
border-radius: 10px;
border: 3px solid var(--surface);
position: absolute;
bottom: -20px;
left: 1rem;
background: var(--surface2);
object-fit: cover;
}
.card-body {
padding: 1.4rem 1rem 1rem;
}
.card-name {
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 0.15rem;
letter-spacing: -0.01em;
}
.card-handle {
font-family: 'Space Mono', monospace;
font-size: 0.68rem;
color: var(--muted);
margin-bottom: 0.6rem;
word-break: break-all;
}
.card-bio {
font-size: 0.8rem;
color: var(--muted);
line-height: 1.5;
margin-bottom: 0.8rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
margin-bottom: 0.8rem;
}
.card-tag {
font-family: 'Space Mono', monospace;
font-size: 0.63rem;
color: var(--accent);
background: rgba(0,200,150,0.08);
border: 1px solid rgba(0,200,150,0.15);
border-radius: 999px;
padding: 0.15rem 0.5rem;
}
.card-stats {
display: flex;
gap: 1rem;
margin-bottom: 0.9rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.stat-val {
font-family: 'Space Mono', monospace;
font-size: 0.82rem;
font-weight: 700;
color: var(--text);
}
.stat-label {
font-size: 0.65rem;
color: var(--muted2);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card-follow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.follow-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
background: var(--accent);
color: #000;
font-family: 'Space Mono', monospace;
font-size: 0.72rem;
font-weight: 700;
padding: 0.5rem;
border-radius: 7px;
text-decoration: none;
transition: all 0.15s;
}
.follow-btn:hover { background: #00dfa8; }
.score-badge {
font-family: 'Space Mono', monospace;
font-size: 0.65rem;
color: var(--accent3);
background: rgba(251,191,36,0.1);
border: 1px solid rgba(251,191,36,0.2);
border-radius: 5px;
padding: 0.3rem 0.45rem;
white-space: nowrap;
}
/* LIST VIEW CARD */
.list-view .card {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0 1rem;
align-items: center;
padding: 0.8rem 1rem;
}
.list-view .card-header { display: none; }
.list-view .card-body { padding: 0; display: contents; }
.list-view .card-bio { display: none; }
.list-view .card-tags { display: none; }
.list-view .card-avatar-list {
width: 40px;
height: 40px;
border-radius: 8px;
object-fit: cover;
background: var(--surface2);
flex-shrink: 0;
}
.list-view .card-info { min-width: 0; }
.list-view .card-name { font-size: 0.88rem; }
.list-view .card-handle { margin-bottom: 0; }
.list-view .card-stats {
gap: 1.5rem;
margin-bottom: 0;
flex-shrink: 0;
}
.list-view .card-follow { flex-direction: column; gap: 0.3rem; }
.list-view .follow-btn { font-size: 0.68rem; padding: 0.35rem 0.7rem; flex: none; }
/* EMPTY */
.empty {
max-width: 1100px;
margin: 3rem auto;
padding: 0 2rem;
text-align: center;
color: var(--muted);
font-size: 0.9rem;
display: none;
}
/* LOADING */
.loading {
max-width: 1100px;
margin: 4rem auto;
padding: 0 2rem;
text-align: center;
color: var(--muted);
font-family: 'Space Mono', monospace;
font-size: 0.8rem;
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.7s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* RESPONSIVE */
@media (max-width: 600px) {
nav { padding: 0.8rem 1rem; }
.header { padding: 1.5rem 1rem 1rem; }
.controls { padding: 0.8rem; }
.filter-tags, .stats-bar, .dl-bar, .grid { padding-left: 1rem; padding-right: 1rem; }
.grid { grid-template-columns: 1fr; }
nav .nav-updated { display: none; }
}
</style>
</head>
<body>
<nav>
<a href="/start" class="nav-logo">🦣 Mamutovo</a>
<div class="nav-links">
<a href="/start">← Průvodce</a>
<a href="/accounts" class="active">CZ účty</a>
</div>
<span class="nav-updated"><span id="lastUpdated">načítám...</span><span id="refreshCountdown" style="margin-left:0.4rem;color:var(--muted2);font-size:0.65rem;font-family:'Space Mono',monospace;"></span><span id="refreshBadge" style="margin-left:0.5rem;font-size:0.65rem;font-family:'Space Mono',monospace;transition:color 0.3s;"></span></span>
</nav>
<div class="header">
<h1>CZ účty na <span>Mastodonu</span></h1>
<p class="subtitle">Automaticky aktualizovaný seznam aktivních českých a slovenských účtů. Aktualizace každý den.</p>
<div class="controls">
<div class="search-wrap">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" placeholder="Hledej podle jména, bio nebo tagu...">
</div>
<select id="categoryFilter">
<option value="">Všechny kategorie</option>
</select>
<select id="sortFilter">
<option value="score">Nejvyšší skóre</option>
<option value="followers">Nejvíce sledujících</option>
<option value="activity">Nejaktivnější</option>
</select>
</div>
</div>
<div class="filter-tags" id="filterTags">
<button class="ftag active" data-tag="">Vše</button>
</div>
<div class="stats-bar">
<span class="stats-count">Zobrazuji <strong id="visibleCount">0</strong> z <strong id="totalCount">0</strong> účtů</span>
<div class="view-btns">
<button class="view-btn active" id="gridBtn" title="Mřížka"></button>
<button class="view-btn" id="listBtn" title="Seznam"></button>
</div>
</div>
<div class="dl-bar">
<button class="btn btn-accent" id="downloadCsvBtn">⬇ Stáhnout CSV (viditelné)</button>
<button class="btn btn-ghost" id="downloadAllCsvBtn">⬇ Celý seznam CSV</button>
<a href="accounts.json" class="btn btn-ghost" download>{ } JSON</a>
</div>
<div class="loading" id="loadingEl">
<div class="spinner"></div>
Načítám účty...
</div>
<div class="grid" id="grid"></div>
<div class="empty" id="emptyEl">Žádné účty neodpovídají filtru. Zkus jiný výraz.</div>
<script>
// ────────────────────────────────
// DATA fallback když není JSON
// ────────────────────────────────
const FALLBACK_ACCOUNTS = [
{
name: "Mamutovo",
handle: "mamutovo@mamutovo.cz",
bio: "Oficiální účet české Mastodon instance Mamutovo. Novinky, tipy a komunita.",
avatar: "",
followers: 1200,
statuses: 340,
score: 95,
tags: ["česky", "komunita"],
category: "ostatni",
last_active: "2025-03-28"
},
{
name: "Linux CZ",
handle: "linuxcz@fosstodon.org",
bio: "Česká komunita okolo Linuxu a open source. Tipy, návody, diskuse.",
avatar: "",
followers: 870,
statuses: 1200,
score: 88,
tags: ["linux", "opensource", "tech"],
category: "tech",
last_active: "2025-03-29"
},
{
name: "Fedi.Tips CZ",
handle: "feditips@mstdn.social",
bio: "Tipy jak používat Mastodon a fediverse. Česky i anglicky.",
avatar: "",
followers: 650,
statuses: 980,
score: 82,
tags: ["mastodon", "fediverse", "tipy"],
category: "tech",
last_active: "2025-03-27"
},
{
name: "Open Source CZ",
handle: "oscz@mastodon.social",
bio: "Open source projekty, svobodný software a decentralizace po česku.",
avatar: "",
followers: 540,
statuses: 760,
score: 79,
tags: ["opensource", "svobodný software", "tech"],
category: "tech",
last_active: "2025-03-26"
},
{
name: "Česká věda",
handle: "ceskaveda@scholar.social",
bio: "Popularizace vědy a výzkumu v češtině. Biologie, fyzika, astronomie.",
avatar: "",
followers: 430,
statuses: 520,
score: 74,
tags: ["věda", "vzdělávání", "veda"],
category: "veda",
last_active: "2025-03-25"
},
{
name: "Foto CZ",
handle: "fotocz@mastodon.social",
bio: "Česká fotografická komunita. Krajiny, portréty, street foto.",
avatar: "",
followers: 390,
statuses: 890,
score: 71,
tags: ["fotografie", "foto", "umění"],
category: "foto",
last_active: "2025-03-28"
},
{
name: "Gaming CZ",
handle: "gamingcz@mastodon.social",
bio: "Videohry po česku. Recenze, novinky, diskuse bez korporátního hype.",
avatar: "",
followers: 310,
statuses: 430,
score: 65,
tags: ["gaming", "hry", "videohry"],
category: "gaming",
last_active: "2025-03-20"
},
{
name: "Kultura CZ",
handle: "kulturacz@mastodon.social",
bio: "Kultura, knihy, filmy a hudba v češtině.",
avatar: "",
followers: 280,
statuses: 350,
score: 61,
tags: ["kultura", "knihy", "hudba"],
category: "kultura",
last_active: "2025-03-22"
}
];
// ────────────────────────────────
// STATE
// ────────────────────────────────
let allAccounts = [];
let filtered = [];
let viewMode = 'grid'; // 'grid' | 'list'
// loadAccounts() přesunuto dolů (auto-refresh)
// ────────────────────────────────
// FILTER + SORT
// ────────────────────────────────
function applyFilters() {
const q = document.getElementById('searchInput').value.toLowerCase().trim();
const cat = document.getElementById('categoryFilter').value;
const sort = document.getElementById('sortFilter').value;
const activeTag = document.querySelector('.ftag.active')?.dataset.tag || '';
filtered = allAccounts.filter(a => {
const matchQ = !q ||
a.name.toLowerCase().includes(q) ||
(a.bio || '').toLowerCase().includes(q) ||
(a.handle || '').toLowerCase().includes(q) ||
(a.tags || []).some(t => t.toLowerCase().includes(q));
const matchCat = !cat || a.category === cat;
const matchTag = !activeTag ||
(a.tags || []).some(t => t.toLowerCase().includes(activeTag.toLowerCase()));
return matchQ && matchCat && matchTag;
});
filtered.sort((a, b) => {
if (sort === 'followers') return (b.followers || 0) - (a.followers || 0);
if (sort === 'activity') return (b.statuses || 0) - (a.statuses || 0);
return (b.score || 0) - (a.score || 0);
});
document.getElementById('visibleCount').textContent = filtered.length;
document.getElementById('emptyEl').style.display = filtered.length ? 'none' : 'block';
renderGrid();
}
// ────────────────────────────────
// RENDER
// ────────────────────────────────
function fmt(n) {
if (!n) return '0';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n);
}
function followUrl(handle) {
const instance = 'https://mamutovo.cz';
return `${instance}/authorize_interaction?uri=${encodeURIComponent('@' + handle)}`;
}
function avatarFallback(name) {
const colors = ['#00c896','#6364ff','#ff6b6b','#fbbf24','#a78bfa'];
const i = name.charCodeAt(0) % colors.length;
return `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='52' height='52'><rect width='52' height='52' fill='${encodeURIComponent(colors[i])}' rx='8'/><text x='50%' y='50%' font-size='22' text-anchor='middle' dominant-baseline='central' fill='white' font-family='sans-serif'>${name.charAt(0).toUpperCase()}</text></svg>`;
}
function cardHTML(a, idx) {
const av = a.avatar || avatarFallback(a.name);
const tags = (a.tags || []).slice(0, 3).map(t =>
`<span class="card-tag">#${t}</span>`).join('');
if (viewMode === 'list') {
return `
<div class="card" style="animation-delay:${idx * 0.03}s">
<img class="card-avatar-list" src="${av}" alt="" onerror="this.src='${avatarFallback(a.name)}'">
<div class="card-info">
<div class="card-name">${a.name}</div>
<div class="card-handle">@${a.handle}</div>
</div>
<div class="card-stats">
<div class="stat"><span class="stat-val">${fmt(a.followers)}</span><span class="stat-label">sledující</span></div>
<div class="stat"><span class="stat-val">${fmt(a.statuses)}</span><span class="stat-label">postů</span></div>
</div>
<div class="card-follow">
<a href="${followUrl(a.handle)}" target="_blank" class="follow-btn">+ Sledovat</a>
<span class="score-badge">★ ${a.score || '?'}</span>
</div>
</div>`;
}
return `
<div class="card" style="animation-delay:${idx * 0.04}s">
<div class="card-header">
${a.header ? `<img class="card-header-img" src="${a.header}" alt="">` : ''}
<img class="card-avatar" src="${av}" alt="" onerror="this.src='${avatarFallback(a.name)}'">
</div>
<div class="card-body">
<div class="card-name">${a.name}</div>
<div class="card-handle">@${a.handle}</div>
<div class="card-bio">${a.bio || ''}</div>
<div class="card-tags">${tags}</div>
<div class="card-stats">
<div class="stat">
<span class="stat-val">${fmt(a.followers)}</span>
<span class="stat-label">Sledující</span>
</div>
<div class="stat">
<span class="stat-val">${fmt(a.statuses)}</span>
<span class="stat-label">Postů</span>
</div>
</div>
<div class="card-follow">
<a href="${followUrl(a.handle)}" target="_blank" class="follow-btn">+ Sledovat</a>
<span class="score-badge">★ ${a.score || '?'}</span>
</div>
</div>
</div>`;
}
function renderGrid() {
const grid = document.getElementById('grid');
grid.innerHTML = filtered.map((a, i) => cardHTML(a, i)).join('');
grid.className = viewMode === 'list' ? 'grid list-view' : 'grid';
}
// ────────────────────────────────
// CSV EXPORT
// ────────────────────────────────
function toCSV(accounts) {
const rows = ['Account address,Show boosts'];
accounts.forEach(a => rows.push(`${a.handle},true`));
return rows.join('\n');
}
function downloadCSV(content, filename) {
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
}
// ────────────────────────────────
// EVENTS
// ────────────────────────────────
document.getElementById('searchInput').addEventListener('input', applyFilters);
document.getElementById('categoryFilter').addEventListener('change', applyFilters);
document.getElementById('sortFilter').addEventListener('change', applyFilters);
document.querySelectorAll('.ftag').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.ftag').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
applyFilters();
});
});
document.getElementById('gridBtn').addEventListener('click', () => {
viewMode = 'grid';
document.getElementById('gridBtn').classList.add('active');
document.getElementById('listBtn').classList.remove('active');
renderGrid();
});
document.getElementById('listBtn').addEventListener('click', () => {
viewMode = 'list';
document.getElementById('listBtn').classList.add('active');
document.getElementById('gridBtn').classList.remove('active');
renderGrid();
});
document.getElementById('downloadCsvBtn').addEventListener('click', () => {
downloadCSV(toCSV(filtered), 'mastodon-cz-filtered.csv');
});
document.getElementById('downloadAllCsvBtn').addEventListener('click', () => {
downloadCSV(toCSV(allAccounts), 'mastodon-cz-all.csv');
});
// ────────────────────────────────
// KATEGORIE popisky
// ────────────────────────────────
const CATEGORY_LABELS = {
tech: 'Tech & Linux',
kultura: 'Kultura & Umění',
zpravy: 'Zprávy & Politika',
veda: 'Věda & Vzdělávání',
foto: 'Fotografie',
gaming: 'Gaming',
ostatni: 'Ostatní',
};
// TOP hashtagy ze všech účtů zobrazíme nejčastější
function buildDynamicUI() {
// --- Kategorie select ---
const cats = [...new Set(allAccounts.map(a => a.category).filter(Boolean))].sort();
const sel = document.getElementById('categoryFilter');
// zachovej první option
sel.innerHTML = '<option value="">Všechny kategorie</option>';
cats.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat;
opt.textContent = CATEGORY_LABELS[cat] || cat;
sel.appendChild(opt);
});
// --- Hashtag tlačítka top 8 nejčastějších tagů ---
const tagCount = {};
allAccounts.forEach(a => (a.tags || []).forEach(t => {
tagCount[t] = (tagCount[t] || 0) + 1;
}));
const topTags = Object.entries(tagCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([t]) => t);
const wrap = document.getElementById('filterTags');
wrap.innerHTML = '<button class="ftag active" data-tag="">Vše</button>';
topTags.forEach(tag => {
const btn = document.createElement('button');
btn.className = 'ftag';
btn.dataset.tag = tag;
btn.textContent = '#' + tag;
wrap.appendChild(btn);
});
// Re-attach events na nová tlačítka
wrap.querySelectorAll('.ftag').forEach(btn => {
btn.addEventListener('click', () => {
wrap.querySelectorAll('.ftag').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
applyFilters();
});
});
}
// ────────────────────────────────
// AUTO-REFRESH každou hodinu
// ────────────────────────────────
const REFRESH_INTERVAL_MS = 60 * 60 * 1000; // 1 hodina
let lastGeneratedAt = null;
let refreshTimer = null;
let countdownTimer = null;
let nextRefreshAt = null;
function startCountdown() {
if (countdownTimer) clearInterval(countdownTimer);
nextRefreshAt = Date.now() + REFRESH_INTERVAL_MS;
countdownTimer = setInterval(() => {
const remaining = nextRefreshAt - Date.now();
if (remaining <= 0) { clearInterval(countdownTimer); return; }
const m = Math.floor(remaining / 60000);
const s = Math.floor((remaining % 60000) / 1000);
const el = document.getElementById('refreshCountdown');
if (el) el.textContent = `· obnoví za ${m}:${String(s).padStart(2,'0')}`;
}, 1000);
}
async function silentRefresh() {
try {
const res = await fetch('accounts.json?t=' + Date.now());
if (!res.ok) return;
const data = await res.json();
const newTs = data.updated_at || data.generated_at;
// Pouze pokud jsou data novější
if (newTs && newTs !== lastGeneratedAt) {
lastGeneratedAt = newTs;
allAccounts = data.accounts || data;
document.getElementById('totalCount').textContent = allAccounts.length;
document.getElementById('lastUpdated').textContent =
'Aktualizováno: ' + new Date(newTs).toLocaleDateString('cs-CZ', {
day: 'numeric', month: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit'
});
buildDynamicUI();
applyFilters();
// Flash indikátor
const badge = document.getElementById('refreshBadge');
if (badge) {
badge.textContent = '↻ aktualizováno';
badge.style.color = 'var(--accent)';
setTimeout(() => { badge.textContent = ''; badge.style.color = ''; }, 3000);
}
}
} catch { /* tiché selhání */ }
startCountdown();
refreshTimer = setTimeout(silentRefresh, REFRESH_INTERVAL_MS);
}
// ────────────────────────────────
// LOAD první načtení
// ────────────────────────────────
async function loadAccounts() {
try {
const res = await fetch('accounts.json?t=' + Date.now());
if (!res.ok) throw new Error('no json');
const data = await res.json();
allAccounts = data.accounts || data;
lastGeneratedAt = data.updated_at || data.generated_at || null;
if (lastGeneratedAt) {
document.getElementById('lastUpdated').textContent =
'Aktualizováno: ' + new Date(lastGeneratedAt).toLocaleDateString('cs-CZ', {
day: 'numeric', month: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit'
});
}
} catch {
allAccounts = FALLBACK_ACCOUNTS;
document.getElementById('lastUpdated').textContent = 'Ukázková data';
}
document.getElementById('loadingEl').style.display = 'none';
document.getElementById('totalCount').textContent = allAccounts.length;
buildDynamicUI();
applyFilters();
// Spusť auto-refresh
startCountdown();
refreshTimer = setTimeout(silentRefresh, REFRESH_INTERVAL_MS);
}
// ────────────────────────────────
// INIT
// ────────────────────────────────
loadAccounts();
</script>
</body>
</html>