website polish
This commit is contained in:
1 parent
a55e339ba8
commit
6b52f9ea14
16 files changed
+382
-481
No files matched your search
@@ -7,3 +7,5 @@ Thumbs.db
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
firebase.txt
|
firebase.txt
|
||||||
manipulate_boxes.md
|
manipulate_boxes.md
|
||||||
|
bot/
|
||||||
|
AGENTS.md
|
||||||
@@ -5,26 +5,26 @@
|
|||||||
```
|
```
|
||||||
website/
|
website/
|
||||||
├── index.html # Single-page entry point — all 5 tabs live here
|
├── 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)
|
├── _redirects # Netlify SPA rewrite rule (/* → /index.html)
|
||||||
├── js/
|
├── js/
|
||||||
│ ├── tabs.js # Client-side routing, tab switching, background video swap
|
│ ├── tabs.js # Client-side routing, tab switching, background video swap
|
||||||
│ ├── comments.js # Steam-style comment wall (Firestore: comments + stats)
|
│ ├── comments.js # Steam-style comment wall (Firestore: comments + stats)
|
||||||
│ ├── yuukabot.js # Live bot dashboard (Firestore: sysinfo/server)
|
│ ├── yuukabot.js # Live bot dashboard (Firestore: sysinfo/server)
|
||||||
│ ├── calendar.js # Event calendar (Firestore: calendar_events)
|
│ ├── 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/
|
├── style/
|
||||||
│ ├── styles.css # Global layout, nav bar, base typography
|
│ ├── styles.css # Global layout, nav bar, base typography
|
||||||
│ ├── comments.css # Steam-profile comment UI
|
│ ├── comments.css # Steam-profile comment UI
|
||||||
│ ├── yuukabot.css # Dashboard glassmorphism cards + grid
|
│ ├── yuukabot.css # Dashboard glassmorphism cards + grid
|
||||||
│ ├── calendar.css # Calendar grid, popups, sidebar
|
│ ├── calendar.css # Calendar grid, popups, sidebar
|
||||||
│ ├── gallery.css # Gallery grid + lightbox
|
│ ├── bucketlist.css # Lock screen + list panel
|
||||||
│ └── ahmet.css # Bucketlist tab button
|
│ └── gallery.css # Gallery grid + lightbox
|
||||||
├── gallery/ # 50 full-quality images + thumbnails/ subdirectory
|
├── gallery/ # Full-quality images, thumbnails/, and manifest.json
|
||||||
├── textures/ # Background MP4 videos + clickable character PNG
|
├── textures/ # Background MP4 videos + clickable character PNG
|
||||||
├── sounds/ # Click SFX (yuuka_click.mp3)
|
├── sounds/ # Click SFX (yuuka_click.mp3)
|
||||||
├── generate_thumbnails.py # Pillow script: resizes gallery/ images → thumbnails/
|
├── generate_thumbnails.py # Pillow script: builds thumbnails/ + manifest.json
|
||||||
└── firebase.txt # Firestore security rules (reference only)
|
└── firebase.txt # Firestore security rules (reference only, gitignored)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **No frameworks, no build step, no package manager.** Every file is served directly.
|
- **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.
|
- `_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.
|
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 `<section id="…" class="tab-content">`. IDs are kebab-case.
|
- **HTML:** Semantic elements where practical. Tab contents use `<section id="…" class="tab-content">`. IDs are kebab-case.
|
||||||
- **CSS:** Each tab has its own stylesheet under `style/`. Class names follow BEM-lite (`cal-day`, `dash-card`). CSS variables define layout knobs (`--dash-columns`, `--dash-gap`).
|
- **CSS:** Each tab has its own stylesheet under `style/`. Class names follow BEM-lite (`cal-day`, `dash-card`). CSS variables define layout knobs (`--dash-columns`, `--dash-gap`).
|
||||||
- **JavaScript:** Vanilla ES6+, no transpilation. Functions are `camelCase`, DOM-referencing IDs match kebab-case HTML IDs. Each script is self-contained with a `DOMContentLoaded` guard. Firebase config is repeated per-file (same values) — keep this pattern.
|
- **JavaScript:** Vanilla ES6+, no transpilation. Functions are `camelCase`, DOM-referencing IDs match kebab-case HTML IDs. Each script is self-contained with a `DOMContentLoaded` guard, and Firestore-backed scripts are wrapped in an IIFE so their helpers don't leak into (and clobber each other in) the global scope. Firebase config is repeated per-file (same values) — keep this pattern.
|
||||||
- **Whitespace:** 4-space indentation in JS/CSS, 2-space in HTML. No trailing semicolons are required but consistency within each file is expected.
|
- **Whitespace:** 4-space indentation in JS/CSS, 2-space in HTML. No trailing semicolons are required but consistency within each file is expected.
|
||||||
- No linters or formatters are configured.
|
- 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 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.
|
- `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.
|
- 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
|
## Adding a New Feature
|
||||||
|
|
||||||
|
|||||||
+54
-26
@@ -1,41 +1,54 @@
|
|||||||
|
"""Build gallery thumbnails + manifest.
|
||||||
|
|
||||||
|
Run after adding images to gallery/:
|
||||||
|
python generate_thumbnails.py
|
||||||
|
|
||||||
|
- Creates gallery/thumbnails/<name>.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
|
import os
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
def create_thumbnails(base_folder):
|
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif"}
|
||||||
if not os.path.exists(base_folder):
|
THUMB_SIZE = (400, 400)
|
||||||
print(f"Folder {base_folder} does not exist.")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
thumb_dir = os.path.join(base_folder, "thumbnails")
|
||||||
os.makedirs(thumb_dir, exist_ok=True)
|
os.makedirs(thumb_dir, exist_ok=True)
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
for filename in os.listdir(base_folder):
|
for filename in images:
|
||||||
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]
|
|
||||||
# Always save thumbnail as jpg
|
# Always save thumbnail as jpg
|
||||||
thumb_path = os.path.join(thumb_dir, f"{base_name}.jpg")
|
stem = os.path.splitext(filename)[0]
|
||||||
|
thumb_path = os.path.join(thumb_dir, f"{stem}.jpg")
|
||||||
# If thumbnail already exists, skip
|
|
||||||
if os.path.exists(thumb_path):
|
if os.path.exists(thumb_path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Image.open(filepath) as img:
|
with Image.open(os.path.join(base_folder, filename)) as img:
|
||||||
# Convert to RGB if necessary (e.g., for RGBA PNGs or GIFs)
|
# JPEG needs RGB (e.g. for RGBA PNGs or palette GIFs)
|
||||||
if img.mode != 'RGB':
|
if img.mode != "RGB":
|
||||||
img = img.convert('RGB')
|
img = img.convert("RGB")
|
||||||
|
img.thumbnail(THUMB_SIZE) # keeps aspect ratio
|
||||||
# Resize keeping aspect ratio
|
|
||||||
# Set a max dimension (e.g., 400x400)
|
|
||||||
img.thumbnail((400, 400))
|
|
||||||
img.save(thumb_path, "JPEG", quality=85)
|
img.save(thumb_path, "JPEG", quality=85)
|
||||||
count += 1
|
count += 1
|
||||||
print(f"Created thumbnail for {filename}")
|
print(f"Created thumbnail for {filename}")
|
||||||
@@ -44,5 +57,20 @@ def create_thumbnails(base_folder):
|
|||||||
|
|
||||||
print(f"Created {count} thumbnails in {base_folder}")
|
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__":
|
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)
|
||||||
+13
-21
@@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
<link rel="stylesheet" href="style/styles.css">
|
<link rel="stylesheet" href="style/styles.css">
|
||||||
<link rel="stylesheet" href="style/gallery.css">
|
<link rel="stylesheet" href="style/gallery.css">
|
||||||
<link rel="stylesheet" href="style/ahmet.css">
|
|
||||||
<link rel="stylesheet" href="style/comments.css">
|
<link rel="stylesheet" href="style/comments.css">
|
||||||
<link rel="stylesheet" href="style/yuukabot.css">
|
<link rel="stylesheet" href="style/yuukabot.css">
|
||||||
<link rel="stylesheet" href="style/calendar.css">
|
<link rel="stylesheet" href="style/calendar.css">
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
<source src="textures/cz_background.mp4" type="video/mp4">
|
<source src="textures/cz_background.mp4" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
<div id="top-line">
|
<div id="top-line">
|
||||||
<a href="index.html" id="home-btn"></a>
|
|
||||||
<button id="about-btn">About This Website</button>
|
<button id="about-btn">About This Website</button>
|
||||||
<button id="gallery-btn">Gallery</button>
|
<button id="gallery-btn">Gallery</button>
|
||||||
<button id="bucketlist-btn">Bucketlist</button>
|
<button id="bucketlist-btn">Bucketlist</button>
|
||||||
@@ -57,13 +56,8 @@
|
|||||||
<span id="comments-title"></span>
|
<span id="comments-title"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="firebase-setup-warning">
|
|
||||||
⚠️ <strong>Setup needed:</strong> Open <code>js/comments.js</code> and follow the setup instructions at the top
|
|
||||||
of the file.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Post form — at the top -->
|
<!-- Post form — at the top -->
|
||||||
<div class="steam-form-area" style="margin-bottom: 20px; border-top: none; padding-top: 0; margin-top: 20px;">
|
<div class="steam-form-area">
|
||||||
<p class="steam-form-label">Leave a comment:</p>
|
<p class="steam-form-label">Leave a comment:</p>
|
||||||
|
|
||||||
<!-- Avatar picker -->
|
<!-- Avatar picker -->
|
||||||
@@ -112,16 +106,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="gallery" class="tab-content">
|
<section id="gallery" class="tab-content">
|
||||||
<div class="gallery-grid">
|
|
||||||
</div>
|
|
||||||
<div id="gallery-container" class="gallery-grid"></div>
|
<div id="gallery-container" class="gallery-grid"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="bucketlist" class="tab-content"></section>
|
<section id="bucketlist" class="tab-content"></section>
|
||||||
|
|
||||||
<section id="yuukabot" class="tab-content">
|
<section id="yuukabot" class="tab-content">
|
||||||
<header class="dash-header">
|
<header class="dash-header">
|
||||||
<h1><span class="status-dot" id="dash-status-dot"></span>Yuuka Bot</h1>
|
<h1><span class="status-dot pending" id="dash-status-dot"></span>Yuuka Bot</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="dash-grid">
|
<div class="dash-grid">
|
||||||
@@ -129,7 +121,7 @@
|
|||||||
<div class="dash-card span2">
|
<div class="dash-card span2">
|
||||||
<div class="card-top"><span class="card-label">Uptime</span></div>
|
<div class="card-top"><span class="card-label">Uptime</span></div>
|
||||||
<p class="card-value" id="dash-uptime">—</p>
|
<p class="card-value" id="dash-uptime">—</p>
|
||||||
<div class="card-spacer"></div>
|
<span class="card-detail" id="dash-bot-uptime"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dash-card span2">
|
<div class="dash-card span2">
|
||||||
<div class="card-top"><span class="card-label">Disk</span></div>
|
<div class="card-top"><span class="card-label">Disk</span></div>
|
||||||
@@ -210,22 +202,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Lightbox (gallery) -->
|
||||||
|
<div id="lightbox" class="lightbox">
|
||||||
|
<span class="lightbox-close">×</span>
|
||||||
|
<img class="lightbox-content" id="lightbox-img" alt="">
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="js/gallery.js" defer></script>
|
<script src="js/gallery.js" defer></script>
|
||||||
<script src="js/tabs.js" defer></script>
|
<script src="js/tabs.js" defer></script>
|
||||||
|
|
||||||
<!-- Firebase SDK (loaded before comments.js) -->
|
<!-- Firebase SDK (must load before the Firestore-backed scripts below) -->
|
||||||
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js"></script>
|
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js"></script>
|
||||||
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore-compat.js"></script>
|
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore-compat.js"></script>
|
||||||
<script src="js/comments.js"></script>
|
<script src="js/comments.js"></script>
|
||||||
<script src="js/yuukabot.js"></script>
|
<script src="js/yuukabot.js"></script>
|
||||||
<script src="js/calendar.js"></script>
|
<script src="js/calendar.js"></script>
|
||||||
<script src="js/bucketlist.js"></script>
|
<script src="js/bucketlist.js"></script>
|
||||||
|
|
||||||
<!-- Lightbox -->
|
|
||||||
<div id="lightbox" class="lightbox">
|
|
||||||
<span class="lightbox-close">×</span>
|
|
||||||
<img class="lightbox-content" id="lightbox-img">
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+40
-34
@@ -1,43 +1,44 @@
|
|||||||
// Bucketlist tab — password-gated, real-time Firestore list
|
// Bucketlist tab — password-gated, real-time Firestore list
|
||||||
const BL_FB_CONFIG = {
|
(() => {
|
||||||
|
const FIREBASE_CONFIG = {
|
||||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||||
authDomain: "esef-514bf.firebaseapp.com",
|
authDomain: "esef-514bf.firebaseapp.com",
|
||||||
projectId: "esef-514bf",
|
projectId: "esef-514bf",
|
||||||
storageBucket: "esef-514bf.firebasestorage.app",
|
storageBucket: "esef-514bf.firebasestorage.app",
|
||||||
messagingSenderId: "353969651290",
|
messagingSenderId: "353969651290",
|
||||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
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 ---------------------------------------------------
|
// --- SHA-256 via Web Crypto ---------------------------------------------------
|
||||||
async function sha256(text) {
|
async function sha256(text) {
|
||||||
const buf = new TextEncoder().encode(text);
|
const buf = new TextEncoder().encode(text);
|
||||||
const hash = await crypto.subtle.digest("SHA-256", buf);
|
const hash = await crypto.subtle.digest("SHA-256", buf);
|
||||||
return Array.from(new Uint8Array(hash))
|
return Array.from(new Uint8Array(hash))
|
||||||
.map(b => b.toString(16).padStart(2, "0"))
|
.map(b => b.toString(16).padStart(2, "0"))
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Lock-screen HTML (only thing in DOM before auth) -------------------------
|
// --- Lock-screen HTML (only thing in DOM before auth) -------------------------
|
||||||
function showLock(container) {
|
function showLock(container) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="bl-lock">
|
<div class="bl-lock">
|
||||||
<div class="bl-lock-box">
|
<form class="bl-lock-box" id="bl-form">
|
||||||
<h2>Bucketlist</h2>
|
<h2>Bucketlist</h2>
|
||||||
<input type="text" id="bl-user" placeholder="Username" autofocus>
|
<input type="text" id="bl-user" placeholder="Username" autocomplete="username">
|
||||||
<input type="password" id="bl-pass" placeholder="Password">
|
<input type="password" id="bl-pass" placeholder="Password" autocomplete="current-password">
|
||||||
<button id="bl-submit">Unlock</button>
|
<button type="submit" id="bl-submit">Unlock</button>
|
||||||
<p id="bl-err"></p>
|
<p id="bl-err"></p>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Build the real panel + attach Firestore after auth -----------------------
|
// --- Build the real panel + attach Firestore after auth -----------------------
|
||||||
function unlock(container, db, hash) {
|
function unlock(container, db, hash) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="bl-panel">
|
<div class="bl-panel">
|
||||||
<div class="bl-list" id="bl-list">
|
<div class="bl-list" id="bl-list">
|
||||||
@@ -68,15 +69,14 @@ function unlock(container, db, hash) {
|
|||||||
}, () => {
|
}, () => {
|
||||||
if (listEl) listEl.innerHTML = '<p class="bl-empty">Could not load.</p>';
|
if (listEl) listEl.innerHTML = '<p class="bl-empty">Could not load.</p>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(listEl, items) {
|
function render(listEl, items) {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
listEl.innerHTML = '<p class="bl-empty">Nothing here yet.</p>';
|
listEl.innerHTML = '<p class="bl-empty">Nothing here yet.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: item.done adds .bl-done class — hook for completed-item styling (CSS still TODO)
|
|
||||||
listEl.innerHTML = items.map(item =>
|
listEl.innerHTML = items.map(item =>
|
||||||
'<div class="bl-item' + (item.done ? ' bl-done' : '') + '">' +
|
'<div class="bl-item' + (item.done ? ' bl-done' : '') + '">' +
|
||||||
'<div class="bl-title">' + esc(item.title) + '</div>' +
|
'<div class="bl-title">' + esc(item.title) + '</div>' +
|
||||||
@@ -84,23 +84,23 @@ function render(listEl, items) {
|
|||||||
(item.description ? '<div class="bl-desc">' + esc(item.description) + '</div>' : '') +
|
(item.description ? '<div class="bl-desc">' + esc(item.description) + '</div>' : '') +
|
||||||
'</div>'
|
'</div>'
|
||||||
).join("");
|
).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
if (!s) return "";
|
if (!s) return "";
|
||||||
return String(s).replace(/&/g,"&").replace(/</g,"<")
|
return String(s).replace(/&/g,"&").replace(/</g,"<")
|
||||||
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// ENTRY
|
// ENTRY
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const tab = document.getElementById("bucketlist");
|
const tab = document.getElementById("bucketlist");
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
if (!firebase.apps.length) firebase.initializeApp(BL_FB_CONFIG);
|
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
|
|
||||||
const container = tab; // the <section> itself — nothing else exists inside it
|
const container = tab; // the <section> itself — nothing else exists inside it
|
||||||
@@ -123,7 +123,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
async function tryUnlock() {
|
async function tryUnlock() {
|
||||||
const user = userInput.value.trim();
|
const user = userInput.value.trim();
|
||||||
const pass = passInput.value;
|
const pass = passInput.value;
|
||||||
if (!user || !pass) return;
|
if (!user || !pass) {
|
||||||
|
(user ? passInput : userInput).focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
errEl.textContent = "";
|
errEl.textContent = "";
|
||||||
@@ -146,7 +149,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitBtn.onclick = tryUnlock;
|
// Enter in either field submits the form
|
||||||
userInput.onkeydown = e => { if (e.key === "Enter") passInput.focus(); };
|
document.getElementById("bl-form").onsubmit = e => {
|
||||||
passInput.onkeydown = e => { if (e.key === "Enter") tryUnlock(); };
|
e.preventDefault();
|
||||||
});
|
tryUnlock();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})();
|
||||||
+64
-60
@@ -1,86 +1,67 @@
|
|||||||
// Calendar tab — timezone-aware (auto-converts to your local time)
|
// Calendar tab — timezone-aware (auto-converts to your local time)
|
||||||
const CAL_FB_CONFIG = {
|
(() => {
|
||||||
|
const FIREBASE_CONFIG = {
|
||||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||||
authDomain: "esef-514bf.firebaseapp.com",
|
authDomain: "esef-514bf.firebaseapp.com",
|
||||||
projectId: "esef-514bf",
|
projectId: "esef-514bf",
|
||||||
storageBucket: "esef-514bf.firebasestorage.app",
|
storageBucket: "esef-514bf.firebasestorage.app",
|
||||||
messagingSenderId: "353969651290",
|
messagingSenderId: "353969651290",
|
||||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
||||||
};
|
};
|
||||||
|
|
||||||
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
const MONTHS = ["January","February","March","April","May","June",
|
const MONTHS = ["January","February","March","April","May","June",
|
||||||
"July","August","September","October","November","December"];
|
"July","August","September","October","November","December"];
|
||||||
|
|
||||||
function fmtTime(utcIso) {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
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");
|
const tab = document.getElementById("calendar");
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
if (!firebase.apps.length) firebase.initializeApp(CAL_FB_CONFIG);
|
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
|
|
||||||
let allEvents = [];
|
let allEvents = [];
|
||||||
let viewDate = new Date();
|
const now = new Date();
|
||||||
|
let viewYear = now.getFullYear();
|
||||||
|
let viewMonth = now.getMonth();
|
||||||
|
|
||||||
const gridEl = document.getElementById("cal-grid");
|
const gridEl = document.getElementById("cal-grid");
|
||||||
const navEl = document.getElementById("cal-nav-title");
|
const navEl = document.getElementById("cal-nav-title");
|
||||||
const sidebarEl = document.getElementById("cal-sidebar-list");
|
const sidebarEl = document.getElementById("cal-sidebar-list");
|
||||||
|
|
||||||
db.collection("calendar_events").onSnapshot(snap => {
|
db.collection("calendar_events").onSnapshot(snap => {
|
||||||
allEvents = [];
|
allEvents = snap.docs.map(doc => toEvent(doc.data()));
|
||||||
snap.forEach(doc => {
|
allEvents.sort((a, b) => a.when - b.when);
|
||||||
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();
|
render();
|
||||||
}, () => {
|
}, () => {
|
||||||
if (gridEl) gridEl.innerHTML = '<p class="cal-side-empty">Could not load.</p>';
|
gridEl.innerHTML = '<p class="cal-side-empty">Could not load.</p>';
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("cal-prev").onclick = () => { viewDate.setMonth(viewDate.getMonth()-1); render(); };
|
document.getElementById("cal-prev").onclick = () => shiftMonth(-1);
|
||||||
document.getElementById("cal-next").onclick = () => { viewDate.setMonth(viewDate.getMonth()+1); render(); };
|
document.getElementById("cal-next").onclick = () => shiftMonth(1);
|
||||||
|
|
||||||
// Auto-refresh every 60s so "Upcoming" stays current as time passes
|
// Auto-refresh every 60s so "Upcoming" stays current as time passes
|
||||||
setInterval(render, 60000);
|
setInterval(render, 60000);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const year = viewDate.getFullYear();
|
navEl.textContent = MONTHS[viewMonth] + " " + viewYear;
|
||||||
const month = viewDate.getMonth();
|
|
||||||
navEl.textContent = MONTHS[month] + " " + year;
|
|
||||||
|
|
||||||
const byDate = {};
|
const byDate = {};
|
||||||
allEvents.forEach(e => {
|
allEvents.forEach(e => {
|
||||||
if (!byDate[e.date]) byDate[e.date] = [];
|
(byDate[e.date] = byDate[e.date] || []).push(e);
|
||||||
byDate[e.date].push(e);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstDay = new Date(year, month, 1).getDay();
|
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
||||||
const today = new Date();
|
const todayStr = ymd(new Date());
|
||||||
const todayStr = today.getFullYear() + "-" +
|
|
||||||
String(today.getMonth()+1).padStart(2,"0") + "-" +
|
|
||||||
String(today.getDate()).padStart(2,"0");
|
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
WEEKDAYS.forEach(d => html += '<div class="cal-day-header">' + d + "</div>");
|
WEEKDAYS.forEach(d => html += '<div class="cal-day-header">' + d + "</div>");
|
||||||
@@ -90,15 +71,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let d = 1; d <= daysInMonth; d++) {
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
const dateStr = year + "-" +
|
const dateStr = ymd(new Date(viewYear, viewMonth, d));
|
||||||
String(month+1).padStart(2,"0") + "-" +
|
|
||||||
String(d).padStart(2,"0");
|
|
||||||
const dayEvents = byDate[dateStr] || [];
|
const dayEvents = byDate[dateStr] || [];
|
||||||
const isToday = dateStr === todayStr;
|
|
||||||
const hasEvents = dayEvents.length > 0;
|
const hasEvents = dayEvents.length > 0;
|
||||||
|
|
||||||
html += '<div class="cal-day' +
|
html += '<div class="cal-day' +
|
||||||
(isToday ? " today" : "") +
|
(dateStr === todayStr ? " today" : "") +
|
||||||
(hasEvents ? " has-events" : "") + '">';
|
(hasEvents ? " has-events" : "") + '">';
|
||||||
html += '<div class="day-num">' + d + "</div>";
|
html += '<div class="day-num">' + d + "</div>";
|
||||||
|
|
||||||
@@ -107,9 +85,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
html += '<div class="cal-popup"><h4>' + dateStr + "</h4>";
|
html += '<div class="cal-popup"><h4>' + dateStr + "</h4>";
|
||||||
dayEvents.forEach(e => {
|
dayEvents.forEach(e => {
|
||||||
html += '<div class="cal-popup-item">';
|
html += '<div class="cal-popup-item">';
|
||||||
if (e.time) {
|
if (e.time) html += '<span class="pop-time">' + esc(e.time) + "</span> ";
|
||||||
html += '<span class="pop-time">' + fmtTime(e.utc) + "</span> ";
|
|
||||||
}
|
|
||||||
html += esc(e.title);
|
html += esc(e.title);
|
||||||
if (e.description) html += '<div class="pop-desc">' + esc(e.description) + "</div>";
|
if (e.description) html += '<div class="pop-desc">' + esc(e.description) + "</div>";
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
@@ -121,25 +97,53 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
gridEl.innerHTML = html;
|
gridEl.innerHTML = html;
|
||||||
|
|
||||||
const nowISO = new Date().toISOString();
|
const nowMs = Date.now();
|
||||||
const upcoming = allEvents.filter(e => e._sort >= nowISO);
|
const upcoming = allEvents.filter(e => e.when >= nowMs);
|
||||||
if (upcoming.length === 0) {
|
if (upcoming.length === 0) {
|
||||||
sidebarEl.innerHTML = '<p class="cal-side-empty">No upcoming events.<br>Use !book in Discord.</p>';
|
sidebarEl.innerHTML = '<p class="cal-side-empty">No upcoming events.<br>Use !book in Discord.</p>';
|
||||||
} else {
|
} else {
|
||||||
sidebarEl.innerHTML = upcoming.slice(0, 15).map(e =>
|
sidebarEl.innerHTML = upcoming.slice(0, 15).map(e =>
|
||||||
'<div class="cal-side-event">' +
|
'<div class="cal-side-event">' +
|
||||||
'<div class="se-date">' + e.date +
|
'<div class="se-date">' + esc(e.date) + (e.time ? " " + esc(e.time) : "") + "</div>" +
|
||||||
(e.time ? " " + fmtTime(e.utc) : "") + "</div>" +
|
|
||||||
'<div class="se-title">' + esc(e.title) + "</div>" +
|
'<div class="se-title">' + esc(e.title) + "</div>" +
|
||||||
(e.description ? '<div class="se-desc">' + esc(e.description) + "</div>" : "") +
|
(e.description ? '<div class="se-desc">' + esc(e.description) + "</div>" : "") +
|
||||||
"</div>"
|
"</div>"
|
||||||
).join("");
|
).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) {
|
function esc(s) {
|
||||||
if (!s) return "";
|
if (!s) return "";
|
||||||
return String(s).replace(/&/g,"&").replace(/</g,"<")
|
return String(s).replace(/&/g,"&").replace(/</g,"<")
|
||||||
.replace(/>/g,">").replace(/"/g,""");
|
.replace(/>/g,">").replace(/"/g,""");
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
+84
-132
@@ -1,21 +1,23 @@
|
|||||||
const FIREBASE_CONFIG = {
|
// About tab — Steam-style comment wall + global click counter
|
||||||
|
(() => {
|
||||||
|
const FIREBASE_CONFIG = {
|
||||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||||
authDomain: "esef-514bf.firebaseapp.com",
|
authDomain: "esef-514bf.firebaseapp.com",
|
||||||
projectId: "esef-514bf",
|
projectId: "esef-514bf",
|
||||||
storageBucket: "esef-514bf.firebasestorage.app",
|
storageBucket: "esef-514bf.firebasestorage.app",
|
||||||
messagingSenderId: "353969651290",
|
messagingSenderId: "353969651290",
|
||||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
||||||
};
|
};
|
||||||
|
|
||||||
// ================================================================
|
const MAX_CHARS = 300;
|
||||||
|
const MAX_NAME = 32;
|
||||||
const MAX_CHARS = 300;
|
const COMMENTS_PER_PAGE = 5;
|
||||||
const COLLECTION = "comments";
|
const COLLECTION = "comments";
|
||||||
|
const DEFAULT_COLOR = "#5b6fde";
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
const COUNT_COLOR = "#364858";
|
||||||
|
const COUNT_LOW_COLOR = "#e05555";
|
||||||
const configured = FIREBASE_CONFIG.apiKey !== "YOUR_API_KEY";
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const form = document.getElementById('comment-form');
|
const form = document.getElementById('comment-form');
|
||||||
const nameInput = document.getElementById('comment-name');
|
const nameInput = document.getElementById('comment-name');
|
||||||
const msgInput = document.getElementById('comment-message');
|
const msgInput = document.getElementById('comment-message');
|
||||||
@@ -23,46 +25,45 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const charCountEl = document.getElementById('char-count');
|
const charCountEl = document.getElementById('char-count');
|
||||||
const listEl = document.getElementById('comments-list');
|
const listEl = document.getElementById('comments-list');
|
||||||
const titleEl = document.getElementById('comments-title');
|
const titleEl = document.getElementById('comments-title');
|
||||||
|
const paginationEl = document.getElementById('comments-pagination');
|
||||||
const colorInput = document.getElementById('comment-avatar-color');
|
const colorInput = document.getElementById('comment-avatar-color');
|
||||||
const b64Input = document.getElementById('comment-avatar-b64');
|
const b64Input = document.getElementById('comment-avatar-b64');
|
||||||
const pfpUpload = document.getElementById('pfp-upload');
|
const pfpUpload = document.getElementById('pfp-upload');
|
||||||
const pfpPreview = document.getElementById('pfp-preview');
|
const pfpPreview = document.getElementById('pfp-preview');
|
||||||
const pfpClear = document.getElementById('pfp-clear');
|
const pfpClear = document.getElementById('pfp-clear');
|
||||||
const warning = document.getElementById('firebase-setup-warning');
|
|
||||||
|
|
||||||
if (!form || !listEl) return;
|
if (!form || !listEl) return;
|
||||||
|
|
||||||
|
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||||
|
const db = firebase.firestore();
|
||||||
|
|
||||||
// ── Avatar colour picker ─────────────────────────────────────
|
// ── Avatar colour picker ─────────────────────────────────────
|
||||||
document.querySelectorAll('.av-opt').forEach(btn => {
|
const colorOpts = document.querySelectorAll('.av-opt');
|
||||||
|
colorOpts.forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.av-opt').forEach(b => b.classList.remove('selected'));
|
colorOpts.forEach(b => b.classList.remove('selected'));
|
||||||
btn.classList.add('selected');
|
btn.classList.add('selected');
|
||||||
colorInput.value = btn.dataset.color;
|
colorInput.value = btn.dataset.color;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Custom PFP Upload ────────────────────────────────────────
|
// ── Custom PFP upload (center-cropped + shrunk to 64×64 JPEG) ─
|
||||||
if (pfpUpload) {
|
pfpUpload.addEventListener('change', () => {
|
||||||
pfpUpload.addEventListener('change', (e) => {
|
const file = pfpUpload.files[0];
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = () => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
// Shrink to max 64x64 to save space
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
canvas.width = 64;
|
canvas.width = 64;
|
||||||
canvas.height = 64;
|
canvas.height = 64;
|
||||||
|
|
||||||
// Center and crop
|
|
||||||
const size = Math.min(img.width, img.height);
|
const size = Math.min(img.width, img.height);
|
||||||
const sx = (img.width - size) / 2;
|
const sx = (img.width - size) / 2;
|
||||||
const sy = (img.height - size) / 2;
|
const sy = (img.height - size) / 2;
|
||||||
|
canvas.getContext('2d').drawImage(img, sx, sy, size, size, 0, 0, 64, 64);
|
||||||
ctx.drawImage(img, sx, sy, size, size, 0, 0, 64, 64);
|
|
||||||
|
|
||||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
||||||
b64Input.value = dataUrl;
|
b64Input.value = dataUrl;
|
||||||
@@ -70,7 +71,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pfpPreview.style.display = 'block';
|
pfpPreview.style.display = 'block';
|
||||||
pfpClear.style.display = 'inline-flex';
|
pfpClear.style.display = 'inline-flex';
|
||||||
};
|
};
|
||||||
img.src = event.target.result;
|
img.src = reader.result;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
@@ -82,43 +83,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pfpPreview.style.backgroundImage = 'none';
|
pfpPreview.style.backgroundImage = 'none';
|
||||||
pfpClear.style.display = 'none';
|
pfpClear.style.display = 'none';
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// ── Char counter ─────────────────────────────────────────────
|
// ── Char counter ─────────────────────────────────────────────
|
||||||
charCountEl.textContent = MAX_CHARS;
|
function updateCharCount() {
|
||||||
msgInput.addEventListener('input', () => {
|
|
||||||
const left = MAX_CHARS - msgInput.value.length;
|
const left = MAX_CHARS - msgInput.value.length;
|
||||||
charCountEl.textContent = left;
|
charCountEl.textContent = left;
|
||||||
charCountEl.style.color = left < 40 ? '#e05555' : '#364858';
|
charCountEl.style.color = left < 40 ? COUNT_LOW_COLOR : COUNT_COLOR;
|
||||||
});
|
|
||||||
|
|
||||||
// ── 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;
|
|
||||||
}
|
}
|
||||||
|
msgInput.addEventListener('input', updateCharCount);
|
||||||
|
updateCharCount();
|
||||||
|
|
||||||
// Firebase is configured — hide the setup warning
|
// ── Real-time feed + pagination ──────────────────────────────
|
||||||
if (warning) warning.style.display = 'none';
|
|
||||||
|
|
||||||
// ── Firebase init ────────────────────────────────────────────
|
|
||||||
firebase.initializeApp(FIREBASE_CONFIG);
|
|
||||||
const db = firebase.firestore();
|
|
||||||
|
|
||||||
// ── Real-time feed & Pagination ──────────────────────────────
|
|
||||||
let allComments = [];
|
let allComments = [];
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
const COMMENTS_PER_PAGE = 5;
|
|
||||||
const paginationEl = document.getElementById('comments-pagination');
|
|
||||||
|
|
||||||
function renderPage() {
|
function renderPage() {
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
if (paginationEl) paginationEl.innerHTML = '';
|
paginationEl.innerHTML = '';
|
||||||
|
|
||||||
if (allComments.length === 0) {
|
if (allComments.length === 0) {
|
||||||
listEl.innerHTML = '<p class="no-comments">No messages yet — be the first!</p>';
|
listEl.innerHTML = '<p class="no-comments">No messages yet — be the first!</p>';
|
||||||
@@ -126,17 +107,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = Math.ceil(allComments.length / COMMENTS_PER_PAGE);
|
const totalPages = Math.ceil(allComments.length / COMMENTS_PER_PAGE);
|
||||||
if (currentPage > totalPages) currentPage = totalPages;
|
currentPage = Math.min(Math.max(currentPage, 1), totalPages);
|
||||||
if (currentPage < 1) currentPage = 1;
|
|
||||||
|
|
||||||
const start = (currentPage - 1) * COMMENTS_PER_PAGE;
|
const start = (currentPage - 1) * COMMENTS_PER_PAGE;
|
||||||
const end = start + COMMENTS_PER_PAGE;
|
allComments.slice(start, start + COMMENTS_PER_PAGE)
|
||||||
const slice = allComments.slice(start, end);
|
.forEach(data => listEl.appendChild(buildBubble(data)));
|
||||||
|
|
||||||
slice.forEach(data => listEl.appendChild(buildBubble(data)));
|
if (totalPages > 1) {
|
||||||
|
|
||||||
// Build pagination controls
|
|
||||||
if (totalPages > 1 && paginationEl) {
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'steam-page-btn' + (i === currentPage ? ' active' : '');
|
btn.className = 'steam-page-btn' + (i === currentPage ? ' active' : '');
|
||||||
@@ -150,35 +127,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Global Click Counter ─────────────────────────────────────
|
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 = '<p class="no-comments">Could not load comments.</p>';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Global click counter ─────────────────────────────────────
|
||||||
const clickBtn = document.getElementById('yuuka-click-btn');
|
const clickBtn = document.getElementById('yuuka-click-btn');
|
||||||
const clickDisplay = document.getElementById('click-counter-display');
|
const clickDisplay = document.getElementById('click-counter-display');
|
||||||
const clickSound = new Audio('sounds/yuuka_click.mp3');
|
const clickSound = new Audio('sounds/yuuka_click.mp3');
|
||||||
|
|
||||||
if (clickBtn && clickDisplay && configured) {
|
if (clickBtn && clickDisplay) {
|
||||||
const statsRef = db.collection('stats').doc('global_clicks');
|
const statsRef = db.collection('stats').doc('global_clicks');
|
||||||
|
|
||||||
// Listen to changes in real-time
|
// Firestore applies local writes to this listener instantly,
|
||||||
|
// so clicks show up immediately without a manual optimistic update.
|
||||||
statsRef.onSnapshot(doc => {
|
statsRef.onSnapshot(doc => {
|
||||||
if (doc.exists) {
|
const count = doc.exists ? doc.data().count : 0;
|
||||||
clickDisplay.textContent = doc.data().count.toLocaleString();
|
clickDisplay.textContent = (count || 0).toLocaleString();
|
||||||
} else {
|
|
||||||
clickDisplay.textContent = '0';
|
|
||||||
}
|
|
||||||
}, err => {
|
}, err => {
|
||||||
console.error("Click counter error:", err);
|
console.error("Click counter error:", err);
|
||||||
clickDisplay.textContent = 'Error';
|
clickDisplay.textContent = 'Error';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Increment on click
|
|
||||||
clickBtn.addEventListener('click', () => {
|
clickBtn.addEventListener('click', () => {
|
||||||
// Play sound
|
clickSound.currentTime = 0; // restart so rapid clicks all play
|
||||||
clickSound.currentTime = 0; // Reset to allow rapid clicking
|
clickSound.play().catch(() => {});
|
||||||
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({
|
statsRef.set({
|
||||||
count: firebase.firestore.FieldValue.increment(1)
|
count: firebase.firestore.FieldValue.increment(1)
|
||||||
@@ -186,34 +166,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
db.collection(COLLECTION)
|
|
||||||
.orderBy('timestamp', 'desc')
|
|
||||||
.onSnapshot(snap => {
|
|
||||||
if (titleEl) {
|
|
||||||
titleEl.textContent = snap.size > 0 ? `(${snap.size})` : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
allComments = [];
|
|
||||||
snap.forEach(doc => allComments.push(doc.data()));
|
|
||||||
|
|
||||||
renderPage();
|
|
||||||
|
|
||||||
}, err => {
|
|
||||||
console.error(err);
|
|
||||||
listEl.innerHTML = '<p class="no-comments">Could not load comments. Check your Firebase config.</p>';
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Submit ───────────────────────────────────────────────────
|
// ── Submit ───────────────────────────────────────────────────
|
||||||
form.addEventListener('submit', async e => {
|
form.addEventListener('submit', async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const name = clean(nameInput.value.trim()).slice(0, 32) || 'Anonymous';
|
const name = nameInput.value.trim().slice(0, MAX_NAME) || 'Anonymous';
|
||||||
const message = clean(msgInput.value.trim()).slice(0, MAX_CHARS);
|
const message = msgInput.value.trim().slice(0, MAX_CHARS);
|
||||||
const color = colorInput.value || '#5b6fde';
|
const color = colorInput.value || DEFAULT_COLOR;
|
||||||
const avatarUrl = b64Input.value;
|
const avatarUrl = b64Input.value;
|
||||||
|
|
||||||
if (!message) { msgInput.focus(); return; }
|
if (!message) { msgInput.focus(); return; }
|
||||||
|
|
||||||
|
const label = submitBtn.textContent;
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.textContent = 'Posting...';
|
submitBtn.textContent = 'Posting...';
|
||||||
|
|
||||||
@@ -224,78 +188,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
nameInput.value = '';
|
nameInput.value = '';
|
||||||
msgInput.value = '';
|
msgInput.value = '';
|
||||||
charCountEl.textContent = MAX_CHARS;
|
updateCharCount();
|
||||||
charCountEl.style.color = '#364858';
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Failed to post. Try again!');
|
alert('Failed to post. Try again!');
|
||||||
} finally {
|
} finally {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.textContent = 'POST';
|
submitBtn.textContent = label;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Build a single comment bubble (Steam layout) ─────────────────
|
// ── Build a single comment bubble (Steam layout) ─────────────────
|
||||||
|
function buildBubble(data) {
|
||||||
function buildBubble(data) {
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'comment-bubble';
|
div.className = 'comment-bubble';
|
||||||
|
|
||||||
|
// Pending server timestamps are null until the write lands
|
||||||
const ts = data.timestamp ? data.timestamp.toDate() : new Date();
|
const ts = data.timestamp ? data.timestamp.toDate() : new Date();
|
||||||
const timeStr = ts.toLocaleDateString('en-US', {
|
const timeStr = ts.toLocaleDateString('en-US', {
|
||||||
year: 'numeric', month: 'short', day: 'numeric',
|
year: 'numeric', month: 'short', day: 'numeric',
|
||||||
hour: '2-digit', minute: '2-digit'
|
hour: '2-digit', minute: '2-digit'
|
||||||
});
|
});
|
||||||
|
|
||||||
const color = data.color || '#64748b';
|
const name = legacyUnescape(data.name) || 'Anonymous';
|
||||||
const initial = (data.name || '?').charAt(0).toUpperCase();
|
const message = legacyUnescape(data.message);
|
||||||
|
|
||||||
let avatarHtml = '';
|
let avatarHtml;
|
||||||
if (data.avatarUrl) {
|
if (typeof data.avatarUrl === 'string' && data.avatarUrl.startsWith('data:image/')) {
|
||||||
avatarHtml = `<img src="${esc(data.avatarUrl)}" class="comment-avatar" alt="Avatar">`;
|
avatarHtml = `<img src="${esc(data.avatarUrl)}" class="comment-avatar" alt="">`;
|
||||||
} else {
|
} else {
|
||||||
avatarHtml = `
|
const color = /^#[0-9a-f]{6}$/i.test(data.color) ? data.color : '#64748b';
|
||||||
<div class="comment-avatar" style="background:${esc(color)}">
|
avatarHtml = `<div class="comment-avatar" style="background:${color}">${esc(name.charAt(0).toUpperCase())}</div>`;
|
||||||
${esc(initial)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
${avatarHtml}
|
${avatarHtml}
|
||||||
<div class="comment-body">
|
<div class="comment-body">
|
||||||
<div class="comment-header">
|
<div class="comment-header">
|
||||||
<span class="comment-name">${esc(data.name)}</span>
|
<span class="comment-name">${esc(name)}</span>
|
||||||
<span class="comment-time">${timeStr}</span>
|
<span class="comment-time">${timeStr}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="comment-text">${esc(data.message)}</p>
|
<p class="comment-text">${esc(message)}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Demo bubbles shown before Firebase is set up ─────────────────
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function showDemo(list) {
|
// Older comments were stored pre-escaped (< and > only), which made
|
||||||
const demos = [
|
// them render as literal "<" once escaped again. Undo that here.
|
||||||
{ name: 'Visitor', message: 'cool site lol', color: '#3b82f6', timestamp: { toDate: () => new Date(Date.now() - 60000) } },
|
function legacyUnescape(str) {
|
||||||
{ name: 'Ahmet', message: 'when is the gallery getting updated', color: '#10b981', timestamp: { toDate: () => new Date(Date.now() - 300000) } },
|
if (!str) return '';
|
||||||
{ name: 'Someone', message: 'Ahmet museum is peak', color: '#8b5cf6', timestamp: { toDate: () => new Date(Date.now() - 800000) } },
|
return String(str).replace(/</g, '<').replace(/>/g, '>');
|
||||||
];
|
}
|
||||||
list.innerHTML = '';
|
|
||||||
demos.forEach(d => list.appendChild(buildBubble(d)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────
|
function esc(str) {
|
||||||
|
|
||||||
function clean(str) {
|
|
||||||
return str.replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function esc(str) {
|
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
return String(str)
|
return String(str)
|
||||||
.replace(/&/g, '&').replace(/</g, '<')
|
.replace(/&/g, '&').replace(/</g, '<')
|
||||||
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
+36
-59
@@ -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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const container = document.getElementById('gallery-container');
|
||||||
const lightbox = document.getElementById('lightbox');
|
const lightbox = document.getElementById('lightbox');
|
||||||
const lightboxImg = document.getElementById('lightbox-img');
|
const lightboxImg = document.getElementById('lightbox-img');
|
||||||
const closeBtn = document.querySelector('.lightbox-close');
|
|
||||||
|
|
||||||
// Close lightbox when clicking the X
|
if (!container || !lightbox) return;
|
||||||
if (closeBtn) {
|
|
||||||
closeBtn.onclick = () => {
|
|
||||||
lightbox.style.display = "none";
|
|
||||||
lightboxImg.src = ""; // Clear source to stop GIFs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close lightbox when clicking outside the image
|
const openLightbox = (src) => {
|
||||||
if (lightbox) {
|
lightboxImg.src = src;
|
||||||
lightbox.onclick = (e) => {
|
lightbox.style.display = 'block';
|
||||||
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 closeLightbox = () => {
|
||||||
const loadGallery = (containerId, folderName, totalImages) => {
|
lightbox.style.display = 'none';
|
||||||
const container = document.getElementById(containerId);
|
lightboxImg.src = ''; // Clear source to stop GIFs
|
||||||
if (!container) return;
|
};
|
||||||
|
|
||||||
for (let i = 1; i <= totalImages; i++) {
|
// 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');
|
const img = document.createElement('img');
|
||||||
img.alt = `${folderName} Image ${i}`;
|
img.src = thumbPath(file);
|
||||||
|
img.alt = `Gallery image ${i + 1}`;
|
||||||
// Load the thumbnail instead of the full image
|
img.loading = 'lazy';
|
||||||
img.src = `${folderName}/thumbnails/${i}.jpg`;
|
img.onclick = () => openLightbox(`gallery/${file}`);
|
||||||
|
|
||||||
// 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);
|
container.appendChild(img);
|
||||||
}
|
});
|
||||||
};
|
})
|
||||||
|
.catch(err => console.error('Could not load gallery manifest:', err));
|
||||||
// Load your original Gallery
|
|
||||||
loadGallery('gallery-container', 'gallery', 50);
|
|
||||||
});
|
});
|
||||||
+7
-18
@@ -59,15 +59,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Navigate to a tab (updates URL + shows it) ────────
|
// ── Navigate to a tab (updates URL + shows it) ────────
|
||||||
function navigateTo(tabId, push = true) {
|
function navigateTo(tabId) {
|
||||||
showTab(tabId);
|
showTab(tabId);
|
||||||
if (push) {
|
window.scrollTo(0, 0);
|
||||||
history.pushState({ tab: tabId }, '', tabToPath(tabId));
|
history.pushState({ tab: tabId }, '', tabToPath(tabId));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ── Initial load: show tab from URL ────────────────────
|
// ── 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 ──────────────────────────────────
|
// ── Tab button clicks ──────────────────────────────────
|
||||||
const buttons = document.querySelectorAll('#top-line button');
|
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 ─────────────────────
|
// ── Back / forward browser buttons ─────────────────────
|
||||||
window.addEventListener('popstate', (e) => {
|
window.addEventListener('popstate', (e) => {
|
||||||
if (e.state && e.state.tab) {
|
showTab(e.state && e.state.tab ? e.state.tab : pathToTab(window.location.pathname));
|
||||||
showTab(e.state.tab);
|
|
||||||
} else {
|
|
||||||
showTab(pathToTab(window.location.pathname));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
+58
-39
@@ -1,80 +1,99 @@
|
|||||||
// Yuuka Bot Dashboard — live Firestore reader
|
// Yuuka Bot Dashboard — live Firestore reader
|
||||||
const YUUKA_FIREBASE_CONFIG = {
|
(() => {
|
||||||
|
const FIREBASE_CONFIG = {
|
||||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||||
authDomain: "esef-514bf.firebaseapp.com",
|
authDomain: "esef-514bf.firebaseapp.com",
|
||||||
projectId: "esef-514bf",
|
projectId: "esef-514bf",
|
||||||
storageBucket: "esef-514bf.firebasestorage.app",
|
storageBucket: "esef-514bf.firebasestorage.app",
|
||||||
messagingSenderId: "353969651290",
|
messagingSenderId: "353969651290",
|
||||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
// 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;
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const dash = document.getElementById("yuukabot");
|
const dash = document.getElementById("yuukabot");
|
||||||
if (!dash) return;
|
if (!dash) return;
|
||||||
|
|
||||||
if (!firebase.apps.length) firebase.initializeApp(YUUKA_FIREBASE_CONFIG);
|
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
|
|
||||||
db.collection("sysinfo").doc("server").onSnapshot(
|
|
||||||
(doc) => {
|
|
||||||
if (!doc.exists) return;
|
|
||||||
const d = doc.data();
|
|
||||||
|
|
||||||
// Online dot
|
|
||||||
const dot = document.getElementById("dash-status-dot");
|
const dot = document.getElementById("dash-status-dot");
|
||||||
if (dot) dot.className = d.online ? "status-dot" : "status-dot offline";
|
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;
|
||||||
|
|
||||||
// Uptime
|
|
||||||
setText("dash-uptime", d.uptime);
|
setText("dash-uptime", d.uptime);
|
||||||
|
setText("dash-bot-uptime", d.botUptime ? "bot up " + d.botUptime : "");
|
||||||
|
|
||||||
// CPU, RAM, Disk, Swap — bars
|
// CPU, RAM, Disk, Swap — value + bar
|
||||||
setBar("dash-cpu-val", "dash-cpu-bar", null, d.cpu, "%", "cpu");
|
setBar("dash-cpu-val", "dash-cpu-bar", d.cpu);
|
||||||
setBar("dash-ram-val", "dash-ram-bar", null, d.ram, "%", "ram");
|
setBar("dash-ram-val", "dash-ram-bar", d.ram);
|
||||||
setText("dash-ram-detail", d.ramUsed ? d.ramUsed + " / " + d.ramTotal : "");
|
setBar("dash-disk-val", "dash-disk-bar", d.disk);
|
||||||
setBar("dash-disk-val", "dash-disk-bar", null, d.disk, "%", "disk");
|
setBar("dash-swap-val", "dash-swap-bar", d.swap);
|
||||||
setText("dash-disk-detail", d.diskUsed ? d.diskUsed + " / " + d.diskTotal : "");
|
setText("dash-ram-detail", usedOfTotal(d.ramUsed, d.ramTotal));
|
||||||
setBar("dash-swap-val", "dash-swap-bar", null, d.swap, "%", "swap");
|
setText("dash-disk-detail", usedOfTotal(d.diskUsed, d.diskTotal));
|
||||||
setText("dash-swap-detail", d.swapUsed ? d.swapUsed + " / " + d.swapTotal : "");
|
setText("dash-swap-detail", usedOfTotal(d.swapUsed, d.swapTotal));
|
||||||
|
|
||||||
// Load average
|
|
||||||
if (d.load1 != null) {
|
if (d.load1 != null) {
|
||||||
setText("dash-load", d.load1 + " / " + d.load5 + " / " + d.load15);
|
setText("dash-load", d.load1 + " / " + d.load5 + " / " + d.load15);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network
|
|
||||||
if (d.netSent) {
|
if (d.netSent) {
|
||||||
setText("dash-net", "sent " + d.netSent);
|
setText("dash-net", "sent " + d.netSent);
|
||||||
setText("dash-net-detail", "recv " + (d.netRecv || "—"));
|
setText("dash-net-detail", "recv " + (d.netRecv || "—"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processes
|
|
||||||
// Fastfetch
|
|
||||||
setText("dash-fetch", d.fetch);
|
|
||||||
|
|
||||||
if (d.processes != null) {
|
if (d.processes != null) {
|
||||||
setText("dash-procs", d.processes.toLocaleString());
|
setText("dash-procs", d.processes.toLocaleString());
|
||||||
}
|
}
|
||||||
},
|
|
||||||
() => {
|
|
||||||
const dot = document.getElementById("dash-status-dot");
|
|
||||||
if (dot) dot.className = "status-dot offline";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function setText(id, value) {
|
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);
|
const el = document.getElementById(id);
|
||||||
if (el && value != null) el.textContent = value;
|
if (el && value != null) el.textContent = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBar(valId, barId, _detailId, value, suffix, barClass) {
|
function setBar(valId, barId, value) {
|
||||||
const valEl = document.getElementById(valId);
|
const valEl = document.getElementById(valId);
|
||||||
const barEl = document.getElementById(barId);
|
const barEl = document.getElementById(barId);
|
||||||
if (valEl && value != null) {
|
if (valEl && value != null) {
|
||||||
valEl.innerHTML = value + '<span>' + suffix + '</span>';
|
valEl.textContent = value;
|
||||||
|
const unit = document.createElement("span");
|
||||||
|
unit.textContent = "%";
|
||||||
|
valEl.appendChild(unit);
|
||||||
}
|
}
|
||||||
if (barEl) {
|
if (barEl) {
|
||||||
barEl.style.width = Math.min(Math.max(value || 0, 0), 100) + "%";
|
barEl.style.width = Math.min(Math.max(value || 0, 0), 100) + "%";
|
||||||
barEl.className = "card-bar-fill " + barClass;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
@@ -107,9 +107,9 @@
|
|||||||
box-shadow: 0 3px 16px rgba(184, 68, 87, 0.08);
|
box-shadow: 0 3px 16px rgba(184, 68, 87, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: .bl-done styles — dim + strikethrough for completed items */
|
/* Completed items */
|
||||||
/* .bl-item.bl-done { opacity: 0.5; }
|
.bl-item.bl-done { opacity: 0.5; }
|
||||||
.bl-item.bl-done .bl-title { text-decoration: line-through; } */
|
.bl-item.bl-done .bl-title { text-decoration: line-through; }
|
||||||
|
|
||||||
.bl-item .bl-title {
|
.bl-item .bl-title {
|
||||||
color: #F4F2F5;
|
color: #F4F2F5;
|
||||||
|
|||||||
+1
-7
@@ -1,11 +1,5 @@
|
|||||||
/* Calendar tab */
|
/* 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 ─────────────────────────────────────── */
|
/* ── Two-column layout ─────────────────────────────────────── */
|
||||||
.cal-wrap {
|
.cal-wrap {
|
||||||
max-width: 95vw; margin: 0 auto;
|
max-width: 95vw; margin: 0 auto;
|
||||||
@@ -93,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Sidebar ───────────────────────────────────────────────── */
|
/* ── Sidebar ───────────────────────────────────────────────── */
|
||||||
.cal-sidebar h3 { margin-top: 0;
|
.cal-sidebar h3 {
|
||||||
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px;
|
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px;
|
||||||
color: rgba(244,242,245,0.65); margin: 0 0 14px;
|
color: rgba(244,242,245,0.65); margin: 0 0 14px;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
|
|||||||
+3
-24
@@ -97,18 +97,6 @@
|
|||||||
margin: 0;
|
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 ────────────────────────────────────────── */
|
/* ── Comment rows ────────────────────────────────────────── */
|
||||||
#comments-list {
|
#comments-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -169,7 +157,7 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Steam cyan username -> Now Black */
|
/* Username */
|
||||||
.comment-name {
|
.comment-name {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -209,12 +197,10 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Post form (bottom, Steam style) ────────────────────── */
|
/* ── Post form (above the feed) ──────────────────────────── */
|
||||||
.steam-form-area {
|
.steam-form-area {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
margin: 20px 0;
|
||||||
padding: 20px 0 0 0;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.steam-form-label {
|
.steam-form-label {
|
||||||
@@ -338,13 +324,6 @@
|
|||||||
}
|
}
|
||||||
#comment-submit:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
|
#comment-submit:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
|
||||||
|
|
||||||
/* 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 ───────────────────────────────────── */
|
||||||
.custom-pfp-upload {
|
.custom-pfp-upload {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+1
-15
@@ -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 {
|
.gallery-grid {
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 70px;
|
padding: 210px 70px 70px; /* extra top padding clears the fixed nav bar */
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-grid img {
|
.gallery-grid img {
|
||||||
|
|||||||
+2
-8
@@ -32,8 +32,8 @@ html, body {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 95px;
|
height: 115px; /* 95px + 2×10px padding — other tabs' top offsets assume this */
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
box-sizing: border-box;
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
-webkit-backdrop-filter: blur(2px);
|
-webkit-backdrop-filter: blur(2px);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -85,12 +85,6 @@ html, body {
|
|||||||
transition: border-color 0.4s ease, color 0.4s ease;
|
transition: border-color 0.4s ease, color 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
title {
|
|
||||||
font-size: large;
|
|
||||||
font-weight: 100;
|
|
||||||
margin-bottom: auto;
|
|
||||||
color:#000000;
|
|
||||||
}
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
|
|||||||
+3
-26
@@ -8,13 +8,9 @@
|
|||||||
--dash-gap: 20px; /* space between boxes */
|
--dash-gap: 20px; /* space between boxes */
|
||||||
--dash-min-card-height: 200px;/* minimum height of each box */
|
--dash-min-card-height: 200px;/* minimum height of each box */
|
||||||
--dash-max-width: none; /* max width of the whole grid */
|
--dash-max-width: none; /* max width of the whole grid */
|
||||||
--dash-padding: 120px 48px 60px; /* top right/left bottom */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* To make boxes take different amounts of space, add a class
|
/* Card widths come from the .spanN helpers at the bottom of this file. */
|
||||||
to the HTML like "wide" or "tall" and define it here:
|
|
||||||
|
|
||||||
.dash-card.tall { grid-row: span 2; } */
|
|
||||||
|
|
||||||
/* ── Tab header ───────────────────────────────────────────── */
|
/* ── Tab header ───────────────────────────────────────────── */
|
||||||
.dash-header {
|
.dash-header {
|
||||||
@@ -41,6 +37,7 @@
|
|||||||
50% { box-shadow: 0 0 20px #22c55e, 0 0 40px rgba(34,197,94,0.4); }
|
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 .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 {
|
.dash-grid {
|
||||||
@@ -71,19 +68,12 @@
|
|||||||
box-shadow: 0 6px 28px rgba(91, 111, 222, 0.12);
|
box-shadow: 0 6px 28px rgba(91, 111, 222, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Card header (icon + label) ───────────────────────────── */
|
/* ── Card header ──────────────────────────────────────────── */
|
||||||
.card-top {
|
.card-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
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 {
|
.card-label {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -133,18 +123,6 @@
|
|||||||
margin-top: 6px;
|
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 ──────────────────────────────────── */
|
/* ── Fastfetch text block ──────────────────────────────────── */
|
||||||
.card-fetch {
|
.card-fetch {
|
||||||
margin: 12px 0 0;
|
margin: 12px 0 0;
|
||||||
@@ -160,4 +138,3 @@
|
|||||||
.dash-card.span2 { grid-column: span 2; } /* 3 per row */
|
.dash-card.span2 { grid-column: span 2; } /* 3 per row */
|
||||||
.dash-card.span3 { grid-column: span 3; } /* 2 per row — stretched */
|
.dash-card.span3 { grid-column: span 3; } /* 2 per row — stretched */
|
||||||
.dash-card.span6 { grid-column: span 6; } /* full width */
|
.dash-card.span6 { grid-column: span 6; } /* full width */
|
||||||
.card-spacer { height: 32px; }
|
|
||||||
Reference in new issue
Block a user