Compare commits

...

10 Commits

Author SHA1 Message Date
5142bb66ca feat: Implement "How many rats fit in a cage" 2025-04-13 22:17:57 +02:00
16d7c8f3dc refactor: abstract functions to reuse independent of number of rats 2025-04-13 19:17:23 +02:00
3ab46cbb72 feat: Add basis for how many rats calculator 2025-04-13 19:10:27 +02:00
276c4d6977 refactor: Add fontawesome to prod dependencies 2025-04-13 18:55:28 +02:00
53cac9f3d2 fix: Fix i18n infrastructure
Add dev dependency fix paths and key names so that the default HTML lexer can pick them up
2025-04-13 18:54:35 +02:00
de91ad9367 feat: Move result to tab content 2025-04-13 18:25:55 +02:00
4f9c7d82f8 feat: Add meta information 2025-04-13 18:15:44 +02:00
64b492a758 fix: Make sure first tab content is shown 2025-04-13 17:54:32 +02:00
9241250138 feat: Move app name to navbar 2025-04-13 17:54:13 +02:00
bbf30c4350 feat: Use fontawsome icons for measurements 2025-04-13 17:35:00 +02:00
9 changed files with 2174 additions and 111 deletions

View File

@@ -2,9 +2,10 @@ module.exports = {
defaultNamespace: 'translation',
lexers: {
js: ['JsxLexer'], // we're writing jsx inside .js files
html: ['HTMLLexer'],
default: ['JavascriptLexer'],
},
locales: ['en', 'de'],
output: 'public/lang/$LOCALE.json',
output: 'public/i18n/$LOCALE.json',
input: [ 'src/*.js', 'public/*.html', ],
}

1933
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"css-loader": "^7.1.2",
"i18next-parser": "^9.3.0",
"sass": "^1.86.3",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
@@ -34,10 +34,10 @@
"dependencies": {
"bulma": "^1.0.3",
"bulma-tooltip": "^3.0.2",
"font-awesome": "^4.7.0",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"sass-migrator": "^2.3.1"
"sass-migrator": "^2.3.1",
"@fortawesome/fontawesome-free": "^6.7.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,25 +1,23 @@
{
"back-to-home": "Zurück zur Homepage",
"app-name": "Käfigrechner",
"number-of-rats": "Anzahl an Ratten",
"full-floors": "Vollebenen",
"how-many-rats-does-this-cage-fit": "Wie viele Ratten passen in den Käfig?",
"what-cage-should-i-get": "Käfiggröße rechnen",
"check-existing-cage": "Käfig prüfen",
"cage-measurements": "Käfigmaße",
"width-cm": "Breite (cm)",
"depth-cm": "Tiefe (cm)",
"height-cm": "Höhe (cm)",
"full-floors-tooltip": "Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des Käfigbodens.",
"full-floors": "Vollebenen",
"number-of-rats": "Anzahl an Ratten",
"result": "Ergebnis",
"change-language": "Sprache ändern",
"failed-base-area": "Die Mindestgrundfläche des Käfigs muss {{ MINIMUM_BASE_AREA }}m² (also z.B. 100x50cm) betragen.",
"failed-overall-area": "Die Gesamtfläche im Käfig ist zu klein.",
"failed-fall-height": "Die mögliche Fallhöhe darf nicht mehr als {{ maximum_fall_height }}cm betragen.",
"failed-fall-height": "Die mögliche Fallhöhe darf nicht mehr als {{ maximum_fall_height }}cm betragen.",
"failed-floor-height": "Der Mindestabstand zwischen Ebenen muss {{ minimum_floor_height }}cm betragen.",
"failed-num-rats": "Es müssen mindestens 3 Ratten zusammenleben, Paarhaltung ist nicht artgerecht.",
"failed-minimum-length-long-side": "Die lange Seite des Käfig muss mindestens {{ minimum_length_long_side }}cm lang sein um Rennen zu ermöglichen.",
"failed-minimum-length-short-side": "Die kurze Seite des Käfig muss mindestens {{ minimum_length_short_side }}cm lang sein.",
"alt-savic-xl": "Bild des Käfigs Savic Suite Royal XL, ein Drahtkäfig mit vier Türen die erlauben die gesamte Vorderseite zu öffnen. Auf halber Höhe ist eine weitere Vollebene.",
"alt-savic-95-double": "Bild eines dekorierten Käfigs, des Savic Suite Royale 95 Double. Auf insgesamt vie Vollebenen sind Holz- und Papphäusschen, Hängematten, Futterschalen und mehr.",
"alt-tiaki": "Bild des Tiaki-Käfigs, ein Drahtkäfig mit vier Türen die erlauben die gesamte Vorderseite zu öffnen. Auf halber Höhe ist eine weitere Vollebene.",
"cage-measurements": "Käfigmaße",
"cage-complies-with-all-criteria": "Der Käfig erfüllt alle Kriterien!",
"cage-for-x-rats": "Käfig für {{ num_rats }} Ratten",
"change-language": "Sprache ändern",
"result": "Ergebnis"
}
"cage-for-x-rats": "Käfig für {{ num_rats }} Ratten"
}

