website is now phone
This commit is contained in:
1 parent
f4457c5c17
commit
827f524107
16 files changed
+358
-174
No files matched your search
@@ -53,6 +53,7 @@ There are **no automated tests** in this project.
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
- **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`).
|
||||
- **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.
|
||||
|
||||
+7
-18
@@ -1,14 +1,5 @@
|
||||
"""Build gallery thumbnails + manifest.
|
||||
|
||||
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).
|
||||
"""
|
||||
# run this after adding images to gallery/
|
||||
# makes thumbnails, turns gifs into way smaller mp4s (needs ffmpeg) and updates manifest.json
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -23,7 +14,7 @@ FFMPEG = shutil.which("ffmpeg")
|
||||
|
||||
|
||||
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]
|
||||
return (0, int(stem), "") if stem.isdigit() else (1, 0, stem.lower())
|
||||
|
||||
@@ -48,10 +39,10 @@ def convert_gifs(base_folder):
|
||||
continue
|
||||
with Image.open(src) as img:
|
||||
if getattr(img, "n_frames", 1) < 2:
|
||||
continue # still image, keep as GIF
|
||||
continue # not animated
|
||||
subprocess.run([
|
||||
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",
|
||||
"-an", "-movflags", "+faststart", dst,
|
||||
], check=True)
|
||||
@@ -69,10 +60,9 @@ def make_thumbnail(src, thumb_path):
|
||||
], check=True)
|
||||
return
|
||||
with Image.open(src) as img:
|
||||
# JPEG needs RGB (e.g. for RGBA PNGs or palette GIFs)
|
||||
if img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
img.thumbnail(THUMB_SIZE) # keeps aspect ratio
|
||||
img.thumbnail(THUMB_SIZE)
|
||||
img.save(thumb_path, "JPEG", quality=85)
|
||||
|
||||
|
||||
@@ -82,7 +72,6 @@ def create_thumbnails(base_folder, files):
|
||||
|
||||
count = 0
|
||||
for filename in files:
|
||||
# Always save thumbnail as jpg
|
||||
stem = os.path.splitext(filename)[0]
|
||||
thumb_path = os.path.join(thumb_dir, f"{stem}.jpg")
|
||||
if os.path.exists(thumb_path):
|
||||
@@ -98,7 +87,7 @@ def create_thumbnails(base_folder, 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 = {}
|
||||
for f in files:
|
||||
stem, ext = os.path.splitext(f)
|
||||
|
||||
+17
-20
@@ -3,6 +3,7 @@
|
||||
|
||||
<head>
|
||||
<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/gallery.css">
|
||||
<link rel="stylesheet" href="style/comments.css">
|
||||
@@ -28,17 +29,22 @@
|
||||
<source src="textures/bot_background.mp4" type="video/mp4">
|
||||
</video>
|
||||
<div id="top-line">
|
||||
<button id="about-btn">About This Website</button>
|
||||
<button id="gallery-btn">Gallery</button>
|
||||
<button id="bucketlist-btn">Bucketlist</button>
|
||||
<button id="yuukabot-btn">Bot Status</button>
|
||||
<button id="calendar-btn">Calendar</button>
|
||||
<button id="services-btn">Services</button>
|
||||
<span id="nav-current"></span>
|
||||
<nav id="nav-tabs">
|
||||
<button id="about-btn">About This Website</button>
|
||||
<button id="gallery-btn">Gallery</button>
|
||||
<button id="bucketlist-btn">Bucketlist</button>
|
||||
<button id="yuukabot-btn">Bot Status</button>
|
||||
<button id="calendar-btn">Calendar</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>
|
||||
|
||||
<section id="about" class="tab-content">
|
||||
|
||||
<!-- Top half: welcome -->
|
||||
<div id="about-hero">
|
||||
<div id="click-counter-container">
|
||||
<img src="textures/yuuka_click.webp" id="yuuka-click-btn" alt="Click Me!">
|
||||
@@ -52,20 +58,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom half: Steam-style comments -->
|
||||
<!-- comments -->
|
||||
<div id="comments-section">
|
||||
|
||||
<!-- Steam panel header -->
|
||||
<div class="steam-header">
|
||||
<span class="steam-header-title">COMMENTS</span>
|
||||
<span id="comments-title"></span>
|
||||
</div>
|
||||
|
||||
<!-- Post form — at the top -->
|
||||
<div class="steam-form-area">
|
||||
<p class="steam-form-label">Leave a comment:</p>
|
||||
|
||||
<!-- Avatar picker -->
|
||||
<div id="avatar-picker">
|
||||
<span class="picker-label">Avatar:</span>
|
||||
|
||||
@@ -101,10 +104,8 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Comments feed -->
|
||||
<div id="comments-list"></div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="comments-pagination" class="steam-pagination"></div>
|
||||
|
||||
</div>
|
||||
@@ -122,7 +123,6 @@
|
||||
</header>
|
||||
|
||||
<div class="dash-grid">
|
||||
<!-- Row 1: 3 boxes -->
|
||||
<div class="dash-card span2">
|
||||
<div class="card-top"><span class="card-label">Uptime</span></div>
|
||||
<p class="card-value" id="dash-uptime">—</p>
|
||||
@@ -142,7 +142,6 @@
|
||||
<span class="card-detail">1m / 5m / 15m</span>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: 3 boxes -->
|
||||
<div class="dash-card span2">
|
||||
<div class="card-top"><span class="card-label">Network</span></div>
|
||||
<p class="card-value" id="dash-net">—</p>
|
||||
@@ -162,7 +161,6 @@
|
||||
<span class="card-detail" id="dash-swap-detail"></span>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: 2 boxes stretched -->
|
||||
<div class="dash-card span3">
|
||||
<div class="card-top"><span class="card-label">CPU</span></div>
|
||||
<p class="card-value" id="dash-cpu-val">—</p>
|
||||
@@ -179,7 +177,6 @@
|
||||
<span class="card-detail" id="dash-ram-detail"></span>
|
||||
</div>
|
||||
|
||||
<!-- Row 4: full width -->
|
||||
<div class="dash-card span6">
|
||||
<div class="card-top"><span class="card-label">System Info</span></div>
|
||||
<pre class="card-fetch" id="dash-fetch">—</pre>
|
||||
@@ -213,7 +210,7 @@
|
||||
</header>
|
||||
|
||||
<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">
|
||||
<img class="svc-logo" src="textures/revolt_logo.png" alt="">
|
||||
<div class="svc-info">
|
||||
@@ -225,7 +222,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Lightbox (gallery) -->
|
||||
<!-- gallery lightbox -->
|
||||
<div id="lightbox" class="lightbox">
|
||||
<span class="lightbox-close">×</span>
|
||||
<img class="lightbox-content" id="lightbox-img" alt="">
|
||||
@@ -235,7 +232,7 @@
|
||||
<script src="js/gallery.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-firestore-compat.js"></script>
|
||||
<script src="js/comments.js"></script>
|
||||
|
||||
+5
-18
@@ -1,4 +1,4 @@
|
||||
// Bucketlist tab — password-gated, real-time Firestore list
|
||||
// password locked bucketlist
|
||||
(() => {
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -9,11 +9,6 @@
|
||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PASSWORD GATE — items never touch DOM until authenticated
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// --- SHA-256 via Web Crypto ---------------------------------------------------
|
||||
async function sha256(text) {
|
||||
const buf = new TextEncoder().encode(text);
|
||||
const hash = await crypto.subtle.digest("SHA-256", buf);
|
||||
@@ -22,7 +17,6 @@
|
||||
.join("");
|
||||
}
|
||||
|
||||
// --- Lock-screen HTML (only thing in DOM before auth) -------------------------
|
||||
function showLock(container) {
|
||||
container.innerHTML = `
|
||||
<div class="bl-lock">
|
||||
@@ -37,7 +31,6 @@
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Build the real panel + attach Firestore after auth -----------------------
|
||||
function unlock(container, db, hash) {
|
||||
container.innerHTML = `
|
||||
<div class="bl-panel">
|
||||
@@ -50,7 +43,7 @@
|
||||
const listEl = document.getElementById("bl-list");
|
||||
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 => {
|
||||
const items = [];
|
||||
snap.forEach(doc => {
|
||||
@@ -92,10 +85,6 @@
|
||||
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ENTRY
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const tab = document.getElementById("bucketlist");
|
||||
if (!tab) return;
|
||||
@@ -103,16 +92,15 @@
|
||||
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||
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");
|
||||
if (savedHash) {
|
||||
unlock(container, db, savedHash);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Show lock screen ----------------------------------------------------
|
||||
showLock(container);
|
||||
|
||||
const userInput = document.getElementById("bl-user");
|
||||
@@ -131,7 +119,7 @@
|
||||
submitBtn.disabled = true;
|
||||
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);
|
||||
try {
|
||||
const doc = await db.collection("bucketlist_auth").doc(hash).get();
|
||||
@@ -149,7 +137,6 @@
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// Enter in either field submits the form
|
||||
document.getElementById("bl-form").onsubmit = e => {
|
||||
e.preventDefault();
|
||||
tryUnlock();
|
||||
|
||||
+4
-8
@@ -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 = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -40,11 +40,11 @@
|
||||
document.getElementById("cal-prev").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);
|
||||
|
||||
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);
|
||||
viewYear = d.getFullYear();
|
||||
viewMonth = d.getMonth();
|
||||
@@ -113,10 +113,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Normalise a Firestore doc into local date/time. Timed events use the
|
||||
// `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.
|
||||
// events with a time get converted to local time, all day events keep their date
|
||||
function toEvent(d) {
|
||||
const utc = d.utc ? new Date(d.utc) : null;
|
||||
const hasUtc = utc && !isNaN(utc.getTime());
|
||||
@@ -130,7 +127,6 @@
|
||||
description: d.description || "",
|
||||
date,
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
+8
-16
@@ -1,4 +1,4 @@
|
||||
// About tab — Steam-style comment wall + global click counter
|
||||
// comment wall + click counter on the about tab
|
||||
(() => {
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -37,7 +37,7 @@
|
||||
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||
const db = firebase.firestore();
|
||||
|
||||
// ── Avatar colour picker ─────────────────────────────────────
|
||||
// avatar colour picker
|
||||
const colorOpts = document.querySelectorAll('.av-opt');
|
||||
colorOpts.forEach(btn => {
|
||||
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', () => {
|
||||
const file = pfpUpload.files[0];
|
||||
if (!file) return;
|
||||
@@ -84,7 +84,6 @@
|
||||
pfpClear.style.display = 'none';
|
||||
});
|
||||
|
||||
// ── Char counter ─────────────────────────────────────────────
|
||||
function updateCharCount() {
|
||||
const left = MAX_CHARS - msgInput.value.length;
|
||||
charCountEl.textContent = left;
|
||||
@@ -93,7 +92,7 @@
|
||||
msgInput.addEventListener('input', updateCharCount);
|
||||
updateCharCount();
|
||||
|
||||
// ── Real-time feed + pagination ──────────────────────────────
|
||||
// comments feed + pages
|
||||
let allComments = [];
|
||||
let currentPage = 1;
|
||||
|
||||
@@ -138,7 +137,7 @@
|
||||
listEl.innerHTML = '<p class="no-comments">Could not load comments.</p>';
|
||||
});
|
||||
|
||||
// ── Global click counter ─────────────────────────────────────
|
||||
// click counter
|
||||
const clickBtn = document.getElementById('yuuka-click-btn');
|
||||
const clickDisplay = document.getElementById('click-counter-display');
|
||||
const clickSound = new Audio('sounds/yuuka_click.mp3');
|
||||
@@ -146,8 +145,6 @@
|
||||
if (clickBtn && clickDisplay) {
|
||||
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 => {
|
||||
const count = doc.exists ? doc.data().count : 0;
|
||||
clickDisplay.textContent = (count || 0).toLocaleString();
|
||||
@@ -157,7 +154,7 @@
|
||||
});
|
||||
|
||||
clickBtn.addEventListener('click', () => {
|
||||
clickSound.currentTime = 0; // restart so rapid clicks all play
|
||||
clickSound.currentTime = 0; // so spam clicking plays every time
|
||||
clickSound.play().catch(() => {});
|
||||
|
||||
statsRef.set({
|
||||
@@ -166,7 +163,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Submit ───────────────────────────────────────────────────
|
||||
form.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -199,12 +195,11 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ── Build a single comment bubble (Steam layout) ─────────────────
|
||||
function buildBubble(data) {
|
||||
const div = document.createElement('div');
|
||||
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 timeStr = ts.toLocaleDateString('en-US', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
@@ -235,10 +230,7 @@
|
||||
return div;
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// Older comments were stored pre-escaped (< and > only), which made
|
||||
// them render as literal "<" once escaped again. Undo that here.
|
||||
// old comments were saved already escaped, this stops them showing "<"
|
||||
function legacyUnescape(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
+2
-6
@@ -1,5 +1,4 @@
|
||||
// Gallery tab — thumbnails from gallery/manifest.json + full-size lightbox.
|
||||
// The manifest is (re)generated by generate_thumbnails.py.
|
||||
// manifest.json comes from generate_thumbnails.py
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('gallery-container');
|
||||
const lightbox = document.getElementById('lightbox');
|
||||
@@ -8,8 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (!container || !lightbox) return;
|
||||
|
||||
// Animated GIFs are served as MP4 (a few % of the size), so those
|
||||
// open in the <video>; everything else in the <img>.
|
||||
// gifs are converted to mp4 so those open in the video player
|
||||
const openLightbox = (src) => {
|
||||
const isVideo = src.endsWith('.mp4');
|
||||
lightboxImg.style.display = isVideo ? 'none' : 'block';
|
||||
@@ -26,13 +24,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const closeLightbox = () => {
|
||||
lightbox.style.display = 'none';
|
||||
lightboxImg.removeAttribute('src');
|
||||
// Stop playback and any download still in progress
|
||||
lightboxVideo.pause();
|
||||
lightboxVideo.removeAttribute('src');
|
||||
lightboxVideo.load();
|
||||
};
|
||||
|
||||
// Close on the X, on a click outside the media, or with Escape
|
||||
lightbox.addEventListener('click', (e) => {
|
||||
if (e.target !== lightboxImg && e.target !== lightboxVideo) closeLightbox();
|
||||
});
|
||||
|
||||
+35
-14
@@ -6,11 +6,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const bucketlistVideo = document.getElementById('bucketlist-background-video');
|
||||
const botVideo = document.getElementById('yuukabot-background-video');
|
||||
|
||||
// ── Map URL path → tab id ──────────────────────────────
|
||||
function pathToTab(path) {
|
||||
const slug = path.replace(/^\/+|\/+$/g, '') || 'about';
|
||||
const valid = ['about', 'gallery', 'bucketlist', 'yuukabot', 'calendar', 'services'];
|
||||
// backward compat: old /ahmet links
|
||||
// old links
|
||||
if (slug === 'ahmet') return 'bucketlist';
|
||||
return valid.includes(slug) ? slug : 'about';
|
||||
}
|
||||
@@ -19,10 +18,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return '/' + (tabId === 'about' ? '' : tabId);
|
||||
}
|
||||
|
||||
// ── Swap background videos + top-bar theme ────────────
|
||||
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 = {
|
||||
calendar: calVideo,
|
||||
bucketlist: bucketlistVideo,
|
||||
@@ -31,12 +29,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
function setBgForTab(tabId) {
|
||||
// Hide + pause ALL videos first
|
||||
[mainVideo, calVideo, bucketlistVideo, botVideo].forEach(v => {
|
||||
if (v) { v.style.display = 'none'; v.pause(); }
|
||||
});
|
||||
|
||||
// Show the right one
|
||||
const target = tabVideos[tabId];
|
||||
if (target) {
|
||||
target.style.display = 'block';
|
||||
@@ -46,49 +42,74 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (mainVideo.paused) mainVideo.play().catch(() => {});
|
||||
}
|
||||
|
||||
// Top-bar theme toggle
|
||||
if (topLine) {
|
||||
topLine.classList.toggle('cal-theme', tabId === 'calendar');
|
||||
topLine.classList.toggle('bucketlist-theme', tabId === 'bucketlist');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Show a specific tab ────────────────────────────────
|
||||
const navCurrent = document.getElementById('nav-current');
|
||||
|
||||
function showTab(tabId) {
|
||||
tabs.forEach(t => t.style.display = 'none');
|
||||
const target = document.getElementById(tabId);
|
||||
if (target) target.style.display = 'block';
|
||||
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) {
|
||||
if (window.location.protocol === 'file:') return;
|
||||
history[method]({ tab: tabId }, '', tabToPath(tabId) + window.location.search);
|
||||
}
|
||||
|
||||
// ── Navigate to a tab (updates URL + shows it) ────────
|
||||
function navigateTo(tabId) {
|
||||
showTab(tabId);
|
||||
window.scrollTo(0, 0);
|
||||
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);
|
||||
showTab(initialTab);
|
||||
setUrl('replaceState', initialTab);
|
||||
|
||||
// ── Tab button clicks ──────────────────────────────────
|
||||
const buttons = document.querySelectorAll('#top-line button');
|
||||
const buttons = document.querySelectorAll('#nav-tabs button');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const targetId = button.id.replace('-btn', '');
|
||||
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) => {
|
||||
showTab(e.state && e.state.tab ? e.state.tab : pathToTab(window.location.pathname));
|
||||
});
|
||||
|
||||
+5
-7
@@ -1,4 +1,4 @@
|
||||
// Yuuka Bot Dashboard — live Firestore reader
|
||||
// bot status dashboard
|
||||
(() => {
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -9,8 +9,7 @@
|
||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
||||
};
|
||||
|
||||
// The bot pushes every 10s; no update for this long means it's down
|
||||
// (generous so a visitor's slightly-off clock doesn't flip the dot).
|
||||
// bot updates every 10s, nothing for a minute means it's probably dead
|
||||
const STALE_MS = 60 * 1000;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@@ -22,15 +21,15 @@
|
||||
|
||||
const dot = document.getElementById("dash-status-dot");
|
||||
let online = false;
|
||||
let updatedAt = null; // ms; null for bots that don't send updatedAt yet
|
||||
let loaded = false; // keep the grey "pending" dot until first data
|
||||
let updatedAt = null;
|
||||
let loaded = false; // dot stays grey until the first data
|
||||
|
||||
function refreshDot() {
|
||||
if (!loaded) return;
|
||||
const fresh = updatedAt == null || Date.now() - updatedAt < STALE_MS;
|
||||
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);
|
||||
|
||||
db.collection("sysinfo").doc("server").onSnapshot(doc => {
|
||||
@@ -44,7 +43,6 @@
|
||||
setText("dash-uptime", d.uptime);
|
||||
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-ram-val", "dash-ram-bar", d.ram);
|
||||
setBar("dash-disk-val", "dash-disk-bar", d.disk);
|
||||
|
||||
+18
-9
@@ -1,6 +1,4 @@
|
||||
/* Bucketlist tab — password gated, baked-in left panel, internal scroll only */
|
||||
|
||||
/* ── Lock screen (pitch black, centered form) ─────────────── */
|
||||
/* lock screen */
|
||||
.bl-lock {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
@@ -61,14 +59,14 @@
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
/* ── Lock section to viewport so body never scrolls ────────── */
|
||||
/* lock section to viewport so body never scrolls */
|
||||
#bucketlist.tab-content {
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Panel (framed, no extra space) ────────────────────────── */
|
||||
/* panel */
|
||||
.bl-panel {
|
||||
width: 420px;
|
||||
height: calc(100vh - 210px);
|
||||
@@ -83,7 +81,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Scrollable list (fills the frame exactly) ────────────── */
|
||||
/* scrollable list */
|
||||
.bl-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -93,7 +91,7 @@
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
/* ── Item card ────────────────────────────────────────────── */
|
||||
/* item card */
|
||||
.bl-item {
|
||||
padding: 14px 18px;
|
||||
background: rgba(35, 35, 41, 0.75);
|
||||
@@ -107,7 +105,7 @@
|
||||
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 .bl-title { text-decoration: line-through; }
|
||||
|
||||
@@ -128,7 +126,7 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Empty state ─────────────────────────────────────────── */
|
||||
/* empty state */
|
||||
.bl-empty {
|
||||
text-align: center;
|
||||
color: rgba(244, 242, 245, 0.3);
|
||||
@@ -136,3 +134,14 @@
|
||||
font-family: Arial, sans-serif;
|
||||
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
@@ -1,6 +1,6 @@
|
||||
/* Calendar tab */
|
||||
|
||||
/* ── Two-column layout ─────────────────────────────────────── */
|
||||
/* two-column layout */
|
||||
.cal-wrap {
|
||||
max-width: 95vw; margin: 0 auto;
|
||||
padding: 110px 24px 40px;
|
||||
@@ -10,7 +10,7 @@
|
||||
.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; }
|
||||
|
||||
/* ── Month nav ─────────────────────────────────────────────── */
|
||||
/* month nav */
|
||||
.cal-nav {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 20px; margin-bottom: 18px;
|
||||
@@ -26,7 +26,7 @@
|
||||
}
|
||||
.cal-nav button:hover { background: rgba(251,196,204,0.4); }
|
||||
|
||||
/* ── Month grid ────────────────────────────────────────────── */
|
||||
/* month grid */
|
||||
.cal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
@@ -59,7 +59,7 @@
|
||||
margin: 2px 2px 0 0; vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ── Hover popup ───────────────────────────────────────────── */
|
||||
/* hover popup */
|
||||
.cal-popup {
|
||||
display: none; position: absolute;
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── Sidebar ───────────────────────────────────────────────── */
|
||||
/* sidebar */
|
||||
.cal-sidebar h3 {
|
||||
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px;
|
||||
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; }
|
||||
|
||||
/* 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
@@ -1,8 +1,4 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
STEAM PROFILE COMMENT STYLE
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── About hero (top half) ──────────────────────────────── */
|
||||
/* about hero */
|
||||
#about-hero {
|
||||
padding: 120px 20px 50px;
|
||||
display: flex;
|
||||
@@ -59,7 +55,7 @@
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* ── Steam panel wrapper ─────────────────────────────────── */
|
||||
/* steam panel wrapper */
|
||||
#comments-section {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
@@ -71,7 +67,7 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ── Panel header bar ────────────────────────────────────── */
|
||||
/* panel header bar */
|
||||
.steam-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -97,7 +93,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Comment rows ────────────────────────────────────────── */
|
||||
/* comment rows */
|
||||
#comments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -157,7 +153,6 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Username */
|
||||
.comment-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
@@ -174,7 +169,6 @@
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* Comment text */
|
||||
.comment-text {
|
||||
color: #000000 !important;
|
||||
font-size: 0.85rem !important;
|
||||
@@ -197,7 +191,7 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── Post form (above the feed) ──────────────────────────── */
|
||||
/* post form */
|
||||
.steam-form-area {
|
||||
background: transparent;
|
||||
margin: 20px 0;
|
||||
@@ -214,7 +208,7 @@
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* ── Avatar colour picker ────────────────────────────────── */
|
||||
/* avatar colour picker */
|
||||
#avatar-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -256,7 +250,7 @@
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
/* ── Form inputs ─────────────────────────────────────────── */
|
||||
/* form inputs */
|
||||
#comment-form input[type="text"],
|
||||
#comment-form textarea {
|
||||
width: 100%;
|
||||
@@ -324,7 +318,7 @@
|
||||
}
|
||||
#comment-submit:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
|
||||
|
||||
/* ── Custom PFP Upload ───────────────────────────────────── */
|
||||
/* custom pfp upload */
|
||||
.custom-pfp-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -380,7 +374,7 @@
|
||||
color: #ff7070;
|
||||
}
|
||||
|
||||
/* ── Pagination ──────────────────────────────────────────── */
|
||||
/* pagination */
|
||||
.steam-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -412,3 +406,30 @@
|
||||
color: #000;
|
||||
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
@@ -2,7 +2,7 @@
|
||||
display:grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
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 {
|
||||
@@ -70,3 +70,14 @@
|
||||
text-decoration: none;
|
||||
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
@@ -1,10 +1,6 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
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).
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
/* to add a service copy a .svc-banner in index.html, --brand is the stripe color */
|
||||
|
||||
/* ── Tab header (matches .dash-header) ────────────────────── */
|
||||
/* tab header */
|
||||
.svc-header {
|
||||
padding: 120px 20px 30px;
|
||||
text-align: center;
|
||||
@@ -16,7 +12,7 @@
|
||||
text-shadow: 0 0 30px rgba(91, 111, 222, 0.5);
|
||||
}
|
||||
|
||||
/* ── Banner list ──────────────────────────────────────────── */
|
||||
/* banner list */
|
||||
.svc-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -24,7 +20,7 @@
|
||||
padding: 0 48px 60px;
|
||||
}
|
||||
|
||||
/* ── Banner (same glass as .dash-card) ────────────────────── */
|
||||
/* banner */
|
||||
.svc-banner {
|
||||
--brand: #5b6fde;
|
||||
display: flex;
|
||||
@@ -61,7 +57,7 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* overrides the global p rule in styles.css */
|
||||
/* global p is huge */
|
||||
.svc-banner .svc-desc {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
@@ -90,3 +86,22 @@
|
||||
outline: 2px solid #fff;
|
||||
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
@@ -33,7 +33,7 @@ html, body {
|
||||
top: 0;
|
||||
left: 0;
|
||||
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;
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
@@ -45,7 +45,12 @@ html, body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#top-line button {
|
||||
#nav-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#nav-tabs button {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
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;
|
||||
}
|
||||
|
||||
#top-line button:hover {
|
||||
#nav-tabs button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Calendar-themed top bar buttons only ─────────────── */
|
||||
#top-line.cal-theme button {
|
||||
/* calendar theme */
|
||||
#top-line.cal-theme #nav-tabs button {
|
||||
border-bottom-color: #FBC4CC;
|
||||
border-left-color: #FBC4CC;
|
||||
border-right-color: #FBC4CC;
|
||||
@@ -77,8 +82,8 @@ html, body {
|
||||
transition: border-color 0.4s ease, color 0.4s ease;
|
||||
}
|
||||
|
||||
/* ── Bucketlist-themed top bar buttons ─────────────────── */
|
||||
#top-line.bucketlist-theme button {
|
||||
/* bucketlist theme */
|
||||
#top-line.bucketlist-theme #nav-tabs button {
|
||||
border-bottom-color: #B5B1C6;
|
||||
border-left-color: #B5B1C6;
|
||||
border-right-color: #B5B1C6;
|
||||
@@ -102,3 +107,117 @@ p {
|
||||
img {
|
||||
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
@@ -1,18 +1,12 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
YUUKA BOT DASHBOARD — live system stats
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── LAYOUT: edit these to change the grid ────────────────── */
|
||||
/* grid settings */
|
||||
:root {
|
||||
--dash-columns: 6; /* number of columns (2 = 2×2, 3 = 3-wide, etc.) */
|
||||
--dash-gap: 20px; /* space between boxes */
|
||||
--dash-min-card-height: 200px;/* minimum height of each box */
|
||||
--dash-max-width: none; /* max width of the whole grid */
|
||||
--dash-columns: 6;
|
||||
--dash-gap: 20px;
|
||||
--dash-min-card-height: 200px;
|
||||
--dash-max-width: none;
|
||||
}
|
||||
|
||||
/* Card widths come from the .spanN helpers at the bottom of this file. */
|
||||
|
||||
/* ── Tab header ───────────────────────────────────────────── */
|
||||
/* tab header */
|
||||
.dash-header {
|
||||
padding: 120px 20px 30px;
|
||||
text-align: center;
|
||||
@@ -39,7 +33,7 @@
|
||||
.dash-header .offline { background: #ef4444; box-shadow: 0 0 12px #ef4444; animation: none; }
|
||||
.dash-header .pending { background: #64748b; box-shadow: none; animation: none; }
|
||||
|
||||
/* ── Card grid ────────────────────────────────────────────── */
|
||||
/* card grid */
|
||||
.dash-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--dash-columns), 1fr);
|
||||
@@ -49,7 +43,7 @@
|
||||
padding: 0 48px 60px;
|
||||
}
|
||||
|
||||
/* ── Glass card ───────────────────────────────────────────── */
|
||||
/* glass card */
|
||||
.dash-card {
|
||||
background: rgba(15, 15, 35, 0.55);
|
||||
backdrop-filter: blur(16px);
|
||||
@@ -68,7 +62,7 @@
|
||||
box-shadow: 0 6px 28px rgba(91, 111, 222, 0.12);
|
||||
}
|
||||
|
||||
/* ── Card header ──────────────────────────────────────────── */
|
||||
/* card header */
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -82,7 +76,7 @@
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* ── Card value ───────────────────────────────────────────── */
|
||||
/* card value */
|
||||
.card-value {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
@@ -96,7 +90,7 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Progress bar (CPU / RAM / Disk) ──────────────────────── */
|
||||
/* progress bar */
|
||||
.card-bar-wrap {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
@@ -115,7 +109,7 @@
|
||||
.card-bar-fill.disk { background: linear-gradient(90deg, #34d399, #6ee7b7); }
|
||||
.card-bar-fill.swap { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
|
||||
|
||||
/* ── Detail text (used / total) ───────────────────────────── */
|
||||
/* detail text */
|
||||
.card-detail {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
@@ -123,7 +117,7 @@
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ── Fastfetch text block ──────────────────────────────────── */
|
||||
/* fastfetch text block */
|
||||
.card-fetch {
|
||||
margin: 12px 0 0;
|
||||
font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace;
|
||||
@@ -134,7 +128,28 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Span helpers (6-col grid) ──────────────────────────────── */
|
||||
/* card widths */
|
||||
.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 */
|
||||
|
||||
/* 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; }
|
||||
}
|
||||
Reference in new issue
Block a user