Compare commits

...

47 Commits

Author SHA1 Message Date
666cc732bf feat: Style main result
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-18 09:27:26 +02:00
ecc3d418fc fix: remove placeholder when result is calculated 2025-04-18 09:27:12 +02:00
154f550775 fix: Cage checker
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-18 08:37:12 +02:00
cb71f61a91 feat: Fill yet empty result field 2025-04-18 08:27:11 +02:00
168e4acf6a fix: Round down number of rats if not int 2025-04-18 08:14:38 +02:00
cfe305e698 feat: Adjust title based on language
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-18 01:00:41 +02:00
1606c7dcf1 feat: add floating button to give feedback 2025-04-18 00:39:24 +02:00
d309ea2b46 fix: make sure classes are not overwritten
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-18 00:19:18 +02:00
b1372a5fbb feat: Restyle feedback explanation 2025-04-18 00:11:30 +02:00
4bc098bc09 feat(trans): Ann english translations of questions 2025-04-17 23:57:04 +02:00
34c2d24891 feat: Adjust the questions to cage calculator 2025-04-17 23:36:59 +02:00
74d1ba96b3 fix(trans): finally fixed 2025-04-17 23:30:11 +02:00
47040057b1 fix(trans): use dynamic content 2025-04-17 23:07:43 +02:00
2984ae4e6d fix(trans): keep dynamic content 2025-04-17 23:07:33 +02:00
db273b641d fix(trans): adding stuff 2025-04-17 23:01:55 +02:00
8f6a28a33e fix(trans): i hate it 2025-04-17 22:48:28 +02:00
4fbc47ad8c feat(trans): Use de as standard 2025-04-17 22:48:11 +02:00
f405c325f7 fix(trans): Autogen 2025-04-17 22:47:56 +02:00
da52a1dfdf feat: Add sus questions 2025-04-17 22:39:08 +02:00
a13a818485 feat: Add various translations 2025-04-17 21:45:37 +02:00
d5a467aa55 feat: Add sus (translations missing) 2025-04-17 18:24:37 +02:00
17e1415136 feat: Update appID
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-17 00:13:21 +02:00
a4a6fc4bb9 feat: Send testdata when localhost 2025-04-17 00:12:32 +02:00
0eff71732e fix: cookie retrieve 2025-04-17 00:11:33 +02:00
560f578b26 feat: add basic telemetry
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-16 23:34:54 +02:00
9cde2502e2 fix: Add js file extension
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-16 20:48:31 +02:00
68b7149133 feat: Add feedback modal
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-16 20:22:26 +02:00
7ae4debfeb feat: properly use fields, add tooltip as helptext
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-15 22:53:57 +02:00
e17cfb384c feat: restrict logo size 2025-04-15 22:19:51 +02:00
e83245e25b feat: Add logo to footer 2025-04-15 22:02:51 +02:00
041e391692 feat. Expand footer 2025-04-15 20:59:31 +02:00
6a0b6d3f72 fix: Add labels and about section 2025-04-15 17:26:35 +02:00
2470005892 refactor: comments + print 2025-04-14 20:24:17 +02:00
b4c90af4f3 fix: use label class correctly 2025-04-14 20:21:23 +02:00
9f716e68a5 feat: Add cage calc that shows requirements for a number of rats 2025-04-14 19:00:49 +02:00
884840eb6e feat: reset bulma colors 2025-04-13 22:21:55 +02:00
e743608539 fix: Use real FA icon 2025-04-13 22:21:38 +02:00
5142bb66ca feat: Implement "How many rats fit in a cage" 2025-04-13 22:17:57 +02:00
16d7c8f3dc refactor: abstract functions to reuse independent of number of rats 2025-04-13 19:17:23 +02:00
3ab46cbb72 feat: Add basis for how many rats calculator 2025-04-13 19:10:27 +02:00
276c4d6977 refactor: Add fontawesome to prod dependencies 2025-04-13 18:55:28 +02:00
53cac9f3d2 fix: Fix i18n infrastructure
Add dev dependency fix paths and key names so that the default HTML lexer can pick them up
2025-04-13 18:54:35 +02:00
de91ad9367 feat: Move result to tab content 2025-04-13 18:25:55 +02:00
4f9c7d82f8 feat: Add meta information 2025-04-13 18:15:44 +02:00
64b492a758 fix: Make sure first tab content is shown 2025-04-13 17:54:32 +02:00
9241250138 feat: Move app name to navbar 2025-04-13 17:54:13 +02:00
bbf30c4350 feat: Use fontawsome icons for measurements 2025-04-13 17:35:00 +02:00
13 changed files with 2956 additions and 306 deletions

View File

@@ -2,9 +2,12 @@ module.exports = {
defaultNamespace: 'translation',
lexers: {
js: ['JsxLexer'], // we're writing jsx inside .js files
html: ['HTMLLexer'],
default: ['JavascriptLexer'],
},
locales: ['en', 'de'],
output: 'public/lang/$LOCALE.json',
output: 'public/i18n/$LOCALE.json',
input: [ 'src/*.js', 'public/*.html', ],
keepRemoved: [/dynamic.*/], //prefixing a key with dynamic. will make it not disappear upon calling i18next
}

