feat: add currency setting with fiat conversion, display prices in user's preferred currency

This commit is contained in:
2026-02-05 17:02:03 +01:00
parent 84493942fe
commit 56cf5a63c3
9 changed files with 125 additions and 20 deletions

View File

@@ -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()
}
}

View File

@@ -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() {

View File

@@ -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: '',

View File

@@ -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() {

View File

@@ -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 {
<option value="fr" ${currentLang === 'fr' ? 'selected' : ''}>Français</option>
</select>
</div>
<div class="setting-item">
<label for="currency-select">${t('settings.currency')}</label>
<select id="currency-select">
<option value="USD" ${this.getCurrentCurrency() === 'USD' ? 'selected' : ''}>USD ($)</option>
<option value="EUR" ${this.getCurrentCurrency() === 'EUR' ? 'selected' : ''}>EUR (€)</option>
<option value="CHF" ${this.getCurrentCurrency() === 'CHF' ? 'selected' : ''}>CHF</option>
</select>
</div>
</section>
<!-- Account -->

View File

@@ -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,