'use strict'; /** * Factory function for Alpine.js converter components. * @param {string} engine - 'linear', 'intermediate' or 'runtime' * @param {object} config - Engine-specific configuration * @returns {object} Alpine component data */ function createConverter(engine, config) { if (engine === 'runtime') { return { inputValue: '1', result: '', rates: {}, ratesError: '', async init() { await this.loadRates(); const params = new URLSearchParams( window.location.search); const valueParam = params.get('v'); if (valueParam) { this.inputValue = valueParam.replace(/,/g, '.'); } this.$watch('inputValue', val => { if (val && val.indexOf(',') !== -1) { this.inputValue = val.replace(/,/g, '.'); } }); this.calculate(); }, async loadRates() { const pbUrl = document.querySelector('meta[name="pocketbase-url"]')?.content || 'https://www.alphabreed.com'; try { const response = await fetch( `${pbUrl}/api/collections/currencies/records`); if (!response.ok) { this.ratesError = 'Wechselkurse konnten nicht geladen werden.'; return; } const data = await response.json(); for (const item of data.items || []) { this.rates[item.id] = item.rate; } } catch (e) { this.ratesError = 'Wechselkurse konnten nicht geladen werden.'; } }, calculate() { try { this.result = ''; if (Object.keys(this.rates).length === 0) { return; } const normalized = parseNumber( this.inputValue); if (!normalized || isNaN(normalized)) { return; } const fromCode = (config.fromCurrency || '') .toUpperCase(); const toCode = (config.toCurrency || '') .toUpperCase(); const fromRate = fromCode === 'EUR' ? 1 : this.rates[fromCode]; const toRate = toCode === 'EUR' ? 1 : this.rates[toCode]; if (!fromRate || !toRate) { return; } const value = new Decimal(normalized); const rate = new Decimal(toRate) .dividedBy(new Decimal(fromRate)); const rawResult = value.times(rate); this.result = prettyNumber(rawResult); const normalizedInputValue = this.inputValue .replace(/,/g, '.'); if (normalizedInputValue && normalizedInputValue !== '0') { history.replaceState( null, '', '?v=' + normalizedInputValue); } } catch (e) { this.result = ''; } } }; } return { inputValue: '1', result: '', ratesError: '', init() { const params = new URLSearchParams( window.location.search); const valueParam = params.get('v'); if (valueParam) { this.inputValue = valueParam.replace(/,/g, '.'); } this.$watch('inputValue', val => { if (val && val.indexOf(',') !== -1) { this.inputValue = val.replace(/,/g, '.'); } }); this.calculate(); }, calculate() { try { const normalized = parseNumber(this.inputValue); if (!normalized || isNaN(normalized)) { this.result = ''; return; } const value = new Decimal(normalized); let rawResult; if (engine === 'linear') { const fromFactor = new Decimal( config.fromFactor); const toFactor = new Decimal( config.toFactor); rawResult = value.times(fromFactor) .dividedBy(toFactor); } else if (engine === 'intermediate') { rawResult = convertTemperature( value, config.fromUnit, config.toUnit ); } else { this.result = ''; return; } this.result = prettyNumber(rawResult); const normalizedInputValue = this.inputValue.replace(/,/g, '.'); if (normalizedInputValue && normalizedInputValue !== '0') { history.replaceState( null, '', '?v=' + normalizedInputValue); } } catch (e) { this.result = ''; } } }; } /** * Alpine.js component for the conversion form. * Handles category/from/to dropdowns and swap button. * @param {object} defaults - Initial values * @returns {object} Alpine component data */ function conversionForm(defaults) { return { category: defaults.category, from: defaults.from, to: defaults.to, get actionUrl() { const search = window.location.search; return '/' + this.from + '-in-' + this.to + '/' + (search || ''); }, swapWithResult() { const resultEl = document.getElementById('result'); const resultVal = resultEl?.value || ''; if (resultVal) { history.replaceState( null, '', '?v=' + resultVal.replace(/,/g, '.')); } const temp = this.from; this.from = this.to; this.to = temp; this.$nextTick(() => { this.$refs.navLink.click(); }); }, onCategoryChange() { const cat = UNITS_DATA[this.category]; if (!cat || !cat.units) return; const units = Object.keys(cat.units); this.from = units[0]; this.to = units[1] || units[0]; this.$nextTick(() => { this.$refs.navLink.click(); }); }, init() { this.$watch('category', (val, old) => { if (val && old && val !== old) { this.onCategoryChange(); } }); } }; } /** * Temperature conversion via Celsius intermediate. * @param {Decimal} value * @param {string} fromUnit * @param {string} toUnit * @returns {number} */ function convertTemperature(value, fromUnit, toUnit) { let v = value.toNumber(); if (fromUnit === 'fahrenheit') { v = (v - 32) * 5 / 9; } else if (fromUnit === 'kelvin') { v = v - 273.15; } else if (fromUnit === 'rankine') { v = (v - 491.67) * 5 / 9; } else if (fromUnit === 'reaumur') { v = v * 5 / 4; } if (toUnit === 'fahrenheit') { v = v * 9 / 5 + 32; } else if (toUnit === 'kelvin') { v = v + 273.15; } else if (toUnit === 'rankine') { v = (v + 273.15) * 9 / 5; } else if (toUnit === 'reaumur') { v = v * 4 / 5; } return v; } /** * Formats a number as plain digits with dot as decimal separator. * @param {Decimal|string|number} num * @param {number} minPrecision - Minimum decimal places * @param {number} maxPrecision - Maximum decimal places * @returns {string} Formatted number string */ function prettyNumber(num, minPrecision, maxPrecision) { minPrecision = minPrecision || 4; maxPrecision = maxPrecision || 10; const d = new Decimal(num); const absVal = d.abs(); // Larger numbers need fewer decimal places for readable output; // smaller numbers get more to stay meaningful. let precision = maxPrecision; if (absVal.gte(1000000)) { precision = 2; } else if (absVal.gte(1000)) { precision = 4; } else if (absVal.gte(1)) { precision = 6; } else if (absVal.gte(0.001)) { precision = 8; } precision = Math.max(minPrecision, Math.min(precision, maxPrecision)); let str = d.toFixed(precision); if (str.indexOf('.') !== -1) { str = str.replace(/0+$/, ''); str = str.replace(/\.$/, ''); } return str; } /** * Parses a German number string. * Accepts both comma and dot as decimal separator. * @param {string} input * @returns {string} Normalized string with dot as decimal */ function parseNumber(input) { if (!input) return '0'; const hasComma = input.indexOf(',') !== -1; const hasDot = input.indexOf('.') !== -1; if (hasComma && hasDot) { return input.replace(/\./g, '').replace(/,/g, '.') || '0'; } return input.replace(/,/g, '.') || '0'; }