first commit
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 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');
|
||||
|
||||
// Default Seed Data
|
||||
const defaultFeedback = [
|
||||
{
|
||||
id: 'fb-1',
|
||||
author: 'Markus T.',
|
||||
type: 'feature',
|
||||
title: 'QR-Code Download für Gäste',
|
||||
text: 'Es wäre super cool, wenn nach der Aufnahme ein QR-Code angezeigt wird, über den sich die Gäste das Foto direkt auf das Handy laden können.',
|
||||
upvotes: 42,
|
||||
voted: false
|
||||
},
|
||||
{
|
||||
id: 'fb-2',
|
||||
author: 'Sabrina',
|
||||
type: 'love',
|
||||
title: 'Sticker-Editor ist genial!',
|
||||
text: 'Unsere Hochzeitsgäste haben stundenlang Emojis und Eventtexte verschoben. Die Bedienung ist wirklich super intuitiv und einfach.',
|
||||
upvotes: 28,
|
||||
voted: false
|
||||
},
|
||||
{
|
||||
id: 'fb-3',
|
||||
author: 'EventTech GmbH',
|
||||
type: 'bug',
|
||||
title: 'Drucker-Timeout bei schlechtem WLAN',
|
||||
text: 'Manchmal meldet die App einen Fehler, wenn der Drucker im lokalen Netzwerk kurz die Verbindung verliert. Ein automatischer Retry wäre hilfreich.',
|
||||
upvotes: 15,
|
||||
voted: false
|
||||
}
|
||||
];
|
||||
|
||||
// Load from LocalStorage or seed defaults
|
||||
let feedbackData = JSON.parse(localStorage.getItem('schnappix_feedback'));
|
||||
if (!feedbackData) {
|
||||
feedbackData = defaultFeedback;
|
||||
localStorage.setItem('schnappix_feedback', JSON.stringify(feedbackData));
|
||||
}
|
||||
|
||||
// Upvote storage track (local to session/device)
|
||||
let upvotedIds = JSON.parse(localStorage.getItem('schnappix_upvoted_ids')) || [];
|
||||
|
||||
const saveToLocalStorage = () => {
|
||||
localStorage.setItem('schnappix_feedback', JSON.stringify(feedbackData));
|
||||
localStorage.setItem('schnappix_upvoted_ids', JSON.stringify(upvotedIds));
|
||||
};
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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>
|
||||
<h4>${escapeHtml(item.title)}</h4>
|
||||
<p>${escapeHtml(item.text)}</p>
|
||||
</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();
|
||||
};
|
||||
|
||||
const escapeHtml = (str) => {
|
||||
return 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', () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
const index = feedbackData.findIndex(item => item.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
if (upvotedIds.includes(id)) {
|
||||
// Undo upvote
|
||||
feedbackData[index].upvotes--;
|
||||
upvotedIds = upvotedIds.filter(vId => vId !== id);
|
||||
} else {
|
||||
// Upvote
|
||||
feedbackData[index].upvotes++;
|
||||
upvotedIds.push(id);
|
||||
}
|
||||
saveToLocalStorage();
|
||||
renderFeedback(getActiveFilter());
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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', (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 newFb = {
|
||||
id: 'fb-' + Date.now(),
|
||||
author: authorInput.value,
|
||||
type: typeSelect.value,
|
||||
title: titleInput.value,
|
||||
text: textInput.value,
|
||||
upvotes: 1,
|
||||
voted: true
|
||||
};
|
||||
|
||||
feedbackData.push(newFb);
|
||||
upvotedIds.push(newFb.id); // Auto-upvote own post
|
||||
saveToLocalStorage();
|
||||
|
||||
// Reset form
|
||||
feedbackForm.reset();
|
||||
|
||||
// Re-render
|
||||
renderFeedback(getActiveFilter());
|
||||
|
||||
// Smooth scroll to list
|
||||
feedbackList.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
}
|
||||
|
||||
// --- Modal Management for Impressum & Privacy ---
|
||||
const modalTriggers = document.querySelectorAll('.legal-trigger');
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
const closeButtons = document.querySelectorAll('.modal-close');
|
||||
|
||||
modalTriggers.forEach(trigger => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const targetId = trigger.getAttribute('href').substring(1);
|
||||
const targetModal = document.getElementById(targetId);
|
||||
if (targetModal) {
|
||||
targetModal.classList.add('open');
|
||||
document.body.style.overflow = 'hidden'; // Stop scrolling background
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const closeModal = (modal) => {
|
||||
modal.classList.remove('open');
|
||||
// Only restore scroll if no other modals are open
|
||||
const anyOpen = Array.from(modals).some(m => m.classList.contains('open'));
|
||||
if (!anyOpen) {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
};
|
||||
|
||||
closeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const modal = btn.closest('.modal-overlay');
|
||||
if (modal) closeModal(modal);
|
||||
});
|
||||
});
|
||||
|
||||
modals.forEach(modal => {
|
||||
// Close modal when clicking outside of the content block
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal(modal);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal on Escape key press
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
modals.forEach(modal => {
|
||||
if (modal.classList.contains('open')) closeModal(modal);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderFeedback();
|
||||
});
|
||||
Reference in New Issue
Block a user