diff --git a/.gitignore b/.gitignore index 1c0ed7e..1e5a2a1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ Thumbs.db *.log .DS_Store firebase.txt -manipulate_boxes.md \ No newline at end of file +manipulate_boxes.md +bot/ +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 733a5a6..f618c1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,26 +5,26 @@ ``` website/ ├── index.html # Single-page entry point — all 5 tabs live here -├── gallery-container.html # Static gallery image tags (50 images) ├── _redirects # Netlify SPA rewrite rule (/* → /index.html) ├── js/ │ ├── tabs.js # Client-side routing, tab switching, background video swap │ ├── comments.js # Steam-style comment wall (Firestore: comments + stats) │ ├── yuukabot.js # Live bot dashboard (Firestore: sysinfo/server) │ ├── calendar.js # Event calendar (Firestore: calendar_events) -│ └── gallery.js # Lightbox + dynamic thumbnail loader +│ ├── bucketlist.js # Password-gated list (Firestore: bucketlist_auth + bucketlist_data) +│ └── gallery.js # Lightbox + thumbnail grid built from gallery/manifest.json ├── style/ │ ├── styles.css # Global layout, nav bar, base typography │ ├── comments.css # Steam-profile comment UI │ ├── yuukabot.css # Dashboard glassmorphism cards + grid │ ├── calendar.css # Calendar grid, popups, sidebar -│ ├── gallery.css # Gallery grid + lightbox -│ └── ahmet.css # Bucketlist tab button -├── gallery/ # 50 full-quality images + thumbnails/ subdirectory +│ ├── bucketlist.css # Lock screen + list panel +│ └── gallery.css # Gallery grid + lightbox +├── gallery/ # Full-quality images, thumbnails/, and manifest.json ├── textures/ # Background MP4 videos + clickable character PNG ├── sounds/ # Click SFX (yuuka_click.mp3) -├── generate_thumbnails.py # Pillow script: resizes gallery/ images → thumbnails/ -└── firebase.txt # Firestore security rules (reference only) +├── generate_thumbnails.py # Pillow script: builds thumbnails/ + manifest.json +└── firebase.txt # Firestore security rules (reference only, gitignored) ``` - **No frameworks, no build step, no package manager.** Every file is served directly. @@ -44,7 +44,7 @@ npx serve . ``` - `_redirects` handles SPA routing on Netlify; for local dev, navigate to `/index.html` directly. -- `generate_thumbnails.py` regenerates gallery thumbnails: `python generate_thumbnails.py`. Requires `Pillow`. +- After adding images to `gallery/`, run `python generate_thumbnails.py` (requires `Pillow`). It creates missing thumbnails and rewrites `gallery/manifest.json`, which is the list the gallery renders — an image not in the manifest won't show up. There are **no automated tests** in this project. @@ -52,7 +52,7 @@ There are **no automated tests** in this project. - **HTML:** Semantic elements where practical. Tab contents use `
`. IDs are kebab-case. - **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. 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. - No linters or formatters are configured. @@ -76,6 +76,8 @@ Examples from the repo: `calendar added`, `fix upcoming filter`, `css polish bs` - **Firebase API keys** are public by design (they identify the project, not authenticate). Do not commit service-account credentials or `.env` files. - `firebase.txt` is in `.gitignore` — keep the live rules reference there, not hardcoded in source. - All Firestore writes are guarded by security rules (see `firebase.txt`): only `comments` and `stats/global_clicks` accept public writes; everything else is read-only. +- Use `allow get` (not `allow read`) for collections keyed by a secret, like `bucketlist_auth` — `read` also permits listing every document. +- Editing `firebase.txt` does nothing by itself: paste it into Firebase Console → Firestore → Rules and publish. ## Adding a New Feature diff --git a/generate_thumbnails.py b/generate_thumbnails.py index af6566a..eef5fdc 100644 --- a/generate_thumbnails.py +++ b/generate_thumbnails.py @@ -1,48 +1,76 @@ +"""Build gallery thumbnails + manifest. + +Run after adding images to gallery/: + python generate_thumbnails.py + +- Creates gallery/thumbnails/.jpg (max 400x400) for any image missing one. +- Rewrites gallery/manifest.json: the ordered list of full-size filenames + that js/gallery.js renders. +""" +import json import os + from PIL import Image -def create_thumbnails(base_folder): - if not os.path.exists(base_folder): - print(f"Folder {base_folder} does not exist.") - return +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif"} +THUMB_SIZE = (400, 400) + +def sort_key(filename): + # Numeric names sort numerically (1, 2, ..., 10), anything else after, alphabetically + stem = os.path.splitext(filename)[0] + return (0, int(stem), "") if stem.isdigit() else (1, 0, stem.lower()) + + +def list_images(base_folder): + return sorted( + (f for f in os.listdir(base_folder) + if os.path.isfile(os.path.join(base_folder, f)) + and os.path.splitext(f)[1].lower() in IMAGE_EXTS), + key=sort_key, + ) + + +def create_thumbnails(base_folder, images): thumb_dir = os.path.join(base_folder, "thumbnails") os.makedirs(thumb_dir, exist_ok=True) count = 0 - for filename in os.listdir(base_folder): - ext = filename.split('.')[-1].lower() - if ext not in ['jpg', 'jpeg', 'png', 'gif']: - continue - - filepath = os.path.join(base_folder, filename) - if not os.path.isfile(filepath): - continue - - base_name = filename.split('.')[0] + for filename in images: # Always save thumbnail as jpg - thumb_path = os.path.join(thumb_dir, f"{base_name}.jpg") - - # If thumbnail already exists, skip + stem = os.path.splitext(filename)[0] + thumb_path = os.path.join(thumb_dir, f"{stem}.jpg") if os.path.exists(thumb_path): continue - + try: - with Image.open(filepath) as img: - # Convert to RGB if necessary (e.g., for RGBA PNGs or GIFs) - if img.mode != 'RGB': - img = img.convert('RGB') - - # Resize keeping aspect ratio - # Set a max dimension (e.g., 400x400) - img.thumbnail((400, 400)) + with Image.open(os.path.join(base_folder, filename)) 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.save(thumb_path, "JPEG", quality=85) count += 1 print(f"Created thumbnail for {filename}") except Exception as e: print(f"Failed to process {filename}: {e}") - + print(f"Created {count} thumbnails in {base_folder}") + +def write_manifest(base_folder, images): + path = os.path.join(base_folder, "manifest.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(images, f, indent=2) + f.write("\n") + print(f"Wrote {path} ({len(images)} images)") + + if __name__ == "__main__": - create_thumbnails("gallery") + folder = "gallery" + if not os.path.isdir(folder): + print(f"Folder {folder} does not exist.") + else: + imgs = list_images(folder) + create_thumbnails(folder, imgs) + write_manifest(folder, imgs) diff --git a/index.html b/index.html index b112850..42dacee 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + + - @@ -24,7 +24,6 @@
- @@ -57,13 +56,8 @@
-
- ⚠️ Setup needed: Open js/comments.js and follow the setup instructions at the top - of the file. -
- -
+

Leave a comment:

@@ -112,16 +106,14 @@
-
+
-

Yuuka Bot

+

Yuuka Bot

@@ -129,7 +121,7 @@
Uptime

—

-
+
Disk
@@ -210,22 +202,22 @@
+ + + - + - - - \ No newline at end of file diff --git a/js/bucketlist.js b/js/bucketlist.js index cc24f5d..751db13 100644 --- a/js/bucketlist.js +++ b/js/bucketlist.js @@ -1,152 +1,158 @@ // Bucketlist tab — password-gated, real-time Firestore list -const BL_FB_CONFIG = { - apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", - authDomain: "esef-514bf.firebaseapp.com", - projectId: "esef-514bf", - storageBucket: "esef-514bf.firebasestorage.app", - messagingSenderId: "353969651290", - appId: "1:353969651290:web:41e3aaf8e54c743702a263" -}; +(() => { + const FIREBASE_CONFIG = { + apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", + authDomain: "esef-514bf.firebaseapp.com", + projectId: "esef-514bf", + storageBucket: "esef-514bf.firebasestorage.app", + messagingSenderId: "353969651290", + appId: "1:353969651290:web:41e3aaf8e54c743702a263" + }; -// ═══════════════════════════════════════════════════════════ -// PASSWORD GATE — items never touch DOM until authenticated -// ═══════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════ + // 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); - return Array.from(new Uint8Array(hash)) - .map(b => b.toString(16).padStart(2, "0")) - .join(""); -} + // --- SHA-256 via Web Crypto --------------------------------------------------- + async function sha256(text) { + const buf = new TextEncoder().encode(text); + const hash = await crypto.subtle.digest("SHA-256", buf); + return Array.from(new Uint8Array(hash)) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); + } -// --- Lock-screen HTML (only thing in DOM before auth) ------------------------- -function showLock(container) { - container.innerHTML = ` -
-
-

