Compare commits

...

27 Commits

Author SHA1 Message Date
a6b88b4bb7 refactor: Use standard components for num of floors 2025-04-13 17:28:19 +02:00
ee0ba6acf8 feat: Add primary tabs for the three different modes 2025-04-13 17:13:09 +02:00
7e473955d9 feat: Add tooltip, improve styling a lot 2025-04-13 13:36:34 +02:00
809c73a767 feat: initial bulma restructuring 2025-04-13 09:00:27 +02:00
7b336f2817 fix: style 2025-02-11 15:27:12 +01:00
3808aafa88 feat: add cages from json 2024-09-15 18:06:38 +02:00
8e0adf54a2 refactor: formatting 2024-09-15 17:17:02 +02:00
76a6480528 fix: use vdrd website as main domain
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-08-25 19:46:34 +02:00
89d9d46a81 feat: add link to real site 2024-08-25 19:46:34 +02:00
6e15ea9006 feat: Enlargen label
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-08-01 13:10:26 +00:00
7ec5de190d fix: Remove hard-coded language, allow changing
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-31 23:19:15 +02:00
63d3a44e4b feat: Style language chooser
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-31 20:45:55 +02:00
6f7cf6e873 fix: Make sure results get cleared
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-31 19:57:29 +02:00
bc7f32b57d feat: Translate result
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-31 19:51:28 +02:00
6897db4bbf feat: Add label for language switcher, hide because obvious 2024-07-31 19:48:29 +02:00
eab39d0d79 trans: fix various translation issues in active components 2024-07-31 19:38:59 +02:00
128ef795d8 trans: Translate success message 2024-07-31 19:29:33 +02:00
b7f2a3ed67 fix: use alt-i18n-key to avoid overwriting alt tag which makes retranslation impossible 2024-07-31 19:20:46 +02:00
3831b81ec2 fix: tiaki alt key 2024-07-31 19:17:35 +02:00
e2632c3934 feat: add alt text to images 2024-07-31 19:17:01 +02:00
ac2715f5f4 feat: style language switcher 2024-07-31 18:48:23 +02:00
527c5b180f trans: Add english translations 2024-07-31 18:21:43 +02:00
04027e26f1 feat: Make translations use interpolation 2024-07-31 18:15:42 +02:00
a86e1b3935 feat: Change rule to allow for smaller floors (25cm) 2024-07-31 18:15:00 +02:00
603e7738ec fix: don't try to load deprecated calculator 2024-07-31 18:04:56 +02:00
45d78a37e9 fix: restructure code so vaildator gets initialized/calculator updates only after translation are loaded 2024-07-31 15:50:46 +02:00
183c41b387 refactor: Move calculator to main js 2024-07-31 15:21:40 +02:00
14 changed files with 1731 additions and 2234 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
node_modules/
public/bundle.js*
/src/main.css
/src/main.css.map

View File

@@ -1,7 +1,15 @@
# RattenheimRechner
**[rechner.vdrd.de](https://rechner.vdrd.de)**
Ein Rechner für die minimale Größe eines Rattenkäfigs nach Standard des [VdRD e.V.](https://vdrd.de)
## Development
```bash
npm run start
```
## CI
CI runs via woodpecker. The following secrets should be set

3110
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,9 @@
"scripts": {
"start": "webpack-dev-server --config webpack.config.js",
"ex-trans": "i18next -c i18next-parser.config.js",
"build": "webpack --config webpack.config.js"
"build": "webpack --config webpack.config.js",
"build-bulma": "sass --load-path=node_modules src/main.scss src/main.css",
"start-bulma": "npm run build-bulma -- --watch"
},
"repository": {
"type": "git",
@@ -19,13 +21,23 @@
"author": "Julian-Samuel Gebühr",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"css-loader": "^7.1.2",
"sass": "^1.86.3",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
},
"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"
"i18next-http-backend": "^2.5.2",
"sass-migrator": "^2.3.1"
}
}

View File

