Compare commits

...

26 Commits

Author SHA1 Message Date
e17cfb384c feat: restrict logo size 2025-04-15 22:19:51 +02:00
e83245e25b feat: Add logo to footer 2025-04-15 22:02:51 +02:00
041e391692 feat. Expand footer 2025-04-15 20:59:31 +02:00
6a0b6d3f72 fix: Add labels and about section 2025-04-15 17:26:35 +02:00
2470005892 refactor: comments + print 2025-04-14 20:24:17 +02:00
b4c90af4f3 fix: use label class correctly 2025-04-14 20:21:23 +02:00
9f716e68a5 feat: Add cage calc that shows requirements for a number of rats 2025-04-14 19:00:49 +02:00
884840eb6e feat: reset bulma colors 2025-04-13 22:21:55 +02:00
e743608539 fix: Use real FA icon 2025-04-13 22:21:38 +02:00
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
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
16 changed files with 3644 additions and 2020 deletions

2
.gitignore vendored
View File

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

View File

@@ -4,6 +4,12 @@
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

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', ],
}

4785
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",
"css-loader": "^7.1.2",
"i18next-parser": "^9.3.0",
"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",
"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",
"@fortawesome/fontawesome-free": "^6.7.2"
}
}

View File

@@ -145,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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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

@@ -0,0 +1,38 @@
{
"app-name": "Käfigrechner",
"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": "Vollebenen",
"result": "Ergebnis",
"number-of-rats": "Anzahl an Ratten",
"change-language": "Sprache ändern",
"information-on-rat-husbandry": "Information",
"basic-rat-info": "Basiswissen Ratten",
"cage": "Käfig",
"food": "Ernährung",
"adopting-rats": "Ratten adoptieren",
"about": "Über uns",
"the-vdrd": "VdRD e.V.",
"imprint": "Impressum",
"source-code": "Quellcode",
"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.",
"base-area": "Die Mindestgrundfläche des Käfigs muss {{ MINIMUM_BASE_AREA }}m² betragen.",
"fall-height": "Die mögliche Fallhöhe darf nicht mehr als {{ maximum_fall_height }}cm betragen.",
"floor-height": "Der Mindestabstand zwischen Ebenen muss {{ minimum_floor_height }}cm betragen.",
"minimum-length-long-side": "Die lange Seite des Käfig muss mindestens {{ minimum_length_long_side }}cm lang sein um Rennen zu ermöglichen.",
"minimum-length-short-side": "Die kurze Seite des Käfig muss mindestens {{ minimum_length_short_side }}cm lang sein.",
"cage-complies-with-all-criteria": "Der Käfig erfüllt alle Kriterien!",
"cage-for-x-rats": "Käfig für {{ num_rats }} Ratten",
"overall-area": "Die Gesamtfläche für {{ numRats }} Ratten muss mindestens {{ minimumOverallArea }}m² betragen."
}

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

@@ -0,0 +1,38 @@
{
"app-name": "Cage Calculator",
"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": "Full floors",
"result": "Result",
"number-of-rats": "Number of Rats",
"change-language": "Change language",
"information-on-rat-husbandry": "Information",
"basic-rat-info": "Basic Rat Information",
"cage": "Cage",
"food": "Food",
"adopting-rats": "Adopting Rats",
"about": "About",
"the-vdrd": "VdRD r.V.",
"imprint": "Imprint",
"source-code": "Source Code",
"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.",
"base-area": "The base area of the cage must not be below {{ MINIMUM_BASE_AREA }}m².",
"fall-height": "The possible fall height between floors must not be above {{ maximum_fall_height }}cm.",
"floor-height": "The height between floors must be above {{ minimum_floor_height }}cm.",
"minimum-length-long-side": "The long side of the cage must be at least {{ minimum_length_long_side }}cm long to enable running.",
"minimum-length-short-side": "The short side of the cage must be at least {{ minimum_length_short_side }}cm.",
"cage-complies-with-all-criteria": "This cage complies with all criteria!",
"cage-for-x-rats": "Cage for {{ num_rats }} rats",
"overall-area": "The overall area in the cage must be above {{ minimumOverallArea }}m² for {{ numRats }} rats."
}

