Erstes Commit

This commit is contained in:
2026-05-27 20:04:58 +02:00
commit e16e9e36fd
218 changed files with 2307 additions and 0 deletions

5
hugo/static/js/alpine-3.14.9.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

280
hugo/static/js/converter.js Normal file
View File

@@ -0,0 +1,280 @@
'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';
}

File diff suppressed because one or more lines are too long