website polish

This commit is contained in:
esef committed 2026-09-23 16:28:16 +03:00
1 parent a55e339ba8
commit 6b52f9ea14
16 files changed
+679 -778

No files matched your search

+142 -136
View File
@@ -1,152 +1,158 @@
// Bucketlist tab — password-gated, real-time Firestore list
const BL_FB_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
(() => {
const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
// ═══════════════════════════════════════════════════════════
// PASSWORD GATE — items never touch DOM until authenticated
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// PASSWORD GATE — items never touch DOM until authenticated
// ═══════════════════════════════════════════════════════════
// --- SHA-256 via Web Crypto ---------------------------------------------------
async function sha256(text) {
const buf = new TextEncoder().encode(text);
const hash = await crypto.subtle.digest("SHA-256", buf);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
// --- SHA-256 via Web Crypto ---------------------------------------------------
async function sha256(text) {
const buf = new TextEncoder().encode(text);
const hash = await crypto.subtle.digest("SHA-256", buf);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
// --- Lock-screen HTML (only thing in DOM before auth) -------------------------
function showLock(container) {
container.innerHTML = `
<div class="bl-lock">
<div class="bl-lock-box">
<h2>Bucketlist</h2>
<input type="text" id="bl-user" placeholder="Username" autofocus>
<input type="password" id="bl-pass" placeholder="Password">
<button id="bl-submit">Unlock</button>
<p id="bl-err"></p>
// --- Lock-screen HTML (only thing in DOM before auth) -------------------------
function showLock(container) {
container.innerHTML = `
<div class="bl-lock">
<form class="bl-lock-box" id="bl-form">
<h2>Bucketlist</h2>
<input type="text" id="bl-user" placeholder="Username" autocomplete="username">
<input type="password" id="bl-pass" placeholder="Password" autocomplete="current-password">
<button type="submit" id="bl-submit">Unlock</button>
<p id="bl-err"></p>
</form>
</div>
</div>
`;
}
`;
}
// --- Build the real panel + attach Firestore after auth -----------------------
function unlock(container, db, hash) {
container.innerHTML = `
<div class="bl-panel">
<div class="bl-list" id="bl-list">
<p class="bl-empty">Loading...</p>
// --- Build the real panel + attach Firestore after auth -----------------------
function unlock(container, db, hash) {
container.innerHTML = `
<div class="bl-panel">
<div class="bl-list" id="bl-list">
<p class="bl-empty">Loading...</p>
</div>
</div>
</div>
`;
`;
const listEl = document.getElementById("bl-list");
if (!listEl) return;
const listEl = document.getElementById("bl-list");
if (!listEl) return;
// items live under bucketlist_data/<sha256(user:pass)>/items — the path IS the key
db.collection("bucketlist_data").doc(hash).collection("items").onSnapshot(snap => {
const items = [];
snap.forEach(doc => {
const d = doc.data();
items.push({
id: doc.id,
title: d.title || "",
description: d.description || "",
date: d.date || "",
utc: d.utc || "",
done: d.done || false
// items live under bucketlist_data/<sha256(user:pass)>/items — the path IS the key
db.collection("bucketlist_data").doc(hash).collection("items").onSnapshot(snap => {
const items = [];
snap.forEach(doc => {
const d = doc.data();
items.push({
id: doc.id,
title: d.title || "",
description: d.description || "",
date: d.date || "",
utc: d.utc || "",
done: d.done || false
});
});
items.sort((a, b) => (a.utc || "").localeCompare(b.utc || ""));
render(listEl, items);
}, () => {
if (listEl) listEl.innerHTML = '<p class="bl-empty">Could not load.</p>';
});
items.sort((a, b) => (a.utc || "").localeCompare(b.utc || ""));
render(listEl, items);
}, () => {
if (listEl) listEl.innerHTML = '<p class="bl-empty">Could not load.</p>';
});
}
function render(listEl, items) {
if (items.length === 0) {
listEl.innerHTML = '<p class="bl-empty">Nothing here yet.</p>';
return;
}
// NOTE: item.done adds .bl-done class — hook for completed-item styling (CSS still TODO)
listEl.innerHTML = items.map(item =>
'<div class="bl-item' + (item.done ? ' bl-done' : '') + '">' +
'<div class="bl-title">' + esc(item.title) + '</div>' +
(item.date ? '<div class="bl-date">' + esc(item.date) + '</div>' : '') +
(item.description ? '<div class="bl-desc">' + esc(item.description) + '</div>' : '') +
'</div>'
).join("");
}
function esc(s) {
if (!s) return "";
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;")
.replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");
}
// ═══════════════════════════════════════════════════════════
// ENTRY
// ═══════════════════════════════════════════════════════════
document.addEventListener("DOMContentLoaded", () => {
const tab = document.getElementById("bucketlist");
if (!tab) return;
if (!firebase.apps.length) firebase.initializeApp(BL_FB_CONFIG);
const db = firebase.firestore();
const container = tab; // the <section> itself — nothing else exists inside it
// --- Already authenticated this session? ---------------------------------
const savedHash = sessionStorage.getItem("bl_hash");
if (savedHash) {
unlock(container, db, savedHash);
return;
}
// --- Show lock screen ----------------------------------------------------
showLock(container);
const userInput = document.getElementById("bl-user");
const passInput = document.getElementById("bl-pass");
const submitBtn = document.getElementById("bl-submit");
const errEl = document.getElementById("bl-err");
async function tryUnlock() {
const user = userInput.value.trim();
const pass = passInput.value;
if (!user || !pass) return;
submitBtn.disabled = true;
errEl.textContent = "";
// hash = sha256("username:password") — both must match the Firestore doc ID
const hash = await sha256(user + ":" + pass);
try {
const doc = await db.collection("bucketlist_auth").doc(hash).get();
if (doc.exists) {
sessionStorage.setItem("bl_hash", hash);
unlock(container, db, hash);
} else {
errEl.textContent = "Wrong credentials.";
passInput.value = "";
passInput.focus();
}
} catch (e) {
errEl.textContent = "Could not verify. Try again.";
function render(listEl, items) {
if (items.length === 0) {
listEl.innerHTML = '<p class="bl-empty">Nothing here yet.</p>';
return;
}
submitBtn.disabled = false;
listEl.innerHTML = items.map(item =>
'<div class="bl-item' + (item.done ? ' bl-done' : '') + '">' +
'<div class="bl-title">' + esc(item.title) + '</div>' +
(item.date ? '<div class="bl-date">' + esc(item.date) + '</div>' : '') +
(item.description ? '<div class="bl-desc">' + esc(item.description) + '</div>' : '') +
'</div>'
).join("");
}
submitBtn.onclick = tryUnlock;
userInput.onkeydown = e => { if (e.key === "Enter") passInput.focus(); };
passInput.onkeydown = e => { if (e.key === "Enter") tryUnlock(); };
});
function esc(s) {
if (!s) return "";
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;")
.replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");
}
// ═══════════════════════════════════════════════════════════
// ENTRY
// ═══════════════════════════════════════════════════════════
document.addEventListener("DOMContentLoaded", () => {
const tab = document.getElementById("bucketlist");
if (!tab) return;
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
const db = firebase.firestore();
const container = tab; // the <section> itself — nothing else exists inside it
// --- Already authenticated this session? ---------------------------------
const savedHash = sessionStorage.getItem("bl_hash");
if (savedHash) {
unlock(container, db, savedHash);
return;
}
// --- Show lock screen ----------------------------------------------------
showLock(container);
const userInput = document.getElementById("bl-user");
const passInput = document.getElementById("bl-pass");
const submitBtn = document.getElementById("bl-submit");
const errEl = document.getElementById("bl-err");
async function tryUnlock() {
const user = userInput.value.trim();
const pass = passInput.value;
if (!user || !pass) {
(user ? passInput : userInput).focus();
return;
}
submitBtn.disabled = true;
errEl.textContent = "";
// hash = sha256("username:password") — both must match the Firestore doc ID
const hash = await sha256(user + ":" + pass);
try {
const doc = await db.collection("bucketlist_auth").doc(hash).get();
if (doc.exists) {
sessionStorage.setItem("bl_hash", hash);
unlock(container, db, hash);
} else {
errEl.textContent = "Wrong credentials.";
passInput.value = "";
passInput.focus();
}
} catch (e) {
errEl.textContent = "Could not verify. Try again.";
}
submitBtn.disabled = false;
}
// Enter in either field submits the form
document.getElementById("bl-form").onsubmit = e => {
e.preventDefault();
tryUnlock();
};
});
})();
+121 -117
View File
@@ -1,140 +1,144 @@
// Calendar tab — timezone-aware (auto-converts to your local time)
const CAL_FB_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
(() => {
const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = ["January","February","March","April","May","June",
"July","August","September","October","November","December"];
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = ["January","February","March","April","May","June",
"July","August","September","October","November","December"];
function fmtTime(utcIso) {
if (utcIso) {
try {
var d = new Date(utcIso);
if (!isNaN(d.getTime())) {
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
} catch (e) {}
}
return "";
}
document.addEventListener("DOMContentLoaded", () => {
const tab = document.getElementById("calendar");
if (!tab) return;
document.addEventListener("DOMContentLoaded", () => {
const tab = document.getElementById("calendar");
if (!tab) return;
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
const db = firebase.firestore();
if (!firebase.apps.length) firebase.initializeApp(CAL_FB_CONFIG);
const db = firebase.firestore();
let allEvents = [];
const now = new Date();
let viewYear = now.getFullYear();
let viewMonth = now.getMonth();
let allEvents = [];
let viewDate = new Date();
const gridEl = document.getElementById("cal-grid");
const navEl = document.getElementById("cal-nav-title");
const sidebarEl = document.getElementById("cal-sidebar-list");
const gridEl = document.getElementById("cal-grid");
const navEl = document.getElementById("cal-nav-title");
const sidebarEl = document.getElementById("cal-sidebar-list");
db.collection("calendar_events").onSnapshot(snap => {
allEvents = [];
snap.forEach(doc => {
const d = doc.data();
allEvents.push({
id: doc.id,
title: d.title || "",
date: d.date || "",
time: d.time || "",
utc: d.utc || "",
description: d.description || "",
_sort: d.utc || (d.date + "T" + (d.time || "00:00"))
});
});
allEvents.sort((a, b) => a._sort.localeCompare(b._sort));
render();
}, () => {
if (gridEl) gridEl.innerHTML = '<p class="cal-side-empty">Could not load.</p>';
});
document.getElementById("cal-prev").onclick = () => { viewDate.setMonth(viewDate.getMonth()-1); render(); };
document.getElementById("cal-next").onclick = () => { viewDate.setMonth(viewDate.getMonth()+1); render(); };
// Auto-refresh every 60s so "Upcoming" stays current as time passes
setInterval(render, 60000);
function render() {
const year = viewDate.getFullYear();
const month = viewDate.getMonth();
navEl.textContent = MONTHS[month] + " " + year;
const byDate = {};
allEvents.forEach(e => {
if (!byDate[e.date]) byDate[e.date] = [];
byDate[e.date].push(e);
db.collection("calendar_events").onSnapshot(snap => {
allEvents = snap.docs.map(doc => toEvent(doc.data()));
allEvents.sort((a, b) => a.when - b.when);
render();
}, () => {
gridEl.innerHTML = '<p class="cal-side-empty">Could not load.</p>';
});
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const today = new Date();
const todayStr = today.getFullYear() + "-" +
String(today.getMonth()+1).padStart(2,"0") + "-" +
String(today.getDate()).padStart(2,"0");
document.getElementById("cal-prev").onclick = () => shiftMonth(-1);
document.getElementById("cal-next").onclick = () => shiftMonth(1);
let html = "";
WEEKDAYS.forEach(d => html += '<div class="cal-day-header">' + d + "</div>");
// Auto-refresh every 60s so "Upcoming" stays current as time passes
setInterval(render, 60000);
for (let i = 0; i < firstDay; i++) {
html += '<div class="cal-day other-month"><div class="day-num"></div></div>';
function shiftMonth(delta) {
// Day 1 avoids overflow (e.g. Jan 31 + 1 month → Mar 3)
const d = new Date(viewYear, viewMonth + delta, 1);
viewYear = d.getFullYear();
viewMonth = d.getMonth();
render();
}
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = year + "-" +
String(month+1).padStart(2,"0") + "-" +
String(d).padStart(2,"0");
const dayEvents = byDate[dateStr] || [];
const isToday = dateStr === todayStr;
const hasEvents = dayEvents.length > 0;
function render() {
navEl.textContent = MONTHS[viewMonth] + " " + viewYear;
html += '<div class="cal-day' +
(isToday ? " today" : "") +
(hasEvents ? " has-events" : "") + '">';
html += '<div class="day-num">' + d + "</div>";
const byDate = {};
allEvents.forEach(e => {
(byDate[e.date] = byDate[e.date] || []).push(e);
});
if (hasEvents) {
dayEvents.forEach(() => html += '<span class="day-dot"></span>');
html += '<div class="cal-popup"><h4>' + dateStr + "</h4>";
dayEvents.forEach(e => {
html += '<div class="cal-popup-item">';
if (e.time) {
html += '<span class="pop-time">' + fmtTime(e.utc) + "</span> ";
}
html += esc(e.title);
if (e.description) html += '<div class="pop-desc">' + esc(e.description) + "</div>";
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
const todayStr = ymd(new Date());
let html = "";
WEEKDAYS.forEach(d => html += '<div class="cal-day-header">' + d + "</div>");
for (let i = 0; i < firstDay; i++) {
html += '<div class="cal-day other-month"><div class="day-num"></div></div>';
}
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = ymd(new Date(viewYear, viewMonth, d));
const dayEvents = byDate[dateStr] || [];
const hasEvents = dayEvents.length > 0;
html += '<div class="cal-day' +
(dateStr === todayStr ? " today" : "") +
(hasEvents ? " has-events" : "") + '">';
html += '<div class="day-num">' + d + "</div>";
if (hasEvents) {
dayEvents.forEach(() => html += '<span class="day-dot"></span>');
html += '<div class="cal-popup"><h4>' + dateStr + "</h4>";
dayEvents.forEach(e => {
html += '<div class="cal-popup-item">';
if (e.time) html += '<span class="pop-time">' + esc(e.time) + "</span> ";
html += esc(e.title);
if (e.description) html += '<div class="pop-desc">' + esc(e.description) + "</div>";
html += "</div>";
});
html += "</div>";
});
}
html += "</div>";
}
html += "</div>";
}
gridEl.innerHTML = html;
gridEl.innerHTML = html;
const nowISO = new Date().toISOString();
const upcoming = allEvents.filter(e => e._sort >= nowISO);
if (upcoming.length === 0) {
sidebarEl.innerHTML = '<p class="cal-side-empty">No upcoming events.<br>Use !book in Discord.</p>';
} else {
sidebarEl.innerHTML = upcoming.slice(0, 15).map(e =>
'<div class="cal-side-event">' +
'<div class="se-date">' + e.date +
(e.time ? " " + fmtTime(e.utc) : "") + "</div>" +
'<div class="se-title">' + esc(e.title) + "</div>" +
(e.description ? '<div class="se-desc">' + esc(e.description) + "</div>" : "") +
"</div>"
).join("");
const nowMs = Date.now();
const upcoming = allEvents.filter(e => e.when >= nowMs);
if (upcoming.length === 0) {
sidebarEl.innerHTML = '<p class="cal-side-empty">No upcoming events.<br>Use !book in Discord.</p>';
} else {
sidebarEl.innerHTML = upcoming.slice(0, 15).map(e =>
'<div class="cal-side-event">' +
'<div class="se-date">' + esc(e.date) + (e.time ? " " + esc(e.time) : "") + "</div>" +
'<div class="se-title">' + esc(e.title) + "</div>" +
(e.description ? '<div class="se-desc">' + esc(e.description) + "</div>" : "") +
"</div>"
).join("");
}
}
});
// Normalise a Firestore doc into local date/time. Timed events use the
// `utc` timestamp so they land on the viewer's local day; date-only events
// keep their `date` (converting their midnight could shift them a day).
// Falls back to the raw fields when `utc` is missing or unparseable.
function toEvent(d) {
const utc = d.utc ? new Date(d.utc) : null;
const hasUtc = utc && !isNaN(utc.getTime());
const localize = hasUtc && d.time;
const date = localize ? ymd(utc) : (d.date || "");
const time = !d.time ? "" : localize
? utc.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
: d.time;
return {
title: d.title || "",
description: d.description || "",
date,
time,
// 0 for malformed events keeps the sort stable and hides them from "Upcoming"
when: (hasUtc ? utc.getTime() : new Date(date + "T" + (d.time || "00:00")).getTime()) || 0
};
}
function ymd(d) {
return d.getFullYear() + "-" +
String(d.getMonth() + 1).padStart(2, "0") + "-" +
String(d.getDate()).padStart(2, "0");
}
function esc(s) {
@@ -142,4 +146,4 @@ document.addEventListener("DOMContentLoaded", () => {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;")
.replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
});
})();
+195 -243
View File
@@ -1,80 +1,81 @@
const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
// About tab — Steam-style comment wall + global click counter
(() => {
const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
// ================================================================
const MAX_CHARS = 300;
const MAX_NAME = 32;
const COMMENTS_PER_PAGE = 5;
const COLLECTION = "comments";
const DEFAULT_COLOR = "#5b6fde";
const COUNT_COLOR = "#364858";
const COUNT_LOW_COLOR = "#e05555";
const MAX_CHARS = 300;
const COLLECTION = "comments";
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('comment-form');
const nameInput = document.getElementById('comment-name');
const msgInput = document.getElementById('comment-message');
const submitBtn = document.getElementById('comment-submit');
const charCountEl = document.getElementById('char-count');
const listEl = document.getElementById('comments-list');
const titleEl = document.getElementById('comments-title');
const paginationEl = document.getElementById('comments-pagination');
const colorInput = document.getElementById('comment-avatar-color');
const b64Input = document.getElementById('comment-avatar-b64');
const pfpUpload = document.getElementById('pfp-upload');
const pfpPreview = document.getElementById('pfp-preview');
const pfpClear = document.getElementById('pfp-clear');
document.addEventListener('DOMContentLoaded', () => {
if (!form || !listEl) return;
const configured = FIREBASE_CONFIG.apiKey !== "YOUR_API_KEY";
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
const db = firebase.firestore();
const form = document.getElementById('comment-form');
const nameInput = document.getElementById('comment-name');
const msgInput = document.getElementById('comment-message');
const submitBtn = document.getElementById('comment-submit');
const charCountEl = document.getElementById('char-count');
const listEl = document.getElementById('comments-list');
const titleEl = document.getElementById('comments-title');
const colorInput = document.getElementById('comment-avatar-color');
const b64Input = document.getElementById('comment-avatar-b64');
const pfpUpload = document.getElementById('pfp-upload');
const pfpPreview = document.getElementById('pfp-preview');
const pfpClear = document.getElementById('pfp-clear');
const warning = document.getElementById('firebase-setup-warning');
if (!form || !listEl) return;
// ── Avatar colour picker ─────────────────────────────────────
document.querySelectorAll('.av-opt').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.av-opt').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
colorInput.value = btn.dataset.color;
// ── Avatar colour picker ─────────────────────────────────────
const colorOpts = document.querySelectorAll('.av-opt');
colorOpts.forEach(btn => {
btn.addEventListener('click', () => {
colorOpts.forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
colorInput.value = btn.dataset.color;
});
});
});
// ── Custom PFP Upload ────────────────────────────────────────
if (pfpUpload) {
pfpUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
// ── Custom PFP upload (center-cropped + shrunk to 64×64 JPEG) ─
pfpUpload.addEventListener('change', () => {
const file = pfpUpload.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
reader.onload = () => {
const img = new Image();
img.onload = () => {
// Shrink to max 64x64 to save space
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 64;
canvas.height = 64;
// Center and crop
const size = Math.min(img.width, img.height);
const sx = (img.width - size) / 2;
const sy = (img.height - size) / 2;
ctx.drawImage(img, sx, sy, size, size, 0, 0, 64, 64);
canvas.getContext('2d').drawImage(img, sx, sy, size, size, 0, 0, 64, 64);
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
b64Input.value = dataUrl;
pfpPreview.style.backgroundImage = `url(${dataUrl})`;
pfpPreview.style.display = 'block';
pfpClear.style.display = 'inline-flex';
};
img.src = event.target.result;
img.src = reader.result;
};
reader.readAsDataURL(file);
});
pfpClear.addEventListener('click', () => {
b64Input.value = '';
pfpUpload.value = '';
@@ -82,220 +83,171 @@ document.addEventListener('DOMContentLoaded', () => {
pfpPreview.style.backgroundImage = 'none';
pfpClear.style.display = 'none';
});
}
// ── Char counter ─────────────────────────────────────────────
charCountEl.textContent = MAX_CHARS;
msgInput.addEventListener('input', () => {
const left = MAX_CHARS - msgInput.value.length;
charCountEl.textContent = left;
charCountEl.style.color = left < 40 ? '#e05555' : '#364858';
});
// ── No Firebase → show demo + setup warning ──────────────────
if (!configured) {
if (warning) warning.style.display = 'block';
showDemo(listEl);
form.addEventListener('submit', e => {
e.preventDefault();
alert('Firebase is not connected yet!\n\nOpen js/comments.js and follow the setup instructions at the top of the file.');
});
return;
}
// Firebase is configured — hide the setup warning
if (warning) warning.style.display = 'none';
// ── Firebase init ────────────────────────────────────────────
firebase.initializeApp(FIREBASE_CONFIG);
const db = firebase.firestore();
// ── Real-time feed & Pagination ──────────────────────────────
let allComments = [];
let currentPage = 1;
const COMMENTS_PER_PAGE = 5;
const paginationEl = document.getElementById('comments-pagination');
function renderPage() {
listEl.innerHTML = '';
if (paginationEl) paginationEl.innerHTML = '';
if (allComments.length === 0) {
listEl.innerHTML = '<p class="no-comments">No messages yet — be the first!</p>';
return;
// ── Char counter ─────────────────────────────────────────────
function updateCharCount() {
const left = MAX_CHARS - msgInput.value.length;
charCountEl.textContent = left;
charCountEl.style.color = left < 40 ? COUNT_LOW_COLOR : COUNT_COLOR;
}
msgInput.addEventListener('input', updateCharCount);
updateCharCount();
const totalPages = Math.ceil(allComments.length / COMMENTS_PER_PAGE);
if (currentPage > totalPages) currentPage = totalPages;
if (currentPage < 1) currentPage = 1;
// ── Real-time feed + pagination ──────────────────────────────
let allComments = [];
let currentPage = 1;
const start = (currentPage - 1) * COMMENTS_PER_PAGE;
const end = start + COMMENTS_PER_PAGE;
const slice = allComments.slice(start, end);
function renderPage() {
listEl.innerHTML = '';
paginationEl.innerHTML = '';
slice.forEach(data => listEl.appendChild(buildBubble(data)));
if (allComments.length === 0) {
listEl.innerHTML = '<p class="no-comments">No messages yet — be the first!</p>';
return;
}
// Build pagination controls
if (totalPages > 1 && paginationEl) {
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.className = 'steam-page-btn' + (i === currentPage ? ' active' : '');
btn.textContent = i;
btn.onclick = () => {
currentPage = i;
renderPage();
};
paginationEl.appendChild(btn);
const totalPages = Math.ceil(allComments.length / COMMENTS_PER_PAGE);
currentPage = Math.min(Math.max(currentPage, 1), totalPages);
const start = (currentPage - 1) * COMMENTS_PER_PAGE;
allComments.slice(start, start + COMMENTS_PER_PAGE)
.forEach(data => listEl.appendChild(buildBubble(data)));
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.className = 'steam-page-btn' + (i === currentPage ? ' active' : '');
btn.textContent = i;
btn.onclick = () => {
currentPage = i;
renderPage();
};
paginationEl.appendChild(btn);
}
}
}
}
// ── Global Click Counter ─────────────────────────────────────
const clickBtn = document.getElementById('yuuka-click-btn');
const clickDisplay = document.getElementById('click-counter-display');
const clickSound = new Audio('sounds/yuuka_click.mp3');
if (clickBtn && clickDisplay && configured) {
const statsRef = db.collection('stats').doc('global_clicks');
// Listen to changes in real-time
statsRef.onSnapshot(doc => {
if (doc.exists) {
clickDisplay.textContent = doc.data().count.toLocaleString();
} else {
clickDisplay.textContent = '0';
}
}, err => {
console.error("Click counter error:", err);
clickDisplay.textContent = 'Error';
});
// Increment on click
clickBtn.addEventListener('click', () => {
// Play sound
clickSound.currentTime = 0; // Reset to allow rapid clicking
clickSound.play().catch(e => console.error("Audio play failed:", e));
// Optimistic update for instant feel
let current = parseInt(clickDisplay.textContent.replace(/,/g, '')) || 0;
clickDisplay.textContent = (current + 1).toLocaleString();
statsRef.set({
count: firebase.firestore.FieldValue.increment(1)
}, { merge: true }).catch(err => console.error("Failed to click:", err));
});
}
db.collection(COLLECTION)
.orderBy('timestamp', 'desc')
.onSnapshot(snap => {
if (titleEl) {
db.collection(COLLECTION)
.orderBy('timestamp', 'desc')
.onSnapshot(snap => {
titleEl.textContent = snap.size > 0 ? `(${snap.size})` : '';
allComments = snap.docs.map(doc => doc.data());
renderPage();
}, err => {
console.error(err);
listEl.innerHTML = '<p class="no-comments">Could not load comments.</p>';
});
// ── Global click counter ─────────────────────────────────────
const clickBtn = document.getElementById('yuuka-click-btn');
const clickDisplay = document.getElementById('click-counter-display');
const clickSound = new Audio('sounds/yuuka_click.mp3');
if (clickBtn && clickDisplay) {
const statsRef = db.collection('stats').doc('global_clicks');
// Firestore applies local writes to this listener instantly,
// so clicks show up immediately without a manual optimistic update.
statsRef.onSnapshot(doc => {
const count = doc.exists ? doc.data().count : 0;
clickDisplay.textContent = (count || 0).toLocaleString();
}, err => {
console.error("Click counter error:", err);
clickDisplay.textContent = 'Error';
});
clickBtn.addEventListener('click', () => {
clickSound.currentTime = 0; // restart so rapid clicks all play
clickSound.play().catch(() => {});
statsRef.set({
count: firebase.firestore.FieldValue.increment(1)
}, { merge: true }).catch(err => console.error("Failed to click:", err));
});
}
// ── Submit ───────────────────────────────────────────────────
form.addEventListener('submit', async e => {
e.preventDefault();
const name = nameInput.value.trim().slice(0, MAX_NAME) || 'Anonymous';
const message = msgInput.value.trim().slice(0, MAX_CHARS);
const color = colorInput.value || DEFAULT_COLOR;
const avatarUrl = b64Input.value;
if (!message) { msgInput.focus(); return; }
const label = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = 'Posting...';
try {
await db.collection(COLLECTION).add({
name, message, color, avatarUrl,
timestamp: firebase.firestore.FieldValue.serverTimestamp()
});
nameInput.value = '';
msgInput.value = '';
updateCharCount();
} catch (err) {
console.error(err);
alert('Failed to post. Try again!');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = label;
}
});
});
allComments = [];
snap.forEach(doc => allComments.push(doc.data()));
renderPage();
// ── Build a single comment bubble (Steam layout) ─────────────────
function buildBubble(data) {
const div = document.createElement('div');
div.className = 'comment-bubble';
}, err => {
console.error(err);
listEl.innerHTML = '<p class="no-comments">Could not load comments. Check your Firebase config.</p>';
// Pending server timestamps are null until the write lands
const ts = data.timestamp ? data.timestamp.toDate() : new Date();
const timeStr = ts.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
// ── Submit ───────────────────────────────────────────────────
form.addEventListener('submit', async e => {
e.preventDefault();
const name = legacyUnescape(data.name) || 'Anonymous';
const message = legacyUnescape(data.message);
const name = clean(nameInput.value.trim()).slice(0, 32) || 'Anonymous';
const message = clean(msgInput.value.trim()).slice(0, MAX_CHARS);
const color = colorInput.value || '#5b6fde';
const avatarUrl = b64Input.value;
if (!message) { msgInput.focus(); return; }
submitBtn.disabled = true;
submitBtn.textContent = 'Posting...';
try {
await db.collection(COLLECTION).add({
name, message, color, avatarUrl,
timestamp: firebase.firestore.FieldValue.serverTimestamp()
});
nameInput.value = '';
msgInput.value = '';
charCountEl.textContent = MAX_CHARS;
charCountEl.style.color = '#364858';
} catch (err) {
console.error(err);
alert('Failed to post. Try again!');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'POST';
let avatarHtml;
if (typeof data.avatarUrl === 'string' && data.avatarUrl.startsWith('data:image/')) {
avatarHtml = `<img src="${esc(data.avatarUrl)}" class="comment-avatar" alt="">`;
} else {
const color = /^#[0-9a-f]{6}$/i.test(data.color) ? data.color : '#64748b';
avatarHtml = `<div class="comment-avatar" style="background:${color}">${esc(name.charAt(0).toUpperCase())}</div>`;
}
});
});
// ── Build a single comment bubble (Steam layout) ─────────────────
function buildBubble(data) {
const div = document.createElement('div');
div.className = 'comment-bubble';
const ts = data.timestamp ? data.timestamp.toDate() : new Date();
const timeStr = ts.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
const color = data.color || '#64748b';
const initial = (data.name || '?').charAt(0).toUpperCase();
let avatarHtml = '';
if (data.avatarUrl) {
avatarHtml = `<img src="${esc(data.avatarUrl)}" class="comment-avatar" alt="Avatar">`;
} else {
avatarHtml = `
<div class="comment-avatar" style="background:${esc(color)}">
${esc(initial)}
div.innerHTML = `
${avatarHtml}
<div class="comment-body">
<div class="comment-header">
<span class="comment-name">${esc(name)}</span>
<span class="comment-time">${timeStr}</span>
</div>
<p class="comment-text">${esc(message)}</p>
</div>
`;
return div;
}
div.innerHTML = `
${avatarHtml}
<div class="comment-body">
<div class="comment-header">
<span class="comment-name">${esc(data.name)}</span>
<span class="comment-time">${timeStr}</span>
</div>
<p class="comment-text">${esc(data.message)}</p>
</div>
`;
return div;
}
// ── Helpers ──────────────────────────────────────────────────────
// ── Demo bubbles shown before Firebase is set up ─────────────────
// Older comments were stored pre-escaped (< and > only), which made
// them render as literal "&lt;" once escaped again. Undo that here.
function legacyUnescape(str) {
if (!str) return '';
return String(str).replace(/&lt;/g, '<').replace(/&gt;/g, '>');
}
function showDemo(list) {
const demos = [
{ name: 'Visitor', message: 'cool site lol', color: '#3b82f6', timestamp: { toDate: () => new Date(Date.now() - 60000) } },
{ name: 'Ahmet', message: 'when is the gallery getting updated', color: '#10b981', timestamp: { toDate: () => new Date(Date.now() - 300000) } },
{ name: 'Someone', message: 'Ahmet museum is peak', color: '#8b5cf6', timestamp: { toDate: () => new Date(Date.now() - 800000) } },
];
list.innerHTML = '';
demos.forEach(d => list.appendChild(buildBubble(d)));
}
// ── Helpers ──────────────────────────────────────────────────────
function clean(str) {
return str.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
})();
+39 -62
View File
@@ -1,69 +1,46 @@
// Gallery tab — thumbnails from gallery/manifest.json + full-size lightbox.
// The manifest is (re)generated by generate_thumbnails.py.
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('gallery-container');
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const closeBtn = document.querySelector('.lightbox-close');
// Close lightbox when clicking the X
if (closeBtn) {
closeBtn.onclick = () => {
lightbox.style.display = "none";
lightboxImg.src = ""; // Clear source to stop GIFs
}
}
if (!container || !lightbox) return;
// Close lightbox when clicking outside the image
if (lightbox) {
lightbox.onclick = (e) => {
if (e.target !== lightboxImg && e.target !== closeBtn) {
lightbox.style.display = "none";
lightboxImg.src = ""; // Clear source to stop GIFs
}
}
}
// Function to load images into a specific container from a specific folder
const loadGallery = (containerId, folderName, totalImages) => {
const container = document.getElementById(containerId);
if (!container) return;
for (let i = 1; i <= totalImages; i++) {
const img = document.createElement('img');
img.alt = `${folderName} Image ${i}`;
// Load the thumbnail instead of the full image
img.src = `${folderName}/thumbnails/${i}.jpg`;
// If thumbnail fails to load, it means the image doesn't exist at all
img.onerror = () => { img.style.display = 'none'; };
// Open lightbox on click
img.onclick = () => {
lightbox.style.display = "block";
lightboxImg.src = ""; // Clear previous image
// Try loading original full quality image: jpg -> png -> gif
const tryLoad = (extensions) => {
if (extensions.length === 0) return; // All failed
const testExt = extensions.shift();
const tempImg = new Image();
tempImg.onload = () => {
lightboxImg.src = tempImg.src; // Success! Show it.
};
tempImg.onerror = () => {
tryLoad(extensions); // Failed, try next extension
};
tempImg.src = `${folderName}/${i}.${testExt}`;
};
// Start trying to find the full image
tryLoad(['jpg', 'png', 'gif']);
};
container.appendChild(img);
}
const openLightbox = (src) => {
lightboxImg.src = src;
lightbox.style.display = 'block';
};
// Load your original Gallery
loadGallery('gallery-container', 'gallery', 50);
});
const closeLightbox = () => {
lightbox.style.display = 'none';
lightboxImg.src = ''; // Clear source to stop GIFs
};
// Close on the X, on a click outside the image, or with Escape
lightbox.addEventListener('click', (e) => {
if (e.target !== lightboxImg) closeLightbox();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && lightbox.style.display === 'block') closeLightbox();
});
const thumbPath = (file) => `gallery/thumbnails/${file.replace(/\.[^.]+$/, '')}.jpg`;
fetch('gallery/manifest.json')
.then(res => {
if (!res.ok) throw new Error(res.status);
return res.json();
})
.then(files => {
files.forEach((file, i) => {
const img = document.createElement('img');
img.src = thumbPath(file);
img.alt = `Gallery image ${i + 1}`;
img.loading = 'lazy';
img.onclick = () => openLightbox(`gallery/${file}`);
container.appendChild(img);
});
})
.catch(err => console.error('Could not load gallery manifest:', err));
});
+8 -19
View File
@@ -59,15 +59,17 @@ document.addEventListener('DOMContentLoaded', () => {
}
// ── Navigate to a tab (updates URL + shows it) ────────
function navigateTo(tabId, push = true) {
function navigateTo(tabId) {
showTab(tabId);
if (push) {
history.pushState({ tab: tabId }, '', tabToPath(tabId));
}
window.scrollTo(0, 0);
history.pushState({ tab: tabId }, '', tabToPath(tabId));
}
// ── Initial load: show tab from URL ────────────────────
showTab(pathToTab(window.location.pathname));
// (also normalises unknown / legacy paths like /ahmet in the address bar)
const initialTab = pathToTab(window.location.pathname);
showTab(initialTab);
history.replaceState({ tab: initialTab }, '', tabToPath(initialTab) + window.location.search);
// ── Tab button clicks ──────────────────────────────────
const buttons = document.querySelectorAll('#top-line button');
@@ -78,22 +80,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
// ── Home button (logo) ─────────────────────────────────
const homeBtn = document.getElementById('home-btn');
if (homeBtn) {
homeBtn.addEventListener('click', (e) => {
e.preventDefault();
navigateTo('about');
});
}
// ── Back / forward browser buttons ─────────────────────
window.addEventListener('popstate', (e) => {
if (e.state && e.state.tab) {
showTab(e.state.tab);
} else {
showTab(pathToTab(window.location.pathname));
}
showTab(e.state && e.state.tab ? e.state.tab : pathToTab(window.location.pathname));
});
});
+76 -57
View File
@@ -1,80 +1,99 @@
// Yuuka Bot Dashboard — live Firestore reader
const YUUKA_FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
(() => {
const FIREBASE_CONFIG = {
apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0",
authDomain: "esef-514bf.firebaseapp.com",
projectId: "esef-514bf",
storageBucket: "esef-514bf.firebasestorage.app",
messagingSenderId: "353969651290",
appId: "1:353969651290:web:41e3aaf8e54c743702a263"
};
document.addEventListener("DOMContentLoaded", () => {
const dash = document.getElementById("yuukabot");
if (!dash) return;
// The bot pushes every 10s; no update for this long means it's down
// (generous so a visitor's slightly-off clock doesn't flip the dot).
const STALE_MS = 60 * 1000;
if (!firebase.apps.length) firebase.initializeApp(YUUKA_FIREBASE_CONFIG);
const db = firebase.firestore();
document.addEventListener("DOMContentLoaded", () => {
const dash = document.getElementById("yuukabot");
if (!dash) return;
db.collection("sysinfo").doc("server").onSnapshot(
(doc) => {
if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG);
const db = firebase.firestore();
const dot = document.getElementById("dash-status-dot");
let online = false;
let updatedAt = null; // ms; null for bots that don't send updatedAt yet
let loaded = false; // keep the grey "pending" dot until first data
function refreshDot() {
if (!loaded) return;
const fresh = updatedAt == null || Date.now() - updatedAt < STALE_MS;
if (dot) dot.className = online && fresh ? "status-dot" : "status-dot offline";
}
// Re-check periodically: a crashed bot sends no more snapshots
setInterval(refreshDot, 5000);
db.collection("sysinfo").doc("server").onSnapshot(doc => {
loaded = true;
const d = doc.exists ? doc.data() : {};
online = !!d.online;
updatedAt = d.updatedAt ? d.updatedAt.toMillis() : null;
refreshDot();
if (!doc.exists) return;
const d = doc.data();
// Online dot
const dot = document.getElementById("dash-status-dot");
if (dot) dot.className = d.online ? "status-dot" : "status-dot offline";
// Uptime
setText("dash-uptime", d.uptime);
setText("dash-bot-uptime", d.botUptime ? "bot up " + d.botUptime : "");
// CPU, RAM, Disk, Swap — bars
setBar("dash-cpu-val", "dash-cpu-bar", null, d.cpu, "%", "cpu");
setBar("dash-ram-val", "dash-ram-bar", null, d.ram, "%", "ram");
setText("dash-ram-detail", d.ramUsed ? d.ramUsed + " / " + d.ramTotal : "");
setBar("dash-disk-val", "dash-disk-bar", null, d.disk, "%", "disk");
setText("dash-disk-detail", d.diskUsed ? d.diskUsed + " / " + d.diskTotal : "");
setBar("dash-swap-val", "dash-swap-bar", null, d.swap, "%", "swap");
setText("dash-swap-detail", d.swapUsed ? d.swapUsed + " / " + d.swapTotal : "");
// CPU, RAM, Disk, Swap — value + bar
setBar("dash-cpu-val", "dash-cpu-bar", d.cpu);
setBar("dash-ram-val", "dash-ram-bar", d.ram);
setBar("dash-disk-val", "dash-disk-bar", d.disk);
setBar("dash-swap-val", "dash-swap-bar", d.swap);
setText("dash-ram-detail", usedOfTotal(d.ramUsed, d.ramTotal));
setText("dash-disk-detail", usedOfTotal(d.diskUsed, d.diskTotal));
setText("dash-swap-detail", usedOfTotal(d.swapUsed, d.swapTotal));
// Load average
if (d.load1 != null) {
setText("dash-load", d.load1 + " / " + d.load5 + " / " + d.load15);
}
// Network
if (d.netSent) {
setText("dash-net", "sent " + d.netSent);
setText("dash-net-detail", "recv " + (d.netRecv || "—"));
}
// Processes
// Fastfetch
setText("dash-fetch", d.fetch);
if (d.processes != null) {
setText("dash-procs", d.processes.toLocaleString());
}
},
() => {
const dot = document.getElementById("dash-status-dot");
if (dot) dot.className = "status-dot offline";
setText("dash-fetch", d.fetch);
}, () => {
loaded = true;
online = false;
refreshDot();
});
});
function usedOfTotal(used, total) {
return used ? used + " / " + total : "";
}
function setText(id, value) {
const el = document.getElementById(id);
if (el && value != null) el.textContent = value;
}
function setBar(valId, barId, value) {
const valEl = document.getElementById(valId);
const barEl = document.getElementById(barId);
if (valEl && value != null) {
valEl.textContent = value;
const unit = document.createElement("span");
unit.textContent = "%";
valEl.appendChild(unit);
}
if (barEl) {
barEl.style.width = Math.min(Math.max(value || 0, 0), 100) + "%";
}
);
});
function setText(id, value) {
const el = document.getElementById(id);
if (el && value != null) el.textContent = value;
}
function setBar(valId, barId, _detailId, value, suffix, barClass) {
const valEl = document.getElementById(valId);
const barEl = document.getElementById(barId);
if (valEl && value != null) {
valEl.innerHTML = value + '<span>' + suffix + '</span>';
}
if (barEl) {
barEl.style.width = Math.min(Math.max(value || 0, 0), 100) + "%";
barEl.className = "card-bar-fill " + barClass;
}
}
})();