2206
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,8 @@
"author": "Julian-Samuel Gebühr",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"css-loader": "^7.1.2",
"i18next-parser": "^9.3.0",
"sass": "^1.86.3",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
@@ -32,9 +31,9 @@
"webpack-dev-server": "^5.0.4"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@telemetrydeck/sdk": "^2.0.4",
"bulma": "^1.0.3",
"bulma-tooltip": "^3.0.2",
"font-awesome": "^4.7.0",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,25 +1,65 @@
{
"back-to-home": "Zurück zur Homepage",
"app-name": "Käfigrechner",
"number-of-rats": "Anzahl an Ratten",
"full-floors": "Vollebenen",
"title": "VdRD Käfigrechner für Ratten",
"app-name": "VdRD Käfigrechner für Ratten",
"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-tooltip": "Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des Käfigbodens.",
"full-floors": "Vollebenen",
"result": "Ergebnis",
"input-data-num-rats": "Bitte gib erst die Maße des Käfigs ein, dann siehst du hier die Anzahl der Ratten die hineinpassen.",
"number-of-rats": "Anzahl an Ratten",
"please-input-cage-calc": "Stell ein für wie viel Ratten der Käfig sein soll, danach siehst du hier das Ergebnis.",
"please-input-cage-check": "Bitte gib erst die Maße des Käfigs und die Anzahl an Ratten ein. Danach siehst du hier, ob der Käfig passt.",
"give-feedback": "Feedback geben",
"change-language": "Sprache ändern",
"information-on-rat-husbandry": "Information",
"basic-rat-info": "Basiswissen Ratten",
"cage": "Käfig",
"food": "Ernährung",
"adopting-rats": "Ratten adoptieren",
"about": "Über uns",
"the-vdrd": "VdRD e.V.",
"imprint": "Impressum",
"source-code": "Quellcode",
"feedback": "Feedback",
"sus-title": "Bewerte den Rechner",
"explanation-feedback": "Vielen Dank, dass du uns Feedback gibst! Dein Feedback hilft uns den Rechner zu verbessern. Wir sammeln dabei keinerlei persönliche Daten. Du tust uns einfach nur einen großen Gefallen!",
"submit": "Absenden",
"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-fall-height": "Die mögliche Fallhöhe darf nicht mehr als {{ maximum_fall_height }}cm betragen.",
"failed-floor-height": "Der Mindestabstand zwischen Ebenen muss {{ minimum_floor_height }}cm betragen.",
"failed-num-rats": "Es müssen mindestens 3 Ratten zusammenleben, Paarhaltung ist nicht artgerecht.",
"failed-minimum-length-long-side": "Die lange Seite des Käfig muss mindestens {{ minimum_length_long_side }}cm lang sein um Rennen zu ermöglichen.",
"failed-minimum-length-short-side": "Die kurze Seite des Käfig muss mindestens {{ minimum_length_short_side }}cm lang sein.",
"alt-savic-xl": "Bild des Käfigs Savic Suite Royal XL, ein Drahtkäfig mit vier Türen die erlauben die gesamte Vorderseite zu öffnen. Auf halber Höhe ist eine weitere Vollebene.",
"alt-savic-95-double": "Bild eines dekorierten Käfigs, des Savic Suite Royale 95 Double. Auf insgesamt vie Vollebenen sind Holz- und Papphäusschen, Hängematten, Futterschalen und mehr.",
"alt-tiaki": "Bild des Tiaki-Käfigs, ein Drahtkäfig mit vier Türen die erlauben die gesamte Vorderseite zu öffnen. Auf halber Höhe ist eine weitere Vollebene.",
"cage-measurements": "Käfigmaße",
"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",
"change-language": "Sprache ändern",
"result": "Ergebnis"
}
"overall-area": "Die Gesamtfläche für {{ numRats }} Ratten muss mindestens {{ minimumOverallArea }}m² betragen.",
"strongly-disagree": "Stimme gar nicht zu",
"strongly-agree": "Stimme voll zu",
"submit-success": "Erfolgreich abgesendet!",
"submit-error": "Fehler beim Absenden!",
"network-error": "Fehler bei der Datenübertragung!",
"dynamic.sus-question-easy-to-use": "Ich finde der Käfigrechner ist einfach zu nutzen.",
"dynamic.sus-question-unnecessarily-complex": "Ich finde den Käfigrechner unnötig komplex.",
"dynamic.sus-question-need-support-of-technical-person": "Ich glaube ich brauche die Hilfe eine Technik-Person umd den Rechner zu nutzen.",
"dynamic.sus-question-well-integrated": "Ich finde, dass die verschiedenen Funktionen des Rechners gut integriert sind.",
"dynamic.sus-question-inconsistency": "Ich finde, dass es im Rechner zu viele Inkonsistenzen gibt.",
"dynamic.sus-question-learn-quickly": "Ich vermute, dass die meisten Leute schnell lernen den Rechner zu benutzen",
"dynamic.sus-question-cumbersome": "Ich finde den Rechner umständlich zu nutzen.",
"dynamic.sus-question-confident": "Ich habe mich bei der Nutzung des Rechners sehr sicher gefühlt.",
"dynamic.sus-question-trust": "Ich vertraue den Ergebnissen des Rechners.",
"dynamic.question-whats-missing": "Welche Funktion fehlt dir?",
"dynamic.question-pain-points": "Was muss aus deiner Sicht am Rechner geändert werden?",
"dynamic.question-other-feedback": "Was willst du uns sonst mitteilen?",
"dynamic.submit-success": "Erfolgreich abgesendet!"
}

View File

@@ -1,25 +1,64 @@
{
"back-to-home": "Back to home",
"app-name": "Cage Calculator",
"number-of-rats": "Number of Rats",
"full-floors": "Full floors",
"title": "VdRD Rat Cage Calculator",
"app-name": "VdRD Rat 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-tooltip": "A full floor is each floor with a area greater than XXm², including the bottom of the cage.",
"full-floors": "Full floors",
"result": "Result",
"input-data-num-rats": "Please input them measurements of the cage. Then you will see the number of rats allowed.",
"number-of-rats": "Number of Rats",
"please-input-cage-calc": "First put in how many rats should fit the cage. After that come back here.",
"please-input-cage-check": "First put in how many rats should fit the cage and the cages measurements. After that come back here to see if they fit.",
"give-feedback": "Give Feedback",
"change-language": "Change language",
"information-on-rat-husbandry": "Information",
"basic-rat-info": "Basic Rat Information",
"cage": "Cage",
"food": "Food",
"adopting-rats": "Adopting Rats",
"about": "About",
"the-vdrd": "VdRD r.V.",
"imprint": "Imprint",
"source-code": "Source Code",
"feedback": "Feedback",
"sus-title": "Rate the calculator",
"explanation-feedback": "Thank you for giving us feedback! We will use your feedback to improve the calculator.",
"submit": "Submit",
"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-fall-height": "The possible fall height between floors must not be above {{ maximum_fall_height }}cm.",
"failed-floor-height": "The height between floors must be above {{ minimum_floor_height }}cm.",
"failed-num-rats": "Rats must live in a group of at least three rats, pairs or lone rats are not species-appropriate.",
"failed-minimum-length-long-side": "The long side of the cage must be at least {{ minimum_length_long_side }}cm long to enable running.",
"failed-minimum-length-short-side": "The short side of the cage must be at least {{ minimum_length_short_side }}cm.",
"alt-savic-xl": "A picture of a wire bar cage with four doors that allow to open the full front of the cage. In the middle of the cage is a additional full floor.",
"alt-savic-95-double": "A picture of a decorated wire bar cage with four doors that allow to open the full front of the cage. Inside there are hammocks, toys and four full floors with lots of hides",
"alt-tiaki": "A picture of a decorated wire bar cage with four doors that allow to open the full front of the cage.",
"cage-measurements": "Cage measurements",
"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",
"change-language": "Change language",
"result": "Result"
}
"overall-area": "The overall area in the cage must be above {{ minimumOverallArea }}m² for {{ numRats }} rats.",
"strongly-disagree": "Strongly Disagree",
"strongly-agree": "Strongly Agree",
"submit-success": "Successfully submitted!",
"submit-error": "Error when submitting the form!",
"network-error": "Network error when submitting the form!",
"dynamic.sus-question-easy-to-use": "I thought the calculator is easy to use.",
"dynamic.sus-question-unnecessarily-complex": "I find the calculator unnecessarily complex.",
"dynamic.sus-question-need-support-of-technical-person": "I think that I need the support of a technical person to be able to use the calculator.",
"dynamic.sus-question-well-integrated": "I found various functions in the calculator were well integrated",
"dynamic.sus-question-inconsistency": "I thought there was too much inconsistency in the calculator",
"dynamic.sus-question-learn-quickly": "I would imagine that most people would learn to use the calculator very quickly",
"dynamic.sus-question-cumbersome": "I found the calculator very cumbersome to use",
"dynamic.sus-question-confident": "I felt very confident using the calculator",
"dynamic.sus-question-trust": "I trust the results of the calculator.",
"dynamic.question-whats-missing": "What functionality do you feel is missing?",
"dynamic.question-pain-points": "What do you thing needs to be changed?",
"dynamic.question-other-feedback": "What else do you want to tell us?"
}

View File

@@ -2,8 +2,11 @@
<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>
<title data-i18n="title">VdRD 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">
@@ -12,23 +15,23 @@
<div class="navbar">
<div class="navbar-brand">
<a class="button" href="https://vdrd.de">
<b data-i18n-key="back-to-home">zurück zur Homepage</b>
</a>
<div class="navbar-item">
<img src="assets/img/logo_vdrd.png" alt="VdRD Log">
<h1 data-i18n="app-name" class="title is-3">VdRD Käfigrechner für Ratten</h1>
</div>
</div>
</div>
<div class="content">
<h1 data-i18n-key="app-name" class="title is-1">Käfigrechner</h1>
<div class="tabs is-centered is-boxed is-toggle" id="primary-tabs">
<ul>
<li class="is-active" data-tab-id="1">
<a>
<span class="icon is-small">
<i class="fas fa-number" aria-hidden="true"></i>
<i class="fas fa-hashtag" aria-hidden="true"></i>
</span>
How many rats does this cage fit?
<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">
@@ -36,7 +39,7 @@
<span class="icon is-small">
<i class="fas fa-question" aria-hidden="true"></i>
</span>
What cage should I get?
<p data-i18n="what-cage-should-i-get">What cage should I get?</p>
</a>
</li>
<li data-tab-id="3">
@@ -44,7 +47,7 @@
<span class="icon is-small">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
Check existing cage
<p data-i18n="check-existing-cage">Check existing cage</p>
</a>
</li>
</ul>
@@ -52,82 +55,274 @@
</div>
<div id="tab-content">
<div class="" data-content-id="1">
<p>Here you can check how many rats could fit in your cage.</p>
<!--- 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>
<div class="field">
<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>
</div>
<div class="field">
<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>
</div>
<div class="field">
<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>
<div class="field">
<label for="num-rats-numFullFloors" class="label" data-i18n="full-floors">
Vollebenen
</label>
<input class="control" type="number" id="num-rats-numFullFloors" value="3" step="1">
<p class="help">Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des
Käfigbodens.</p>
</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 title is-2 is-centered" data-i18n="result">Ergebnis</h2>
</div>
<div class="card-content">
<div class="is-size-5" id="num-rats-resultsDiv">
<p data-i18n="input-data-num-rats">
Bitte gib erst die Maße ein, dann siehst du hier die Anzahl
der Ratten die hineinpassen.
</p>
</div>
</div>
</div>
</div>
</div>
<!--- Cage calc --->
<div class="" data-content-id="2">
<p>Here is a list of cages you can get.</p>
<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 title is-2 is-centered" data-i18n="result">Ergebnis</h2>
</div>
<div class="card-content">
<div class="is-size-5" id="cageCalcResultsDiv"></div>
<p data-i18n="please-input-cage-calc">Stell ein für wie viel Ratten der Käfig sein soll, danach
siehst du hier das Ergebnis</p>
</div>
</div>
</div>
</div>
</div>
<div class="is-active" data-content-id="3">
<div class="card" id="container-cages">
<div class="card-content" id="card-ManualMeasurements">
<h2 class="title is-4"><label data-i18n-key="cage-measurements"
<!--- 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">
<div class="input-measurement">
<label for="width" class="label" data-i18n-key="width-cm">Breite (cm)</label>
<input class="input" type="number" id="width">
</div>
<div class="input-measurement">
<label for="depth" class="label" data-i18n-key="depth-cm">Tiefe (cm)</label>
<input class="input" type="number" id="depth">
</div>
<label for="height" class="label" data-i18n-key="height-cm">Höhe (cm)</label>
<input class="input" type="number" id="height">
<div class="field">
<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>
</div>
<div class="field">
<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>
</div>
<div class="field">
<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>
<div class="columns">
<div class="column">
<label for="numFullFloors" class="label"
data-i18n-key="full-floors">Vollebenen</label>
<span data-tooltip="Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive des Käfigbodens.">
<svg class="text-grey-dark" width="18" height="18" viewBox="0 0 18 18" fill="none"
stroke="currentColor">
<path d="M9.00026 12.6C9.00026 12.6 9.00026 12.1224 9.00026 11.5333V8.86666C9.00026 8.57211 8.76148 8.33333 8.46693 8.33333H7.93359"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M8.73346 5.26666C8.58619 5.26666 8.4668 5.38605 8.4668 5.53333C8.4668 5.68061 8.58619 5.8 8.73346 5.8C8.88074 5.8 9.00013 5.68061 9.00013 5.53333C9.00013 5.38605 8.88074 5.26666 8.73346 5.26666V5.26666"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M9 17C13.4183 17 17 13.4183 17 9C17 4.58172 13.4183 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4183 4.58172 17 9 17Z"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</span>
<input type="number" id="numFullFloors" value="3" step="1">
<div class="field">
<label for="numFullFloors" class="label"
data-i18n="full-floors">Vollebenen</label>
<input class="control" type="number" id="numFullFloors" value="3" step="1">
<p class="help">Als Vollebenen zählen alle Ebenen die größer als 0.5m² sind, inklusive
des Käfigbodens.</p>
</div>
</div>
<div class="column">
<label for="numRats" id="labelNumRats" data-i18n-key="number-of-rats">Anzahl an
Ratten</label>
<input type="range" min="3" max="15" value="4" class="slider" id="numRats">
<div class="field">
<label class="label" for="numRats" id="labelNumRats" data-i18n="number-of-rats">Anzahl
an
Ratten</label>
<input type="range" min="3" max="15" value="4" class="control slider" id="numRats">
</div>
</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 title is-2 is-centered" data-i18n="result">Ergebnis</h2>
</div>
<div class="card-content">
<div id="resultsDiv">
<p data-i18n="please-input-cage-check">Bitte gib erst die Maße des Käfigs und die Anzahl an
Ratten ein. Danach siehst du hier, ob der Käfig passt.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<button class="button is-primary floating js-modal-trigger" data-target="modal-feedback"
data-i18n="give-feedback"></button>
<div class="footer" aria-label="Footer">
<div class="columns">
<div class="column">
<div class="block">
<img class="footer-logo" src="assets/img/logo_ausgeschrieben.png"
alt="Logo Verein der Rattenliebhaber und -halter Deutschland e.v.">
</div>
<div class="block">
<div class="language-switcher">
<label for="locale-switcher" class="footer-title title label" data-i18n="change-language">Sprache
ändern</label>
<select data-i18n-switcher class="select" id="locale-switcher">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</div>
</div>
</div>
<div class="column">
<div class="block">
<h3 class="footer-title title" data-i18n="information-on-rat-husbandry">Informationen zur
Rattenhaltung</h3>
<ul class="footer-links">
<li class="footer-link"><a href="https://vdrd.de/vor-der-anschaffung/" data-i18n="basic-rat-info">Grundwissen
Rattenhaltung</a></li>
<li class="footer-link"><a href="https://vdrd.de/das-rattenheim/" data-i18n="cage">Käfig</a></li>
<li class="footer-link"><a href="https://vdrd.de/ernaehrung/" data-i18n="food">Ernährung</a></li>
<li class="footer-link"><a href="https://vdrd.de/anzahl-geschlecht-und-bezugsquelle/"
data-i18n="adopting-rats">Ratten adoptieren</a>
</li>
</ul>
</div>
</div>
<div class="column">
<div class="block">
<h3 class="footer-title title" data-i18n="about">Über uns</h3>
<ul class="footer-links">
<li class="footer-link"><a href="https://vdrd.de/" data-i18n="the-vdrd">Der VdRD e.V.</a></li>
<li class="footer-link"><a href="https://vdrd.de/impressum" data-i18n="imprint">Impressum</a></li>
<li class="footer-link"><a href="https://codeberg.org/moanos/rettenrechner" data-i18n="source-code">Quellcode</a>
</li>
<li class="footer-link">
<a class="js-modal-trigger is-text is-link" data-target="modal-feedback"
data-i18n="give-feedback">
Feedback geben
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="title is-3" data-i18n-key="result">Ergebnis</h2>
</div>
<div class="card-content">
<div id="resultsDiv"></div>
<div id="modal-feedback" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<h2 data-i18n="feedback" class="modal-card-title">Feedback</h2>
<button class="delete" aria-label="close"></button>
</header>
<div class="modal-card-body">
<h1 class="title" data-i18n="sus-title">Bewerte den Rechner</h1>
<p class="is-spaced mb-4" data-i18n="explanation-feedback"></p>
<form id="sus-form">
<!--- Questions here --->
<div class="control" id="sus-control">
<button class="button is-primary is-fullwidth" type="submit" data-i18n="submit">Absenden</button>
</div>
</form>
<div id="response-message" class="notification is-hidden mt-3"></div>
</div>
<div class="modal-card-foot">
<p>
Der Code dieser Website ist <a href="https://codeberg.org/moanos/rettenrechner">öffentlich
einsehbar.</a>
Gerne kannst du auch direkt dort <a href="https://codeberg.org/moanos/rettenrechner/issues">einen
Issue</a> eröffnen!
</p>
</div>
</div>
</div>
<script src="./bundle.js"></script>
<div class="footer">
<div class="language-switcher">
<label aria-hidden="false" style="display: none" data-i18n-key="change-language"></label>
<select data-i18n-switcher class="locale-switcher" id="locale-switcher">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</div>
</div>
</body>
</html>

47
src/feedback.js Normal file
View File

@@ -0,0 +1,47 @@
import {send} from './telemetry';
document.addEventListener('DOMContentLoaded', () => {
// Functions to open and close a modal
function openModal($el) {
$el.classList.add('is-active');
send("Modal.open", {
modal: $el.id
});
}
function closeModal($el) {
$el.classList.remove('is-active');
}
function closeAllModals() {
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
closeModal($modal);
});
}
// Add a click event on buttons to open a specific modal
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
const modal = $trigger.dataset.target;
const $target = document.getElementById(modal);
$trigger.addEventListener('click', () => {
openModal($target);
});
});
// Add a click event on various child elements to close the parent modal
(document.querySelectorAll('.modal-background, .modal-close, .delete') || []).forEach(($close) => {
const $target = $close.closest('.modal');
$close.addEventListener('click', () => {
closeModal($target);
});
});
// Add a keyboard event to close all modals
document.addEventListener('keydown', (event) => {
if (event.key === "Escape") {
closeAllModals();
}
});
});

View File

@@ -1,11 +1,14 @@
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 '@fortawesome/fontawesome-free/js/fontawesome';
import '@fortawesome/fontawesome-free/js/solid';
import '@fortawesome/fontawesome-free/js/regular';
import '@fortawesome/fontawesome-free/js/brands';
import './feedback.js';
import './main.scss';
import {send} from './telemetry';
import './sus.js';
/////////////////
// TRANSLATION //
@@ -18,7 +21,7 @@ async function initI18next() {
.init({
supportedLngs: ["en", "de"],
nonExplicitSupportedLngs: true,
fallbackLng: "en",
fallbackLng: "de",
debug: true,
backend: {
loadPath: "/i18n/{{lng}}.json",
@@ -29,10 +32,10 @@ async function initI18next() {
function translatePageElements() {
// Translate content inside a tag
const translatableElements = document.querySelectorAll(
"[data-i18n-key]",
"[data-i18n]",
);
translatableElements.forEach((el) => {
const key = el.getAttribute("data-i18n-key");
const key = el.getAttribute("data-i18n");
el.innerHTML = i18next.t(key);
});
// Translate alt texts
@@ -55,7 +58,7 @@ function bindLocaleSwitcher(initialValue) {
i18next
.changeLanguage(e.target.value)
.then(translatePageElements)
.then(update);
.then(updateCageCheck);
};
}
@@ -63,7 +66,7 @@ function bindLocaleSwitcher(initialValue) {
await initI18next();
translatePageElements();
bindLocaleSwitcher(i18next.resolvedLanguage);
update();
updateCageCheck();
})();
//////////
@@ -71,7 +74,6 @@ function bindLocaleSwitcher(initialValue) {
//////////
function initTabs(tabs, tabContent, activationClass) {
tabs.forEach((tab) => {
tab.addEventListener('click', (e) => {
@@ -122,13 +124,21 @@ const MINIMUM_LENGTH_LONG_SIDE = 0.8;
const MINIMUM_LENGTH_SHORT_SIDE = 0.5;
const MINIMUM_FLOOR_HEIGHT = 0.25;
const FAILED_BASE_AREA = "base_area";
const FAILED_OVERALL_AREA = "overall_area";
const FAILED_FALL_HEIGHT = "fall_height";
const FAILED_NUM_RATS = "num_rats";
const FAILED_MINIMUM_LENGTH_LONG_SIDE = "length_long_side";
const FAILED_MINIMUM_LENGTH_SHORT_SIDE = "length_short_side";
const FAILED_FLOOR_HEIGHT = "floor_height"
const FAILED_BASE_AREA = "failed_base_area";
const FAILED_OVERALL_AREA = "failed_overall_area";
const FAILED_FALL_HEIGHT = "failed_fall_height";
const FAILED_NUM_RATS = "failed_num_rats";
const FAILED_MINIMUM_LENGTH_LONG_SIDE = "failed_length_long_side";
const FAILED_MINIMUM_LENGTH_SHORT_SIDE = "failed_length_short_side";
const FAILED_FLOOR_HEIGHT = "failed_floor_height"
const CRITERIA_BASE_AREA = "base_area";
const CRITERIA_FALL_HEIGHT = "fall_height";
const CRITERIA_MINIMUM_LENGTH_LONG_SIDE = "length_long_side";
const CRITERIA_MINIMUM_LENGTH_SHORT_SIDE = "length_short_side";
const CRITERIA_FLOOR_HEIGHT = "floor_height"
const CRITERIA_OVERALL_AREA = "overall_area"
class Validator {
@@ -142,32 +152,44 @@ class Validator {
[FAILED_MINIMUM_LENGTH_LONG_SIDE]: i18next.t("failed-minimum-length-long-side", {"minimum_length_long_side": (MINIMUM_LENGTH_LONG_SIDE * 100).toFixed(0)}),
[FAILED_MINIMUM_LENGTH_SHORT_SIDE]: i18next.t("failed-minimum-length-short-side", {"minimum_length_short_side": (MINIMUM_LENGTH_SHORT_SIDE * 100).toFixed(0)}),
};
this.STATIC_CRITERIA = {
[CRITERIA_BASE_AREA]: i18next.t('base-area', {"MINIMUM_BASE_AREA": MINIMUM_BASE_AREA}),
[CRITERIA_FALL_HEIGHT]: i18next.t("fall-height", {"maximum_fall_height": (MAXIMUM_FALL_HEIGHT * 100).toFixed(0)}),
[CRITERIA_FLOOR_HEIGHT]: i18next.t("floor-height", {"minimum_floor_height": (MINIMUM_FLOOR_HEIGHT * 100).toFixed(0)}),
[CRITERIA_MINIMUM_LENGTH_LONG_SIDE]: i18next.t("minimum-length-long-side", {"minimum_length_long_side": (MINIMUM_LENGTH_LONG_SIDE * 100).toFixed(0)}),
[CRITERIA_MINIMUM_LENGTH_SHORT_SIDE]: i18next.t("minimum-length-short-side", {"minimum_length_short_side": (MINIMUM_LENGTH_SHORT_SIDE * 100).toFixed(0)}),
}
}
overallAreaNeeded(numOfRats) {
if (numOfRats < 3 || numOfRats > 15) {
throw new Error("This formula works only from 3 to 15 rats");
}
return MINIMUM_AREA_THREE_RATS + (numOfRats - 3) * AREA_PER_ADDITIONAL_RAT;
return MINIMUM_AREA_THREE_RATS + (numOfRats - 3.0) * AREA_PER_ADDITIONAL_RAT;
}
cageCheck(dimensions, numRats, numFullFloors) {
let failedCriteria = {};
if (numRats < 2 || numRats > 15) {
failedCriteria[FAILED_NUM_RATS] = this.FAIL_CRITERIA[FAILED_NUM_RATS];
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];
}
const areaNeeded = this.overallAreaNeeded(numRats);
if (baseArea * numFullFloors < areaNeeded) {
failedCriteria[FAILED_OVERALL_AREA] = this.FAIL_CRITERIA[FAILED_OVERALL_AREA];
}
if (dimensions.height / numFullFloors > MAXIMUM_FALL_HEIGHT) {
failedCriteria[FAILED_FALL_HEIGHT] = this.FAIL_CRITERIA[FAILED_FALL_HEIGHT];
}
@@ -187,6 +209,30 @@ class Validator {
return failedCriteria;
}
getOverallArea(dimensions, numFullFloors) {
const baseArea = dimensions.depth * dimensions.width;
return baseArea * numFullFloors
}
cageCheck(dimensions, numRats, numFullFloors) {
/*
Function that checks a cage based on overall criteria and the number of rats.
*/
let failedCriteria = this.failCageNumberIndependent(dimensions, numFullFloors);
if (numRats < 2 || numRats > 15) {
failedCriteria[FAILED_NUM_RATS] = this.FAIL_CRITERIA[FAILED_NUM_RATS];
}
const overallArea = this.getOverallArea(dimensions, numFullFloors);
const overallAreaNeeded = this.overallAreaNeeded(numRats);
if (overallArea < overallAreaNeeded) {
failedCriteria[FAILED_OVERALL_AREA] = this.FAIL_CRITERIA[FAILED_OVERALL_AREA];
}
return failedCriteria;
}
}
@@ -212,13 +258,51 @@ class Dimensions {
// DOCUMENT INTERACTION //
//////////////////////////
const inputWidth = document.getElementById("width");
inputWidth.onchange = updateViaManualMeasurements;
const inputDepth = document.getElementById("depth");
inputDepth.onchange = updateViaManualMeasurements;
const inputHeight = document.getElementById("height");
inputHeight.onchange = updateViaManualMeasurements;
///////
// 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");
@@ -226,14 +310,9 @@ let labelNumRats = document.getElementById("labelNumRats");
let ratSlider = document.getElementById("numRats");
ratSlider.oninput = function () {
update();
updateCageCheck();
}
// Full floor functions
let fullFloorNum = document.getElementById("numFullFloors");
fullFloorNum.oninput = function () {
update();
}
function getResultFromChecks(checks) {
if (Object.keys(checks).length > 0) {
@@ -252,23 +331,113 @@ function getResultFromChecks(checks) {
}
function updateViaManualMeasurements() {
update();
function formatCriteria(criteria) {
const ul = document.createElement('ul');
for (const key in criteria) {
const li = document.createElement('li');
li.textContent = `☑️ ` + criteria[key];
ul.appendChild(li);
}
return ul;
}
function update() {
function updateCageCheck() {
labelNumRats.innerHTML = i18next.t("cage-for-x-rats", {"num_rats": ratSlider.value});
const width = inputWidth.value
const depth = inputDepth.value
const height = inputHeight.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);
const numRats = ratSlider.value;
const numFullFloors = fullFloorNum.value;
const failed_checks = validator.cageCheck(dimensions, numRats, numFullFloors);
let resultsDiv = document.getElementById("resultsDiv");
const result = getResultFromChecks(failed_checks);
resultsDiv.innerHTML = "";
resultsDiv.appendChild(result);
// Send telemetry
send("Update.CageCheck", {
width: width,
depth: depth,
height: height,
numRats: numRats,
numFullFloors: numFullFloors
});
}
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(numRats);
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);
// Send telemetry
send("Update.CageCalc", {
numRats: numRats,
});
}
function updateNumRatsCalculator() {
const width = numRatsCalculatorInputWidth.value;
const depth = numRatsCalculatorInputDepth.value;
const height = numRatsCalculatorInputHeight.value;
const numFullFloors = numRatsNumFullFloors.value;
const dimensions = new Dimensions(width / 100, depth / 100, height / 100);
const validator = new Validator();
const failed_checks = validator.failCageNumberIndependent(dimensions, numFullFloors);
let overallArea = validator.getOverallArea(dimensions, numFullFloors);
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": Math.floor(allowedNumRats)});
p.className = " is-size-5 has-text-weight-semibold"
resultsDiv.appendChild(p);
resultsDiv.appendChild(result);
// Send telemetry
send("Update.NumRatsCalc", {
width: width,
depth: depth,
height: height,
numFullFloors: numFullFloors
});
}

View File

@@ -8,10 +8,6 @@ $beige-lighter: #eff0eb;
// Path to Bulma's sass folder
@use "bulma/sass" with (
$family-primary: '"Nunito", sans-serif',
$grey-dark: $brown,
$grey-light: $beige-light,
$primary: $purple,
$link: $pink,
$control-border-width: 2px,
$input-shadow: none
);
@@ -24,6 +20,21 @@ $beige-lighter: #eff0eb;
display: block;
}
body {
padding: 5px;
}
.result-card {
width: 100%;
}
.inline-icon {
height: 1.5rem;
}
.footer-logo {
height: 5rem;
}
// Import the Google Font
@import url("https://fonts.googleapis.com/css?family=Nunito:400,700");
@@ -55,3 +66,108 @@ $beige-lighter: #eff0eb;
opacity: 1;
}
}
/*
TOOLTIP
Reused from Notfellchen
*/
.tooltip {
display: inline-flex;
justify-content: center;
position: relative;
}
.tooltip:hover .tooltiptext {
display: flex;
opacity: 1;
visibility: visible;
}
.tooltip .tooltiptext {
border-radius: 4px;
bottom: calc(100% + 0.6em + 2px);
box-shadow: 0px 2px 4px #07172258;
background-color: var(--primary-dark-one);
color: var(--secondary-light-one);
font-size: 0.68rem;
justify-content: center;
line-height: 1.35em;
padding: 0.5em 0.7em;
position: absolute;
text-align: center;
width: 7rem;
z-index: 1;
display: flex;
opacity: 0;
transition: all 0.3s ease-in;
visibility: hidden;
}
.tooltip .tooltiptext::before {
border-width: 0.6em 0.8em 0;
border-color: transparent;
border-top-color: var(--primary-dark-one);
content: "";
display: block;
border-style: solid;
position: absolute;
top: 100%;
}
/* Makes the tooltip fly from above */
.tooltip.top .tooltiptext {
margin-bottom: 8px;
}
.tooltip.top:hover .tooltiptext {
margin-bottom: 0;
}
/* Make adjustments for bottom */
.tooltip.bottom .tooltiptext {
top: calc(100% + 0.6em + 2px);
margin-top: 8px;
}
.tooltip.bottom:hover .tooltiptext {
margin-top: 0;
}
.tooltip.bottom .tooltiptext::before {
transform: rotate(180deg);
/* 100% of the height of .tooltip */
bottom: 100%;
}
.tooltip:not(.top) .tooltiptext {
bottom: auto;
}
.tooltip:not(.top) .tooltiptext::before {
top: auto;
}
// SUS Slider
.sus-slider {
width: 100%;
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
margin-top: 0.25rem;
}
// FLOATING BUTTON
.floating {
position: fixed;
border-radius: 0.3rem;
bottom: 4.5rem;
right: 1rem;
}

107
src/sus.js Normal file
View File

@@ -0,0 +1,107 @@
import i18next from "i18next";
const scaleQuestions = [
"sus-question-easy-to-use", // I thought CageCalc is easy to use
"sus-question-unnecessarily-complex", // I find CageCalc unnecessarily complex
"sus-question-need-support-of-technical-person", // I think that I need the support of a technical person to be able to use CageCalc
"sus-question-well-integrated", // I found various functions in CageCalc were well integrated
"sus-question-inconsistency", // I thought there was too much inconsistency in CageCalc
"sus-question-learn-quickly", // I would imagine that most people would learn to use CageCalc very quickly
"sus-question-cumbersome", // I found CageCalc very cumbersome to use
"sus-question-confident", // I felt very confident using CageCalc
"sus-question-trust", // I felt very confident using CageCalc
];
const freetextQuestions = [
"question-whats-missing", // Welche Funktion fehlt dir?
"question-pain-points", // Was muss aus deiner Sicht an XX geändert werden?
"question-other-feedback" // Was willst du uns mitgeben?
]
function prepareQuestionnaire() {
const form = document.getElementById("sus-form");
scaleQuestions.forEach((key, index) => {
const field = document.createElement("div");
field.className = "field";
field.id = key;
field.innerHTML = `
<label class="label" data-i18n="dynamic.${key}"></label>
<p class="control">
<input class="sus-slider" type="range" min="1" max="5" step="1" name=${key} required>
<div class="slider-labels">
<span data-i18n="strongly-disagree">${i18next.t('strongly-disagree')}</span>
<span data-i18n="strongly-agree">${i18next.t('strongly-agree')}</span>
</div>
</p>
`;
form.insertBefore(field, form.querySelector('#sus-control'));
});
freetextQuestions.forEach((key, index) => {
const field = document.createElement("div");
field.classList.add("field");
field.id = key;
field.innerHTML = `
<label class="label" data-i18n="dynamic.${key}"></label>
<p class="control">
<input class="input" type="text" name=${key}/>
</p>
`
form.insertBefore(field, form.querySelector('#sus-control'));
})
return form;
}
window.addEventListener('DOMContentLoaded', () => {
let form = prepareQuestionnaire();
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
const jsonData = {};
for (let [key, value] of formData.entries()) {
if (key.startsWith("sws-question")) {
jsonData[key] = parseInt(value, 10);
} else {
jsonData[key] = value;
}
}
try {
const response = await fetch("https://storandom.hyteck.de/submit", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(jsonData)
});
const messageDiv = document.getElementById("response-message");
if (response.ok) {
messageDiv.classList.add("is-success");
messageDiv.innerHTML = i18next.t("submit-success");
} else {
messageDiv.classList.add("is-danger");
messageDiv.innerHTML = i18next.t("submit-error");
}
messageDiv.classList.remove("is-hidden");
} catch (error) {
const messageDiv = document.getElementById("response-message");
messageDiv.classList.add("is-danger");
messageDiv.removeAttribute("data-i18n");
messageDiv.setAttribute("data-i18n", "");
messageDiv.innerHTML = i18next.t("network-error");
let errorP = document.createElement("p");
errorP.className = "error";
errorP.innerText = error;
messageDiv.appendChild(errorP);
messageDiv.classList.remove("is-hidden");
}
});
});

57
src/telemetry.js Normal file
View File

@@ -0,0 +1,57 @@
import TelemetryDeck from '@telemetrydeck/sdk';
///////////////
// TELEMETRY //
///////////////
// Telemetry Deck only collects fully anonymized data!
const getCookieValueOrNull = (name) => {
const cookie = document.cookie
.split(";")
.map(c => c.trim())
.find(c => c.startsWith(name + "="));
return cookie ? cookie.split("=")[1] : null;
};
function getOrCreateUUID() {
let cookie_val = getCookieValueOrNull("id");
if (
cookie_val
) {
return cookie_val;
} else {
let uuid =crypto.randomUUID();
const days = 365;
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `id=${uuid}; expires=${expires}`;
return uuid;
}
}
// Send Test Signals when running locally
function init() {
const appId = "E453AAB8-B1AD-4F3E-87DF-97FC3A0400B9";
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
const td = new TelemetryDeck({
appID: appId,
clientUser: getOrCreateUUID(),
testMode: true
});
return td;
} else {
const td = new TelemetryDeck({
appID: appId,
clientUser: getOrCreateUUID()
});
return td;
}
}
let td = init();
export function send(type, payload) {
td.signal(type, payload);
}