website is now phone
This commit is contained in:
1 parent
f4457c5c17
commit
827f524107
16 files changed
+359
-175
No files matched your search
+5
-18
@@ -1,4 +1,4 @@
|
||||
// Bucketlist tab — password-gated, real-time Firestore list
|
||||
// password locked bucketlist
|
||||
(() => {
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -9,11 +9,6 @@
|
||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PASSWORD GATE — items never touch DOM until authenticated
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// --- SHA-256 via Web Crypto ---------------------------------------------------
|
||||
async function sha256(text) {
|
||||
const buf = new TextEncoder().encode(text);
|
||||
const hash = await crypto.subtle.digest("SHA-256", buf);
|
||||
@@ -22,7 +17,6 @@
|
||||
.join("");
|
||||
}
|
||||
|
||||
// --- Lock-screen HTML (only thing in DOM before auth) -------------------------
|
||||
function showLock(container) {
|
||||
container.innerHTML = `
|
||||
<div class="bl-lock">
|
||||
@@ -37,7 +31,6 @@
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Build the real panel + attach Firestore after auth -----------------------
|
||||
function unlock(container, db, hash) {
|
||||
container.innerHTML = `
|
||||
<div class="bl-panel">
|
||||
@@ -50,7 +43,7 @@
|
||||
const listEl = document.getElementById("bl-list");
|
||||
if (!listEl) return;
|
||||
|
||||
// items live under bucketlist_data/<sha256(user:pass)>/items — the path IS the key
|
||||
// the hash is the doc id, no hash no items
|
||||
db.collection("bucketlist_data").doc(hash).collection("items").onSnapshot(snap => {
|
||||
const items = [];
|
||||
snap.forEach(doc => {
|
||||
@@ -92,10 +85,6 @@
|
||||
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ENTRY
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const tab = document.getElementById("bucketlist");
|
||||
if (!tab) return;
|
||||
@@ -103,16 +92,15 @@
|
||||
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||
const db = firebase.firestore();
|
||||
|
||||
const container = tab; // the <section> itself — nothing else exists inside it
|
||||
const container = tab;
|
||||
|
||||
// --- Already authenticated this session? ---------------------------------
|
||||
// already logged in this session
|
||||
const savedHash = sessionStorage.getItem("bl_hash");
|
||||
if (savedHash) {
|
||||
unlock(container, db, savedHash);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Show lock screen ----------------------------------------------------
|
||||
showLock(container);
|
||||
|
||||
const userInput = document.getElementById("bl-user");
|
||||
@@ -131,7 +119,7 @@
|
||||
submitBtn.disabled = true;
|
||||
errEl.textContent = "";
|
||||
|
||||
// hash = sha256("username:password") — both must match the Firestore doc ID
|
||||
// sha256 of "user:pass" has to match a doc in bucketlist_auth
|
||||
const hash = await sha256(user + ":" + pass);
|
||||
try {
|
||||
const doc = await db.collection("bucketlist_auth").doc(hash).get();
|
||||
@@ -149,7 +137,6 @@
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// Enter in either field submits the form
|
||||
document.getElementById("bl-form").onsubmit = e => {
|
||||
e.preventDefault();
|
||||
tryUnlock();
|
||||
|
||||
+4
-8
@@ -1,4 +1,4 @@
|
||||
// Calendar tab — timezone-aware (auto-converts to your local time)
|
||||
// calendar tab, shows everything in the visitor's timezone
|
||||
(() => {
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -40,11 +40,11 @@
|
||||
document.getElementById("cal-prev").onclick = () => shiftMonth(-1);
|
||||
document.getElementById("cal-next").onclick = () => shiftMonth(1);
|
||||
|
||||
// Auto-refresh every 60s so "Upcoming" stays current as time passes
|
||||
// refresh every minute so old events drop off upcoming
|
||||
setInterval(render, 60000);
|
||||
|
||||
function shiftMonth(delta) {
|
||||
// Day 1 avoids overflow (e.g. Jan 31 + 1 month → Mar 3)
|
||||
// day 1 or jan 31 + 1 month turns into march
|
||||
const d = new Date(viewYear, viewMonth + delta, 1);
|
||||
viewYear = d.getFullYear();
|
||||
viewMonth = d.getMonth();
|
||||
@@ -113,10 +113,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Normalise a Firestore doc into local date/time. Timed events use the
|
||||
// `utc` timestamp so they land on the viewer's local day; date-only events
|
||||
// keep their `date` (converting their midnight could shift them a day).
|
||||
// Falls back to the raw fields when `utc` is missing or unparseable.
|
||||
// events with a time get converted to local time, all day events keep their date
|
||||
function toEvent(d) {
|
||||
const utc = d.utc ? new Date(d.utc) : null;
|
||||
const hasUtc = utc && !isNaN(utc.getTime());
|
||||
@@ -130,7 +127,6 @@
|
||||
description: d.description || "",
|
||||
date,
|
||||
time,
|
||||
// 0 for malformed events keeps the sort stable and hides them from "Upcoming"
|
||||
when: (hasUtc ? utc.getTime() : new Date(date + "T" + (d.time || "00:00")).getTime()) || 0
|
||||
};
|
||||
}
|
||||
|
||||
+8
-16
@@ -1,4 +1,4 @@
|
||||
// About tab — Steam-style comment wall + global click counter
|
||||
// comment wall + click counter on the about tab
|
||||
(() => {
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -37,7 +37,7 @@
|
||||
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
|
||||
const db = firebase.firestore();
|
||||
|
||||
// ── Avatar colour picker ─────────────────────────────────────
|
||||
// avatar colour picker
|
||||
const colorOpts = document.querySelectorAll('.av-opt');
|
||||
colorOpts.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -47,7 +47,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ── Custom PFP upload (center-cropped + shrunk to 64×64 JPEG) ─
|
||||
// pfp upload, cropped to a 64x64 square
|
||||
pfpUpload.addEventListener('change', () => {
|
||||
const file = pfpUpload.files[0];
|
||||
if (!file) return;
|
||||
@@ -84,7 +84,6 @@
|
||||
pfpClear.style.display = 'none';
|
||||
});
|
||||
|
||||
// ── Char counter ─────────────────────────────────────────────
|
||||
function updateCharCount() {
|
||||
const left = MAX_CHARS - msgInput.value.length;
|
||||
charCountEl.textContent = left;
|
||||
@@ -93,7 +92,7 @@
|
||||
msgInput.addEventListener('input', updateCharCount);
|
||||
updateCharCount();
|
||||
|
||||
// ── Real-time feed + pagination ──────────────────────────────
|
||||
// comments feed + pages
|
||||
let allComments = [];
|
||||
let currentPage = 1;
|
||||
|
||||
@@ -138,7 +137,7 @@
|
||||
listEl.innerHTML = '<p class="no-comments">Could not load comments.</p>';
|
||||
});
|
||||
|
||||
// ── Global click counter ─────────────────────────────────────
|
||||
// click counter
|
||||
const clickBtn = document.getElementById('yuuka-click-btn');
|
||||
const clickDisplay = document.getElementById('click-counter-display');
|
||||
const clickSound = new Audio('sounds/yuuka_click.mp3');
|
||||
@@ -146,8 +145,6 @@
|
||||
if (clickBtn && clickDisplay) {
|
||||
const statsRef = db.collection('stats').doc('global_clicks');
|
||||
|
||||
// Firestore applies local writes to this listener instantly,
|
||||
// so clicks show up immediately without a manual optimistic update.
|
||||
statsRef.onSnapshot(doc => {
|
||||
const count = doc.exists ? doc.data().count : 0;
|
||||
clickDisplay.textContent = (count || 0).toLocaleString();
|
||||
@@ -157,7 +154,7 @@
|
||||
});
|
||||
|
||||
clickBtn.addEventListener('click', () => {
|
||||
clickSound.currentTime = 0; // restart so rapid clicks all play
|
||||
clickSound.currentTime = 0; // so spam clicking plays every time
|
||||
clickSound.play().catch(() => {});
|
||||
|
||||
statsRef.set({
|
||||
@@ -166,7 +163,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Submit ───────────────────────────────────────────────────
|
||||
form.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -199,12 +195,11 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ── Build a single comment bubble (Steam layout) ─────────────────
|
||||
function buildBubble(data) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'comment-bubble';
|
||||
|
||||
// Pending server timestamps are null until the write lands
|
||||
// timestamp is null for a sec right after posting
|
||||
const ts = data.timestamp ? data.timestamp.toDate() : new Date();
|
||||
const timeStr = ts.toLocaleDateString('en-US', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
@@ -235,10 +230,7 @@
|
||||
return div;
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// Older comments were stored pre-escaped (< and > only), which made
|
||||
// them render as literal "<" once escaped again. Undo that here.
|
||||
// old comments were saved already escaped, this stops them showing "<"
|
||||
function legacyUnescape(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
+2
-6
@@ -1,5 +1,4 @@
|
||||
// Gallery tab — thumbnails from gallery/manifest.json + full-size lightbox.
|
||||
// The manifest is (re)generated by generate_thumbnails.py.
|
||||
// manifest.json comes from generate_thumbnails.py
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('gallery-container');
|
||||
const lightbox = document.getElementById('lightbox');
|
||||
@@ -8,8 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (!container || !lightbox) return;
|
||||
|
||||
// Animated GIFs are served as MP4 (a few % of the size), so those
|
||||
// open in the <video>; everything else in the <img>.
|
||||
// gifs are converted to mp4 so those open in the video player
|
||||
const openLightbox = (src) => {
|
||||
const isVideo = src.endsWith('.mp4');
|
||||
lightboxImg.style.display = isVideo ? 'none' : 'block';
|
||||
@@ -26,13 +24,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const closeLightbox = () => {
|
||||
lightbox.style.display = 'none';
|
||||
lightboxImg.removeAttribute('src');
|
||||
// Stop playback and any download still in progress
|
||||
lightboxVideo.pause();
|
||||
lightboxVideo.removeAttribute('src');
|
||||
lightboxVideo.load();
|
||||
};
|
||||
|
||||
// Close on the X, on a click outside the media, or with Escape
|
||||
lightbox.addEventListener('click', (e) => {
|
||||
if (e.target !== lightboxImg && e.target !== lightboxVideo) closeLightbox();
|
||||
});
|
||||
|
||||
+35
-14
@@ -6,11 +6,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const bucketlistVideo = document.getElementById('bucketlist-background-video');
|
||||
const botVideo = document.getElementById('yuukabot-background-video');
|
||||
|
||||
// ── Map URL path → tab id ──────────────────────────────
|
||||
function pathToTab(path) {
|
||||
const slug = path.replace(/^\/+|\/+$/g, '') || 'about';
|
||||
const valid = ['about', 'gallery', 'bucketlist', 'yuukabot', 'calendar', 'services'];
|
||||
// backward compat: old /ahmet links
|
||||
// old links
|
||||
if (slug === 'ahmet') return 'bucketlist';
|
||||
return valid.includes(slug) ? slug : 'about';
|
||||
}
|
||||
@@ -19,10 +18,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return '/' + (tabId === 'about' ? '' : tabId);
|
||||
}
|
||||
|
||||
// ── Swap background videos + top-bar theme ────────────
|
||||
const topLine = document.getElementById('top-line');
|
||||
|
||||
// map tab id → its dedicated video element (unlisted = use main)
|
||||
// tabs not listed here use the main video
|
||||
const tabVideos = {
|
||||
calendar: calVideo,
|
||||
bucketlist: bucketlistVideo,
|
||||
@@ -31,12 +29,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
function setBgForTab(tabId) {
|
||||
// Hide + pause ALL videos first
|
||||
[mainVideo, calVideo, bucketlistVideo, botVideo].forEach(v => {
|
||||
if (v) { v.style.display = 'none'; v.pause(); }
|
||||
});
|
||||
|
||||
// Show the right one
|
||||
const target = tabVideos[tabId];
|
||||
if (target) {
|
||||
target.style.display = 'block';
|
||||
@@ -46,49 +42,74 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (mainVideo.paused) mainVideo.play().catch(() => {});
|
||||
}
|
||||
|
||||
// Top-bar theme toggle
|
||||
if (topLine) {
|
||||
topLine.classList.toggle('cal-theme', tabId === 'calendar');
|
||||
topLine.classList.toggle('bucketlist-theme', tabId === 'bucketlist');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Show a specific tab ────────────────────────────────
|
||||
const navCurrent = document.getElementById('nav-current');
|
||||
|
||||
function showTab(tabId) {
|
||||
tabs.forEach(t => t.style.display = 'none');
|
||||
const target = document.getElementById(tabId);
|
||||
if (target) target.style.display = 'block';
|
||||
setBgForTab(tabId);
|
||||
|
||||
// tab name next to the hamburger on mobile
|
||||
const btn = document.getElementById(tabId + '-btn');
|
||||
if (navCurrent && btn) navCurrent.textContent = btn.textContent;
|
||||
}
|
||||
|
||||
// changing the url breaks every button when index.html is opened as a file
|
||||
function setUrl(method, tabId) {
|
||||
if (window.location.protocol === 'file:') return;
|
||||
history[method]({ tab: tabId }, '', tabToPath(tabId) + window.location.search);
|
||||
}
|
||||
|
||||
// ── Navigate to a tab (updates URL + shows it) ────────
|
||||
function navigateTo(tabId) {
|
||||
showTab(tabId);
|
||||
window.scrollTo(0, 0);
|
||||
setUrl('pushState', tabId);
|
||||
}
|
||||
|
||||
// ── Initial load: show tab from URL ────────────────────
|
||||
// (also normalises unknown / legacy paths like /ahmet in the address bar)
|
||||
const initialTab = pathToTab(window.location.pathname);
|
||||
showTab(initialTab);
|
||||
setUrl('replaceState', initialTab);
|
||||
|
||||
// ── Tab button clicks ──────────────────────────────────
|
||||
const buttons = document.querySelectorAll('#top-line button');
|
||||
const buttons = document.querySelectorAll('#nav-tabs button');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const targetId = button.id.replace('-btn', '');
|
||||
navigateTo(targetId);
|
||||
closeMenu();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Back / forward browser buttons ─────────────────────
|
||||
// hamburger menu (mobile)
|
||||
const hamburger = document.getElementById('nav-hamburger');
|
||||
|
||||
function openMenu() {
|
||||
topLine.classList.add('nav-open');
|
||||
hamburger.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
topLine.classList.remove('nav-open');
|
||||
hamburger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
hamburger.addEventListener('click', () => {
|
||||
topLine.classList.contains('nav-open') ? closeMenu() : openMenu();
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!topLine.contains(e.target)) closeMenu();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeMenu();
|
||||
});
|
||||
|
||||
// back/forward buttons
|
||||
window.addEventListener('popstate', (e) => {
|
||||
showTab(e.state && e.state.tab ? e.state.tab : pathToTab(window.location.pathname));
|
||||
});
|
||||
|
||||
+5
-7
@@ -1,4 +1,4 @@
|
||||
// Yuuka Bot Dashboard — live Firestore reader
|
||||
// bot status dashboard
|
||||
(() => {
|
||||
const FIREBASE_CONFIG = {
|
||||
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
|
||||
@@ -9,8 +9,7 @@
|
||||
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
|
||||
};
|
||||
|
||||
// The bot pushes every 10s; no update for this long means it's down
|
||||
// (generous so a visitor's slightly-off clock doesn't flip the dot).
|
||||
// bot updates every 10s, nothing for a minute means it's probably dead
|
||||
const STALE_MS = 60 * 1000;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@@ -22,15 +21,15 @@
|
||||
|
||||
const dot = document.getElementById("dash-status-dot");
|
||||
let online = false;
|
||||
let updatedAt = null; // ms; null for bots that don't send updatedAt yet
|
||||
let loaded = false; // keep the grey "pending" dot until first data
|
||||
let updatedAt = null;
|
||||
let loaded = false; // dot stays grey until the first data
|
||||
|
||||
function refreshDot() {
|
||||
if (!loaded) return;
|
||||
const fresh = updatedAt == null || Date.now() - updatedAt < STALE_MS;
|
||||
if (dot) dot.className = online && fresh ? "status-dot" : "status-dot offline";
|
||||
}
|
||||
// Re-check periodically: a crashed bot sends no more snapshots
|
||||
// a crashed bot stops sending updates so keep checking
|
||||
setInterval(refreshDot, 5000);
|
||||
|
||||
db.collection("sysinfo").doc("server").onSnapshot(doc => {
|
||||
@@ -44,7 +43,6 @@
|
||||
setText("dash-uptime", d.uptime);
|
||||
setText("dash-bot-uptime", d.botUptime ? "bot up " + d.botUptime : "");
|
||||
|
||||
// CPU, RAM, Disk, Swap — value + bar
|
||||
setBar("dash-cpu-val", "dash-cpu-bar", d.cpu);
|
||||
setBar("dash-ram-val", "dash-ram-bar", d.ram);
|
||||
setBar("dash-disk-val", "dash-disk-bar", d.disk);
|
||||
|
||||
Reference in new issue
Block a user