View File

@@ -1,25 +1,23 @@
{
"back-to-home": "Back to home",
"app-name": "Cage Calculator",
"number-of-rats": "Number of Rats",
"full-floors": "Full floors",
"how-many-rats-does-this-cage-fit": "How many rats does this cage fit?",
"what-cage-should-i-get": "Calculate cage size",
"check-existing-cage": "Check cage",
"cage-measurements": "Cage measurements",
"width-cm": "Width (cm)",
"depth-cm": "Depth (cm)",
"height-cm": "Height (cm)",
"full-floors-tooltip": "A full floor is each floor with a area greater than XXm², including the bottom of the cage.",
"full-floors": "Full floors",
"number-of-rats": "Number of Rats",
"result": "Result",
"change-language": "Change language",
"failed-base-area": "The base area of the cage must not be below {{ MINIMUM_BASE_AREA }}m².",
"failed-overall-area": "The overall area in the cage is to small.",
"failed-fall-height": "The possible fall height between floors must not be above {{ maximum_fall_height }}cm.",
"failed-fall-height": "The possible fall height between floors must not be above {{ maximum_fall_height }}cm.",
"failed-floor-height": "The height between floors must be above {{ minimum_floor_height }}cm.",
"failed-num-rats": "Rats must live in a group of at least three rats, pairs or lone rats are not species-appropriate.",
"failed-minimum-length-long-side": "The long side of the cage must be at least {{ minimum_length_long_side }}cm long to enable running.",
"failed-minimum-length-short-side": "The short side of the cage must be at least {{ minimum_length_short_side }}cm.",
"alt-savic-xl": "A picture of a wire bar cage with four doors that allow to open the full front of the cage. In the middle of the cage is a additional full floor.",
"alt-savic-95-double": "A picture of a decorated wire bar cage with four doors that allow to open the full front of the cage. Inside there are hammocks, toys and four full floors with lots of hides",
"alt-tiaki": "A picture of a decorated wire bar cage with four doors that allow to open the full front of the cage.",
"cage-measurements": "Cage measurements",
"cage-complies-with-all-criteria": "This cage complies with all criteria!",
"cage-for-x-rats": "Cage for {{ num_rats }} rats",
"change-language": "Change language",
"result": "Result"
}
"cage-for-x-rats": "Cage for {{ num_rats }} rats"
}

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8">
<title>Käfigrechner</title>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<meta name="description"
content="Rechner für die Größe eines Rattenkäfigs basierend auf den Empfehlungen des VdRD e.V.">
<meta name="keywords" content="Farbratten, Käfig, Ratten, Rechner, Rat, Calculator, Cage">
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon/favicon-16x16.png">
@@ -12,15 +15,15 @@
<div class="navbar">
<div class="navbar-brand">
<a class="button" href="https://vdrd.de">
<b data-i18n-key="back-to-home">zurück zur Homepage</b>
</a>
<div class="navbar-item">
<img src="assets/img/logo_vdrd.png" alt="VdRD Log">
<h1 data-i18n="app-name" class="title is-3">Käfigrechner</h1>
</div>
</div>
</div>
<div class="content">
<h1 data-i18n-key="app-name" class="title is-1">Käfigrechner</h1>
<div class="tabs is-centered is-boxed is-toggle" id="primary-tabs">
<ul>
<li class="is-active" data-tab-id="1">
@@ -28,7 +31,7 @@
<span class="icon is-small">
<i class="fas fa-number" aria-hidden="true"></i>
</span>
How many rats does this cage fit?
<p data-i18n="how-many-rats-does-this-cage-fit">How many rats does this cage fit?</p>
</a>
</li>
<li data-tab-id="2">
@@ -36,7 +39,7 @@
<span class="icon is-small">
<i class="fas fa-question" aria-hidden="true"></i>
</span>
What cage should I get?
<p data-i18n="what-cage-should-i-get">What cage should I get?</p>
</a>
</li>
<li data-tab-id="3">
@@ -44,7 +47,7 @@
<span class="icon is-small">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
Check existing cage
<p data-i18n="check-existing-cage">Check existing cage</p>
</a>
</li>
</ul>
@@ -52,34 +55,107 @@
</div>
<div id="tab-content">
<div class="" data-content-id="1">
<p>Here you can check how many rats could fit in your cage.</p>
<div class="is-active" data-content-id="1">
<div class="card">
<div class="card-content">
<h2 class="title is-4"><label data-i18n="cage-measurements"
for="form-cage-measurements">Käfigmaße</label></h2>
<form>
<label for="num-rats-width" class="label" data-i18n="width-cm">Breite (cm)</label>
<p class="control has-icons-left">
<input class="input" type="number" id="num-rats-width" placeholder="100 cm">
<span class="icon is-small is-left">
<i class="fas fa-ruler-horizontal"></i>
</span>
</p>
<label for="num-rats-depth" class="label" data-i18n="depth-cm">Tiefe (cm)</label>
<p class="control has-icons-left">
<input class="input" type="number" id="num-rats-depth" placeholder="50 cm">
<span class="icon is-small is-left">
<i class="fas fa-ruler"></i>
</span>
</p>
<label for="num-rats-height" class="label" data-i18n="height-cm">Höhe (cm)</label>
<p class="control has-icons-left">
<input class="input" type="number" id="num-rats-height" placeholder="120 cm">
<span class="icon is-small is-left">
<i class="fas fa-ruler-vertical"></i>
</span>
</p>
<div class="columns">
<div class="column">
<label for="num-rats-numFullFloors" class="label"
data-i18n="full-floors">Vollebenen</label>
<span data-tooltip="Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des Käfigbodens.">
<svg class="text-grey-dark" width="18" height="18" viewBox="0 0 18 18" fill="none"
stroke="currentColor">
<path d="M9.00026 12.6C9.00026 12.6 9.00026 12.1224 9.00026 11.5333V8.86666C9.00026 8.57211 8.76148 8.33333 8.46693 8.33333H7.93359"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M8.73346 5.26666C8.58619 5.26666 8.4668 5.38605 8.4668 5.53333C8.4668 5.68061 8.58619 5.8 8.73346 5.8C8.88074 5.8 9.00013 5.68061 9.00013 5.53333C9.00013 5.38605 8.88074 5.26666 8.73346 5.26666V5.26666"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M9 17C13.4183 17 17 13.4183 17 9C17 4.58172 13.4183 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4183 4.58172 17 9 17Z"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</span>
<input type="number" id="num-rats-numFullFloors" value="3" step="1">
</div>
</div>
</form>
</div>
</div>
<div class="card-footer is-fullwidth">
<div class="card result-card" id="num-rats-result-card">
<div class="card-header">
<h2 class="card-header-title is-3 is-centered" data-i18n="result">Ergebnis</h2>
</div>
<div class="card-content">
<div id="num-rats-resultsDiv"></div>
</div>
</div>
</div>
</div>
<div class="" data-content-id="2">
<p>Here is a list of cages you can get.</p>
</div>
<div class="is-active" data-content-id="3">
<div class="card" id="container-cages">
<div class="card-content" id="card-ManualMeasurements">
<h2 class="title is-4"><label data-i18n-key="cage-measurements"
<div class="" data-content-id="3">
<div class="card">
<div class="card-content">
<h2 class="title is-4"><label data-i18n="cage-measurements"
for="form-cage-measurements">Käfigmaße</label></h2>
<form id="form-cage-measurements" class="form-measurements">
<div class="input-measurement">
<label for="width" class="label" data-i18n-key="width-cm">Breite (cm)</label>
<input class="input" type="number" id="width">
</div>
<div class="input-measurement">
<label for="depth" class="label" data-i18n-key="depth-cm">Tiefe (cm)</label>
<input class="input" type="number" id="depth">
</div>
<label for="height" class="label" data-i18n-key="height-cm">Höhe (cm)</label>
<input class="input" type="number" id="height">
<label for="width" class="label" data-i18n="width-cm">Breite (cm)</label>
<p class="control has-icons-left">
<input class="input" type="number" id="width" placeholder="100 cm">
<span class="icon is-small is-left">
<i class="fas fa-ruler-horizontal"></i>
</span>
</p>
<label for="depth" class="label" data-i18n="depth-cm">Tiefe (cm)</label>
<p class="control has-icons-left">
<input class="input" type="number" id="depth" placeholder="50 cm">
<span class="icon is-small is-left">
<i class="fas fa-ruler"></i>
</span>
</p>
<label for="height" class="label" data-i18n="height-cm">Höhe (cm)</label>
<p class="control has-icons-left">
<input class="input" type="number" id="height" placeholder="120 cm">
<span class="icon is-small is-left">
<i class="fas fa-ruler-vertical"></i>
</span>
</p>
<div class="columns">
<div class="column">
<label for="numFullFloors" class="label"
data-i18n-key="full-floors">Vollebenen</label>
data-i18n="full-floors">Vollebenen</label>
<span data-tooltip="Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des Käfigbodens.">
<svg class="text-grey-dark" width="18" height="18" viewBox="0 0 18 18" fill="none"
stroke="currentColor">
@@ -96,7 +172,7 @@
</div>
<div class="column">
<label for="numRats" id="labelNumRats" data-i18n-key="number-of-rats">Anzahl an
<label for="numRats" id="labelNumRats" data-i18n="number-of-rats">Anzahl an
Ratten</label>
<input type="range" min="3" max="15" value="4" class="slider" id="numRats">
</div>
@@ -104,16 +180,16 @@
</form>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="title is-3" data-i18n-key="result">Ergebnis</h2>
</div>
<div class="card-content">
<div id="resultsDiv"></div>
<div class="card-footer is-fullwidth">
<div class="card result-card" id="result-card">
<div class="card-header">
<h2 class="card-header-title is-3 is-centered" data-i18n="result">Ergebnis</h2>
</div>
<div class="card-content">
<div id="resultsDiv"></div>
</div>
</div>
</div>
</div>
</div>
@@ -121,8 +197,8 @@
<div class="footer">
<div class="language-switcher">
<label aria-hidden="false" style="display: none" data-i18n-key="change-language"></label>
<select data-i18n-switcher class="locale-switcher" id="locale-switcher">
<label aria-hidden="false" style="display: none" data-i18n="change-language"></label>
<select data-i18n-switcher class="select" id="locale-switcher">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>

