website is now phone

This commit is contained in:
esef committed 2026-09-23 17:21:50 +03:00
1 parent f4457c5c17
commit 827f524107
16 files changed
+352 -168

No files matched your search

+1
View File
@@ -53,6 +53,7 @@ There are **no automated tests** in this project.
## Coding Style & Naming Conventions ## Coding Style & Naming Conventions
- **HTML:** Semantic elements where practical. Tab contents use `<section id="…" class="tab-content">`. IDs are kebab-case. - **HTML:** Semantic elements where practical. Tab contents use `<section id="…" class="tab-content">`. IDs are kebab-case.
- **Responsive:** modelled on userevolt.app. `styles.css` owns the breakpoints: ≤1300px tightens the nav buttons, ≤900px switches to a 56px bar with a hamburger dropdown (`#nav-hamburger`, `.nav-open` on `#top-line`, handled in `tabs.js`). Each tab stylesheet ends with its own `@media (max-width: 900px)` block (top offsets drop from ~120px to ~72px there). Tab buttons live in `#nav-tabs`, so style them as `#nav-tabs button`.
- **CSS:** Each tab has its own stylesheet under `style/`. Class names follow BEM-lite (`cal-day`, `dash-card`). CSS variables define layout knobs (`--dash-columns`, `--dash-gap`). - **CSS:** Each tab has its own stylesheet under `style/`. Class names follow BEM-lite (`cal-day`, `dash-card`). CSS variables define layout knobs (`--dash-columns`, `--dash-gap`).
- **JavaScript:** Vanilla ES6+, no transpilation. Functions are `camelCase`, DOM-referencing IDs match kebab-case HTML IDs. Each script is self-contained with a `DOMContentLoaded` guard, and Firestore-backed scripts are wrapped in an IIFE so their helpers don't leak into (and clobber each other in) the global scope. Firebase config is repeated per-file (same values) — keep this pattern. - **JavaScript:** Vanilla ES6+, no transpilation. Functions are `camelCase`, DOM-referencing IDs match kebab-case HTML IDs. Each script is self-contained with a `DOMContentLoaded` guard, and Firestore-backed scripts are wrapped in an IIFE so their helpers don't leak into (and clobber each other in) the global scope. Firebase config is repeated per-file (same values) — keep this pattern.
- **Whitespace:** 4-space indentation in JS/CSS, 2-space in HTML. No trailing semicolons are required but consistency within each file is expected. - **Whitespace:** 4-space indentation in JS/CSS, 2-space in HTML. No trailing semicolons are required but consistency within each file is expected.
+7 -18
View File
@@ -1,14 +1,5 @@
"""Build gallery thumbnails + manifest. # run this after adding images to gallery/
# makes thumbnails, turns gifs into way smaller mp4s (needs ffmpeg) and updates manifest.json
Run after adding images to gallery/:
python generate_thumbnails.py
- Converts animated GIFs to MP4 (needs ffmpeg on PATH). Same animation at a
few % of the size; the GIF stays as the source but the site uses the MP4.
- Creates gallery/thumbnails/<name>.jpg (max 400x400) for any item missing one.
- Rewrites gallery/manifest.json: the ordered list of full-size filenames
that js/gallery.js renders (the MP4 wins when a GIF has one).
"""
import json import json
import os import os
import shutil import shutil
@@ -23,7 +14,7 @@ FFMPEG = shutil.which("ffmpeg")
def sort_key(filename): def sort_key(filename):
# Numeric names sort numerically (1, 2, ..., 10), anything else after, alphabetically # so 10.jpg comes after 9.jpg
stem = os.path.splitext(filename)[0] stem = os.path.splitext(filename)[0]
return (0, int(stem), "") if stem.isdigit() else (1, 0, stem.lower()) return (0, int(stem), "") if stem.isdigit() else (1, 0, stem.lower())
@@ -48,10 +39,10 @@ def convert_gifs(base_folder):
continue continue
with Image.open(src) as img: with Image.open(src) as img:
if getattr(img, "n_frames", 1) < 2: if getattr(img, "n_frames", 1) < 2:
continue # still image, keep as GIF continue # not animated
subprocess.run([ subprocess.run([
FFMPEG, "-hide_banner", "-loglevel", "error", "-y", "-i", src, FFMPEG, "-hide_banner", "-loglevel", "error", "-y", "-i", src,
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", # h264 needs even sizes "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", # mp4 needs even width/height
"-c:v", "libx264", "-crf", "23", "-pix_fmt", "yuv420p", "-c:v", "libx264", "-crf", "23", "-pix_fmt", "yuv420p",
"-an", "-movflags", "+faststart", dst, "-an", "-movflags", "+faststart", dst,
], check=True) ], check=True)
@@ -69,10 +60,9 @@ def make_thumbnail(src, thumb_path):
], check=True) ], check=True)
return return
with Image.open(src) as img: with Image.open(src) as img:
# JPEG needs RGB (e.g. for RGBA PNGs or palette GIFs)
if img.mode != "RGB": if img.mode != "RGB":
img = img.convert("RGB") img = img.convert("RGB")
img.thumbnail(THUMB_SIZE) # keeps aspect ratio img.thumbnail(THUMB_SIZE)
img.save(thumb_path, "JPEG", quality=85) img.save(thumb_path, "JPEG", quality=85)
@@ -82,7 +72,6 @@ def create_thumbnails(base_folder, files):
count = 0 count = 0
for filename in files: for filename in files:
# Always save thumbnail as jpg
stem = os.path.splitext(filename)[0] stem = os.path.splitext(filename)[0]
thumb_path = os.path.join(thumb_dir, f"{stem}.jpg") thumb_path = os.path.join(thumb_dir, f"{stem}.jpg")
if os.path.exists(thumb_path): if os.path.exists(thumb_path):
@@ -98,7 +87,7 @@ def create_thumbnails(base_folder, files):
def pick_for_site(files): def pick_for_site(files):
# One entry per name; prefer the MP4 over its source GIF # use the mp4 instead of the gif when both exist
chosen = {} chosen = {}
for f in files: for f in files:
stem, ext = os.path.splitext(f) stem, ext = os.path.splitext(f)
+11 -14
View File
@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style/styles.css"> <link rel="stylesheet" href="style/styles.css">
<link rel="stylesheet" href="style/gallery.css"> <link rel="stylesheet" href="style/gallery.css">
<link rel="stylesheet" href="style/comments.css"> <link rel="stylesheet" href="style/comments.css">
@@ -28,17 +29,22 @@
<source src="textures/bot_background.mp4" type="video/mp4"> <source src="textures/bot_background.mp4" type="video/mp4">
</video> </video>
<div id="top-line"> <div id="top-line">
<span id="nav-current"></span>
<nav id="nav-tabs">
<button id="about-btn">About This Website</button> <button id="about-btn">About This Website</button>
<button id="gallery-btn">Gallery</button> <button id="gallery-btn">Gallery</button>
<button id="bucketlist-btn">Bucketlist</button> <button id="bucketlist-btn">Bucketlist</button>
<button id="yuukabot-btn">Bot Status</button> <button id="yuukabot-btn">Bot Status</button>
<button id="calendar-btn">Calendar</button> <button id="calendar-btn">Calendar</button>
<button id="services-btn">Services</button> <button id="services-btn">Services</button>
</nav>
<button id="nav-hamburger" aria-label="Menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
</div> </div>
<section id="about" class="tab-content"> <section id="about" class="tab-content">
<!-- Top half: welcome -->
<div id="about-hero"> <div id="about-hero">
<div id="click-counter-container"> <div id="click-counter-container">
<img src="textures/yuuka_click.webp" id="yuuka-click-btn" alt="Click Me!"> <img src="textures/yuuka_click.webp" id="yuuka-click-btn" alt="Click Me!">
@@ -52,20 +58,17 @@
</div> </div>
</div> </div>
<!-- Bottom half: Steam-style comments --> <!-- comments -->
<div id="comments-section"> <div id="comments-section">
<!-- Steam panel header -->
<div class="steam-header"> <div class="steam-header">
<span class="steam-header-title">COMMENTS</span> <span class="steam-header-title">COMMENTS</span>
<span id="comments-title"></span> <span id="comments-title"></span>
</div> </div>
<!-- Post form — at the top -->
<div class="steam-form-area"> <div class="steam-form-area">
<p class="steam-form-label">Leave a comment:</p> <p class="steam-form-label">Leave a comment:</p>
<!-- Avatar picker -->
<div id="avatar-picker"> <div id="avatar-picker">
<span class="picker-label">Avatar:</span> <span class="picker-label">Avatar:</span>
@@ -101,10 +104,8 @@
</form> </form>
</div> </div>
<!-- Comments feed -->
<div id="comments-list"></div> <div id="comments-list"></div>
<!-- Pagination -->
<div id="comments-pagination" class="steam-pagination"></div> <div id="comments-pagination" class="steam-pagination"></div>
</div> </div>
@@ -122,7 +123,6 @@
</header> </header>
<div class="dash-grid"> <div class="dash-grid">
<!-- Row 1: 3 boxes -->
<div class="dash-card span2"> <div class="dash-card span2">
<div class="card-top"><span class="card-label">Uptime</span></div> <div class="card-top"><span class="card-label">Uptime</span></div>
<p class="card-value" id="dash-uptime">—</p> <p class="card-value" id="dash-uptime">—</p>
@@ -142,7 +142,6 @@
<span class="card-detail">1m / 5m / 15m</span> <span class="card-detail">1m / 5m / 15m</span>
</div> </div>
<!-- Row 2: 3 boxes -->
<div class="dash-card span2"> <div class="dash-card span2">
<div class="card-top"><span class="card-label">Network</span></div> <div class="card-top"><span class="card-label">Network</span></div>
<p class="card-value" id="dash-net">—</p> <p class="card-value" id="dash-net">—</p>
@@ -162,7 +161,6 @@
<span class="card-detail" id="dash-swap-detail"></span> <span class="card-detail" id="dash-swap-detail"></span>
</div> </div>
<!-- Row 3: 2 boxes stretched -->
<div class="dash-card span3"> <div class="dash-card span3">
<div class="card-top"><span class="card-label">CPU</span></div> <div class="card-top"><span class="card-label">CPU</span></div>
<p class="card-value" id="dash-cpu-val">—</p> <p class="card-value" id="dash-cpu-val">—</p>
@@ -179,7 +177,6 @@
<span class="card-detail" id="dash-ram-detail"></span> <span class="card-detail" id="dash-ram-detail"></span>
</div> </div>
<!-- Row 4: full width -->
<div class="dash-card span6"> <div class="dash-card span6">
<div class="card-top"><span class="card-label">System Info</span></div> <div class="card-top"><span class="card-label">System Info</span></div>
<pre class="card-fetch" id="dash-fetch">—</pre> <pre class="card-fetch" id="dash-fetch">—</pre>
@@ -213,7 +210,7 @@
</header> </header>
<div class="svc-list"> <div class="svc-list">
<!-- One banner per service — copy this block to add another --> <!-- copy this to add another service -->
<article class="svc-banner" style="--brand: #FFD600"> <article class="svc-banner" style="--brand: #FFD600">
<img class="svc-logo" src="textures/revolt_logo.png" alt=""> <img class="svc-logo" src="textures/revolt_logo.png" alt="">
<div class="svc-info"> <div class="svc-info">
@@ -225,7 +222,7 @@
</div> </div>
</section> </section>
<!-- Lightbox (gallery) --> <!-- gallery lightbox -->
<div id="lightbox" class="lightbox"> <div id="lightbox" class="lightbox">
<span class="lightbox-close">&times;</span> <span class="lightbox-close">&times;</span>
<img class="lightbox-content" id="lightbox-img" alt=""> <img class="lightbox-content" id="lightbox-img" alt="">
@@ -235,7 +232,7 @@
<script src="js/gallery.js" defer></script> <script src="js/gallery.js" defer></script>
<script src="js/tabs.js" defer></script> <script src="js/tabs.js" defer></script>
<!-- Firebase SDK (must load before the Firestore-backed scripts below) --> <!-- firebase has to load before the scripts below -->
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js"></script> <script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore-compat.js"></script> <script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore-compat.js"></script>
<script src="js/comments.js"></script> <script src="js/comments.js"></script>
+5 -18
View File
@@ -1,4 +1,4 @@
// Bucketlist tab — password-gated, real-time Firestore list // password locked bucketlist
(() => { (() => {
const FIREBASE_CONFIG = { const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
@@ -9,11 +9,6 @@
appId: "1:353969651290:web:41e3aaf8e54c743702a263" appId: "1:353969651290:web:41e3aaf8e54c743702a263"
}; };
// ═══════════════════════════════════════════════════════════
// PASSWORD GATE — items never touch DOM until authenticated
// ═══════════════════════════════════════════════════════════
// --- SHA-256 via Web Crypto ---------------------------------------------------
async function sha256(text) { async function sha256(text) {
const buf = new TextEncoder().encode(text); const buf = new TextEncoder().encode(text);
const hash = await crypto.subtle.digest("SHA-256", buf); const hash = await crypto.subtle.digest("SHA-256", buf);
@@ -22,7 +17,6 @@
.join(""); .join("");
} }
// --- Lock-screen HTML (only thing in DOM before auth) -------------------------
function showLock(container) { function showLock(container) {
container.innerHTML = ` container.innerHTML = `
<div class="bl-lock"> <div class="bl-lock">
@@ -37,7 +31,6 @@
`; `;
} }
// --- Build the real panel + attach Firestore after auth -----------------------
function unlock(container, db, hash) { function unlock(container, db, hash) {
container.innerHTML = ` container.innerHTML = `
<div class="bl-panel"> <div class="bl-panel">
@@ -50,7 +43,7 @@
const listEl = document.getElementById("bl-list"); const listEl = document.getElementById("bl-list");
if (!listEl) return; if (!listEl) return;
// items live under bucketlist_data/<sha256(user:pass)>/items — the path IS the key // the hash is the doc id, no hash no items
db.collection("bucketlist_data").doc(hash).collection("items").onSnapshot(snap => { db.collection("bucketlist_data").doc(hash).collection("items").onSnapshot(snap => {
const items = []; const items = [];
snap.forEach(doc => { snap.forEach(doc => {
@@ -92,10 +85,6 @@
.replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;"); .replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");
} }
// ═══════════════════════════════════════════════════════════
// ENTRY
// ═══════════════════════════════════════════════════════════
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const tab = document.getElementById("bucketlist"); const tab = document.getElementById("bucketlist");
if (!tab) return; if (!tab) return;
@@ -103,16 +92,15 @@
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG); if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
const db = firebase.firestore(); const db = firebase.firestore();
const container = tab; // the <section> itself — nothing else exists inside it const container = tab;
// --- Already authenticated this session? --------------------------------- // already logged in this session
const savedHash = sessionStorage.getItem("bl_hash"); const savedHash = sessionStorage.getItem("bl_hash");
if (savedHash) { if (savedHash) {
unlock(container, db, savedHash); unlock(container, db, savedHash);
return; return;
} }
// --- Show lock screen ----------------------------------------------------
showLock(container); showLock(container);
const userInput = document.getElementById("bl-user"); const userInput = document.getElementById("bl-user");
@@ -131,7 +119,7 @@
submitBtn.disabled = true; submitBtn.disabled = true;
errEl.textContent = ""; errEl.textContent = "";
// hash = sha256("username:password") — both must match the Firestore doc ID // sha256 of "user:pass" has to match a doc in bucketlist_auth
const hash = await sha256(user + ":" + pass); const hash = await sha256(user + ":" + pass);
try { try {
const doc = await db.collection("bucketlist_auth").doc(hash).get(); const doc = await db.collection("bucketlist_auth").doc(hash).get();
@@ -149,7 +137,6 @@
submitBtn.disabled = false; submitBtn.disabled = false;
} }
// Enter in either field submits the form
document.getElementById("bl-form").onsubmit = e => { document.getElementById("bl-form").onsubmit = e => {
e.preventDefault(); e.preventDefault();
tryUnlock(); tryUnlock();
+4 -8
View File
@@ -1,4 +1,4 @@
// Calendar tab — timezone-aware (auto-converts to your local time) // calendar tab, shows everything in the visitor's timezone
(() => { (() => {
const FIREBASE_CONFIG = { const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
@@ -40,11 +40,11 @@
document.getElementById("cal-prev").onclick = () => shiftMonth(-1); document.getElementById("cal-prev").onclick = () => shiftMonth(-1);
document.getElementById("cal-next").onclick = () => shiftMonth(1); document.getElementById("cal-next").onclick = () => shiftMonth(1);
// Auto-refresh every 60s so "Upcoming" stays current as time passes // refresh every minute so old events drop off upcoming
setInterval(render, 60000); setInterval(render, 60000);
function shiftMonth(delta) { function shiftMonth(delta) {
// Day 1 avoids overflow (e.g. Jan 31 + 1 month → Mar 3) // day 1 or jan 31 + 1 month turns into march
const d = new Date(viewYear, viewMonth + delta, 1); const d = new Date(viewYear, viewMonth + delta, 1);
viewYear = d.getFullYear(); viewYear = d.getFullYear();
viewMonth = d.getMonth(); viewMonth = d.getMonth();
@@ -113,10 +113,7 @@
} }
}); });
// Normalise a Firestore doc into local date/time. Timed events use the // events with a time get converted to local time, all day events keep their date
// `utc` timestamp so they land on the viewer's local day; date-only events
// keep their `date` (converting their midnight could shift them a day).
// Falls back to the raw fields when `utc` is missing or unparseable.
function toEvent(d) { function toEvent(d) {
const utc = d.utc ? new Date(d.utc) : null; const utc = d.utc ? new Date(d.utc) : null;
const hasUtc = utc && !isNaN(utc.getTime()); const hasUtc = utc && !isNaN(utc.getTime());
@@ -130,7 +127,6 @@
description: d.description || "", description: d.description || "",
date, date,
time, time,
// 0 for malformed events keeps the sort stable and hides them from "Upcoming"
when: (hasUtc ? utc.getTime() : new Date(date + "T" + (d.time || "00:00")).getTime()) || 0 when: (hasUtc ? utc.getTime() : new Date(date + "T" + (d.time || "00:00")).getTime()) || 0
}; };
} }
+8 -16
View File
@@ -1,4 +1,4 @@
// About tab — Steam-style comment wall + global click counter // comment wall + click counter on the about tab
(() => { (() => {
const FIREBASE_CONFIG = { const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
@@ -37,7 +37,7 @@
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG); if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
const db = firebase.firestore(); const db = firebase.firestore();
// ── Avatar colour picker ───────────────────────────────────── // avatar colour picker
const colorOpts = document.querySelectorAll('.av-opt'); const colorOpts = document.querySelectorAll('.av-opt');
colorOpts.forEach(btn => { colorOpts.forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
@@ -47,7 +47,7 @@
}); });
}); });
// ── Custom PFP upload (center-cropped + shrunk to 64×64 JPEG) ─ // pfp upload, cropped to a 64x64 square
pfpUpload.addEventListener('change', () => { pfpUpload.addEventListener('change', () => {
const file = pfpUpload.files[0]; const file = pfpUpload.files[0];
if (!file) return; if (!file) return;
@@ -84,7 +84,6 @@
pfpClear.style.display = 'none'; pfpClear.style.display = 'none';
}); });
// ── Char counter ─────────────────────────────────────────────
function updateCharCount() { function updateCharCount() {
const left = MAX_CHARS - msgInput.value.length; const left = MAX_CHARS - msgInput.value.length;
charCountEl.textContent = left; charCountEl.textContent = left;
@@ -93,7 +92,7 @@
msgInput.addEventListener('input', updateCharCount); msgInput.addEventListener('input', updateCharCount);
updateCharCount(); updateCharCount();
// ── Real-time feed + pagination ────────────────────────────── // comments feed + pages
let allComments = []; let allComments = [];
let currentPage = 1; let currentPage = 1;
@@ -138,7 +137,7 @@
listEl.innerHTML = '<p class="no-comments">Could not load comments.</p>'; listEl.innerHTML = '<p class="no-comments">Could not load comments.</p>';
}); });
// ── Global click counter ───────────────────────────────────── // click counter
const clickBtn = document.getElementById('yuuka-click-btn'); const clickBtn = document.getElementById('yuuka-click-btn');
const clickDisplay = document.getElementById('click-counter-display'); const clickDisplay = document.getElementById('click-counter-display');
const clickSound = new Audio('sounds/yuuka_click.mp3'); const clickSound = new Audio('sounds/yuuka_click.mp3');
@@ -146,8 +145,6 @@
if (clickBtn && clickDisplay) { if (clickBtn && clickDisplay) {
const statsRef = db.collection('stats').doc('global_clicks'); const statsRef = db.collection('stats').doc('global_clicks');
// Firestore applies local writes to this listener instantly,
// so clicks show up immediately without a manual optimistic update.
statsRef.onSnapshot(doc => { statsRef.onSnapshot(doc => {
const count = doc.exists ? doc.data().count : 0; const count = doc.exists ? doc.data().count : 0;
clickDisplay.textContent = (count || 0).toLocaleString(); clickDisplay.textContent = (count || 0).toLocaleString();
@@ -157,7 +154,7 @@
}); });
clickBtn.addEventListener('click', () => { clickBtn.addEventListener('click', () => {
clickSound.currentTime = 0; // restart so rapid clicks all play clickSound.currentTime = 0; // so spam clicking plays every time
clickSound.play().catch(() => {}); clickSound.play().catch(() => {});
statsRef.set({ statsRef.set({
@@ -166,7 +163,6 @@
}); });
} }
// ── Submit ───────────────────────────────────────────────────
form.addEventListener('submit', async e => { form.addEventListener('submit', async e => {
e.preventDefault(); e.preventDefault();
@@ -199,12 +195,11 @@
}); });
}); });
// ── Build a single comment bubble (Steam layout) ─────────────────
function buildBubble(data) { function buildBubble(data) {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'comment-bubble'; div.className = 'comment-bubble';
// Pending server timestamps are null until the write lands // timestamp is null for a sec right after posting
const ts = data.timestamp ? data.timestamp.toDate() : new Date(); const ts = data.timestamp ? data.timestamp.toDate() : new Date();
const timeStr = ts.toLocaleDateString('en-US', { const timeStr = ts.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric', year: 'numeric', month: 'short', day: 'numeric',
@@ -235,10 +230,7 @@
return div; return div;
} }
// ── Helpers ────────────────────────────────────────────────────── // old comments were saved already escaped, this stops them showing "&lt;"
// Older comments were stored pre-escaped (< and > only), which made
// them render as literal "&lt;" once escaped again. Undo that here.
function legacyUnescape(str) { function legacyUnescape(str) {
if (!str) return ''; if (!str) return '';
return String(str).replace(/&lt;/g, '<').replace(/&gt;/g, '>'); return String(str).replace(/&lt;/g, '<').replace(/&gt;/g, '>');
+2 -6
View File
@@ -1,5 +1,4 @@
// Gallery tab — thumbnails from gallery/manifest.json + full-size lightbox. // manifest.json comes from generate_thumbnails.py
// The manifest is (re)generated by generate_thumbnails.py.
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('gallery-container'); const container = document.getElementById('gallery-container');
const lightbox = document.getElementById('lightbox'); const lightbox = document.getElementById('lightbox');
@@ -8,8 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (!container || !lightbox) return; if (!container || !lightbox) return;
// Animated GIFs are served as MP4 (a few % of the size), so those // gifs are converted to mp4 so those open in the video player
// open in the <video>; everything else in the <img>.
const openLightbox = (src) => { const openLightbox = (src) => {
const isVideo = src.endsWith('.mp4'); const isVideo = src.endsWith('.mp4');
lightboxImg.style.display = isVideo ? 'none' : 'block'; lightboxImg.style.display = isVideo ? 'none' : 'block';
@@ -26,13 +24,11 @@ document.addEventListener('DOMContentLoaded', () => {
const closeLightbox = () => { const closeLightbox = () => {
lightbox.style.display = 'none'; lightbox.style.display = 'none';
lightboxImg.removeAttribute('src'); lightboxImg.removeAttribute('src');
// Stop playback and any download still in progress
lightboxVideo.pause(); lightboxVideo.pause();
lightboxVideo.removeAttribute('src'); lightboxVideo.removeAttribute('src');
lightboxVideo.load(); lightboxVideo.load();
}; };
// Close on the X, on a click outside the media, or with Escape
lightbox.addEventListener('click', (e) => { lightbox.addEventListener('click', (e) => {
if (e.target !== lightboxImg && e.target !== lightboxVideo) closeLightbox(); if (e.target !== lightboxImg && e.target !== lightboxVideo) closeLightbox();
}); });
+35 -14
View File
@@ -6,11 +6,10 @@ document.addEventListener('DOMContentLoaded', () => {
const bucketlistVideo = document.getElementById('bucketlist-background-video'); const bucketlistVideo = document.getElementById('bucketlist-background-video');
const botVideo = document.getElementById('yuukabot-background-video'); const botVideo = document.getElementById('yuukabot-background-video');
// ── Map URL path → tab id ──────────────────────────────
function pathToTab(path) { function pathToTab(path) {
const slug = path.replace(/^\/+|\/+$/g, '') || 'about'; const slug = path.replace(/^\/+|\/+$/g, '') || 'about';
const valid = ['about', 'gallery', 'bucketlist', 'yuukabot', 'calendar', 'services']; const valid = ['about', 'gallery', 'bucketlist', 'yuukabot', 'calendar', 'services'];
// backward compat: old /ahmet links // old links
if (slug === 'ahmet') return 'bucketlist'; if (slug === 'ahmet') return 'bucketlist';
return valid.includes(slug) ? slug : 'about'; return valid.includes(slug) ? slug : 'about';
} }
@@ -19,10 +18,9 @@ document.addEventListener('DOMContentLoaded', () => {
return '/' + (tabId === 'about' ? '' : tabId); return '/' + (tabId === 'about' ? '' : tabId);
} }
// ── Swap background videos + top-bar theme ────────────
const topLine = document.getElementById('top-line'); const topLine = document.getElementById('top-line');
// map tab id → its dedicated video element (unlisted = use main) // tabs not listed here use the main video
const tabVideos = { const tabVideos = {
calendar: calVideo, calendar: calVideo,
bucketlist: bucketlistVideo, bucketlist: bucketlistVideo,
@@ -31,12 +29,10 @@ document.addEventListener('DOMContentLoaded', () => {
}; };
function setBgForTab(tabId) { function setBgForTab(tabId) {
// Hide + pause ALL videos first
[mainVideo, calVideo, bucketlistVideo, botVideo].forEach(v => { [mainVideo, calVideo, bucketlistVideo, botVideo].forEach(v => {
if (v) { v.style.display = 'none'; v.pause(); } if (v) { v.style.display = 'none'; v.pause(); }
}); });
// Show the right one
const target = tabVideos[tabId]; const target = tabVideos[tabId];
if (target) { if (target) {
target.style.display = 'block'; target.style.display = 'block';
@@ -46,49 +42,74 @@ document.addEventListener('DOMContentLoaded', () => {
if (mainVideo.paused) mainVideo.play().catch(() => {}); if (mainVideo.paused) mainVideo.play().catch(() => {});
} }
// Top-bar theme toggle
if (topLine) { if (topLine) {
topLine.classList.toggle('cal-theme', tabId === 'calendar'); topLine.classList.toggle('cal-theme', tabId === 'calendar');
topLine.classList.toggle('bucketlist-theme', tabId === 'bucketlist'); topLine.classList.toggle('bucketlist-theme', tabId === 'bucketlist');
} }
} }
// ── Show a specific tab ──────────────────────────────── const navCurrent = document.getElementById('nav-current');
function showTab(tabId) { function showTab(tabId) {
tabs.forEach(t => t.style.display = 'none'); tabs.forEach(t => t.style.display = 'none');
const target = document.getElementById(tabId); const target = document.getElementById(tabId);
if (target) target.style.display = 'block'; if (target) target.style.display = 'block';
setBgForTab(tabId); setBgForTab(tabId);
// tab name next to the hamburger on mobile
const btn = document.getElementById(tabId + '-btn');
if (navCurrent && btn) navCurrent.textContent = btn.textContent;
} }
// changing the url breaks every button when index.html is opened as a file
function setUrl(method, tabId) { function setUrl(method, tabId) {
if (window.location.protocol === 'file:') return; if (window.location.protocol === 'file:') return;
history[method]({ tab: tabId }, '', tabToPath(tabId) + window.location.search); history[method]({ tab: tabId }, '', tabToPath(tabId) + window.location.search);
} }
// ── Navigate to a tab (updates URL + shows it) ────────
function navigateTo(tabId) { function navigateTo(tabId) {
showTab(tabId); showTab(tabId);
window.scrollTo(0, 0); window.scrollTo(0, 0);
setUrl('pushState', tabId); setUrl('pushState', tabId);
} }
// ── Initial load: show tab from URL ────────────────────
// (also normalises unknown / legacy paths like /ahmet in the address bar)
const initialTab = pathToTab(window.location.pathname); const initialTab = pathToTab(window.location.pathname);
showTab(initialTab); showTab(initialTab);
setUrl('replaceState', initialTab); setUrl('replaceState', initialTab);
// ── Tab button clicks ────────────────────────────────── const buttons = document.querySelectorAll('#nav-tabs button');
const buttons = document.querySelectorAll('#top-line button');
buttons.forEach(button => { buttons.forEach(button => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const targetId = button.id.replace('-btn', ''); const targetId = button.id.replace('-btn', '');
navigateTo(targetId); navigateTo(targetId);
closeMenu();
}); });
}); });
// ── Back / forward browser buttons ───────────────────── // hamburger menu (mobile)
const hamburger = document.getElementById('nav-hamburger');
function openMenu() {
topLine.classList.add('nav-open');
hamburger.setAttribute('aria-expanded', 'true');
}
function closeMenu() {
topLine.classList.remove('nav-open');
hamburger.setAttribute('aria-expanded', 'false');
}
hamburger.addEventListener('click', () => {
topLine.classList.contains('nav-open') ? closeMenu() : openMenu();
});
document.addEventListener('click', (e) => {
if (!topLine.contains(e.target)) closeMenu();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
// back/forward buttons
window.addEventListener('popstate', (e) => { window.addEventListener('popstate', (e) => {
showTab(e.state && e.state.tab ? e.state.tab : pathToTab(window.location.pathname)); showTab(e.state && e.state.tab ? e.state.tab : pathToTab(window.location.pathname));
}); });
+5 -7
View File
@@ -1,4 +1,4 @@
// Yuuka Bot Dashboard — live Firestore reader // bot status dashboard
(() => { (() => {
const FIREBASE_CONFIG = { const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
@@ -9,8 +9,7 @@
appId: "1:353969651290:web:41e3aaf8e54c743702a263" appId: "1:353969651290:web:41e3aaf8e54c743702a263"
}; };
// The bot pushes every 10s; no update for this long means it's down // bot updates every 10s, nothing for a minute means it's probably dead
// (generous so a visitor's slightly-off clock doesn't flip the dot).
const STALE_MS = 60 * 1000; const STALE_MS = 60 * 1000;
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
@@ -22,15 +21,15 @@
const dot = document.getElementById("dash-status-dot"); const dot = document.getElementById("dash-status-dot");
let online = false; let online = false;
let updatedAt = null; // ms; null for bots that don't send updatedAt yet let updatedAt = null;
let loaded = false; // keep the grey "pending" dot until first data let loaded = false; // dot stays grey until the first data
function refreshDot() { function refreshDot() {
if (!loaded) return; if (!loaded) return;
const fresh = updatedAt == null || Date.now() - updatedAt < STALE_MS; const fresh = updatedAt == null || Date.now() - updatedAt < STALE_MS;
if (dot) dot.className = online && fresh ? "status-dot" : "status-dot offline"; if (dot) dot.className = online && fresh ? "status-dot" : "status-dot offline";
} }
// Re-check periodically: a crashed bot sends no more snapshots // a crashed bot stops sending updates so keep checking
setInterval(refreshDot, 5000); setInterval(refreshDot, 5000);
db.collection("sysinfo").doc("server").onSnapshot(doc => { db.collection("sysinfo").doc("server").onSnapshot(doc => {
@@ -44,7 +43,6 @@
setText("dash-uptime", d.uptime); setText("dash-uptime", d.uptime);
setText("dash-bot-uptime", d.botUptime ? "bot up " + d.botUptime : ""); setText("dash-bot-uptime", d.botUptime ? "bot up " + d.botUptime : "");
// CPU, RAM, Disk, Swap — value + bar
setBar("dash-cpu-val", "dash-cpu-bar", d.cpu); setBar("dash-cpu-val", "dash-cpu-bar", d.cpu);
setBar("dash-ram-val", "dash-ram-bar", d.ram); setBar("dash-ram-val", "dash-ram-bar", d.ram);
setBar("dash-disk-val", "dash-disk-bar", d.disk); setBar("dash-disk-val", "dash-disk-bar", d.disk);
+18 -9
View File
@@ -1,6 +1,4 @@
/* Bucketlist tab — password gated, baked-in left panel, internal scroll only */ /* lock screen */
/* ── Lock screen (pitch black, centered form) ─────────────── */
.bl-lock { .bl-lock {
width: 100%; width: 100%;
height: 100vh; height: 100vh;
@@ -61,14 +59,14 @@
min-height: 1.2em; min-height: 1.2em;
} }
/* ── Lock section to viewport so body never scrolls ────────── */ /* lock section to viewport so body never scrolls */
#bucketlist.tab-content { #bucketlist.tab-content {
padding: 0; padding: 0;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
/* ── Panel (framed, no extra space) ────────────────────────── */ /* panel */
.bl-panel { .bl-panel {
width: 420px; width: 420px;
height: calc(100vh - 210px); height: calc(100vh - 210px);
@@ -83,7 +81,7 @@
overflow: hidden; overflow: hidden;
} }
/* ── Scrollable list (fills the frame exactly) ────────────── */ /* scrollable list */
.bl-list { .bl-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -93,7 +91,7 @@
padding: 14px 16px; padding: 14px 16px;
} }
/* ── Item card ────────────────────────────────────────────── */ /* item card */
.bl-item { .bl-item {
padding: 14px 18px; padding: 14px 18px;
background: rgba(35, 35, 41, 0.75); background: rgba(35, 35, 41, 0.75);
@@ -107,7 +105,7 @@
box-shadow: 0 3px 16px rgba(184, 68, 87, 0.08); box-shadow: 0 3px 16px rgba(184, 68, 87, 0.08);
} }
/* Completed items */ /* done items */
.bl-item.bl-done { opacity: 0.5; } .bl-item.bl-done { opacity: 0.5; }
.bl-item.bl-done .bl-title { text-decoration: line-through; } .bl-item.bl-done .bl-title { text-decoration: line-through; }
@@ -128,7 +126,7 @@
line-height: 1.5; line-height: 1.5;
} }
/* ── Empty state ─────────────────────────────────────────── */ /* empty state */
.bl-empty { .bl-empty {
text-align: center; text-align: center;
color: rgba(244, 242, 245, 0.3); color: rgba(244, 242, 245, 0.3);
@@ -136,3 +134,14 @@
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
font-size: 0.82rem; font-size: 0.82rem;
} }
/* mobile */
@media (max-width: 900px) {
.bl-panel {
width: auto;
height: calc(100vh - 84px);
margin: 68px 12px 16px;
}
.bl-lock-box input,
.bl-lock-box button { width: min(260px, 80vw); }
}
+22 -5
View File
@@ -1,6 +1,6 @@
/* Calendar tab */ /* Calendar tab */
/* ── Two-column layout ─────────────────────────────────────── */ /* two-column layout */
.cal-wrap { .cal-wrap {
max-width: 95vw; margin: 0 auto; max-width: 95vw; margin: 0 auto;
padding: 110px 24px 40px; padding: 110px 24px 40px;
@@ -10,7 +10,7 @@
.cal-main { flex: 1; min-width: 0; } .cal-main { flex: 1; min-width: 0; }
.cal-sidebar { width: 300px; flex-shrink: 0; background: rgba(35,35,41,0.7); border: 1px solid rgba(251,196,204,0.25); border-radius: 12px; padding: 18px; } .cal-sidebar { width: 300px; flex-shrink: 0; background: rgba(35,35,41,0.7); border: 1px solid rgba(251,196,204,0.25); border-radius: 12px; padding: 18px; }
/* ── Month nav ─────────────────────────────────────────────── */ /* month nav */
.cal-nav { .cal-nav {
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
gap: 20px; margin-bottom: 18px; gap: 20px; margin-bottom: 18px;
@@ -26,7 +26,7 @@
} }
.cal-nav button:hover { background: rgba(251,196,204,0.4); } .cal-nav button:hover { background: rgba(251,196,204,0.4); }
/* ── Month grid ────────────────────────────────────────────── */ /* month grid */
.cal-grid { .cal-grid {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
@@ -59,7 +59,7 @@
margin: 2px 2px 0 0; vertical-align: middle; margin: 2px 2px 0 0; vertical-align: middle;
} }
/* ── Hover popup ───────────────────────────────────────────── */ /* hover popup */
.cal-popup { .cal-popup {
display: none; position: absolute; display: none; position: absolute;
top: 100%; left: 50%; transform: translateX(-50%); top: 100%; left: 50%; transform: translateX(-50%);
@@ -86,7 +86,7 @@
font-size: 0.7rem; color: rgba(244,242,245,0.55); margin-top: 2px; font-size: 0.7rem; color: rgba(244,242,245,0.55); margin-top: 2px;
} }
/* ── Sidebar ───────────────────────────────────────────────── */ /* sidebar */
.cal-sidebar h3 { .cal-sidebar h3 {
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px;
color: rgba(244,242,245,0.65); margin: 0 0 14px; color: rgba(244,242,245,0.65); margin: 0 0 14px;
@@ -112,3 +112,20 @@
} }
.cal-tz-note { font-size: 0.65rem; color: rgba(244,242,245,0.35); text-align: center; margin-top: 16px; } .cal-tz-note { font-size: 0.65rem; color: rgba(244,242,245,0.35); text-align: center; margin-top: 16px; }
/* mobile */
@media (max-width: 900px) {
.cal-wrap {
flex-direction: column;
padding: 72px 0 20px;
}
.cal-sidebar { width: auto; }
.cal-nav h2 { min-width: 0; font-size: 1.2rem; }
.cal-grid { gap: 2px; }
.cal-day { min-height: 52px; padding: 3px 4px; }
.cal-day .day-dot { width: 7px; height: 7px; }
}
/* popups on the first/last column open inwards so they don't go off screen */
.cal-grid > .cal-day:nth-child(7n+1) .cal-popup { left: 0; transform: none; }
.cal-grid > .cal-day:nth-child(7n) .cal-popup { left: auto; right: 0; transform: none; }
+36 -15
View File
@@ -1,8 +1,4 @@
/* ═══════════════════════════════════════════════════════════ /* about hero */
STEAM PROFILE COMMENT STYLE
═══════════════════════════════════════════════════════════ */
/* ── About hero (top half) ──────────────────────────────── */
#about-hero { #about-hero {
padding: 120px 20px 50px; padding: 120px 20px 50px;
display: flex; display: flex;
@@ -59,7 +55,7 @@
letter-spacing: 1px; letter-spacing: 1px;
} }
/* ── Steam panel wrapper ─────────────────────────────────── */ /* steam panel wrapper */
#comments-section { #comments-section {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -71,7 +67,7 @@
text-align: left; text-align: left;
} }
/* ── Panel header bar ────────────────────────────────────── */ /* panel header bar */
.steam-header { .steam-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -97,7 +93,7 @@
margin: 0; margin: 0;
} }
/* ── Comment rows ────────────────────────────────────────── */ /* comment rows */
#comments-list { #comments-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -157,7 +153,6 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Username */
.comment-name { .comment-name {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: bold; font-weight: bold;
@@ -174,7 +169,6 @@
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
/* Comment text */
.comment-text { .comment-text {
color: #000000 !important; color: #000000 !important;
font-size: 0.85rem !important; font-size: 0.85rem !important;
@@ -197,7 +191,7 @@
font-style: italic; font-style: italic;
} }
/* ── Post form (above the feed) ──────────────────────────── */ /* post form */
.steam-form-area { .steam-form-area {
background: transparent; background: transparent;
margin: 20px 0; margin: 20px 0;
@@ -214,7 +208,7 @@
letter-spacing: 1px; letter-spacing: 1px;
} }
/* ── Avatar colour picker ────────────────────────────────── */ /* avatar colour picker */
#avatar-picker { #avatar-picker {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -256,7 +250,7 @@
transform: scale(1.25); transform: scale(1.25);
} }
/* ── Form inputs ─────────────────────────────────────────── */ /* form inputs */
#comment-form input[type="text"], #comment-form input[type="text"],
#comment-form textarea { #comment-form textarea {
width: 100%; width: 100%;
@@ -324,7 +318,7 @@
} }
#comment-submit:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; } #comment-submit:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
/* ── Custom PFP Upload ───────────────────────────────────── */ /* custom pfp upload */
.custom-pfp-upload { .custom-pfp-upload {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -380,7 +374,7 @@
color: #ff7070; color: #ff7070;
} }
/* ── Pagination ──────────────────────────────────────────── */ /* pagination */
.steam-pagination { .steam-pagination {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -412,3 +406,30 @@
color: #000; color: #000;
font-weight: bold; font-weight: bold;
} }
/* smaller screens: clicker goes next to the text instead of covering it */
@media (max-width: 1300px) {
#click-counter-container { position: static; }
#about-hero { gap: 48px; }
}
/* mobile */
@media (max-width: 900px) {
#about-hero {
flex-direction: column;
gap: 24px;
padding: 80px 8px 20px;
}
#click-counter-container {
position: static;
}
#yuuka-click-btn { width: 150px; }
.hero-text { text-align: center; }
#comments-section {
margin-top: 20px;
padding: 0 4px 30px;
}
/* bigger tap targets for the colour swatches */
.av-opt { width: 26px; height: 26px; }
}
+12 -1
View File
@@ -2,7 +2,7 @@
display:grid; display:grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px; gap: 10px;
padding: 210px 70px 70px; /* extra top padding clears the fixed nav bar */ padding: 210px 70px 70px; /* room for the nav bar */
} }
.gallery-grid img { .gallery-grid img {
@@ -70,3 +70,14 @@
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
/* mobile */
@media (max-width: 900px) {
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 6px;
padding: 72px 4px 20px;
}
.lightbox-content { max-width: 96%; }
.lightbox-close { top: 8px; right: 16px; }
}
+24 -9
View File
@@ -1,10 +1,6 @@
/* ═══════════════════════════════════════════════════════════ /* to add a service copy a .svc-banner in index.html, --brand is the stripe color */
SERVICES TAB — one banner per service, Bot Status look
Add a service by copying a .svc-banner block in index.html and
setting its --brand color (used for the left stripe only).
═══════════════════════════════════════════════════════════ */
/* ── Tab header (matches .dash-header) ────────────────────── */ /* tab header */
.svc-header { .svc-header {
padding: 120px 20px 30px; padding: 120px 20px 30px;
text-align: center; text-align: center;
@@ -16,7 +12,7 @@
text-shadow: 0 0 30px rgba(91, 111, 222, 0.5); text-shadow: 0 0 30px rgba(91, 111, 222, 0.5);
} }
/* ── Banner list ──────────────────────────────────────────── */ /* banner list */
.svc-list { .svc-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -24,7 +20,7 @@
padding: 0 48px 60px; padding: 0 48px 60px;
} }
/* ── Banner (same glass as .dash-card) ────────────────────── */ /* banner */
.svc-banner { .svc-banner {
--brand: #5b6fde; --brand: #5b6fde;
display: flex; display: flex;
@@ -61,7 +57,7 @@
color: #fff; color: #fff;
} }
/* overrides the global p rule in styles.css */ /* global p is huge */
.svc-banner .svc-desc { .svc-banner .svc-desc {
max-width: none; max-width: none;
margin: 0; margin: 0;
@@ -90,3 +86,22 @@
outline: 2px solid #fff; outline: 2px solid #fff;
outline-offset: 3px; outline-offset: 3px;
} }
/* mobile */
@media (max-width: 900px) {
.svc-header { padding: 72px 12px 20px; }
.svc-header h1 { font-size: 2rem; }
.svc-list { padding: 0 4px 30px; gap: 12px; }
.svc-banner {
flex-wrap: wrap;
gap: 16px;
padding: 20px;
}
.svc-logo { width: 52px; height: 52px; }
.svc-name { font-size: 1.3rem; }
.svc-btn {
flex-basis: 100%;
box-sizing: border-box;
text-align: center;
}
}
+126 -7
View File
@@ -33,7 +33,7 @@ html, body {
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 115px; /* 95px + 2×10px padding — other tabs' top offsets assume this */ height: 115px; /* other tabs are spaced for this height */
box-sizing: border-box; box-sizing: border-box;
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
@@ -45,7 +45,12 @@ html, body {
padding: 10px; padding: 10px;
} }
#top-line button { #nav-tabs {
display: flex;
align-items: center;
}
#nav-tabs button {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
@@ -64,12 +69,12 @@ html, body {
transition: box-shadow 0.3s ease, background-color 0.3s ease, border-color 0.4s ease, color 0.4s ease; transition: box-shadow 0.3s ease, background-color 0.3s ease, border-color 0.4s ease, color 0.4s ease;
} }
#top-line button:hover { #nav-tabs button:hover {
text-decoration: underline; text-decoration: underline;
} }
/* ── Calendar-themed top bar buttons only ─────────────── */ /* calendar theme */
#top-line.cal-theme button { #top-line.cal-theme #nav-tabs button {
border-bottom-color: #FBC4CC; border-bottom-color: #FBC4CC;
border-left-color: #FBC4CC; border-left-color: #FBC4CC;
border-right-color: #FBC4CC; border-right-color: #FBC4CC;
@@ -77,8 +82,8 @@ html, body {
transition: border-color 0.4s ease, color 0.4s ease; transition: border-color 0.4s ease, color 0.4s ease;
} }
/* ── Bucketlist-themed top bar buttons ─────────────────── */ /* bucketlist theme */
#top-line.bucketlist-theme button { #top-line.bucketlist-theme #nav-tabs button {
border-bottom-color: #B5B1C6; border-bottom-color: #B5B1C6;
border-left-color: #B5B1C6; border-left-color: #B5B1C6;
border-right-color: #B5B1C6; border-right-color: #B5B1C6;
@@ -102,3 +107,117 @@ p {
img { img {
display:block; display:block;
} }
/* mobile only */
#nav-current,
#nav-hamburger {
display: none;
}
/* mobile, same setup as userevolt.app */
@media (max-width: 1300px) {
#nav-tabs button {
padding: 22px 18px;
margin: 30px 8px;
}
}
@media (max-width: 900px) {
#top-line {
height: 56px;
padding: 0 8px 0 16px;
justify-content: space-between;
background: rgba(13, 17, 23, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
#nav-current {
display: block;
color: #fff;
font-weight: bold;
font-size: 1.1rem;
}
#nav-hamburger {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 8px;
width: 40px;
height: 40px;
flex-shrink: 0;
}
#nav-hamburger span {
display: block;
width: 22px;
height: 2px;
background: #fff;
border-radius: 1px;
transition: transform 0.25s, opacity 0.25s;
}
/* turns into an X */
#top-line.nav-open #nav-hamburger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
#top-line.nav-open #nav-hamburger span:nth-child(2) { opacity: 0; }
#top-line.nav-open #nav-hamburger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
#nav-tabs {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
align-items: stretch;
background: rgba(13, 17, 23, 0.95);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: 8px 0;
}
#top-line.nav-open #nav-tabs {
display: flex;
}
#nav-tabs button,
#top-line.cal-theme #nav-tabs button,
#top-line.bucketlist-theme #nav-tabs button {
width: 100%;
margin: 0;
padding: 12px 24px;
min-height: 44px;
border: none;
border-left: 3px solid transparent;
border-radius: 0;
color: rgba(255, 255, 255, 0.85);
font-size: 1rem;
text-align: left;
}
#nav-tabs button:hover {
text-decoration: none;
background: rgba(255, 255, 255, 0.06);
border-left-color: #5b6fde;
}
#top-line.cal-theme #nav-tabs button:hover { border-left-color: #FBC4CC; }
#top-line.bucketlist-theme #nav-tabs button:hover { border-left-color: #B84457; }
/* keep the video centered on tall screens */
#background-video,
#calendar-background-video,
#bucketlist-background-video,
#yuukabot-background-video {
top: 0;
width: 100%;
height: 100%;
}
.tab-content { padding: 12px; }
h1 { font-size: 2.2rem; }
p { font-size: 1.2rem; }
}
+36 -21
View File
@@ -1,18 +1,12 @@
/* ═══════════════════════════════════════════════════════════ /* grid settings */
YUUKA BOT DASHBOARD — live system stats
═══════════════════════════════════════════════════════════ */
/* ── LAYOUT: edit these to change the grid ────────────────── */
:root { :root {
--dash-columns: 6; /* number of columns (2 = 2×2, 3 = 3-wide, etc.) */ --dash-columns: 6;
--dash-gap: 20px; /* space between boxes */ --dash-gap: 20px;
--dash-min-card-height: 200px;/* minimum height of each box */ --dash-min-card-height: 200px;
--dash-max-width: none; /* max width of the whole grid */ --dash-max-width: none;
} }
/* Card widths come from the .spanN helpers at the bottom of this file. */ /* tab header */
/* ── Tab header ───────────────────────────────────────────── */
.dash-header { .dash-header {
padding: 120px 20px 30px; padding: 120px 20px 30px;
text-align: center; text-align: center;
@@ -39,7 +33,7 @@
.dash-header .offline { background: #ef4444; box-shadow: 0 0 12px #ef4444; animation: none; } .dash-header .offline { background: #ef4444; box-shadow: 0 0 12px #ef4444; animation: none; }
.dash-header .pending { background: #64748b; box-shadow: none; animation: none; } .dash-header .pending { background: #64748b; box-shadow: none; animation: none; }
/* ── Card grid ────────────────────────────────────────────── */ /* card grid */
.dash-grid { .dash-grid {
display: grid; display: grid;
grid-template-columns: repeat(var(--dash-columns), 1fr); grid-template-columns: repeat(var(--dash-columns), 1fr);
@@ -49,7 +43,7 @@
padding: 0 48px 60px; padding: 0 48px 60px;
} }
/* ── Glass card ───────────────────────────────────────────── */ /* glass card */
.dash-card { .dash-card {
background: rgba(15, 15, 35, 0.55); background: rgba(15, 15, 35, 0.55);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
@@ -68,7 +62,7 @@
box-shadow: 0 6px 28px rgba(91, 111, 222, 0.12); box-shadow: 0 6px 28px rgba(91, 111, 222, 0.12);
} }
/* ── Card header ──────────────────────────────────────────── */ /* card header */
.card-top { .card-top {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -82,7 +76,7 @@
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
/* ── Card value ───────────────────────────────────────────── */ /* card value */
.card-value { .card-value {
font-size: 3rem; font-size: 3rem;
font-weight: bold; font-weight: bold;
@@ -96,7 +90,7 @@
font-weight: 400; font-weight: 400;
} }
/* ── Progress bar (CPU / RAM / Disk) ──────────────────────── */ /* progress bar */
.card-bar-wrap { .card-bar-wrap {
width: 100%; width: 100%;
height: 8px; height: 8px;
@@ -115,7 +109,7 @@
.card-bar-fill.disk { background: linear-gradient(90deg, #34d399, #6ee7b7); } .card-bar-fill.disk { background: linear-gradient(90deg, #34d399, #6ee7b7); }
.card-bar-fill.swap { background: linear-gradient(90deg, #f59e0b, #fbbf24); } .card-bar-fill.swap { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
/* ── Detail text (used / total) ───────────────────────────── */ /* detail text */
.card-detail { .card-detail {
font-size: 0.78rem; font-size: 0.78rem;
color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.3);
@@ -123,7 +117,7 @@
margin-top: 6px; margin-top: 6px;
} }
/* ── Fastfetch text block ──────────────────────────────────── */ /* fastfetch text block */
.card-fetch { .card-fetch {
margin: 12px 0 0; margin: 12px 0 0;
font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace; font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace;
@@ -134,7 +128,28 @@
overflow: hidden; overflow: hidden;
} }
/* ── Span helpers (6-col grid) ──────────────────────────────── */ /* card widths */
.dash-card.span2 { grid-column: span 2; } /* 3 per row */ .dash-card.span2 { grid-column: span 2; } /* 3 per row */
.dash-card.span3 { grid-column: span 3; } /* 2 per row — stretched */ .dash-card.span3 { grid-column: span 3; } /* 2 per row */
.dash-card.span6 { grid-column: span 6; } /* full width */ .dash-card.span6 { grid-column: span 6; } /* full width */
/* mobile */
/* 2 columns on tablets, 1 on phones */
@media (max-width: 900px) {
:root {
--dash-columns: 2;
--dash-gap: 12px;
--dash-min-card-height: 130px;
}
.dash-header { padding: 72px 12px 20px; }
.dash-header h1 { font-size: 2rem; }
.dash-grid { padding: 0 4px 30px; }
.dash-card { padding: 20px; }
.dash-card.span2,
.dash-card.span3 { grid-column: span 1; }
.dash-card.span6 { grid-column: 1 / -1; }
.card-value { font-size: 2.2rem; }
}
@media (max-width: 575px) {
:root { --dash-columns: 1; }
}