Bucketlist

- - - -

+ // --- Lock-screen HTML (only thing in DOM before auth) ------------------------- + function showLock(container) { + container.innerHTML = ` +
+
+

Bucketlist

+ + + +

+
-
- `; -} + `; + } -// --- Build the real panel + attach Firestore after auth ----------------------- -function unlock(container, db, hash) { - container.innerHTML = ` -
-
-

Loading...

+ // --- Build the real panel + attach Firestore after auth ----------------------- + function unlock(container, db, hash) { + container.innerHTML = ` +
+
+

Loading...

+
-
- `; + `; - const listEl = document.getElementById("bl-list"); - if (!listEl) return; + const listEl = document.getElementById("bl-list"); + if (!listEl) return; - // items live under bucketlist_data//items — the path IS the key - db.collection("bucketlist_data").doc(hash).collection("items").onSnapshot(snap => { - const items = []; - snap.forEach(doc => { - const d = doc.data(); - items.push({ - id: doc.id, - title: d.title || "", - description: d.description || "", - date: d.date || "", - utc: d.utc || "", - done: d.done || false + // items live under bucketlist_data//items — the path IS the key + db.collection("bucketlist_data").doc(hash).collection("items").onSnapshot(snap => { + const items = []; + snap.forEach(doc => { + const d = doc.data(); + items.push({ + id: doc.id, + title: d.title || "", + description: d.description || "", + date: d.date || "", + utc: d.utc || "", + done: d.done || false + }); }); + items.sort((a, b) => (a.utc || "").localeCompare(b.utc || "")); + render(listEl, items); + }, () => { + if (listEl) listEl.innerHTML = '

Could not load.

'; }); - items.sort((a, b) => (a.utc || "").localeCompare(b.utc || "")); - render(listEl, items); - }, () => { - if (listEl) listEl.innerHTML = '

Could not load.

'; - }); -} - -function render(listEl, items) { - if (items.length === 0) { - listEl.innerHTML = '

Nothing here yet.

'; - return; } - // NOTE: item.done adds .bl-done class — hook for completed-item styling (CSS still TODO) - listEl.innerHTML = items.map(item => - '
' + - '
' + esc(item.title) + '
' + - (item.date ? '
' + esc(item.date) + '
' : '') + - (item.description ? '
' + esc(item.description) + '
' : '') + - '
' - ).join(""); -} - -function esc(s) { - if (!s) return ""; - return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); -} - -// ═══════════════════════════════════════════════════════════ -// ENTRY -// ═══════════════════════════════════════════════════════════ - -document.addEventListener("DOMContentLoaded", () => { - const tab = document.getElementById("bucketlist"); - if (!tab) return; - - if (!firebase.apps.length) firebase.initializeApp(BL_FB_CONFIG); - const db = firebase.firestore(); - - const container = tab; // the
itself — nothing else exists inside it - - // --- Already authenticated 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"); - const passInput = document.getElementById("bl-pass"); - const submitBtn = document.getElementById("bl-submit"); - const errEl = document.getElementById("bl-err"); - - async function tryUnlock() { - const user = userInput.value.trim(); - const pass = passInput.value; - if (!user || !pass) return; - - submitBtn.disabled = true; - errEl.textContent = ""; - - // hash = sha256("username:password") — both must match the Firestore doc ID - const hash = await sha256(user + ":" + pass); - try { - const doc = await db.collection("bucketlist_auth").doc(hash).get(); - if (doc.exists) { - sessionStorage.setItem("bl_hash", hash); - unlock(container, db, hash); - } else { - errEl.textContent = "Wrong credentials."; - passInput.value = ""; - passInput.focus(); - } - } catch (e) { - errEl.textContent = "Could not verify. Try again."; + function render(listEl, items) { + if (items.length === 0) { + listEl.innerHTML = '

Nothing here yet.

