diff --git a/public/img/icon-192.png b/public/icons/icon-192.png
similarity index 100%
rename from public/img/icon-192.png
rename to public/icons/icon-192.png
diff --git a/public/img/icon-512.png b/public/icons/icon-512.png
similarity index 100%
rename from public/img/icon-512.png
rename to public/icons/icon-512.png
diff --git a/public/index.html b/public/index.html
index 49b6280..2c57e97 100644
--- a/public/index.html
+++ b/public/index.html
@@ -21,24 +21,41 @@
+
Who pays?
@@ -52,12 +69,13 @@
src="img/a.png"
alt="Person"
class="w-48 h-48 rounded-full shadow-md border-4 border-white object-cover transition-transform duration-500"
+ style="display: none;"
/>
- Load ...
+ LΓ€dt ...
@@ -94,39 +112,6 @@
.then(reg => console.log("β
Service Worker registriert", reg.scope))
.catch(err => console.error("β SW Fehler:", err));
}
-
- // PWA Install Prompt
- let deferredPrompt;
- const installBanner = document.getElementById("installBanner");
- const installBtn = document.getElementById("installBtn");
- const dismissBtn = document.getElementById("dismissBtn");
-
- window.addEventListener("beforeinstallprompt", (e) => {
- console.log("β
PWA installierbar!");
- e.preventDefault();
- deferredPrompt = e;
- installBanner.classList.remove("hidden");
- });
-
- installBtn.addEventListener("click", async () => {
- if (!deferredPrompt) return;
-
- deferredPrompt.prompt();
- const { outcome } = await deferredPrompt.userChoice;
- console.log(`PWA install: ${outcome}`);
-
- deferredPrompt = null;
- installBanner.classList.add("hidden");
- });
-
- dismissBtn.addEventListener("click", () => {
- installBanner.classList.add("hidden");
- });
-
- window.addEventListener("appinstalled", () => {
- console.log("β
PWA erfolgreich installiert!");
- deferredPrompt = null;
- });
diff --git a/public/manifest.json b/public/manifest.json
index f812ea0..490c007 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -11,25 +11,25 @@
"categories": ["utilities", "lifestyle"],
"icons": [
{
- "src": "/img/icon-192.png",
+ "src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
- "src": "/img/icon-192.png",
+ "src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
- "src": "/img/icon-512.png",
+ "src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
- "src": "/img/icon-512.png",
+ "src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
diff --git a/public/script.js b/public/script.js
index 731560e..505e052 100644
--- a/public/script.js
+++ b/public/script.js
@@ -4,11 +4,56 @@ const button = document.getElementById("toggleButton");
const reasonInput = document.getElementById("reasonInput");
const lastReasonBox = document.getElementById("lastReasonBox");
const lastReasonText = document.getElementById("lastReasonText");
+const mainContent = document.getElementById("mainContent");
+
+// Preload images to prevent flickering
+const imageCache = {};
+function preloadImage(src) {
+ return new Promise((resolve, reject) => {
+ if (imageCache[src]) {
+ resolve(imageCache[src]);
+ return;
+ }
+
+ const image = new Image();
+ image.onload = () => {
+ imageCache[src] = image;
+ resolve(image);
+ };
+ image.onerror = reject;
+ image.src = src;
+ });
+}
+
+// Preload all images on page load
+async function preloadAllImages() {
+ try {
+ await Promise.all([
+ preloadImage('img/a.png'),
+ preloadImage('img/b.png'),
+ preloadImage('img/bgA.jpg'),
+ preloadImage('img/bgB.jpg')
+ ]);
+ console.log("β
Alle Bilder vorgeladen");
+ } catch (err) {
+ console.warn("β οΈ Fehler beim Vorladen:", err);
+ }
+}
async function loadStatus() {
+ // Preload images first
+ await preloadAllImages();
+
const res = await fetch("/status");
const data = await res.json();
- showStatus(data.current, false);
+
+ // Set background and image without animation on first load
+ setBackgroundAndImage(data.current, false);
+
+ // Show content smoothly
+ img.style.display = 'block';
+ mainContent.classList.remove('loading');
+ mainContent.classList.add('loaded');
// Show last reason if available
if (data.lastReason) {
@@ -17,59 +62,63 @@ async function loadStatus() {
}
}
-function showStatus(person, animate = true) {
+function setBackgroundAndImage(person, animate = true) {
const file = person === "A" ? "img/a.png" : "img/b.png";
const label = person === "A" ? "Honey" : "Kirby";
+ const bgImage = person === "A" ? "url('img/bgA.jpg')" : "url('img/bgB.jpg')";
- // Hintergrundbilder fΓΌr A und B
- const bgA = "url('img/bgA.jpg')";
- const bgB = "url('img/bgB.jpg')";
-
- // Hintergrundanimation (optional)
- document.body.style.transition = "background 0.4s ease";
-
- // Hintergrund setzen
- document.body.style.backgroundImage = person === "A" ? bgA : bgB;
+ // Set background immediately (smooth transition via CSS)
+ document.body.style.backgroundImage = bgImage;
document.body.style.backgroundSize = "cover";
document.body.style.backgroundPosition = "center";
if (animate) {
img.classList.add("rotate");
- }
-
- setTimeout(() => {
+
+ // Change image and name in the middle of rotation
+ setTimeout(() => {
+ img.src = file;
+ nameEl.textContent = label;
+ }, 300); // Half of rotation duration
+
+ // Remove rotation class after animation
+ setTimeout(() => {
+ img.classList.remove("rotate");
+ }, 600);
+ } else {
+ // First load - no animation
img.src = file;
nameEl.textContent = label;
- }, 150);
-
- setTimeout(() => {
- img.classList.remove("rotate");
- }, 600);
+ }
}
-
button.addEventListener("click", async () => {
const reason = reasonInput.value.trim();
- const res = await fetch("/toggle", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({ reason: reason || undefined })
- });
-
- const data = await res.json();
- showStatus(data.current);
-
- // Update last reason display
- if (reason) {
- lastReasonText.textContent = reason;
- lastReasonBox.classList.remove("hidden");
+ try {
+ const res = await fetch("/toggle", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ reason: reason || undefined })
+ });
+
+ const data = await res.json();
+ setBackgroundAndImage(data.current, true);
+
+ // Update last reason display
+ if (reason) {
+ lastReasonText.textContent = reason;
+ lastReasonBox.classList.remove("hidden");
+ }
+
+ // Clear input
+ reasonInput.value = "";
+ } catch (err) {
+ console.error("Fehler beim Wechseln:", err);
+ nameEl.textContent = "Fehler!";
}
-
- // Clear input
- reasonInput.value = "";
});
// Allow Enter key to trigger switch
@@ -79,4 +128,5 @@ reasonInput.addEventListener("keypress", (e) => {
}
});
+// Start loading
loadStatus();
\ No newline at end of file
diff --git a/public/style.css b/public/style.css
index 68511bd..dddabde 100644
--- a/public/style.css
+++ b/public/style.css
@@ -19,11 +19,11 @@
opacity: 1;
}
50% {
- transform: rotateY(180deg); /* Half flip */
- opacity: 0; /* Make the front disappear */
+ transform: rotateY(90deg);
+ opacity: 0.3;
}
100% {
- transform: rotateY(360deg); /* Complete flip */
+ transform: rotateY(0deg);
opacity: 1;
}
}
@@ -31,5 +31,19 @@
.rotate {
animation: rotate 0.6s ease-in-out;
transform-style: preserve-3d;
- backface-visibility: hidden; /* Hide the back during flip */
+}
+
+/* Smooth transitions */
+#personImg {
+ will-change: transform, opacity;
+}
+
+#personName {
+ will-change: opacity;
+ transition: opacity 0.3s ease;
+}
+
+/* Background transition */
+body {
+ will-change: background-image;
}
\ No newline at end of file
diff --git a/server.js b/server.js
index 37f0292..75af079 100644
--- a/server.js
+++ b/server.js
@@ -57,20 +57,43 @@ const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || "http://localhost:3000";
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || "https://discord.com/api/webhooks/1450872860607381515/zRbiXOykKPUfLJ_H87wD2OxQJPLdro6yQzU2T48Pg4fxSNaXEqBFgxdHGTiIDmmKGHIF";
// --------------------
-// State Handling
+// State Handling - MIT PERSISTENT lastReason
// --------------------
function getState() {
if (!fs.existsSync(stateFile)) {
+ const initialState = {
+ current: "A",
+ lastReason: null
+ };
fs.writeFileSync(
stateFile,
- JSON.stringify({ current: "A", lastReason: null }, null, 2)
+ JSON.stringify(initialState, null, 2)
);
+ console.log("β
Neue state.json erstellt");
+ return initialState;
+ }
+
+ try {
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
+ // Ensure lastReason exists in state
+ if (!state.hasOwnProperty('lastReason')) {
+ state.lastReason = null;
+ }
+ console.log(`π State geladen: ${state.current}, Letzter Grund: ${state.lastReason || 'keiner'}`);
+ return state;
+ } catch (err) {
+ console.error("β Fehler beim Lesen von state.json:", err);
+ return { current: "A", lastReason: null };
}
- return JSON.parse(fs.readFileSync(stateFile, "utf-8"));
}
function setState(newState) {
- fs.writeFileSync(stateFile, JSON.stringify(newState, null, 2));
+ try {
+ fs.writeFileSync(stateFile, JSON.stringify(newState, null, 2));
+ console.log(`πΎ State gespeichert: ${newState.current}, Grund: ${newState.lastReason || 'keiner'}`);
+ } catch (err) {
+ console.error("β Fehler beim Speichern von state.json:", err);
+ }
}
// --------------------
@@ -133,7 +156,8 @@ async function sendDiscordEmbed(previousKey, currentKey, reason) {
// --------------------
app.get("/status", (req, res) => {
- res.json(getState());
+ const state = getState();
+ res.json(state);
});
app.post("/toggle", async (req, res) => {
@@ -141,8 +165,13 @@ app.post("/toggle", async (req, res) => {
const previousKey = state.current;
const reason = req.body?.reason;
+ // Wechseln
state.current = state.current === "A" ? "B" : "A";
+
+ // Grund speichern (oder null wenn keiner angegeben)
state.lastReason = reason || null;
+
+ // State persistent speichern
setState(state);
const logMessage = reason
@@ -151,6 +180,7 @@ app.post("/toggle", async (req, res) => {
console.log(logMessage);
+ // Discord Benachrichtigung
await sendDiscordEmbed(previousKey, state.current, reason);
res.json({
@@ -167,27 +197,22 @@ app.get("/health", (req, res) => {
// --------------------
// SPA Fallback - NO CRASH
// --------------------
-// Only serve index.html for navigation requests, not for missing static files
app.use((req, res, next) => {
- // If request already handled by static middleware, skip
if (res.headersSent) return;
- // If it's an API route that wasn't matched, 404
if (req.url.startsWith('/api/')) {
return res.status(404).json({ error: 'Not found' });
}
- // If Accept header indicates browser navigation (HTML), serve index.html
const acceptsHtml = req.accepts('html');
if (acceptsHtml) {
return res.sendFile(path.join(__dirname, "public/index.html"));
}
- // Otherwise 404
res.status(404).send('Not found');
});
-// Error handler to prevent crashes
+// Error handler
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({ error: 'Internal server error' });
@@ -202,6 +227,12 @@ app.listen(PORT, "0.0.0.0", () => {
console.log(`π Static files directory: ${path.join(__dirname, "public")}`);
console.log(`π Trust proxy: enabled (for reverse proxy support)`);
+ // Initial state laden und anzeigen
+ const initialState = getState();
+ console.log(`\nπ² Aktueller Status:`);
+ console.log(` π€ Wer zahlt: ${CONFIG[initialState.current].name}`);
+ console.log(` π Letzter Grund: ${initialState.lastReason || 'Keiner gespeichert'}`);
+
// Verify critical files exist
const criticalFiles = [
'public/index.html',
@@ -216,7 +247,7 @@ app.listen(PORT, "0.0.0.0", () => {
console.log(` ${exists ? 'β
' : 'β'} ${file}`);
});
- console.log('\nπ Test URLs (adjust domain for your setup):');
+ console.log('\nπ Test URLs:');
console.log(` http://localhost:${PORT}/`);
console.log(` http://localhost:${PORT}/service-worker.js`);
console.log(` http://localhost:${PORT}/manifest.json\n`);