diff --git a/AGENTS.md b/AGENTS.md
index 133a432..5cb8243 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -124,9 +124,10 @@ locales/
1. ~~Seiten für Profil-Dropdown~~ ✅ Fertig (`page-my-listings.js`, `page-messages.js`, `page-favorites.js`, `page-settings.js`)
2. ~~Suchseite mit Filtern~~ ✅ Merged in `page-home.js`
-3. Payment-Integration mit BTCpay Server (https://pay.xmr.rocks/)
-4. Reputation-System (5/15/50 Deals Stufen)
-5. Push-Benachrichtigungen für neue Nachrichten
+3. ~~Listings bearbeiten~~ ✅ Edit-Modus via `#/edit/:id`
+4. Payment-Integration mit BTCpay Server (https://pay.xmr.rocks/)
+5. Reputation-System (5/15/50 Deals Stufen)
+6. Push-Benachrichtigungen für neue Nachrichten
## Directus Berechtigungen (Public-Rolle)
diff --git a/README.md b/README.md
index 5881aac..f503f65 100644
--- a/README.md
+++ b/README.md
@@ -162,6 +162,7 @@ dgray/
- [x] API-Services (`directus.js`, `listings.js`, `categories.js`, `locations.js`)
- [x] Directus Public-Berechtigungen (siehe `docs/DIRECTUS-SCHEMA.md`)
- [x] Neue Seiten: Favoriten, Meine Anzeigen, Nachrichten, Einstellungen
+- [x] Listings bearbeiten (Edit-Modus für eigene Anzeigen)
### Phase 3: Kommunikation
- [x] Chat-System (E2E-verschlüsselt mit NaCl)
diff --git a/docs/DIRECTUS-SCHEMA.md b/docs/DIRECTUS-SCHEMA.md
index 4d4a043..1481b50 100644
--- a/docs/DIRECTUS-SCHEMA.md
+++ b/docs/DIRECTUS-SCHEMA.md
@@ -193,7 +193,7 @@ Meldungen von Anzeigen.
| Collection | Read | Create | Update | Delete | Hinweise |
|------------|:----:|:------:|:------:|:------:|----------|
-| `listings` | ✓ | ✓ | ✓* | - | Nur `status=published` lesen, *Update nur `views` (via Flow) |
+| `listings` | ✓ | ✓ | ✓ | - | Siehe Details unten |
| `listings_files` | ✓ | ✓ | - | - | Für Bilder-Upload |
| `directus_files` | ✓ | ✓ | - | - | Asset-Upload |
| `categories` | ✓ | - | - | - | Nur `status=published` |
@@ -205,6 +205,27 @@ Meldungen von Anzeigen.
| `favorites` | ✓ | ✓ | - | ✓ | Nur eigene |
| `reports` | - | ✓ | - | - | Nur erstellen |
+### Listings Update-Berechtigungen (Detail)
+
+**Custom Filter:**
+```json
+{ "user_created": { "_eq": "$CURRENT_USER" } }
+```
+
+**Field Permissions (Update):**
+- `title`, `slug`, `description`
+- `price`, `currency`, `price_mode`, `price_type`
+- `category`, `condition`, `location`
+- `shipping`, `shipping_cost`
+- `monero_address`
+- `images`
+- `views` (geschützt durch Flow)
+
+**Read Filter:**
+```json
+{ "status": { "_eq": "published" } }
+```
+
---
## Directus Flows
diff --git a/js/components/app-shell.js b/js/components/app-shell.js
index b729244..3fea09e 100644
--- a/js/components/app-shell.js
+++ b/js/components/app-shell.js
@@ -51,6 +51,7 @@ class AppShell extends HTMLElement {
.register('/search', 'page-home') // Redirect search to home
.register('/listing/:id', 'page-listing')
.register('/create', 'page-create')
+ .register('/edit/:id', 'page-create')
.register('/favorites', 'page-favorites')
.register('/my-listings', 'page-my-listings')
.register('/messages', 'page-messages')
diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js
index 57c7d51..f3f43d3 100644
--- a/js/components/pages/page-create.js
+++ b/js/components/pages/page-create.js
@@ -11,7 +11,19 @@ const STORAGE_KEY = 'dgray_create_draft'
class PageCreate extends HTMLElement {
constructor() {
super()
- this.formData = this.loadDraft() || {
+ this.editMode = false
+ this.editId = null
+ this.existingImages = []
+ this.formData = this.loadDraft() || this.getEmptyFormData()
+ this.imageFiles = []
+ this.imagePreviews = []
+ this.categories = []
+ this.submitting = false
+ this.isNewAccount = true
+ }
+
+ getEmptyFormData() {
+ return {
title: '',
description: '',
price: '',
@@ -24,11 +36,6 @@ class PageCreate extends HTMLElement {
shipping: false,
moneroAddress: ''
}
- this.imageFiles = []
- this.imagePreviews = []
- this.categories = []
- this.submitting = false
- this.isNewAccount = true
}
loadDraft() {
@@ -59,13 +66,63 @@ class PageCreate extends HTMLElement {
return
}
- this.hasDraft = !!localStorage.getItem(STORAGE_KEY)
+ // Check if edit mode
+ if (this.dataset.id) {
+ this.editMode = true
+ this.editId = this.dataset.id
+ await this.loadExistingListing()
+ } else {
+ this.hasDraft = !!localStorage.getItem(STORAGE_KEY)
+ }
+
await this.loadCategories()
await this.checkAccountStatus()
this.render()
this.unsubscribe = i18n.subscribe(() => this.render())
}
+ async loadExistingListing() {
+ try {
+ const listing = await directus.getListing(this.editId)
+
+ // Verify ownership
+ const user = await auth.getUser()
+ if (listing.user_created !== user?.id) {
+ window.location.hash = '#/'
+ return
+ }
+
+ this.formData = {
+ title: listing.title || '',
+ description: listing.description || '',
+ price: listing.price?.toString() || '',
+ currency: listing.currency || 'EUR',
+ price_mode: listing.price_mode || 'fiat',
+ price_type: listing.price_type || 'fixed',
+ category: listing.category?.id || listing.category || '',
+ condition: listing.condition || 'good',
+ location: listing.location?.id || listing.location || '',
+ shipping: listing.shipping || false,
+ moneroAddress: listing.monero_address || '',
+ status: listing.status || 'published'
+ }
+
+ // Store existing images
+ this.existingImages = (listing.images || []).map(img => ({
+ id: img.id,
+ fileId: img.directus_files_id?.id || img.directus_files_id
+ }))
+
+ // Create previews for existing images
+ this.imagePreviews = this.existingImages.map(img =>
+ directus.getThumbnailUrl(img.fileId, 200)
+ )
+ } catch (e) {
+ console.error('Failed to load listing for edit:', e)
+ window.location.hash = '#/'
+ }
+ }
+
async checkAccountStatus() {
try {
// Check if user has any published listings
@@ -138,11 +195,13 @@ class PageCreate extends HTMLElement {
}
render() {
+ const pageTitle = this.editMode ? t('create.editTitle') : t('create.title')
+
this.innerHTML = /* html */`
-
${t('create.title')}
+
${pageTitle}
- ${this.hasDraft ? `
+ ${!this.editMode && this.hasDraft ? `
${t('create.draftRestored')}
@@ -289,7 +348,7 @@ class PageCreate extends HTMLElement {
${t('create.cancel')}
@@ -417,8 +476,23 @@ class PageCreate extends HTMLElement {
if (this.submitting) return
- // Validate PoW Captcha (only for new accounts)
- if (this.isNewAccount) {
+ const form = e.target
+
+ // Read current form values directly (more reliable than event listeners)
+ const formElements = {
+ title: form.querySelector('#title')?.value || '',
+ description: form.querySelector('#description')?.value || '',
+ price: form.querySelector('#price')?.value || '',
+ currency: form.querySelector('#currency')?.value || 'EUR',
+ price_mode: form.querySelector('#price_mode')?.value || 'fiat',
+ category: form.querySelector('#category')?.value || '',
+ condition: form.querySelector('#condition')?.value || 'good',
+ shipping: form.querySelector('#shipping')?.checked || false,
+ moneroAddress: form.querySelector('#moneroAddress')?.value || ''
+ }
+
+ // Validate PoW Captcha (only for new accounts and new listings)
+ if (!this.editMode && this.isNewAccount) {
const captcha = this.querySelector('#pow-captcha')
if (!captcha?.isSolved()) {
this.showError(t('captcha.error'))
@@ -427,7 +501,7 @@ class PageCreate extends HTMLElement {
}
// Validate Monero address
- if (this.formData.moneroAddress && !this.validateMoneroAddress(this.formData.moneroAddress)) {
+ if (formElements.moneroAddress && !this.validateMoneroAddress(formElements.moneroAddress)) {
this.showError(t('create.invalidMoneroAddress'))
return
}
@@ -435,35 +509,38 @@ class PageCreate extends HTMLElement {
this.submitting = true
this.clearError()
- const form = e.target
const submitBtn = form.querySelector('[type="submit"]')
submitBtn.disabled = true
- submitBtn.textContent = t('create.publishing')
+ submitBtn.textContent = this.editMode ? t('create.saving') : t('create.publishing')
try {
- // Upload images first
- let imageIds = []
+ // Upload new images first
+ let newImageIds = []
if (this.imageFiles.length > 0) {
const uploadedFiles = await directus.uploadMultipleFiles(this.imageFiles)
- imageIds = uploadedFiles.map(f => f.id)
+ newImageIds = uploadedFiles.map(f => f.id)
}
- // Create listing
+ // Build listing data from form values
const listingData = {
- title: this.formData.title,
- slug: this.generateSlug(this.formData.title),
- description: this.formData.description,
- price: String(parseFloat(this.formData.price) || 0),
- currency: this.formData.currency,
- status: 'published'
+ title: formElements.title,
+ slug: this.generateSlug(formElements.title),
+ description: formElements.description,
+ price: String(parseFloat(formElements.price) || 0),
+ currency: formElements.currency
+ }
+
+ // Only set status on create, not on edit
+ if (!this.editMode) {
+ listingData.status = 'published'
}
// Add optional fields only if set
- if (this.formData.price_mode) listingData.price_mode = this.formData.price_mode
- if (this.formData.category) listingData.category = this.formData.category
- if (this.formData.condition) listingData.condition = this.formData.condition
- if (this.formData.shipping) listingData.shipping = this.formData.shipping
- if (this.formData.moneroAddress) listingData.monero_address = this.formData.moneroAddress
+ if (formElements.price_mode) listingData.price_mode = formElements.price_mode
+ if (formElements.category) listingData.category = formElements.category
+ if (formElements.condition) listingData.condition = formElements.condition
+ listingData.shipping = formElements.shipping
+ if (formElements.moneroAddress) listingData.monero_address = formElements.moneroAddress
// Handle location - find or create in locations collection
if (this.formData.locationData) {
@@ -473,31 +550,45 @@ class PageCreate extends HTMLElement {
}
}
- // Add images in junction table format
- if (imageIds.length > 0) {
- listingData.images = {
- create: imageIds.map((id, index) => ({
- directus_files_id: id,
- sort: index
- }))
+ if (this.editMode) {
+ // Update existing listing
+ // Add new images if any
+ if (newImageIds.length > 0) {
+ listingData.images = {
+ create: newImageIds.map((id, index) => ({
+ directus_files_id: id,
+ sort: this.existingImages.length + index
+ }))
+ }
+ }
+
+ await directus.updateListing(this.editId, listingData)
+ router.navigate(`/listing/${this.editId}`)
+ } else {
+ // Create new listing
+ if (newImageIds.length > 0) {
+ listingData.images = {
+ create: newImageIds.map((id, index) => ({
+ directus_files_id: id,
+ sort: index
+ }))
+ }
+ }
+
+ const listing = await directus.createListing(listingData)
+ this.clearDraft()
+
+ if (listing?.id) {
+ router.navigate(`/listing/${listing.id}`)
+ } else {
+ router.navigate('/')
}
}
-
- const listing = await directus.createListing(listingData)
-
- this.clearDraft()
-
- if (listing?.id) {
- router.navigate(`/listing/${listing.id}`)
- } else {
- // Listing created but no ID returned - go to home
- router.navigate('/')
- }
} catch (error) {
- console.error('Failed to create listing:', error)
+ console.error('Failed to save listing:', error)
console.error('Error details:', JSON.stringify(error.data, null, 2))
submitBtn.disabled = false
- submitBtn.textContent = t('create.publish')
+ submitBtn.textContent = this.editMode ? t('create.saveChanges') : t('create.publish')
this.submitting = false
// Extract detailed error message
diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js
index 2df8c3f..d113e1e 100644
--- a/js/components/pages/page-listing.js
+++ b/js/components/pages/page-listing.js
@@ -1,5 +1,6 @@
import { t, i18n } from '../../i18n.js'
import { directus } from '../../services/directus.js'
+import { auth } from '../../services/auth.js'
import { listingsService } from '../../services/listings.js'
import { getXmrRates, formatPrice as formatCurrencyPrice } from '../../services/currency.js'
import '../chat-widget.js'
@@ -14,6 +15,7 @@ class PageListing extends HTMLElement {
this.loading = true
this.isFavorite = false
this.rates = null
+ this.isOwner = false
}
connectedCallback() {
@@ -33,10 +35,15 @@ class PageListing extends HTMLElement {
this.rates = await getXmrRates()
this.loadFavoriteState()
- // Increment view counter and update local state
- const newViews = await directus.incrementListingViews(this.listingId)
- if (newViews !== null) {
- this.listing.views = newViews
+ // Check if current user is owner
+ await this.checkOwnership()
+
+ // Increment view counter only if not owner
+ if (!this.isOwner) {
+ const newViews = await directus.incrementListingViews(this.listingId)
+ if (newViews !== null) {
+ this.listing.views = newViews
+ }
}
// Load other listings from same seller
@@ -53,6 +60,20 @@ class PageListing extends HTMLElement {
this.setupEventListeners()
}
+ async checkOwnership() {
+ if (!auth.isLoggedIn() || !this.listing?.user_created) {
+ this.isOwner = false
+ return
+ }
+
+ try {
+ const user = await auth.getUser()
+ this.isOwner = user?.id === this.listing.user_created
+ } catch (e) {
+ this.isOwner = false
+ }
+ }
+
async loadSellerListings() {
try {
const response = await directus.getListings({
@@ -234,6 +255,34 @@ class PageListing extends HTMLElement {
}
renderSidebar() {
+ // Owner view: show edit button instead of contact
+ if (this.isOwner) {
+ return /* html */`
+
+ `
+ }
+
return /* html */`