'; + return; } - submitBtn.disabled = false; + + listEl.innerHTML = items.map(item => + '
' + + '
' + esc(item.title) + '
' + + (item.date ? '
' + esc(item.date) + '
' : '') + + (item.description ? '
' + esc(item.description) + '
' : '') + + '
' + ).join(""); } - submitBtn.onclick = tryUnlock; - userInput.onkeydown = e => { if (e.key === "Enter") passInput.focus(); }; - passInput.onkeydown = e => { if (e.key === "Enter") tryUnlock(); }; -}); + function esc(s) { + if (!s) return ""; + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); + } + + // ═══════════════════════════════════════════════════════════ + // ENTRY + // ═══════════════════════════════════════════════════════════ + + document.addEventListener("DOMContentLoaded", () => { + const tab = document.getElementById("bucketlist"); + if (!tab) return; + + if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG); + const db = firebase.firestore(); + + const container = tab; // the
itself — nothing else exists inside it + + // --- Already authenticated 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"); + const passInput = document.getElementById("bl-pass"); + const submitBtn = document.getElementById("bl-submit"); + const errEl = document.getElementById("bl-err"); + + async function tryUnlock() { + const user = userInput.value.trim(); + const pass = passInput.value; + if (!user || !pass) { + (user ? passInput : userInput).focus(); + return; + } + + submitBtn.disabled = true; + errEl.textContent = ""; + + // hash = sha256("username:password") — both must match the Firestore doc ID + const hash = await sha256(user + ":" + pass); + try { + const doc = await db.collection("bucketlist_auth").doc(hash).get(); + if (doc.exists) { + sessionStorage.setItem("bl_hash", hash); + unlock(container, db, hash); + } else { + errEl.textContent = "Wrong credentials."; + passInput.value = ""; + passInput.focus(); + } + } catch (e) { + errEl.textContent = "Could not verify. Try again."; + } + submitBtn.disabled = false; + } + + // Enter in either field submits the form + document.getElementById("bl-form").onsubmit = e => { + e.preventDefault(); + tryUnlock(); + }; + }); +})(); diff --git a/js/calendar.js b/js/calendar.js index 5788528..946e21d 100644 --- a/js/calendar.js +++ b/js/calendar.js @@ -1,140 +1,144 @@ // Calendar tab — timezone-aware (auto-converts to your local time) -const CAL_FB_CONFIG = { - apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", - authDomain: "esef-514bf.firebaseapp.com", - projectId: "esef-514bf", - storageBucket: "esef-514bf.firebasestorage.app", - messagingSenderId: "353969651290", - appId: "1:353969651290:web:41e3aaf8e54c743702a263" -}; +(() => { + const FIREBASE_CONFIG = { + apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", + authDomain: "esef-514bf.firebaseapp.com", + projectId: "esef-514bf", + storageBucket: "esef-514bf.firebasestorage.app", + messagingSenderId: "353969651290", + appId: "1:353969651290:web:41e3aaf8e54c743702a263" + }; -const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -const MONTHS = ["January","February","March","April","May","June", - "July","August","September","October","November","December"]; + const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const MONTHS = ["January","February","March","April","May","June", + "July","August","September","October","November","December"]; -function fmtTime(utcIso) { - if (utcIso) { - try { - var d = new Date(utcIso); - if (!isNaN(d.getTime())) { - return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); - } - } catch (e) {} - } - return ""; -} + document.addEventListener("DOMContentLoaded", () => { + const tab = document.getElementById("calendar"); + if (!tab) return; -document.addEventListener("DOMContentLoaded", () => { - const tab = document.getElementById("calendar"); - if (!tab) return; + if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG); + const db = firebase.firestore(); - if (!firebase.apps.length) firebase.initializeApp(CAL_FB_CONFIG); - const db = firebase.firestore(); + let allEvents = []; + const now = new Date(); + let viewYear = now.getFullYear(); + let viewMonth = now.getMonth(); - let allEvents = []; - let viewDate = new Date(); + const gridEl = document.getElementById("cal-grid"); + const navEl = document.getElementById("cal-nav-title"); + const sidebarEl = document.getElementById("cal-sidebar-list"); - const gridEl = document.getElementById("cal-grid"); - const navEl = document.getElementById("cal-nav-title"); - const sidebarEl = document.getElementById("cal-sidebar-list"); - - db.collection("calendar_events").onSnapshot(snap => { - allEvents = []; - snap.forEach(doc => { - const d = doc.data(); - allEvents.push({ - id: doc.id, - title: d.title || "", - date: d.date || "", - time: d.time || "", - utc: d.utc || "", - description: d.description || "", - _sort: d.utc || (d.date + "T" + (d.time || "00:00")) - }); - }); - allEvents.sort((a, b) => a._sort.localeCompare(b._sort)); - render(); - }, () => { - if (gridEl) gridEl.innerHTML = '

Could not load.

'; - }); - - document.getElementById("cal-prev").onclick = () => { viewDate.setMonth(viewDate.getMonth()-1); render(); }; - document.getElementById("cal-next").onclick = () => { viewDate.setMonth(viewDate.getMonth()+1); render(); }; - - // Auto-refresh every 60s so "Upcoming" stays current as time passes - setInterval(render, 60000); - - function render() { - const year = viewDate.getFullYear(); - const month = viewDate.getMonth(); - navEl.textContent = MONTHS[month] + " " + year; - - const byDate = {}; - allEvents.forEach(e => { - if (!byDate[e.date]) byDate[e.date] = []; - byDate[e.date].push(e); + db.collection("calendar_events").onSnapshot(snap => { + allEvents = snap.docs.map(doc => toEvent(doc.data())); + allEvents.sort((a, b) => a.when - b.when); + render(); + }, () => { + gridEl.innerHTML = '

Could not load.

'; }); - const firstDay = new Date(year, month, 1).getDay(); - const daysInMonth = new Date(year, month + 1, 0).getDate(); - const today = new Date(); - const todayStr = today.getFullYear() + "-" + - String(today.getMonth()+1).padStart(2,"0") + "-" + - String(today.getDate()).padStart(2,"0"); + document.getElementById("cal-prev").onclick = () => shiftMonth(-1); + document.getElementById("cal-next").onclick = () => shiftMonth(1); - let html = ""; - WEEKDAYS.forEach(d => html += '
' + d + "
"); + // Auto-refresh every 60s so "Upcoming" stays current as time passes + setInterval(render, 60000); - for (let i = 0; i < firstDay; i++) { - html += '
'; + function shiftMonth(delta) { + // Day 1 avoids overflow (e.g. Jan 31 + 1 month → Mar 3) + const d = new Date(viewYear, viewMonth + delta, 1); + viewYear = d.getFullYear(); + viewMonth = d.getMonth(); + render(); } - for (let d = 1; d <= daysInMonth; d++) { - const dateStr = year + "-" + - String(month+1).padStart(2,"0") + "-" + - String(d).padStart(2,"0"); - const dayEvents = byDate[dateStr] || []; - const isToday = dateStr === todayStr; - const hasEvents = dayEvents.length > 0; + function render() { + navEl.textContent = MONTHS[viewMonth] + " " + viewYear; - html += '
'; - html += '
' + d + "
"; + const byDate = {}; + allEvents.forEach(e => { + (byDate[e.date] = byDate[e.date] || []).push(e); + }); - if (hasEvents) { - dayEvents.forEach(() => html += ''); - html += '

' + dateStr + "

"; - dayEvents.forEach(e => { - html += '
'; - if (e.time) { - html += '' + fmtTime(e.utc) + " "; - } - html += esc(e.title); - if (e.description) html += '
' + esc(e.description) + "
"; + const firstDay = new Date(viewYear, viewMonth, 1).getDay(); + const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); + const todayStr = ymd(new Date()); + + let html = ""; + WEEKDAYS.forEach(d => html += '
' + d + "
"); + + for (let i = 0; i < firstDay; i++) { + html += '
'; + } + + for (let d = 1; d <= daysInMonth; d++) { + const dateStr = ymd(new Date(viewYear, viewMonth, d)); + const dayEvents = byDate[dateStr] || []; + const hasEvents = dayEvents.length > 0; + + html += '
'; + html += '
' + d + "
"; + + if (hasEvents) { + dayEvents.forEach(() => html += ''); + html += '

' + dateStr + "

"; + dayEvents.forEach(e => { + html += '
'; + if (e.time) html += '' + esc(e.time) + " "; + html += esc(e.title); + if (e.description) html += '
' + esc(e.description) + "
"; + html += "
"; + }); html += "
"; - }); + } html += "
"; } - html += "
"; - } - gridEl.innerHTML = html; + gridEl.innerHTML = html; - const nowISO = new Date().toISOString(); - const upcoming = allEvents.filter(e => e._sort >= nowISO); - if (upcoming.length === 0) { - sidebarEl.innerHTML = '

