feat(ui): introduce games-first landing page design

This commit completely redesigns the landing page to prioritize games over tools, reflecting a strategic shift in focus. The previous generic list has been replaced with a modern, two-column layout.

- A new 'Games' section displays projects as large, visual cards with thumbnails.
- A secondary 'Tools' section lists other services in a compact format.
- The CSS has been entirely rewritten for a new visual identity, improved responsiveness, and better animations.
- JavaScript logic is updated to categorize services into 'games' or 'tools' and render them accordingly.
This commit is contained in:
xiaomai
2025-09-11 15:52:10 +08:00
parent eb23a772d0
commit 6cbcbb1cbb

View File

@@ -3,272 +3,616 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Tootaio — All In One</title>
<meta name="description" content="Tootaio — All In One. Self-hosted tools collection." />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Playfair+Display:wght@600;700;900&display=swap" rel="stylesheet">
<!-- Optional: feather icons (small) -->
<script src="https://unpkg.com/feather-icons"></script>
<title>Tootaio — Games First</title>
<meta name="description" content="Tootaio — Game studio first, tools second." />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&family=Playfair+Display:wght@600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root{
--bg-1:#070406;
--bg-2:#0f0b0f;
--card:#0f0d10;
--muted: rgba(255,255,255,0.72);
--gold1: #FFD166;
--gold2: #F4A261;
--accent: linear-gradient(135deg,var(--gold1),var(--gold2));
:root {
--bg: #07060a;
--card: #131118;
--card-hover: #1a1721;
--muted: #bdb8c4;
--text: #f5f3f7;
--accent: #ffbf58;
--accent-2: #ff7b7b;
--accent-gradient: linear-gradient(135deg, var(--accent), var(--accent-2));
--gap: 24px;
--radius: 16px;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
--transition: all 0.3s ease;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family:Inter,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial;
background: radial-gradient(1200px 600px at 10% 10%, rgba(244,162,97,0.06), transparent),
radial-gradient(800px 400px at 90% 90%, rgba(255,209,102,0.04), transparent),
linear-gradient(180deg,var(--bg-1),var(--bg-2));
color: #fff; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
-webkit-text-size-adjust:100%;
padding:32px;
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Container */
.container{max-width:1200px;margin:0 auto}
/* Header */
.header{display:flex;align-items:center;justify-content:space-between;gap:16px}
.brand{display:flex;align-items:center;gap:14px}
.logo-wrap{width:72px;height:72px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;box-shadow:0 10px 30px rgba(0,0,0,0.6);}
.logo-text{font-family:'Playfair Display',serif;font-weight:700;color:#1b1200;letter-spacing:1px}
.brand-meta{line-height:1}
.brand-title{font-weight:800;font-size:20px}
.brand-sub{font-size:12px;color:var(--muted)}
/* Hero */
.hero{display:flex;flex-wrap:wrap;gap:28px;margin-top:28px}
.hero-left{flex:1;min-width:300px}
.hero-right{width:480px;min-width:280px}
h1{font-family:'Playfair Display',serif;margin:0;font-weight:900;font-size:44px;line-height:1}
p.lead{color:var(--muted);margin-top:14px;max-width:700px}
.cta-row{margin-top:20px;display:flex;gap:12px}
.btn{padding:12px 18px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:8px}
.btn-primary{background:var(--accent);color:#1b1200;font-weight:700;box-shadow:0 8px 30px rgba(248,176,47,0.12)}
.btn-ghost{background:transparent;border:1px solid rgba(255,255,255,0.08);color:var(--muted)}
/* glass card */
.glass{background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.02)); border-radius:14px; padding:18px; border:1px solid rgba(255,255,255,0.04); box-shadow:0 10px 30px rgba(6,6,6,0.6)}
/* Services grid */
.services-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:16px}
.service{padding:14px;border-radius:12px;background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent);border:1px solid rgba(255,255,255,0.03);display:flex;align-items:center;justify-content:space-between;gap:12px;text-decoration:none;color:inherit}
.service:hover{transform:translateY(-6px);transition:transform 220ms cubic-bezier(.2,.9,.3,1)}
.service-title{font-weight:700}
.service-desc{font-size:12px;color:var(--muted);margin-top:6px}
/* Secondary links */
.links-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px}
.link-pill{display:inline-flex;gap:8px;align-items:center;padding:8px 12px;border-radius:999px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);text-decoration:none;color:var(--muted)}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(180deg, #050406 0%, #0b0710 100%);
color: var(--text);
font-family: Inter, system-ui, Segoe UI, Roboto, sans-serif;
padding: 32px;
line-height: 1.6;
}
.wrap {
max-width: 1280px;
margin: 0 auto;
}
/* Header Styles */
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 40px;
}
.logo {
display: flex;
gap: 16px;
align-items: center;
transition: var(--transition);
}
.logo:hover {
transform: translateY(-2px);
}
.logo-mark {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-family: Playfair Display, serif;
background: var(--accent-gradient);
color: #251200;
box-shadow: 0 8px 30px rgba(255, 150, 0, 0.2);
font-size: 22px;
}
h1 {
font-family: Playfair Display, serif;
margin: 0;
font-size: 36px;
font-weight: 800;
letter-spacing: -0.5px;
background: linear-gradient(to right, #ffbf58, #ff7b7b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: var(--muted);
font-size: 14px;
margin-top: 8px;
font-weight: 400;
}
.top-controls {
display: flex;
gap: 12px;
align-items: center;
}
.search {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
padding: 12px 16px;
border-radius: 12px;
display: flex;
gap: 10px;
align-items: center;
transition: var(--transition);
}
.search:focus-within {
border-color: rgba(255, 191, 88, 0.3);
box-shadow: 0 0 0 3px rgba(255, 191, 88, 0.1);
}
.search input {
background: transparent;
border: 0;
outline: none;
color: inherit;
width: 240px;
font-size: 14px;
}
.search input::placeholder {
color: var(--muted);
}
.btn {
padding: 12px 18px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
cursor: pointer;
transition: var(--transition);
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
}
/* Main Content */
.main {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--gap);
margin-top: 32px;
}
.section-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
color: var(--accent);
font-size: 18px;
}
.games, .tools {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
padding: 24px;
border-radius: var(--radius);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: var(--shadow);
transition: var(--transition);
}
.games:hover, .tools:hover {
border-color: rgba(255, 255, 255, 0.08);
}
.games-header, .tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.count {
font-size: 13px;
color: var(--muted);
background: rgba(255, 255, 255, 0.03);
padding: 4px 10px;
border-radius: 20px;
}
/* Games Grid */
.games-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.game-card {
position: relative;
border-radius: 14px;
overflow: hidden;
cursor: pointer;
min-height: 180px;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 16px;
background-size: cover;
background-position: center;
border: 1px solid rgba(0, 0, 0, 0.3);
transition: var(--transition);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.game-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, transparent 70%);
opacity: 0.8;
transition: var(--transition);
}
.game-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
}
.game-card:hover::before {
opacity: 0.9;
}
.game-card .meta {
position: relative;
z-index: 2;
backdrop-filter: blur(6px);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));
padding: 12px;
border-radius: 10px;
transition: var(--transition);
}
.game-card:hover .meta {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.7));
}
.game-title {
font-weight: 700;
font-size: 16px;
margin-bottom: 4px;
}
.game-sub {
font-size: 13px;
color: var(--muted);
line-height: 1.4;
}
/* Tools List */
.tools-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.tool-item {
display: flex;
gap: 16px;
align-items: center;
padding: 16px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.02);
cursor: pointer;
transition: var(--transition);
}
.tool-item:hover {
transform: translateX(5px);
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.08);
}
.tool-icon {
width: 52px;
height: 52px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
background: var(--accent-gradient);
color: #251200;
font-size: 18px;
flex-shrink: 0;
}
.tool-content {
flex: 1;
}
.tool-title {
font-weight: 700;
font-size: 16px;
margin-bottom: 4px;
}
.tool-desc {
font-size: 13px;
color: var(--muted);
line-height: 1.4;
}
/* Footer */
footer{margin-top:36px;padding-top:24px;border-top:1px solid rgba(255,255,255,0.03);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
/* search */
.search{display:flex;align-items:center;background:rgba(255,255,255,0.02);padding:8px 12px;border-radius:12px;border:1px solid rgba(255,255,255,0.03)}
.search input{background:transparent;border:0;color:inherit;outline:none;width:260px}
/* responsiveness */
@media (max-width:900px){
.hero{flex-direction:column}
.hero-right{width:auto}
.services-grid{grid-template-columns:1fr}
h1{font-size:36px}
footer {
margin-top: 40px;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--muted);
font-size: 14px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.footer-brand {
font-weight: 600;
}
.footer-tagline {
display: flex;
align-items: center;
gap: 8px;
}
.footer-tagline i {
color: var(--accent);
font-size: 12px;
}
/* Responsive */
@media (max-width: 1024px) {
.main {
grid-template-columns: 1fr;
}
.games-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
body {
padding: 20px;
}
header {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.top-controls {
width: 100%;
justify-content: space-between;
}
.search input {
width: 100%;
}
.games-grid {
grid-template-columns: 1fr;
}
footer {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
}
@media (max-width: 480px) {
.logo-mark {
width: 52px;
height: 52px;
font-size: 18px;
}
h1 {
font-size: 28px;
}
.btn {
padding: 10px 14px;
}
}
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.game-card, .tool-item {
animation: fadeIn 0.5s ease-out;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* subtle marble SVG overlay */
.marble{position:absolute;right:-120px;top:-120px;opacity:0.06;pointer-events:none}
/* small helper */
.muted{color:var(--muted)}
</style>
</head>
<body>
<div class="container">
<header class="header">
<div class="brand">
<div class="logo-wrap"><div class="logo-text">T AIO</div></div>
<div class="brand-meta">
<div class="brand-title">Tootaio</div>
<div class="brand-sub">All In One · 私有托管 · 高端体验</div>
<div class="wrap">
<header>
<div class="logo">
<div class="logo-mark">T</div>
<div>
<h1>Tootaio</h1>
<div class="subtitle">Game Studio · Tools as a service</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:10px">
<div class="search glass">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input id="q" placeholder="Search services... (按回车)" />
<div class="top-controls">
<div class="search">
<i class="fas fa-search"></i>
<input id="q" placeholder="查找游戏或工具(回车" />
</div>
<button id="langBtn" class="btn btn-ghost">中文</button>
<button id="langBtn" class="btn">
<i class="fas fa-globe"></i>
<span>中文</span>
</button>
</div>
</header>
<main class="hero">
<div class="hero-left">
<h1>奢华·私有·一站式 —— <span style="background:linear-gradient(90deg,var(--gold1),var(--gold2));-webkit-background-clip:text;background-clip:text;color:transparent">Tootaio</span></h1>
<p class="lead">将最常用的自托管服务与精品工具收纳于一处。我们为个人与团队提供稳定、可控且有温度的基础设施体验。</p>
<div class="cta-row">
<a class="btn btn-primary" href="#services">查看服务</a>
<a class="btn btn-ghost" href="#contact">联系我们</a>
</div>
<div class="links-row" id="otherLinksRegion"></div>
</div>
<div class="hero-right">
<div class="glass" id="services">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<div style="font-weight:800">精品服务速览</div>
<div class="muted" style="font-size:12px;margin-top:6px">点击卡片以在新标签页打开服务</div>
</div>
<div id="stats" class="muted" style="font-size:12px">加载中…</div>
<main class="main">
<section class="games" aria-label="Games">
<div class="games-header">
<div class="section-title">
<i class="fas fa-gamepad"></i>
<span>Games</span>
</div>
<div class="services-grid" id="servicesGrid" style="margin-top:12px"></div>
<div class="count" id="gamesCount"></div>
</div>
</div>
<div class="games-grid" id="gamesGrid"></div>
</section>
<aside class="tools" aria-label="Tools">
<div class="tools-header">
<div class="section-title">
<i class="fas fa-tools"></i>
<span>Tools</span>
</div>
<div class="count" id="toolsCount"></div>
</div>
<div class="tools-list" id="toolsList"></div>
</aside>
</main>
<footer id="contact">
<div>
<div style="font-weight:700">Tootaio</div>
<div class="muted" style="font-size:13px;margin-top:6px">欲了解更多,请通过下面链接联系。© <span id="year"></span></div>
<footer>
<div class="footer-brand">Tootaio — <span id="year"></span></div>
<div class="footer-tagline">
<i class="fas fa-heart"></i>
<span>Focused on Games • Tools for ops & creators</span>
</div>
<div id="footerLinks" style="display:flex;gap:12px;align-items:center"></div>
</footer>
</div>
<!-- decorative SVG marble -->
<svg class="marble" width="360" height="360" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="g1" x1="0%" x2="100%"><stop offset="0%" stop-color="#FFD166" stop-opacity="0.16"/><stop offset="100%" stop-color="#F4A261" stop-opacity="0.08"/></linearGradient>
</defs>
<circle cx="100" cy="100" r="80" fill="url(#g1)"/>
</svg>
<script>
// Minimal default config (falls back if siteConfig.json missing)
const defaultSiteConfig = {
services: [
{ name: "supergame.tootaio.com", url: "https://supergame.tootaio.com", description_en: "Our flagship game demo.", description_zh: "我们的旗舰游戏演示。", category: "game", thumbnail: "" },
{ name: "git.tootaio.com", url: "https://git.tootaio.com", description_en: "Gitea self-host.", description_zh: "代码托管服务。", category: "tool" },
{ name: "memos.tootaio.com", url: "https://memos.tootaio.com", description_en: "Notes", description_zh: "笔记空间", category: "tool" },
{ name: "life-restart.tootaio.com", url: "https://life-restart.tootaio.com", description_en: "Mirrored game.", description_zh: "游戏镜像", category: "game" }
]
};
<script>
// Default fallback config (your siteConfig.json can override this by being placed next to index.html)
const defaultSiteConfig = {};
async function loadConfig(){
try{
const res = await fetch('./siteConfig.json', {cache:'no-store'});
if(!res.ok) throw new Error('no siteConfig.json');
const cfg = await res.json();
return cfg;
}catch(e){
console.warn('siteConfig.json not found, using default config.');
return defaultSiteConfig;
}
}
// Try to fetch siteConfig.json; fall back to defaultSiteConfig
async function loadConfig(){
try{
const res = await fetch('./siteConfig.json', {cache:'no-store'});
if(!res.ok) throw new Error('no siteConfig.json');
const cfg = await res.json();
return cfg;
}catch(err){
console.warn('Using embedded default siteConfig (could not fetch ./siteConfig.json) —', err.message);
return defaultSiteConfig;
function guessCategory(s){
// if not present, try to guess from name/description
if(s.category) return s.category;
const name = (s.name || '').toLowerCase();
const desc = (s.description_en||'' + s.description_zh||'').toLowerCase();
if(/game|play|demo|unity|godot|itch|gamepad|pixel/.test(name+desc)) return 'game';
return 'tool';
}
function makeThumbPlaceholder(name, size=320){
// generate simple SVG data URI with initials
const initials = (name || '').split(/[.\\-\\s]/).slice(0,2).map(x=>x[0]?.toUpperCase()||'').join('');
const bg = ['#d97706','#ef4444','#7c3aed','#06b6d4','#f59e0b'][Math.abs(name.length)%5];
const fg = '#fff';
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${Math.round(size*0.6)}'><rect width='100%' height='100%' fill='${bg}' rx='8'/><text x='50%' y='55%' font-size='36' dominant-baseline='middle' text-anchor='middle' fill='${fg}' font-family='Inter,system-ui'>${initials}</text></svg>`;
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}
function render(cfg, lang='zh'){
const services = (cfg.services || []).map(s => ({...s, category: guessCategory(s)}));
const games = services.filter(s => s.category === 'game');
const tools = services.filter(s => s.category !== 'game');
const gamesGrid = document.getElementById('gamesGrid');
const toolsList = document.getElementById('toolsList');
gamesGrid.innerHTML = '';
toolsList.innerHTML = '';
games.forEach(s => {
const thumb = s.thumbnail || makeThumbPlaceholder(s.name);
const a = document.createElement('a');
a.className = 'game-card';
a.href = s.url;
a.target = '_blank';
a.rel = 'noreferrer';
a.style.backgroundImage = `url('${thumb}')`;
const meta = document.createElement('div'); meta.className = 'meta';
const title = document.createElement('div'); title.className = 'game-title'; title.textContent = s.name;
const sub = document.createElement('div'); sub.className = 'game-sub'; sub.textContent = lang === 'zh' ? (s.description_zh || s.description_en || '') : (s.description_en || s.description_zh || '');
meta.appendChild(title); meta.appendChild(sub);
a.appendChild(meta);
gamesGrid.appendChild(a);
});
tools.forEach(s => {
const icon = document.createElement('div'); icon.className = 'tool-icon';
icon.textContent = (s.name || '').slice(0,2).toUpperCase();
const toolContent = document.createElement('div'); toolContent.className = 'tool-content';
const tTitle = document.createElement('div'); tTitle.className = 'tool-title'; tTitle.textContent = s.name;
const tSub = document.createElement('div'); tSub.className = 'tool-desc'; tSub.textContent = lang === 'zh' ? (s.description_zh || '') : (s.description_en || '');
toolContent.appendChild(tTitle); toolContent.appendChild(tSub);
const a = document.createElement('a'); a.className = 'tool-item'; a.href = s.url; a.target='_blank'; a.rel='noreferrer';
a.appendChild(icon); a.appendChild(toolContent);
toolsList.appendChild(a);
});
document.getElementById('gamesCount').textContent = games.length + ' 项目';
document.getElementById('toolsCount').textContent = tools.length + ' 项目';
}
(async function init(){
const cfg = await loadConfig();
let lang = 'zh';
render(cfg, lang);
document.getElementById('year').textContent = new Date().getFullYear();
// search
const q = document.getElementById('q');
q.addEventListener('keydown',(e)=>{
if(e.key === 'Enter'){
const term = q.value.trim().toLowerCase();
if(!term){ render(cfg, lang); return; }
const filtered = (cfg.services||[]).filter(s => {
const txt = (s.name + ' ' + (s.description_en||'') + ' ' + (s.description_zh||'')).toLowerCase();
return txt.includes(term);
});
render({services:filtered}, lang);
}
}
});
// render helpers
function el(tag, props={}, children=null){
const e = document.createElement(tag);
for(const k in props){
if(k === 'class') e.className = props[k];
else if(k === 'html') e.innerHTML = props[k];
else e.setAttribute(k, props[k]);
}
if(children){
if(Array.isArray(children)) children.forEach(c => e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c));
else e.appendChild(typeof children === 'string' ? document.createTextNode(children) : children);
}
return e;
}
// keyboard shortcut to focus search
window.addEventListener('keydown',(e)=>{ if(e.key === '/' && document.activeElement !== q){ e.preventDefault(); q.focus(); }});
// icon map
const ICONS = {
'github': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3"/><path d="M20 8c0-3.6-2.1-6-4.5-6S11 4.4 11 8c0 3.2 2.2 5.2 5 6 2.8.8 5 2.8 5 6 0 3-2.5 4-4.5 4-1 0-2.5-.3-3.5-1.2"/></svg>',
'blog': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 2v4"/><path d="M16 2v4"/></svg>',
'discord': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 14s1.5 1 4 1 4-1 4-1"/><path d="M7 7s1.5-.5 5-1c3.5.5 5 1 5 1v8s-1.5-.8-5-1c-3.2.2-5 1-5 1V7z"/></svg>',
'video': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="15" height="10" rx="2"/><polygon points="23 7 16 12 23 17 23 7"/></svg>'
};
// language toggle
document.getElementById('langBtn').addEventListener('click', ()=>{
lang = (lang === 'zh') ? 'en' : 'zh';
document.getElementById('langBtn').querySelector('span').textContent = (lang === 'zh') ? '中文' : 'EN';
render(cfg, lang);
});
function renderServices(cfg, lang='zh'){
const grid = document.getElementById('servicesGrid'); grid.innerHTML = '';
(cfg.services || []).forEach(s => {
const a = el('a',{class:'service',href:s.url,target:'_blank',rel:'noreferrer'});
const left = el('div');
const title = el('div',{class:'service-title'}, s.name);
const desc = el('div',{class:'service-desc'}, lang === 'zh' ? (s.description_zh || s.description_en || '') : (s.description_en || s.description_zh || ''));
left.appendChild(title); left.appendChild(desc);
const arrow = el('div',{html:'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>'});
a.appendChild(left); a.appendChild(arrow);
grid.appendChild(a);
});
document.getElementById('stats').textContent = (cfg.services||[]).length + ' services';
}
function renderOtherLinks(cfg, lang='zh'){
const region = document.getElementById('otherLinksRegion'); region.innerHTML = '';
const footer = document.getElementById('footerLinks'); footer.innerHTML = '';
(cfg.otherLinks || []).forEach(l =>{
const pill = el('a',{class:'link-pill',href:l.url,target:'_blank',rel:'noreferrer',html:(ICONS[l.icon]||ICONS['blog']) + ' ' + (lang==='zh'?l.name_zh:l.name_en)});
region.appendChild(pill);
const f = el('a',{href:l.url,target:'_blank',rel:'noreferrer',class:'link-pill',html:(ICONS[l.icon]||'') + ' ' + (lang==='zh'?l.name_zh:l.name_en)});
footer.appendChild(f);
});
}
(async function init(){
const cfg = await loadConfig();
let lang = 'zh';
renderServices(cfg, lang);
renderOtherLinks(cfg, lang);
// year
document.getElementById('year').textContent = new Date().getFullYear();
// search
const q = document.getElementById('q');
q.addEventListener('keydown', (e)=>{
if(e.key === 'Enter'){
const term = q.value.trim().toLowerCase();
if(!term){ renderServices(cfg, lang); return; }
const filtered = (cfg.services||[]).filter(s => (s.name + ' ' + (s.description_en||'') + ' ' + (s.description_zh||'')).toLowerCase().includes(term));
renderServices({services:filtered}, lang);
}
});
// language toggle
const langBtn = document.getElementById('langBtn');
langBtn.addEventListener('click', ()=>{
lang = (lang === 'zh') ? 'en' : 'zh';
langBtn.textContent = (lang === 'zh') ? '中文' : 'EN';
renderServices(cfg, lang);
renderOtherLinks(cfg, lang);
// heading translations (minimal)
document.querySelector('.lead').textContent = (lang === 'zh') ? '将最常用的自托管服务与精品工具收纳于一处。我们为个人与团队提供稳定、可控且有温度的基础设施体验。' : 'We gather the most useful self-hosted tools — from source hosting and cloud storage to game asset mirrors. Tootaio delivers luxurious, stable solutions for individuals and teams.';
});
// quick keyboard: '/' focus search
window.addEventListener('keydown', (e)=>{ if(e.key === '/' && document.activeElement !== q){ e.preventDefault(); q.focus(); }});
})();
</script>
<!-- Usage notes (as comments):
- Drop your siteConfig.json next to this index.html (same folder). The page will try to fetch it and use it.
- You can freely edit styles in the <style> block to tune the "luxury" palette and spacing.
- If you want small enhancements: add pagination, category tags, icons per service, or a small admin UI to edit siteConfig in-browser.
-->
})();
</script>
</body>
</html>
</html>