diff --git a/AGENTS.md b/AGENTS.md
index 7df2d25..9310030 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,7 +6,7 @@ Dieses Dokument hilft AI-Assistenten (Amp, Copilot, etc.) das Projekt zu versteh
**dgray.io** ist eine Kleinanzeigen-PWA mit Monero-Bezahlung.
-- **Status**: Early Development (Frontend-only)
+- **Status**: Active Development (Frontend + Directus Backend)
- **Ziel**: Anonyme, dezentrale Marktplatz-Alternative
## Tech-Stack
@@ -62,6 +62,7 @@ js/
docs/
├── DIRECTUS-SETUP.md # Directus Backend Setup
+├── DIRECTUS-SCHEMA.md # Collection-Strukturen & Permissions
└── MONETIZATION.md # Monetarisierung & Anti-Abuse
css/
@@ -114,22 +115,27 @@ locales/
## Nächste Schritte
-1. Seiten für Profil-Dropdown: `page-my-listings.js`, `page-messages.js`, `page-favorites.js`, `page-settings.js`
-2. Payment-Integration mit BTCpay Server (https://pay.xmr.rocks/)
-3. Reputation-System (5/15/50 Deals Stufen)
-4. Suchseite (`page-search.js`) mit Filtern ausbauen
+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
## Directus Berechtigungen (Public-Rolle)
-| Collection | Read | Create | Hinweise |
-|------------|------|--------|----------|
-| `listings` | ✓ | ✓ | Nur `status=published` lesen |
-| `listings_files` | ✓ | ✓ | Junction-Table für Bilder |
-| `directus_files` | ✓ | ✓ | Für Assets/Bilder |
-| `categories` | ✓ | - | Nur `status=published` |
-| `categories_translations` | ✓ | - | Für i18n |
-| `locations` | ✓ | - | Für Standort-Auswahl |
-| `languages` | ✓ | - | Für Sprachen-Liste |
+| Collection | Read | Create | Update | Hinweise |
+|------------|------|--------|--------|----------|
+| `listings` | ✓ | ✓ | - | Nur `status=published` lesen |
+| `listings_files` | ✓ | ✓ | - | Junction-Table für Bilder |
+| `directus_files` | ✓ | ✓ | - | Für Assets/Bilder |
+| `categories` | ✓ | - | - | Nur `status=published` |
+| `categories_translations` | ✓ | - | - | Für i18n |
+| `locations` | ✓ | ✓ | - | User kann neue Orte anlegen |
+| `languages` | ✓ | - | - | Für Sprachen-Liste |
+| `conversations` | ✓ | ✓ | ✓ | Filter via `participant_hash`, Update nur `status` |
+| `messages` | ✓ | ✓ | - | Filter via `conversation` ID |
+
+Siehe `docs/DIRECTUS-SCHEMA.md` für vollständiges Schema.
## Farbpalette
diff --git a/README.md b/README.md
index 63822d6..5881aac 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ dgray.io ermöglicht es Nutzern, Kleinanzeigen zu schalten und Waren/Dienstleist
| Light/Dark Mode | Niedrig | ✅ Fertig |
| i18n (DE/EN/FR) | Niedrig | ✅ Fertig |
| Bildergalerie | Niedrig | ✅ Fertig |
-| E2E-Chat | Hoch | 🔲 Vorbereitet |
+| E2E-Chat (NaCl) | Hoch | ✅ Service fertig |
| **Monero MultiSig** | **Sehr hoch** | 🔲 Offen |
| Rating-System | Mittel | 🔲 Offen |
| 2FA | Mittel | 🔲 Offen |
@@ -148,7 +148,9 @@ dgray/
- [x] Such-Komponente mit Accordion-Kategorien
- [x] Anzeige-Detailseite mit Bildergalerie
- [x] Anzeige-Erstellen-Formular
-- [ ] Suchseite mit Filtern
+- [x] Suchseite mit Filtern (merged in Home)
+- [x] Skeleton Loading, Error Boundary, Offline Indicator
+- [x] Lazy Loading für Bilder
- [ ] Responsive Optimierungen
### Phase 2: Backend-Integration ⬅️ **Aktuell**
@@ -158,16 +160,18 @@ dgray/
- [x] User-Auth (UUID + SHA-256 Hash, anonym)
- [x] Bilder-Upload (Junction-Table)
- [x] API-Services (`directus.js`, `listings.js`, `categories.js`, `locations.js`)
-- [ ] Directus Public-Berechtigungen vervollständigen
+- [x] Directus Public-Berechtigungen (siehe `docs/DIRECTUS-SCHEMA.md`)
+- [x] Neue Seiten: Favoriten, Meine Anzeigen, Nachrichten, Einstellungen
### Phase 3: Kommunikation
-- [ ] Chat-System (E2E-verschlüsselt)
-- [ ] Benachrichtigungen
-- [ ] Merkliste
+- [x] Chat-System (E2E-verschlüsselt mit NaCl)
+- [x] Conversations/Messages Services
+- [x] Merkliste (Favoriten-Seite)
+- [ ] Benachrichtigungen (Push)
### Phase 4: Payments
-- [ ] XMR-Kursabfrage API
-- [ ] Fiat ↔ XMR Umrechnung
+- [x] XMR-Kursabfrage API (CoinGecko)
+- [x] Fiat ↔ XMR Umrechnung (Dual-Preis-Anzeige)
- [ ] Wallet-Anbindung (monero-wallet-rpc)
- [ ] MultiSig Escrow
diff --git a/docs/DIRECTUS-SCHEMA.md b/docs/DIRECTUS-SCHEMA.md
index 19cb73a..05f4c98 100644
--- a/docs/DIRECTUS-SCHEMA.md
+++ b/docs/DIRECTUS-SCHEMA.md
@@ -124,31 +124,40 @@ Verfügbare Sprachen.
## conversations
-Chat-Konversationen zwischen Usern.
+Zero-Knowledge Chat-Konversationen zwischen anonymen Usern.
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | UUID | Primary Key |
-| `listing` | UUID | FK → listings |
-| `buyer` | UUID | FK → directus_users (Interessent) |
-| `seller` | UUID | FK → directus_users (Verkäufer) |
+| `listing_id` | UUID | FK → listings |
+| `participant_hash_1` | string(64) | SHA-256 Hash des ersten Teilnehmers (Käufer) |
+| `participant_hash_2` | string(64) | SHA-256 Hash des zweiten Teilnehmers (Verkäufer) |
+| `public_key_1` | text | NaCl Public Key Teilnehmer 1 |
+| `public_key_2` | text | NaCl Public Key Teilnehmer 2 |
+| `status` | string | `active`, `closed` |
| `date_created` | datetime | Erstellungsdatum |
| `date_updated` | datetime | Letzte Nachricht |
+**Hinweis:** Die `participant_hash_*` Felder ermöglichen anonyme Zuordnung ohne Directus-User-Accounts.
+
---
## messages
-Nachrichten in Konversationen.
+E2E-verschlüsselte Nachrichten in Konversationen.
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | UUID | Primary Key |
| `conversation` | UUID | FK → conversations |
-| `sender` | UUID | FK → directus_users |
-| `content` | text | Verschlüsselter Inhalt |
+| `sender_hash` | string(64) | SHA-256 Hash des Senders |
+| `content_encrypted` | text | NaCl-verschlüsselter Inhalt |
+| `nonce` | string | Nonce für Entschlüsselung |
+| `type` | string | `text`, `image`, `system` |
| `date_created` | datetime | Zeitstempel |
+**Hinweis:** Nachrichten sind client-seitig E2E-verschlüsselt. Server sieht nur Ciphertext.
+
---
## favorites
@@ -191,7 +200,7 @@ Meldungen von Anzeigen.
| `categories_translations` | ✓ | - | - | - | Für i18n |
| `locations` | ✓ | ✓ | - | - | User kann neue Orte anlegen |
| `languages` | ✓ | - | - | - | Für Sprach-Auswahl |
-| `conversations` | ✓ | ✓ | - | - | Nur eigene |
-| `messages` | ✓ | ✓ | - | - | Nur in eigenen Konversationen |
+| `conversations` | ✓ | ✓ | ✓ | - | Filter via API-Query mit `participant_hash`, Update nur `status` |
+| `messages` | ✓ | ✓ | - | - | Filter via `conversation` ID |
| `favorites` | ✓ | ✓ | - | ✓ | Nur eigene |
| `reports` | - | ✓ | - | - | Nur erstellen |
diff --git a/js/components/app-shell.js b/js/components/app-shell.js
index c47c87d..b729244 100644
--- a/js/components/app-shell.js
+++ b/js/components/app-shell.js
@@ -7,6 +7,10 @@ import './auth-modal.js'
import './pages/page-home.js'
import './pages/page-listing.js'
import './pages/page-create.js'
+import './pages/page-favorites.js'
+import './pages/page-my-listings.js'
+import './pages/page-messages.js'
+import './pages/page-settings.js'
import './pages/page-not-found.js'
class AppShell extends HTMLElement {
@@ -47,6 +51,10 @@ class AppShell extends HTMLElement {
.register('/search', 'page-home') // Redirect search to home
.register('/listing/:id', 'page-listing')
.register('/create', 'page-create')
+ .register('/favorites', 'page-favorites')
+ .register('/my-listings', 'page-my-listings')
+ .register('/messages', 'page-messages')
+ .register('/settings', 'page-settings')
router.handleRouteChange()
}
diff --git a/js/components/pages/page-favorites.js b/js/components/pages/page-favorites.js
new file mode 100644
index 0000000..643b7e8
--- /dev/null
+++ b/js/components/pages/page-favorites.js
@@ -0,0 +1,171 @@
+import { t, i18n } from '../../i18n.js'
+import { directus } from '../../services/directus.js'
+import '../listing-card.js'
+import '../skeleton-card.js'
+
+class PageFavorites extends HTMLElement {
+ constructor() {
+ super()
+ this.listings = []
+ this.loading = true
+ this.error = null
+ }
+
+ connectedCallback() {
+ this.render()
+ this.loadFavorites()
+ this.unsubscribe = i18n.subscribe(() => this.render())
+ }
+
+ disconnectedCallback() {
+ if (this.unsubscribe) this.unsubscribe()
+ }
+
+ getFavoriteIds() {
+ return JSON.parse(localStorage.getItem('favorites') || '[]')
+ }
+
+ async loadFavorites() {
+ const ids = this.getFavoriteIds()
+
+ if (ids.length === 0) {
+ this.loading = false
+ this.listings = []
+ this.updateContent()
+ return
+ }
+
+ try {
+ const response = await directus.getListings({
+ filter: {
+ id: { _in: ids },
+ status: { _eq: 'published' }
+ },
+ limit: 50
+ })
+ this.listings = response.items || []
+ this.loading = false
+ this.updateContent()
+ } catch (err) {
+ console.error('Failed to load favorites:', err)
+ this.error = err.message
+ this.loading = false
+ this.updateContent()
+ }
+ }
+
+ render() {
+ this.innerHTML = /* html */`
+
+
+
+
+ ${this.renderContent()}
+
+
+ `
+ }
+
+ updateContent() {
+ const container = this.querySelector('#favorites-content')
+ if (container) {
+ container.innerHTML = this.renderContent()
+ }
+ }
+
+ renderContent() {
+ if (this.loading) {
+ return Array(4).fill(0).map(() => '').join('')
+ }
+
+ if (this.error) {
+ return /* html */`
+
+
⚠️
+
${t('common.error')}
+
+ `
+ }
+
+ if (this.listings.length === 0) {
+ return /* html */`
+
+ `
+ }
+
+ return this.listings.map(listing => {
+ const imageId = listing.images?.[0]?.directus_files_id?.id || listing.images?.[0]?.directus_files_id
+ const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 300) : ''
+ const locationName = listing.location?.name || ''
+
+ return /* html */`
+
+ `
+ }).join('')
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div')
+ div.textContent = text
+ return div.innerHTML
+ }
+}
+
+customElements.define('page-favorites', PageFavorites)
+
+const style = document.createElement('style')
+style.textContent = /* css */`
+ page-favorites .favorites-page {
+ padding: var(--space-lg) 0;
+ }
+
+ page-favorites .page-header {
+ margin-bottom: var(--space-xl);
+ }
+
+ page-favorites .page-header h1 {
+ margin: 0 0 var(--space-xs);
+ }
+
+ page-favorites .page-subtitle {
+ color: var(--color-text-muted);
+ margin: 0;
+ }
+
+ page-favorites .empty-state {
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: var(--space-3xl);
+ }
+
+ page-favorites .empty-icon {
+ font-size: 4rem;
+ margin-bottom: var(--space-md);
+ filter: grayscale(1);
+ }
+
+ page-favorites .empty-state h3 {
+ margin: 0 0 var(--space-sm);
+ }
+
+ page-favorites .empty-state p {
+ color: var(--color-text-muted);
+ margin: 0 0 var(--space-lg);
+ }
+`
+document.head.appendChild(style)
diff --git a/js/components/pages/page-messages.js b/js/components/pages/page-messages.js
new file mode 100644
index 0000000..4f92ef8
--- /dev/null
+++ b/js/components/pages/page-messages.js
@@ -0,0 +1,333 @@
+import { t, i18n } from '../../i18n.js'
+import { auth } from '../../services/auth.js'
+import { directus } from '../../services/directus.js'
+
+class PageMessages extends HTMLElement {
+ constructor() {
+ super()
+ this.conversations = []
+ this.loading = true
+ this.error = null
+ this.isLoggedIn = false
+ }
+
+ connectedCallback() {
+ this.isLoggedIn = auth.isLoggedIn()
+ this.render()
+
+ if (this.isLoggedIn) {
+ this.loadConversations()
+ } else {
+ this.loading = false
+ }
+
+ this.unsubscribe = i18n.subscribe(() => this.render())
+ this.authUnsubscribe = auth.subscribe(() => {
+ const wasLoggedIn = this.isLoggedIn
+ this.isLoggedIn = auth.isLoggedIn()
+
+ if (!wasLoggedIn && this.isLoggedIn) {
+ this.loading = true
+ this.render()
+ this.loadConversations()
+ } else if (wasLoggedIn && !this.isLoggedIn) {
+ this.conversations = []
+ this.loading = false
+ this.render()
+ }
+ })
+ }
+
+ disconnectedCallback() {
+ if (this.unsubscribe) this.unsubscribe()
+ if (this.authUnsubscribe) this.authUnsubscribe()
+ }
+
+ async loadConversations() {
+ try {
+ const user = auth.getUser()
+ if (!user) {
+ this.loading = false
+ this.updateContent()
+ return
+ }
+
+ // Load conversations for current user (matched by participant hash)
+ const userHash = await auth.hashString(user.id)
+
+ const response = await directus.get('/items/conversations', {
+ fields: [
+ 'id',
+ 'date_created',
+ 'date_updated',
+ 'listing_id.id',
+ 'listing_id.title',
+ 'listing_id.images.directus_files_id.id',
+ 'status'
+ ],
+ filter: {
+ _or: [
+ { participant_hash_1: { _eq: userHash } },
+ { participant_hash_2: { _eq: userHash } }
+ ]
+ },
+ sort: ['-date_updated'],
+ limit: 50
+ })
+
+ this.conversations = response.data || []
+ this.loading = false
+ this.updateContent()
+ } catch (err) {
+ console.error('Failed to load conversations:', err)
+ this.error = err.message
+ this.loading = false
+ this.updateContent()
+ }
+ }
+
+ showAuthModal() {
+ document.querySelector('auth-modal')?.show()
+ }
+
+ render() {
+ this.innerHTML = /* html */`
+
+
+
+
+ ${this.renderContent()}
+
+
+ `
+
+ this.querySelector('#login-btn')?.addEventListener('click', () => this.showAuthModal())
+ }
+
+ updateContent() {
+ const container = this.querySelector('#messages-content')
+ if (container) {
+ container.innerHTML = this.renderContent()
+ this.querySelector('#login-btn')?.addEventListener('click', () => this.showAuthModal())
+ }
+ }
+
+ renderContent() {
+ if (!this.isLoggedIn) {
+ return /* html */`
+
+
🔐
+
${t('messages.loginRequired')}
+
${t('messages.loginHint')}
+
+
+ `
+ }
+
+ if (this.loading) {
+ return /* html */`
+
+
+
${t('common.loading')}
+
+ `
+ }
+
+ if (this.error) {
+ return /* html */`
+
+
⚠️
+
${t('common.error')}
+
+ `
+ }
+
+ if (this.conversations.length === 0) {
+ return /* html */`
+
+ `
+ }
+
+ return this.conversations.map(conv => {
+ const listing = conv.listing_id
+ const imageId = listing?.images?.[0]?.directus_files_id?.id
+ const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 80) : ''
+ const title = listing?.title || t('messages.unknownListing')
+ const dateStr = this.formatDate(conv.date_updated || conv.date_created)
+
+ return /* html */`
+
+
+ ${imageUrl
+ ? `

`
+ : `
📦
`}
+
+
+
${this.escapeHtml(title)}
+
${dateStr}
+
+ →
+
+ `
+ }).join('')
+ }
+
+ formatDate(dateStr) {
+ if (!dateStr) return ''
+ const date = new Date(dateStr)
+ const now = new Date()
+ const diffMs = now - date
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
+
+ if (diffDays === 0) return t('messages.today')
+ if (diffDays === 1) return t('messages.yesterday')
+ if (diffDays < 7) return t('messages.daysAgo', { days: diffDays })
+
+ return date.toLocaleDateString()
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div')
+ div.textContent = text
+ return div.innerHTML
+ }
+}
+
+customElements.define('page-messages', PageMessages)
+
+const style = document.createElement('style')
+style.textContent = /* css */`
+ page-messages .messages-page {
+ padding: var(--space-lg) 0;
+ }
+
+ page-messages .page-header {
+ margin-bottom: var(--space-xl);
+ }
+
+ page-messages .page-header h1 {
+ margin: 0 0 var(--space-xs);
+ }
+
+ page-messages .page-subtitle {
+ color: var(--color-text-muted);
+ margin: 0;
+ }
+
+ page-messages .conversations-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ }
+
+ page-messages .conversation-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ padding: var(--space-md);
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ text-decoration: none;
+ color: inherit;
+ transition: all var(--transition-fast);
+ }
+
+ page-messages .conversation-item:hover {
+ border-color: var(--color-primary);
+ box-shadow: var(--shadow-sm);
+ }
+
+ page-messages .conversation-image {
+ width: 60px;
+ height: 60px;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ background: var(--color-bg-tertiary);
+ flex-shrink: 0;
+ }
+
+ page-messages .conversation-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ page-messages .image-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.5rem;
+ filter: grayscale(1);
+ }
+
+ page-messages .conversation-info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ page-messages .conversation-title {
+ margin: 0 0 var(--space-xs);
+ font-size: var(--font-size-base);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ page-messages .conversation-date {
+ margin: 0;
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ }
+
+ page-messages .conversation-arrow {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-lg);
+ }
+
+ page-messages .loading-state,
+ page-messages .empty-state {
+ text-align: center;
+ padding: var(--space-3xl);
+ }
+
+ page-messages .spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--color-border);
+ border-top-color: var(--color-primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto var(--space-md);
+ }
+
+ @keyframes spin {
+ to { transform: rotate(360deg); }
+ }
+
+ page-messages .empty-icon {
+ font-size: 4rem;
+ margin-bottom: var(--space-md);
+ filter: grayscale(1);
+ }
+
+ page-messages .empty-state h3 {
+ margin: 0 0 var(--space-sm);
+ }
+
+ page-messages .empty-state p {
+ color: var(--color-text-muted);
+ margin: 0 0 var(--space-lg);
+ }
+`
+document.head.appendChild(style)
diff --git a/js/components/pages/page-my-listings.js b/js/components/pages/page-my-listings.js
new file mode 100644
index 0000000..788a391
--- /dev/null
+++ b/js/components/pages/page-my-listings.js
@@ -0,0 +1,239 @@
+import { t, i18n } from '../../i18n.js'
+import { directus } from '../../services/directus.js'
+import { auth } from '../../services/auth.js'
+import '../listing-card.js'
+import '../skeleton-card.js'
+
+class PageMyListings extends HTMLElement {
+ constructor() {
+ super()
+ this.listings = []
+ this.loading = true
+ this.error = null
+ this.isLoggedIn = false
+ }
+
+ connectedCallback() {
+ this.isLoggedIn = auth.isLoggedIn()
+ this.render()
+
+ if (this.isLoggedIn) {
+ this.loadMyListings()
+ } else {
+ this.loading = false
+ }
+
+ this.unsubscribe = i18n.subscribe(() => this.render())
+ this.authUnsubscribe = auth.subscribe(() => {
+ const wasLoggedIn = this.isLoggedIn
+ this.isLoggedIn = auth.isLoggedIn()
+
+ if (!wasLoggedIn && this.isLoggedIn) {
+ this.loading = true
+ this.render()
+ this.loadMyListings()
+ } else if (wasLoggedIn && !this.isLoggedIn) {
+ this.listings = []
+ this.loading = false
+ this.render()
+ }
+ })
+ }
+
+ disconnectedCallback() {
+ if (this.unsubscribe) this.unsubscribe()
+ if (this.authUnsubscribe) this.authUnsubscribe()
+ }
+
+ async loadMyListings() {
+ try {
+ const user = auth.getUser()
+ if (!user) {
+ this.loading = false
+ this.updateContent()
+ return
+ }
+
+ const response = await directus.getListings({
+ filter: {
+ user_created: { _eq: user.id }
+ },
+ sort: ['-date_created'],
+ limit: 50
+ })
+ this.listings = response.items || []
+ this.loading = false
+ this.updateContent()
+ } catch (err) {
+ console.error('Failed to load my listings:', err)
+ this.error = err.message
+ this.loading = false
+ this.updateContent()
+ }
+ }
+
+ showAuthModal() {
+ document.querySelector('auth-modal')?.show()
+ }
+
+ render() {
+ this.innerHTML = /* html */`
+
+
+
+
+ ${this.renderContent()}
+
+
+ `
+
+ this.querySelector('#login-btn')?.addEventListener('click', () => this.showAuthModal())
+ }
+
+ updateContent() {
+ const container = this.querySelector('#my-listings-content')
+ if (container) {
+ container.innerHTML = this.renderContent()
+ this.querySelector('#login-btn')?.addEventListener('click', () => this.showAuthModal())
+ }
+ }
+
+ renderContent() {
+ if (!this.isLoggedIn) {
+ return /* html */`
+
+
🔐
+
${t('myListings.loginRequired')}
+
${t('myListings.loginHint')}
+
+
+ `
+ }
+
+ if (this.loading) {
+ return Array(4).fill(0).map(() => '').join('')
+ }
+
+ if (this.error) {
+ return /* html */`
+
+
⚠️
+
${t('common.error')}
+
+ `
+ }
+
+ if (this.listings.length === 0) {
+ return /* html */`
+
+ `
+ }
+
+ return this.listings.map(listing => {
+ const imageId = listing.images?.[0]?.directus_files_id?.id || listing.images?.[0]?.directus_files_id
+ const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 300) : ''
+ const locationName = listing.location?.name || ''
+ const statusBadge = listing.status !== 'published'
+ ? `${t(`myListings.status.${listing.status}`)}`
+ : ''
+
+ return /* html */`
+
+ ${statusBadge}
+
+
+ `
+ }).join('')
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div')
+ div.textContent = text
+ return div.innerHTML
+ }
+}
+
+customElements.define('page-my-listings', PageMyListings)
+
+const style = document.createElement('style')
+style.textContent = /* css */`
+ page-my-listings .my-listings-page {
+ padding: var(--space-lg) 0;
+ }
+
+ page-my-listings .page-header {
+ margin-bottom: var(--space-xl);
+ }
+
+ page-my-listings .page-header h1 {
+ margin: 0 0 var(--space-xs);
+ }
+
+ page-my-listings .page-subtitle {
+ color: var(--color-text-muted);
+ margin: 0;
+ }
+
+ page-my-listings .listing-wrapper {
+ position: relative;
+ }
+
+ page-my-listings .status-badge {
+ position: absolute;
+ top: var(--space-sm);
+ left: var(--space-sm);
+ padding: var(--space-xs) var(--space-sm);
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-medium);
+ border-radius: var(--radius-sm);
+ z-index: 2;
+ }
+
+ page-my-listings .status-draft {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-muted);
+ }
+
+ page-my-listings .status-archived {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-muted);
+ text-decoration: line-through;
+ }
+
+ page-my-listings .empty-state {
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: var(--space-3xl);
+ }
+
+ page-my-listings .empty-icon {
+ font-size: 4rem;
+ margin-bottom: var(--space-md);
+ filter: grayscale(1);
+ }
+
+ page-my-listings .empty-state h3 {
+ margin: 0 0 var(--space-sm);
+ }
+
+ page-my-listings .empty-state p {
+ color: var(--color-text-muted);
+ margin: 0 0 var(--space-lg);
+ }
+`
+document.head.appendChild(style)
diff --git a/js/components/pages/page-settings.js b/js/components/pages/page-settings.js
new file mode 100644
index 0000000..e8d9558
--- /dev/null
+++ b/js/components/pages/page-settings.js
@@ -0,0 +1,382 @@
+import { t, i18n } from '../../i18n.js'
+import { auth } from '../../services/auth.js'
+
+class PageSettings extends HTMLElement {
+ constructor() {
+ super()
+ this.isLoggedIn = false
+ }
+
+ connectedCallback() {
+ this.isLoggedIn = auth.isLoggedIn()
+ this.render()
+ this.setupEventListeners()
+
+ this.unsubscribe = i18n.subscribe(() => {
+ this.render()
+ this.setupEventListeners()
+ })
+
+ this.authUnsubscribe = auth.subscribe(() => {
+ this.isLoggedIn = auth.isLoggedIn()
+ this.render()
+ this.setupEventListeners()
+ })
+ }
+
+ disconnectedCallback() {
+ if (this.unsubscribe) this.unsubscribe()
+ if (this.authUnsubscribe) this.authUnsubscribe()
+ }
+
+ setupEventListeners() {
+ // Theme toggle
+ this.querySelectorAll('input[name="theme"]').forEach(input => {
+ input.addEventListener('change', (e) => {
+ this.setTheme(e.target.value)
+ })
+ })
+
+ // Language select
+ this.querySelector('#lang-select')?.addEventListener('change', (e) => {
+ i18n.setLocale(e.target.value)
+ })
+
+ // Clear favorites
+ this.querySelector('#clear-favorites')?.addEventListener('click', () => {
+ if (confirm(t('settings.confirmClearFavorites'))) {
+ localStorage.removeItem('favorites')
+ this.showToast(t('settings.favoritesCleared'))
+ }
+ })
+
+ // Clear search history
+ this.querySelector('#clear-search')?.addEventListener('click', () => {
+ if (confirm(t('settings.confirmClearSearch'))) {
+ localStorage.removeItem('searchFilters')
+ this.showToast(t('settings.searchCleared'))
+ }
+ })
+
+ // Logout
+ this.querySelector('#logout-btn')?.addEventListener('click', () => {
+ auth.logout()
+ this.showToast(t('settings.loggedOut'))
+ })
+
+ // Login
+ this.querySelector('#login-btn')?.addEventListener('click', () => {
+ document.querySelector('auth-modal')?.show()
+ })
+ }
+
+ setTheme(theme) {
+ if (theme === 'system') {
+ localStorage.removeItem('theme')
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+ document.documentElement.dataset.theme = prefersDark ? 'dark' : 'light'
+ } else {
+ localStorage.setItem('theme', theme)
+ document.documentElement.dataset.theme = theme
+ }
+ }
+
+ getCurrentTheme() {
+ return localStorage.getItem('theme') || 'system'
+ }
+
+ showToast(message) {
+ const existing = document.querySelector('.settings-toast')
+ existing?.remove()
+
+ const toast = document.createElement('div')
+ toast.className = 'settings-toast'
+ toast.textContent = message
+ document.body.appendChild(toast)
+
+ requestAnimationFrame(() => toast.classList.add('visible'))
+
+ setTimeout(() => {
+ toast.classList.remove('visible')
+ setTimeout(() => toast.remove(), 300)
+ }, 2000)
+ }
+
+ render() {
+ const currentTheme = this.getCurrentTheme()
+ const currentLang = i18n.getLocale()
+ const user = auth.getUser()
+
+ this.innerHTML = /* html */`
+
+
+
+
+
+
+ ${t('settings.appearance')}
+
+
+
+
+
+
+
+
+
+
+
+ ${t('settings.account')}
+
+ ${this.isLoggedIn ? /* html */`
+
+
+ ${user?.id?.slice(0, 8)}...
+
+
+
+
+ ` : /* html */`
+
+
${t('settings.notLoggedIn')}
+
+
+ `}
+
+
+
+
+ ${t('settings.data')}
+
+
+
+
+
${t('settings.favoritesHint')}
+
+
+
+
+
+
+
+
${t('settings.searchHistoryHint')}
+
+
+
+
+
+
+
+ ${t('settings.about')}
+
+ dgray.io v1.0.0
+
+
+
+ `
+ }
+}
+
+customElements.define('page-settings', PageSettings)
+
+const style = document.createElement('style')
+style.textContent = /* css */`
+ page-settings .settings-page {
+ padding: var(--space-lg) 0;
+ max-width: 600px;
+ }
+
+ page-settings .page-header {
+ margin-bottom: var(--space-xl);
+ }
+
+ page-settings .page-header h1 {
+ margin: 0;
+ }
+
+ page-settings .settings-sections {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xl);
+ }
+
+ page-settings .settings-section {
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+ }
+
+ page-settings .settings-section h2 {
+ font-size: var(--font-size-lg);
+ margin: 0 0 var(--space-lg);
+ padding-bottom: var(--space-sm);
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ page-settings .setting-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-md);
+ padding: var(--space-sm) 0;
+ }
+
+ page-settings .setting-item + .setting-item {
+ border-top: 1px solid var(--color-border);
+ margin-top: var(--space-sm);
+ padding-top: var(--space-md);
+ }
+
+ page-settings .setting-item > label {
+ font-weight: var(--font-weight-medium);
+ }
+
+ page-settings .setting-hint {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ margin: var(--space-xs) 0 0;
+ }
+
+ page-settings .theme-options {
+ display: flex;
+ gap: var(--space-sm);
+ }
+
+ page-settings .theme-option {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: var(--space-xs) var(--space-sm);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: var(--font-size-sm);
+ transition: all var(--transition-fast);
+ }
+
+ page-settings .theme-option:has(input:checked) {
+ border-color: var(--color-primary);
+ background: var(--color-primary-light);
+ }
+
+ page-settings .theme-option input {
+ display: none;
+ }
+
+ page-settings select {
+ padding: var(--space-xs) var(--space-sm);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-sm);
+ background: var(--color-bg);
+ color: var(--color-text);
+ font-size: var(--font-size-sm);
+ }
+
+ page-settings .user-id {
+ padding: var(--space-xs) var(--space-sm);
+ background: var(--color-bg-tertiary);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-sm);
+ }
+
+ page-settings .btn-danger {
+ color: var(--color-error, #dc2626);
+ border-color: var(--color-error, #dc2626);
+ }
+
+ page-settings .btn-danger:hover {
+ background: var(--color-error, #dc2626);
+ color: white;
+ }
+
+ page-settings .about-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-md);
+ margin-bottom: var(--space-md);
+ }
+
+ page-settings .about-links a {
+ color: var(--color-text-secondary);
+ text-decoration: none;
+ }
+
+ page-settings .about-links a:hover {
+ color: var(--color-primary);
+ }
+
+ page-settings .version {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ margin: 0;
+ }
+
+ @media (max-width: 768px) {
+ page-settings .setting-item {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ page-settings .theme-options {
+ flex-wrap: wrap;
+ }
+ }
+
+ /* Toast */
+ .settings-toast {
+ position: fixed;
+ bottom: var(--space-lg);
+ left: 50%;
+ transform: translateX(-50%) translateY(100px);
+ padding: var(--space-sm) var(--space-lg);
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ opacity: 0;
+ transition: all 0.3s ease;
+ z-index: 1000;
+ }
+
+ .settings-toast.visible {
+ transform: translateX(-50%) translateY(0);
+ opacity: 1;
+ }
+`
+document.head.appendChild(style)
diff --git a/locales/de.json b/locales/de.json
index 7d2540f..8caa4cf 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -224,5 +224,66 @@
"confirmSaved": "Ich habe meine UUID gespeichert",
"registrationFailed": "Registrierung fehlgeschlagen",
"loginRequired": "Bitte melde dich an, um fortzufahren"
+ },
+ "favorites": {
+ "title": "Favoriten",
+ "subtitle": "Deine gemerkten Anzeigen",
+ "empty": "Keine Favoriten",
+ "emptyHint": "Klicke auf das Herz-Symbol bei einer Anzeige, um sie zu speichern.",
+ "browse": "Anzeigen durchsuchen"
+ },
+ "myListings": {
+ "title": "Meine Anzeigen",
+ "subtitle": "Verwalte deine Anzeigen",
+ "empty": "Keine Anzeigen",
+ "emptyHint": "Du hast noch keine Anzeigen erstellt.",
+ "create": "Anzeige erstellen",
+ "loginRequired": "Anmeldung erforderlich",
+ "loginHint": "Melde dich an, um deine Anzeigen zu sehen.",
+ "login": "Anmelden",
+ "status": {
+ "draft": "Entwurf",
+ "archived": "Archiviert"
+ }
+ },
+ "messages": {
+ "title": "Nachrichten",
+ "subtitle": "Deine Konversationen",
+ "empty": "Keine Nachrichten",
+ "emptyHint": "Kontaktiere einen Verkäufer, um eine Konversation zu starten.",
+ "browse": "Anzeigen durchsuchen",
+ "loginRequired": "Anmeldung erforderlich",
+ "loginHint": "Melde dich an, um deine Nachrichten zu sehen.",
+ "login": "Anmelden",
+ "unknownListing": "Unbekannte Anzeige",
+ "today": "Heute",
+ "yesterday": "Gestern",
+ "daysAgo": "Vor {{days}} Tagen"
+ },
+ "settings": {
+ "title": "Einstellungen",
+ "appearance": "Darstellung",
+ "theme": "Design",
+ "themeLight": "Hell",
+ "themeDark": "Dunkel",
+ "themeSystem": "System",
+ "language": "Sprache",
+ "account": "Konto",
+ "userId": "Benutzer-ID",
+ "logout": "Abmelden",
+ "login": "Anmelden",
+ "notLoggedIn": "Du bist nicht angemeldet.",
+ "loggedOut": "Erfolgreich abgemeldet",
+ "data": "Daten",
+ "favorites": "Favoriten",
+ "favoritesHint": "Lokal gespeicherte Favoriten löschen",
+ "searchHistory": "Suchverlauf",
+ "searchHistoryHint": "Gespeicherte Suchfilter löschen",
+ "clear": "Löschen",
+ "confirmClearFavorites": "Alle Favoriten löschen?",
+ "confirmClearSearch": "Suchverlauf löschen?",
+ "favoritesCleared": "Favoriten gelöscht",
+ "searchCleared": "Suchverlauf gelöscht",
+ "about": "Über"
}
}
diff --git a/locales/en.json b/locales/en.json
index 184648d..dd422c5 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -224,5 +224,66 @@
"confirmSaved": "I have saved my UUID",
"registrationFailed": "Registration failed",
"loginRequired": "Please log in to continue"
+ },
+ "favorites": {
+ "title": "Favorites",
+ "subtitle": "Your saved listings",
+ "empty": "No favorites",
+ "emptyHint": "Click the heart icon on a listing to save it.",
+ "browse": "Browse listings"
+ },
+ "myListings": {
+ "title": "My Listings",
+ "subtitle": "Manage your listings",
+ "empty": "No listings",
+ "emptyHint": "You haven't created any listings yet.",
+ "create": "Create listing",
+ "loginRequired": "Login required",
+ "loginHint": "Log in to see your listings.",
+ "login": "Login",
+ "status": {
+ "draft": "Draft",
+ "archived": "Archived"
+ }
+ },
+ "messages": {
+ "title": "Messages",
+ "subtitle": "Your conversations",
+ "empty": "No messages",
+ "emptyHint": "Contact a seller to start a conversation.",
+ "browse": "Browse listings",
+ "loginRequired": "Login required",
+ "loginHint": "Log in to see your messages.",
+ "login": "Login",
+ "unknownListing": "Unknown listing",
+ "today": "Today",
+ "yesterday": "Yesterday",
+ "daysAgo": "{{days}} days ago"
+ },
+ "settings": {
+ "title": "Settings",
+ "appearance": "Appearance",
+ "theme": "Theme",
+ "themeLight": "Light",
+ "themeDark": "Dark",
+ "themeSystem": "System",
+ "language": "Language",
+ "account": "Account",
+ "userId": "User ID",
+ "logout": "Logout",
+ "login": "Login",
+ "notLoggedIn": "You are not logged in.",
+ "loggedOut": "Logged out successfully",
+ "data": "Data",
+ "favorites": "Favorites",
+ "favoritesHint": "Delete locally saved favorites",
+ "searchHistory": "Search history",
+ "searchHistoryHint": "Delete saved search filters",
+ "clear": "Clear",
+ "confirmClearFavorites": "Delete all favorites?",
+ "confirmClearSearch": "Delete search history?",
+ "favoritesCleared": "Favorites deleted",
+ "searchCleared": "Search history deleted",
+ "about": "About"
}
}
diff --git a/locales/fr.json b/locales/fr.json
index 137faee..dd8367d 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -224,5 +224,66 @@
"confirmSaved": "J'ai sauvegardé mon UUID",
"registrationFailed": "Échec de l'inscription",
"loginRequired": "Veuillez vous connecter pour continuer"
+ },
+ "favorites": {
+ "title": "Favoris",
+ "subtitle": "Vos annonces sauvegardées",
+ "empty": "Aucun favori",
+ "emptyHint": "Cliquez sur l'icône cœur d'une annonce pour la sauvegarder.",
+ "browse": "Parcourir les annonces"
+ },
+ "myListings": {
+ "title": "Mes annonces",
+ "subtitle": "Gérez vos annonces",
+ "empty": "Aucune annonce",
+ "emptyHint": "Vous n'avez pas encore créé d'annonce.",
+ "create": "Créer une annonce",
+ "loginRequired": "Connexion requise",
+ "loginHint": "Connectez-vous pour voir vos annonces.",
+ "login": "Connexion",
+ "status": {
+ "draft": "Brouillon",
+ "archived": "Archivé"
+ }
+ },
+ "messages": {
+ "title": "Messages",
+ "subtitle": "Vos conversations",
+ "empty": "Aucun message",
+ "emptyHint": "Contactez un vendeur pour démarrer une conversation.",
+ "browse": "Parcourir les annonces",
+ "loginRequired": "Connexion requise",
+ "loginHint": "Connectez-vous pour voir vos messages.",
+ "login": "Connexion",
+ "unknownListing": "Annonce inconnue",
+ "today": "Aujourd'hui",
+ "yesterday": "Hier",
+ "daysAgo": "Il y a {{days}} jours"
+ },
+ "settings": {
+ "title": "Paramètres",
+ "appearance": "Apparence",
+ "theme": "Thème",
+ "themeLight": "Clair",
+ "themeDark": "Sombre",
+ "themeSystem": "Système",
+ "language": "Langue",
+ "account": "Compte",
+ "userId": "ID utilisateur",
+ "logout": "Déconnexion",
+ "login": "Connexion",
+ "notLoggedIn": "Vous n'êtes pas connecté.",
+ "loggedOut": "Déconnecté avec succès",
+ "data": "Données",
+ "favorites": "Favoris",
+ "favoritesHint": "Supprimer les favoris enregistrés localement",
+ "searchHistory": "Historique de recherche",
+ "searchHistoryHint": "Supprimer les filtres de recherche enregistrés",
+ "clear": "Effacer",
+ "confirmClearFavorites": "Supprimer tous les favoris ?",
+ "confirmClearSearch": "Supprimer l'historique de recherche ?",
+ "favoritesCleared": "Favoris supprimés",
+ "searchCleared": "Historique de recherche supprimé",
+ "about": "À propos"
}
}