@@ -62,6 +62,7 @@ ul {
label {
font-weight: bold;
word-break: break-word;
font-size: x-large;
}
.slidecontainer {
@@ -144,7 +145,7 @@ label {
border-radius: 10px;
}
@media screen and (min-width: 800px) {
@media screen and (max-width: 800px) {
flex: 0 1 calc(25% - 0.5em);
}
@@ -199,15 +200,31 @@ input.measurement {
flex-wrap: wrap;
}
.navigation-sticky {
.top-navigation {
margin: 0;
border: none;
width: 100%;
display: flex;
}
.homebutton {
background-color: var(--secondary-light-one);
color: var(--primary-light-one);
border-bottom-right-radius: 8px;
font-weight: bold;
padding: 16px;
width: 20%;
}
.language-switcher {
background-color: var(--secondary-light-one);
color: var(--primary-light-one);
padding: 16px;
margin: 0;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
border: none;
font-weight: bold;
width: 20%;
width: 15%;
margin-left: auto;
}
@@ -231,4 +248,12 @@ input.measurement {
.tooltip:hover .tooltiptext {
visibility: visible;
}
select, option {
background-color: var(--primary-light-one);
border-radius: 0.2rem;
border: none;
color: var(--text-one);
width: 100%;
}

View File

@@ -1,85 +0,0 @@
const MINIMUM_BASE_AREA = 0.5;
const MINIMUM_AREA_THREE_RATS = 1.5;
const AREA_PER_ADDITIONAL_RAT = 0.25;
const MAXIMUM_FALL_HEIGHT = 0.5;
const MINIMUM_LENGTH_LONG_SIDE = 0.8;
const MINIMUM_LENGTH_SHORT_SIDE = 0.5;
const MINIMUM_FLOOR_HEIGHT = 0.3;
const FAILED_BASE_AREA = "base_area";
const FAILED_OVERALL_AREA = "overall_area";
const FAILED_FALL_HEIGHT = "fall_height";
const FAILED_NUM_RATS = "num_rats";
const FAILED_MINIMUM_LENGTH_LONG_SIDE = "length_long_side";
const FAILED_MINIMUM_LENGTH_SHORT_SIDE= "length_short_side";
const FAILED_FLOOR_HEIGHT = "floor_height"
const FAIL_CRITERIA = {
[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 * 100).toFixed(0)}cm betragen.`,
[FAILED_FLOOR_HEIGHT]: `Der Mindestabstand zwischen Ebenen muss ${(MINIMUM_FLOOR_HEIGHT * 100).toFixed(0)}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 * 100).toFixed(0)}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 * 100).toFixed(0)}cm lang sein.`,
};
class Dimensions {
constructor(width, depth, height) {
this.width = width;
this.depth = depth;
this.height = height;
}
toString() {
return `${this.width}x${this.depth}x${this.height}`;
}
static fromDict(data) {
const { width, depth, height } = data;
return new Dimensions(width, depth, height);
}
}
function overallAreaNeeded(numOfRats) {
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;
}
function cageCheck(dimensions, numRats, numFullFloors) {
let failedCriteria = {};
if (numRats < 2 || numRats > 15) {
failedCriteria[FAILED_NUM_RATS] = FAIL_CRITERIA[FAILED_NUM_RATS];
}
const baseArea = dimensions.depth * dimensions.width;
if (baseArea < MINIMUM_BASE_AREA) {
failedCriteria[FAILED_BASE_AREA] = FAIL_CRITERIA[FAILED_BASE_AREA];
}
const areaNeeded = overallAreaNeeded(numRats);
if (baseArea * numFullFloors < areaNeeded) {
failedCriteria[FAILED_OVERALL_AREA] = FAIL_CRITERIA[FAILED_OVERALL_AREA];
}
if (dimensions.height / numFullFloors > MAXIMUM_FALL_HEIGHT) {
failedCriteria[FAILED_FALL_HEIGHT] = FAIL_CRITERIA[FAILED_FALL_HEIGHT];
}
if (dimensions.width < MINIMUM_LENGTH_LONG_SIDE && dimensions.depth < MINIMUM_LENGTH_LONG_SIDE) {
failedCriteria[FAILED_MINIMUM_LENGTH_LONG_SIDE] = FAIL_CRITERIA[FAILED_MINIMUM_LENGTH_LONG_SIDE];
}
if (dimensions.width < MINIMUM_LENGTH_SHORT_SIDE || dimensions.depth < MINIMUM_LENGTH_SHORT_SIDE) {
failedCriteria[FAILED_MINIMUM_LENGTH_SHORT_SIDE] = FAIL_CRITERIA[FAILED_MINIMUM_LENGTH_SHORT_SIDE];
}
if (dimensions.height / numFullFloors < MINIMUM_FLOOR_HEIGHT) {
failedCriteria[FAILED_FLOOR_HEIGHT] = FAIL_CRITERIA[FAILED_FLOOR_HEIGHT];
}
return failedCriteria;
}

25
public/i18n/de.json Normal file
View File

@@ -0,0 +1,25 @@
{
"back-to-home": "Zurück zur Homepage",
"app-name": "Käfigrechner",
"number-of-rats": "Anzahl an Ratten",
"full-floors": "Vollebenen",
"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.",
"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-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"
}

25
public/i18n/en.json Normal file
View File

@@ -0,0 +1,25 @@
{
"back-to-home": "Back to home",
"app-name": "Cage Calculator",
"number-of-rats": "Number of Rats",
"full-floors": "Full floors",
"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.",
"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-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"
}

View File

@@ -3,119 +3,131 @@
<head>
<meta charset="UTF-8">
<title>Käfigrechner</title>
<link rel="stylesheet" href="assets/css/style.css">
<script src="assets/js/calculator.js"></script>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<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">
</head>
<body>
<div class="navigation-sticky">
<a href="https://notfellchen.org">
<b data-i18n-key="back-to-home">zurück zur Homepage</b>
</a>
<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>
</div>
<img src="img/translation-icon@2x.png" class="translation-icon" / >
<select data-i18n-switcher class="locale-switcher">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
<div class="content">
<h1 data-i18n-key="app-name">Käfigrechner</h1>
<div class="container-form">
<div class="cards">
<div class="card" id="card-SavicSuiteRoyaleXL">
<label for="SavicSuiteRoyaleXL">
<input type="checkbox" id="SavicSuiteRoyaleXL"/>
<div class="card-photo">
<img src="assets/img/savic-xl.jpeg">
</div>
<div class="info-container">
<h4><b>Savic Suite Royale XL</b></h4>
</div>
</label>
</div>
<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">
<a>
<span class="icon is-small">
<i class="fas fa-number" aria-hidden="true"></i>
</span>
How many rats does this cage fit?
</a>
</li>
<li data-tab-id="2">
<a>
<span class="icon is-small">
<i class="fas fa-question" aria-hidden="true"></i>
</span>
What cage should I get?
</a>
</li>
<li data-tab-id="3">
<a>
<span class="icon is-small">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
Check existing cage
</a>
</li>
</ul>
</div>
</div>
<div class="card" id="card-SavicSuiteRoyale95Double">
<label for="SavicSuiteRoyale95Double">
<input type="checkbox" id="SavicSuiteRoyale95Double"/>
<div class="card-photo">
<div>
<img src="assets/img/savic-95-double.jpg">
</div>
<div class="info-container">
<h4><b>Savic Suite Royale 95 Double</b></h4>
</div>
</div>
</label>
</div>
<div class="card" id="card-TiakiKleintierkäfigEtagere">
<label for="TiakiKleintierkäfigEtagere">
<input type="checkbox" id="TiakiKleintierkäfigEtagere"/>
<div class="card-photo">
<img src="assets/img/tiaki.jpeg">
</div>
<div class="info-container">
<h4><b>TIAKI Kleintierkäfig Étagère</b></h4>
</div>
</label>
</div>
<div class="card" id="card-ManualMeasurements">
<label for="form-cage-measurements">Käfigmaße</label>
<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>
<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"
for="form-cage-measurements">Käfigmaße</label></h2>
<form id="form-cage-measurements" class="form-measurements">
<div class="input-measurement">
<label for="width" data-i18n-key="width-cm">Breite (cm)</label>
<input class="measurement" type="number" id="width">
<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" data-i18n-key="depth-cm">Tiefe (cm)</label>
<input class="measurement" type="number" id="depth">
<label for="depth" class="label" data-i18n-key="depth-cm">Tiefe (cm)</label>
<input class="input" type="number" id="depth">
</div>
<div class="input-measurement">
<label for="height" data-i18n-key="height-cm">Höhe (cm)</label>
<input class="measurement" type="number" id="height">
<label for="height" class="label" data-i18n-key="height-cm">Höhe (cm)</label>
<input class="input" type="number" id="height">
<div class="columns">
<div class="column">
<label for="numFullFloors" class="label"
data-i18n-key="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="numFullFloors" value="3" step="1">
</div>
<div class="column">
<label for="numRats" id="labelNumRats" data-i18n-key="number-of-rats">Anzahl an
Ratten</label>
<input type="range" min="3" max="15" value="4" class="slider" id="numRats">
</div>
</div>
</form>
</div>
</div>
</div>
<div class="container-inputs">
<div class="input-element">
<label for="numFullFloors" data-i18n-key="full-floors">Vollebenen</label>
<div class="tooltip">
<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 class="tooltiptext" data-i18n-key="full-floors-tooltip">Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des Käfigbodens.</span>
</div>
<div class="ncontainer">
<div class="input-group">
<button id="decreaseFloorNum">-</button>
<input type="text" id="numFullFloors" value="3" readonly>
<button id="increaseFloorNum">+</button>
</div>
</div>
</div>
<div class="input-element">
<div class="slidecontainer">
<label for="numRats" id="labelNumRats" data-i18n-key="number-of-rats">Anzahl an Ratten ?</label>
<input type="range" min="3" max="15" value="4" class="slider" id="numRats">
</div>
</div>
</div>
<div class="container output-element" id="resultsDiv">
</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>
</div>
<script src="./bundle.js"></script>
<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">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</div>
</div>
</body>
</html>

View File

@@ -1,10 +0,0 @@
{
"back-to-home": "Zurück zur Homepage",
"app-name": "Käfigrechner",
"number-of-rats": "Anzahl an Ratten",
"full-floors": "Vollebenen",
"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."
}

View File

@@ -1,10 +0,0 @@
{
"back-to-home": "Back to home",
"app-name": "Cage Calculator",
"number-of-rats": "Number of Rats",
"full-floors": "Full floors",
"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 0.5m², including the bottom of the cage."
}

View File

@@ -1,171 +1,33 @@
const inputDecreaseFloorNum = document.getElementById("decreaseFloorNum");
inputDecreaseFloorNum.onclick = decreaseFloorNum;
const inputIncreaseFloorNum = document.getElementById("increaseFloorNum");
inputIncreaseFloorNum.onclick = increaseFloorNum;
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 selectSavicSuiteRoyaleXL = document.getElementById("SavicSuiteRoyaleXL");
const selectSavicSuiteRoyale95Double = document.getElementById("SavicSuiteRoyale95Double");
const selectTiakiKleintierkäfigEtagere = document.getElementById("TiakiKleintierkäfigEtagere");
const cardSavicSuiteRoyaleXL = document.getElementById("card-SavicSuiteRoyaleXL");
const cardSavicSuiteRoyale95Double = document.getElementById("card-SavicSuiteRoyale95Double");
const cardTiakiKleintierkäfigEtagere = document.getElementById("card-TiakiKleintierkäfigEtagere");
function markActiveCage(cageName) {
cardSavicSuiteRoyaleXL.classList.remove("card-active");
cardSavicSuiteRoyale95Double.classList.remove("card-active");
cardTiakiKleintierkäfigEtagere.classList.remove("card-active");
if (cageName != "") {
const activeCage = document.getElementById("card-" + cageName);
activeCage.classList.add("card-active")
}
}
function updateCage(event) {
selectSavicSuiteRoyaleXL.checked = false;
selectSavicSuiteRoyale95Double.checked = false;
selectTiakiKleintierkäfigEtagere.checked = false;
const selectedCage = event.currentTarget
selectedCage.checked = true;
const cageName = selectedCage.id;
console.log(cageName);
var dim = getCageDimensions(cageName);
inputWidth.value = dim.width;
inputDepth.value = dim.depth;
inputHeight.value = dim.height;
markActiveCage(cageName);
update();
}
selectSavicSuiteRoyaleXL.onchange = updateCage;
selectSavicSuiteRoyale95Double.onchange = updateCage;
selectTiakiKleintierkäfigEtagere.onchange = updateCage;
var labelNumRats = document.getElementById("labelNumRats");
var ratSlider = document.getElementById("numRats");
labelNumRats.innerHTML = `Anzahl an Ratten: ` + ratSlider.value;
ratSlider.oninput = function () {
labelNumRats.innerHTML = `Anzahl an Ratten: ` + this.value;
update();
}
// Full floor functions
var fullFloorNum = document.getElementById("numFullFloors");
function getCageDimensions(cageName) {
console.log(cageName);
if (cageName == "SavicSuiteRoyaleXL") {
return new Dimensions(115, 67.5, 153);
}
if (cageName == "SavicSuiteRoyale95Double") {
return new Dimensions(95, 63, 120);
}
if (cageName == "TiakiKleintierkäfigEtagere") {
return new Dimensions(93.5, 63, 141.2);
}
}
function getResultFromChecks(checks) {
console.log(checks.length)
if (Object.keys(checks).length > 0) {
const ul = document.createElement('ul');
for (const key in checks) {
const li = document.createElement('li');
li.textContent = `` + checks[key];
ul.appendChild(li);
}
return ul;
} else {
const p = document.createElement('p');
p.innerHTML = "✅ Der Käfig erfüllt alle Kriterien!"
return p;
}
}
function updateViaManualMeasurements() {
markActiveCage("ManualMeasurements");
update();
}
function update() {
const width = inputWidth.value
const depth = inputDepth.value
const height = inputHeight.value
const dimensions = new Dimensions(width / 100, depth / 100, height / 100);
const failed_checks = cageCheck(dimensions, ratSlider.value, fullFloorNum.value);
console.log(failed_checks);
let resultsDiv = document.getElementById("resultsDiv");
resultsDiv.innerHTML = `<strong>Ergebnis</strong>`;
const result = getResultFromChecks(failed_checks);
resultsDiv.appendChild(result);
}
function decreaseFloorNum() {
var input = document.getElementById('numFullFloors');
var value = parseInt(input.value);
if (value > 0) {
input.value = value - 1;
}
update();
}
function increaseFloorNum() {
var input = document.getElementById('numFullFloors');
var value = parseInt(input.value);
input.value = value + 1;
update();
}
update();
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import HttpApi from "i18next-http-backend";
import '@fortawesome/fontawesome-free/js/fontawesome'
import '@fortawesome/fontawesome-free/js/solid'
import '@fortawesome/fontawesome-free/js/regular'
import '@fortawesome/fontawesome-free/js/brands'
import './main.scss';
/////////////////
// TRANSLATION //
/////////////////
async function initI18next() {
// We use() the backend and await it to load
// the translations from the network
await i18next
.use(HttpApi)
.use(LanguageDetector)
.init({
lng: "en",
supportedLngs: ["en", "de"],
nonExplicitSupportedLngs: true,
fallbackLng: "en",
debug: true,
backend: {
loadPath: "/lang/{{lng}}.json",
loadPath: "/i18n/{{lng}}.json",
},
});
}
function translatePageElements() {
// Translate content inside a tag
const translatableElements = document.querySelectorAll(
"[data-i18n-key]",
);
@@ -173,9 +35,16 @@ function translatePageElements() {
const key = el.getAttribute("data-i18n-key");
el.innerHTML = i18next.t(key);
});
// Translate alt texts
const translatableAltTexts = document.querySelectorAll(
"[alt-i18n-key]",
);
translatableAltTexts.forEach((el) => {
const translation_key = el.getAttribute("alt-i18n-key");
el.setAttribute("alt", i18next.t(translation_key));
});
}
// ...
function bindLocaleSwitcher(initialValue) {
const switcher = document.querySelector(
@@ -185,7 +54,8 @@ function bindLocaleSwitcher(initialValue) {
switcher.onchange = (e) => {
i18next
.changeLanguage(e.target.value)
.then(translatePageElements);
.then(translatePageElements)
.then(update);
};
}
@@ -193,4 +63,212 @@ function bindLocaleSwitcher(initialValue) {
await initI18next();
translatePageElements();
bindLocaleSwitcher(i18next.resolvedLanguage);
})();
update();
})();
//////////
// TABS //
//////////
function initTabs(tabs, tabContent, activationClass) {
tabs.forEach((tab) => {
tab.addEventListener('click', (e) => {
let selectedTabID = tab.getAttribute('data-tab-id');
updateActiveTab(tabs, tab);
updateActiveContent(tabContent, selectedTabID);
})
})
}
function updateActiveTab(tabs, selectedTabID) {
tabs.forEach((tab) => {
if (tab && tab.classList.contains(ACTIVATION_CLASS)) {
tab.classList.remove(ACTIVATION_CLASS);
}
});
selectedTabID.classList.add(ACTIVATION_CLASS);
}
function updateActiveContent(tabsContent, selectedTabID) {
tabsContent.forEach((item) => {
if (item && item.classList.contains(ACTIVATION_CLASS)) {
item.classList.remove(ACTIVATION_CLASS);
}
let data = item.getAttribute('data-content-id');
if (data === selectedTabID) {
item.classList.add(ACTIVATION_CLASS);
}
});
}
const primaryTabs = [...document.querySelectorAll('#primary-tabs li')];
const primaryTabsContent = [...document.querySelectorAll('#tab-content div')];
const ACTIVATION_CLASS = 'is-active';
initTabs(primaryTabs, primaryTabsContent, ACTIVATION_CLASS);
////////////////
// CALCULATOR //
////////////////
const MINIMUM_BASE_AREA = 0.5;
const MINIMUM_AREA_THREE_RATS = 1.5;
const AREA_PER_ADDITIONAL_RAT = 0.25;
const MAXIMUM_FALL_HEIGHT = 0.5;
const MINIMUM_LENGTH_LONG_SIDE = 0.8;
const MINIMUM_LENGTH_SHORT_SIDE = 0.5;
const MINIMUM_FLOOR_HEIGHT = 0.25;
const FAILED_BASE_AREA = "base_area";
const FAILED_OVERALL_AREA = "overall_area";
const FAILED_FALL_HEIGHT = "fall_height";
const FAILED_NUM_RATS = "num_rats";
const FAILED_MINIMUM_LENGTH_LONG_SIDE = "length_long_side";
const FAILED_MINIMUM_LENGTH_SHORT_SIDE = "length_short_side";
const FAILED_FLOOR_HEIGHT = "floor_height"
class Validator {
constructor() {
this.FAIL_CRITERIA = {
[FAILED_BASE_AREA]: i18next.t('failed-base-area', {"MINIMUM_BASE_AREA": MINIMUM_BASE_AREA}),
[FAILED_OVERALL_AREA]: i18next.t("failed-overall-area"),
[FAILED_FALL_HEIGHT]: i18next.t("failed-fall-height", {"maximum_fall_height": (MAXIMUM_FALL_HEIGHT * 100).toFixed(0)}),
[FAILED_FLOOR_HEIGHT]: i18next.t("failed-floor-height", {"minimum_floor_height": (MINIMUM_FLOOR_HEIGHT * 100).toFixed(0)}),
[FAILED_NUM_RATS]: i18next.t("failed-num-rats"),
[FAILED_MINIMUM_LENGTH_LONG_SIDE]: i18next.t("failed-minimum-length-long-side", {"minimum_length_long_side": (MINIMUM_LENGTH_LONG_SIDE * 100).toFixed(0)}),
[FAILED_MINIMUM_LENGTH_SHORT_SIDE]: i18next.t("failed-minimum-length-short-side", {"minimum_length_short_side": (MINIMUM_LENGTH_SHORT_SIDE * 100).toFixed(0)}),
};
}
overallAreaNeeded(numOfRats) {
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;
}
cageCheck(dimensions, numRats, numFullFloors) {
let failedCriteria = {};
if (numRats < 2 || numRats > 15) {
failedCriteria[FAILED_NUM_RATS] = this.FAIL_CRITERIA[FAILED_NUM_RATS];
}
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];
}
if (dimensions.width < MINIMUM_LENGTH_LONG_SIDE && dimensions.depth < MINIMUM_LENGTH_LONG_SIDE) {
failedCriteria[FAILED_MINIMUM_LENGTH_LONG_SIDE] = this.FAIL_CRITERIA[FAILED_MINIMUM_LENGTH_LONG_SIDE];
}
if (dimensions.width < MINIMUM_LENGTH_SHORT_SIDE || dimensions.depth < MINIMUM_LENGTH_SHORT_SIDE) {
failedCriteria[FAILED_MINIMUM_LENGTH_SHORT_SIDE] = this.FAIL_CRITERIA[FAILED_MINIMUM_LENGTH_SHORT_SIDE];
}
if (dimensions.height / numFullFloors < MINIMUM_FLOOR_HEIGHT) {
failedCriteria[FAILED_FLOOR_HEIGHT] = this.FAIL_CRITERIA[FAILED_FLOOR_HEIGHT];
}
return failedCriteria;
}
}
class Dimensions {
constructor(width, depth, height) {
this.width = width;
this.depth = depth;
this.height = height;
}
toString() {
return `${this.width}x${this.depth}x${this.height}`;
}
static fromDict(data) {
const {width, depth, height} = data;
return new Dimensions(width, depth, height);
}
}
//////////////////////////
// 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;
let labelNumRats = document.getElementById("labelNumRats");
let ratSlider = document.getElementById("numRats");
ratSlider.oninput = function () {
update();
}
// Full floor functions
let fullFloorNum = document.getElementById("numFullFloors");
fullFloorNum.oninput = function () {
update();
}
function getResultFromChecks(checks) {
if (Object.keys(checks).length > 0) {
const ul = document.createElement('ul');
for (const key in checks) {
const li = document.createElement('li');
li.textContent = `` + checks[key];
ul.appendChild(li);
}
return ul;
} else {
const p = document.createElement('p');
p.innerHTML = "✅ " + i18next.t("cage-complies-with-all-criteria")
return p;
}
}
function updateViaManualMeasurements() {
update();
}
function update() {
labelNumRats.innerHTML = i18next.t("cage-for-x-rats", {"num_rats": ratSlider.value});
const width = inputWidth.value
const depth = inputDepth.value
const height = inputHeight.value
const dimensions = new Dimensions(width / 100, depth / 100, height / 100);
const validator = new Validator();
const failed_checks = validator.cageCheck(dimensions, ratSlider.value, fullFloorNum.value);
let resultsDiv = document.getElementById("resultsDiv");
const result = getResultFromChecks(failed_checks);
resultsDiv.innerHTML = "";
resultsDiv.appendChild(result);
}

57
src/main.scss Normal file
View File

@@ -0,0 +1,57 @@
// Set your brand colors
$purple: #8a4d76;
$pink: #7c95fa;
$brown: #757763;
$beige-light: #d0d1cd;
$beige-lighter: #eff0eb;
// Path to Bulma's sass folder
@use "bulma/sass" with (
$family-primary: '"Nunito", sans-serif',
$grey-dark: $brown,
$grey-light: $beige-light,
$primary: $purple,
$link: $pink,
$control-border-width: 2px,
$input-shadow: none
);
#tab-content > div {
display: none;
}
#tab-content > div.is-active {
display: block;
}
// Import the Google Font
@import url("https://fonts.googleapis.com/css?family=Nunito:400,700");
.tooltip {
position: relative;
cursor: help;
&::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.75);
color: white;
padding: 0.5em;
border-radius: 4px;
white-space: nowrap;
font-size: 0.75rem;
line-height: 1.2;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
z-index: 1000;
}
&:hover::after {
opacity: 1;
}
}

View File

@@ -22,4 +22,16 @@ module.exports = {
},
static: "public",
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
'style-loader', // Injects styles into DOM
'css-loader', // Resolves CSS imports
'sass-loader' // Compiles Sass to CSS
],
},
],
},
};