View File

@@ -29,10 +29,10 @@ async function initI18next() {
function translatePageElements() {
// Translate content inside a tag
const translatableElements = document.querySelectorAll(
"[data-i18n-key]",
"[data-i18n]",
);
translatableElements.forEach((el) => {
const key = el.getAttribute("data-i18n-key");
const key = el.getAttribute("data-i18n");
el.innerHTML = i18next.t(key);
});
// Translate alt texts
@@ -55,7 +55,7 @@ function bindLocaleSwitcher(initialValue) {
i18next
.changeLanguage(e.target.value)
.then(translatePageElements)
.then(update);
.then(updateCageCheck);
};
}
@@ -63,7 +63,7 @@ function bindLocaleSwitcher(initialValue) {
await initI18next();
translatePageElements();
bindLocaleSwitcher(i18next.resolvedLanguage);
update();
updateCageCheck();
})();
//////////
@@ -148,26 +148,33 @@ class Validator {
if (numOfRats < 3 || numOfRats > 15) {
throw new Error("This formula works only from 3 to 15 rats");
}
return MINIMUM_AREA_THREE_RATS + (numOfRats - 3) * AREA_PER_ADDITIONAL_RAT;
return MINIMUM_AREA_THREE_RATS + (numOfRats - 3.0) * AREA_PER_ADDITIONAL_RAT;
}
cageCheck(dimensions, numRats, numFullFloors) {
let failedCriteria = {};
if (numRats < 2 || numRats > 15) {
failedCriteria[FAILED_NUM_RATS] = this.FAIL_CRITERIA[FAILED_NUM_RATS];
allowedNumberOfRats(overallArea) {
/*
Calculates the number of rats that are allowed for a certain overall area.
*/
let result = 3.0 + (overallArea-MINIMUM_AREA_THREE_RATS) / AREA_PER_ADDITIONAL_RAT;
console.log(`Area left: ${overallArea-MINIMUM_AREA_THREE_RATS}`);
console.log(`Result: ${result}`);
if (result < 3) {
throw new Error("Cages must be for three rats or more");
}
return result;
}
failCageNumberIndependent(dimensions, numFullFloors) {
/*
Function that checks a cage independent of the number of rats.
*/
let failedCriteria = {};
const baseArea = dimensions.depth * dimensions.width;
if (baseArea < MINIMUM_BASE_AREA) {
failedCriteria[FAILED_BASE_AREA] = this.FAIL_CRITERIA[FAILED_BASE_AREA];
}
const areaNeeded = this.overallAreaNeeded(numRats);
if (baseArea * numFullFloors < areaNeeded) {
failedCriteria[FAILED_OVERALL_AREA] = this.FAIL_CRITERIA[FAILED_OVERALL_AREA];
}
if (dimensions.height / numFullFloors > MAXIMUM_FALL_HEIGHT) {
failedCriteria[FAILED_FALL_HEIGHT] = this.FAIL_CRITERIA[FAILED_FALL_HEIGHT];
}
@@ -187,6 +194,30 @@ class Validator {
return failedCriteria;
}
getOverallArea(dimensions, numFullFloors) {
const baseArea = dimensions.depth * dimensions.width;
return baseArea * numFullFloors
}
cageCheck(dimensions, numRats, numFullFloors) {
/*
Function that checks a cage based on overall criteria and the number of rats.
*/
let failedCriteria = this.failCageNumberIndependent(dimensions, numFullFloors);
if (numRats < 2 || numRats > 15) {
failedCriteria[FAILED_NUM_RATS] = this.FAIL_CRITERIA[FAILED_NUM_RATS];
}
const overallArea = this.getOverallArea(dimensions, numFullFloors);
const overallAreaNeeded = this.overallAreaNeeded(numRats);
if (overallArea < overallAreaNeeded) {
failedCriteria[FAILED_OVERALL_AREA] = this.FAIL_CRITERIA[FAILED_OVERALL_AREA];
}
return failedCriteria;
}
}
@@ -212,13 +243,30 @@ class Dimensions {
// DOCUMENT INTERACTION //
//////////////////////////
const inputWidth = document.getElementById("width");
inputWidth.onchange = updateViaManualMeasurements;
const inputDepth = document.getElementById("depth");
inputDepth.onchange = updateViaManualMeasurements;
const inputHeight = document.getElementById("height");
inputHeight.onchange = updateViaManualMeasurements;
const numRatsCalculatorInputWidth = document.getElementById("num-rats-width");
numRatsCalculatorInputWidth.onchange = updateNumRatsCalculator;
const numRatsCalculatorInputDepth = document.getElementById("num-rats-depth");
numRatsCalculatorInputDepth.onchange = updateNumRatsCalculator;
const numRatsCalculatorInputHeight = document.getElementById("num-rats-height");
numRatsCalculatorInputHeight.onchange = updateNumRatsCalculator;
let numRatsNumFullFloors = document.getElementById("num-rats-numFullFloors");
numRatsNumFullFloors.oninput = function () {
updateNumRatsCalculator();
}
const inputWidth = document.getElementById("width");
inputWidth.onchange = updateCageCheck;
const inputDepth = document.getElementById("depth");
inputDepth.onchange = updateCageCheck;
const inputHeight = document.getElementById("height");
inputHeight.onchange = updateCageCheck;
let fullFloorNum = document.getElementById("numFullFloors");
fullFloorNum.oninput = function () {
updateCageCheck();
}
let labelNumRats = document.getElementById("labelNumRats");
@@ -226,14 +274,9 @@ let labelNumRats = document.getElementById("labelNumRats");
let ratSlider = document.getElementById("numRats");
ratSlider.oninput = function () {
update();
updateCageCheck();
}
// Full floor functions
let fullFloorNum = document.getElementById("numFullFloors");
fullFloorNum.oninput = function () {
update();
}
function getResultFromChecks(checks) {
if (Object.keys(checks).length > 0) {
@@ -252,11 +295,7 @@ function getResultFromChecks(checks) {
}
function updateViaManualMeasurements() {
update();
}
function update() {
function updateCageCheck() {
labelNumRats.innerHTML = i18next.t("cage-for-x-rats", {"num_rats": ratSlider.value});
const width = inputWidth.value
@@ -272,3 +311,38 @@ function update() {
resultsDiv.innerHTML = "";
resultsDiv.appendChild(result);
}
function updateNumRatsCalculator() {
const width = numRatsCalculatorInputWidth.value
const depth = numRatsCalculatorInputDepth.value
const height = numRatsCalculatorInputHeight.value
const dimensions = new Dimensions(width / 100, depth / 100, height / 100);
const validator = new Validator();
const failed_checks = validator.failCageNumberIndependent(dimensions, fullFloorNum.value);
let overallArea = validator.getOverallArea(dimensions, numRatsNumFullFloors.value);
let allowedNumRats;
try {
allowedNumRats = validator.allowedNumberOfRats(overallArea);
} catch (e) {
console.log(e);
failed_checks[FAILED_BASE_AREA] = validator.FAIL_CRITERIA[FAILED_OVERALL_AREA];
allowedNumRats = 0;
}
console.log(`Allowed number: ${allowedNumRats}`);
let resultsDiv = document.getElementById("num-rats-resultsDiv");
const result = getResultFromChecks(failed_checks);
resultsDiv.innerHTML = "";
const p = document.createElement('p');
p.textContent = i18next.t("cage-for-x-rats", {"num_rats": allowedNumRats});
resultsDiv.appendChild(p);
resultsDiv.appendChild(result);
}
const validator = new Validator();

View File

@@ -24,6 +24,13 @@ $beige-lighter: #eff0eb;
display: block;
}
body {
padding: 5px;
}
.result-card {
width: 100%;
}
// Import the Google Font
@import url("https://fonts.googleapis.com/css?family=Nunito:400,700");