Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)

* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
This commit was merged in pull request #18.
This commit is contained in:
Masterluke77
2026-07-09 17:09:00 +02:00
committed by GitHub
parent f4923437ca
commit 6f48827f4d
556 changed files with 77265 additions and 210 deletions
+14
View File
@@ -0,0 +1,14 @@
VERALTETER ORDNER
Die Browser-Erweiterungen befinden sich ab Version 1.3.0 unter:
- extensions/chrome
- extensions/edge
- extensions/firefox
Bitte die bisher geladene Erweiterung entfernen und den passenden neuen Unterordner laden.
HOTFIX 1.9.0:
Nach dem Drueberkopieren die Erweiterung unter chrome://extensions bzw. edge://extensions mit "Neu laden" aktualisieren und Vinted neu laden. Die Zustandsauswahl darf danach das Titelfeld nicht mehr ueberschreiben.
VERSION 1.9.0: Publishing-Queue-Kopplung und Abschlussbestätigung.
+60
View File
@@ -0,0 +1,60 @@
const ext = globalThis.browser || globalThis.chrome;
const csrfCache = new Map();
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000;
let binary = '';
for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
return btoa(binary);
}
async function getCsrf(serverUrl) {
if (csrfCache.has(serverUrl)) return csrfCache.get(serverUrl);
const response = await fetch(`${serverUrl}/api/me`, { credentials: 'include', cache: 'no-store' });
if (!response.ok) return '';
const data = await response.json();
const token = data.csrf || '';
csrfCache.set(serverUrl, token);
return token;
}
ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type === 'api') {
(async () => {
try {
const { serverUrl, path, method = 'GET', body } = msg;
const opts = { method, headers: {}, credentials: 'include', cache: 'no-store' };
if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
const csrf = await getCsrf(serverUrl);
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
}
let response = await fetch(`${serverUrl}${path}`, opts);
if (response.status === 403 && !['GET', 'HEAD', 'OPTIONS'].includes(method)) {
csrfCache.delete(serverUrl);
const csrf = await getCsrf(serverUrl);
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
response = await fetch(`${serverUrl}${path}`, opts);
}
const contentType = response.headers.get('content-type') || '';
const data = contentType.includes('application/json') ? await response.json() : { error: await response.text() };
sendResponse({ ok: response.ok, status: response.status, data });
} catch (error) { sendResponse({ ok: false, status: 0, error: error.message }); }
})();
return true;
}
if (msg.type === 'fetch-image') {
(async () => {
try {
const response = await fetch(msg.url, { cache: 'no-store', credentials: 'include' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
if (!blob.type.startsWith('image/')) throw new Error('Antwort ist kein Bild');
const buffer = await blob.arrayBuffer();
sendResponse({ ok: true, dataUrl: `data:${blob.type};base64,${arrayBufferToBase64(buffer)}`, mime: blob.type, size: blob.size });
} catch (error) { sendResponse({ ok: false, error: error.message }); }
})();
return true;
}
});
File diff suppressed because one or more lines are too long
+1453
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" rx="10" fill="#fffdf9"/>
<g transform="translate(26 22) rotate(-25 17 42)">
<rect x="0" y="0" width="28" height="84" rx="10" fill="#ff4a12"/>
</g>
<path d="M76 24h21l11 12v21L91 70 72 61V39z" fill="#ff4a12"/>
<circle cx="94" cy="38" r="6" fill="#fffdf9"/>
<circle cx="84" cy="91" r="21" fill="#24262a"/>
<path d="m74 91 7 7 14-16" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 542 B

