Repository: ssciwr/mondey
Pull request #364: Clean up data input
Author: login: lkeegan
Created: 2025-05-12T08:50:41Z | Merged: 2025-05-12T14:15:05Z

- child birth year/month are now localised select options
- DataInput no longer overwrites a supplied placeholder
- simplify typing of component type and remove unused types
- remove no longer used / debugging code


Diff:
diff --git a/frontend/package.json b/frontend/package.json
index 06069faa..2ae9c4d2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -27,6 +27,7 @@
 		"@testing-library/svelte": "^5.2.7",
 		"@types/eslint": "^9.6.1",
 		"@types/file-saver": "^2.0.7",
+		"@types/luxon": "^3.6.2",
 		"@types/node": "^22.14.0",
 		"@vitest/coverage-v8": "^3.1.1",
 		"autoprefixer": "^10.4.21",
@@ -53,6 +54,7 @@
 		"export-to-csv": "^1.4.0",
 		"file-saver": "^2.0.5",
 		"iso-639-1": "3.1.5",
+		"luxon": "^3.6.1",
 		"svelte-dnd-action": "^0.9.60"
 	},
 	"pnpm": {
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index e898f1b7..f05babf6 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
       iso-639-1:
         specifier: 3.1.5
         version: 3.1.5
+      luxon:
+        specifier: ^3.6.1
+        version: 3.6.1
       svelte-dnd-action:
         specifier: ^0.9.60
         version: 0.9.60(svelte@5.25.6)
@@ -60,6 +63,9 @@ importers:
       '@types/file-saver':
         specifier: ^2.0.7
         version: 2.0.7
+      '@types/luxon':
+        specifier: ^3.6.2
+        version: 3.6.2
       '@types/node':
         specifier: ^22.14.0
         version: 22.14.0
@@ -904,6 +910,9 @@ packages:
   '@types/long@4.0.2':
     resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==}
 
+  '@types/luxon@3.6.2':
+    resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==}
+
   '@types/mapbox__point-geometry@0.1.4':
     resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==}
 
@@ -2493,6 +2502,10 @@ packages:
   lru-cache@10.4.3:
     resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
 
+  luxon@3.6.1:
+    resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
+    engines: {node: '>=12'}
+
   lws-basic-auth@2.0.0:
     resolution: {integrity: sha512-zzyoGFLQPuKaQJvHMLmmSyfT6lIvocwcDXllTVW5brD0t0YgHYopILkzja+x+MIlJX/YhNKniaTSasujniYVjw==}
     engines: {node: '>=10'}
@@ -4506,6 +4519,8 @@ snapshots:
 
   '@types/long@4.0.2': {}
 
+  '@types/luxon@3.6.2': {}
+
   '@types/mapbox__point-geometry@0.1.4': {}
 
   '@types/mapbox__vector-tile@1.3.4':
@@ -6268,6 +6283,8 @@ snapshots:
 
   lru-cache@10.4.3: {}
 
+  luxon@3.6.1: {}
+
   lws-basic-auth@2.0.0:
     dependencies:
       basic-auth: 2.0.1
diff --git a/frontend/src/lib/components/DataInput/DataInput.svelte b/frontend/src/lib/components/DataInput/DataInput.svelte
index ae06e169..60045b3e 100644
--- a/frontend/src/lib/components/DataInput/DataInput.svelte
+++ b/frontend/src/lib/components/DataInput/DataInput.svelte
@@ -1,12 +1,27 @@
 <svelte:options runes={true} />
 
 <script lang="ts">
+import Fileupload from "$lib/components/DataInput/Fileupload.svelte";
 import { i18n } from "$lib/i18n.svelte";
-import { Label, Textarea } from "flowbite-svelte";
+import {
+	Input,
+	Label,
+	Select,
+	type SelectOptionType,
+	Textarea,
+} from "flowbite-svelte";
+export type Component = "input" | "textarea" | "select" | "fileupload";
+
+const componentTable = {
+	input: Input,
+	textarea: Textarea,
+	select: Select,
+	fileupload: Fileupload,
+};
 
 // variables
 let {
-	component = null,
+	component = "input",
 	value = $bindable(null),
 	additionalValue = $bindable(null),
 	label = null,
@@ -19,8 +34,9 @@ let {
 	items = [],
 	placeholder = undefined,
 	kwargs = {},
+	testid = undefined,
 }: {
-	component?: any;
+	component?: Component;
 	value?: any;
 	additionalRequired?: boolean;
 	label?: string | null;
@@ -30,9 +46,10 @@ let {
 	required?: boolean;
 	disabled?: boolean;
 	id?: string | undefined;
-	items?: any[];
+	items?: SelectOptionType<string | number>[];
 	placeholder?: string | undefined;
 	kwargs?: any;
+	testid?: string;
 } = $props();
 
 let valid: boolean = $state(false);
@@ -56,9 +73,9 @@ let showTextField: boolean = $derived.by(
 	},
 );
 let highlight = $derived(!valid && required === true);
-
-let realPlaceholder = $derived(
-	i18n.tr ? i18n.tr.misc.selectPlaceholder : undefined,
+let derivedPlaceholder = $derived(
+	placeholder ??
+		(component === "select" ? i18n.tr.misc.selectPlaceholder : undefined),
 );
 </script>
 
@@ -74,23 +91,23 @@ let realPlaceholder = $derived(
 
 <div class="space-y-4">
 	<svelte:component
-		this={component}
+		this={componentTable[component]}
 		class={highlight
 			? "rounded border-2 border-primary-600 dark:border-primary-600 " +
 				componentClass
 			: componentClass}
 		bind:value = {value}
-		{realPlaceholder}
+		placeholder = {derivedPlaceholder}
 		{items}
 		{required}
 		{disabled}
 		{...kwargs}
 		for={label}
+		data-testid={testid}
 	/>
 
 	{#if showTextField === true}
 		<Textarea
-
 			bind:value={additionalValue}
 			required={additionalRequired}
 			{disabled}
diff --git a/frontend/src/lib/components/DataInput/DateInput.svelte b/frontend/src/lib/components/DataInput/DateInput.svelte
deleted file mode 100644
index 536e2f72..00000000
--- a/frontend/src/lib/components/DataInput/DateInput.svelte
+++ /dev/null
@@ -1,23 +0,0 @@
-<svelte:options runes={true} />
-
-<script lang="ts">
-import { Input } from "flowbite-svelte";
-
-let {
-	value = $bindable(null),
-	outerClass = "space-y-4",
-	innerClass = null,
-	disabled = false,
-	required = true,
-}: {
-	value: string | null;
-	outerClass: string | null;
-	innerClass: string | null;
-	disabled: boolean;
-	required: boolean;
-} = $props();
-</script>
-
-<div class={outerClass}>
-	<Input type="date" bind:value class={innerClass} {required} {disabled} />
-</div>
diff --git a/frontend/src/lib/components/DataInput/RadioList.svelte b/frontend/src/lib/components/DataInput/RadioList.svelte
deleted file mode 100644
index aa80be0a..00000000
--- a/frontend/src/lib/components/DataInput/RadioList.svelte
+++ /dev/null
@@ -1,55 +0,0 @@
-<script lang="ts">
-import { Radio, RadioButton } from "flowbite-svelte";
-import { createEventDispatcher } from "svelte";
-
-const dispatch = createEventDispatcher();
-
-function handleChange(event) {
-	dispatch("change", { value: event.target.value });
-}
-
-function handleBlur(event) {
-	dispatch("blur", { value: event.target.value });
-}
-
-function handleClick(event) {
-	dispatch("click", { value: event.target.value });
-}
-export let value: any;
-export let disabled = false;
-export let required = true;
-let windowWidth = 1920;
-$: smallScreen = windowWidth < 800;
-</script>
-
-<svelte:window bind:innerWidth={windowWidth} />
-
-<div class="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
-	{#if smallScreen}
-		{#each $$props.items as item}
-			<RadioButton
-				class="mx-2"
-				{disabled}
-				{required}
-				bind:group={value}
-				value={item.value}
-				on:change={handleChange}
-				on:blur={handleBlur}
-				on:click={handleClick}>{item.label}</RadioButton
-			>
-		{/each}
-	{:else}
-		{#each $$props.items as item}
-			<Radio
-				class="mx-2"
-				{disabled}
-				{required}
-				bind:group={value}
-				value={item.value}
-				on:change={handleChange}
-				on:blur={handleBlur}
-				on:click={handleClick}>{item.label}</Radio
-			>
-		{/each}
-	{/if}
-</div>
diff --git a/frontend/src/lib/components/UserForgotPassword.svelte b/frontend/src/lib/components/UserForgotPassword.svelte
index b5be0ab4..313c8ff8 100644
--- a/frontend/src/lib/components/UserForgotPassword.svelte
+++ b/frontend/src/lib/components/UserForgotPassword.svelte
@@ -10,7 +10,7 @@ import { preventDefault } from "$lib/util";
 import { Button, Card, Heading, Input } from "flowbite-svelte";
 
 const maildata = {
-	component: Input,
+	component: "input",
 	type: "email",
 	props: {
 		placeholder: i18n.tr.forgotPw.placeholder,
diff --git a/frontend/src/lib/components/UserLandingPage.svelte b/frontend/src/lib/components/UserLandingPage.svelte
index e62517c6..3769608c 100644
--- a/frontend/src/lib/components/UserLandingPage.svelte
+++ b/frontend/src/lib/components/UserLandingPage.svelte
@@ -3,7 +3,6 @@ import { afterNavigate, goto } from "$app/navigation";
 import UserVerify from "$lib/components/UserVerify.svelte";
 import UserlandSidebar from "$lib/components/UserlandSidebar.svelte";
 import { i18n } from "$lib/i18n.svelte";
-import { activePage, componentTable } from "$lib/stores/componentStore";
 import { user } from "$lib/stores/userStore.svelte";
 import { Button } from "flowbite-svelte";
 import { Drawer } from "flowbite-svelte";
@@ -15,10 +14,6 @@ let hideDrawer: boolean = $state(true);
 // get user state
 onMount(user.load);
 
-// set this initially to user data such that the user data page is shown first
-// todo: Remove this before #242 is PR merged, just here to compare and test the old version and new version of routing.
-activePage.set("childrenGallery");
-
 afterNavigate(() => {
 	hideDrawer = true;
 });
@@ -41,10 +36,6 @@ afterNavigate(() => {
 				<Drawer transitionType="fly" placement="right" transitionParams={{duration: 200}} bind:hidden = {hideDrawer} id="menuDrawer">
 					<UserlandSidebar setHideDrawer={(to) => { hideDrawer = to }} />
 				</Drawer>
-
-				<div class = "m-2 p-2 w-full pl-12 md:pl-2 md:w-auto">
-					<svelte:component this={componentTable[$activePage]}/>
-				</div>
 			</div>
 		{:else}
 			<UserVerify />
diff --git a/frontend/src/lib/stores/componentStore.ts b/frontend/src/lib/stores/componentStore.ts
deleted file mode 100644
index faf771d4..00000000
--- a/frontend/src/lib/stores/componentStore.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import DateInput from "$lib/components/DataInput/DateInput.svelte";
-import Fileupload from "$lib/components/DataInput/Fileupload.svelte";
-import RadioList from "$lib/components/DataInput/RadioList.svelte";
-import { Input, MultiSelect, Select, Textarea } from "flowbite-svelte";
-import { writable } from "svelte/store";
-
-interface ComponentTable {
-	[key: string]: any; // README: flowbite components are not yet svelte5 => 'Component' type which should be used instead of 'any' here throws errors for flowbite components.
-}
-
-export const componentTable: ComponentTable = {
-	radioList: RadioList,
-	input: Input,
-	date: DateInput,
-	multiSelect: MultiSelect,
-	select: Select,
-	fileupload: Fileupload,
-	textarea: Textarea,
-};
-
-// used in userlandingpage to make it single page
-export const activePage = writable("userDataInput");
diff --git a/frontend/src/lib/translations.ts b/frontend/src/lib/translations.ts
index 3fb69e93..8812870b 100644
--- a/frontend/src/lib/translations.ts
+++ b/frontend/src/lib/translations.ts
@@ -303,7 +303,6 @@ export const translationIds = {
 		searchRemarkLabel: "Bemerkung",
 		searchRemarkPlaceholder: "Bemerkungen zu Kindern durchsuchen",
 		pleaseEnter: "Bitte eintragen",
-		pleaseEnterNumber: "Bitte als Zahl eintragen",
 		childName: "Name des Kindes?",
 		childBirthYear: "Geburtsjahr des Kindes?",
 		childBirthMonth: "Geburtsmonat des Kindes?",
diff --git a/frontend/src/routes/resetPassword/[[code]]/+page.svelte b/frontend/src/routes/resetPassword/[[code]]/+page.svelte
index 4527161c..2fc16a7b 100644
--- a/frontend/src/routes/resetPassword/[[code]]/+page.svelte
+++ b/frontend/src/routes/resetPassword/[[code]]/+page.svelte
@@ -6,7 +6,7 @@ import DataInput from "$lib/components/DataInput/DataInput.svelte";
 import { i18n } from "$lib/i18n.svelte";
 import { alertStore } from "$lib/stores/alertStore.svelte";
 import { preventDefault } from "$lib/util";
-import { Button, Card, Heading, Input } from "flowbite-svelte";
+import { Button, Card, Heading } from "flowbite-svelte";
 import { CheckCircleOutline } from "flowbite-svelte-icons";
 import { onMount } from "svelte";
 
@@ -86,9 +86,9 @@ async function submitData(): Promise<void> {
         <form onsubmit={preventDefault(submitData)} class = "space-y-4">
             <div class="m-2 mx-auto w-full flex-col space-y-6 p-2">
 
-                <DataInput component = {Input} bind:value={pw} required={true} id="restPw" kwargs={{type: "password"}} label={i18n.tr.forgotPw.inputLabelPw}/>
+                <DataInput component="input" bind:value={pw} required={true} id="restPw" kwargs={{type: "password"}} label={i18n.tr.forgotPw.inputlabelPw}/>
 
-                <DataInput component = {Input} bind:value={confirmPw} required={true} id="restConfirmPw" kwargs={{type: "password"}} label={i18n.tr.forgotPw.inputlabelPwConfirm}/>
+                <DataInput component="input" bind:value={confirmPw} required={true} id="restConfirmPw" kwargs={{type: "password"}} label={i18n.tr.forgotPw.inputlabelPwConfirm}/>
             </div>
 			<div class="m-2 flex w-full items-center justify-center p-2">
                 <Button size="md" type="submit">{i18n.tr.forgotPw.pending}</Button>
diff --git a/frontend/src/routes/userLand/children/registration/+page.svelte b/frontend/src/routes/userLand/children/registration/+page.svelte
index 2142fbeb..9015196f 100644
--- a/frontend/src/routes/userLand/children/registration/+page.svelte
+++ b/frontend/src/routes/userLand/children/registration/+page.svelte
@@ -27,7 +27,6 @@ import { displayChildImages } from "$lib/features";
 import { i18n } from "$lib/i18n.svelte";
 import { alertStore } from "$lib/stores/alertStore.svelte";
 import { currentChild } from "$lib/stores/childrenStore.svelte";
-import { activePage, componentTable } from "$lib/stores/componentStore";
 import { preventDefault } from "$lib/util";
 import {
 	Button,
@@ -156,16 +155,9 @@ async function setup(): Promise<{
 			answers = currentAnswers.data;
 			disableEdit = true;
 		}
-	}
-
-	// DEBUG/TEMPORARY: when we have no answers for existing questions,
-	// we need to create empty
-	// ones that bind to the form. this might not be necessary in the final
-	// version b/c it only can happen when the admin changes questions on the
-	// fly in the database. Doing so should eventually elicit database
-	// migration though, which should assure consistency
-	for (const question of questionnaire) {
-		if (answers[question.id] === undefined) {
+	} else {
+		// if the child doesn't exist yet (i.e. user clicked "new") then we need empty answers for all questions
+		for (const question of questionnaire) {
 			answers[question.id] = {
 				question_id: question.id,
 				answer: "",
@@ -173,16 +165,17 @@ async function setup(): Promise<{
 			};
 		}
 	}
+
 	return { questionnaire: questionnaire, answers: answers };
 }
 
-let birthmonthtext = $derived.by(() => {
-	return birthmonth !== null
-		? Intl.DateTimeFormat(i18n.locale || "en", { month: "long" }).format(
-				new Date(birthmonth.toString()),
-			)
-		: "";
-});
+let birthmonthtext = $derived(
+	birthmonth !== null
+		? new Date(birthmonth.toString()).toLocaleString(i18n.locale, {
+				month: "long",
+			})
+		: "",
+);
 
 async function submitChildData(): Promise<void> {
 	console.log("SubmitChild Data was called.");
@@ -322,6 +315,22 @@ async function submitData(): Promise<void> {
 	console.log("submission of child data successful.");
 	goto("/userLand/children/gallery");
 }
+
+// localised months
+const monthOptions = Array.from({ length: 12 }, (x, i) => {
+	return {
+		name: new Date(1980, i, 1).toLocaleString(i18n.locale, { month: "long" }),
+		value: i + 1,
+	};
+});
+// years from 2019 up to current year
+const yearOptions = Array.from(
+	{ length: new Date().getFullYear() - 2018 },
+	(x, i) => {
+		const year = new Date().getFullYear();
+		return { name: year - i, value: year - i };
+	},
+);
 </script>
 
 {#if i18n.locale}
@@ -364,7 +373,7 @@ async function submitData(): Promise<void> {
                     >
                         {#if false === disableEdit && showChildQuestions === false}
                             <DataInput
-                                    component={componentTable["input"]}
+                                    component="input"
                                     bind:value={name}
                                     label={i18n.tr.childData.childName}
                                     required={true}
@@ -375,29 +384,29 @@ async function submitData(): Promise<void> {
                             />
 
                             <DataInput
-                                    component={componentTable["input"]}
+                                    component="select"
                                     bind:value={birthmonth}
                                     label={i18n.tr.childData.childBirthMonth}
                                     required={true}
-                                    placeholder={i18n.tr.childData.pleaseEnterNumber}
                                     disabled={disableEdit}
                                     id="child_birthmonth"
-                                    kwargs = {{type: "number", min: 0, max:12, step: '1'}}
+                                    items={monthOptions}
+                                    testid="birthMonth"
                             />
 
                             <DataInput
-                                    component={componentTable["input"]}
+                                    component="select"
                                     bind:value={birthyear}
                                     label={i18n.tr.childData.childBirthYear}
                                     required={true}
-                                    placeholder={i18n.tr.childData.pleaseEnterNumber}
                                     disabled={disableEdit}
                                     id="child_birthyear"
-                                    kwargs = {{type: "number", min: 2007, step: '1', max: new Date().getFullYear()}}
+                                    items={yearOptions}
+                                    testid="birthYear"
                             />
 
                             <DataInput
-                                    component={Input}
+                                    component="input"
                                     bind:value={color}
                                     label={i18n.tr.childData.childColor}
                                     required={false}
@@ -410,7 +419,7 @@ async function submitData(): Promise<void> {
 
                             {#if displayChildImages}
                                 <DataInput
-                                        component={componentTable["fileupload"]}
+                                        component="fileupload"
                                         bind:value={image}
                                         label={image !== null ? i18n.tr.childData.imageOfChildChange : i18n.tr.childData.imageOfChildNew}
                                         required={false}
@@ -491,7 +500,7 @@ async function submitData(): Promise<void> {
                         {#if (!disableEdit || (disableEdit && showChildQuestions))}
                             {#each questionnaire as element, i}
                                 <DataInput
-                                        component={element.component ? componentTable[element.component] : undefined}
+                                        component={element?.component}
                                         bind:value={answers[element.id].answer}
                                         bind:additionalValue={answers[element.id].additional_answer}
                                         label={element?.text?.[i18n.locale].question}
@@ -505,7 +514,6 @@ async function submitData(): Promise<void> {
                                               element?.text?.[i18n.locale].options_json ?? '',
                                           )}
                                         disabled={disableEdit}
-                                        placeholder=""
                                 />
                             {/each}
                         {/if}
diff --git a/frontend/src/routes/userLand/dataInput/+page.svelte b/frontend/src/routes/userLand/dataInput/+page.svelte
index d1357a77..08941a00 100644
--- a/frontend/src/routes/userLand/dataInput/+page.svelte
+++ b/frontend/src/routes/userLand/dataInput/+page.svelte
@@ -13,7 +13,6 @@ import {
 import DataInput from "$lib/components/DataInput/DataInput.svelte";
 import { i18n } from "$lib/i18n.svelte";
 import { alertStore } from "$lib/stores/alertStore.svelte";
-import { componentTable } from "$lib/stores/componentStore";
 import { preventDefault } from "$lib/util";
 import { Button, Card, Heading } from "flowbite-svelte";
 
@@ -135,7 +134,7 @@ function safeParseJsonList(str: string | null | undefined): any[] {
                 {#each questionnaire as element, i}
                     {#if element.text && element.component}
                         <DataInput
-                                component={componentTable[element.component]}
+                                component={element.component}
                                 bind:value={answers[element.id].answer}
                                 bind:additionalValue={answers[element.id]
 								.additional_answer}
diff --git a/frontend/tests/e2e/milestonedatainputadmin.spec.ts b/frontend/tests/e2e/milestonedatainputadmin.spec.ts
index e7c29c59..e5c38359 100644
--- a/frontend/tests/e2e/milestonedatainputadmin.spec.ts
+++ b/frontend/tests/e2e/milestonedatainputadmin.spec.ts
@@ -1,4 +1,5 @@
 import { expect, test } from "@playwright/test";
+import { DateTime } from "luxon";
 import { login } from "./utils";
 
 test("/userLand/children/gallery - Childs Milestone % gets updated when you select it to have been achieved", async ({
@@ -8,6 +9,8 @@ test("/userLand/children/gallery - Childs Milestone % gets updated when you sele
 	await login(page, "admin@mondey.de", "admin");
 
 	// Create a new child so existing milestone data isn't affected
+	// set the date of birth to 6 months ago to ensure we see the expected milestones
+	const birthDate = DateTime.now().minus({ months: 6 });
 	// and use a unique name for each child to allow tests to run in parallel without affecting each other
 	const childName = `child-${crypto.randomUUID()}`;
 
@@ -17,9 +20,9 @@ test("/userLand/children/gallery - Childs Milestone % gets updated when you sele
 
 	await page.locator('input[for="Name des Kindes?"]').fill(childName);
 	await page
-		.locator('input[for="Geburtsjahr des Kindes?"]')
-		.fill((new Date().getFullYear() - 1).toString());
-	await page.locator('input[for="Geburtsmonat des Kindes?"]').fill("9");
+		.getByTestId("birthMonth")
+		.selectOption(birthDate.setLocale("de").toLocaleString({ month: "long" }));
+	await page.getByTestId("birthYear").selectOption(`${birthDate.year}`);
 
 	// Make sure the button is visible before clicking
 	const finishButton = page.getByRole("button", { name: "Abschließen" });


Reviews:
user: login: jmsssc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T13:39:50Z
user: login: jmsssc
body: Looks good to me
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-12T13:56:28Z
user: login: lkeegan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T14:14:55Z

Review comments:
diff_hunk: @@ -156,33 +155,27 @@ async function setup(): Promise<{
 			answers = currentAnswers.data;
 			disableEdit = true;
 		}
-	}
-
-	// DEBUG/TEMPORARY: when we have no answers for existing questions,
-	// we need to create empty
-	// ones that bind to the form. this might not be necessary in the final
-	// version b/c it only can happen when the admin changes questions on the
-	// fly in the database. Doing so should eventually elicit database
-	// migration though, which should assure consistency
-	for (const question of questionnaire) {
-		if (answers[question.id] === undefined) {
+	} else {
+		// if the child doesn't exist yet (i.e. user clicked "new") then we need empty answers for all questions
+		for (const question of questionnaire) {
 			answers[question.id] = {
 				question_id: question.id,
 				answer: "",
 				additional_answer: "",
 			};
 		}
 	}
+
 	return { questionnaire: questionnaire, answers: answers };
 }
 
-let birthmonthtext = $derived.by(() => {
-	return birthmonth !== null
-		? Intl.DateTimeFormat(i18n.locale || "en", { month: "long" }).format(
-				new Date(birthmonth.toString()),
-			)
-		: "";
-});
+let birthmonthtext = $derived(
path: frontend/src/routes/userLand/children/registration/+page.svelte
user: login: jmsssc
body: I note that you just need $derived if you are using a ternary in Svelte 5, and $derived.by only when you need a function to do some logic... (because I used .by unnecessarily in the earlier version of this)
created_at: 2025-05-12T13:39:50Z
author_association: COLLABORATOR
diff_hunk: @@ -156,33 +155,27 @@ async function setup(): Promise<{
 			answers = currentAnswers.data;
 			disableEdit = true;
 		}
-	}
-
-	// DEBUG/TEMPORARY: when we have no answers for existing questions,
-	// we need to create empty
-	// ones that bind to the form. this might not be necessary in the final
-	// version b/c it only can happen when the admin changes questions on the
-	// fly in the database. Doing so should eventually elicit database
-	// migration though, which should assure consistency
-	for (const question of questionnaire) {
-		if (answers[question.id] === undefined) {
+	} else {
+		// if the child doesn't exist yet (i.e. user clicked "new") then we need empty answers for all questions
+		for (const question of questionnaire) {
 			answers[question.id] = {
 				question_id: question.id,
 				answer: "",
 				additional_answer: "",
 			};
 		}
 	}
+
 	return { questionnaire: questionnaire, answers: answers };
 }
 
-let birthmonthtext = $derived.by(() => {
-	return birthmonth !== null
-		? Intl.DateTimeFormat(i18n.locale || "en", { month: "long" }).format(
-				new Date(birthmonth.toString()),
-			)
-		: "";
-});
+let birthmonthtext = $derived(
path: frontend/src/routes/userLand/children/registration/+page.svelte
user: login: lkeegan
body: yes if it's a one liner then you can just use `$derived` and omit the function definition (although using `$derived.by` with a function is also perfectly fine - as far as I know it's just syntactic sugar)
created_at: 2025-05-12T14:14:55Z
author_association: MEMBER

PR comments:
user: login: codecov-commenter
created_at: 2025-05-12T08:53:01Z
body: ## [Codecov](https://app.codecov.io/gh/ssciwr/mondey/pull/364?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) Report
Attention: Patch coverage is `0%` with `38 lines` in your changes missing coverage. Please review.
> Project coverage is 26.33%. Comparing base [(`d911c4c`)](https://app.codecov.io/gh/ssciwr/mondey/commit/d911c4c623c75b34d0ff8823baf72a3ba81c8d82?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) to head [(`4e132ae`)](https://app.codecov.io/gh/ssciwr/mondey/commit/4e132ae45d666b379f2af9f58da7dca0aa5a1f6b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr).

| [Files with missing lines](https://app.codecov.io/gh/ssciwr/mondey/pull/364?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) | Patch % | Lines |
|---|---|---|
| [...routes/userLand/children/registration/+page.svelte](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&filepath=frontend%2Fsrc%2Froutes%2FuserLand%2Fchildren%2Fregistration%2F%2Bpage.svelte&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr#diff-ZnJvbnRlbmQvc3JjL3JvdXRlcy91c2VyTGFuZC9jaGlsZHJlbi9yZWdpc3RyYXRpb24vK3BhZ2Uuc3ZlbHRl) | 0.00% | [20 Missing :warning: ](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) |
| [...tend/src/lib/components/DataInput/DataInput.svelte](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&filepath=frontend%2Fsrc%2Flib%2Fcomponents%2FDataInput%2FDataInput.svelte&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr#diff-ZnJvbnRlbmQvc3JjL2xpYi9jb21wb25lbnRzL0RhdGFJbnB1dC9EYXRhSW5wdXQuc3ZlbHRl) | 0.00% | [13 Missing :warning: ](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) |
| [...end/src/routes/resetPassword/[[code]]/+page.svelte](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&filepath=frontend%2Fsrc%2Froutes%2FresetPassword%2F%5B%5Bcode%5D%5D%2F%2Bpage.svelte&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr#diff-ZnJvbnRlbmQvc3JjL3JvdXRlcy9yZXNldFBhc3N3b3JkL1tbY29kZV1dLytwYWdlLnN2ZWx0ZQ==) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) |
| [...ntend/src/lib/components/UserForgotPassword.svelte](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&filepath=frontend%2Fsrc%2Flib%2Fcomponents%2FUserForgotPassword.svelte&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr#diff-ZnJvbnRlbmQvc3JjL2xpYi9jb21wb25lbnRzL1VzZXJGb3Jnb3RQYXNzd29yZC5zdmVsdGU=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) |
| [...rontend/src/routes/userLand/dataInput/+page.svelte](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&filepath=frontend%2Fsrc%2Froutes%2FuserLand%2FdataInput%2F%2Bpage.svelte&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr#diff-ZnJvbnRlbmQvc3JjL3JvdXRlcy91c2VyTGFuZC9kYXRhSW5wdXQvK3BhZ2Uuc3ZlbHRl) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ssciwr/mondey/pull/364?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #364      +/-   ##
==========================================
+ Coverage   26.17%   26.33%   +0.15%     
==========================================
  Files         133      130       -3     
  Lines        6544     6498      -46     
  Branches      118      115       -3     
==========================================
- Hits         1713     1711       -2     
+ Misses       4819     4776      -43     
+ Partials       12       11       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ssciwr/mondey/pull/364?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ssciwr).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-12T09:46:42Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ssciwr_mondey&pullRequest=364) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=ssciwr_mondey&pullRequest=364&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ssciwr_mondey&pullRequest=364&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ssciwr_mondey&pullRequest=364&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=ssciwr_mondey&pullRequest=364&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ssciwr_mondey&pullRequest=364&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ssciwr_mondey&pullRequest=364)


author_association: NONE