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:
@@ -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.
|
||||
@@ -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
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 |
@@ -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 |
@@ -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
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user