View File

@@ -3,97 +3,94 @@
<head>
<meta charset="UTF-8">
<title>Käfigrechner</title>
<link rel="stylesheet" href="assets/css/style.css">
<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">
</head>
<body>
<div class="top-navigation">
<div class="homebutton">
<a href="https://vdrd.de">
<b data-i18n-key="back-to-home">zurück zur Homepage</b>
</a>
<div class="navbar">
<div class="navbar-brand">
<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="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>
<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 alt-i18n-key="alt-savic-xl" src="assets/img/savic-xl.jpeg">
</div>
<div class="info-container">
<h4><b>Savic Suite Royale XL</b></h4>
</div>
</label>
</div>
<div class="card" id="card-SavicSuiteRoyale95Double">
<label for="SavicSuiteRoyale95Double">
<input type="checkbox" id="SavicSuiteRoyale95Double"/>
<div class="card-photo">
<div>
<img alt-i18n-key="alt-savic-95-double" 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 alt-i18n-key="alt-tiaki" 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 data-i18n-key="cage-measurements" for="form-cage-measurements">Käfigmaße</label>
<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">
</div>
<div class="input-measurement">
<label for="depth" data-i18n-key="depth-cm">Tiefe (cm)</label>
<input class="measurement" 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">
</div>
</form>
</div>
<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-hashtag" aria-hidden="true"></i>
</span>
<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">
<a>
<span class="icon is-small">
<i class="fas fa-question" aria-hidden="true"></i>
</span>
<p data-i18n="what-cage-should-i-get">What cage should I get?</p>
</a>
</li>
<li data-tab-id="3">
<a>
<span class="icon is-small">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
<p data-i18n="check-existing-cage">Check existing cage</p>
</a>
</li>
</ul>
</div>
</div>
<div class="container-inputs">
<div class="input-element">
<label for="numFullFloors" data-i18n-key="full-floors">Vollebenen</label>
<div class="tooltip">
<div id="tab-content">
<!--- Check how many rats fit --->
<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"
@@ -104,28 +101,168 @@
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>
</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>
<!--- Cage calc --->
<div class="" data-content-id="2">
<div class="card">
<div class="card-content">
<h2 class="title is-4">
<label data-i18n="number-of-rats" class="label" for="cage-calc">Anzahl der Ratten</label>
</h2>
<form id="cage-calc">
<div class="column">
<label id="cageCalcLabelNumRats" for="cageCalcNumRats" class="label"></label>
<input type="range" min="3" max="15" value="4" class="slider" id="cageCalcNumRats">
<img class="inline-icon" src="assets/img/logo_transparent.png" alt="Kleine Ratte">
</div>
</form>
</div>
<div class="card-footer is-fullwidth">
<div class="card result-card" id="cage-calc-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="cageCalcResultsDiv"></div>
</div>
</div>
</div>
</div>
</div>
<!--- Check cage --->
<div class="" data-content-id="3">
<div class="card">
<div class="card-content">
<h2 class="title is-4"><label class="label" data-i18n="cage-measurements"
for="form-cage-measurements">Käfigmaße</label></h2>
<form id="form-cage-measurements" class="form-measurements">
<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="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="input-element">
<div class="slidecontainer">
<label for="numRats" id="labelNumRats"></label>
<div class="column">
<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>
</div>
</form>
</div>
</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>
<div class="container output-element">
<strong data-i18n-key="result">Ergebnis</strong>
<div id="resultsDiv"></div>
<div class="footer" aria-label="Footer">
<div class="columns">
<div class="column">
<div class="block">
<img class="footer-logo" src="assets/img/logo_ausgeschrieben.png"
alt="Logo Verein der Rattenliebhaber und -halter Deutschland e.v.">
</div>
<div class="block">
<div class="language-switcher">
<label for="locale-switcher" class="footer-title title label" data-i18n="change-language">Sprache
ändern</label>
<select data-i18n-switcher class="select" id="locale-switcher">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</div>
</div>
</div>
<div class="column">
<div class="block">
<h3 class="footer-title title" data-i18n="information-on-rat-husbandry">Informationen zur
Rattenhaltung</h3>
<ul class="footer-links">
<li class="footer-link"><a href="https://vdrd.de/vor-der-anschaffung/" data-i18n="basic-rat-info">Grundwissen
Rattenhaltung</a></li>
<li class="footer-link"><a href="https://vdrd.de/das-rattenheim/" data-i18n="cage">Käfig</a></li>
<li class="footer-link"><a href="https://vdrd.de/ernaehrung/" data-i18n="food">Ernährung</a></li>
<li class="footer-link"><a href="https://vdrd.de/anzahl-geschlecht-und-bezugsquelle/"
data-i18n="adopting-rats">Ratten adoptieren</a>
</li>
</ul>
</div>
</div>
<div class="column">
<div class="block">
<h3 class="footer-title title" data-i18n="about">Über uns</h3>
<ul class="footer-links">
<li class="footer-link"><a href="https://vdrd.de/" data-i18n="the-vdrd">Der VdRD e.V.</a></li>
<li class="footer-link"><a href="https://vdrd.de/impressum" data-i18n="imprint">Impressum</a></li>
<li class="footer-link"><a href="https://codeberg.org/moanos/rettenrechner" data-i18n="source-code">Quellcode</a>
</li>
</ul>
</div>
</div>
</div>
</div>

