Compare commits

...

46 Commits

Author SHA1 Message Date
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
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
a886d6575f ci: Add npm bulid
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-31 15:05:42 +02:00
8124b08b88 feat: Exclude build JS files from git 2024-07-31 14:49:29 +02:00
e80902e965 refactor: move js files to dedicated folder 2024-07-31 14:48:59 +02:00
c078eaa6c5 feat: Add basic translation framework
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-30 20:02:37 +02:00
32 changed files with 7429 additions and 344 deletions

4
.gitignore vendored
View File

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

View File

@@ -3,7 +3,11 @@
steps:
build:
image: node:latest
commands:
- npm install
- npm run build
deploy:
image: appleboy/drone-scp
settings:
@@ -14,6 +18,6 @@ steps:
from_secret: ssh_user
target:
from_secret: path
source: src/
source: public/
key:
from_secret: ssh_key

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

11
i18next-parser.config.js Normal file
View File

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

6524
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "kaefigrechner",
"version": "0.0.1",
"description": "Ein Rechner für die minimale Größe eines Rattenkäfigs nach Standard des VdRD e.V.",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --config webpack.config.js",
"ex-trans": "i18next -c i18next-parser.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",
"url": "https://git.hyteck.de/moanos/RattenheimRechner"
},
"keywords": [
"animal",
"welfare"
],
"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",
"sass-migrator": "^2.3.1",
"@fortawesome/fontawesome-free": "^6.7.2"
}
}

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

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

After

Width:  |  Height:  |  Size: 834 B

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 639 B

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

@@ -0,0 +1,29 @@
{
"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",
"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."
}

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

@@ -0,0 +1,29 @@
{
"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",
"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."
}

235
public/index.html Normal file
View File

@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<head>
<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">
</head>
<body>
<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="content">
<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 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"
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>
<!--- 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="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>
<script src="./bundle.js"></script>
<div class="footer">
<div class="language-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>
</div>
</div>
</body>
</html>

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;
}

View File

@@ -1,252 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Käfigrechner</title>
<link rel="stylesheet" href="assets/css/style.css">
<script src="assets/calculator.js"></script>
<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>
<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>
<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>
<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>
</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 onclick="decreaseFloorNum()">-</button>
<input type="text" id="numFullFloors" value="3" readonly>
<button onclick="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>
<script>
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();
</script>
</body>
</html>

408
src/index.js Normal file
View File

@@ -0,0 +1,408 @@
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() {
await i18next
.use(HttpApi)
.use(LanguageDetector)
.init({
supportedLngs: ["en", "de"],
nonExplicitSupportedLngs: true,
fallbackLng: "en",
debug: true,
backend: {
loadPath: "/i18n/{{lng}}.json",
},
});
}
function translatePageElements() {
// Translate content inside a tag
const translatableElements = document.querySelectorAll(
"[data-i18n]",
);
translatableElements.forEach((el) => {
const key = el.getAttribute("data-i18n");
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(
"[data-i18n-switcher]",
);
switcher.value = initialValue;
switcher.onchange = (e) => {
i18next
.changeLanguage(e.target.value)
.then(translatePageElements)
.then(updateCageCheck);
};
}
(async function () {
await initI18next();
translatePageElements();
bindLocaleSwitcher(i18next.resolvedLanguage);
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 //
////////////////
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 = "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 {
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)}),
};
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.0) * AREA_PER_ADDITIONAL_RAT;
}
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 = {};
const baseArea = dimensions.depth * dimensions.width;
if (baseArea < MINIMUM_BASE_AREA) {
failedCriteria[FAILED_BASE_AREA] = this.FAIL_CRITERIA[FAILED_BASE_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;
}
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;
}
}
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 //
//////////////////////////
///////
// 1 //
///////
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 = 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");
let ratSlider = document.getElementById("numRats");
ratSlider.oninput = function () {
updateCageCheck();
}
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 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 updateCageCheck() {
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);
}
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 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();

64
src/main.scss Normal file
View File

@@ -0,0 +1,64 @@
// 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;
}
// 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;
}
}

37
webpack.config.js Normal file
View File

@@ -0,0 +1,37 @@
const path = require("path");
const port = 3000;
const openBrowser = true;
module.exports = {
entry: {
app: ["./src/index.js"],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "public"),
publicPath: "/",
},
mode: "development",
devtool: "source-map",
devServer: {
port: port,
open: openBrowser,
historyApiFallback: {
index: "index.html",
},
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
],
},
],
},
};