No upcoming events.
Use !book in Discord.

'; - } else { - sidebarEl.innerHTML = upcoming.slice(0, 15).map(e => - '
' + - '
' + e.date + - (e.time ? " " + fmtTime(e.utc) : "") + "
" + - '
' + esc(e.title) + "
" + - (e.description ? '
' + esc(e.description) + "
" : "") + - "
" - ).join(""); + const nowMs = Date.now(); + const upcoming = allEvents.filter(e => e.when >= nowMs); + if (upcoming.length === 0) { + sidebarEl.innerHTML = '

No upcoming events.
Use !book in Discord.

'; + } else { + sidebarEl.innerHTML = upcoming.slice(0, 15).map(e => + '
' + + '
' + esc(e.date) + (e.time ? " " + esc(e.time) : "") + "
" + + '
' + esc(e.title) + "
" + + (e.description ? '
' + esc(e.description) + "
" : "") + + "
" + ).join(""); + } } + }); + + // 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. + function toEvent(d) { + const utc = d.utc ? new Date(d.utc) : null; + const hasUtc = utc && !isNaN(utc.getTime()); + const localize = hasUtc && d.time; + const date = localize ? ymd(utc) : (d.date || ""); + const time = !d.time ? "" : localize + ? utc.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + : d.time; + return { + title: d.title || "", + 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 + }; + } + + function ymd(d) { + return d.getFullYear() + "-" + + String(d.getMonth() + 1).padStart(2, "0") + "-" + + String(d.getDate()).padStart(2, "0"); } function esc(s) { @@ -142,4 +146,4 @@ document.addEventListener("DOMContentLoaded", () => { return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); } -}); +})(); diff --git a/js/comments.js b/js/comments.js index 4f26997..0f4e6b6 100644 --- a/js/comments.js +++ b/js/comments.js @@ -1,80 +1,81 @@ -const FIREBASE_CONFIG = { - apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", - authDomain: "esef-514bf.firebaseapp.com", - projectId: "esef-514bf", - storageBucket: "esef-514bf.firebasestorage.app", - messagingSenderId: "353969651290", - appId: "1:353969651290:web:41e3aaf8e54c743702a263" -}; +// About tab — Steam-style comment wall + global click counter +(() => { + const FIREBASE_CONFIG = { + apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", + authDomain: "esef-514bf.firebaseapp.com", + projectId: "esef-514bf", + storageBucket: "esef-514bf.firebasestorage.app", + messagingSenderId: "353969651290", + appId: "1:353969651290:web:41e3aaf8e54c743702a263" + }; -// ================================================================ + const MAX_CHARS = 300; + const MAX_NAME = 32; + const COMMENTS_PER_PAGE = 5; + const COLLECTION = "comments"; + const DEFAULT_COLOR = "#5b6fde"; + const COUNT_COLOR = "#364858"; + const COUNT_LOW_COLOR = "#e05555"; -const MAX_CHARS = 300; -const COLLECTION = "comments"; + document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('comment-form'); + const nameInput = document.getElementById('comment-name'); + const msgInput = document.getElementById('comment-message'); + const submitBtn = document.getElementById('comment-submit'); + const charCountEl = document.getElementById('char-count'); + const listEl = document.getElementById('comments-list'); + const titleEl = document.getElementById('comments-title'); + const paginationEl = document.getElementById('comments-pagination'); + const colorInput = document.getElementById('comment-avatar-color'); + const b64Input = document.getElementById('comment-avatar-b64'); + const pfpUpload = document.getElementById('pfp-upload'); + const pfpPreview = document.getElementById('pfp-preview'); + const pfpClear = document.getElementById('pfp-clear'); -document.addEventListener('DOMContentLoaded', () => { + if (!form || !listEl) return; - const configured = FIREBASE_CONFIG.apiKey !== "YOUR_API_KEY"; + if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG); + const db = firebase.firestore(); - const form = document.getElementById('comment-form'); - const nameInput = document.getElementById('comment-name'); - const msgInput = document.getElementById('comment-message'); - const submitBtn = document.getElementById('comment-submit'); - const charCountEl = document.getElementById('char-count'); - const listEl = document.getElementById('comments-list'); - const titleEl = document.getElementById('comments-title'); - const colorInput = document.getElementById('comment-avatar-color'); - const b64Input = document.getElementById('comment-avatar-b64'); - const pfpUpload = document.getElementById('pfp-upload'); - const pfpPreview = document.getElementById('pfp-preview'); - const pfpClear = document.getElementById('pfp-clear'); - const warning = document.getElementById('firebase-setup-warning'); - - if (!form || !listEl) return; - - // ── Avatar colour picker ───────────────────────────────────── - document.querySelectorAll('.av-opt').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('.av-opt').forEach(b => b.classList.remove('selected')); - btn.classList.add('selected'); - colorInput.value = btn.dataset.color; + // ── Avatar colour picker ───────────────────────────────────── + const colorOpts = document.querySelectorAll('.av-opt'); + colorOpts.forEach(btn => { + btn.addEventListener('click', () => { + colorOpts.forEach(b => b.classList.remove('selected')); + btn.classList.add('selected'); + colorInput.value = btn.dataset.color; + }); }); - }); - // ── Custom PFP Upload ──────────────────────────────────────── - if (pfpUpload) { - pfpUpload.addEventListener('change', (e) => { - const file = e.target.files[0]; + // ── Custom PFP upload (center-cropped + shrunk to 64×64 JPEG) ─ + pfpUpload.addEventListener('change', () => { + const file = pfpUpload.files[0]; if (!file) return; const reader = new FileReader(); - reader.onload = (event) => { + reader.onload = () => { const img = new Image(); img.onload = () => { - // Shrink to max 64x64 to save space const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); canvas.width = 64; canvas.height = 64; - - // Center and crop + const size = Math.min(img.width, img.height); const sx = (img.width - size) / 2; const sy = (img.height - size) / 2; - - ctx.drawImage(img, sx, sy, size, size, 0, 0, 64, 64); - + canvas.getContext('2d').drawImage(img, sx, sy, size, size, 0, 0, 64, 64); + const dataUrl = canvas.toDataURL('image/jpeg', 0.8); b64Input.value = dataUrl; pfpPreview.style.backgroundImage = `url(${dataUrl})`; pfpPreview.style.display = 'block'; pfpClear.style.display = 'inline-flex'; }; - img.src = event.target.result; + img.src = reader.result; }; reader.readAsDataURL(file); }); - + pfpClear.addEventListener('click', () => { b64Input.value = ''; pfpUpload.value = ''; @@ -82,220 +83,171 @@ document.addEventListener('DOMContentLoaded', () => { pfpPreview.style.backgroundImage = 'none'; pfpClear.style.display = 'none'; }); - } - // ── Char counter ───────────────────────────────────────────── - charCountEl.textContent = MAX_CHARS; - msgInput.addEventListener('input', () => { - const left = MAX_CHARS - msgInput.value.length; - charCountEl.textContent = left; - charCountEl.style.color = left < 40 ? '#e05555' : '#364858'; - }); - - // ── No Firebase → show demo + setup warning ────────────────── - if (!configured) { - if (warning) warning.style.display = 'block'; - showDemo(listEl); - form.addEventListener('submit', e => { - e.preventDefault(); - alert('Firebase is not connected yet!\n\nOpen js/comments.js and follow the setup instructions at the top of the file.'); - }); - return; - } - - // Firebase is configured — hide the setup warning - if (warning) warning.style.display = 'none'; - - // ── Firebase init ──────────────────────────────────────────── - firebase.initializeApp(FIREBASE_CONFIG); - const db = firebase.firestore(); - - // ── Real-time feed & Pagination ────────────────────────────── - let allComments = []; - let currentPage = 1; - const COMMENTS_PER_PAGE = 5; - const paginationEl = document.getElementById('comments-pagination'); - - function renderPage() { - listEl.innerHTML = ''; - if (paginationEl) paginationEl.innerHTML = ''; - - if (allComments.length === 0) { - listEl.innerHTML = '