View File

@@ -1,25 +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.",
"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"
}

View File

@@ -1,25 +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 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

@@ -1,6 +1,11 @@
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 //
@@ -16,7 +21,7 @@ async function initI18next() {
fallbackLng: "en",
debug: true,
backend: {
loadPath: "/lang/{{lng}}.json",
loadPath: "/i18n/{{lng}}.json",
},
});
}
@@ -24,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
@@ -50,7 +55,7 @@ function bindLocaleSwitcher(initialValue) {
i18next
.changeLanguage(e.target.value)
.then(translatePageElements)
.then(update);
.then(updateCageCheck);
};
}
@@ -58,9 +63,52 @@ function bindLocaleSwitcher(initialValue) {
await initI18next();
translatePageElements();
bindLocaleSwitcher(i18next.resolvedLanguage);
update();
updateCageCheck();
})();
//////////
// 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 //
////////////////
@@ -73,13 +121,21 @@ 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"
const FAILED_BASE_AREA = "failed_base_area";
const FAILED_OVERALL_AREA = "failed_overall_area";
const FAILED_FALL_HEIGHT = "failed_fall_height";
const FAILED_NUM_RATS = "failed_num_rats";
const FAILED_MINIMUM_LENGTH_LONG_SIDE = "failed_length_long_side";
const FAILED_MINIMUM_LENGTH_SHORT_SIDE = "failed_length_short_side";
const FAILED_FLOOR_HEIGHT = "failed_floor_height"
const CRITERIA_BASE_AREA = "base_area";
const CRITERIA_FALL_HEIGHT = "fall_height";
const CRITERIA_MINIMUM_LENGTH_LONG_SIDE = "length_long_side";
const CRITERIA_MINIMUM_LENGTH_SHORT_SIDE = "length_short_side";
const CRITERIA_FLOOR_HEIGHT = "floor_height"
const CRITERIA_OVERALL_AREA = "overall_area"
class Validator {
@@ -93,32 +149,44 @@ class Validator {
[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)}),
};
this.STATIC_CRITERIA = {
[CRITERIA_BASE_AREA]: i18next.t('base-area', {"MINIMUM_BASE_AREA": MINIMUM_BASE_AREA}),
[CRITERIA_FALL_HEIGHT]: i18next.t("fall-height", {"maximum_fall_height": (MAXIMUM_FALL_HEIGHT * 100).toFixed(0)}),
[CRITERIA_FLOOR_HEIGHT]: i18next.t("floor-height", {"minimum_floor_height": (MINIMUM_FLOOR_HEIGHT * 100).toFixed(0)}),
[CRITERIA_MINIMUM_LENGTH_LONG_SIDE]: i18next.t("minimum-length-long-side", {"minimum_length_long_side": (MINIMUM_LENGTH_LONG_SIDE * 100).toFixed(0)}),
[CRITERIA_MINIMUM_LENGTH_SHORT_SIDE]: i18next.t("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;
return MINIMUM_AREA_THREE_RATS + (numOfRats - 3.0) * AREA_PER_ADDITIONAL_RAT;
}
cageCheck(dimensions, numRats, numFullFloors) {
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;
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 = {};
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];
}
@@ -138,6 +206,29 @@ 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;
}
}
@@ -164,87 +255,61 @@ class Dimensions {
// DOCUMENT INTERACTION //
//////////////////////////
const inputDecreaseFloorNum = document.getElementById("decreaseFloorNum");
inputDecreaseFloorNum.onclick = decreaseFloorNum;
///////
// 1 //
///////
const inputIncreaseFloorNum = document.getElementById("increaseFloorNum");
inputIncreaseFloorNum.onclick = increaseFloorNum;
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();
}
///////
// 2 //
///////
let cageCalcLabelNumRats = document.getElementById("cageCalcLabelNumRats");
let cageCalcRatSlider = document.getElementById("cageCalcNumRats");
cageCalcRatSlider.oninput = function () {
updateCageCalc();
}
///////
// 3 //
///////
const inputWidth = document.getElementById("width");
inputWidth.onchange = updateViaManualMeasurements;
inputWidth.onchange = updateCageCheck;
const inputDepth = document.getElementById("depth");
inputDepth.onchange = updateViaManualMeasurements;
inputDepth.onchange = updateCageCheck;
const inputHeight = document.getElementById("height");
inputHeight.onchange = updateViaManualMeasurements;
inputHeight.onchange = updateCageCheck;
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")
let fullFloorNum = document.getElementById("numFullFloors");
fullFloorNum.oninput = function () {
updateCageCheck();
}
}
function updateCage(event) {
selectSavicSuiteRoyaleXL.checked = false;
selectSavicSuiteRoyale95Double.checked = false;
selectTiakiKleintierkäfigEtagere.checked = false;
const selectedCage = event.currentTarget
selectedCage.checked = true;
const cageName = selectedCage.id;
let labelNumRats = document.getElementById("labelNumRats");
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");
let ratSlider = document.getElementById("numRats");
ratSlider.oninput = function () {
update();
updateCageCheck();
}
// Full floor functions
var fullFloorNum = document.getElementById("numFullFloors");
function getCageDimensions(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) {
if (Object.keys(checks).length > 0) {
@@ -263,12 +328,19 @@ function getResultFromChecks(checks) {
}
function updateViaManualMeasurements() {
markActiveCage("ManualMeasurements");
update();
function formatCriteria(criteria) {
const ul = document.createElement('ul');
for (const key in criteria) {
const li = document.createElement('li');
li.textContent = `☑️ ` + criteria[key];
ul.appendChild(li);
}
return ul;
}
function update() {
function updateCageCheck() {
labelNumRats.innerHTML = i18next.t("cage-for-x-rats", {"num_rats": ratSlider.value});
const width = inputWidth.value
@@ -285,19 +357,52 @@ function update() {
resultsDiv.appendChild(result);
}
function updateCageCalc() {
let numRats = cageCalcRatSlider.value
cageCalcLabelNumRats.innerHTML = i18next.t("cage-for-x-rats", {"num_rats": numRats});
const validator = new Validator();
let criteria = validator.STATIC_CRITERIA;
let minimumOverallArea = validator.overallAreaNeeded(cageCalcRatSlider.value);
criteria[CRITERIA_OVERALL_AREA] = i18next.t('overall-area', {"numRats": numRats, "minimumOverallArea": minimumOverallArea});
let resultsDiv = document.getElementById("cageCalcResultsDiv");
const result = formatCriteria(criteria);
resultsDiv.innerHTML = "";
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();
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;
}
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();

68
src/main.scss Normal file
View File

@@ -0,0 +1,68 @@
// 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',
$control-border-width: 2px,
$input-shadow: none
);
#tab-content > div {
display: none;
}
#tab-content > div.is-active {
display: block;
}
body {
padding: 5px;
}
.result-card {
width: 100%;
}
.inline-icon {
height: 1.5rem;
}
.footer-logo {
height: 5rem;
}
// 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
],
},
],
},
};