Files

14 lines
4.3 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><link rel="icon" href="/static/favicon.svg?v=2" type="image/svg+xml"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ .site_title }}</title><link rel="stylesheet" href="/static/css/tonav.css?v=mobile2"></head>
<body class="app-shell">
<div class="app-layout">
<aside class="sidebar"><a class="brand" href="/">{{ .site_title }}</a><div class="sidebar-section"><div class="section-label">SITES</div><button class="nav-item active" data-category="all">All <span id="allCount"></span></button><div id="categoryNav"></div></div><div class="sidebar-footer"><a href="/admin/login">Admin sign in <span>↗</span></a></div></aside>
<main class="main"><header class="topbar"><div><div class="eyebrow">SITES</div><h1>Site directory</h1></div><div class="top-actions"><a class="admin-link" href="/admin/login">Sign in</a><button class="icon-button" id="themeBtn" title="Toggle theme" aria-label="Toggle theme">◐</button></div></header>
<div class="mobile-categories" id="mobileCategories"></div><div class="toolbar"><label class="search"><span>⌕</span><input id="searchInput" placeholder="Search sites, tags, descriptions…" autocomplete="off"><kbd>/</kbd></label><div class="result-count" id="resultCount"></div></div>
<section class="service-list" id="sitesGrid"><div class="empty-state">Loading…</div></section>
<footer class="page-footer"><span>ToNav</span><span id="lastCheckTime"></span></footer></main></div>
<script>const categoriesData={{ .categories_json }};const sitesData={{ .services_json }};let currentTab='all',currentKeyword='';const map={};categoriesData.forEach(c=>map[c.id]=c.name);const esc=s=>{const d=document.createElement('div');d.textContent=s||'';return d.innerHTML};
function renderNav(){const n=document.getElementById('categoryNav');const m=document.getElementById('mobileCategories');document.getElementById('allCount').textContent=sitesData.length;const items=categoriesData.map(c=>`<button class="nav-item" data-category="${esc(c.name)}">${esc(c.name)} <span>${sitesData.filter(s=>s.category_id===c.id).length}</span></button>`).join('');n.innerHTML=items;m.innerHTML=`<button class="mobile-category active" data-category="all">All <span>${sitesData.length}</span></button>`+categoriesData.map(c=>`<button class="mobile-category" data-category="${esc(c.name)}">${esc(c.name)} <span>${sitesData.filter(s=>s.category_id===c.id).length}</span></button>`).join('');document.querySelectorAll('.nav-item,.mobile-category').forEach(b=>b.onclick=()=>{currentTab=b.dataset.category;document.querySelectorAll('.nav-item,.mobile-category').forEach(x=>x.classList.toggle('active',x.dataset.category===currentTab));render()})}
function render(){let a=currentTab==='all'?sitesData:sitesData.filter(s=>map[s.category_id]===currentTab);if(currentKeyword)a=a.filter(s=>(s.name+' '+(s.description||'')+' '+(s.tags||'')).toLowerCase().includes(currentKeyword));document.getElementById('resultCount').textContent=`${a.length} sites`;document.getElementById('sitesGrid').innerHTML=a.length?a.map(s=>`<a class="service-row" href="${esc(s.url)}" target="_blank" rel="noopener"><span class="service-icon">${esc(s.icon)||'↗'}</span><span class="service-main"><strong>${esc(s.name)}</strong><span>${esc(s.description||s.url)}</span></span><span class="service-meta"><span class="status ${s.status==='online'?'online':s.status==='offline'?'offline':''}"></span>${esc(map[s.category_id]||'Uncategorized')}</span><span class="service-arrow">→</span></a>`).join(''):'<div class="empty-state">No sites found</div>'}
document.getElementById('searchInput').oninput=e=>{currentKeyword=e.target.value.toLowerCase().trim();render()};document.addEventListener('keydown',e=>{if(e.key==='/'&&document.activeElement.tagName!=='INPUT'){e.preventDefault();document.getElementById('searchInput').focus()}});document.getElementById('themeBtn').onclick=()=>{document.documentElement.classList.toggle('dark');localStorage.setItem('tonav-theme',document.documentElement.classList.contains('dark')?'dark':'light')};if(localStorage.getItem('tonav-theme')==='dark')document.documentElement.classList.add('dark');renderNav();render();document.getElementById('lastCheckTime').textContent=new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'});</script><script src="/static/js/i18n.js?v=3"></script></body></html>