Views, api uind readme

This commit is contained in:
none 2026-06-12 14:05:30 +02:00
parent d534ca87a1
commit caf416ec32
8 changed files with 572 additions and 0 deletions

71
Dilomarkt/README.md Normal file
View file

@ -0,0 +1,71 @@
# Dilomarkt Digitaler Lokalmarkt
Dilomarkt ist ein spezialisierter Marktplatz, der lokale Anbieter von Baumaterialien und Werkzeugen mit regionalen Nachfragern verbindet. Entwickelt von Studenten, adressiert die Plattform die Ineffizienzen großer, unspezialisierter Portale und stärkt lokale Wertschöpfungsketten.
## Tech Stack
- **Server:** PHP mit Laravel-Framework
- **Datenbank:** SQLite
- **Client:** Vue.js (HTML, CSS, JavaScript)
- **Client-Server-Kommunikation:** REST API
- **Weiteres:** Responsive Webdesign
## Installation
```bash
git clone https://github.com/serialexperiments0815/Dilomarkt---Digitaler-Lokalmarkt/
```
### Backend (Laravel)
```bash
cd Dilomarkt---Digitaler-Lokalmarkt/Dilomarkt/dilomarkt-api
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan db:seed
```
### Frontend (Vue)
```bash
cd Dilomarkt---Digitaler-Lokalmarkt/Dilomarkt
npm install
```
## Verwendung
**Terminal 1 Backend starten:**
```bash
cd dilomarkt-api
php artisan serve
```
API läuft auf `http://localhost:8000`
**Terminal 2 Frontend starten:**
```bash
cd Dilomarkt
npm run dev
```
App läuft auf `http://localhost:5173`
## API Endpunkte
| Method | Endpunkt | Beschreibung |
|--------|----------|--------------|
| GET | `/api/products` | Produkte mit Geo-Filter (PLZ, Radius, Kategorie) |
| GET | `/api/products/{id}` | Einzelprodukt mit Preisvergleich |
| GET | `/api/providers` | Alle Anbieter |
| GET | `/api/providers/{id}` | Anbieterprofil mit Statistiken |
### Query Parameter für `/api/products`
| Parameter | Beispiel | Beschreibung |
|-----------|----------|--------------|
| `plz` | `42103` | Standort (NRW) |
| `radius` | `20` | Umkreis in km |
| `q` | `Klinker` | Suchbegriff |
| `category` | `Werkzeug` | Kategorie |
| `max_price` | `100` | Maximaler Preis |
| `type` | `Fachhandel` | Anbietertyp |

View file