No messages yet — be the first!

'; - return; + // ── Char counter ───────────────────────────────────────────── + function updateCharCount() { + const left = MAX_CHARS - msgInput.value.length; + charCountEl.textContent = left; + charCountEl.style.color = left < 40 ? COUNT_LOW_COLOR : COUNT_COLOR; } + msgInput.addEventListener('input', updateCharCount); + updateCharCount(); - const totalPages = Math.ceil(allComments.length / COMMENTS_PER_PAGE); - if (currentPage > totalPages) currentPage = totalPages; - if (currentPage < 1) currentPage = 1; + // ── Real-time feed + pagination ────────────────────────────── + let allComments = []; + let currentPage = 1; - const start = (currentPage - 1) * COMMENTS_PER_PAGE; - const end = start + COMMENTS_PER_PAGE; - const slice = allComments.slice(start, end); + function renderPage() { + listEl.innerHTML = ''; + paginationEl.innerHTML = ''; - slice.forEach(data => listEl.appendChild(buildBubble(data))); + if (allComments.length === 0) { + listEl.innerHTML = '

No messages yet — be the first!

'; + return; + } - // Build pagination controls - if (totalPages > 1 && paginationEl) { - for (let i = 1; i <= totalPages; i++) { - const btn = document.createElement('button'); - btn.className = 'steam-page-btn' + (i === currentPage ? ' active' : ''); - btn.textContent = i; - btn.onclick = () => { - currentPage = i; - renderPage(); - }; - paginationEl.appendChild(btn); + const totalPages = Math.ceil(allComments.length / COMMENTS_PER_PAGE); + currentPage = Math.min(Math.max(currentPage, 1), totalPages); + + const start = (currentPage - 1) * COMMENTS_PER_PAGE; + allComments.slice(start, start + COMMENTS_PER_PAGE) + .forEach(data => listEl.appendChild(buildBubble(data))); + + if (totalPages > 1) { + for (let i = 1; i <= totalPages; i++) { + const btn = document.createElement('button'); + btn.className = 'steam-page-btn' + (i === currentPage ? ' active' : ''); + btn.textContent = i; + btn.onclick = () => { + currentPage = i; + renderPage(); + }; + paginationEl.appendChild(btn); + } } } - } - // ── Global Click Counter ───────────────────────────────────── - const clickBtn = document.getElementById('yuuka-click-btn'); - const clickDisplay = document.getElementById('click-counter-display'); - const clickSound = new Audio('sounds/yuuka_click.mp3'); - - if (clickBtn && clickDisplay && configured) { - const statsRef = db.collection('stats').doc('global_clicks'); - - // Listen to changes in real-time - statsRef.onSnapshot(doc => { - if (doc.exists) { - clickDisplay.textContent = doc.data().count.toLocaleString(); - } else { - clickDisplay.textContent = '0'; - } - }, err => { - console.error("Click counter error:", err); - clickDisplay.textContent = 'Error'; - }); - - // Increment on click - clickBtn.addEventListener('click', () => { - // Play sound - clickSound.currentTime = 0; // Reset to allow rapid clicking - clickSound.play().catch(e => console.error("Audio play failed:", e)); - - // Optimistic update for instant feel - let current = parseInt(clickDisplay.textContent.replace(/,/g, '')) || 0; - clickDisplay.textContent = (current + 1).toLocaleString(); - - statsRef.set({ - count: firebase.firestore.FieldValue.increment(1) - }, { merge: true }).catch(err => console.error("Failed to click:", err)); - }); - } - - db.collection(COLLECTION) - .orderBy('timestamp', 'desc') - .onSnapshot(snap => { - if (titleEl) { + db.collection(COLLECTION) + .orderBy('timestamp', 'desc') + .onSnapshot(snap => { titleEl.textContent = snap.size > 0 ? `(${snap.size})` : ''; + allComments = snap.docs.map(doc => doc.data()); + renderPage(); + }, err => { + console.error(err); + listEl.innerHTML = '

Could not load comments.

'; + }); + + // ── Global click counter ───────────────────────────────────── + const clickBtn = document.getElementById('yuuka-click-btn'); + const clickDisplay = document.getElementById('click-counter-display'); + const clickSound = new Audio('sounds/yuuka_click.mp3'); + + 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(); + }, err => { + console.error("Click counter error:", err); + clickDisplay.textContent = 'Error'; + }); + + clickBtn.addEventListener('click', () => { + clickSound.currentTime = 0; // restart so rapid clicks all play + clickSound.play().catch(() => {}); + + statsRef.set({ + count: firebase.firestore.FieldValue.increment(1) + }, { merge: true }).catch(err => console.error("Failed to click:", err)); + }); + } + + // ── Submit ─────────────────────────────────────────────────── + form.addEventListener('submit', async e => { + e.preventDefault(); + + const name = nameInput.value.trim().slice(0, MAX_NAME) || 'Anonymous'; + const message = msgInput.value.trim().slice(0, MAX_CHARS); + const color = colorInput.value || DEFAULT_COLOR; + const avatarUrl = b64Input.value; + + if (!message) { msgInput.focus(); return; } + + const label = submitBtn.textContent; + submitBtn.disabled = true; + submitBtn.textContent = 'Posting...'; + + try { + await db.collection(COLLECTION).add({ + name, message, color, avatarUrl, + timestamp: firebase.firestore.FieldValue.serverTimestamp() + }); + nameInput.value = ''; + msgInput.value = ''; + updateCharCount(); + } catch (err) { + console.error(err); + alert('Failed to post. Try again!'); + } finally { + submitBtn.disabled = false; + submitBtn.textContent = label; } + }); + }); - allComments = []; - snap.forEach(doc => allComments.push(doc.data())); - - renderPage(); + // ── Build a single comment bubble (Steam layout) ───────────────── + function buildBubble(data) { + const div = document.createElement('div'); + div.className = 'comment-bubble'; - }, err => { - console.error(err); - listEl.innerHTML = '

Could not load comments. Check your Firebase config.

