Compare commits

...

2 Commits

Author SHA1 Message Date
ec56a231e9 Runtime Engine Fix 2026-05-29 23:03:50 +02:00
04a29ae8cd Runtime Engine generisch 2026-05-29 21:39:21 +02:00
4 changed files with 164 additions and 213 deletions

View File

@@ -59,8 +59,6 @@
{{ $description := printf {{ $description := printf
"Konvertieren Sie %s in %s. Einfache und schnelle Umrechnung mit Einheitsumrechner." "Konvertieren Sie %s in %s. Einfache und schnelle Umrechnung mit Einheitsumrechner."
$fromName $toName }} $fromName $toName }}
{{ $fromCurrency := $fromCode }}
{{ $toCurrency := $toCode }}
{{ $fromRate := 1 }} {{ $fromRate := 1 }}
{{ if ne $fromCode "eur" }} {{ if ne $fromCode "eur" }}
{{ $fromRate = index $pbRates $fromCode }} {{ $fromRate = index $pbRates $fromCode }}
@@ -79,8 +77,6 @@
"slug" $slug "slug" $slug
"description" $description "description" $description
"engine" "runtime" "engine" "runtime"
"currency_from" $fromCurrency
"currency_to" $toCurrency
"rate" $rate "rate" $rate
"rates_updated" $latestUpdate "rates_updated" $latestUpdate
}} }}

View File