@ -7,3 +7,74 @@
</nav> </nav>
</div> </div>
</template> </template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
</script>
<style>
:root {
--bg-dark-card: #1e1e1e;
--bg-app-dark: #121212;
--text-main: #e0e0e0;
--primary-accent: #2196f3;
--border-color: #333333;
}
body { margin: 0; background-color: var(--bg-app-dark); color: var(--text-main); font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.view-container { max-width: 1200px; margin: 0 auto; padding: 20px; padding-bottom: 90px; }
.hero-section { text-align: center; padding: 20px 10px; }
.search-container { display: flex; gap: 10px; max-width: 700px; margin: 20px auto; background: var(--bg-dark-card); padding: 10px; border-radius: 8px; border: 1px solid var(--border-color); }
.search-container input { flex: 2; background: transparent; border: none; color: white; padding: 10px; font-size: 14px; }
.search-container input.location-input { flex: 1; border-left: 1px solid var(--border-color); }
.search-container select { background: #2a2a2a; border: none; border-left: 1px solid var(--border-color); color: var(--text-main); padding: 10px; cursor: pointer; }
.marketplace-body { display: flex; gap: 20px; }
.filter-sidebar { width: 220px; flex-shrink: 0; background: var(--bg-dark-card); padding: 20px; border-radius: 8px; height: fit-content; }
.filter-sidebar h3 { font-size: 11px; color: #888; letter-spacing: 1px; margin: 16px 0 8px; }
.filter-sidebar label { display: block; margin: 8px 0; font-size: 14px; cursor: pointer; }
.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
.product-card { background: var(--bg-dark-card); border-radius: 8px; overflow: hidden; border: 1px solid var(--border-color); cursor: pointer; transition: border-color 0.2s; }
.product-card:hover { border-color: var(--primary-accent); }
.card-image-placeholder { background: #2a2a2a; height: 120px; display: flex; align-items: center; justify-content: center; font-size: 40px; }
.card-details { padding: 15px; }
.price { font-size: 20px; font-weight: bold; color: var(--primary-accent); }
.card-footer { display: flex; justify-content: space-between; font-size: 12px; color: #777; margin-top: 10px; }
.badge { background: #2e7d32; color: white; padding: 2px 6px; border-radius: 4px; }
.badge-cat { background: #37474f; color: #ccc; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.back-link { background: transparent; border: none; color: var(--primary-accent); cursor: pointer; margin-bottom: 20px; font-size: 14px; padding: 0; }
.detail-layout { display: flex; gap: 30px; margin-top: 20px; }
.detail-main { flex: 2; }
.detail-sidebar { flex: 1; }
.large-image-placeholder { background: #2a2a2a; height: 250px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 60px; }
.detail-price { font-size: 28px; font-weight: bold; color: var(--primary-accent); }
.seller-card { background: var(--bg-dark-card); border: 1px solid var(--border-color); border-radius: 8px; padding: 20px; }
.seller-avatar, .avatar-large { width: 50px; height: 50px; border-radius: 50%; background: var(--primary-accent); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; }
.avatar-large { width: 70px; height: 70px; font-size: 22px; flex-shrink: 0; }
.btn-primary, .btn-action { background: var(--primary-accent); color: white; border: none; padding: 12px; border-radius: 6px; font-weight: bold; cursor: pointer; }
.btn-action { width: 100%; margin-top: 10px; }
.comparison-box { margin-top: 20px; background: var(--bg-dark-card); padding: 15px; border-radius: 8px; }
.comparison-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border-color); }
.comparison-row.active { color: var(--primary-accent); font-weight: bold; }
.profile-header-card { background: var(--bg-dark-card); border: 1px solid var(--border-color); border-radius: 8px; padding: 20px; display: flex; gap: 20px; align-items: center; }
.profile-stats { display: flex; gap: 20px; margin-top: 20px; }
.stat-box { background: var(--bg-dark-card); border: 1px solid var(--border-color); border-radius: 8px; padding: 16px; text-align: center; flex: 1; }
.stat-box .num { font-size: 28px; font-weight: bold; color: var(--primary-accent); }
.stat-box .label { font-size: 12px; color: #888; margin-top: 4px; }
.category-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; }
.chip { background: #2a2a2a; border: 1px solid var(--border-color); border-radius: 20px; padding: 6px 14px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
.chip:hover, .chip.active { background: var(--primary-accent); border-color: var(--primary-accent); color: white; }
.mobile-bottom-nav { position: fixed; bottom: 0; left: 0; right: 0; height: 60px; background: #1a1a1a; border-top: 1px solid var(--border-color); display: flex; justify-content: space-around; align-items: center; z-index: 100; }
.mobile-bottom-nav button { background: transparent; border: none; color: #888; font-size: 12px; cursor: pointer; }
.mobile-bottom-nav button.active { color: var(--primary-accent); }
.feed-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.tag { font-size: 11px; color: #aaa; }
@media (max-width: 768px) {
.filter-sidebar { display: none; }
.search-container { flex-direction: column; }
.search-container input.location-input, .search-container select { border-left: none; border-top: 1px solid var(--border-color); }
.detail-layout { flex-direction: column; }
.products-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.profile-stats { flex-wrap: wrap; }
}
</style>

26
Dilomarkt/src/api.js Normal file
View file

@ -0,0 +1,26 @@
const BASE = 'http://localhost:8000/api'
export async function fetchProducts(params = {}) {
const q = new URLSearchParams(params).toString()
const res = await fetch(`${BASE}/products?${q}`)
if (!res.ok) throw new Error('Fehler beim Laden der Produkte')
return res.json()
}
export async function fetchProduct(id) {
const res = await fetch(`${BASE}/products/${id}`)
if (!res.ok) throw new Error('Produkt nicht gefunden')
return res.json()
}
export async function fetchProviders() {
const res = await fetch(`${BASE}/providers`)
if (!res.ok) throw new Error('Fehler beim Laden der Anbieter')
return res.json()
}
export async function fetchProvider(id) {
const res = await fetch(`${BASE}/providers/${id}`)
if (!res.ok) throw new Error('Anbieter nicht gefunden')
return res.json()
}

View file

@ -0,0 +1,94 @@
<template>
<div class="view-container">
<header class="hero-section">
<h1>Lokale Bau- und Heimwerkermärkte in deiner Nähe</h1>
<p>Finde Materialien, Werkzeug und Restposten von regionalen Anbietern direkt zum Abholen.</p>
<div class="search-container">
<input v-model="q" @keyup.enter="doSearch" placeholder="z. B. Klinker, Bohrmaschine..." />
<input v-model="plz" class="location-input" placeholder="PLZ (z.B. 42103)" />
<select v-model="cat">
<option value="">Alle Kategorien</option>
<option v-for="c in CATEGORIES" :key="c">{{ c }}</option>
</select>
<button class="btn-primary" @click="doSearch">Suchen</button>
</div>
</header>
<div class="category-chips">
<span v-for="c in CATEGORIES" :key="c" class="chip" @click="goCategory(c)">{{ categoryIcon(c) }} {{ c }}</span>
</div>
<div class="marketplace-body">
<aside class="filter-sidebar">
<h3>UMKREIS</h3>
<label v-for="r in [5,10,20]" :key="r"><input type="radio" v-model="radius" :value="r" /> {{ r }} km</label>
<h3>ANBIETERTYP</h3>
<label><input type="checkbox" v-model="types" value="Fachhandel" /> Fachhandel</label>
<label><input type="checkbox" v-model="types" value="Baumarkt" /> Baumarkt</label>
</aside>
<main style="flex:1">
<div class="feed-header"><strong>{{ products.length }} Angebote</strong></div>
<div v-if="loading" style="color:#aaa;padding:20px">Lädt...</div>
<div v-if="error" style="color:#f44336;padding:20px">{{ error }}</div>
<div class="products-grid">
<div v-for="p in products" :key="p.id" class="product-card" @click="router.push(`/produkt/${p.id}`)">
<div class="card-image-placeholder"><span>{{ p.icon }}</span></div>
<div class="card-details">
<span class="price">{{ p.price }} </span>
<h4 style="margin:6px 0 4px">{{ p.title }}</h4>
<p style="margin:0;font-size:12px;color:#aaa">{{ p.provider }}</p>
<div class="card-footer">
<span>📍 {{ p.distance }} km</span>
<span class="badge-cat">{{ p.category }}</span>
<span class="badge">Abholung</span>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { fetchProducts } from '@/api.js'
const CATEGORIES = ['Baumaterial','Werkzeug','Holz & Platten','Farben & Lacke','Sanitär','Elektro']
const router = useRouter()
const q = ref('')
const plz = ref('42103')
const cat = ref('')
const radius = ref(10)
const types = ref([])
const products = ref([])
const loading = ref(false)
const error = ref('')
async function load() {
loading.value = true
error.value = ''
try {
products.value = await fetchProducts({ plz: plz.value, radius: radius.value, q: q.value, category: cat.value })
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
watch([radius, cat], load)
onMounted(load)
function doSearch() {
router.push({ path: '/suche', query: { q: q.value, cat: cat.value, plz: plz.value } })
}
function goCategory(c) {
router.push({ path: '/suche', query: { cat: c } })
}
function categoryIcon(c) {
const m = { 'Baumaterial':'🧱','Werkzeug':'🔧','Holz & Platten':'🪵','Farben & Lacke':'💧','Sanitär':'🚿','Elektro':'💡' }
return m[c] ?? '📦'
}
</script>

View file

@ -0,0 +1,70 @@
<template>
<div class="view-container" v-if="product">
<button class="back-link" @click="router.back()"> Zurück</button>
<div class="detail-layout">
<div class="detail-main">
<div class="large-image-placeholder"><span>{{ product.icon }}</span></div>
<div style="display:flex;gap:8px;align-items:center;margin-top:12px">
<span class="badge-cat">{{ product.category }}</span>
<span style="font-size:13px;color:#888">Lagerbestand: {{ product.stock }} Stück</span>
</div>
<h2 style="margin:10px 0 4px">{{ product.title }}</h2>
<p class="detail-price">{{ product.price }} </p>
<p style="color:#aaa;line-height:1.6">{{ product.description }}</p>
<div class="comparison-box">
<h3 style="margin-top:0">Preisvergleich</h3>
<div class="comparison-row active">
<span>{{ product.provider }}</span><strong>{{ product.price }} </strong>
</div>
<div class="comparison-row" v-for="a in alternatives" :key="a.id">
<span style="cursor:pointer;color:var(--primary-accent)" @click="router.push(`/produkt/${a.id}`)">{{ a.provider }}</span>
<strong>{{ a.price }} </strong>
</div>
<div v-if="!alternatives.length" style="color:#666;font-size:13px;padding-top:8px">Keine weiteren Angebote.</div>
</div>
</div>
<div class="detail-sidebar">
<div class="seller-card" v-if="product">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
<div class="seller-avatar">{{ product.initials }}</div>
<div>
<h3 style="margin:0;cursor:pointer;color:var(--primary-accent)" @click="router.push(`/anbieter/${product.provider_id}`)">{{ product.provider }}</h3>
<p style="margin:4px 0 0;font-size:13px;color:#aaa">{{ product.provider_type }}</p>
</div>
</div>
<p style="font-size:13px;color:#aaa">📦 {{ product.address }}, {{ product.city }}</p>
<div v-if="product.verified" style="font-size:12px;color:#4caf50;margin:8px 0"> Verifizierter Anbieter</div>
<button class="btn-action" @click="alert('Anfrage gesendet!')"> Abholung anfragen</button>
<button class="btn-action" style="background:#333;margin-top:8px" @click="router.push(`/anbieter/${product.provider_id}`)">Alle Angebote ansehen</button>
</div>
</div>
</div>
</div>
<div v-else-if="loading" class="view-container" style="color:#aaa">Lädt...</div>
<div v-else class="view-container" style="color:#f44336">{{ error || 'Produkt nicht gefunden.' }}</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchProduct } from '@/api.js'
const route = useRoute()
const router = useRouter()
const product = ref(null)
const alternatives = ref([])
const loading = ref(true)
const error = ref('')
onMounted(async () => {
try {
const data = await fetchProduct(route.params.id)
product.value = data.product
alternatives.value = data.alternatives
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>

View file

@ -0,0 +1,71 @@
<template>
<div class="view-container" v-if="provider">
<button class="back-link" @click="router.back()"> Zurück</button>
<div class="profile-header-card">
<div class="avatar-large">{{ provider.initials }}</div>
<div>
<h2 style="margin:0">{{ provider.name }}</h2>
<p style="margin:4px 0;color:#aaa;font-size:14px">{{ provider.address }}, {{ provider.zip }} {{ provider.city }}</p>
<p style="margin:0;font-size:13px;color:#888">{{ provider.type }} · seit {{ provider.since }}</p>
<span v-if="provider.verified" style="font-size:12px;color:#4caf50"> Verifiziert</span>
</div>
</div>
<div class="profile-stats">
<div class="stat-box"><div class="num">{{ stats.product_count }}</div><div class="label">Aktive Angebote</div></div>
<div class="stat-box"><div class="num">{{ stats.total_stock }}</div><div class="label">Einheiten gesamt</div></div>
<div class="stat-box"><div class="num">{{ stats.min_price }} </div><div class="label">Ab Preis</div></div>
</div>
<h3 style="margin-top:28px">Kategorien</h3>
<div class="category-chips">
<span :class="['chip', activeCat==='' && 'active']" @click="activeCat=''">Alle</span>
<span v-for="c in categories" :key="c" :class="['chip', activeCat===c && 'active']" @click="activeCat=c">{{ c }}</span>
</div>
<h3>Angebote</h3>
<div class="products-grid">
<div v-for="p in filteredProducts" :key="p.id" class="product-card" @click="router.push(`/produkt/${p.id}`)">
<div class="card-image-placeholder"><span>{{ p.icon }}</span></div>
<div class="card-details">
<span class="price">{{ p.price }} </span>
<h4 style="margin:6px 0 4px">{{ p.title }}</h4>
<div class="card-footer">
<span class="badge-cat">{{ p.category }}</span>
<span style="font-size:12px;color:#888">{{ p.stock }} Stk.</span>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="loading" class="view-container" style="color:#aaa">Lädt...</div>
<div v-else class="view-container" style="color:#f44336">{{ error }}</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchProvider } from '@/api.js'
const route = useRoute()
const router = useRouter()
const provider = ref(null)
const products = ref([])
const stats = ref({})
const activeCat = ref('')
const loading = ref(true)
const error = ref('')
const categories = computed(() => [...new Set(products.value.map(p => p.category))])
const filteredProducts = computed(() => activeCat.value ? products.value.filter(p => p.category === activeCat.value) : products.value)
onMounted(async () => {
try {
const data = await fetchProvider(route.params.id)
provider.value = data.provider
products.value = data.products
stats.value = data.stats
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>

View file

@ -0,0 +1,98 @@
<template>
<div class="view-container">
<div class="search-container" style="max-width:100%;margin-bottom:16px">
<input v-model="q" @keyup.enter="load" placeholder="Suchen..." />
<select v-model="cat" @change="load">
<option value="">Alle Kategorien</option>
<option v-for="c in CATEGORIES" :key="c">{{ c }}</option>
</select>
<input v-model="plz" class="location-input" placeholder="PLZ" @change="load" />
<button class="btn-primary" @click="load">Suchen</button>
</div>
<div class="category-chips">
<span :class="['chip', !cat && 'active']" @click="cat='';load()">Alle</span>
<span v-for="c in CATEGORIES" :key="c" :class="['chip', cat===c && 'active']" @click="cat=c;load()">{{ c }}</span>
</div>
<div class="marketplace-body">
<aside class="filter-sidebar">
<h3>UMKREIS</h3>
<label v-for="r in [5,10,20,50]" :key="r"><input type="radio" v-model="radius" :value="r" @change="load" /> {{ r }} km</label>
<h3>ANBIETERTYP</h3>
<label><input type="checkbox" v-model="types" value="Fachhandel" @change="load" /> Fachhandel</label>
<label><input type="checkbox" v-model="types" value="Baumarkt" @change="load" /> Baumarkt</label>
<h3>PREIS BIS</h3>
<input type="range" v-model="maxPrice" min="1" max="500" style="width:100%" @change="load" />
<span style="font-size:13px">max. {{ maxPrice }} </span>
</aside>
<main style="flex:1">
<div class="feed-header">
<strong>{{ products.length }} Ergebnisse</strong>
<span class="tag" v-if="q"> für {{ q }}"</span>
<span class="tag"> · {{ radius }} km um {{ plz }}</span>
</div>
<div v-if="loading" style="color:#aaa;padding:20px">Lädt...</div>
<div v-if="error" style="color:#f44336;padding:20px">{{ error }}</div>
<div v-if="!loading && products.length === 0" style="color:#888;margin-top:40px;text-align:center">Keine Ergebnisse gefunden.</div>
<div class="products-grid">
<div v-for="p in products" :key="p.id" class="product-card" @click="router.push(`/produkt/${p.id}`)">
<div class="card-image-placeholder"><span>{{ p.icon }}</span></div>
<div class="card-details">
<span class="price">{{ p.price }} </span>
<h4 style="margin:6px 0 4px">{{ p.title }}</h4>
<p style="margin:0;font-size:12px;color:#aaa">{{ p.provider }}</p>
<div class="card-footer">
<span>📍 {{ p.distance }} km</span>
<span class="badge-cat">{{ p.category }}</span>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchProducts } from '@/api.js'
const CATEGORIES = ['Baumaterial','Werkzeug','Holz & Platten','Farben & Lacke','Sanitär','Elektro']
const route = useRoute()
const router = useRouter()
const q = ref(route.query.q ?? '')
const cat = ref(route.query.cat ?? '')
const plz = ref(route.query.plz ?? '42103')
const radius = ref(20)
const maxPrice = ref(500)
const types = ref([])
const products = ref([])
const loading = ref(false)
const error = ref('')
async function load() {
loading.value = true
error.value = ''
try {
const params = { plz: plz.value, radius: radius.value, q: q.value, category: cat.value, max_price: maxPrice.value }
if (types.value.length === 1) params.type = types.value[0]
products.value = await fetchProducts(params)
router.replace({ query: { q: q.value, cat: cat.value, plz: plz.value } })
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
watch(() => route.query, q => {
q.value = route.query.q ?? ''
cat.value = route.query.cat ?? ''
load()
})
onMounted(load)
</script>

71
README.md Normal file
View file

@ -0,0 +1,71 @@
# Dilomarkt Digitaler Lokalmarkt
Dilomarkt ist ein spezialisierter Marktplatz, der lokale Anbieter von Baumaterialien und Werkzeugen mit regionalen Nachfragern verbindet. Entwickelt von Studenten, adressiert die Plattform die Ineffizienzen großer, unspezialisierter Portale und stärkt lokale Wertschöpfungsketten.
## Tech Stack
- **Server:** PHP mit Laravel-Framework
- **Datenbank:** SQLite
- **Client:** Vue.js (HTML, CSS, JavaScript)
- **Client-Server-Kommunikation:** REST API
- **Weiteres:** Responsive Webdesign
## Installation
```bash
git clone https://github.com/serialexperiments0815/Dilomarkt---Digitaler-Lokalmarkt/
```
### Backend (Laravel)
```bash
cd Dilomarkt---Digitaler-Lokalmarkt/Dilomarkt/dilomarkt-api
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan db:seed
```
### Frontend (Vue)
```bash
cd Dilomarkt---Digitaler-Lokalmarkt/Dilomarkt
npm install
```
## Verwendung
**Terminal 1 Backend starten:**
```bash
cd dilomarkt-api
php artisan serve
```
API läuft auf `http://localhost:8000`
**Terminal 2 Frontend starten:**
```bash
cd Dilomarkt
npm run dev
```
App läuft auf `http://localhost:5173`
## API Endpunkte
| Method | Endpunkt | Beschreibung |
|--------|----------|--------------|
| GET | `/api/products` | Produkte mit Geo-Filter (PLZ, Radius, Kategorie) |
| GET | `/api/products/{id}` | Einzelprodukt mit Preisvergleich |
| GET | `/api/providers` | Alle Anbieter |
| GET | `/api/providers/{id}` | Anbieterprofil mit Statistiken |
### Query Parameter für `/api/products`
| Parameter | Beispiel | Beschreibung |
|-----------|----------|--------------|
| `plz` | `42103` | Standort (NRW) |
| `radius` | `20` | Umkreis in km |
| `q` | `Klinker` | Suchbegriff |
| `category` | `Werkzeug` | Kategorie |
| `max_price` | `100` | Maximaler Preis |
| `type` | `Fachhandel` | Anbietertyp |