diff --git a/api/search_products.php b/api/search_products.php new file mode 100644 index 0000000..22b6e8d --- /dev/null +++ b/api/search_products.php @@ -0,0 +1,79 @@ + 15) { + $limit = 15; + } + + // Minimum query length to reduce load/noise + if (mb_strlen($q, 'UTF-8') < 2) { + echo json_encode(['items' => []], JSON_UNESCAPED_UNICODE); + exit; + } + + // Escape LIKE wildcards (% _), then add %...% + $like = addcslashes($q, "%_\\"); + $like = '%' . $like . '%'; + + // Simple search: model + description + $sql = " + SELECT p.productID, p.model, p.description, p.imagePath + FROM products p + WHERE (p.model LIKE ? OR p.description LIKE ?) + ORDER BY + CASE WHEN p.model LIKE ? THEN 0 ELSE 1 END, + p.model ASC + LIMIT ? + "; + + $stmt = $conn->prepare($sql); + if (!$stmt) { + http_response_code(500); + echo json_encode(['error' => 'DB-Query konnte nicht vorbereitet werden.'], JSON_UNESCAPED_UNICODE); + exit; + } + + $stmt->bind_param('sssi', $like, $like, $like, $limit); + $stmt->execute(); + $res = $stmt->get_result(); + + $items = []; + while ($row = $res->fetch_assoc()) { + $id = (int)($row['productID'] ?? 0); + if ($id <= 0) { + continue; + } + + $items[] = [ + 'id' => $id, + 'model' => (string)($row['model'] ?? ''), + 'description' => (string)($row['description'] ?? ''), + 'imagePath' => (string)($row['imagePath'] ?? ''), + 'url' => 'productpage.php?id=' . $id, + ]; + } + + echo json_encode(['items' => $items], JSON_UNESCAPED_UNICODE); +} catch (Throwable $e) { + http_response_code(500); + echo json_encode(['error' => 'Serverfehler'], JSON_UNESCAPED_UNICODE); +} + diff --git a/assets/js/search-autocomplete.js b/assets/js/search-autocomplete.js new file mode 100644 index 0000000..565b731 --- /dev/null +++ b/assets/js/search-autocomplete.js @@ -0,0 +1,222 @@ +(function () { + 'use strict'; + + function debounce(fn, delay) { + var t; + return function () { + var ctx = this; + var args = arguments; + clearTimeout(t); + t = setTimeout(function () { fn.apply(ctx, args); }, delay); + }; + } + + function createDropdownEl() { + var el = document.createElement('div'); + el.className = 'searchDropdown'; + el.hidden = true; + el.setAttribute('role', 'listbox'); + return el; + } + + function attachToInput(input) { + if (!input || input.dataset.autocompleteBound === '1') return; + input.dataset.autocompleteBound = '1'; + + // wrapper for positioning + var wrapper = input.closest('.nav__searchField'); + if (!wrapper) return; + wrapper.classList.add('nav__searchField--hasDropdown'); + + // dropdown + var dropdown = createDropdownEl(); + wrapper.appendChild(dropdown); + + var activeIndex = -1; + var items = []; + var abortController = null; + + function close() { + dropdown.hidden = true; + dropdown.innerHTML = ''; + dropdown.classList.remove('is-open'); + activeIndex = -1; + items = []; + } + + function open() { + dropdown.hidden = false; + dropdown.classList.add('is-open'); + } + + function setStatus(text) { + dropdown.innerHTML = ''; + var row = document.createElement('div'); + row.className = 'searchDropdown__status'; + row.textContent = text; + dropdown.appendChild(row); + open(); + } + + function render(list) { + dropdown.innerHTML = ''; + items = list || []; + activeIndex = -1; + + if (!items.length) { + setStatus('Keine Produkte gefunden.'); + return; + } + + for (var i = 0; i < items.length; i++) { + (function (idx) { + var it = items[idx]; + + var a = document.createElement('a'); + a.className = 'searchDropdown__item'; + a.href = it.url; + a.setAttribute('role', 'option'); + a.tabIndex = -1; + + var img = document.createElement('img'); + img.className = 'searchDropdown__img'; + img.alt = ''; + img.loading = 'lazy'; + img.decoding = 'async'; + img.src = it.imagePath && it.imagePath.length ? it.imagePath : 'assets/images/placeholder.png'; + + var meta = document.createElement('div'); + meta.className = 'searchDropdown__meta'; + + var title = document.createElement('div'); + title.className = 'searchDropdown__title'; + title.textContent = it.model || ''; + + var desc = document.createElement('div'); + desc.className = 'searchDropdown__desc'; + desc.textContent = it.description || ''; + + meta.appendChild(title); + meta.appendChild(desc); + + a.appendChild(img); + a.appendChild(meta); + + a.addEventListener('mouseenter', function () { + setActive(idx); + }); + + dropdown.appendChild(a); + })(i); + } + + open(); + } + + function setActive(idx) { + var children = dropdown.querySelectorAll('.searchDropdown__item'); + for (var i = 0; i < children.length; i++) { + children[i].classList.toggle('is-active', i === idx); + } + activeIndex = idx; + } + + function fetchResults(q) { + if (abortController) { + abortController.abort(); + } + abortController = new AbortController(); + + var url = 'api/search_products.php?q=' + encodeURIComponent(q) + '&limit=8'; + + setStatus('Suche…'); + + fetch(url, { signal: abortController.signal, headers: { 'Accept': 'application/json' } }) + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function (data) { + render((data && data.items) ? data.items : []); + }) + .catch(function (err) { + if (err && err.name === 'AbortError') return; + setStatus('Fehler bei der Suche.'); + }); + } + + var debounced = debounce(function () { + var q = (input.value || '').trim(); + if (q.length < 2) { + close(); + return; + } + fetchResults(q); + }, 180); + + input.addEventListener('input', debounced); + + input.addEventListener('focus', function () { + var q = (input.value || '').trim(); + if (q.length >= 2) { + fetchResults(q); + } + }); + + input.addEventListener('keydown', function (e) { + if (dropdown.hidden) return; + + if (e.key === 'Escape') { + close(); + return; + } + + var max = items.length - 1; + if (max < 0) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + var next = activeIndex + 1; + if (next > max) next = 0; + setActive(next); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + var prev = activeIndex - 1; + if (prev < 0) prev = max; + setActive(prev); + } else if (e.key === 'Enter') { + if (activeIndex >= 0) { + var links = dropdown.querySelectorAll('.searchDropdown__item'); + if (links[activeIndex]) { + e.preventDefault(); + window.location.href = links[activeIndex].href; + } + } + } + }); + + document.addEventListener('click', function (e) { + if (wrapper.contains(e.target)) return; + close(); + }); + + // prevent blur-close when clicking inside dropdown until navigation happens + dropdown.addEventListener('mousedown', function (e) { + e.preventDefault(); + }); + } + + function init() { + var inputs = document.querySelectorAll('input.nav__searchInput[name="search"]'); + for (var i = 0; i < inputs.length; i++) { + attachToInput(inputs[i]); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); + diff --git a/compcards.php b/compcards.php index ffdae3b..68d5e5d 100644 --- a/compcards.php +++ b/compcards.php @@ -11,6 +11,65 @@ error_reporting(E_ALL); $conn = db_connect(); ?> += 2) { + $like = addcslashes($searchTerm, "%_\\"); + $like = '%' . $like . '%'; + + $stmtSearch = $conn->prepare(" + SELECT productID, model, description, imagePath + FROM products + WHERE model LIKE ? OR description LIKE ? + ORDER BY model ASC + LIMIT 60 + "); + + if ($stmtSearch) { + $stmtSearch->bind_param('ss', $like, $like); + $stmtSearch->execute(); + $resultSearch = $stmtSearch->get_result(); + ?> + +
+

