From 56cf5a63c333158e50d4b165e3a23c28ede937b1 Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Thu, 5 Feb 2026 17:02:03 +0100 Subject: [PATCH] feat: add currency setting with fiat conversion, display prices in user's preferred currency --- js/components/app-footer.js | 33 ++++++++++++++----- js/components/listing-card.js | 11 +++++++ js/components/pages/page-create.js | 9 +++-- js/components/pages/page-listing.js | 7 ++++ js/components/pages/page-settings.js | 24 ++++++++++++++ js/services/currency.js | 49 ++++++++++++++++++++++++---- locales/de.json | 4 ++- locales/en.json | 4 ++- locales/fr.json | 4 ++- 9 files changed, 125 insertions(+), 20 deletions(-) diff --git a/js/components/app-footer.js b/js/components/app-footer.js index 23561a7..f6a1f4d 100644 --- a/js/components/app-footer.js +++ b/js/components/app-footer.js @@ -4,28 +4,43 @@ import { getXmrRates, formatFiat } from '../services/currency.js' class AppFooter extends HTMLElement { constructor() { super() - this.xmrRate = null + this.rates = null + this.handleCurrencyChange = this.handleCurrencyChange.bind(this) } async connectedCallback() { this.render() - await this.loadXmrRate() + await this.loadXmrRates() + window.addEventListener('currency-changed', this.handleCurrencyChange) } - async loadXmrRate() { + disconnectedCallback() { + window.removeEventListener('currency-changed', this.handleCurrencyChange) + } + + handleCurrencyChange() { + this.updateRateDisplay() + } + + getCurrency() { + return localStorage.getItem('dgray_currency') || 'USD' + } + + async loadXmrRates() { try { - const rates = await getXmrRates() - this.xmrRate = rates.EUR + this.rates = await getXmrRates() this.updateRateDisplay() } catch (e) { - console.error('Failed to load XMR rate:', e) + console.error('Failed to load XMR rates:', e) } } updateRateDisplay() { const rateEl = this.querySelector('.xmr-rate') - if (rateEl && this.xmrRate) { - rateEl.textContent = `1 XMR ≈ ${formatFiat(this.xmrRate, 'EUR')}` + if (rateEl && this.rates) { + const currency = this.getCurrency() + const rate = this.rates[currency] || this.rates.USD + rateEl.textContent = `1 XMR ≈ ${formatFiat(rate, currency)}` rateEl.classList.add('loaded') } } @@ -51,7 +66,7 @@ class AppFooter extends HTMLElement { updateTranslations() { this.render() - if (this.xmrRate) this.updateRateDisplay() + if (this.rates) this.updateRateDisplay() } } diff --git a/js/components/listing-card.js b/js/components/listing-card.js index 9d112c8..69fe2ef 100644 --- a/js/components/listing-card.js +++ b/js/components/listing-card.js @@ -15,6 +15,7 @@ class ListingCard extends HTMLElement { this.isFavorite = false this.rates = null this.isOwner = false + this.handleCurrencyChange = this.handleCurrencyChange.bind(this) } async connectedCallback() { @@ -23,6 +24,16 @@ class ListingCard extends HTMLElement { await this.checkOwnership() this.render() this.setupEventListeners() + window.addEventListener('currency-changed', this.handleCurrencyChange) + } + + disconnectedCallback() { + window.removeEventListener('currency-changed', this.handleCurrencyChange) + } + + handleCurrencyChange() { + this.render() + this.setupEventListeners() } async checkOwnership() { diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index fcf9388..6ddf602 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -2,7 +2,7 @@ import { t, i18n } from '../../i18n.js' import { router } from '../../router.js' import { auth } from '../../services/auth.js' import { directus } from '../../services/directus.js' -import { SUPPORTED_CURRENCIES } from '../../services/currency.js' +import { SUPPORTED_CURRENCIES, getDisplayCurrency } from '../../services/currency.js' import { escapeHTML } from '../../utils/helpers.js' import '../location-picker.js' import '../pow-captcha.js' @@ -25,11 +25,16 @@ class PageCreate extends HTMLElement { } getEmptyFormData() { + // Use user's preferred currency from settings as default + const defaultCurrency = getDisplayCurrency() + // Map display currencies to listing currencies (XMR not for fiat listings) + const currency = ['EUR', 'USD', 'CHF'].includes(defaultCurrency) ? defaultCurrency : 'EUR' + return { title: '', description: '', price: '', - currency: 'EUR', + currency, price_mode: 'fiat', price_type: 'fixed', category: '', diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js index eae387a..901b6a5 100644 --- a/js/components/pages/page-listing.js +++ b/js/components/pages/page-listing.js @@ -17,6 +17,7 @@ class PageListing extends HTMLElement { this.isFavorite = false this.rates = null this.isOwner = false + this.handleCurrencyChange = this.handleCurrencyChange.bind(this) } connectedCallback() { @@ -24,10 +25,16 @@ class PageListing extends HTMLElement { this.render() this.loadListing() this.unsubscribe = i18n.subscribe(() => this.render()) + window.addEventListener('currency-changed', this.handleCurrencyChange) } disconnectedCallback() { if (this.unsubscribe) this.unsubscribe() + window.removeEventListener('currency-changed', this.handleCurrencyChange) + } + + handleCurrencyChange() { + this.render() } async loadListing() { diff --git a/js/components/pages/page-settings.js b/js/components/pages/page-settings.js index ee6952a..f4ef13d 100644 --- a/js/components/pages/page-settings.js +++ b/js/components/pages/page-settings.js @@ -50,6 +50,12 @@ class PageSettings extends HTMLElement { i18n.setLocale(e.target.value) }) + // Currency select + this.querySelector('#currency-select')?.addEventListener('change', (e) => { + this.setCurrency(e.target.value) + this.showToast(t('settings.currencyChanged')) + }) + // Clear favorites this.querySelector('#clear-favorites')?.addEventListener('click', () => { if (confirm(t('settings.confirmClearFavorites'))) { @@ -93,6 +99,15 @@ class PageSettings extends HTMLElement { return localStorage.getItem('theme') || 'system' } + getCurrentCurrency() { + return localStorage.getItem('dgray_currency') || 'USD' + } + + setCurrency(currency) { + localStorage.setItem('dgray_currency', currency) + window.dispatchEvent(new CustomEvent('currency-changed', { detail: { currency } })) + } + showToast(message) { const existing = document.querySelector('.settings-toast') existing?.remove() @@ -152,6 +167,15 @@ class PageSettings extends HTMLElement { + +
+ + +
diff --git a/js/services/currency.js b/js/services/currency.js index eb7dc2e..1236d11 100644 --- a/js/services/currency.js +++ b/js/services/currency.js @@ -165,14 +165,44 @@ export function convertFromXmr(xmrAmount, currency, rates) { return xmrAmount * rates[currency] } +/** + * Converts between fiat currencies using XMR as bridge + * @param {number} amount - Amount in source currency + * @param {string} fromCurrency - Source currency + * @param {string} toCurrency - Target currency + * @param {Object} rates - Rates from getXmrRates() (XMR -> Fiat) + * @returns {number} Amount in target currency + */ +export function convertFiat(amount, fromCurrency, toCurrency, rates) { + if (fromCurrency === toCurrency) return amount + if (!rates[fromCurrency] || !rates[toCurrency]) return amount + + // Use XMR as bridge: EUR -> XMR -> USD + // 1 XMR = rates[EUR] EUR, so 1 EUR = 1/rates[EUR] XMR + // 1 XMR = rates[USD] USD + // Therefore: 1 EUR = rates[USD] / rates[EUR] USD + return amount * (rates[toCurrency] / rates[fromCurrency]) +} + +/** + * Get user's preferred display currency from settings + * @returns {string} Currency code (default: 'USD') + */ +export function getDisplayCurrency() { + return localStorage.getItem('dgray_currency') || 'USD' +} + /** * Formats a price for display + * Shows price in user's preferred currency with XMR equivalent * @param {Object} listing - Listing with price, currency, price_mode * @param {Object} rates - Rates from getXmrRates() + * @param {string} [displayCurrency] - Override display currency * @returns {Object} { primary, secondary, xmrAmount } */ -export function formatPrice(listing, rates) { +export function formatPrice(listing, rates, displayCurrency = null) { const { price, currency, price_mode } = listing + const userCurrency = displayCurrency || getDisplayCurrency() if (!price || price === 0) { return { @@ -185,20 +215,25 @@ export function formatPrice(listing, rates) { // XMR mode: XMR is the reference price if (price_mode === 'xmr' || currency === 'XMR') { const xmrPrice = currency === 'XMR' ? price : convertToXmr(price, currency, rates) - const fiatEquivalent = currency !== 'XMR' ? price : convertFromXmr(price, 'EUR', rates) + const fiatEquivalent = convertFromXmr(xmrPrice, userCurrency, rates) return { - primary: formatXmr(xmrPrice), - secondary: currency !== 'XMR' ? `≈ ${formatFiat(price, currency)}` : `≈ ${formatFiat(fiatEquivalent, 'EUR')}`, + primary: formatFiat(fiatEquivalent, userCurrency), + secondary: `≈ ${formatXmr(xmrPrice)}`, xmrAmount: xmrPrice } } - // Fiat mode: Fiat is the reference price + // Fiat mode: Convert to user's preferred currency const xmrEquivalent = convertToXmr(price, currency, rates) + + // Always show in user's currency as primary + const displayPrice = userCurrency === currency + ? price + : convertFiat(price, currency, userCurrency, rates) return { - primary: formatFiat(price, currency), + primary: formatFiat(displayPrice, userCurrency), secondary: `≈ ${formatXmr(xmrEquivalent)}`, xmrAmount: xmrEquivalent } @@ -253,8 +288,10 @@ export const SUPPORTED_CURRENCIES = ['XMR', 'EUR', 'CHF', 'USD', 'GBP', 'JPY'] export default { getXmrRates, + getDisplayCurrency, convertToXmr, convertFromXmr, + convertFiat, formatPrice, formatXmr, formatFiat, diff --git a/locales/de.json b/locales/de.json index 4651ff7..15b397e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -305,6 +305,8 @@ "confirmClearSearch": "Suchverlauf löschen?", "favoritesCleared": "Favoriten gelöscht", "searchCleared": "Suchverlauf gelöscht", - "about": "Über" + "about": "Über", + "currency": "Währung", + "currencyChanged": "Währung geändert" } } diff --git a/locales/en.json b/locales/en.json index ce5ac31..c71cbae 100644 --- a/locales/en.json +++ b/locales/en.json @@ -305,6 +305,8 @@ "confirmClearSearch": "Delete search history?", "favoritesCleared": "Favorites deleted", "searchCleared": "Search history deleted", - "about": "About" + "about": "About", + "currency": "Currency", + "currencyChanged": "Currency changed" } } diff --git a/locales/fr.json b/locales/fr.json index dbb246f..bd2b12b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -305,6 +305,8 @@ "confirmClearSearch": "Supprimer l'historique de recherche ?", "favoritesCleared": "Favoris supprimés", "searchCleared": "Historique de recherche supprimé", - "about": "À propos" + "about": "À propos", + "currency": "Devise", + "currencyChanged": "Devise modifiée" } }