Compare commits
35 Commits
7167d275b6
...
translatio
Author | SHA1 | Date | |
---|---|---|---|
a886d6575f | |||
8124b08b88 | |||
e80902e965 | |||
c078eaa6c5 | |||
75a6df9d9d | |||
9dfc567d5c | |||
093d0c0997 | |||
c86ed21c1e | |||
9ce58c9860 | |||
355c3055cb | |||
a5c3cacef9 | |||
40db2b814b | |||
3c689015ac | |||
8a8c24d7ac | |||
07f999aeb2 | |||
6012d48197 | |||
90f08bf454 | |||
aafa70fc1f | |||
e3e065af16 | |||
9341c58dba | |||
ecd7027bdd | |||
75c814c057 | |||
1d41b0cfc7 | |||
04dc1e6b90 | |||
6b964c81a8 | |||
fb83a94d4b | |||
b17f21a04f | |||
da5dc1da3b | |||
b9282962f9 | |||
4d690e2a2a | |||
143dc31c0e | |||
f2138425b9 | |||
3b4cbd8ae1 | |||
583b0fb49f | |||
9e3c89f94d |
2
.gitignore
vendored
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
public/bundle.js*
|
||||
|
@@ -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
|
10
i18next-parser.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
defaultNamespace: 'translation',
|
||||
lexers: {
|
||||
js: ['JsxLexer'], // we're writing jsx inside .js files
|
||||
default: ['JavascriptLexer'],
|
||||
},
|
||||
locales: ['en', 'de'],
|
||||
output: 'public/lang/$LOCALE.json',
|
||||
input: [ 'src/*.js', 'public/*.html', ],
|
||||
}
|
5269
package-lock.json
generated
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"webpack": "^5.93.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.5.2"
|
||||
}
|
||||
}
|
@@ -20,9 +20,13 @@
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--background-one);
|
||||
color: var(--text-two);
|
||||
}
|
||||
.content {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
word-wrap: break-word;
|
||||
@@ -30,6 +34,16 @@ h1, h2 {
|
||||
text-shadow: 2px 2px var(--shadow-one);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.container-form {
|
||||
background: var(--background-three);
|
||||
color: var(--text-two);
|
||||
@@ -47,7 +61,7 @@ h1, h2 {
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.slidecontainer {
|
||||
@@ -76,7 +90,7 @@ label {
|
||||
width: 23px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
background: url('/src/assets/img/logo_transparent.png');
|
||||
background: url('../img/logo_transparent.png');
|
||||
background-size: 100% 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -85,13 +99,12 @@ label {
|
||||
width: 23px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
background: url('/src/assets/img/logo_transparent.png');
|
||||
background: url('../img/logo_transparent.png');
|
||||
background-size: 100% 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -113,4 +126,109 @@ label {
|
||||
border: 1px solid #ccc;
|
||||
background-color: var(--secondary-light-one);
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: 0.3s;
|
||||
margin: 0.4em;
|
||||
min-width: 300px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
flex: 0 1 calc(25% - 0.5em);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-active {
|
||||
border: 3px solid var(--highlight-two);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
|
||||
.measurement {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.cage-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.form-measurements {
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.form-measurements input {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
input.measurement {
|
||||
margin-top: 5%;
|
||||
}
|
||||
|
||||
.container-inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navigation-sticky {
|
||||
background-color: var(--secondary-light-one);
|
||||
color: var(--primary-light-one);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
border-bottom-right-radius: 8px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
width: 200px;
|
||||
background-color: var(--primary-dark-one);
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
BIN
public/assets/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/assets/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
public/assets/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 432 B |
BIN
public/assets/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
public/assets/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
public/assets/favicon/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
BIN
public/assets/img/checked.png
Normal file
After Width: | Height: | Size: 11 KiB |
1
public/assets/img/info.svg
Normal 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 |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 546 B |
BIN
public/assets/img/savic-95-double.jpg
Normal file
After Width: | Height: | Size: 3.2 MiB |
BIN
public/assets/img/savic-xl.jpeg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/assets/img/tiaki.jpeg
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
public/assets/img/unchecked.png
Normal file
After Width: | Height: | Size: 639 B |
85
public/assets/js/calculator.js
Normal file
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
}
|
121
public/index.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<!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/js/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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
|
||||
<script src="./bundle.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
10
public/lang/de.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"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."
|
||||
}
|
10
public/lang/en.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"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."
|
||||
}
|
@@ -1,89 +0,0 @@
|
||||
const MINIMUM_BASE_AREA = 0.5;
|
||||
const MINIMUM_AREA_THREE_RATS = 1.8;
|
||||
const AREA_PER_ADDITIONAL_RAT = 0.2;
|
||||
const MAXIMUM_FALL_HEIGHT = 0.5;
|
||||
const MINIMUM_LENGTH = 0.8;
|
||||
|
||||
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_LENGTH = "length";
|
||||
|
||||
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(3)}cm betragen.`,
|
||||
[FAILED_NUM_RATS]: "Es müssen mindestens 3 Ratten zusammenleben, Paarhaltung ist nicht artgerecht.",
|
||||
[FAILED_LENGTH]: `Eine Seite des Käfig muss mindestens ${(MINIMUM_LENGTH * 100).toFixed(3)}cm lang sein um Rennen zu ermöglichen.`,
|
||||
};
|
||||
|
||||
class Dimensions {
|
||||
constructor(length, width, height) {
|
||||
this.length = length;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.length}x${this.width}x${this.height}`;
|
||||
}
|
||||
|
||||
static fromDict(data) {
|
||||
const { length, width, height } = data;
|
||||
return new Dimensions(length, width, 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.length * 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 && dimensions.length < MINIMUM_LENGTH) {
|
||||
failedCriteria[FAILED_LENGTH] = FAIL_CRITERIA[FAILED_LENGTH];
|
||||
}
|
||||
|
||||
return failedCriteria;
|
||||
}
|
||||
|
||||
const expectedAreaPerNumRats = { 3: 1.8, 4: 2.0 };
|
||||
for (let numRats in expectedAreaPerNumRats) {
|
||||
console.log(`Area needed for ${numRats}: Calculated=${overallAreaNeeded(parseInt(numRats))}`);
|
||||
}
|
||||
|
||||
const exampleCageChecks = [
|
||||
[[], new Dimensions(1, 0.50, 1), 4, 4],
|
||||
[[FAIL_CRITERIA[FAILED_BASE_AREA]], new Dimensions(0.99, 0.50, 1), 3, 4],
|
||||
[[FAIL_CRITERIA[FAILED_OVERALL_AREA]], new Dimensions(0.99, 0.50, 1), 4, 4],
|
||||
[[FAIL_CRITERIA[FAILED_OVERALL_AREA]], new Dimensions(0.79, 0.79, 1), 4, 4],
|
||||
];
|
||||
|
||||
for (let idx in exampleCageChecks) {
|
||||
let check = exampleCageChecks[idx];
|
||||
console.log(`Example ${idx}: ${check[1]} n(rats): ${check[2]}, n(floors): ${check[3]}`);
|
||||
console.log(`Result: ${JSON.stringify(cageCheck(check[1], check[2], check[3]))}\n`);
|
||||
}
|
103
src/index.html
@@ -1,103 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Knastrechner</title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
<script src="assets/calculator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Käfigrechner</h1>
|
||||
<div class="content">
|
||||
<div class="container-form">
|
||||
<div class="input-element">
|
||||
<div class="slidecontainer">
|
||||
<label for="numRats" id="labelNumRats">Anzahl an Ratten ?</label>
|
||||
<input type="range" min="1" max="20" value="4" class="slider" id="numRats">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-element">
|
||||
<label for="numFullFloors">Vollebenen</label>
|
||||
<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="container output-element" id="resultsDiv">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
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");
|
||||
|
||||
|
||||
var savicSuiteRoyaleDim = new Dimensions(1, 0.5, 1);
|
||||
|
||||
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 update() {
|
||||
var failed_checks = cageCheck(savicSuiteRoyaleDim, ratSlider.value, fullFloorNum.value);
|
||||
var 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>
|
196
src/index.js
Normal file
@@ -0,0 +1,196 @@
|
||||
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";
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function translatePageElements() {
|
||||
const translatableElements = document.querySelectorAll(
|
||||
"[data-i18n-key]",
|
||||
);
|
||||
translatableElements.forEach((el) => {
|
||||
const key = el.getAttribute("data-i18n-key");
|
||||
el.innerHTML = i18next.t(key);
|
||||
});
|
||||
|
||||
}
|
||||
// ...
|
||||
|
||||
function bindLocaleSwitcher(initialValue) {
|
||||
const switcher = document.querySelector(
|
||||
"[data-i18n-switcher]",
|
||||
);
|
||||
switcher.value = initialValue;
|
||||
switcher.onchange = (e) => {
|
||||
i18next
|
||||
.changeLanguage(e.target.value)
|
||||
.then(translatePageElements);
|
||||
};
|
||||
}
|
||||
|
||||
(async function () {
|
||||
await initI18next();
|
||||
translatePageElements();
|
||||
bindLocaleSwitcher(i18next.resolvedLanguage);
|
||||
})();
|
25
webpack.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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",
|
||||
},
|
||||
};
|