+57
View File
@@ -0,0 +1,57 @@
{
"manifest_version": 3,
"name": "Vendoo Link Vinted Assistant",
"version": "1.9.0",
"description": "Vendoo Link mit sicherem Vinted-Auto-Fill, eindeutiger Feldzuordnung, Bildtransfer und Publishing.",
"permissions": [
"activeTab",
"storage"
],
"host_permissions": [
"http://localhost:8124/*",
"http://127.0.0.1:8124/*",
"https://www.vinted.de/*",
"https://www.vinted.fr/*",
"https://www.vinted.co.uk/*",
"https://www.vinted.com/*",
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": [
"https://www.vinted.de/*",
"https://www.vinted.fr/*",
"https://www.vinted.co.uk/*",
"https://www.vinted.com/*"
],
"js": [
"vinted-categories.js",
"content.js"
],
"css": [
"content.css"
],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup.html",
"default_title": "Vendoo Assistant",
"default_icon": {
"16": "icon-16.png",
"32": "icon-32.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
},
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
}
File diff suppressed because one or more lines are too long
+49
View File
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Vendoo Link</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<main class="popup-shell">
<header class="popup-header">
<div class="brand-lockup"><img src="icon-48.png" alt=""><div><strong>Vendoo Link</strong><span>Vinted Assistant</span></div></div>
<div id="connection-chip" class="connection-chip"><i></i><span>Prüfe …</span></div>
<button id="settings-toggle" class="icon-button" type="button" title="Einstellungen"></button>
</header>
<section id="dashboard-view" class="popup-content">
<article class="queue-card">
<div class="queue-icon"></div>
<div><span>Vinted Workflow</span><strong id="queue-state">Wird geladen …</strong><small>Listings kontrolliert in Vinted übernehmen</small><div class="queue-progress"><i id="queue-progress-fill"></i></div></div>
<div class="queue-count"><b id="queue-count">0 / 0</b><button id="open-queue" type="button">Öffnen</button></div>
</article>
<article id="listing-card" class="listing-card hidden">
<header><div><span>Aktuelles Listing</span><b>Bereit</b></div><button id="listing-menu" type="button">•••</button></header>
<div class="listing-overview">
<div class="listing-photo"><img id="listing-image" alt="Produktbild"></div>
<div class="listing-copy"><h2 id="listing-title"></h2><p id="listing-meta"></p><code id="listing-sku"></code><dl><dt>Plattform</dt><dd><span class="platform-chip">Vinted</span></dd><dt>Preis</dt><dd id="listing-price"></dd><dt>Erstellt</dt><dd id="listing-created"></dd></dl></div>
</div>
<div class="safe-notice"><b>Nur ausfüllen</b><span>Öffnet Vinted und trägt die Daten ein ohne Entwurf und ohne Veröffentlichung.</span></div>
<button id="fill-listing" class="primary-action" type="button">Auf Vinted ausfüllen <span></span></button>
<div class="action-row"><button id="next-listing" class="secondary-action" type="button">Nächstes Listing</button><button id="prepare-images" class="secondary-action" type="button">Bilder vorbereiten</button></div>
</article>
<article id="empty-card" class="empty-card hidden"><strong>Keine offenen Vinted-Listings</strong><p>Neue Listings kannst du direkt in Vendoo erstellen.</p><button id="empty-open-app" type="button">Vendoo öffnen</button></article>
</section>
<section id="settings-view" class="settings-view hidden">
<div class="settings-heading"><button id="back-dashboard" type="button"></button><div><strong>Einstellungen</strong><span>Verbindung und Standardwerte</span></div></div>
<div class="setting"><label for="server-url">Vendoo Server</label><input id="server-url" type="url" spellcheck="false"></div>
<div class="setting-row"><div class="setting"><label for="provider">AI Provider</label><select id="provider"><option value="openrouter">OpenRouter</option><option value="claude">Claude</option><option value="openai">OpenAI</option><option value="local">Ollama</option></select></div><div class="setting"><label for="language">Sprache</label><select id="language"><option value="de">Deutsch</option><option value="en">English</option><option value="fr">Français</option><option value="nl">Nederlands</option></select></div></div>
<button id="save-settings" class="primary-action" type="button">Einstellungen speichern</button>
</section>
<footer><span>Vendoo Link v1.9.0</span><button id="open-app" type="button">Vendoo öffnen ↗</button></footer>
</main>
<script src="popup.js"></script>
</body>
</html>
+129
View File
@@ -0,0 +1,129 @@
const browserApi = globalThis.browser || null;
const chromeApi = globalThis.chrome || null;
const $ = selector => document.querySelector(selector);
const state = { queue: [], current: 0, serverUrl: 'http://localhost:8124' };
function sendMessage(message) {
if (browserApi) return browserApi.runtime.sendMessage(message);
return new Promise((resolve, reject) => {
chromeApi.runtime.sendMessage(message, response => {
const error = chromeApi.runtime.lastError;
if (error) reject(new Error(error.message)); else resolve(response);
});
});
}
function storageGet(keys) {
if (browserApi) return browserApi.storage.local.get(keys);
return new Promise(resolve => chromeApi.storage.local.get(keys, resolve));
}
function storageSet(values) {
if (browserApi) return browserApi.storage.local.set(values);
return new Promise(resolve => chromeApi.storage.local.set(values, resolve));
}
function openTab(url) {
if (browserApi) return browserApi.tabs.create({ url });
return chromeApi.tabs.create({ url });
}
async function ensureServerPermission(url) {
const parsed = new URL(url);
const origin = `${parsed.protocol}//${parsed.host}/*`;
const permissions = browserApi?.permissions || chromeApi?.permissions;
if (!permissions) return true;
if (browserApi) {
if (await permissions.contains({ origins: [origin] })) return true;
return permissions.request({ origins: [origin] });
}
return new Promise(resolve => permissions.contains({ origins: [origin] }, has => has ? resolve(true) : permissions.request({ origins: [origin] }, resolve)));
}
async function api(path, method = 'GET', body) {
const response = await sendMessage({ type: 'api', serverUrl: state.serverUrl, path, method, body });
if (!response?.ok) throw new Error(response?.data?.error || response?.error || 'Server nicht erreichbar');
return response.data;
}
function setConnection(ok, text) {
const chip = $('#connection-chip');
chip.className = `connection-chip ${ok ? 'ok' : 'error'}`;
chip.querySelector('span').textContent = text;
}
async function imageData(filename) {
if (!filename) return '';
const url = `${state.serverUrl}/uploads/${String(filename).split('/').map(encodeURIComponent).join('/')}`;
const response = await sendMessage({ type: 'fetch-image', url });
return response?.ok ? response.dataUrl : '';
}
function formatDate(value) {
const date = new Date(String(value || '').replace(' ', 'T') + 'Z');
return Number.isNaN(date.getTime()) ? '' : date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
}
async function renderCurrent() {
const listing = state.queue[state.current];
$('#listing-card').classList.toggle('hidden', !listing);
$('#empty-card').classList.toggle('hidden', !!listing);
if (!listing) return;
$('#listing-title').textContent = listing.title || 'Ohne Titel';
$('#listing-meta').textContent = [listing.size, listing.color, listing.condition].filter(Boolean).join(' · ') || 'Noch keine Eigenschaften';
$('#listing-sku').textContent = listing.sku || `VD-${String(listing.id).padStart(4, '0')}`;
$('#listing-price').textContent = listing.price ? `${Number(listing.price).toFixed(2).replace('.', ',')}` : '';
$('#listing-created').textContent = formatDate(listing.created_at);
const src = await imageData(listing.photos?.[0]);
const image = $('#listing-image');
if (src) { image.src = src; image.classList.remove('hidden'); } else { image.removeAttribute('src'); image.classList.add('hidden'); }
}
async function loadQueue() {
try {
const data = await api('/api/extension/queue?platform=vinted');
state.queue = data.queue || [];
state.current = Math.min(state.current, Math.max(0, state.queue.length - 1));
setConnection(true, 'Verbunden');
$('#queue-state').textContent = state.queue.length ? 'Bereit zur Übertragung' : 'Queue leer';
$('#queue-count').textContent = `${state.queue.length} / ${data.total || state.queue.length}`;
$('#queue-progress-fill').style.width = `${data.total ? ((data.total - state.queue.length) / data.total) * 100 : 0}%`;
await renderCurrent();
} catch (error) {
setConnection(false, 'Offline');
$('#queue-state').textContent = error.message;
$('#listing-card').classList.add('hidden');
$('#empty-card').classList.remove('hidden');
}
}
async function init() {
const settings = await storageGet(['serverUrl', 'provider', 'language']);
state.serverUrl = String(settings.serverUrl || 'http://localhost:8124').replace(/\/$/, '');
$('#server-url').value = state.serverUrl;
if (settings.provider) $('#provider').value = settings.provider;
if (settings.language) $('#language').value = settings.language;
await loadQueue();
}
function openUrl(url) { openTab(url); }
$('#settings-toggle').addEventListener('click', () => { $('#dashboard-view').classList.add('hidden'); $('#settings-view').classList.remove('hidden'); });
$('#back-dashboard').addEventListener('click', () => { $('#settings-view').classList.add('hidden'); $('#dashboard-view').classList.remove('hidden'); });
$('#save-settings').addEventListener('click', async () => {
state.serverUrl = $('#server-url').value.trim().replace(/\/$/, '');
if (!await ensureServerPermission(state.serverUrl)) throw new Error('Zugriff auf die Serveradresse wurde nicht erlaubt.');
await storageSet({ serverUrl: state.serverUrl, provider: $('#provider').value, language: $('#language').value });
$('#settings-view').classList.add('hidden'); $('#dashboard-view').classList.remove('hidden');
await loadQueue();
});
$('#next-listing').addEventListener('click', async () => { if (state.queue.length) state.current = (state.current + 1) % state.queue.length; await renderCurrent(); });
$('#fill-listing').addEventListener('click', async () => {
const listing = state.queue[state.current];
if (!listing) return;
await storageSet({ pendingFillListingId: listing.id });
openUrl('https://www.vinted.de/items/new');
window.close();
});
$('#prepare-images').addEventListener('click', () => openUrl(`${state.serverUrl}/#publish`));
$('#open-queue').addEventListener('click', () => openUrl(`${state.serverUrl}/#publish`));
$('#open-app').addEventListener('click', () => openUrl(state.serverUrl));
$('#empty-open-app').addEventListener('click', () => openUrl(state.serverUrl));
init();
+337
View File
@@ -0,0 +1,337 @@
/* eslint-disable no-var */
var VINTED_CATEGORIES = [
{
id: 5, name: 'Damen', children: [
{ id: 1904, name: 'Kleidung', children: [
{ id: 1, name: 'Tops & T-Shirts' },
{ id: 2, name: 'Pullover & Strickjacken' },
{ id: 3, name: 'Blusen & Hemden' },
{ id: 4, name: 'Kleider' },
{ id: 6, name: 'Röcke' },
{ id: 7, name: 'Hosen & Leggings' },
{ id: 8, name: 'Jeans' },
{ id: 9, name: 'Jacken & Mäntel' },
{ id: 10, name: 'Anzüge & Blazer' },
{ id: 11, name: 'Sportbekleidung' },
{ id: 12, name: 'Bademode' },
{ id: 13, name: 'Unterwäsche & Nachtwäsche' },
{ id: 14, name: 'Umstandsmode' },
{ id: 15, name: 'Overalls & Jumpsuits' },
{ id: 16, name: 'Kostüme & Verkleidungen' },
{ id: 1226, name: 'Shorts' },
{ id: 1904, name: 'Sonstige Kleidung' },
]},
{ id: 1905, name: 'Schuhe', children: [
{ id: 17, name: 'Sneaker' },
{ id: 18, name: 'Stiefel & Stiefeletten' },
{ id: 19, name: 'Pumps & High Heels' },
{ id: 20, name: 'Sandalen' },
{ id: 21, name: 'Ballerinas' },
{ id: 22, name: 'Schnürschuhe & Slipper' },
{ id: 23, name: 'Sportschuhe' },
{ id: 24, name: 'Hausschuhe' },
{ id: 1905, name: 'Sonstige Schuhe' },
]},
{ id: 1906, name: 'Taschen', children: [
{ id: 25, name: 'Handtaschen' },
{ id: 26, name: 'Umhängetaschen' },
{ id: 27, name: 'Rucksäcke' },
{ id: 28, name: 'Clutches & Abendtaschen' },
{ id: 29, name: 'Geldbörsen & Etuis' },
{ id: 30, name: 'Shopper & Tragetaschen' },
{ id: 1906, name: 'Sonstige Taschen' },
]},
{ id: 1907, name: 'Accessoires', children: [
{ id: 31, name: 'Schmuck' },
{ id: 32, name: 'Uhren' },
{ id: 33, name: 'Sonnenbrillen' },
{ id: 34, name: 'Schals & Tücher' },
{ id: 35, name: 'Gürtel' },
{ id: 36, name: 'Mützen & Hüte' },
{ id: 37, name: 'Handschuhe' },
{ id: 1907, name: 'Sonstige Accessoires' },
]},
{ id: 1919, name: 'Pflege & Kosmetik', children: [
{ id: 38, name: 'Makeup' },
{ id: 39, name: 'Hautpflege' },
{ id: 40, name: 'Haarpflege' },
{ id: 41, name: 'Parfum' },
{ id: 42, name: 'Nagelpflege' },
{ id: 1919, name: 'Sonstige Pflege' },
]},
]
},
{
id: 1206, name: 'Herren', children: [
{ id: 1908, name: 'Kleidung', children: [
{ id: 50, name: 'T-Shirts & Poloshirts' },
{ id: 51, name: 'Pullover & Hoodies' },
{ id: 52, name: 'Hemden' },
{ id: 53, name: 'Hosen' },
{ id: 54, name: 'Jeans' },
{ id: 55, name: 'Shorts' },
{ id: 56, name: 'Jacken & Mäntel' },
{ id: 57, name: 'Anzüge & Sakkos' },
{ id: 58, name: 'Sportbekleidung' },
{ id: 59, name: 'Bademode' },
{ id: 60, name: 'Unterwäsche & Nachtwäsche' },
{ id: 61, name: 'Kostüme & Verkleidungen' },
{ id: 1908, name: 'Sonstige Kleidung' },
]},
{ id: 1909, name: 'Schuhe', children: [
{ id: 62, name: 'Sneaker' },
{ id: 63, name: 'Stiefel & Stiefeletten' },
{ id: 64, name: 'Business-Schuhe' },
{ id: 65, name: 'Sandalen' },
{ id: 66, name: 'Sportschuhe' },
{ id: 67, name: 'Hausschuhe' },
{ id: 1909, name: 'Sonstige Schuhe' },
]},
{ id: 1910, name: 'Taschen', children: [
{ id: 68, name: 'Rucksäcke' },
{ id: 69, name: 'Umhängetaschen' },
{ id: 70, name: 'Aktentaschen' },
{ id: 71, name: 'Geldbörsen & Etuis' },
{ id: 1910, name: 'Sonstige Taschen' },
]},
{ id: 1911, name: 'Accessoires', children: [
{ id: 72, name: 'Uhren' },
{ id: 73, name: 'Sonnenbrillen' },
{ id: 74, name: 'Krawatten & Fliegen' },
{ id: 75, name: 'Gürtel' },
{ id: 76, name: 'Mützen & Hüte' },
{ id: 77, name: 'Schals' },
{ id: 78, name: 'Schmuck' },
{ id: 1911, name: 'Sonstige Accessoires' },
]},
{ id: 1920, name: 'Pflege', children: [
{ id: 79, name: 'Rasur & Bartpflege' },
{ id: 80, name: 'Hautpflege' },
{ id: 81, name: 'Parfum' },
{ id: 1920, name: 'Sonstige Pflege' },
]},
]
},
{
id: 1207, name: 'Kinder', children: [
{ id: 1912, name: 'Mädchen-Kleidung', children: [
{ id: 100, name: 'Tops & T-Shirts' },
{ id: 101, name: 'Pullover & Strickjacken' },
{ id: 102, name: 'Kleider & Röcke' },
{ id: 103, name: 'Hosen & Jeans' },
{ id: 104, name: 'Jacken & Mäntel' },
{ id: 1912, name: 'Sonstige Mädchen-Kleidung' },
]},
{ id: 1913, name: 'Jungen-Kleidung', children: [
{ id: 110, name: 'Tops & T-Shirts' },
{ id: 111, name: 'Pullover & Hoodies' },
{ id: 112, name: 'Hosen & Jeans' },
{ id: 113, name: 'Jacken & Mäntel' },
{ id: 1913, name: 'Sonstige Jungen-Kleidung' },
]},
{ id: 1914, name: 'Baby-Kleidung', children: [
{ id: 120, name: 'Strampler & Bodies' },
{ id: 121, name: 'Sets & Outfits' },
{ id: 122, name: 'Jacken' },
{ id: 1914, name: 'Sonstige Baby-Kleidung' },
]},
{ id: 1915, name: 'Kinderschuhe', children: [
{ id: 130, name: 'Sneaker' },
{ id: 131, name: 'Stiefel' },
{ id: 132, name: 'Sandalen' },
{ id: 133, name: 'Sportschuhe' },
{ id: 1915, name: 'Sonstige Kinderschuhe' },
]},
{ id: 1916, name: 'Spielzeug', children: [
{ id: 140, name: 'Puppen & Zubehör' },
{ id: 141, name: 'Fahrzeuge' },
{ id: 142, name: 'Bausteine & Baukästen' },
{ id: 143, name: 'Kuscheltiere' },
{ id: 144, name: 'Brettspiele & Puzzles' },
{ id: 145, name: 'Lernspielzeug' },
{ id: 1916, name: 'Sonstiges Spielzeug' },
]},
]
},
{
id: 1208, name: 'Haus & Wohnen', children: [
{ id: 1917, name: 'Dekoration', children: [
{ id: 200, name: 'Bilder & Poster' },
{ id: 201, name: 'Kerzen & Windlichter' },
{ id: 202, name: 'Vasen' },
{ id: 203, name: 'Spiegel' },
{ id: 204, name: 'Figuren & Skulpturen' },
{ id: 1917, name: 'Sonstige Dekoration' },
]},
{ id: 245, name: 'Küche & Essen', children: [
{ id: 210, name: 'Geschirr' },
{ id: 211, name: 'Gläser & Tassen' },
{ id: 212, name: 'Besteck' },
{ id: 213, name: 'Küchengeräte' },
{ id: 214, name: 'Aufbewahrung' },
]},
{ id: 246, name: 'Textilien', children: [
{ id: 220, name: 'Bettwäsche' },
{ id: 221, name: 'Handtücher' },
{ id: 222, name: 'Kissen & Decken' },
{ id: 223, name: 'Teppiche' },
{ id: 224, name: 'Vorhänge' },
]},
{ id: 247, name: 'Möbel', children: [
{ id: 230, name: 'Regale' },
{ id: 231, name: 'Tische' },
{ id: 232, name: 'Stühle' },
{ id: 233, name: 'Schränke & Kommoden' },
{ id: 234, name: 'Betten' },
]},
{ id: 248, name: 'Garten', children: [
{ id: 240, name: 'Gartenmöbel' },
{ id: 241, name: 'Pflanzen & Töpfe' },
{ id: 242, name: 'Grill & Outdoor' },
{ id: 243, name: 'Gartengeräte' },
]},
]
},
{
id: 1209, name: 'Unterhaltung', children: [
{ id: 249, name: 'Bücher', children: [
{ id: 300, name: 'Romane & Erzählungen' },
{ id: 301, name: 'Sachbücher' },
{ id: 302, name: 'Kinder- & Jugendbücher' },
{ id: 303, name: 'Comics & Manga' },
{ id: 304, name: 'Fach- & Schulbücher' },
]},
{ id: 250, name: 'Musik & Film', children: [
{ id: 310, name: 'CDs' },
{ id: 311, name: 'Vinyl & Schallplatten' },
{ id: 312, name: 'DVDs & Blu-ray' },
]},
{ id: 251, name: 'Spiele & Konsolen', children: [
{ id: 320, name: 'Videospiele' },
{ id: 321, name: 'Konsolen' },
{ id: 322, name: 'Zubehör' },
{ id: 323, name: 'Brettspiele' },
]},
{ id: 252, name: 'Sport & Freizeit', children: [
{ id: 330, name: 'Fitnessgeräte' },
{ id: 331, name: 'Fahrräder & Zubehör' },
{ id: 332, name: 'Camping & Wandern' },
{ id: 333, name: 'Wintersport' },
{ id: 334, name: 'Ballsport' },
]},
]
},
{
id: 1210, name: 'Elektronik & Technik', children: [
{ id: 253, name: 'Handys & Smartphones', children: [
{ id: 400, name: 'iPhones' },
{ id: 401, name: 'Samsung' },
{ id: 402, name: 'Sonstige Smartphones' },
{ id: 403, name: 'Handyhüllen & -zubehör' },
]},
{ id: 254, name: 'Computer & Tablets', children: [
{ id: 410, name: 'Laptops' },
{ id: 411, name: 'Tablets' },
{ id: 412, name: 'PCs & Desktops' },
{ id: 413, name: 'Monitore' },
{ id: 414, name: 'Netzwerkspeicher (NAS)' },
{ id: 415, name: 'Server' },
{ id: 416, name: 'Drucker & Scanner' },
{ id: 417, name: 'PC-Zubehör' },
]},
{ id: 255, name: 'Foto & Kamera', children: [
{ id: 420, name: 'Digitalkameras' },
{ id: 421, name: 'Objektive' },
{ id: 422, name: 'Kamerazubehör' },
]},
{ id: 256, name: 'Audio & HiFi', children: [
{ id: 430, name: 'Kopfhörer' },
{ id: 431, name: 'Lautsprecher' },
{ id: 432, name: 'HiFi-Anlagen' },
]},
{ id: 257, name: 'TV & Video', children: [
{ id: 440, name: 'Fernseher' },
{ id: 441, name: 'Streaming-Geräte' },
{ id: 442, name: 'Beamer' },
]},
{ id: 258, name: 'Kabel & Zubehör', children: [
{ id: 450, name: 'Kabel' },
{ id: 451, name: 'Adapter' },
{ id: 452, name: 'Speicherkarten & USB-Sticks' },
{ id: 453, name: 'Ladegeräte & Akkus' },
]},
]
},
{
id: 1211, name: 'Haustiere', children: [
{ id: 259, name: 'Hunde', children: [
{ id: 500, name: 'Hundeleine & Halsband' },
{ id: 501, name: 'Hundebett & Decke' },
{ id: 502, name: 'Hundespielzeug' },
{ id: 503, name: 'Hundebekleidung' },
]},
{ id: 260, name: 'Katzen', children: [
{ id: 510, name: 'Kratzbäume' },
{ id: 511, name: 'Katzenspielzeug' },
{ id: 512, name: 'Katzenbett' },
]},
{ id: 261, name: 'Sonstige Haustiere' },
]
},
];
function flattenCategories(tree, path) {
const result = [];
path = path || [];
for (const cat of tree) {
const currentPath = [...path, cat.name];
if (cat.children) {
for (const sub of cat.children) {
const subPath = [...currentPath, sub.name];
if (sub.children) {
for (const leaf of sub.children) {
result.push({
id: leaf.id,
name: leaf.name,
path: [...subPath, leaf.name],
pathStr: [...subPath, leaf.name].join(' > '),
search: [...subPath, leaf.name].join(' ').toLowerCase(),
});
}
} else {
result.push({
id: sub.id,
name: sub.name,
path: subPath,
pathStr: subPath.join(' > '),
search: subPath.join(' ').toLowerCase(),
});
}
}
}
}
return result;
}
var VINTED_CATEGORIES_FLAT = flattenCategories(VINTED_CATEGORIES);
function matchCategory(query) {
if (!query) return [];
const q = query.toLowerCase().trim();
const words = q.split(/\s+/);
const scored = VINTED_CATEGORIES_FLAT.map(cat => {
let score = 0;
const s = cat.search;
if (s.includes(q)) score += 100;
for (const w of words) {
if (w.length < 2) continue;
if (s.includes(w)) score += 20;
if (cat.name.toLowerCase().includes(w)) score += 30;
}
// Bonus for exact leaf match
if (cat.name.toLowerCase() === q) score += 200;
return { ...cat, score };
}).filter(c => c.score > 0);
scored.sort((a, b) => b.score - a.score);
return scored.slice(0, 15);
}