@@ -13,134 +13,141 @@
"{ toIntermediate: v => %s, fromIntermediate: v => %s }" "{ toIntermediate: v => %s, fromIntermediate: v => %s }"
$toExpr $fromExpr }} $toExpr $fromExpr }}
{{ else if eq .Params.engine "runtime" }} {{ else if eq .Params.engine "runtime" }}
{{ $config = dict "fromCurrency" .Params.from {{ $fromCode := .Params.from }}
"toCurrency" .Params.to | jsonify }} {{ $toCode := .Params.to }}
{{ $config = printf
"{ init: async function () { 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(); let latestUpdate = ''; for (const item of data.items || []) { this.rates[item.id] = item.rate; if (!latestUpdate || item.updated > latestUpdate) { latestUpdate = item.updated; } } const fromRate = '%s' === 'eur' ? 1 : this.rates['%s']; const toRate = '%s' === 'eur' ? 1 : this.rates['%s']; if (fromRate && toRate) { this.currentRate = parseFloat(toRate) / parseFloat(fromRate); } if (latestUpdate) { this.ratesUpdated = new Date(latestUpdate).toLocaleString('de-DE', { day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } } catch (e) { this.ratesError = 'Wechselkurse konnten nicht geladen werden.'; } }, convert: function (value) { const fromRate = '%s' === 'eur' ? 1 : this.rates['%s']; const toRate = '%s' === 'eur' ? 1 : this.rates['%s']; if (!fromRate || !toRate) { return null; } return value.times(new Decimal(toRate)).dividedBy(new Decimal(fromRate)); } }"
$fromCode $fromCode $toCode $toCode
$fromCode $fromCode $toCode $toCode }}
{{ end }} {{ end }}
{{ $availableUnits := partial "available-units.html" {{ $availableUnits := partial "available-units.html"
(dict "category" .Params.category "units" $catData.units) }} (dict "category" .Params.category "units" $catData.units) }}
<div id="conversionform" <div x-data="createConverter('{{ .Params.engine }}',
class="bg-white p-8 rounded-lg shadow-md w-full {{ $config | safeJS }})"
dark:bg-gray-700 dark:text-grey-200" x-init="init()">
x-data="navActions()">
<h1 id="headline" <div id="conversionform"
class="text-2xl font-bold mb-6 text-center"> class="bg-white p-8 rounded-lg shadow-md w-full
{{ .Params.from_name }} in {{ .Params.to_name }} dark:bg-gray-700 dark:text-grey-200"
umrechnen x-data="navActions()">
</h1>
<div> <h1 id="headline"
class="text-2xl font-bold mb-6 text-center">
<a x-ref="ajaxLink" {{ .Params.from_name }} in {{ .Params.to_name }}
x-target.push="maincontent" umrechnen
href="/" </h1>
class="hidden"></a>
<div> <div>
<label for="type"
class="block text-sm font-medium opacity-75">
Einheitentyp:
</label>
<select id="type" name="type"
@change="navigate($event.target.value)"
class="select mt-1 block w-full">
{{ range $slug, $data := hugo.Data }}
{{ if and $data.slug $data.name }}
{{ $catUrl := partial "category-url.html"
(dict "category" $data.slug
"units" $data.units) }}
<option
value="{{ $catUrl }}"
{{ if eq $data.slug
$.Params.category }}selected{{ end }}>
{{ $data.name }}
</option>
{{ end }}
{{ end }}
</select>
</div>
<div class="flex flex-col sm:flex-row items-center gap-4 <a x-ref="ajaxLink"
mt-4"> x-target.push="maincontent"
href="/"
class="hidden"></a>
<div class="flex-1 w-full"> <div>
<label for="from" <label for="type"
class="block text-sm font-medium opacity-75"> class="block text-sm font-medium opacity-75">
Umrechnen von: Einheitentyp:
</label> </label>
<select id="from" name="from" <select id="type" name="type"
@change="navigateWithValue($event.target.value)" @change="navigate($event.target.value)"
class="select mt-1 block w-full"> class="select mt-1 block w-full">
{{ range $unit, $unitData := $availableUnits }} {{ range $slug, $data := hugo.Data }}
{{ if ne $unit $.Params.to }} {{ if and $data.slug $data.name }}
{{ $catUrl := partial "category-url.html"
(dict "category" $data.slug
"units" $data.units) }}
<option <option
value="/{{ $unit }}-in-{{ $.Params.to }}/" value="{{ $catUrl }}"
{{ if eq $unit {{ if eq $data.slug
$.Params.from }}selected{{ end }}> $.Params.category }}selected{{ end }}>
{{ $unitData.name }} {{ $data.name }}
</option> </option>
{{ end }} {{ end }}
{{ end }} {{ end }}
</select> </select>
</div> </div>
<div class="text-center sm:pt-6"> <div class="flex flex-col sm:flex-row items-center gap-4
<button type="button" mt-4">
@click="navigateWithResult(
$el.dataset.swapUrl)" <div class="flex-1 w-full">
data-swap-url="/{{ .Params.to }}-in-{{ .Params.from }}/" <label for="from"
class="inline-flex items-center text-sm class="block text-sm font-medium
font-medium text-primary opacity-75">
dark:text-primary-light"> Umrechnen von:
<svg xmlns="http://www.w3.org/2000/svg" </label>
fill="none" viewBox="0 0 24 24" <select id="from" name="from"
stroke-width="1.5" @change="navigateWithValue(
stroke="currentColor" $event.target.value)"
class="w-6 h-6"> class="select mt-1 block w-full">
<path stroke-linecap="round" {{ range $unit, $unitData := $availableUnits }}
stroke-linejoin="round" {{ if ne $unit $.Params.to }}
d="M7.5 21 3 16.5m0 0L7.5 12M3 <option
16.5h13.5m0-13.5L21 7.5m0 value="/{{ $unit }}-in-{{ $.Params.to }}/"
0L16.5 12M21 7.5H7.5" /> {{ if eq $unit
</svg> $.Params.from }}selected{{ end }}>
</button> {{ $unitData.name }}
</div> </option>
{{ end }}
{{ end }}
</select>
</div>
<div class="text-center sm:pt-6">
<button type="button"
@click="navigateWithResult(
$el.dataset.swapUrl)"
data-swap-url="/{{ .Params.to }}-in-{{ .Params.from }}/"
class="inline-flex items-center text-sm
font-medium text-primary
dark:text-primary-light">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M7.5 21 3 16.5m0 0L7.5 12M3
16.5h13.5m0-13.5L21 7.5m0
0L16.5 12M21 7.5H7.5" />
</svg>
</button>
</div>
<div class="flex-1 w-full">
<label for="to"
class="block text-sm font-medium
opacity-75">
Umrechnen in:
</label>
<select id="to" name="to"
@change="navigateWithValue(
$event.target.value)"
class="select mt-1 block w-full">
{{ range $unit, $unitData := $availableUnits }}
{{ if ne $unit $.Params.from }}
<option
value="/{{ $.Params.from }}-in-{{ $unit }}/"
{{ if eq $unit
$.Params.to }}selected{{ end }}>
{{ $unitData.name }}
</option>
{{ end }}
{{ end }}
</select>
</div>
<div class="flex-1 w-full">
<label for="to"
class="block text-sm font-medium opacity-75">
Umrechnen in:
</label>
<select id="to" name="to"
@change="navigateWithValue($event.target.value)"
class="select mt-1 block w-full">
{{ range $unit, $unitData := $availableUnits }}
{{ if ne $unit $.Params.from }}
<option
value="/{{ $.Params.from }}-in-{{ $unit }}/"
{{ if eq $unit
$.Params.to }}selected{{ end }}>
{{ $unitData.name }}
</option>
{{ end }}
{{ end }}
</select>
</div> </div>
</div> </div>
</div>
<div x-data="createConverter('{{ .Params.engine }}',
{{ $config | safeJS }})"
x-init="init()">
<div class="flex flex-col sm:flex-row gap-4 mt-4"> <div class="flex flex-col sm:flex-row gap-4 mt-4">
<div class="flex-1"> <div class="flex-1">
<label for="value" <label for="value"
class="block text-sm font-medium class="block text-sm font-medium opacity-75">
opacity-75">
Wert in {{ .Params.from_name }}: Wert in {{ .Params.from_name }}:
</label> </label>
<input type="text" id="value" <input type="text" id="value"
@@ -152,8 +159,7 @@
</div> </div>
<div class="flex-1"> <div class="flex-1">
<label for="result" <label for="result"
class="block text-sm font-medium class="block text-sm font-medium opacity-75">
opacity-75">
Ergebnis in {{ .Params.to_name }}: Ergebnis in {{ .Params.to_name }}:
</label> </label>
<input type="text" id="result" <input type="text" id="result"
@@ -170,13 +176,13 @@
</div> </div>
</div> <div id="formula" class="mt-4">
{{ partial "conversion-formula.html" . }}
</div>
<div id="info">
{{ partial "conversion-seo-texts.html" . }}
</div>
<div id="formula" class="mt-4">
{{ partial "conversion-formula.html" . }}
</div>
<div id="info">
{{ partial "conversion-seo-texts.html" . }}
</div> </div>
{{ end }} {{ end }}