Suchergebnisse für „

+ + num_rows <= 0): ?> +

Keine Produkte gefunden.

+ +
+ fetch_assoc()): ?> + + + <?= htmlspecialchars($product['model'] ?? '') ?> + +
+

+

+
+
+ +
+ +
+ + close(); + } + + // Wichtig: In Suchmodus KEINE Kategorien rendern. + return; +} +?> + diff --git a/header.php b/header.php index 683bfaa..d774274 100644 --- a/header.php +++ b/header.php @@ -161,3 +161,4 @@ } })(); + diff --git a/productpage.php b/productpage.php index 47a10a6..f3f671d 100644 --- a/productpage.php +++ b/productpage.php @@ -238,8 +238,23 @@ $productId = isset($_GET['id']) ? (int)$_GET['id'] : 0; 0) { + $urlColumn = 'productURL'; + } else { + $colCheck2 = mysqli_query($conn, "SHOW COLUMNS FROM offers LIKE 'offerURL'"); + if ($colCheck2 && mysqli_num_rows($colCheck2) > 0) { + $urlColumn = 'offerURL'; + } + } + + $urlSelect = $urlColumn !== '' ? ("offers." . $urlColumn . " AS offerURL") : "'' AS offerURL"; + $stmt = mysqli_prepare($conn, - "SELECT price, shippingCost, inStock, shops.name, offers.offerURL, shops.logoPath, shops.shippingTime + "SELECT price, shippingCost, inStock, shops.name, $urlSelect, shops.logoPath, shops.shippingTime FROM offers INNER JOIN shops ON offers.shopID = shops.shopID WHERE offers.productID = ? ORDER BY offers.price ASC"); diff --git a/style.css b/style.css index ff57ec5..a6a920b 100755 --- a/style.css +++ b/style.css @@ -258,6 +258,87 @@ a:hover { gap: 1.25rem; } +/* ========================================================== + SEARCH AUTOCOMPLETE DROPDOWN + ========================================================== */ +.nav__searchField--hasDropdown { + position: relative; +} + +.searchDropdown { + position: absolute; + left: 0; + right: 0; + top: calc(100% + 10px); + z-index: 1200; + background: rgba(21, 25, 35, 0.96); + backdrop-filter: blur(18px) saturate(160%); + -webkit-backdrop-filter: blur(18px) saturate(160%); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-md); + overflow: hidden; +} + +.searchDropdown__status { + padding: 12px 14px; + color: var(--text-secondary); + font-size: 0.95rem; +} + +.searchDropdown__item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + color: var(--text-primary); + text-decoration: none; + border-top: 1px solid rgba(94, 96, 117, 0.18); + transition: background var(--transition-fast), transform var(--transition-fast); +} + +.searchDropdown__item:first-child { + border-top: 0; +} + +.searchDropdown__item:hover, +.searchDropdown__item.is-active { + background: rgba(39, 74, 151, 0.18); +} + +.searchDropdown__img { + width: 44px; + height: 44px; + object-fit: cover; + border-radius: var(--radius-md); + border: 1px solid rgba(94, 96, 117, 0.25); + flex: 0 0 auto; +} + +.searchDropdown__meta { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.searchDropdown__title { + font-weight: 700; + font-size: 0.98rem; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.searchDropdown__desc { + font-size: 0.86rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .nav__inner.container { margin-left: auto; } @@ -646,6 +727,12 @@ a:hover { gap: 1.6rem; } +/* Hinweistext bei leeren Suchergebnissen */ +.search-empty { + color: var(--text-secondary); + margin: 0.5rem 2rem 0; +} + /* ========================================================== PRODUCT CARD (global) ========================================================== */