Newwebshop/templates/mobile/products.html.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>