From bb50615e0a5a23f736e21bb005e8e15709943f5e Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Sat, 7 Feb 2026 11:23:39 +0100 Subject: [PATCH] refactor: replace hardcoded categories with Directus-powered category tree and translations --- AGENTS.md | 7 +- docs/import-categories.sh | 255 ++++++++++++++++++++++++++++ js/components/pages/page-create.js | 18 +- js/components/pages/page-home.js | 30 +++- js/components/pages/page-listing.js | 5 +- js/components/search-box.js | 76 +++++---- js/services/categories.js | 30 +++- js/services/directus.js | 9 +- js/services/listings.js | 16 +- locales/de.json | 47 ----- locales/en.json | 47 ----- locales/fr.json | 47 ----- 12 files changed, 391 insertions(+), 196 deletions(-) create mode 100644 docs/import-categories.sh diff --git a/AGENTS.md b/AGENTS.md index f51843b..803716b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,10 +40,11 @@ git push origin master ## Commit-Konvention -Wenn der User nach **"commit text (en)"** oder **"commit text (de)"** fragt: -- Erstelle eine einzeilige Commit-Message im Format: `git commit -m "..."` +Wenn der User nach **"commit text"** fragt: +- **Immer nur eine einzeilige** Commit-Message im Format: `git commit -m "..."` +- Keine mehrzeiligen Messages oder Body-Text - Nutze [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` -- Sprache entsprechend der Anfrage (en/de) +- Standardsprache: Englisch (außer User fragt explizit nach "de") ## Dateistruktur diff --git a/docs/import-categories.sh b/docs/import-categories.sh new file mode 100644 index 0000000..9f77e76 --- /dev/null +++ b/docs/import-categories.sh @@ -0,0 +1,255 @@ +#!/bin/bash +# Bulk-Import: All categories for dgray.io +# Usage: DIRECTUS_TOKEN=your_admin_token bash docs/import-categories.sh + +API="https://api.dgray.io" +TOKEN="${DIRECTUS_TOKEN:?Set DIRECTUS_TOKEN environment variable}" + +create_category() { + local slug="$1" name="$2" icon="$3" sort="$4" parent="$5" + local name_de="$6" name_en="$7" name_fr="$8" + + local parent_field="null" + if [ -n "$parent" ]; then + parent_field="\"$parent\"" + fi + + local response + response=$(curl -s -X POST "$API/items/categories" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$name\", + \"slug\": \"$slug\", + \"icon\": \"$icon\", + \"status\": \"published\", + \"sort\": $sort, + \"parent\": $parent_field, + \"translations\": { + \"create\": [ + { \"languages_code\": \"de-DE\", \"name\": \"$name_de\" }, + { \"languages_code\": \"en-US\", \"name\": \"$name_en\" }, + { \"languages_code\": \"fr-FR\", \"name\": \"$name_fr\" } + ] + } + }") + + local id + id=$(echo "$response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + + if [ -n "$id" ]; then + echo "$id" + else + echo "ERROR: $response" >&2 + echo "" + fi +} + +echo "=== Importing categories into Directus ===" +echo "" + +# ── 1. Electronics ── +echo "1/12 Electronics..." +ID_ELECTRONICS=$(create_category "electronics" "Electronics" "💻" 1 "" \ + "Elektronik" "Electronics" "Électronique") +echo " → $ID_ELECTRONICS" + +create_category "phones" "Phones & Tablets" "📱" 1 "$ID_ELECTRONICS" \ + "Handy & Tablets" "Phones & Tablets" "Téléphones & Tablettes" > /dev/null +create_category "computers" "Computers & Accessories" "🖥️" 2 "$ID_ELECTRONICS" \ + "Computer & Zubehör" "Computers & Accessories" "Ordinateurs & Accessoires" > /dev/null +create_category "tv-audio" "TV, Audio & Video" "📺" 3 "$ID_ELECTRONICS" \ + "TV, Audio & Video" "TV, Audio & Video" "TV, Audio & Vidéo" > /dev/null +create_category "gaming" "Gaming & Consoles" "🎮" 4 "$ID_ELECTRONICS" \ + "Gaming & Konsolen" "Gaming & Consoles" "Jeux & Consoles" > /dev/null +create_category "appliances" "Home Appliances" "🏠" 5 "$ID_ELECTRONICS" \ + "Haushaltsgeräte" "Home Appliances" "Électroménager" > /dev/null +create_category "cameras" "Cameras & Photography" "📷" 6 "$ID_ELECTRONICS" \ + "Kameras & Fotografie" "Cameras & Photography" "Appareils photo" > /dev/null +echo " ✓ 6 subcategories" + +# ── 2. Vehicles ── +echo "2/12 Vehicles..." +ID_VEHICLES=$(create_category "vehicles" "Vehicles" "🚗" 2 "" \ + "Fahrzeuge" "Vehicles" "Véhicules") +echo " → $ID_VEHICLES" + +create_category "cars" "Cars" "🚙" 1 "$ID_VEHICLES" \ + "Autos" "Cars" "Voitures" > /dev/null +create_category "motorcycles" "Motorcycles" "🏍️" 2 "$ID_VEHICLES" \ + "Motorräder" "Motorcycles" "Motos" > /dev/null +create_category "bikes" "Bicycles & E-Bikes" "🚲" 3 "$ID_VEHICLES" \ + "Fahrräder & E-Bikes" "Bicycles & E-Bikes" "Vélos & E-Bikes" > /dev/null +create_category "vehicle-parts" "Parts & Accessories" "🔧" 4 "$ID_VEHICLES" \ + "Ersatzteile & Zubehör" "Parts & Accessories" "Pièces & Accessoires" > /dev/null +create_category "boats" "Boats & Watercraft" "⛵" 5 "$ID_VEHICLES" \ + "Boote & Wasserfahrzeuge" "Boats & Watercraft" "Bateaux" > /dev/null +echo " ✓ 5 subcategories" + +# ── 3. Home & Garden ── +echo "3/12 Home & Garden..." +ID_HOME=$(create_category "home-garden" "Home & Garden" "🏡" 3 "" \ + "Haus & Garten" "Home & Garden" "Maison & Jardin") +echo " → $ID_HOME" + +create_category "furniture" "Furniture" "🪑" 1 "$ID_HOME" \ + "Möbel" "Furniture" "Meubles" > /dev/null +create_category "kitchen" "Kitchen & Dining" "🍳" 2 "$ID_HOME" \ + "Küche & Essen" "Kitchen & Dining" "Cuisine" > /dev/null +create_category "garden" "Garden & Outdoor" "🌿" 3 "$ID_HOME" \ + "Garten & Outdoor" "Garden & Outdoor" "Jardin & Extérieur" > /dev/null +create_category "tools" "Tools & Workshop" "🔨" 4 "$ID_HOME" \ + "Werkzeuge & Werkstatt" "Tools & Workshop" "Outils & Atelier" > /dev/null +create_category "decoration" "Decoration & Art" "🖼️" 5 "$ID_HOME" \ + "Deko & Kunst" "Decoration & Art" "Décoration & Art" > /dev/null +create_category "bathroom" "Bathroom & Sanitary" "🚿" 6 "$ID_HOME" \ + "Bad & Sanitär" "Bathroom & Sanitary" "Salle de bain" > /dev/null +echo " ✓ 6 subcategories" + +# ── 4. Fashion & Accessories ── +echo "4/12 Fashion..." +ID_FASHION=$(create_category "fashion" "Fashion & Accessories" "👗" 4 "" \ + "Mode & Accessoires" "Fashion & Accessories" "Mode & Accessoires") +echo " → $ID_FASHION" + +create_category "women" "Women's Fashion" "👚" 1 "$ID_FASHION" \ + "Damenmode" "Women's Fashion" "Mode femme" > /dev/null +create_category "men" "Men's Fashion" "👔" 2 "$ID_FASHION" \ + "Herrenmode" "Men's Fashion" "Mode homme" > /dev/null +create_category "kids-fashion" "Kids' Fashion" "👶" 3 "$ID_FASHION" \ + "Kindermode" "Kids' Fashion" "Mode enfant" > /dev/null +create_category "shoes" "Shoes" "👟" 4 "$ID_FASHION" \ + "Schuhe" "Shoes" "Chaussures" > /dev/null +create_category "watches-jewelry" "Watches & Jewelry" "⌚" 5 "$ID_FASHION" \ + "Uhren & Schmuck" "Watches & Jewelry" "Montres & Bijoux" > /dev/null +create_category "bags" "Bags & Luggage" "👜" 6 "$ID_FASHION" \ + "Taschen & Gepäck" "Bags & Luggage" "Sacs & Bagages" > /dev/null +echo " ✓ 6 subcategories" + +# ── 5. Sports & Leisure ── +echo "5/12 Sports..." +ID_SPORTS=$(create_category "sports" "Sports & Leisure" "⚽" 5 "" \ + "Sport & Freizeit" "Sports & Leisure" "Sports & Loisirs") +echo " → $ID_SPORTS" + +create_category "fitness" "Fitness & Gym" "🏋️" 1 "$ID_SPORTS" \ + "Fitness & Training" "Fitness & Gym" "Fitness" > /dev/null +create_category "outdoor-sports" "Hiking & Outdoor" "🥾" 2 "$ID_SPORTS" \ + "Wandern & Outdoor" "Hiking & Outdoor" "Randonnée & Outdoor" > /dev/null +create_category "winter-sports" "Winter Sports" "⛷️" 3 "$ID_SPORTS" \ + "Wintersport" "Winter Sports" "Sports d'hiver" > /dev/null +create_category "water-sports" "Water Sports" "🏄" 4 "$ID_SPORTS" \ + "Wassersport" "Water Sports" "Sports nautiques" > /dev/null +create_category "team-sports" "Team Sports" "🏀" 5 "$ID_SPORTS" \ + "Mannschaftssport" "Team Sports" "Sports d'équipe" > /dev/null +create_category "cycling" "Cycling Gear" "🚴" 6 "$ID_SPORTS" \ + "Radzubehör" "Cycling Gear" "Équipement cyclisme" > /dev/null +echo " ✓ 6 subcategories" + +# ── 6. Family & Kids ── +echo "6/12 Family & Kids..." +ID_FAMILY=$(create_category "family-kids" "Family & Kids" "👨‍👩‍👧" 6 "" \ + "Familie & Kinder" "Family & Kids" "Famille & Enfants") +echo " → $ID_FAMILY" + +create_category "toys" "Toys & Games" "🧸" 1 "$ID_FAMILY" \ + "Spielzeug & Spiele" "Toys & Games" "Jouets & Jeux" > /dev/null +create_category "baby" "Baby & Toddler" "🍼" 2 "$ID_FAMILY" \ + "Baby & Kleinkind" "Baby & Toddler" "Bébé & Puériculture" > /dev/null +create_category "school" "School Supplies" "🎒" 3 "$ID_FAMILY" \ + "Schulsachen" "School Supplies" "Fournitures scolaires" > /dev/null +echo " ✓ 3 subcategories" + +# ── 7. Books & Media ── +echo "7/12 Books & Media..." +ID_BOOKS=$(create_category "books-media" "Books & Media" "📚" 7 "" \ + "Bücher & Medien" "Books & Media" "Livres & Médias") +echo " → $ID_BOOKS" + +create_category "fiction" "Fiction" "📖" 1 "$ID_BOOKS" \ + "Belletristik" "Fiction" "Fiction" > /dev/null +create_category "nonfiction" "Non-Fiction & Science" "📘" 2 "$ID_BOOKS" \ + "Sachbücher" "Non-Fiction & Science" "Non-fiction" > /dev/null +create_category "textbooks" "Textbooks & Courses" "🎓" 3 "$ID_BOOKS" \ + "Lehrbücher & Kurse" "Textbooks & Courses" "Manuels scolaires" > /dev/null +create_category "music-movies" "Music, Movies & Games" "🎵" 4 "$ID_BOOKS" \ + "Musik, Filme & Spiele" "Music, Movies & Games" "Musique, Films & Jeux" > /dev/null +echo " ✓ 4 subcategories" + +# ── 8. Pets & Animals ── +echo "8/12 Pets..." +ID_PETS=$(create_category "pets" "Pets & Animals" "🐾" 8 "" \ + "Haustiere & Tierzubehör" "Pets & Animals" "Animaux") +echo " → $ID_PETS" + +create_category "pet-supplies" "Pet Supplies" "🦴" 1 "$ID_PETS" \ + "Tierbedarf" "Pet Supplies" "Accessoires animaux" > /dev/null +create_category "pet-adoption" "Adoption & Rehoming" "🐕" 2 "$ID_PETS" \ + "Adoption & Vermittlung" "Adoption & Rehoming" "Adoption" > /dev/null +echo " ✓ 2 subcategories" + +# ── 9. Jobs & Services ── +echo "9/12 Jobs..." +ID_JOBS=$(create_category "jobs" "Jobs & Services" "💼" 9 "" \ + "Jobs & Dienstleistungen" "Jobs & Services" "Emplois & Services") +echo " → $ID_JOBS" + +create_category "full-time" "Full-Time" "🏢" 1 "$ID_JOBS" \ + "Festanstellung" "Full-Time" "CDI" > /dev/null +create_category "part-time" "Part-Time & Mini Jobs" "⏰" 2 "$ID_JOBS" \ + "Teilzeit & Minijobs" "Part-Time & Mini Jobs" "Temps partiel" > /dev/null +create_category "freelance" "Freelance & Remote" "💻" 3 "$ID_JOBS" \ + "Freelance & Remote" "Freelance & Remote" "Freelance & Télétravail" > /dev/null +create_category "services" "Services & Crafts" "🛠️" 4 "$ID_JOBS" \ + "Dienstleistungen & Handwerk" "Services & Crafts" "Services & Artisanat" > /dev/null +echo " ✓ 4 subcategories" + +# ── 10. Real Estate ── +echo "10/12 Real Estate..." +ID_REALESTATE=$(create_category "real-estate" "Real Estate" "🏘️" 10 "" \ + "Immobilien" "Real Estate" "Immobilier") +echo " → $ID_REALESTATE" + +create_category "apartments" "Apartments" "🏢" 1 "$ID_REALESTATE" \ + "Wohnungen" "Apartments" "Appartements" > /dev/null +create_category "houses" "Houses" "🏠" 2 "$ID_REALESTATE" \ + "Häuser" "Houses" "Maisons" > /dev/null +create_category "rooms" "Rooms & WG" "🛏️" 3 "$ID_REALESTATE" \ + "Zimmer & WG" "Rooms & WG" "Chambres & Colocation" > /dev/null +create_category "commercial" "Commercial" "🏪" 4 "$ID_REALESTATE" \ + "Gewerbe" "Commercial" "Commercial" > /dev/null +echo " ✓ 4 subcategories" + +# ── 11. Collectibles & Hobbies ── +echo "11/12 Collectibles..." +ID_COLLECT=$(create_category "collectibles" "Collectibles & Hobbies" "🏆" 11 "" \ + "Sammeln & Hobby" "Collectibles & Hobbies" "Collections & Hobbies") +echo " → $ID_COLLECT" + +create_category "antiques" "Antiques" "🏺" 1 "$ID_COLLECT" \ + "Antiquitäten" "Antiques" "Antiquités" > /dev/null +create_category "coins-stamps" "Coins & Stamps" "🪙" 2 "$ID_COLLECT" \ + "Münzen & Briefmarken" "Coins & Stamps" "Monnaies & Timbres" > /dev/null +create_category "models" "Models & Figures" "🚂" 3 "$ID_COLLECT" \ + "Modellbau & Figuren" "Models & Figures" "Modélisme & Figurines" > /dev/null +create_category "art-crafts" "Art & Handmade" "🎨" 4 "$ID_COLLECT" \ + "Kunst & Handgemacht" "Art & Handmade" "Art & Fait main" > /dev/null +echo " ✓ 4 subcategories" + +# ── 12. Other ── +echo "12/12 Other..." +ID_OTHER=$(create_category "other" "Other" "📦" 12 "" \ + "Sonstiges" "Other" "Autres") +echo " → $ID_OTHER" + +create_category "free-stuff" "Free Stuff" "🎁" 1 "$ID_OTHER" \ + "Zu verschenken" "Free Stuff" "Gratuit" > /dev/null +create_category "barter" "Barter & Trade" "🔄" 2 "$ID_OTHER" \ + "Tauschen" "Barter & Trade" "Troc" > /dev/null +create_category "lost-found" "Lost & Found" "🔍" 3 "$ID_OTHER" \ + "Fundsachen" "Lost & Found" "Objets trouvés" > /dev/null +echo " ✓ 3 subcategories" + +echo "" +echo "=== Import complete! ===" +echo "12 main categories + 59 subcategories created." diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index 454fe0b..84cb5f3 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -21,7 +21,7 @@ class PageCreate extends HTMLElement { this.formData = this.loadDraft() || this.getEmptyFormData() this.imageFiles = [] this.imagePreviews = [] - this.categories = [] + this.categoryTree = [] this.submitting = false this.isNewAccount = true } @@ -183,10 +183,10 @@ class PageCreate extends HTMLElement { async loadCategories() { try { - this.categories = await categoriesService.getAll() + this.categoryTree = await categoriesService.getTree() } catch (e) { console.error('Failed to load categories:', e) - this.categories = [] + this.categoryTree = [] } } @@ -238,10 +238,14 @@ class PageCreate extends HTMLElement { diff --git a/js/components/pages/page-home.js b/js/components/pages/page-home.js index 18dff4c..f1b86a2 100644 --- a/js/components/pages/page-home.js +++ b/js/components/pages/page-home.js @@ -21,6 +21,7 @@ class PageHome extends HTMLElement { // Filter state this.query = '' this.category = '' + this.subcategory = '' this.sort = 'newest' this.minPrice = null this.maxPrice = null @@ -39,8 +40,7 @@ class PageHome extends HTMLElement { this.setupPullToRefresh() this.loadListings() this.unsubscribe = i18n.subscribe(() => { - this.render() - this.setupEventListeners() + this.updateTextContent() }) // Re-render listings on auth change to show owner badges @@ -129,6 +129,7 @@ class PageHome extends HTMLElement { searchBox?.addEventListener('filter-change', (e) => { const hadLocation = this.hasUserLocation() this.category = e.detail.category || '' + this.subcategory = e.detail.subcategory || '' this.query = e.detail.query || '' this.updateUserLocation(e.detail) this.updateUrl() @@ -147,6 +148,7 @@ class PageHome extends HTMLElement { e.stopPropagation() this.query = e.detail.query || '' this.category = e.detail.category || '' + this.subcategory = e.detail.subcategory || '' this.updateUserLocation(e.detail) this.updateUrl() this.resetAndSearch() @@ -184,6 +186,7 @@ class PageHome extends HTMLElement { this.querySelector('#clear-filters')?.addEventListener('click', () => { this.query = '' this.category = '' + this.subcategory = '' this.sort = 'newest' this.minPrice = null this.maxPrice = null @@ -210,6 +213,7 @@ class PageHome extends HTMLElement { const filters = { search: this.query || undefined, category: this.category || undefined, + subcategory: this.subcategory || undefined, sort: isDistanceSort ? 'newest' : this.sort, // Backend can't sort by distance minPrice: this.minPrice, maxPrice: this.maxPrice, @@ -348,6 +352,28 @@ class PageHome extends HTMLElement { } } + updateTextContent() { + const titleEl = this.querySelector('.listings-title') + if (titleEl) titleEl.textContent = this.getListingsTitle() + + const sortSelect = this.querySelector('#sort-select') + if (sortSelect) { + sortSelect.querySelectorAll('option').forEach(opt => { + const key = { + distance: 'search.sortDistance', + newest: 'search.sortNewest', + oldest: 'search.sortOldest', + price_asc: 'search.sortPriceAsc', + price_desc: 'search.sortPriceDesc' + }[opt.value] + if (key) opt.textContent = t(key) + }) + } + + const searchBox = this.querySelector('search-box') + if (searchBox) searchBox.loadAndRender() + } + getListingsTitle() { if (this.hasActiveFilters()) { const count = this.listings.length diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js index d00654f..20d7ca7 100644 --- a/js/components/pages/page-listing.js +++ b/js/components/pages/page-listing.js @@ -7,6 +7,7 @@ import { escapeHTML } from '../../utils/helpers.js' import '../chat-widget.js' import '../location-map.js' import '../listing-card.js' +import { categoriesService } from '../../services/categories.js' class PageListing extends HTMLElement { constructor() { @@ -150,7 +151,9 @@ class PageListing extends HTMLElement { const firstImage = hasImages ? this.getImageUrl(images[0]) : null this.allImages = images - const categoryName = this.listing.category?.name || '' + const categoryName = this.listing.category + ? categoriesService.getTranslatedName(this.listing.category) + : '' const priceInfo = this.getFormattedPrice() const createdDate = this.listing.date_created ? new Date(this.listing.date_created).toLocaleDateString() diff --git a/js/components/search-box.js b/js/components/search-box.js index b085bd2..29f7286 100644 --- a/js/components/search-box.js +++ b/js/components/search-box.js @@ -1,16 +1,6 @@ import { t, i18n } from '../i18n.js' import { escapeHTML } from '../utils/helpers.js' - -const CATEGORIES = { - electronics: ['phones', 'computers', 'tv_audio', 'gaming', 'appliances'], - vehicles: ['cars', 'motorcycles', 'bikes', 'parts'], - furniture: ['living', 'bedroom', 'office', 'outdoor_furniture'], - clothing: ['women', 'men', 'kids', 'shoes', 'accessories'], - sports: ['fitness', 'outdoor', 'winter', 'water', 'team_sports'], - books: ['fiction', 'nonfiction', 'textbooks', 'music_movies'], - garden: ['plants', 'tools', 'outdoor_living', 'decoration'], - other: ['collectibles', 'art', 'handmade', 'services'] -} +import { categoriesService } from '../services/categories.js' const COUNTRIES = ['ch', 'de', 'at', 'fr', 'it', 'li'] const RADIUS_OPTIONS = [5, 10, 20, 50, 100, 200] @@ -51,6 +41,7 @@ class SearchBox extends HTMLElement { this.selectedRadius = 50 this.useCurrentLocation = false this.searchQuery = '' + this.categoryTree = [] this.geoLoading = false this.currentLat = null this.currentLng = null @@ -83,12 +74,18 @@ class SearchBox extends HTMLElement { this.searchQuery = this.getAttribute('query') } + this.loadAndRender() + this.unsubscribe = i18n.subscribe(() => this.loadAndRender()) + } + + async loadAndRender() { + try { + this.categoryTree = await categoriesService.getTree() + } catch (e) { + this.categoryTree = [] + } this.render() this.setupEventListeners() - this.unsubscribe = i18n.subscribe(() => { - this.render() - this.setupEventListeners() - }) } disconnectedCallback() { @@ -151,6 +148,18 @@ class SearchBox extends HTMLElement { ` } + getCategoryLabel() { + const cat = this.categoryTree.find(c => c.slug === this.selectedCategory) + if (!cat) return this.selectedCategory + + const catName = categoriesService.getTranslatedName(cat) + if (this.selectedSubcategory) { + const sub = (cat.children || []).find(s => s.slug === this.selectedSubcategory) + if (sub) return `${catName} › ${categoriesService.getTranslatedName(sub)}` + } + return catName + } + hasActiveFilters() { return this.searchQuery || this.selectedCategory || this.useCurrentLocation } @@ -183,9 +192,7 @@ class SearchBox extends HTMLElement { // Category badge if (this.selectedCategory) { - const categoryLabel = this.selectedSubcategory - ? `${t(`categories.${this.selectedCategory}`)} › ${t(`subcategories.${this.selectedSubcategory}`)}` - : t(`categories.${this.selectedCategory}`) + const categoryLabel = this.getCategoryLabel() badges.push(/* html */`
- - ${CATEGORIES[cat].map(sub => ` - `).join('')}
@@ -696,15 +701,16 @@ class SearchBox extends HTMLElement { if (filters.useCurrentLocation !== undefined) this.useCurrentLocation = filters.useCurrentLocation this.saveFiltersToStorage() - this.render() - this.setupEventListeners() + if (this.categoryTree && this.categoryTree.length > 0) { + this.render() + this.setupEventListeners() + } } clearFilters() { this.resetFilters() localStorage.removeItem('searchFilters') - this.render() - this.setupEventListeners() + this.loadAndRender() } } @@ -1174,4 +1180,4 @@ style.textContent = /* css */` ` document.head.appendChild(style) -export { SearchBox, CATEGORIES, COUNTRIES, RADIUS_OPTIONS } +export { SearchBox, COUNTRIES, RADIUS_OPTIONS } diff --git a/js/services/categories.js b/js/services/categories.js index bc3fb66..64642f8 100644 --- a/js/services/categories.js +++ b/js/services/categories.js @@ -10,6 +10,7 @@ class CategoriesService { this.cache = null this.cacheTimestamp = 0 this.cacheTimeout = 10 * 60 * 1000 // 10 minutes + this._pending = null } async getAll() { @@ -17,10 +18,19 @@ class CategoriesService { return this.cache } - const categories = await directus.getCategories() - this.cache = categories - this.cacheTimestamp = Date.now() - return categories + if (this._pending) return this._pending + + this._pending = directus.getCategories().then(categories => { + this.cache = categories + this.cacheTimestamp = Date.now() + this._pending = null + return categories + }).catch(err => { + this._pending = null + throw err + }) + + return this._pending } async getById(id) { @@ -32,7 +42,17 @@ class CategoriesService { } async getTree() { - return directus.getCategoryTree() + const all = await this.getAll() + return this.buildTree(all) + } + + buildTree(categories, parentId = null) { + return categories + .filter(cat => (cat.parent?.id || cat.parent) === parentId) + .map(cat => ({ + ...cat, + children: this.buildTree(categories, cat.id) + })) } async getSubcategories(parentId) { diff --git a/js/services/directus.js b/js/services/directus.js index 0ae380e..72e4a3f 100644 --- a/js/services/directus.js +++ b/js/services/directus.js @@ -134,7 +134,7 @@ class DirectusService { * @returns {Promise} Response data or null for 204 * @throws {DirectusError} On request failure */ - async request(endpoint, options = {}) { + async request(endpoint, options = {}, _retryCount = 0) { const url = `${this.baseUrl}${endpoint}` const headers = { @@ -163,6 +163,12 @@ class DirectusService { this.clearTokens() } } + + if (response.status === 429 && _retryCount < 3) { + const retryAfter = parseInt(response.headers.get('Retry-After') || '3', 10) + await new Promise(r => setTimeout(r, retryAfter * 1000)) + return this.request(endpoint, options, _retryCount + 1) + } if (!response.ok) { const error = await response.json().catch(() => ({})) @@ -405,6 +411,7 @@ class DirectusService { 'category.id', 'category.name', 'category.slug', + 'category.translations.*', 'location.id', 'location.name', 'location.postal_code', diff --git a/js/services/listings.js b/js/services/listings.js index 7d9b35e..6f65078 100644 --- a/js/services/listings.js +++ b/js/services/listings.js @@ -4,13 +4,27 @@ */ import { directus } from './directus.js' +import { categoriesService } from './categories.js' class ListingsService { async getListingsWithFilters(filters = {}) { const directusFilter = { status: { _eq: 'published' } } if (filters.category) { - directusFilter.category = { slug: { _eq: filters.category } } + if (filters.subcategory) { + directusFilter.category = { slug: { _eq: filters.subcategory } } + } else { + const all = await categoriesService.getAll() + const parent = all.find(c => c.slug === filters.category && !c.parent) + if (parent) { + const childSlugs = all + .filter(c => (c.parent?.id || c.parent) === parent.id) + .map(c => c.slug) + directusFilter.category = { slug: { _in: [filters.category, ...childSlugs] } } + } else { + directusFilter.category = { slug: { _eq: filters.category } } + } + } } if (filters.location) { diff --git a/locales/de.json b/locales/de.json index 3f026bd..5a9e70a 100644 --- a/locales/de.json +++ b/locales/de.json @@ -40,16 +40,6 @@ "retry": "Erneut versuchen", "offline": "Keine Internetverbindung" }, - "categories": { - "electronics": "Elektronik", - "furniture": "Möbel", - "clothing": "Kleidung", - "vehicles": "Fahrzeuge", - "sports": "Sport & Freizeit", - "books": "Bücher & Medien", - "garden": "Garten", - "other": "Sonstiges" - }, "search": { "title": "Suche", "placeholder": "Suchbegriff eingeben...", @@ -76,43 +66,6 @@ "sortPriceDesc": "Preis absteigend", "sortDistance": "In der Nähe" }, - "subcategories": { - "phones": "Handy & Telefon", - "computers": "Computer & Zubehör", - "tv_audio": "TV & Audio", - "gaming": "Gaming & Konsolen", - "appliances": "Haushaltsgeräte", - "cars": "Autos", - "motorcycles": "Motorräder", - "bikes": "Fahrräder", - "parts": "Ersatzteile & Zubehör", - "living": "Wohnzimmer", - "bedroom": "Schlafzimmer", - "office": "Büro", - "outdoor_furniture": "Gartenmöbel", - "women": "Damen", - "men": "Herren", - "kids": "Kinder", - "shoes": "Schuhe", - "accessories": "Accessoires", - "fitness": "Fitness", - "outdoor": "Outdoor", - "winter": "Wintersport", - "water": "Wassersport", - "team_sports": "Mannschaftssport", - "fiction": "Belletristik", - "nonfiction": "Sachbücher", - "textbooks": "Lehrbücher", - "music_movies": "Musik & Filme", - "plants": "Pflanzen", - "tools": "Werkzeuge", - "outdoor_living": "Outdoor-Living", - "decoration": "Dekoration", - "collectibles": "Sammlerstücke", - "art": "Kunst", - "handmade": "Handgemacht", - "services": "Dienstleistungen" - }, "countries": { "ch": "Schweiz", "de": "Deutschland", diff --git a/locales/en.json b/locales/en.json index 34183b7..68137ae 100644 --- a/locales/en.json +++ b/locales/en.json @@ -40,16 +40,6 @@ "retry": "Try again", "offline": "No internet connection" }, - "categories": { - "electronics": "Electronics", - "furniture": "Furniture", - "clothing": "Clothing", - "vehicles": "Vehicles", - "sports": "Sports & Leisure", - "books": "Books & Media", - "garden": "Garden", - "other": "Other" - }, "search": { "title": "Search", "placeholder": "Enter search term...", @@ -76,43 +66,6 @@ "sortPriceDesc": "Price: high to low", "sortDistance": "Nearby" }, - "subcategories": { - "phones": "Phones & Tablets", - "computers": "Computers & Accessories", - "tv_audio": "TV & Audio", - "gaming": "Gaming & Consoles", - "appliances": "Appliances", - "cars": "Cars", - "motorcycles": "Motorcycles", - "bikes": "Bicycles", - "parts": "Parts & Accessories", - "living": "Living Room", - "bedroom": "Bedroom", - "office": "Office", - "outdoor_furniture": "Outdoor Furniture", - "women": "Women", - "men": "Men", - "kids": "Kids", - "shoes": "Shoes", - "accessories": "Accessories", - "fitness": "Fitness", - "outdoor": "Outdoor", - "winter": "Winter Sports", - "water": "Water Sports", - "team_sports": "Team Sports", - "fiction": "Fiction", - "nonfiction": "Non-Fiction", - "textbooks": "Textbooks", - "music_movies": "Music & Movies", - "plants": "Plants", - "tools": "Tools", - "outdoor_living": "Outdoor Living", - "decoration": "Decoration", - "collectibles": "Collectibles", - "art": "Art", - "handmade": "Handmade", - "services": "Services" - }, "countries": { "ch": "Switzerland", "de": "Germany", diff --git a/locales/fr.json b/locales/fr.json index e339793..ab5ef0f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -40,16 +40,6 @@ "retry": "Réessayer", "offline": "Pas de connexion internet" }, - "categories": { - "electronics": "Électronique", - "furniture": "Meubles", - "clothing": "Vêtements", - "vehicles": "Véhicules", - "sports": "Sports & Loisirs", - "books": "Livres & Médias", - "garden": "Jardin", - "other": "Autres" - }, "search": { "title": "Recherche", "placeholder": "Entrez un terme de recherche...", @@ -76,43 +66,6 @@ "sortPriceDesc": "Prix décroissant", "sortDistance": "À proximité" }, - "subcategories": { - "phones": "Téléphones & Tablettes", - "computers": "Ordinateurs & Accessoires", - "tv_audio": "TV & Audio", - "gaming": "Jeux & Consoles", - "appliances": "Électroménager", - "cars": "Voitures", - "motorcycles": "Motos", - "bikes": "Vélos", - "parts": "Pièces & Accessoires", - "living": "Salon", - "bedroom": "Chambre", - "office": "Bureau", - "outdoor_furniture": "Mobilier extérieur", - "women": "Femmes", - "men": "Hommes", - "kids": "Enfants", - "shoes": "Chaussures", - "accessories": "Accessoires", - "fitness": "Fitness", - "outdoor": "Plein air", - "winter": "Sports d'hiver", - "water": "Sports nautiques", - "team_sports": "Sports d'équipe", - "fiction": "Fiction", - "nonfiction": "Non-fiction", - "textbooks": "Manuels scolaires", - "music_movies": "Musique & Films", - "plants": "Plantes", - "tools": "Outils", - "outdoor_living": "Vie extérieure", - "decoration": "Décoration", - "collectibles": "Objets de collection", - "art": "Art", - "handmade": "Fait main", - "services": "Services" - }, "countries": { "ch": "Suisse", "de": "Allemagne",