From 047476af2a3dcb2f5b8bfd2b112dc858fdd44b2b Mon Sep 17 00:00:00 2001 From: EETagent Date: Tue, 6 Dec 2022 16:01:23 +0100 Subject: [PATCH 1/7] feat: nested form --- frontend/src/lib/stores/candidate.ts | 34 +-- .../(authenticated)/register/+page.svelte | 196 +++++++++++------- 2 files changed, 139 insertions(+), 91 deletions(-) diff --git a/frontend/src/lib/stores/candidate.ts b/frontend/src/lib/stores/candidate.ts index 0d152af..e0ba7ee 100644 --- a/frontend/src/lib/stores/candidate.ts +++ b/frontend/src/lib/stores/candidate.ts @@ -1,21 +1,25 @@ import { writable } from 'svelte/store'; export interface CandidateData { - name?: string; - surname?: string; - birthplace?: string; - birthdate?: string; - address?: string; - telephone?: string; - citizenship?: string; - email?: string; - sex?: string; - study?: string; - personalIdNumber?: string; - parentName?: string; - parentSurname?: string; - parentTelephone?: string; - parentEmail?: string; + candidate: { + name?: string; + surname?: string; + birthplace?: string; + birthdate?: string; + address?: string; + telephone?: string; + citizenship?: string; + email?: string; + sex?: string; + study?: string; + personalIdNumber?: string; + }; + parents: Array<{ + name?: string; + surname?: string; + telephone?: string; + email?: string; + }>; } export interface CandidatePreview { diff --git a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte index 107bdc6..534a62f 100644 --- a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte +++ b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte @@ -14,54 +14,96 @@ import TextField from '$lib/components/textfield/TextField.svelte'; import { createForm } from 'svelte-forms-lib'; + import type { Writable } from 'svelte/store'; import * as yup from 'yup'; const pageCount = 3; let pageIndex = 0; let pagesFilled = 0; - const formInitialValues = { - name: '', - surname: '', - email: '', - telephone: '', - birthplace: '', - birthdate: '', - sex: '', - address: '', - citizenship: '', - personalIdNumber: '', - study: '', - parentName: '', - parentSurname: '', - parentTelephone: '', - parentEmail: '' + interface FormInterface + { + candidate: { + name: string; + surname: string; + email: string; + telephone: string; + birthplace: string; + birthdate: string; + sex: string; + address: string; + citizenship: string; + personalIdNumber: string; + study: string; + } + parents: Array<{ + name: string; + surname: string; + email: string; + telephone: string; + }>; + + } + const formInitialValues: FormInterface = { + candidate: { + name: '', + surname: '', + email: '', + telephone: '', + birthplace: '', + birthdate: '', + sex: '', + address: '', + citizenship: '', + personalIdNumber: '', + study: '' + }, + parents: [ + { + name: '', + surname: '', + email: '', + telephone: '' + }, + { + name: '', + surname: '', + email: '', + telephone: '' + } + ] }; const { form, errors, handleSubmit, handleChange } = createForm({ initialValues: formInitialValues, validationSchema: yup.object().shape({ - name: yup.string().required(), - surname: yup.string(), - email: yup.string().email().required(), - telephone: yup - .string() - .required() - .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), - birthplace: yup.string().required(), - birthdate: yup.string().required(), - sex: yup.string(), - address: yup.string().required(), - citizenship: yup.string().required(), - personalIdNumber: yup.string().required(), - study: yup.string().required(), - parentName: yup.string(), - parentSurname: yup.string(), - parentTelephone: yup - .string() - .required() - .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), - parentEmail: yup.string().email().required() + candidate: yup.object().shape({ + name: yup.string().required(), + surname: yup.string(), + email: yup.string().email().required(), + telephone: yup + .string() + .required() + .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), + birthplace: yup.string().required(), + birthdate: yup.string().required(), + sex: yup.string(), + address: yup.string().required(), + citizenship: yup.string().required(), + personalIdNumber: yup.string().required(), + study: yup.string().required() + }).required(), + parents: yup.array().of( + yup.object().shape({ + name: yup.string().required(), + surname: yup.string().required(), + email: yup.string().email().required(), + telephone: yup + .string() + .required() + .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), + }) + ).required() }), onSubmit: async (values) => { @@ -70,7 +112,7 @@ console.log('submit'); // @ts-ignore // love javascript delete values.undefined; - values.birthdate = '2000-01-01'; // TODO: reformat user typed date + values.candidate.birthdate = '2000-01-01'; // TODO: reformat user typed date await apiFillDetails(values); goto('/dashboard'); } catch (e) { @@ -80,35 +122,35 @@ } }); - $: console.log($errors); + $: typedErrors = errors as Writable; const isPageInvalid = (): boolean => { switch (pageIndex) { case 0: - if ($errors.name || $errors.email || $errors.telephone) { + if ($typedErrors["candidate"]["name"] || $typedErrors["candidate"]["email"] || $typedErrors["candidate"]["telephone"]) { return true; } break; case 1: if ( - /* $errors.birthdurname || */ $errors.birthplace || - $errors.birthdate /* || $errors.sex */ + /* $typedErrors.birthdurname || */ $typedErrors["candidate"]["birthplace"] || + $typedErrors["candidate"]["birthdate"] /* || $typedErrors.sex */ ) { return true; } break; case 2: - if ($errors.address || $errors.parentEmail || $errors.parentTelephone) { + if ($typedErrors["candidate"]["address"] || $typedErrors["parents"][0]["email"] || $typedErrors["parents"][0]["telephone"]) { return true; } break; case 3: if ( - $errors.citizenship || - $errors.personalIdNumber || - $errors.study //|| - // $errors.applicationId + $typedErrors["candidate"]["citizenship"] || + $typedErrors["candidate"]["personalIdNumber"] || + $typedErrors["candidate"]["study"] //|| + // $typedErrors.applicationId ) { return true; } @@ -135,27 +177,27 @@
@@ -169,23 +211,23 @@
-
+
@@ -194,17 +236,17 @@
@@ -219,9 +261,9 @@
@@ -229,17 +271,17 @@
@@ -254,9 +296,9 @@
@@ -267,16 +309,16 @@
@@ -295,6 +337,7 @@ pagesFilled++; pageIndex++; } + // @ts-ignore errors.set(formInitialValues); }} value={pageIndex === pageCount ? 'Odeslat' : 'Pokračovat'} @@ -315,6 +358,7 @@ if (isPageInvalid()) return; pagesFilled++; pageIndex++; + // @ts-ignore errors.set(formInitialValues); } }} From 214aa7ace8fd9ca825cf32fd733d6967b99fb526 Mon Sep 17 00:00:00 2001 From: EETagent Date: Tue, 6 Dec 2022 17:26:03 +0100 Subject: [PATCH 2/7] fix: fix non submitting --- .../(candidate)/(authenticated)/register/+page.svelte | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte index 534a62f..5a56d39 100644 --- a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte +++ b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte @@ -65,12 +65,6 @@ email: '', telephone: '' }, - { - name: '', - surname: '', - email: '', - telephone: '' - } ] }; From c78529b9bb0183bc32f3889f2aa05606b81d2f79 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Tue, 6 Dec 2022 17:42:38 +0100 Subject: [PATCH 3/7] feat: post candidate data --- frontend/src/lib/@api/candidate.ts | 10 ++++- .../(authenticated)/register/+page.svelte | 42 +++++++------------ 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/frontend/src/lib/@api/candidate.ts b/frontend/src/lib/@api/candidate.ts index fcd82b9..a8182c8 100644 --- a/frontend/src/lib/@api/candidate.ts +++ b/frontend/src/lib/@api/candidate.ts @@ -79,11 +79,19 @@ export const apiLogin = async (data: CandidateLogin): Promise => { }; export const apiFillDetails = async (data: CandidateData): Promise => { - Object.keys(data).forEach((key) => { + Object.keys(data.candidate).forEach((key) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore data[key] = DOMPurify.sanitize(data[key]); }); + data.parents.forEach((parent) => { + Object.keys(parent).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + parent[key] = DOMPurify.sanitize(parent[key]); + }); + }); + console.log(data); try { const res = await axios.post(API_URL + '/candidate/details', data, { withCredentials: true }); return res.data; diff --git a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte index 5a56d39..1868ed9 100644 --- a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte +++ b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte @@ -12,6 +12,7 @@ import NameField from '$lib/components/textfield/NameField.svelte'; import TelephoneField from '$lib/components/textfield/TelephoneField.svelte'; import TextField from '$lib/components/textfield/TextField.svelte'; + import type { CandidateData } from '$lib/stores/candidate'; import { createForm } from 'svelte-forms-lib'; import type { Writable } from 'svelte/store'; @@ -21,30 +22,7 @@ let pageIndex = 0; let pagesFilled = 0; - interface FormInterface - { - candidate: { - name: string; - surname: string; - email: string; - telephone: string; - birthplace: string; - birthdate: string; - sex: string; - address: string; - citizenship: string; - personalIdNumber: string; - study: string; - } - parents: Array<{ - name: string; - surname: string; - email: string; - telephone: string; - }>; - - } - const formInitialValues: FormInterface = { + const formInitialValues = { candidate: { name: '', surname: '', @@ -100,13 +78,25 @@ ).required() }), - onSubmit: async (values) => { + onSubmit: async (values: CandidateData) => { + console.log("page count: " + pageIndex); + console.log(values.candidate); + console.log(values.parents); + console.log(values); if (pageIndex === pageCount) { try { console.log('submit'); // @ts-ignore // love javascript delete values.undefined; - values.candidate.birthdate = '2000-01-01'; // TODO: reformat user typed date + // convert birthdate from dd.mm.yyyy to yyyy-mm-dd + let birthdate_formttted = values.candidate.birthdate! + .split('.') + .map((x) => x.padStart(2, '0')) + .reverse() + .join('-'); + + values.candidate.birthdate = birthdate_formttted; + await apiFillDetails(values); goto('/dashboard'); } catch (e) { From 8369b7dc930fdd9d47813609983aca49061ec7e8 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Tue, 6 Dec 2022 17:44:15 +0100 Subject: [PATCH 4/7] fix: candidate details on dashboard --- .../(candidate)/(authenticated)/dashboard/+page.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/(candidate)/(authenticated)/dashboard/+page.svelte b/frontend/src/routes/(candidate)/(authenticated)/dashboard/+page.svelte index 0fd5d9d..50fbfe3 100644 --- a/frontend/src/routes/(candidate)/(authenticated)/dashboard/+page.svelte +++ b/frontend/src/routes/(candidate)/(authenticated)/dashboard/+page.svelte @@ -34,8 +34,8 @@
- - {$candidateData.email} + + {$candidateData.candidate.email} Uchazeč na SSPŠ
@@ -51,8 +51,8 @@
- - {$candidateData.email} + + {$candidateData.candidate.email} Uchazeč na SSPŠ
From 05387691e35f6c64a396de2d254bfab1f7f1a6ac Mon Sep 17 00:00:00 2001 From: EETagent Date: Tue, 6 Dec 2022 17:55:02 +0100 Subject: [PATCH 5/7] fix: fix sanitize --- frontend/src/lib/@api/candidate.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/@api/candidate.ts b/frontend/src/lib/@api/candidate.ts index a8182c8..251a64e 100644 --- a/frontend/src/lib/@api/candidate.ts +++ b/frontend/src/lib/@api/candidate.ts @@ -79,18 +79,21 @@ export const apiLogin = async (data: CandidateLogin): Promise => { }; export const apiFillDetails = async (data: CandidateData): Promise => { + // Sanitize candidate data Object.keys(data.candidate).forEach((key) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - data[key] = DOMPurify.sanitize(data[key]); + data.candidate[key] = DOMPurify.sanitize(data.candidate[key]); }); - data.parents.forEach((parent) => { - Object.keys(parent).forEach((key) => { + // Sanitize parents data + for (let index = 0; index < data.parents.length; index++) { + Object.keys(data.parents[index]).forEach((key) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - parent[key] = DOMPurify.sanitize(parent[key]); + data.parents[index][key] = DOMPurify.sanitize(data.parents[index][key]); }); - }); + } + console.log(data); try { const res = await axios.post(API_URL + '/candidate/details', data, { withCredentials: true }); From a5331adfcaed6f878ec9e777f749c8843f0fc045 Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Tue, 6 Dec 2022 17:56:23 +0100 Subject: [PATCH 6/7] feat: birthdate regex --- .../routes/(candidate)/(authenticated)/register/+page.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte index 1868ed9..b9216f7 100644 --- a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte +++ b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte @@ -58,7 +58,10 @@ .required() .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), birthplace: yup.string().required(), - birthdate: yup.string().required(), + birthdate: yup + .string() + .required() + .matches(/^([0-3]?[0-9])\.([1-9]|1[0-2])\.[0-9]{4}$/), sex: yup.string(), address: yup.string().required(), citizenship: yup.string().required(), From 391a48a45e85a6c03a251320aaa6f38cf4803a2e Mon Sep 17 00:00:00 2001 From: EETagent Date: Tue, 6 Dec 2022 19:09:42 +0100 Subject: [PATCH 7/7] feat: add types for form errors --- .../(authenticated)/register/+page.svelte | 145 +++++++++++------- 1 file changed, 86 insertions(+), 59 deletions(-) diff --git a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte index b9216f7..7d1d74d 100644 --- a/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte +++ b/frontend/src/routes/(candidate)/(authenticated)/register/+page.svelte @@ -34,7 +34,7 @@ address: '', citizenship: '', personalIdNumber: '', - study: '' + study: '', }, parents: [ { @@ -42,47 +42,52 @@ surname: '', email: '', telephone: '' - }, + } ] }; + const formValidationSchema = yup.object().shape({ + candidate: yup.object().shape({ + name: yup.string().required(), + surname: yup.string(), + email: yup.string().email().required(), + telephone: yup + .string() + .required() + .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), + birthplace: yup.string().required(), + birthdate: yup + .string() + .required() + .matches(/^([0-3]?[0-9])\.([1-9]|1[0-2])\.[0-9]{4}$/), + sex: yup.string(), + address: yup.string().required(), + citizenship: yup.string().required(), + personalIdNumber: yup.string().required(), + study: yup.string().required() + }), + parents: yup + .array() + .of( + yup.object().shape({ + name: yup.string().required(), + surname: yup.string().required(), + email: yup.string().email().required(), + telephone: yup + .string() + .required() + .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/) + }) + ) + .required() + }); + const { form, errors, handleSubmit, handleChange } = createForm({ initialValues: formInitialValues, - validationSchema: yup.object().shape({ - candidate: yup.object().shape({ - name: yup.string().required(), - surname: yup.string(), - email: yup.string().email().required(), - telephone: yup - .string() - .required() - .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), - birthplace: yup.string().required(), - birthdate: yup - .string() - .required() - .matches(/^([0-3]?[0-9])\.([1-9]|1[0-2])\.[0-9]{4}$/), - sex: yup.string(), - address: yup.string().required(), - citizenship: yup.string().required(), - personalIdNumber: yup.string().required(), - study: yup.string().required() - }).required(), - parents: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - surname: yup.string().required(), - email: yup.string().email().required(), - telephone: yup - .string() - .required() - .matches(/^\+\d{1,3} \d{3} \d{3} \d{3}$/), - }) - ).required() - }), + validationSchema: formValidationSchema, onSubmit: async (values: CandidateData) => { - console.log("page count: " + pageIndex); + console.log('page count: ' + pageIndex); console.log(values.candidate); console.log(values.parents); console.log(values); @@ -92,10 +97,10 @@ // @ts-ignore // love javascript delete values.undefined; // convert birthdate from dd.mm.yyyy to yyyy-mm-dd - let birthdate_formttted = values.candidate.birthdate! - .split('.') + let birthdate_formttted = values.candidate + .birthdate!.split('.') .map((x) => x.padStart(2, '0')) - .reverse() + .reverse() .join('-'); values.candidate.birthdate = birthdate_formttted; @@ -109,34 +114,56 @@ } }); - $: typedErrors = errors as Writable; + type FormErrorType = { + [K in keyof typeof formInitialValues]: typeof formInitialValues[K] extends Record< + string, + unknown + > + ? { + [K2 in keyof typeof formInitialValues[K]]: string; + } + : typeof formInitialValues[K] extends Array> + ? Array<{ [K3 in keyof typeof formInitialValues[K][number]]: string }> + : string; + }; + + // TODO: https://github.com/tjinauyeung/svelte-forms-lib/issues/171!! (Zatím tenhle mega typ) + $: typedErrors = errors as unknown as Writable; const isPageInvalid = (): boolean => { switch (pageIndex) { case 0: - if ($typedErrors["candidate"]["name"] || $typedErrors["candidate"]["email"] || $typedErrors["candidate"]["telephone"]) { + if ( + $typedErrors['candidate']['name'] || + $typedErrors['candidate']['email'] || + $typedErrors['candidate']['telephone'] + ) { return true; } break; case 1: if ( - /* $typedErrors.birthdurname || */ $typedErrors["candidate"]["birthplace"] || - $typedErrors["candidate"]["birthdate"] /* || $typedErrors.sex */ + /* $typedErrors.birthdurname || */ $typedErrors['candidate']['birthplace'] || + $typedErrors['candidate']['birthdate'] /* || $typedErrors.sex */ ) { return true; } break; case 2: - if ($typedErrors["candidate"]["address"] || $typedErrors["parents"][0]["email"] || $typedErrors["parents"][0]["telephone"]) { + if ( + $typedErrors['candidate']['address'] || + $typedErrors['parents'][0]['email'] || + $typedErrors['parents'][0]['telephone'] + ) { return true; } break; case 3: if ( - $typedErrors["candidate"]["citizenship"] || - $typedErrors["candidate"]["personalIdNumber"] || - $typedErrors["candidate"]["study"] //|| + $typedErrors['candidate']['citizenship'] || + $typedErrors['candidate']['personalIdNumber'] || + $typedErrors['candidate']['study'] //|| // $typedErrors.applicationId ) { return true; @@ -164,7 +191,7 @@