752 lines
24 KiB
Twig
752 lines
24 KiB
Twig
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title><?= $title ?></title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
<link rel="manifest" href="/manifest.json">
|
|
<meta name="theme-color" content="#764ba2">
|
|
<style>
|
|
:root {
|
|
--primary-color: #667eea;
|
|
--secondary-color: #764ba2;
|
|
--success-color: #28a745;
|
|
--warning-color: #ffc107;
|
|
--danger-color: #dc3545;
|
|
--light-color: #f8f9fa;
|
|
--dark-color: #343a40;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--light-color);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
.mobile-header {
|
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
|
color: white;
|
|
padding: 1rem;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1000;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.search-bar {
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
border-radius: 25px;
|
|
color: white;
|
|
padding: 0.75rem 1rem;
|
|
width: 100%;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.search-bar::placeholder {
|
|
color: rgba(255,255,255,0.8);
|
|
}
|
|
|
|
.filter-bar {
|
|
background: white;
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid #dee2e6;
|
|
position: sticky;
|
|
top: 80px;
|
|
z-index: 999;
|
|
}
|
|
|
|
.filter-btn {
|
|
background: var(--light-color);
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 20px;
|
|
padding: 0.5rem 1rem;
|
|
font-size: 0.875rem;
|
|
color: var(--dark-color);
|
|
margin-right: 0.5rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.filter-btn.active {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.product-grid {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.product-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
margin-bottom: 1rem;
|
|
overflow: hidden;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
.product-card:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.product-image {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: cover;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.product-info {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.product-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
line-height: 1.3;
|
|
color: var(--dark-color);
|
|
}
|
|
|
|
.product-price {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--primary-color);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.product-rating {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.rating-stars {
|
|
color: var(--warning-color);
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.rating-count {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.product-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn-add-cart {
|
|
flex: 1;
|
|
background: var(--success-color);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 0.75rem;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.btn-wishlist {
|
|
background: var(--light-color);
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 0.75rem;
|
|
color: var(--dark-color);
|
|
width: 48px;
|
|
}
|
|
|
|
.btn-wishlist.active {
|
|
background: var(--danger-color);
|
|
color: white;
|
|
border-color: var(--danger-color);
|
|
}
|
|
|
|
.stock-badge {
|
|
position: absolute;
|
|
top: 0.5rem;
|
|
right: 0.5rem;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.stock-in {
|
|
background: var(--success-color);
|
|
color: white;
|
|
}
|
|
|
|
.stock-low {
|
|
background: var(--warning-color);
|
|
color: var(--dark-color);
|
|
}
|
|
|
|
.stock-out {
|
|
background: var(--danger-color);
|
|
color: white;
|
|
}
|
|
|
|
.new-badge {
|
|
position: absolute;
|
|
top: 0.5rem;
|
|
left: 0.5rem;
|
|
background: var(--primary-color);
|
|
color: white;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.loading-spinner {
|
|
display: none;
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.loading-spinner.show {
|
|
display: block;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 3rem 1rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 3rem;
|
|
margin-bottom: 1rem;
|
|
color: #dee2e6;
|
|
}
|
|
|
|
.floating-cart {
|
|
position: fixed;
|
|
bottom: 2rem;
|
|
right: 1rem;
|
|
background: var(--primary-color);
|
|
color: white;
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
z-index: 1000;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.floating-cart:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.cart-badge {
|
|
position: absolute;
|
|
top: -5px;
|
|
right: -5px;
|
|
background: var(--danger-color);
|
|
color: white;
|
|
border-radius: 50%;
|
|
width: 20px;
|
|
height: 20px;
|
|
font-size: 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pull-to-refresh {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
@media (max-width: 576px) {
|
|
.product-grid {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.product-card {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.product-image {
|
|
height: 180px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Mobile Header -->
|
|
<div class="mobile-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h1 class="h5 mb-0">
|
|
<i class="fas fa-shopping-bag me-2"></i>Produkte
|
|
</h1>
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-link text-white p-0" onclick="openCart()">
|
|
<i class="fas fa-shopping-cart fa-lg"></i>
|
|
<span class="cart-badge" id="cartBadge">0</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="text" class="search-bar" placeholder="Produkte suchen..."
|
|
id="searchInput" oninput="handleSearch()">
|
|
</div>
|
|
|
|
<!-- Filter Bar -->
|
|
<div class="filter-bar">
|
|
<div class="d-flex overflow-auto">
|
|
<button class="filter-btn active" data-sort="newest" onclick="setSort('newest')">
|
|
<i class="fas fa-clock me-1"></i>Neueste
|
|
</button>
|
|
<button class="filter-btn" data-sort="price_asc" onclick="setSort('price_asc')">
|
|
<i class="fas fa-sort-amount-down me-1"></i>Preis ↑
|
|
</button>
|
|
<button class="filter-btn" data-sort="price_desc" onclick="setSort('price_desc')">
|
|
<i class="fas fa-sort-amount-up me-1"></i>Preis ↓
|
|
</button>
|
|
<button class="filter-btn" data-sort="rating" onclick="setSort('rating')">
|
|
<i class="fas fa-star me-1"></i>Beste
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pull to Refresh Indicator -->
|
|
<div class="pull-to-refresh" id="pullToRefresh" style="display: none;">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>Lade neue Produkte...
|
|
</div>
|
|
|
|
<!-- Product Grid -->
|
|
<div class="product-grid" id="productGrid">
|
|
<!-- Products will be loaded here -->
|
|
</div>
|
|
|
|
<!-- Loading Spinner -->
|
|
<div class="loading-spinner" id="loadingSpinner">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Lade...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Produkte werden geladen...</p>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div class="empty-state" id="emptyState" style="display: none;">
|
|
<i class="fas fa-search"></i>
|
|
<h5>Keine Produkte gefunden</h5>
|
|
<p>Versuchen Sie andere Suchbegriffe oder Filter</p>
|
|
<button class="btn btn-primary" onclick="resetFilters()">
|
|
<i class="fas fa-redo me-2"></i>Filter zurücksetzen
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Floating Cart Button -->
|
|
<div class="floating-cart" onclick="openCart()" id="floatingCart" style="display: none;">
|
|
<i class="fas fa-shopping-cart"></i>
|
|
<span class="cart-badge" id="floatingCartBadge">0</span>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
// Global variables
|
|
let currentPage = 1;
|
|
let currentSort = 'newest';
|
|
let currentSearch = '';
|
|
let isLoading = false;
|
|
let hasMorePages = true;
|
|
let cartItemCount = 0;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadProducts();
|
|
loadCartCount();
|
|
setupPullToRefresh();
|
|
setupIntersectionObserver();
|
|
});
|
|
|
|
// Load products
|
|
async function loadProducts(reset = false) {
|
|
if (isLoading) return;
|
|
|
|
isLoading = true;
|
|
|
|
if (reset) {
|
|
currentPage = 1;
|
|
document.getElementById('productGrid').innerHTML = '';
|
|
}
|
|
|
|
showLoading(true);
|
|
hideEmptyState();
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: currentPage,
|
|
limit: 10,
|
|
sort: currentSort,
|
|
search: currentSearch
|
|
});
|
|
|
|
const response = await fetch(`/api/mobile/products?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
displayProducts(data.data.products);
|
|
hasMorePages = data.data.pagination.has_next;
|
|
currentPage++;
|
|
} else {
|
|
showError('Fehler beim Laden der Produkte');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading products:', error);
|
|
showError('Netzwerkfehler - Prüfen Sie Ihre Verbindung');
|
|
} finally {
|
|
isLoading = false;
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// Display products
|
|
function displayProducts(products) {
|
|
const grid = document.getElementById('productGrid');
|
|
|
|
if (products.length === 0 && currentPage === 1) {
|
|
showEmptyState();
|
|
return;
|
|
}
|
|
|
|
products.forEach(product => {
|
|
const productCard = createProductCard(product);
|
|
grid.appendChild(productCard);
|
|
});
|
|
}
|
|
|
|
// Create product card
|
|
function createProductCard(product) {
|
|
const card = document.createElement('div');
|
|
card.className = 'product-card position-relative';
|
|
card.onclick = () => openProduct(product.id);
|
|
|
|
const stockBadge = getStockBadge(product.stock_status);
|
|
const newBadge = product.is_new ? '<div class="new-badge">NEU</div>' : '';
|
|
|
|
card.innerHTML = `
|
|
${newBadge}
|
|
${stockBadge}
|
|
<img src="${product.image_url || '/img/placeholder.png'}"
|
|
alt="${product.name}" class="product-image"
|
|
onerror="this.src='/img/placeholder.png'">
|
|
<div class="product-info">
|
|
<h3 class="product-title">${product.name}</h3>
|
|
<div class="product-price">${product.formatted_price}</div>
|
|
<div class="product-rating">
|
|
<div class="rating-stars">
|
|
${getRatingStars(product.rating)}
|
|
</div>
|
|
<span class="rating-count">(${product.review_count})</span>
|
|
</div>
|
|
<div class="product-actions">
|
|
<button class="btn-add-cart" onclick="event.stopPropagation(); addToCart(${product.id})"
|
|
${!product.quick_add ? 'disabled' : ''}>
|
|
<i class="fas fa-cart-plus me-1"></i>
|
|
${product.quick_add ? 'In Warenkorb' : 'Nicht verfügbar'}
|
|
</button>
|
|
<button class="btn-wishlist" onclick="event.stopPropagation(); toggleWishlist(${product.id})">
|
|
<i class="fas fa-heart"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|
|
|
|
// Get stock badge
|
|
function getStockBadge(status) {
|
|
const badges = {
|
|
'in_stock': '<div class="stock-badge stock-in">Verfügbar</div>',
|
|
'low_stock': '<div class="stock-badge stock-low">Nur noch wenige</div>',
|
|
'out_of_stock': '<div class="stock-badge stock-out">Ausverkauft</div>'
|
|
};
|
|
return badges[status] || '';
|
|
}
|
|
|
|
// Get rating stars
|
|
function getRatingStars(rating) {
|
|
const fullStars = Math.floor(rating);
|
|
const hasHalfStar = rating % 1 >= 0.5;
|
|
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
|
|
|
|
return '★'.repeat(fullStars) +
|
|
(hasHalfStar ? '☆' : '') +
|
|
'☆'.repeat(emptyStars);
|
|
}
|
|
|
|
// Search handling
|
|
let searchTimeout;
|
|
function handleSearch() {
|
|
const searchTerm = document.getElementById('searchInput').value;
|
|
currentSearch = searchTerm;
|
|
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
currentPage = 1;
|
|
loadProducts(true);
|
|
}, 500);
|
|
}
|
|
|
|
// Sort handling
|
|
function setSort(sort) {
|
|
currentSort = sort;
|
|
|
|
// Update active filter button
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
event.target.classList.add('active');
|
|
|
|
currentPage = 1;
|
|
loadProducts(true);
|
|
}
|
|
|
|
// Add to cart
|
|
async function addToCart(productId) {
|
|
try {
|
|
const response = await fetch('/api/mobile/cart/add', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${getToken()}`
|
|
},
|
|
body: JSON.stringify({
|
|
product_id: productId,
|
|
quantity: 1
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('Produkt zum Warenkorb hinzugefügt', 'success');
|
|
loadCartCount();
|
|
} else {
|
|
showToast('Fehler beim Hinzufügen zum Warenkorb', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding to cart:', error);
|
|
showToast('Netzwerkfehler', 'error');
|
|
}
|
|
}
|
|
|
|
// Toggle wishlist
|
|
async function toggleWishlist(productId) {
|
|
try {
|
|
const response = await fetch('/api/mobile/wishlist/toggle', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${getToken()}`
|
|
},
|
|
body: JSON.stringify({
|
|
product_id: productId
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const btn = event.target.closest('.btn-wishlist');
|
|
btn.classList.toggle('active');
|
|
showToast(data.data.in_wishlist ? 'Zur Wunschliste hinzugefügt' : 'Von Wunschliste entfernt', 'success');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error toggling wishlist:', error);
|
|
showToast('Fehler bei der Wunschliste', 'error');
|
|
}
|
|
}
|
|
|
|
// Load cart count
|
|
async function loadCartCount() {
|
|
try {
|
|
const response = await fetch('/api/mobile/cart/count', {
|
|
headers: {
|
|
'Authorization': `Bearer ${getToken()}`
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
cartItemCount = data.data.count;
|
|
updateCartBadges();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading cart count:', error);
|
|
}
|
|
}
|
|
|
|
// Update cart badges
|
|
function updateCartBadges() {
|
|
const badges = document.querySelectorAll('.cart-badge');
|
|
badges.forEach(badge => {
|
|
badge.textContent = cartItemCount;
|
|
});
|
|
|
|
const floatingCart = document.getElementById('floatingCart');
|
|
if (cartItemCount > 0) {
|
|
floatingCart.style.display = 'flex';
|
|
} else {
|
|
floatingCart.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Open product details
|
|
function openProduct(productId) {
|
|
window.location.href = `/mobile/product/${productId}`;
|
|
}
|
|
|
|
// Open cart
|
|
function openCart() {
|
|
window.location.href = '/mobile/cart';
|
|
}
|
|
|
|
// Reset filters
|
|
function resetFilters() {
|
|
currentSearch = '';
|
|
currentSort = 'newest';
|
|
document.getElementById('searchInput').value = '';
|
|
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
document.querySelector('[data-sort="newest"]').classList.add('active');
|
|
|
|
currentPage = 1;
|
|
loadProducts(true);
|
|
}
|
|
|
|
// Show/hide loading
|
|
function showLoading(show) {
|
|
const spinner = document.getElementById('loadingSpinner');
|
|
if (show) {
|
|
spinner.classList.add('show');
|
|
} else {
|
|
spinner.classList.remove('show');
|
|
}
|
|
}
|
|
|
|
// Show/hide empty state
|
|
function showEmptyState() {
|
|
document.getElementById('emptyState').style.display = 'block';
|
|
}
|
|
|
|
function hideEmptyState() {
|
|
document.getElementById('emptyState').style.display = 'none';
|
|
}
|
|
|
|
// Show error
|
|
function showError(message) {
|
|
showToast(message, 'error');
|
|
}
|
|
|
|
// Show toast
|
|
function showToast(message, type = 'info') {
|
|
// Simple toast implementation
|
|
const toast = document.createElement('div');
|
|
toast.className = `alert alert-${type === 'error' ? 'danger' : type} position-fixed`;
|
|
toast.style.cssText = 'top: 1rem; right: 1rem; z-index: 9999; min-width: 300px;';
|
|
toast.textContent = message;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
// Get token from localStorage
|
|
function getToken() {
|
|
return localStorage.getItem('auth_token');
|
|
}
|
|
|
|
// Pull to refresh
|
|
function setupPullToRefresh() {
|
|
let startY = 0;
|
|
let currentY = 0;
|
|
let pullDistance = 0;
|
|
const threshold = 80;
|
|
|
|
document.addEventListener('touchstart', (e) => {
|
|
if (window.scrollY === 0) {
|
|
startY = e.touches[0].clientY;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('touchmove', (e) => {
|
|
if (window.scrollY === 0 && startY > 0) {
|
|
currentY = e.touches[0].clientY;
|
|
pullDistance = currentY - startY;
|
|
|
|
if (pullDistance > 0) {
|
|
e.preventDefault();
|
|
document.getElementById('pullToRefresh').style.display = 'block';
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('touchend', () => {
|
|
if (pullDistance > threshold) {
|
|
loadProducts(true);
|
|
}
|
|
|
|
startY = 0;
|
|
pullDistance = 0;
|
|
document.getElementById('pullToRefresh').style.display = 'none';
|
|
});
|
|
}
|
|
|
|
// Infinite scroll
|
|
function setupIntersectionObserver() {
|
|
const options = {
|
|
root: null,
|
|
rootMargin: '100px',
|
|
threshold: 0.1
|
|
};
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting && hasMorePages && !isLoading) {
|
|
loadProducts();
|
|
}
|
|
});
|
|
}, options);
|
|
|
|
// Observe the last product card
|
|
const observeLastCard = () => {
|
|
const cards = document.querySelectorAll('.product-card');
|
|
if (cards.length > 0) {
|
|
observer.observe(cards[cards.length - 1]);
|
|
}
|
|
};
|
|
|
|
// Re-observe when new cards are added
|
|
const originalDisplayProducts = displayProducts;
|
|
displayProducts = function(products) {
|
|
originalDisplayProducts(products);
|
|
setTimeout(observeLastCard, 100);
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |