wip: Mobile-Theme-Testphase und PHP-8.2-Guards (Stand Juni 2026)

This commit is contained in:
Thomas Bartelt
2026-09-13 19:10:56 +02:00
parent 08beac8d67
commit 20d55f77b8
1448 changed files with 16015 additions and 583 deletions
@@ -0,0 +1,206 @@
<!-- mobile:content_item -->
{* MVP-Mobile-Item-Detail mit Bootstrap 5 Card-Layout.
Variablen aus $shopArticle (siehe Desktop-content_item.tpl).
POST geht zur selben URL (kein action= nötig). *}
{assign var=item_image_first value=""}
{if isset($shopArticle->files[0]) && $shopArticle->files[0]->file_name}
{assign var=item_image_first value=$shopArticle->files[0]->file_name}
{/if}
<form method="post" id="item_buy_form">
<input type="hidden" name="buy_quantity" value="{$shopArticle->buy_quantity}">
<input type="hidden" name="min_quantity" value="{$shopArticle->min_quantity}">
<div class="mb-3">
<a href="javascript:history.back()" class="text-muted text-decoration-none small">
<i class="bi bi-arrow-left"></i> zurück
</a>
</div>
{* --- Hauptbild + Bildergalerie --- *}
<div class="card shop-item-card mb-3">
{if $item_image_first}
<div id="itemImageSlider" class="carousel slide" data-bs-ride="false">
{if $shopArticle->files|@count > 1}
<div class="carousel-indicators">
{foreach from=$shopArticle->files item=file name=indi}
<button type="button" data-bs-target="#itemImageSlider" data-bs-slide-to="{$smarty.foreach.indi.index}"
{if $smarty.foreach.indi.first}class="active"{/if} aria-label="Bild {$smarty.foreach.indi.iteration}"></button>
{/foreach}
</div>
{/if}
<div class="carousel-inner bg-white">
{foreach from=$shopArticle->files item=file name=slides}
<div class="carousel-item {if $smarty.foreach.slides.first}active{/if}">
<a href="{$SHOP_DIR}images/article/{$file->file_name}" target="_blank">
<img src="{$SHOP_DIR}images/article/details_{$file->file_name}"
class="d-block w-100" style="height:280px;object-fit:contain;background:#fff;"
alt="{if isset($file->alt)}{$file->alt|escape:'html'}{else}{$shopArticle->name|escape:'html'}{/if}">
</a>
</div>
{/foreach}
</div>
{if $shopArticle->files|@count > 1}
<button class="carousel-control-prev" type="button" data-bs-target="#itemImageSlider" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true" style="background-color:rgba(0,0,0,.3);border-radius:50%;"></span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#itemImageSlider" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true" style="background-color:rgba(0,0,0,.3);border-radius:50%;"></span>
</button>
{/if}
</div>
{else}
<div class="card-body text-center text-muted py-5">
<i class="bi bi-image" style="font-size:3rem;"></i><br>
<small>Kein Bild verfügbar</small>
</div>
{/if}
</div>
{* --- Titel + Hersteller + Artikelnummer --- *}
<div class="card shop-item-card mb-3">
<div class="card-body">
<h1 class="h4 mb-2">
{if !$user_lang || $user_lang == $system_configuration.default_system_language || !$shopArticle->name_en}
{$shopArticle->name}
{else}
{$shopArticle->name_en}
{/if}
</h1>
{if isset($shopArticle->manufacturer_data->name) && $shopArticle->manufacturer_data->name}
<span class="badge bg-secondary mb-2">{$shopArticle->manufacturer_data->name}</span>
{/if}
{if $shopArticle->number}
<div class="small text-muted mb-2">
{$template_text.item_details.item_number}:
<strong>
{if $shopArticle->attribute_7 && $shopArticle->number != $shopArticle->attribute_7}
{$shopArticle->attribute_7}
<span class="text-muted">(alt: {$shopArticle->number})</span>
{else}
{$shopArticle->number}
{/if}
</strong>
</div>
{/if}
{* Lagerbestand *}
{if $shopArticle->inventory > 0}
<div class="text-success small">
<i class="bi bi-check-circle-fill"></i> Auf Lager — {$shopArticle->inventory} Stück verfügbar
</div>
{elseif $shopArticle->inventory === 0}
<div class="text-danger small">
<i class="bi bi-x-circle-fill"></i> Aktuell nicht verfügbar
</div>
{else}
<div class="text-muted small">
<i class="bi bi-info-circle"></i> Lieferzeit auf Anfrage
</div>
{/if}
</div>
</div>
{* --- Preis + Add-to-Cart --- *}
<div class="card shop-item-card mb-3" id="priceBox">
<div class="card-body">
{if $shopArticle->shop_price_1 > 0}
<div class="mb-2">
<span class="h3 text-danger">
{$shopArticle->shop_price_1|number_format:"2":",":"."}&nbsp;&euro;
</span>
<small class="text-muted">inkl. MwSt.</small>
</div>
{else}
<div class="mb-2 text-muted">
Preis auf Anfrage
</div>
{/if}
<div class="d-flex gap-2 align-items-center">
<label for="qty" class="visually-hidden">Menge</label>
<input type="number" name="quantity" id="qty"
class="form-control text-center" style="max-width:90px;"
value="{if $shopArticle->min_quantity > 0}{$shopArticle->min_quantity}{else}1{/if}"
min="{if $shopArticle->min_quantity > 0}{$shopArticle->min_quantity}{else}1{/if}"
step="{if $shopArticle->buy_quantity > 0}{$shopArticle->buy_quantity}{else}1{/if}">
<button type="submit" name="item_to_cart" value="1" class="btn btn-brand btn-lg flex-grow-1">
<i class="bi bi-bag-plus"></i> In den Warenkorb
</button>
</div>
</div>
</div>
{* --- Beschreibung + weitere Tabs als Accordion --- *}
{if isset($shopArticle->tab_content) && $shopArticle->tab_content}
<div class="accordion mb-3" id="itemDetailsAcc">
{foreach from=$shopArticle->tab_content item=content key=content_id name=tabs}
{assign var=tab_label value=""}
{if isset($shopArticle->tab_title[$content_id])}
{assign var=tab_label value=$shopArticle->tab_title[$content_id]}
{else}
{assign var=tab_label value=$template_text.item_details.description}
{/if}
{if $content}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button {if !$smarty.foreach.tabs.first}collapsed{/if}"
type="button" data-bs-toggle="collapse"
data-bs-target="#tabColl_{$content_id}"
aria-expanded="{if $smarty.foreach.tabs.first}true{else}false{/if}">
{$tab_label}
</button>
</h2>
<div id="tabColl_{$content_id}" class="accordion-collapse collapse {if $smarty.foreach.tabs.first}show{/if}" data-bs-parent="#itemDetailsAcc">
<div class="accordion-body">
{$content}
</div>
</div>
</div>
{/if}
{/foreach}
{* Technische Daten als zusätzlicher Block, falls Attribute vorhanden *}
{if isset($shopArticle->schema_data_attributes) && $shopArticle->schema_data_attributes}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#tabColl_attributes" aria-expanded="false">
Technische Daten
</button>
</h2>
<div id="tabColl_attributes" class="accordion-collapse collapse" data-bs-parent="#itemDetailsAcc">
<div class="accordion-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
{foreach from=$shopArticle->schema_data_attributes item=attribute}
{if $attribute.name}
<tr>
<th scope="row" class="text-muted fw-normal" style="width:45%;">{$attribute.name}</th>
<td>{$attribute.value|default:''}{if $attribute.unit} {$attribute.unit}{/if}</td>
</tr>
{/if}
{/foreach}
</tbody>
</table>
</div>
</div>
</div>
{/if}
</div>
{/if}
</form>
{* --- Sticky-Bottom-Bar (Quick Add) --- *}
<div class="position-fixed bottom-0 start-0 end-0 bg-white shadow-lg p-2 d-md-none" style="z-index:1030;border-top:1px solid #eee;">
<div class="d-flex gap-2">
<a href="#priceBox" class="btn btn-brand flex-grow-1">
<i class="bi bi-bag-plus"></i> Zum Warenkorb hinzufügen
</a>
</div>
</div>
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>{if isset($html_meta.title)}{$html_meta.title}{else}{$system_configuration.full_shop_name}{/if}</title>
{if isset($html_meta.description)}<meta name="description" content="{$html_meta.description}">{/if}
{if isset($html_meta.keywords)}<meta name="keywords" content="{$html_meta.keywords}">{/if}
<link rel="canonical" href="{$serverProtocol}://{$servername}{$currentURI}">
<link rel="icon" href="{$SUB_DIR}favicon.ico" type="image/x-icon">
{* Bootstrap 5.3 *}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
{* Eigenes Mobile-CSS *}
<link rel="stylesheet" href="{$THEME_DIR}/media/css/mobile.css?v=6">
<style>
:root {
--brand-color: #c00;
--brand-color-dark: #a00;
}
body { padding-top: 56px; padding-bottom: 80px; }
.navbar-brand img { max-height: 36px; }
.shop-item-card { border: 0; box-shadow: 0 1px 3px rgba(0,0,0,.08); margin-bottom: 12px; }
.btn-brand { background: var(--brand-color); color: #fff; border: 0; }
.btn-brand:hover { background: var(--brand-color-dark); color: #fff; }
.theme-switch-hint {
position: fixed; bottom: 8px; right: 8px; z-index: 999;
background: rgba(0,0,0,.7); color: #fff; padding: 4px 10px;
border-radius: 12px; font-size: 11px; text-decoration: none;
}
</style>
</head>
<body>
{* --- Top-Bar --- *}
<nav class="navbar navbar-expand-lg fixed-top bg-white shadow-sm" aria-label="Hauptnavigation">
<div class="container-fluid">
<button class="navbar-toggler border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#mobileNav" aria-controls="mobileNav" aria-label="Menü öffnen">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand mx-auto" href="{$SUB_DIR}">
<strong>{$system_configuration.full_shop_name}</strong>
</a>
<a href="{$SUB_DIR}index.php?website_modul=website_shoppingcart" class="btn btn-link text-dark p-0" aria-label="Warenkorb">
<i class="bi bi-bag" style="font-size: 1.5rem;"></i>
</a>
</div>
</nav>
{* --- Off-Canvas Nav --- *}
<div class="offcanvas offcanvas-start" tabindex="-1" id="mobileNav" aria-labelledby="mobileNavLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="mobileNavLabel">Menü</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Schließen"></button>
</div>
<div class="offcanvas-body">
<form action="{$SUB_DIR}" method="get" class="mb-3">
<div class="input-group">
<input type="text" name="search_item[search_item]" class="form-control" placeholder="Suchen…" autocomplete="off">
<button class="btn btn-brand" type="submit"><i class="bi bi-search"></i></button>
</div>
</form>
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="{$SUB_DIR}">Startseite</a></li>
<li class="nav-item"><a class="nav-link" href="{$SUB_DIR}Mein-Konto/">Mein Konto</a></li>
<li class="nav-item"><a class="nav-link" href="{$SUB_DIR}Kontakt/">Kontakt</a></li>
<li class="nav-item"><a class="nav-link" href="{$SUB_DIR}Widerrufsrecht/">Widerrufsbelehrung</a></li>
<li class="nav-item"><a class="nav-link" href="{$SUB_DIR}AGB/">AGB</a></li>
<li class="nav-item"><a class="nav-link" href="{$SUB_DIR}Datenschutz/">Datenschutz</a></li>
<li class="nav-item"><a class="nav-link" href="{$SUB_DIR}Impressum/">Impressum</a></li>
</ul>
</div>
</div>
{* --- Content --- *}
<main class="container py-3">
{$maincontent}
</main>
{* --- Footer --- *}
<footer class="bg-light py-4 mt-4 border-top">
<div class="container text-center">
<a class="btn btn-brand btn-lg mb-2" href="{$SUB_DIR}widerruf.php">
Vertrag widerrufen
</a>
<p class="small text-muted mb-1">Elektronische Widerrufsfunktion gem. § 356a BGB</p>
<hr class="my-3">
<p class="small text-muted mb-0">
<strong>{$system_configuration.full_shop_name}</strong> &middot;
{$system_configuration.footer_copyright_text}
</p>
<p class="small mt-2 mb-0">
<a href="?theme=theme2" class="text-muted">Zur Desktop-Version</a>
</p>
</div>
</footer>
{* Theme-Switch Hint (nur Test-Phase) *}
<a href="?theme=theme2" class="theme-switch-hint">📱 mobile · zurück zur Desktop-Version</a>
{* Bootstrap 5 JS bundle *}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>