View File

@@ -49,27 +49,40 @@
</p> </p>
{{ end }} {{ end }}
{{ else if eq .Params.engine "runtime" }} {{ else if eq .Params.engine "runtime" }}
{{ $rate := float .Params.rate }}
<p class="mb-4 mt-8"> <p class="mb-4 mt-8">
Die Umrechnung von {{ $fromName }} in {{ $toName }} Die Umrechnung von {{ $fromName }} in {{ $toName }}
erfolgt mittels dieser Formel: erfolgt mittels dieser Formel:
</p> </p>
{{ if ge $rate 1 }} <div x-show="currentRate > 0">
<p class="text-lg text-primary dark:text-primary-light"> <p class="text-lg text-primary dark:text-primary-light"
<b>{{ $toName }} = {{ $fromName }} × x-show="currentRate >= 1">
{{ printf "%.12g" $rate }}</b> <b x-text="'{{ $toName }} = {{ $fromName }} × '
+ prettyNumber(currentRate)"></b>
</p> </p>
{{ else }} <p class="text-lg text-primary dark:text-primary-light"
{{ $invRate := div 1 $rate }} x-show="currentRate < 1">
<p class="text-lg text-primary dark:text-primary-light"> <b x-text="'{{ $toName }} = {{ $fromName }} / '
<b>{{ $toName }} = {{ $fromName }} / + prettyNumber(1 / currentRate)"></b>
{{ printf "%.12g" $invRate }}</b>
</p> </p>
{{ end }} </div>
{{ with .Params.rates_updated }} <div x-show="!currentRate">
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2"> {{ $rate := float .Params.rate }}
Wechselkurs zuletzt aktualisiert: {{ if ge $rate 1 }}
{{ time.Format "2. Januar 2006, 15:04" . }} <p class="text-lg text-primary dark:text-primary-light">
</p> <b>{{ $toName }} = {{ $fromName }} ×
{{ end }} {{ printf "%.12g" $rate }}</b>
</p>
{{ else }}
{{ $invRate := div 1 $rate }}
<p class="text-lg text-primary dark:text-primary-light">
<b>{{ $toName }} = {{ $fromName }} /
{{ printf "%.12g" $invRate }}</b>
</p>
{{ end }}
</div>
<p x-show="ratesUpdated"
class="text-sm text-gray-600 dark:text-gray-400 mt-2">
Wechselkurs zuletzt aktualisiert:
<span x-text="ratesUpdated"></span>
</p>
{{ end }} {{ end }}

