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.
618 lines
17 KiB
HTML
618 lines
17 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<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: #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;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
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: 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);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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 class="top-controls">
|
|
<div class="search">
|
|
<i class="fas fa-search"></i>
|
|
<input id="q" placeholder="查找游戏或工具(回车)" />
|
|
</div>
|
|
<button id="langBtn" class="btn">
|
|
<i class="fas fa-globe"></i>
|
|
<span>中文</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<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="count" id="gamesCount">—</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>
|
|
<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>
|
|
</footer>
|
|
</div>
|
|
|
|
<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" }
|
|
]
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
// keyboard shortcut to focus search
|
|
window.addEventListener('keydown',(e)=>{ if(e.key === '/' && document.activeElement !== q){ e.preventDefault(); q.focus(); }});
|
|
|
|
// language toggle
|
|
document.getElementById('langBtn').addEventListener('click', ()=>{
|
|
lang = (lang === 'zh') ? 'en' : 'zh';
|
|
document.getElementById('langBtn').querySelector('span').textContent = (lang === 'zh') ? '中文' : 'EN';
|
|
render(cfg, lang);
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |