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

1
hugo/static/css/main.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/static/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/static/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#6d28d9","background_color":"#ffffff","display":"standalone"}

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="M5 21h14c1.1 0 2-.9 2-2V8c0-.27-.11-.52-.29-.71l-4-4A1 1 0 0 0 16 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2m10-2H9v-5h6zM11 5h2v2h-2zM5 5h2v4h8V5h.59L19 8.41V19h-2v-5c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v5H5z"></path></svg>

After

Width:  |  Height:  |  Size: 382 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="m20,10h-4v-3c0-.55-.45-1-1-1h-6c-.55,0-1,.45-1,1v3H4c-.38,0-.73.22-.9.57-.17.35-.12.76.12,1.06l8,10c.19.24.48.38.78.38s.59-.14.78-.38l8-10c.24-.3.29-.71.12-1.06-.17-.35-.52-.57-.9-.57Zm-8,9.4l-5.92-7.4h2.92c.55,0,1-.45,1-1v-3h4v3c0,.55.45,1,1,1h2.92l-5.92,7.4Z"></path><path d="M8 2H16V4H8z"></path></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="m5,15h5v6c0,.43.28.82.69.95.1.03.21.05.31.05.31,0,.62-.15.81-.41l8-11c.22-.3.25-.71.08-1.04-.17-.34-.52-.55-.89-.55h-5V3c0-.43-.28-.82-.69-.95-.41-.13-.86.01-1.12.36L4.19,13.41c-.22.3-.25.71-.08,1.04.17.34.52.55.89.55Zm7-8.92v3.92c0,.55.45,1,1,1h4.04l-5.04,6.92v-3.92c0-.55-.45-1-1-1h-4.04l5.04-6.92Z"></path></svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M5 19V5h14v14z"></path><path d="M12 9h3v3h2V7h-5zM9 12H7v5h5v-2H9z"></path></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="m19.58 11.19-7-5A.997.997 0 0 0 11 7v3.06L5.58 6.19A.997.997 0 0 0 4 7v10c0 .37.21.72.54.89.14.07.3.11.46.11.2 0 .41-.06.58-.19L11 13.94V17c0 .37.21.72.54.89.14.07.3.11.46.11.2 0 .41-.06.58-.19l7-5c.26-.19.42-.49.42-.81s-.16-.63-.42-.81M6 15.06V8.95l4.28 3.06L6 15.07Zm7 0V8.95l4.28 3.06L13 15.07Z"></path></svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="m22,8c0-.55-.45-1-1-1h-2v-1c0-.55-.45-1-1-1h-3c-.55,0-1,.45-1,1v5h-4v-5c0-.55-.45-1-1-1h-3c-.55,0-1,.45-1,1v1h-2c-.55,0-1,.45-1,1v3h-1v2h1v3c0,.55.45,1,1,1h2v1c0,.55.45,1,1,1h3c.55,0,1-.45,1-1v-5h4v5c0,.55.45,1,1,1h3c.55,0,1-.45,1-1v-1h2c.55,0,1-.45,1-1v-3h1v-2h-1v-3ZM4,15v-6h1v6h-1Zm4,2h-1V7h1v10Zm9,0h-1V7h1v10Zm3-2h-1v-6h1v6Z"></path></svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="M22 7H2c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1h20c.55 0 1-.45 1-1V8c0-.55-.45-1-1-1m-1 8H3V9h2v3h2V9h2v4h2V9h2v3h2V9h2v4h2V9h2z"></path></svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="M7.51 18.35c1.63 1.71 3.8 2.65 6.11 2.65s4.48-.94 6.11-2.65l-1.45-1.38c-1.25 1.31-2.9 2.03-4.66 2.03s-3.41-.72-4.66-2.03c-.55-.58-.99-1.25-1.31-1.97h4.36v-2H7.08c-.04-.33-.07-.66-.07-1s.03-.67.07-1h4.93V9H7.65c.32-.72.76-1.39 1.31-1.97C10.21 5.72 11.86 5 13.62 5s3.41.72 4.66 2.03l1.45-1.38C18.1 3.94 15.93 3 13.62 3s-4.48.94-6.11 2.65C6.59 6.61 5.92 7.75 5.5 9H3v2h2.06c-.03.33-.06.66-.06 1s.02.67.06 1H3v2h2.5a9 9 0 0 0 2.01 3.35"></path></svg>

After

Width:  |  Height:  |  Size: 614 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="m15,11.81v-7.81c0-1.65-1.35-3-3-3s-3,1.35-3,3v7.81c-2.16,1.25-3.34,3.73-2.91,6.25.41,2.38,2.27,4.32,4.63,4.81.42.09.85.13,1.27.13,1.38,0,2.71-.46,3.79-1.34,1.41-1.15,2.22-2.84,2.22-4.66,0-2.16-1.16-4.13-3-5.19Zm-.48,8.3c-.95.77-2.16,1.06-3.39.8-1.54-.32-2.8-1.63-3.06-3.19-.32-1.84.65-3.64,2.34-4.38.36-.16.6-.52.6-.92V4c0-.55.45-1,1-1s1,.45,1,1v8.42c0,.4.24.76.6.92,1.46.64,2.4,2.08,2.4,3.66,0,1.21-.54,2.34-1.48,3.11Z"></path></svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="M3 16c0 .34.18.67.47.85l8 5a1.01 1.01 0 0 0 1.06 0l8-5c.29-.18.47-.5.47-.85V8c0-.34-.18-.67-.47-.85l-8-5c-.32-.2-.74-.2-1.06 0l-8 5c-.29.18-.47.5-.47.85zm2-6.53 6 3.6v6.13l-6-3.75zm8 9.73v-6.13l6-3.6v5.98zM12 4.18l5.84 3.65-5.84 3.5-5.84-3.5z"></path></svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="M21 4H3c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1m-1 11c-1.66 0-3 1.34-3 3H7c0-1.66-1.34-3-3-3V9c1.66 0 3-1.34 3-3h10c0 1.66 1.34 3 3 3z"></path><path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4m0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2"></path></svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="m13.75,11.71l-1.31,1.59c1.22,1.65,2.06,3.59,2.4,5.71H5.12L17.77,3.64l-1.54-1.27-5.39,6.55c-2.48-1.82-5.53-2.91-8.83-2.91v2c2.82,0,5.43.92,7.56,2.46l-7.33,8.9c-.25.3-.3.71-.13,1.06s.52.57.9.57h19v-2h-5.15c-.37-2.73-1.47-5.23-3.1-7.29Z"></path></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" transform="" id="injected-svg"><!--Boxicons v3.0 https://boxicons.com | License https://docs.boxicons.com/free--><path d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10 10-4.49 10-10S17.51 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8"></path><path d="M13 6h-2v6c0 .55.45 1 1 1h6v-2h-5z"></path></svg>

After

Width:  |  Height:  |  Size: 357 B

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