553 lines
22 KiB
JavaScript
553 lines
22 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- Mobile Menu Toggle ---
|
|
const menuToggle = document.querySelector('.menu-toggle');
|
|
const navLinks = document.querySelector('.nav-links');
|
|
|
|
if (menuToggle && navLinks) {
|
|
menuToggle.addEventListener('click', () => {
|
|
navLinks.classList.toggle('mobile-open');
|
|
const icon = menuToggle.querySelector('i');
|
|
if (icon) {
|
|
icon.classList.toggle('fa-bars');
|
|
icon.classList.toggle('fa-xmark');
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Tablet Mockup Interactive Demo ---
|
|
const captureBtn = document.getElementById('demo-capture-btn');
|
|
const screenWelcome = document.getElementById('demo-screen-welcome');
|
|
const screenCountdown = document.getElementById('demo-screen-countdown');
|
|
const screenPreview = document.getElementById('demo-screen-preview');
|
|
const timerDisplay = document.getElementById('demo-timer');
|
|
const flashOverlay = document.getElementById('demo-flash');
|
|
|
|
const btnPrint = document.getElementById('demo-btn-print');
|
|
const btnReset = document.getElementById('demo-btn-reset');
|
|
|
|
if (captureBtn && screenWelcome && screenCountdown && screenPreview && timerDisplay && flashOverlay) {
|
|
captureBtn.addEventListener('click', () => {
|
|
// Step 1: Switch screen to countdown
|
|
screenWelcome.style.display = 'none';
|
|
screenCountdown.style.display = 'flex';
|
|
|
|
// Step 2: 3-second countdown countdown
|
|
let timeLeft = 3;
|
|
timerDisplay.textContent = timeLeft;
|
|
|
|
const timerInterval = setInterval(() => {
|
|
timeLeft--;
|
|
if (timeLeft > 0) {
|
|
timerDisplay.textContent = timeLeft;
|
|
} else {
|
|
clearInterval(timerInterval);
|
|
// Step 3: Trigger Camera Flash overlay
|
|
flashOverlay.classList.add('flash-active');
|
|
|
|
setTimeout(() => {
|
|
flashOverlay.classList.remove('flash-active');
|
|
// Step 4: Transition to captured photo preview screen
|
|
screenCountdown.style.display = 'none';
|
|
screenPreview.style.display = 'flex';
|
|
}, 400);
|
|
}
|
|
}, 800);
|
|
});
|
|
}
|
|
|
|
if (btnReset && screenWelcome && screenPreview) {
|
|
btnReset.addEventListener('click', () => {
|
|
screenPreview.style.display = 'none';
|
|
screenWelcome.style.display = 'flex';
|
|
});
|
|
}
|
|
|
|
// --- Accordion FAQ ---
|
|
const accordionHeaders = document.querySelectorAll('.accordion-header');
|
|
|
|
accordionHeaders.forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
const item = header.parentElement;
|
|
const isActive = item.classList.contains('active');
|
|
|
|
// Close all items
|
|
document.querySelectorAll('.accordion-item').forEach(i => {
|
|
i.classList.remove('active');
|
|
const content = i.querySelector('.accordion-content');
|
|
if (content) content.style.maxHeight = null;
|
|
});
|
|
|
|
// Toggle active item
|
|
if (!isActive) {
|
|
item.classList.add('active');
|
|
const content = item.querySelector('.accordion-content');
|
|
if (content) {
|
|
content.style.maxHeight = content.scrollHeight + "px";
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- Feedback Hub Logic ---
|
|
const feedbackForm = document.getElementById('feedback-form');
|
|
const feedbackList = document.getElementById('feedback-list');
|
|
const filterButtons = document.querySelectorAll('.filter-btn');
|
|
|
|
let feedbackData = [];
|
|
|
|
// Upvote storage track (local to session/device)
|
|
let upvotedIds = JSON.parse(localStorage.getItem('schnappix_upvoted_ids')) || [];
|
|
|
|
const saveUpvotesToLocal = () => {
|
|
localStorage.setItem('schnappix_upvoted_ids', JSON.stringify(upvotedIds));
|
|
};
|
|
|
|
// Admin State
|
|
let isAdmin = sessionStorage.getItem('schnappix_is_admin') === 'true';
|
|
|
|
const loadFeedback = async () => {
|
|
if (!feedbackList) return;
|
|
try {
|
|
const response = await fetch('feedback_backend.php?action=get');
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
feedbackData = result.data;
|
|
renderFeedback(getActiveFilter());
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading feedback:', error);
|
|
}
|
|
};
|
|
|
|
const renderFeedback = (filter = 'all') => {
|
|
if (!feedbackList) return;
|
|
feedbackList.innerHTML = '';
|
|
|
|
// Sort by upvotes descending
|
|
const sortedData = [...feedbackData].sort((a, b) => b.upvotes - a.upvotes);
|
|
|
|
sortedData.forEach(item => {
|
|
if (filter !== 'all' && item.type !== filter) return;
|
|
|
|
const isVoted = upvotedIds.includes(item.id);
|
|
const card = document.createElement('div');
|
|
card.className = 'feedback-item glass';
|
|
|
|
let badgeIcon = '✨';
|
|
let categoryLabel = 'Feature';
|
|
if (item.type === 'bug') {
|
|
badgeIcon = '🐛';
|
|
categoryLabel = 'Bug';
|
|
} else if (item.type === 'love') {
|
|
badgeIcon = '💖';
|
|
categoryLabel = 'Lob';
|
|
}
|
|
|
|
// Show admin actions if user is admin
|
|
let adminActionsHtml = '';
|
|
if (isAdmin) {
|
|
adminActionsHtml = `
|
|
<div class="fb-admin-actions" style="margin-top: 10px; margin-bottom: 15px; display: flex; gap: 10px;">
|
|
<button class="fb-admin-answer-btn btn btn-secondary" style="padding: 5px 10px; font-size: 12px;" data-id="${item.id}" title="Antworten"><i class="fa-solid fa-reply"></i> Antworten</button>
|
|
<button class="fb-admin-delete-btn btn" style="padding: 5px 10px; font-size: 12px; background: rgba(255,50,50,0.2); color: #ff6b6b;" data-id="${item.id}" title="Eintrag löschen"><i class="fa-solid fa-trash"></i> Löschen</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
let answerHtml = '';
|
|
if (item.answer) {
|
|
answerHtml = `
|
|
<div class="fb-answer" style="margin-top: 15px; padding: 12px; background: rgba(var(--accent-color-rgb), 0.1); border-left: 3px solid var(--accent-color); border-radius: 4px;">
|
|
<div class="fb-answer-header" style="font-weight: bold; font-size: 13px; color: var(--accent-color); margin-bottom: 5px;"><i class="fa-solid fa-check-circle"></i> Offizielle Antwort</div>
|
|
<p style="margin: 0; font-size: 14px;">${escapeHtml(item.answer)}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
card.innerHTML = `
|
|
<div class="fb-card-content">
|
|
<div class="fb-meta">
|
|
<span class="fb-tag ${item.type}">${badgeIcon} ${categoryLabel}</span>
|
|
<span class="fb-author">von ${escapeHtml(item.author)}</span>
|
|
</div>
|
|
${adminActionsHtml}
|
|
<h4>${escapeHtml(item.title)}</h4>
|
|
<p>${escapeHtml(item.text)}</p>
|
|
${answerHtml}
|
|
</div>
|
|
<div class="fb-upvote-section">
|
|
<button class="upvote-btn ${isVoted ? 'voted' : ''}" data-id="${item.id}" aria-label="Abstimmen">
|
|
<i class="fa-solid fa-caret-up"></i>
|
|
<span>${item.upvotes}</span>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
feedbackList.appendChild(card);
|
|
});
|
|
|
|
setupUpvoteButtons();
|
|
setupAdminDeleteButtons();
|
|
setupAdminAnswerButtons();
|
|
};
|
|
|
|
const setupAdminDeleteButtons = () => {
|
|
if (!isAdmin) return;
|
|
const deleteButtons = document.querySelectorAll('.fb-admin-delete-btn');
|
|
deleteButtons.forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.getAttribute('data-id');
|
|
if (confirm('Möchten Sie diesen Eintrag wirklich permanent löschen?')) {
|
|
try {
|
|
const response = await fetch('feedback_backend.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'delete', id: id, secret: sessionStorage.getItem('schnappix_admin_secret') || '' })
|
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
loadFeedback();
|
|
} else {
|
|
alert('Fehler beim Löschen.');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const setupAdminAnswerButtons = () => {
|
|
if (!isAdmin) return;
|
|
const answerButtons = document.querySelectorAll('.fb-admin-answer-btn');
|
|
answerButtons.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const id = btn.getAttribute('data-id');
|
|
const item = feedbackData.find(i => i.id === id);
|
|
const currentAnswer = item ? (item.answer || '') : '';
|
|
|
|
const answer = prompt('Antwort für das Feedback (leer lassen zum Löschen der Antwort):', currentAnswer);
|
|
if (answer !== null) {
|
|
submitAnswer(id, answer);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const submitAnswer = async (id, answer) => {
|
|
try {
|
|
const response = await fetch('feedback_backend.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'answer', id: id, answer: answer, secret: sessionStorage.getItem('schnappix_admin_secret') || '' })
|
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
loadFeedback();
|
|
} else {
|
|
alert('Fehler beim Speichern der Antwort.');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
const escapeHtml = (str) => {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
};
|
|
|
|
const setupUpvoteButtons = () => {
|
|
const buttons = document.querySelectorAll('.upvote-btn');
|
|
buttons.forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.getAttribute('data-id');
|
|
const isVoted = upvotedIds.includes(id);
|
|
|
|
// Optimistic UI update
|
|
const btnSpan = btn.querySelector('span');
|
|
let currentVotes = parseInt(btnSpan.textContent);
|
|
|
|
if (isVoted) {
|
|
btn.classList.remove('voted');
|
|
btnSpan.textContent = currentVotes - 1;
|
|
upvotedIds = upvotedIds.filter(vId => vId !== id);
|
|
} else {
|
|
btn.classList.add('voted');
|
|
btnSpan.textContent = currentVotes + 1;
|
|
upvotedIds.push(id);
|
|
}
|
|
saveUpvotesToLocal();
|
|
|
|
try {
|
|
await fetch('feedback_backend.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'upvote', id: id, undo: isVoted })
|
|
});
|
|
} catch (e) {
|
|
console.error('Error upvoting', e);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const getActiveFilter = () => {
|
|
const activeBtn = document.querySelector('.filter-btn.active');
|
|
return activeBtn ? activeBtn.getAttribute('data-filter') : 'all';
|
|
};
|
|
|
|
// Filter switching
|
|
filterButtons.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
filterButtons.forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
renderFeedback(btn.getAttribute('data-filter'));
|
|
});
|
|
});
|
|
|
|
// Form Submission
|
|
if (feedbackForm) {
|
|
feedbackForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const authorInput = document.getElementById('fb-author');
|
|
const typeSelect = document.getElementById('fb-type');
|
|
const titleInput = document.getElementById('fb-title');
|
|
const textInput = document.getElementById('fb-text');
|
|
const submitBtn = feedbackForm.querySelector('button[type="submit"]');
|
|
|
|
const newFb = {
|
|
action: 'add',
|
|
author: authorInput.value,
|
|
type: typeSelect.value,
|
|
title: titleInput.value,
|
|
text: textInput.value
|
|
};
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Wird gesendet...';
|
|
|
|
try {
|
|
const response = await fetch('feedback_backend.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(newFb)
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Auto-upvote own post
|
|
upvotedIds.push(result.data.id);
|
|
saveUpvotesToLocal();
|
|
|
|
feedbackForm.reset();
|
|
await loadFeedback(); // Reload the list
|
|
|
|
feedbackList.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
} else {
|
|
alert('Fehler beim Senden des Feedbacks.');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert('Netzwerkfehler. Bitte später erneut versuchen.');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Absenden';
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Tester Registration Logic ---
|
|
const testerForm = document.getElementById('tester-form');
|
|
if (testerForm) {
|
|
testerForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const emailInput = document.getElementById('tester-email');
|
|
const submitBtn = document.getElementById('tester-submit-btn');
|
|
const successMsg = document.getElementById('tester-success-msg');
|
|
const errorMsg = document.getElementById('tester-error-msg');
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Wird gesendet...';
|
|
successMsg.style.display = 'none';
|
|
errorMsg.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch('tester_backend.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: emailInput.value })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
successMsg.style.display = 'block';
|
|
testerForm.reset();
|
|
} else {
|
|
errorMsg.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> ' + (result.error || 'Unbekannter Fehler');
|
|
errorMsg.style.display = 'block';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
errorMsg.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> Netzwerkfehler. Bitte später erneut versuchen.';
|
|
errorMsg.style.display = 'block';
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Als Tester anmelden';
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Modal Management for Impressum & Privacy ---
|
|
const modalTriggers = document.querySelectorAll('.legal-trigger');
|
|
const modals = document.querySelectorAll('.modal-overlay');
|
|
const closeButtons = document.querySelectorAll('.modal-close');
|
|
|
|
// --- Modal close helpers for admin modals ---
|
|
const closeModal = (modal) => {
|
|
modal.classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
};
|
|
|
|
closeButtons.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const modal = btn.closest('.modal-overlay');
|
|
if (modal) closeModal(modal);
|
|
});
|
|
});
|
|
|
|
modals.forEach(modal => {
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) {
|
|
closeModal(modal);
|
|
}
|
|
});
|
|
});
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
modals.forEach(modal => {
|
|
if (modal.classList.contains('open')) closeModal(modal);
|
|
});
|
|
}
|
|
});
|
|
|
|
// --- Admin Panel Modals & Operations ---
|
|
const adminTrigger = document.getElementById('admin-login-trigger');
|
|
const adminLoginModal = document.getElementById('admin-login-modal');
|
|
const adminPanelModal = document.getElementById('admin-panel-modal');
|
|
const adminLoginForm = document.getElementById('admin-login-form');
|
|
const adminPasswordInput = document.getElementById('admin-password');
|
|
const adminLoginError = document.getElementById('admin-login-error');
|
|
|
|
const adminLogoutBtn = document.getElementById('admin-logout-btn');
|
|
const adminExportBtn = document.getElementById('admin-export-btn');
|
|
const exportContainer = document.getElementById('export-container');
|
|
const exportTextarea = document.getElementById('export-textarea');
|
|
|
|
if (adminTrigger) {
|
|
adminTrigger.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
if (isAdmin) {
|
|
adminPanelModal.classList.add('open');
|
|
} else {
|
|
adminLoginModal.classList.add('open');
|
|
adminPasswordInput.focus();
|
|
}
|
|
document.body.style.overflow = 'hidden';
|
|
});
|
|
}
|
|
|
|
if (adminLoginForm) {
|
|
adminLoginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const password = adminPasswordInput.value;
|
|
|
|
try {
|
|
const response = await fetch('feedback_backend.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'verify_password', secret: password })
|
|
});
|
|
|
|
if (response.ok) {
|
|
isAdmin = true;
|
|
sessionStorage.setItem('schnappix_is_admin', 'true');
|
|
sessionStorage.setItem('schnappix_admin_secret', password);
|
|
adminPasswordInput.value = '';
|
|
adminLoginError.style.display = 'none';
|
|
|
|
// Switch Modals
|
|
adminLoginModal.classList.remove('open');
|
|
adminPanelModal.classList.add('open');
|
|
|
|
// Re-render to show trash cans
|
|
renderFeedback(getActiveFilter());
|
|
} else {
|
|
adminLoginError.style.display = 'block';
|
|
adminPasswordInput.value = '';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
adminLoginError.style.display = 'block';
|
|
}
|
|
});
|
|
}
|
|
|
|
if (adminLogoutBtn) {
|
|
adminLogoutBtn.addEventListener('click', () => {
|
|
isAdmin = false;
|
|
sessionStorage.removeItem('schnappix_is_admin');
|
|
adminPanelModal.classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
|
|
// Hide export code if visible
|
|
if (exportContainer) exportContainer.style.display = 'none';
|
|
|
|
// Re-render to hide trash cans
|
|
renderFeedback(getActiveFilter());
|
|
});
|
|
}
|
|
|
|
if (adminExportBtn && exportContainer && exportTextarea) {
|
|
adminExportBtn.addEventListener('click', () => {
|
|
// Strip out own custom elements or internal helper flags if necessary
|
|
const cleanFeedback = feedbackData.map(item => ({
|
|
id: item.id,
|
|
author: item.author,
|
|
type: item.type,
|
|
title: item.title,
|
|
text: item.text,
|
|
upvotes: item.upvotes,
|
|
voted: false
|
|
}));
|
|
|
|
exportTextarea.value = JSON.stringify(cleanFeedback, null, 4);
|
|
exportContainer.style.display = 'block';
|
|
exportTextarea.select();
|
|
});
|
|
}
|
|
|
|
// Modal Close overrides to reset error states
|
|
const allCloseButtons = document.querySelectorAll('.modal-close');
|
|
allCloseButtons.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
if (adminLoginError) adminLoginError.style.display = 'none';
|
|
if (exportContainer) exportContainer.style.display = 'none';
|
|
});
|
|
});
|
|
|
|
// Initial render
|
|
if (feedbackList) {
|
|
loadFeedback();
|
|
}
|
|
});
|