Compare commits
	
		
			22 Commits
		
	
	
		
			1d41b0cfc7
			...
			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 | 
							
								
								
									
										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,8 +61,7 @@ h1, h2 { | ||||
| 
 | ||||
| label { | ||||
|     font-weight: bold; | ||||
|     word-break: break-word;; | ||||
| 
 | ||||
|     word-break: break-word; | ||||
| } | ||||
| 
 | ||||
| .slidecontainer { | ||||
| @@ -77,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; | ||||
| } | ||||
| @@ -86,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; | ||||
| @@ -116,47 +128,46 @@ label { | ||||
|     height: 36px; | ||||
| } | ||||
| 
 | ||||
| .cage-selector{ | ||||
|     gap: 15px; | ||||
| .cards { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     flex-wrap: wrap; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .card { | ||||
|     text-align: center; | ||||
|     color: var(--text-two); | ||||
|     flex: 1 25%; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| .card-photo { | ||||
|     height: 200px; | ||||
|     min-width: 200px; | ||||
|     background-color: var(--background-one); | ||||
|     box-shadow: 0 0 25px rgba(17, 1, 68, 0.08); | ||||
|     border-radius: 8px; | ||||
|     position: relative; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .card-photo img { | ||||
|     width: 70%; | ||||
|     height: 95%; | ||||
|     position: absolute; | ||||
|     margin: auto; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
|     bottom: 0; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .card-photo label { | ||||
|     color: var(--text-one); | ||||
|     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; | ||||
| } | ||||
| @@ -167,32 +178,57 @@ label { | ||||
| } | ||||
| 
 | ||||
| input[type="checkbox"] { | ||||
|     -webkit-appearance: none; | ||||
|     position: relative; | ||||
|     width: 100%; | ||||
|     cursor: pointer; | ||||
| } | ||||
| input[type="checkbox"]:after { | ||||
|     position: absolute; | ||||
|     font-weight: 400; | ||||
|     content: "0"; | ||||
|     font-size: 18px; | ||||
|     color: #478bfb; | ||||
|     right: 10px; | ||||
|     top: -5px; | ||||
| } | ||||
| input[type="checkbox"]:checked:after { | ||||
|     font-weight: 900; | ||||
|     content: "X"; | ||||
|     color: #478bfb; | ||||
|     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 | 
| Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.2 MiB | 
| Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB | 
| Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/assets/img/unchecked.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 639 B | 
| @@ -1,21 +1,27 @@ | ||||
| const MINIMUM_BASE_AREA = 0.5; | ||||
| const MINIMUM_AREA_THREE_RATS = 1.8; | ||||
| const AREA_PER_ADDITIONAL_RAT = 0.2; | ||||
| const MINIMUM_AREA_THREE_RATS = 1.5; | ||||
| const AREA_PER_ADDITIONAL_RAT = 0.25; | ||||
| const MAXIMUM_FALL_HEIGHT = 0.5; | ||||
| const MINIMUM_LENGTH = 0.8; | ||||
| 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_LENGTH = "length"; | ||||
| 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(3)}cm betragen.`, | ||||
|     [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_LENGTH]: `Eine Seite des Käfig muss mindestens ${(MINIMUM_LENGTH * 100).toFixed(3)}cm lang sein um Rennen zu ermöglichen.`, | ||||
|     [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 { | ||||
| @@ -63,8 +69,16 @@ function cageCheck(dimensions, numRats, numFullFloors) { | ||||
|         failedCriteria[FAILED_FALL_HEIGHT] = FAIL_CRITERIA[FAILED_FALL_HEIGHT]; | ||||
|     } | ||||
| 
 | ||||
|     if (dimensions.width < MINIMUM_LENGTH && dimensions.depth < MINIMUM_LENGTH) { | ||||
|         failedCriteria[FAILED_LENGTH] = FAIL_CRITERIA[FAILED_LENGTH]; | ||||
|     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." | ||||
| } | ||||
							
								
								
									
										206
									
								
								src/index.html
									
									
									
									
									
								
							
							
						
						| @@ -1,206 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" xmlns="http://www.w3.org/1999/html"> | ||||
| <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="cage-selector"> | ||||
|             <div class="card"> | ||||
|                 <div class="card-photo"> | ||||
|                     <input type="checkbox" id="SavicSuiteRoyaleXL"/> | ||||
|                     <div> | ||||
|                         <img src="assets/img/savic-xl.jpeg"> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <label for="SavicSuiteRoyaleXL"> Savic Suite Royale XL</label> | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|             <div class="card"> | ||||
|                 <div class="card-photo"> | ||||
|                     <input type="checkbox" id="SavicSuiteRoyale95Double"/> | ||||
|                     <div> | ||||
|                         <img src="assets/img/savic-95-double.jpg"> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <label for="SavicSuiteRoyale95Double">Savic Suite Royale 95 Double</label> | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|             <div class="card"> | ||||
|                 <div class="card-photo"> | ||||
|                     <input type="checkbox" id="TiakiKleintierkäfigEtagere"/> | ||||
|                     <div> | ||||
|                         <img src="assets/img/tiaki.jpeg"> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <label for="TiakiKleintierkäfigEtagere">TIAKI Kleintierkäfig Étagère</label> | ||||
|             </div> | ||||
|  | ||||
|             <div class="card"> | ||||
|                 <div class="card-photo"> | ||||
|  | ||||
|                     <label for="form-cage-measurements">Käfigmaße</label> | ||||
|                     <form id="form-cage-measurements" class="form-measurements"> | ||||
|                         <div class="input-measurement"> | ||||
|                             <label for="width">Breite (cm)</label> | ||||
|                             <input class="measurement" type="number" id="width"> | ||||
|                         </div> | ||||
|                         <div class="input-measurement"> | ||||
|                             <label for="depth">Tiefe (cm)</label> | ||||
|                             <input class="measurement" type="number" id="depth"> | ||||
|                         </div> | ||||
|                         <div class="input-measurement"> | ||||
|                             <label for="height">Höhe (cm)</label> | ||||
|                             <input class="measurement" type="number" id="height"> | ||||
|                         </div> | ||||
|                     </form> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|         </div> | ||||
|         <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> | ||||
| </div> | ||||
| <script> | ||||
|  | ||||
|     const inputWidth = document.getElementById("width"); | ||||
|     const inputDepth = document.getElementById("depth"); | ||||
|     const inputHeight = document.getElementById("height"); | ||||
|  | ||||
|  | ||||
|  | ||||
|     const selectSavicSuiteRoyaleXL = document.getElementById("SavicSuiteRoyaleXL"); | ||||
|     const selectSavicSuiteRoyale95Double = document.getElementById("SavicSuiteRoyale95Double"); | ||||
|     const selectTiakiKleintierkäfigEtagere = document.getElementById("TiakiKleintierkäfigEtagere"); | ||||
|  | ||||
|     function updateCage(event) { | ||||
|         console.log("dada"); | ||||
|         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; | ||||
|  | ||||
|         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 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> | ||||
							
								
								
									
										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", | ||||
|     }, | ||||
| }; | ||||