View File

@@ -36,92 +36,14 @@ function navActions() {
* @returns {object} Alpine component data * @returns {object} Alpine component data
*/ */
function createConverter(engine, config) { 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 || '';
const toCode = config.toCurrency || '';
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 { return {
inputValue: '1', inputValue: '1',
result: '', result: '',
rates: {},
ratesError: '', ratesError: '',
init() { ratesUpdated: '',
currentRate: 0,
async init() {
const params = new URLSearchParams( const params = new URLSearchParams(
window.location.search); window.location.search);
const valueParam = params.get('v'); const valueParam = params.get('v');
@@ -133,6 +55,9 @@ function createConverter(engine, config) {
this.inputValue = val.replace(/,/g, '.'); this.inputValue = val.replace(/,/g, '.');
} }
}); });
if (typeof config.init === 'function') {
await config.init.call(this);
}
this.calculate(); this.calculate();
}, },
calculate() { calculate() {
@@ -144,7 +69,9 @@ function createConverter(engine, config) {
} }
const value = new Decimal(normalized); const value = new Decimal(normalized);
let rawResult; let rawResult;
if (engine === 'linear') { if (typeof config.convert === 'function') {
rawResult = config.convert.call(this, value);
} else if (engine === 'linear') {
const fromFactor = new Decimal( const fromFactor = new Decimal(
config.fromFactor); config.fromFactor);
const toFactor = new Decimal( const toFactor = new Decimal(
@@ -185,7 +112,16 @@ function createConverter(engine, config) {
function prettyNumber(num, minPrecision, maxPrecision) { function prettyNumber(num, minPrecision, maxPrecision) {
minPrecision = minPrecision || 4; minPrecision = minPrecision || 4;
maxPrecision = maxPrecision || 10; maxPrecision = maxPrecision || 10;
const d = new Decimal(num); const val = num instanceof Decimal ? num.toNumber() : Number(num);
if (!Number.isFinite(val)) {
return '';
}
let d;
try {
d = new Decimal(num);
} catch (e) {
return '';
}
const absVal = d.abs(); const absVal = d.abs();
// Larger numbers need fewer decimal places for readable output; // Larger numbers need fewer decimal places for readable output;
// smaller numbers get more to stay meaningful. // smaller numbers get more to stay meaningful.