'; + // Pending server timestamps are null until the write lands + const ts = data.timestamp ? data.timestamp.toDate() : new Date(); + const timeStr = ts.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric', + hour: '2-digit', minute: '2-digit' }); - // ── Submit ─────────────────────────────────────────────────── - form.addEventListener('submit', async e => { - e.preventDefault(); + const name = legacyUnescape(data.name) || 'Anonymous'; + const message = legacyUnescape(data.message); - const name = clean(nameInput.value.trim()).slice(0, 32) || 'Anonymous'; - const message = clean(msgInput.value.trim()).slice(0, MAX_CHARS); - const color = colorInput.value || '#5b6fde'; - const avatarUrl = b64Input.value; - - if (!message) { msgInput.focus(); return; } - - submitBtn.disabled = true; - submitBtn.textContent = 'Posting...'; - - try { - await db.collection(COLLECTION).add({ - name, message, color, avatarUrl, - timestamp: firebase.firestore.FieldValue.serverTimestamp() - }); - nameInput.value = ''; - msgInput.value = ''; - charCountEl.textContent = MAX_CHARS; - charCountEl.style.color = '#364858'; - } catch (err) { - console.error(err); - alert('Failed to post. Try again!'); - } finally { - submitBtn.disabled = false; - submitBtn.textContent = 'POST'; + let avatarHtml; + if (typeof data.avatarUrl === 'string' && data.avatarUrl.startsWith('data:image/')) { + avatarHtml = ``; + } else { + const color = /^#[0-9a-f]{6}$/i.test(data.color) ? data.color : '#64748b'; + avatarHtml = `
${esc(name.charAt(0).toUpperCase())}
`; } - }); -}); -// ── Build a single comment bubble (Steam layout) ───────────────── - -function buildBubble(data) { - const div = document.createElement('div'); - div.className = 'comment-bubble'; - - const ts = data.timestamp ? data.timestamp.toDate() : new Date(); - const timeStr = ts.toLocaleDateString('en-US', { - year: 'numeric', month: 'short', day: 'numeric', - hour: '2-digit', minute: '2-digit' - }); - - const color = data.color || '#64748b'; - const initial = (data.name || '?').charAt(0).toUpperCase(); - - let avatarHtml = ''; - if (data.avatarUrl) { - avatarHtml = `Avatar`; - } else { - avatarHtml = ` -
- ${esc(initial)} + div.innerHTML = ` + ${avatarHtml} +
+
+ ${esc(name)} + ${timeStr} +
+

${esc(message)}

`; + return div; } - div.innerHTML = ` - ${avatarHtml} -
-
- ${esc(data.name)} - ${timeStr} -
-

${esc(data.message)}

-
- `; - return div; -} + // ── Helpers ────────────────────────────────────────────────────── -// ── Demo bubbles shown before Firebase is set up ───────────────── + // Older comments were stored pre-escaped (< and > only), which made + // them render as literal "<" once escaped again. Undo that here. + function legacyUnescape(str) { + if (!str) return ''; + return String(str).replace(/</g, '<').replace(/>/g, '>'); + } -function showDemo(list) { - const demos = [ - { name: 'Visitor', message: 'cool site lol', color: '#3b82f6', timestamp: { toDate: () => new Date(Date.now() - 60000) } }, - { name: 'Ahmet', message: 'when is the gallery getting updated', color: '#10b981', timestamp: { toDate: () => new Date(Date.now() - 300000) } }, - { name: 'Someone', message: 'Ahmet museum is peak', color: '#8b5cf6', timestamp: { toDate: () => new Date(Date.now() - 800000) } }, - ]; - list.innerHTML = ''; - demos.forEach(d => list.appendChild(buildBubble(d))); -} - -// ── Helpers ────────────────────────────────────────────────────── - -function clean(str) { - return str.replace(//g, '>'); -} - -function esc(str) { - if (!str) return ''; - return String(str) - .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); -} + function esc(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); + } +})(); diff --git a/js/gallery.js b/js/gallery.js index 72184aa..07338d0 100644 --- a/js/gallery.js +++ b/js/gallery.js @@ -1,69 +1,46 @@ +// Gallery tab — thumbnails from gallery/manifest.json + full-size lightbox. +// The manifest is (re)generated by generate_thumbnails.py. document.addEventListener('DOMContentLoaded', () => { + const container = document.getElementById('gallery-container'); const lightbox = document.getElementById('lightbox'); const lightboxImg = document.getElementById('lightbox-img'); - const closeBtn = document.querySelector('.lightbox-close'); - // Close lightbox when clicking the X - if (closeBtn) { - closeBtn.onclick = () => { - lightbox.style.display = "none"; - lightboxImg.src = ""; // Clear source to stop GIFs - } - } + if (!container || !lightbox) return; - // Close lightbox when clicking outside the image - if (lightbox) { - lightbox.onclick = (e) => { - if (e.target !== lightboxImg && e.target !== closeBtn) { - lightbox.style.display = "none"; - lightboxImg.src = ""; // Clear source to stop GIFs - } - } - } - - // Function to load images into a specific container from a specific folder - const loadGallery = (containerId, folderName, totalImages) => { - const container = document.getElementById(containerId); - if (!container) return; - - for (let i = 1; i <= totalImages; i++) { - const img = document.createElement('img'); - img.alt = `${folderName} Image ${i}`; - - // Load the thumbnail instead of the full image - img.src = `${folderName}/thumbnails/${i}.jpg`; - - // If thumbnail fails to load, it means the image doesn't exist at all - img.onerror = () => { img.style.display = 'none'; }; - - // Open lightbox on click - img.onclick = () => { - lightbox.style.display = "block"; - lightboxImg.src = ""; // Clear previous image - - // Try loading original full quality image: jpg -> png -> gif - const tryLoad = (extensions) => { - if (extensions.length === 0) return; // All failed - const testExt = extensions.shift(); - - const tempImg = new Image(); - tempImg.onload = () => { - lightboxImg.src = tempImg.src; // Success! Show it. - }; - tempImg.onerror = () => { - tryLoad(extensions); // Failed, try next extension - }; - tempImg.src = `${folderName}/${i}.${testExt}`; - }; - - // Start trying to find the full image - tryLoad(['jpg', 'png', 'gif']); - }; - - container.appendChild(img); - } + const openLightbox = (src) => { + lightboxImg.src = src; + lightbox.style.display = 'block'; }; - // Load your original Gallery - loadGallery('gallery-container', 'gallery', 50); -}); \ No newline at end of file + const closeLightbox = () => { + lightbox.style.display = 'none'; + lightboxImg.src = ''; // Clear source to stop GIFs + }; + + // Close on the X, on a click outside the image, or with Escape + lightbox.addEventListener('click', (e) => { + if (e.target !== lightboxImg) closeLightbox(); + }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && lightbox.style.display === 'block') closeLightbox(); + }); + + const thumbPath = (file) => `gallery/thumbnails/${file.replace(/\.[^.]+$/, '')}.jpg`; + + fetch('gallery/manifest.json') + .then(res => { + if (!res.ok) throw new Error(res.status); + return res.json(); + }) + .then(files => { + files.forEach((file, i) => { + const img = document.createElement('img'); + img.src = thumbPath(file); + img.alt = `Gallery image ${i + 1}`; + img.loading = 'lazy'; + img.onclick = () => openLightbox(`gallery/${file}`); + container.appendChild(img); + }); + }) + .catch(err => console.error('Could not load gallery manifest:', err)); +}); diff --git a/js/tabs.js b/js/tabs.js index 6a885c7..915c545 100644 --- a/js/tabs.js +++ b/js/tabs.js @@ -59,15 +59,17 @@ document.addEventListener('DOMContentLoaded', () => { } // ── Navigate to a tab (updates URL + shows it) ──────── - function navigateTo(tabId, push = true) { + function navigateTo(tabId) { showTab(tabId); - if (push) { - history.pushState({ tab: tabId }, '', tabToPath(tabId)); - } + window.scrollTo(0, 0); + history.pushState({ tab: tabId }, '', tabToPath(tabId)); } // ── Initial load: show tab from URL ──────────────────── - showTab(pathToTab(window.location.pathname)); + // (also normalises unknown / legacy paths like /ahmet in the address bar) + const initialTab = pathToTab(window.location.pathname); + showTab(initialTab); + history.replaceState({ tab: initialTab }, '', tabToPath(initialTab) + window.location.search); // ── Tab button clicks ────────────────────────────────── const buttons = document.querySelectorAll('#top-line button'); @@ -78,22 +80,9 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // ── Home button (logo) ───────────────────────────────── - const homeBtn = document.getElementById('home-btn'); - if (homeBtn) { - homeBtn.addEventListener('click', (e) => { - e.preventDefault(); - navigateTo('about'); - }); - } - // ── Back / forward browser buttons ───────────────────── window.addEventListener('popstate', (e) => { - if (e.state && e.state.tab) { - showTab(e.state.tab); - } else { - showTab(pathToTab(window.location.pathname)); - } + showTab(e.state && e.state.tab ? e.state.tab : pathToTab(window.location.pathname)); }); }); diff --git a/js/yuukabot.js b/js/yuukabot.js index 675731d..93cf500 100644 --- a/js/yuukabot.js +++ b/js/yuukabot.js @@ -1,80 +1,99 @@ // Yuuka Bot Dashboard — live Firestore reader -const YUUKA_FIREBASE_CONFIG = { - apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", - authDomain: "esef-514bf.firebaseapp.com", - projectId: "esef-514bf", - storageBucket: "esef-514bf.firebasestorage.app", - messagingSenderId: "353969651290", - appId: "1:353969651290:web:41e3aaf8e54c743702a263" -}; +(() => { + const FIREBASE_CONFIG = { + apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", + authDomain: "esef-514bf.firebaseapp.com", + projectId: "esef-514bf", + storageBucket: "esef-514bf.firebasestorage.app", + messagingSenderId: "353969651290", + appId: "1:353969651290:web:41e3aaf8e54c743702a263" + }; -document.addEventListener("DOMContentLoaded", () => { - const dash = document.getElementById("yuukabot"); - if (!dash) return; + // 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). + const STALE_MS = 60 * 1000; - if (!firebase.apps.length) firebase.initializeApp(YUUKA_FIREBASE_CONFIG); - const db = firebase.firestore(); + document.addEventListener("DOMContentLoaded", () => { + const dash = document.getElementById("yuukabot"); + if (!dash) return; - db.collection("sysinfo").doc("server").onSnapshot( - (doc) => { + if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG); + const db = firebase.firestore(); + + 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 + + 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 + setInterval(refreshDot, 5000); + + db.collection("sysinfo").doc("server").onSnapshot(doc => { + loaded = true; + const d = doc.exists ? doc.data() : {}; + online = !!d.online; + updatedAt = d.updatedAt ? d.updatedAt.toMillis() : null; + refreshDot(); if (!doc.exists) return; - const d = doc.data(); - // Online dot - const dot = document.getElementById("dash-status-dot"); - if (dot) dot.className = d.online ? "status-dot" : "status-dot offline"; - - // Uptime setText("dash-uptime", d.uptime); + setText("dash-bot-uptime", d.botUptime ? "bot up " + d.botUptime : ""); - // CPU, RAM, Disk, Swap — bars - setBar("dash-cpu-val", "dash-cpu-bar", null, d.cpu, "%", "cpu"); - setBar("dash-ram-val", "dash-ram-bar", null, d.ram, "%", "ram"); - setText("dash-ram-detail", d.ramUsed ? d.ramUsed + " / " + d.ramTotal : ""); - setBar("dash-disk-val", "dash-disk-bar", null, d.disk, "%", "disk"); - setText("dash-disk-detail", d.diskUsed ? d.diskUsed + " / " + d.diskTotal : ""); - setBar("dash-swap-val", "dash-swap-bar", null, d.swap, "%", "swap"); - setText("dash-swap-detail", d.swapUsed ? d.swapUsed + " / " + d.swapTotal : ""); + // 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); + setBar("dash-swap-val", "dash-swap-bar", d.swap); + setText("dash-ram-detail", usedOfTotal(d.ramUsed, d.ramTotal)); + setText("dash-disk-detail", usedOfTotal(d.diskUsed, d.diskTotal)); + setText("dash-swap-detail", usedOfTotal(d.swapUsed, d.swapTotal)); - // Load average if (d.load1 != null) { setText("dash-load", d.load1 + " / " + d.load5 + " / " + d.load15); } - // Network if (d.netSent) { setText("dash-net", "sent " + d.netSent); setText("dash-net-detail", "recv " + (d.netRecv || "—")); } - // Processes - // Fastfetch - setText("dash-fetch", d.fetch); - if (d.processes != null) { setText("dash-procs", d.processes.toLocaleString()); } - }, - () => { - const dot = document.getElementById("dash-status-dot"); - if (dot) dot.className = "status-dot offline"; + + setText("dash-fetch", d.fetch); + }, () => { + loaded = true; + online = false; + refreshDot(); + }); + }); + + function usedOfTotal(used, total) { + return used ? used + " / " + total : ""; + } + + function setText(id, value) { + const el = document.getElementById(id); + if (el && value != null) el.textContent = value; + } + + function setBar(valId, barId, value) { + const valEl = document.getElementById(valId); + const barEl = document.getElementById(barId); + if (valEl && value != null) { + valEl.textContent = value; + const unit = document.createElement("span"); + unit.textContent = "%"; + valEl.appendChild(unit); + } + if (barEl) { + barEl.style.width = Math.min(Math.max(value || 0, 0), 100) + "%"; } - ); -}); - -function setText(id, value) { - const el = document.getElementById(id); - if (el && value != null) el.textContent = value; -} - -function setBar(valId, barId, _detailId, value, suffix, barClass) { - const valEl = document.getElementById(valId); - const barEl = document.getElementById(barId); - if (valEl && value != null) { - valEl.innerHTML = value + '' + suffix + ''; } - if (barEl) { - barEl.style.width = Math.min(Math.max(value || 0, 0), 100) + "%"; - barEl.className = "card-bar-fill " + barClass; - } -} \ No newline at end of file +})(); diff --git a/style/bucketlist.css b/style/bucketlist.css index 3a2919f..ff604d9 100644 --- a/style/bucketlist.css +++ b/style/bucketlist.css @@ -107,9 +107,9 @@ box-shadow: 0 3px 16px rgba(184, 68, 87, 0.08); } -/* TODO: .bl-done styles — dim + strikethrough for completed items */ -/* .bl-item.bl-done { opacity: 0.5; } - .bl-item.bl-done .bl-title { text-decoration: line-through; } */ +/* Completed items */ +.bl-item.bl-done { opacity: 0.5; } +.bl-item.bl-done .bl-title { text-decoration: line-through; } .bl-item .bl-title { color: #F4F2F5; diff --git a/style/calendar.css b/style/calendar.css index 33da11e..70de0a4 100644 --- a/style/calendar.css +++ b/style/calendar.css @@ -1,11 +1,5 @@ /* Calendar tab */ -#calendar-btn { - background-color: #FBC4CC; color: #232329; - border: none; padding: 12px 24px; border-radius: 8px; - cursor: pointer; transition: box-shadow 0.3s ease; -} - /* ── Two-column layout ─────────────────────────────────────── */ .cal-wrap { max-width: 95vw; margin: 0 auto; @@ -93,7 +87,7 @@ } /* ── Sidebar ───────────────────────────────────────────────── */ -.cal-sidebar h3 { margin-top: 0; +.cal-sidebar h3 { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; color: rgba(244,242,245,0.65); margin: 0 0 14px; font-family: Arial, sans-serif; @@ -117,4 +111,4 @@ font-family: Arial, sans-serif; font-size: 0.8rem; } -.cal-tz-note { font-size: 0.65rem; color: rgba(244,242,245,0.35); text-align: center; margin-top: 16px; } \ No newline at end of file +.cal-tz-note { font-size: 0.65rem; color: rgba(244,242,245,0.35); text-align: center; margin-top: 16px; } diff --git a/style/comments.css b/style/comments.css index 0f05f10..edb4bec 100644 --- a/style/comments.css +++ b/style/comments.css @@ -97,18 +97,6 @@ margin: 0; } -/* ── Firebase warning ────────────────────────────────────── */ -#firebase-setup-warning { - background: rgba(180, 80, 30, 0.25); - border-bottom: 1px solid rgba(180, 80, 30, 0.4); - padding: 10px 14px; - color: #ffb89a; - font-size: 0.8rem; - line-height: 1.5; - display: none; - font-family: Arial, Helvetica, sans-serif; -} - /* ── Comment rows ────────────────────────────────────────── */ #comments-list { display: flex; @@ -169,7 +157,7 @@ flex-wrap: wrap; } -/* Steam cyan username -> Now Black */ +/* Username */ .comment-name { font-size: 0.85rem; font-weight: bold; @@ -209,12 +197,10 @@ font-style: italic; } -/* ── Post form (bottom, Steam style) ────────────────────── */ +/* ── Post form (above the feed) ──────────────────────────── */ .steam-form-area { background: transparent; - border-top: 1px solid rgba(255, 255, 255, 0.1); - padding: 20px 0 0 0; - margin-top: 10px; + margin: 20px 0; } .steam-form-label { @@ -338,13 +324,6 @@ } #comment-submit:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; } -/* Comment count next to header */ -.comment-count-badge { - font-size: 0.72rem; - color: #556772; - font-family: Arial, Helvetica, sans-serif; -} - /* ── Custom PFP Upload ───────────────────────────────────── */ .custom-pfp-upload { display: flex; diff --git a/style/gallery.css b/style/gallery.css index b75e2c2..c7129ce 100644 --- a/style/gallery.css +++ b/style/gallery.css @@ -1,22 +1,8 @@ -#gallery-link-btn { - background-color:aliceblue; - color:#f0f8ff; - border: none; - padding: 12px 24px; - border-radius: 8px; - cursor: pointer; - transition: box-shadow 0.3s ease; -} - -#gallery-link-btn:hover { - box-shadow: 0 0 10px #02e3ff; -} - .gallery-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; - padding: 70px; + padding: 210px 70px 70px; /* extra top padding clears the fixed nav bar */ } .gallery-grid img { diff --git a/style/styles.css b/style/styles.css index 06b3736..2c3deae 100644 --- a/style/styles.css +++ b/style/styles.css @@ -32,8 +32,8 @@ html, body { top: 0; left: 0; width: 100%; - height: 95px; - background-color: rgba(0, 0, 0, 0.3); + height: 115px; /* 95px + 2×10px padding — other tabs' top offsets assume this */ + box-sizing: border-box; backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); z-index: 10; @@ -85,12 +85,6 @@ html, body { transition: border-color 0.4s ease, color 0.4s ease; } -title { - font-size: large; - font-weight: 100; - margin-bottom: auto; - color:#000000; -} h1 { font-size: 3rem; color: #000000; diff --git a/style/yuukabot.css b/style/yuukabot.css index 661b0fc..d836211 100644 --- a/style/yuukabot.css +++ b/style/yuukabot.css @@ -7,14 +7,10 @@ --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-padding: 120px 48px 60px; /* top right/left bottom */ + --dash-max-width: none; /* max width of the whole grid */ } -/* To make boxes take different amounts of space, add a class - to the HTML like "wide" or "tall" and define it here: - - .dash-card.tall { grid-row: span 2; } */ +/* Card widths come from the .spanN helpers at the bottom of this file. */ /* ── Tab header ───────────────────────────────────────────── */ .dash-header { @@ -41,6 +37,7 @@ 50% { box-shadow: 0 0 20px #22c55e, 0 0 40px rgba(34,197,94,0.4); } } .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 ────────────────────────────────────────────── */ .dash-grid { @@ -71,19 +68,12 @@ box-shadow: 0 6px 28px rgba(91, 111, 222, 0.12); } -/* ── Card header (icon + label) ───────────────────────────── */ +/* ── Card header ──────────────────────────────────────────── */ .card-top { display: flex; align-items: center; gap: 12px; } -.card-icon { - font-size: 1.6rem; - width: 44px; height: 44px; - display: flex; align-items: center; justify-content: center; - background: rgba(91, 111, 222, 0.12); - border-radius: 10px; -} .card-label { font-size: 0.8rem; text-transform: uppercase; @@ -133,18 +123,6 @@ margin-top: 6px; } -/* ── Footer ───────────────────────────────────────────────── */ -.dash-footer { - text-align: center; - color: rgba(255, 255, 255, 0.2); - font-size: 0.7rem; - padding-bottom: 50px; - font-family: Arial, Helvetica, sans-serif; -} - -/* ── Wide card (span 2 columns) ────────────────────────────── */ - - /* ── Fastfetch text block ──────────────────────────────────── */ .card-fetch { margin: 12px 0 0; @@ -160,4 +138,3 @@ .dash-card.span2 { grid-column: span 2; } /* 3 per row */ .dash-card.span3 { grid-column: span 3; } /* 2 per row — stretched */ .dash-card.span6 { grid-column: span 6; } /* full width */ -.card-spacer { height: 32px; } \ No newline at end of file