Merge pull request #174 from EETagent/frontend_school_order_selection

Frontend school order selection
This commit is contained in:
Vojtěch Jungmann 2023-01-26 23:27:05 +01:00 committed by GitHub
commit 95d74d192b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 759 additions and 284 deletions

View file

@ -1,7 +1,7 @@
{
"baseLocale": "cs",
"adapter": "svelte",
"esmImports": true,
"outputPath": "./src/translations",
"$schema": "https://unpkg.com/typesafe-i18n@5.20.0/schema/typesafe-i18n.json"
}
"baseLocale": "cs",
"adapter": "svelte",
"esmImports": true,
"outputPath": "./src/translations",
"$schema": "https://unpkg.com/typesafe-i18n@5.20.0/schema/typesafe-i18n.json"
}

View file

@ -4,7 +4,6 @@
export let linkOk: boolean = false;
export let linkError: boolean = false;
export let applications: Array<number>;
let title1 = $LL.components.checkbox.accountLinkCheckBox.multiple.title({
first: applications[0],
second: applications[1]
@ -20,8 +19,6 @@
title2 = $LL.components.checkbox.accountLinkCheckBox.single.title2();
}
$: console.log(linkOk, linkError);
export let error: string = '';
const switchSelection = (id: number) => {

View file

@ -13,7 +13,9 @@
<div class="w-full text-lg font-semibold">{$LL.components.checkbox.gdprCheckBox.title()}</div>
<div class="w-full text-sm">{$LL.components.checkbox.gdprCheckBox.description()}</div>
<div class="w-full text-sm"><a class="underline" href="/gdpr">{$LL.components.checkbox.gdprCheckBox.here()}</a></div>
<div class="w-full text-sm">
<a class="underline" href="/gdpr">{$LL.components.checkbox.gdprCheckBox.here()}</a>
</div>
</div>
</label>

View file

@ -0,0 +1,82 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
export let personalIdOk: boolean = false;
export let personalIdErr: boolean = false;
export let personalIdNumber: string;
let titleOk = $LL.components.checkbox.personalIdConfirmCheckBox.titleOk({
personalId: personalIdNumber
});
let titleErr = $LL.components.checkbox.personalIdConfirmCheckBox.titleErr({
personalId: personalIdNumber
});
export let error: string = '';
const switchSelection = (id: number) => {
if (id === 0) {
personalIdOk = true;
personalIdErr = false;
} else {
personalIdOk = false;
personalIdErr = true;
}
};
</script>
<div>
<input
on:click={(_) => switchSelection(0)}
class:error
on:change
type="checkbox"
id="linkOk"
checked={personalIdOk}
class="peer hidden"
/>
<label for="linkOk" class="peer-checked:border-sspsBlue peer-checked:text-gray-600" class:error>
<div class="block">
<span class="text-2xl">📜</span>
<div class="w-full text-lg font-semibold">
{titleOk}
</div>
<div class="w-full text-sm">{$LL.components.checkbox.personalIdConfirmCheckBox.ok()}</div>
</div>
</label>
</div>
<div class="mt-2">
<input
on:click={(_) => switchSelection(1)}
on:change
type="checkbox"
id="linkError"
checked={personalIdErr}
class="peer hidden"
/>
<label for="linkError" class="peer-checked:border-sspsBlue peer-checked:text-gray-600">
<div class="block">
<span class="text-2xl">📜</span>
<div class="w-full text-lg font-semibold">
{titleErr}
</div>
<div class="w-full text-sm">
{$LL.components.checkbox.personalIdConfirmCheckBox.whatHappened()}
</div>
</div>
</label>
</div>
<style lang="postcss">
label {
@apply inline-flex w-full items-center justify-between;
@apply cursor-pointer;
@apply bg-white p-5 text-gray-500;
@apply hover:bg-gray-50 hover:text-gray-600;
@apply rounded-lg border-2 border-gray-200;
}
.error {
@apply border-red-700;
}
</style>

View file

@ -160,30 +160,33 @@
<span class="font-bold">{$baseCandidateData.applications[1]}</span></span
>
{/if}
<span>{$LL.input.address()}: <span class="font-bold">{$candidateData.candidate.address}</span></span>
<span
>{$LL.input.birthDate()}: <span class="font-bold">{$candidateData.candidate.birthdate}</span
></span
>{$LL.input.address()}:
<span class="font-bold">{$candidateData.candidate.address}</span></span
>
<span
>{$LL.input.birthPlace()}: <span class="font-bold">{$candidateData.candidate.birthplace}</span
></span
>{$LL.input.birthDate()}:
<span class="font-bold">{$candidateData.candidate.birthdate}</span></span
>
<span
>{$LL.input.personalIdentificationNumber()}: <span class="font-bold"
>{$candidateData.candidate.personalIdNumber}</span
></span
>{$LL.input.birthPlace()}:
<span class="font-bold">{$candidateData.candidate.birthplace}</span></span
>
<span
>{$LL.input.schoolIzo()}: <span class="font-bold">{$candidateData.candidate.schoolName}</span
></span
>{$LL.input.personalIdentificationNumber()}:
<span class="font-bold">{$candidateData.candidate.personalIdNumber}</span></span
>
<span
>{$LL.input.insuranceNumber()}: <span class="font-bold"
>{$candidateData.candidate.healthInsurance}</span
></span
>{$LL.input.schoolIzo()}:
<span class="font-bold">{$candidateData.candidate.schoolName}</span></span
>
<span>{$LL.input.telephone()}: <span class="font-bold">{$candidateData.candidate.telephone}</span></span
<span
>{$LL.input.insuranceNumber()}:
<span class="font-bold">{$candidateData.candidate.healthInsurance}</span></span
>
<span
>{$LL.input.telephone()}:
<span class="font-bold">{$candidateData.candidate.telephone}</span></span
>
</div>
<div
@ -202,7 +205,9 @@
>{parent.name + ' ' + parent.surname}</span
>
<span>{$LL.input.email()}: <span class="font-bold">{parent.email}</span></span>
<span>{$LL.input.telephone()}: <span class="font-bold">{parent.telephone}</span></span>
<span
>{$LL.input.telephone()}: <span class="font-bold">{parent.telephone}</span></span
>
</div>
{/each}
</div>

View file

@ -105,7 +105,8 @@
{#if status === 'uploaded'}
<button
class="mr-3 rounded-xl bg-[#ef8b46] py-0.5 px-2 text-white shadow-md transition-all duration-300 hover:bg-orange-400"
on:click={debounce(() => dispatch('delete'), 150)}>{$LL.components.dashboard.dashboardUploadCard.delete()}</button
on:click={debounce(() => dispatch('delete'), 150)}
>{$LL.components.dashboard.dashboardUploadCard.delete()}</button
>
{/if}
<StatusNotificationDot {status} />
@ -133,9 +134,16 @@
>
<div class="hidden items-center xl:block">
{#if bytesTotal === 0 || Math.round(progress * 100) === 100}
<h2 class="text-xl font-bold">{status === 'submitted' ? $LL.components.dashboard.dashboardUploadCard.sent() : $LL.components.dashboard.dashboardUploadCard.uploaded()}</h2>
<h2 class="text-xl font-bold">
{status === 'submitted'
? $LL.components.dashboard.dashboardUploadCard.sent()
: $LL.components.dashboard.dashboardUploadCard.uploaded()}
</h2>
{:else}
<h2 class="text-xl">{$LL.components.dashboard.dashboardUploadCard.uploaded()} {((bytesTotal / 1_000_000) * progress).toFixed(1)} MB</h2>
<h2 class="text-xl">
{$LL.components.dashboard.dashboardUploadCard.uploaded()}
{((bytesTotal / 1_000_000) * progress).toFixed(1)} MB
</h2>
<h2 class="self-center text-xl">z {(bytesTotal / 1_000_000).toFixed(1)} MB</h2>
{/if}
</div>

View file

@ -29,7 +29,7 @@
}
return grades;
};
let gradesLocal: Array<Grade> =
grades.length > 0
? convertGradeBackendToGrade(grades)

View file

@ -50,7 +50,7 @@
.view {
@apply z-10;
@apply absolute top-0 right-0 bottom-0 left-0 m-auto md:top-auto md:bottom-auto md:left-auto md:m-0;
@apply md:h-screen md:w-[50vw];
@apply md:h-screen md:w-[60vw];
@apply md:my-auto;
@apply bg-white;
}

View file

@ -0,0 +1,41 @@
<script lang="ts">
export let itemLabel: string;
export let highlighted: boolean;
</script>
<li class="autocomplete-items" class:autocomplete-active={highlighted} on:click on:keydown={null}>
{@html itemLabel}
</li>
<style>
li.autocomplete-items {
list-style: none;
border-bottom: 1px solid #d4d4d4;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
padding: 10px;
cursor: pointer;
background-color: #fff;
}
li.autocomplete-items:hover {
/*when hovering an item:*/
background-color: #81921f;
color: white;
}
li.autocomplete-items:active {
/*when navigating through the items using the arrow keys:*/
background-color: DodgerBlue !important;
color: #ffffff;
}
.autocomplete-active {
/*when navigating through the items using the arrow keys:*/
background-color: DodgerBlue !important;
color: #ffffff;
}
</style>

View file

@ -0,0 +1,145 @@
<script lang="ts">
import LL from '$i18n/i18n-svelte';
import schoollistString from '$lib/assets/schoollist.txt?raw';
import School from './School.svelte';
import type { School as SchoolType } from '$lib/stores/candidate';
const schoolList: Array<string> = schoollistString.split(';');
let filteredSchools: Array<string> = [];
const filterSchools = () => {
let storageArr: Array<string> = [];
if (schoolNameInputValue) {
schoolList.forEach((school) => {
if (school.toLowerCase().startsWith(schoolNameInputValue.toLowerCase())) {
storageArr = [...storageArr, makeMatchBold(school)];
}
});
}
filteredSchools = storageArr;
};
let searchInput: HTMLInputElement;
let optionsList: HTMLUListElement;
let schoolNameInputValue = '';
let schoolFieldInputValue = '';
$: if (!schoolNameInputValue) {
filteredSchools = [];
hiLiteIndex = -1;
}
const setInputVal = (schoolName: string) => {
schoolNameInputValue = removeBold(schoolName);
filteredSchools = [];
hiLiteIndex = -1;
searchInput.focus();
};
const makeMatchBold = (str: string) => {
let matched = str.substring(0, schoolNameInputValue.length);
let makeBold = `<strong>${matched}</strong>`;
let boldedMatch = str.replace(matched, makeBold);
return boldedMatch;
};
const removeBold = (str: string) => {
return str.replace(/<(.)*?>/g, '');
};
let hiLiteIndex: number = 0;
const navigateList = (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
if (hiLiteIndex < filteredSchools.length - 1) {
hiLiteIndex++;
// scroll optionsList
let option = optionsList.children[hiLiteIndex];
if (option) {
option.scrollIntoView({ block: 'nearest' });
}
}
} else if (e.key === 'ArrowUp') {
if (hiLiteIndex > 0) {
hiLiteIndex--;
}
} else if (e.key === 'Enter') {
if (hiLiteIndex > -1) {
setInputVal(filteredSchools[hiLiteIndex]);
}
}
};
export let selectedSchool: SchoolType;
export let error: string = '';
schoolFieldInputValue = selectedSchool.field;
schoolNameInputValue = selectedSchool.name;
$: selectedSchool.field = schoolFieldInputValue;
$: selectedSchool.name = schoolNameInputValue;
</script>
<svelte:window on:keydown={navigateList} />
<div class="autocomplete">
<div class="flex">
<input
class:error
class="flex-1"
type="text"
bind:this={searchInput}
bind:value={schoolNameInputValue}
on:input={filterSchools}
placeholder={$LL.input.schoolName()}
/>
<input
class:error
class="ml-2 w-2/5"
type="text"
bind:value={schoolFieldInputValue}
placeholder={$LL.input.fieldOfStudy()}
/>
</div>
{#if filteredSchools.length > 0}
<ul bind:this={optionsList} class="schoolAutocompleteList">
{#each filteredSchools as country, i}
<School
itemLabel={country}
highlighted={i === hiLiteIndex}
on:click={() => setInputVal(country)}
/>
{/each}
</ul>
{/if}
</div>
<style lang="postcss">
div,
input {
@apply w-full;
}
div {
@apply relative flex items-center justify-center;
}
input {
@apply hover:border-sspsBlue w-full rounded-lg border border-2 bg-[#f8fafb] p-3 text-xl shadow-lg outline-none transition-colors duration-300;
}
.error {
@apply border-red-700;
}
.autocomplete {
@apply relative;
}
.schoolAutocompleteList {
@apply absolute top-20 z-50;
@apply w-full;
@apply max-h-72 overflow-scroll;
}
</style>

View file

@ -1,13 +1,19 @@
import type { GradeBackend } from '$lib/components/grades/GradesTable.svelte';
import { writable } from 'svelte/store';
export interface School {
name: string;
field: string;
}
export interface CandidateData {
candidate: {
name: string;
surname: string;
birthSurname: string;
birthplace: string;
birthdate: string;
address: string;
letterAddress: string;
telephone: string;
citizenship: string;
email: string;
@ -16,6 +22,8 @@ export interface CandidateData {
schoolName: string;
healthInsurance: string;
grades: Array<GradeBackend>;
firstSchool: School;
secondSchool: School;
testLanguage: string;
};
parents: Array<{
@ -66,9 +74,11 @@ export const candidateData = writable<CandidateData>({
candidate: {
name: '',
surname: '',
birthSurname: '',
birthplace: '',
birthdate: '',
address: '',
letterAddress: '',
telephone: '',
citizenship: '',
email: '',
@ -77,6 +87,8 @@ export const candidateData = writable<CandidateData>({
schoolName: '',
healthInsurance: '',
grades: [],
firstSchool: { name: '', field: '' },
secondSchool: { name: '', field: '' },
testLanguage: ''
},
parents: []

View file

@ -0,0 +1,75 @@
// TODO: nefunguje pro lidi nar. pred 1.1.1954 :D
export const isPersonalIdNumberValid = (personalIdNumber: string): boolean => {
const idFmt = personalIdNumber.split('/').join('');
const lastDigitCheck =
Number(idFmt.slice(0, 9)) % 11 === Number(idFmt.at(-1)) ||
Number(idFmt.slice(0, 9)) % 11 === 10; // an edge case that could occur
const divisibleBy11 = Number(idFmt) % 11 === 0;
if (lastDigitCheck && divisibleBy11) {
return true;
} else {
return false;
}
};
export const isPersonalIdNumberWithBirthdateValid = (
personalIdNumber: string,
birthdate: string
): boolean => {
const dateFmt = birthdate
.split('.')
.map((x) => x.padStart(2, '0'))
.reverse()
.join('')
.slice(2);
const idFmt = personalIdNumber.split('/').join('');
const divisionValid = isPersonalIdNumberValid(personalIdNumber);
const idMonth = Number(idFmt.slice(2, 4));
const dateMonth = Number(dateFmt.slice(2, 4));
const monthValid =
idMonth === dateMonth ||
idMonth === dateMonth + 50 ||
idMonth === dateMonth + 20 ||
idMonth === dateMonth + 70;
if (
idFmt.slice(0, 2) === dateFmt.slice(0, 2) &&
monthValid &&
idFmt.slice(4, 6) === dateFmt.slice(4, 6) &&
divisionValid
) {
return true;
} else {
return false;
}
};
export const deriveBirthdateFromPersonalId = (
personalIdNumber: string
): [birthdate: string, sex: 'MUŽ' | 'ŽENA'] => {
const year = Number(personalIdNumber.slice(0, 2));
const idMonth = Number(personalIdNumber.slice(2, 4));
let month;
let sex: 'MUŽ' | 'ŽENA';
if (idMonth > 12 && idMonth <= 32) {
month = idMonth - 20;
sex = 'MUŽ';
} else if (idMonth > 50 && idMonth <= 52) {
month = idMonth - 50;
sex = 'ŽENA';
} else if (idMonth > 70 && idMonth <= 82) {
month = idMonth - 70;
sex = 'ŽENA';
} else {
month = idMonth;
sex = 'MUŽ';
}
const day = Number(personalIdNumber.slice(4, 6));
const birthdate = `${day}.${month}.${year}`;
return [birthdate, sex];
};

View file

@ -10,9 +10,11 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
candidate: {
name: '',
surname: '',
birthSurname: '',
birthplace: '',
birthdate: '',
address: '',
letterAddress: '',
telephone: '',
citizenship: '',
email: '',
@ -21,6 +23,8 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
schoolName: '',
healthInsurance: '',
grades: [],
firstSchool: { name: '', field: '' },
secondSchool: { name: '', field: '' },
testLanguage: ''
},
parents: []

View file

@ -6,7 +6,6 @@
import Submit from '$lib/components/button/Submit.svelte';
import GdprCheckBox from '$lib/components/checkbox/GdprCheckBox.svelte';
import Home from '$lib/components/icons/Home.svelte';
import SchoolBadge from '$lib/components/icons/SchoolBadge.svelte';
import SplitLayout from '$lib/components/layout/SplitLayout.svelte';
import SelectField from '$lib/components/select/SelectField.svelte';
@ -24,10 +23,14 @@
import type { CandidateData } from '$lib/stores/candidate';
import AccountLinkCheckBox from '$lib/components/checkbox/AccountLinkCheckBox.svelte';
import GradesTable from '$lib/components/grades/GradesTable.svelte';
import SchoolSelect from '$lib/components/select/SchoolSelect/SchoolSelect.svelte';
import PersonalIdConfirmCheckBox from '$lib/components/checkbox/PersonalIdConfirmCheckBox.svelte';
import { isPersonalIdNumberWithBirthdateValid } from '$lib/utils/personalIdFormat';
const pageCount = 7;
let pageIndex = 0;
let pagesFilled = [false, false, false, false, false, false, false];
let pagesFilled = [false, false, false, false, false, false, false, false];
let editModePageIndex = 3;
const pageCount = pagesFilled.length;
let pageTexts = [
$LL.candidate.register.second.title(),
$LL.candidate.register.third.title(),
@ -42,19 +45,24 @@
let details = data.candidate;
let baseCandidateDetails = data.whoami;
let personalIdBirthdateMatch = true;
const formInitialValues = {
gdpr: false,
personalIdOk: false,
personalIdErr: false,
linkOk: false,
linkError: false,
candidate: {
name: '',
surname: '',
birthSurname: '',
email: '',
telephone: '',
birthplace: '',
birthdate: '',
sex: '',
address: '',
letterAddress: '',
street: '',
houseNumber: '',
city: '',
@ -64,6 +72,8 @@
schoolName: '',
healthInsurance: '',
grades: [],
firstSchool: { name: '', field: '' },
secondSchool: { name: '', field: '' },
testLanguage: ''
},
parents: [
@ -84,6 +94,8 @@
const formValidationSchema = yup.object().shape({
gdpr: yup.boolean().oneOf([true]),
personalIdOk: yup.boolean().oneOf([true]),
personalIdErr: yup.boolean().oneOf([false]),
linkOk: yup.boolean().oneOf([true]),
linkError: yup.boolean().oneOf([false]),
candidate: yup.object().shape({
@ -99,6 +111,7 @@
.string()
.required()
.matches(/^([0-3]?[0-9])\.(0?[1-9]|1[0-2])\.[0-9]{4}$/),
birthSurname: yup.string().required(),
sex: yup.string(),
address: yup.string(),
street: yup.string().required(),
@ -126,6 +139,14 @@
.required()
)
.required(),
firstSchool: yup.object().shape({
name: yup.string().required(),
field: yup.string().required()
}),
secondSchool: yup.object().shape({
name: yup.string().required(),
field: yup.string().required()
}),
testLanguage: yup.string().required()
}),
parents: yup.array().of(
@ -167,7 +188,12 @@
unknown
>
? {
[K2 in keyof (typeof formInitialValues)[K]]: string;
[K2 in keyof (typeof formInitialValues)[K]]: (typeof formInitialValues)[K][K2] extends Record<
string,
unknown
>
? { [K4 in keyof (typeof formInitialValues)[K][K2]]: string }
: string;
}
: (typeof formInitialValues)[K] extends Array<Record<string, unknown>>
? Array<{ [K3 in keyof (typeof formInitialValues)[K][number]]: string }>
@ -177,80 +203,34 @@
// TODO: https://github.com/tjinauyeung/svelte-forms-lib/issues/171!! (Zatím tenhle mega typ)
$: typedErrors = errors as unknown as Writable<FormErrorType>;
// TODO: validate on admin dashboard, move somewhere
// TODO: nefunguje pro lidi nar. pred 1.1.1954 :D
const isPersonalIdNumberValid = (personalIdNumber: string): boolean => {
const idFmt = personalIdNumber.split('/').join('');
const lastDigitCheck =
Number(idFmt.slice(0, 9)) % 11 === Number(idFmt.at(-1)) ||
Number(idFmt.slice(0, 9)) % 11 === 10; // an edge case that could occur
const divisibleBy11 = Number(idFmt) % 11 === 0;
if (lastDigitCheck && divisibleBy11) {
return true;
} else {
return false;
}
};
const isPersonalIdNumberWithBirthdateValid = (
personalIdNumber: string,
birthdate: string
): boolean => {
const dateFmt = birthdate
.split('.')
.map((x) => x.padStart(2, '0'))
.reverse()
.join('')
.slice(2);
const idFmt = personalIdNumber.split('/').join('');
const divisionValid = isPersonalIdNumberValid(personalIdNumber);
const idMonth = Number(idFmt.slice(2, 4));
const dateMonth = Number(dateFmt.slice(2, 4));
const monthValid =
idMonth === dateMonth ||
idMonth === dateMonth + 50 ||
idMonth === dateMonth + 20 ||
idMonth === dateMonth + 70;
if (
idFmt.slice(0, 2) === dateFmt.slice(0, 2) &&
monthValid &&
idFmt.slice(4, 6) === dateFmt.slice(4, 6) &&
divisionValid
) {
return true;
} else {
return false;
}
};
$: console.log($typedErrors);
const onSubmit = async (values: CandidateData) => {
if (pageIndex === 3) {
if (values.candidate.citizenship === 'Česká republika') {
if (
!isPersonalIdNumberWithBirthdateValid(
values.candidate.personalIdNumber,
values.candidate.birthdate
)
) {
toast.push('Rodné číslo neodpovídá oficiální specifikaci či datumu narození', {
theme: {
'--toastColor': 'mintcream',
'--toastBackground': '#b91c1c',
'--toastBarBackground': '#7f1d1d'
}
});
personalIdBirthdateMatch = false;
throw new Error('Rodné číslo neodpovídá datumu narození');
}
}
personalIdBirthdateMatch = true;
}
if (pageIndex === pageCount) {
console.log('submitting');
// clone values to oldValues
let oldValues = JSON.parse(JSON.stringify(values));
try {
if (values.candidate.citizenship === 'Česká republika') {
if (
!isPersonalIdNumberWithBirthdateValid(
values.candidate.personalIdNumber,
values.candidate.birthdate
)
) {
// alert('Rodné číslo neodpovídá oficiální specifikaci či datumu narození'); // TODO: alerts
toast.push('Rodné číslo neodpovídá oficiální specifikaci či datumu narození', {
theme: {
'--toastColor': 'mintcream',
'--toastBackground': '#b91c1c',
'--toastBarBackground': '#7f1d1d'
}
});
throw new Error('Rodné číslo neodpovídá datumu narození');
}
}
// @ts-ignore // love javascript
delete values.undefined;
// convert birthdate from dd.mm.yyyy to yyyy-mm-dd
@ -304,40 +284,51 @@
const isPageInvalid = (index: number): boolean => {
switch (index) {
case 0:
if ($typedErrors['linkOk'] || $typedErrors['linkError']) {
if ($typedErrors['personalIdOk'] || $typedErrors['personalIdErr']) {
return true;
}
break;
case 1:
if ($typedErrors['gdpr']) {
if ($typedErrors['linkOk'] || $typedErrors['linkError']) {
return true;
}
break;
case 2:
if ($typedErrors['gdpr']) {
return true;
}
break;
case 3:
if (
$typedErrors['candidate']['name'] ||
$typedErrors['candidate']['surname'] ||
$typedErrors['candidate']['email'] ||
$typedErrors['candidate']['telephone']
$typedErrors['candidate']['telephone'] ||
$typedErrors['candidate']['city'] ||
$typedErrors['candidate']['street'] ||
$typedErrors['candidate']['houseNumber'] ||
$typedErrors['candidate']['zip']
) {
return true;
}
break;
case 3:
case 4:
if (
$typedErrors['candidate']['birthplace'] ||
$typedErrors['candidate']['citizenship'] ||
$typedErrors['candidate']['personalIdNumber'] ||
$typedErrors['candidate']['schoolName'] ||
$typedErrors['candidate']['healthInsurance'] ||
$typedErrors['candidate']['birthdate'] ||
$typedErrors['candidate']['street'] ||
$typedErrors['candidate']['houseNumber'] ||
$typedErrors['candidate']['city'] ||
$typedErrors['candidate']['zip']
// $typedErrors['candidate']['address']
$typedErrors['candidate']['birthplace'] ||
$typedErrors['candidate']['personalIdNumber'] ||
$typedErrors['candidate']['testLanguage'] ||
!personalIdBirthdateMatch
) {
return true;
}
break;
case 4:
case 5:
if (
$typedErrors['parents'][0]['name'] ||
$typedErrors['parents'][0]['surname'] ||
@ -347,7 +338,7 @@
return true;
}
break;
case 5:
case 6:
if (
$typedErrors['parents'][1]['name'] ||
$typedErrors['parents'][1]['surname'] ||
@ -357,17 +348,17 @@
return true;
}
break;
case 6:
case 7:
if (
$typedErrors['candidate']['citizenship'] ||
$typedErrors['candidate']['personalIdNumber'] ||
$typedErrors['candidate']['schoolName'] ||
$typedErrors['candidate']['healthInsurance']
$typedErrors['candidate']['firstSchool']['name'] ||
$typedErrors['candidate']['firstSchool']['field'] ||
$typedErrors['candidate']['secondSchool']['name'] ||
$typedErrors['candidate']['secondSchool']['field']
) {
return true;
}
break;
case 7:
case 8:
if ($typedErrors['candidate']['grades'].length > 0) return true;
break;
default:
@ -380,6 +371,11 @@
return '+' + telephone.match(/[0-9]{1,3}/g)!.join(' ');
};
/* $form.candidate.personalIdNumber = data.whoami.personalIdNumber;
const [birthdate, sex] = deriveBirthdateFromPersonalId(data.whoami.personalIdNumber);
$form.candidate.birthdate = birthdate;
$form.candidate.sex = sex; */
if (details !== undefined) {
details.candidate.birthdate = details.candidate.birthdate.split('-').reverse().join('.');
@ -387,10 +383,13 @@
details.parents.map(
(x) => (x.telephone = x.telephone != '' ? formatTelephone(x.telephone) : '')
);
form.set({
gdpr: true,
linkOk: true,
linkError: false,
personalIdOk: true,
personalIdErr: false,
candidate: {
...details.candidate,
street: details.candidate.address.split(',')[0].split(' ')[0],
@ -414,23 +413,35 @@
}
]
});
pageIndex = 2; // skip gdpr page
pageIndex = editModePageIndex; // skip gdpr page
pageTexts[2] = $LL.candidate.register.fourth.titleEdit();
}
</script>
<SplitLayout>
<SvelteToast />
<div class="form relative">
<div class="bottom-3/12 absolute flex w-full flex-col md:h-auto">
<!-- TODO: Find different way how to display SchoolBadge -->
{#if pageIndex !== 0 && pageIndex !== 7}
<div class="<md:h-24 <md:w-24 mb-4 h-32 w-32 self-center">
<SchoolBadge />
</div>
{/if}
<div class="form relative bg-center">
<div class="bottom-5/24 absolute flex w-full flex-col md:h-auto">
<div class="<md:hidden self-center">
<SchoolBadge />
</div>
<form on:submit={handleSubmit} id="triggerForm" class="invisible hidden" />
{#if pageIndex === 0}
<form on:submit={handleSubmit}>
<h1 class="title mt-8">{$LL.candidate.register.first.title()}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.first.description()}
</p>
<div class="field">
<PersonalIdConfirmCheckBox
personalIdNumber={baseCandidateDetails.personalIdNumber}
bind:personalIdOk={$form.personalIdOk}
bind:personalIdErr={$form.personalIdErr}
error={$typedErrors['personalIdOk']}
/>
</div>
</form>
{:else if pageIndex === 1}
<form on:submit={handleSubmit}>
<h1 class="title mt-8">{$LL.candidate.register.first.title()}</h1>
<p class="description mt-8 block text-center">
@ -445,7 +456,7 @@
/>
</div>
</form>
{:else if pageIndex === 1}
{:else if pageIndex === 2}
<form on:submit={handleSubmit}>
<h1 class="title mt-8">{pageTexts[0]}</h1>
<p class="description mt-8 block text-center">
@ -456,89 +467,104 @@
<GdprCheckBox bind:value={$form.gdpr} error={$typedErrors['gdpr']} />
</div>
</form>
{:else if pageIndex === 2}
{:else if pageIndex === 3}
<form on:submit={handleSubmit}>
<h1 class="title mt-8">{pageTexts[1]}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.third.description()}
</p>
<div class="flex flex-col">
<span class="field">
<NameField
error={$typedErrors['candidate']['name'] || $typedErrors['candidate']['surname']}
bind:valueName={$form.candidate.name}
bind:valueSurname={$form.candidate.surname}
placeholder={$LL.input.nameSurname()}
/>
</span>
<span class="field">
<EmailField
error={$typedErrors['candidate']['email']}
bind:value={$form.candidate.email}
placeholder={$LL.input.email()}
/>
</span>
<span class="field">
<TelephoneField
error={$typedErrors['candidate']['telephone']}
bind:value={$form.candidate.telephone}
placeholder={$LL.input.telephone()}
/>
</span>
<div class="w-full">
<div class="flex flex-col">
<div class="field flex">
<span class="w-[50%]">
<NameField
error={$typedErrors['candidate']['name'] ||
$typedErrors['candidate']['surname']}
bind:valueName={$form.candidate.name}
bind:valueSurname={$form.candidate.surname}
placeholder={$LL.input.nameSurname()}
/>
</span>
<span class="ml-2 w-[50%]">
<TextField
error={$typedErrors['candidate']['birthSurname']}
bind:value={$form.candidate.birthSurname}
placeholder={$LL.input.birthSurname()}
/>
</span>
</div>
<div class="field flex">
<span class="w-[50%]">
<EmailField
error={$typedErrors['candidate']['email']}
bind:value={$form.candidate.email}
placeholder={$LL.input.email()}
/>
</span>
<span class="ml-2 w-[50%]">
<TelephoneField
error={$typedErrors['candidate']['telephone']}
bind:value={$form.candidate.telephone}
placeholder={$LL.input.telephone()}
/>
</span>
</div>
<span class="field">
<TextField
error={$typedErrors['candidate']['city']}
bind:value={$form.candidate.city}
type="text"
placeholder={$LL.input.city()}
helperText="Uveďte poštovní směrovací číslo. (např. 602 00)"
/>
</span>
</div>
<div class="field flex">
<span class="w-[66%]">
<NameField
error={$typedErrors['candidate']['street'] ||
$typedErrors['candidate']['houseNumber']}
bind:valueName={$form.candidate.street}
bind:valueSurname={$form.candidate.houseNumber}
placeholder={$LL.input.address()}
helperText="Uveďte ulici a číslo popisné (např. Preslova 72)."
/>
</span>
<span class="ml-2 w-[33%]">
<TextField
error={$typedErrors['candidate']['zip']}
bind:value={$form.candidate.zip}
type="number"
placeholder={$LL.input.zipCode()}
helperText="Uveďte poštovní směrovací číslo. (např. 602 00)"
/>
</span>
</div>
</div>
</form>
{:else if pageIndex === 3}
{:else if pageIndex === 4}
<h1 class="title mt-8">{pageTexts[2]}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.fourth.description()}
</p>
<div class="field flex">
<span class="w-[66%]">
<NameField
error={$typedErrors['candidate']['street'] ||
$typedErrors['candidate']['houseNumber']}
bind:valueName={$form.candidate.street}
bind:valueSurname={$form.candidate.houseNumber}
placeholder={$LL.input.address()}
helperText="Uveďte ulici a číslo popisné (např. Preslova 72)."
<div class="field flex w-full">
<span class="w-[50%]">
<SelectField
error={$typedErrors['candidate']['citizenship']}
bind:value={$form.candidate.citizenship}
placeholder={$LL.input.citizenship()}
options={['Česká republika', 'Slovenská republika', 'Ukrajina', 'Jiné']}
/>
</span>
<span class="ml-2 w-[33%]">
<TextField
error={$typedErrors['candidate']['zip']}
bind:value={$form.candidate.zip}
type="number"
placeholder={$LL.input.zipCode()}
helperText="Uveďte poštovní směrovací číslo. (např. 602 00)"
<span class="ml-2 w-[50%]">
<SelectField
error={$typedErrors['candidate']['testLanguage']}
bind:value={$form.candidate.testLanguage}
placeholder={$LL.input.testLanguage()}
options={['Čeština', 'Angličtina']}
/>
</span>
</div>
<div class="field flex">
<span>
<TextField
error={$typedErrors['candidate']['city']}
bind:value={$form.candidate.city}
type="text"
placeholder={$LL.input.city()}
helperText="Uveďte město"
/>
</span>
<span class="ml-2">
<TextField
error={$typedErrors['candidate']['birthplace']}
bind:value={$form.candidate.birthplace}
type="text"
placeholder={$LL.input.birthPlace()}
helperText="Uveďte město"
icon
>
<div slot="icon" class="text-sspsBlue flex items-center justify-center">
<Home />
</div>
</TextField>
</span>
</div>
<div class="field flex items-center">
<TextField
error={$typedErrors['candidate']['birthdate']}
@ -547,6 +573,30 @@
placeholder={$LL.input.birthDate()}
helperText="TODO: (Uveďte ve formátu DD.MM.RRRR)"
/>
<div class="ml-2">
<TextField
error={$typedErrors['candidate']['birthplace']}
bind:value={$form.candidate.birthplace}
type="text"
placeholder={$LL.input.birthPlace()}
helperText="TODO: (Místo narození)"
/>
</div>
</div>
<div class="field flex items-center justify-center">
{#if $form.candidate.citizenship === 'Česká republika' || !$form.candidate.citizenship}
<IdField
error={$typedErrors['candidate']['personalIdNumber']}
bind:value={$form.candidate.personalIdNumber}
placeholder={$LL.input.personalIdentificationNumber()}
/>
{:else}
<TextField
error={$typedErrors['candidate']['personalIdNumber']}
bind:value={$form.candidate.personalIdNumber}
placeholder={$LL.input.personalIdentificationNumber()}
/>
{/if}
<div class="ml-2">
<SelectField
error={$typedErrors['candidate']['sex']}
@ -556,7 +606,35 @@
/>
</div>
</div>
{:else if pageIndex === 4}
<div class="field flex flex-row">
<span>
{#if $form.candidate.citizenship === 'Česká republika' || !$form.candidate.citizenship}
<TextField
error={$typedErrors['candidate']['schoolName']}
type="number"
bind:value={$form.candidate.schoolName}
placeholder={$LL.input.schoolIzo()}
/>
{:else}
<TextField
error={$typedErrors['candidate']['schoolName']}
type="text"
bind:value={$form.candidate.schoolName}
placeholder={$LL.input.schoolName()}
/>
{/if}
</span>
<span class="ml-2">
<TextField
error={$typedErrors['candidate']['healthInsurance']}
type="text"
bind:value={$form.candidate.healthInsurance}
placeholder={$LL.input.insuranceNumber()}
/>
</span>
</div>
{:else if pageIndex === 5}
<h1 class="title mt-8">{pageTexts[3]}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.fifth.description()}
@ -585,7 +663,7 @@
/>
</span>
</div>
{:else if pageIndex === 5}
{:else if pageIndex === 6}
<h1 class="title mt-8">{pageTexts[4]}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.sixth.description()}
@ -614,75 +692,28 @@
/>
</span>
</div>
{:else if pageIndex === 6}
{:else if pageIndex === 7}
<h1 class="title mt-8">{pageTexts[5]}</h1>
<p class="description mt-8 block text-center">
<p class="description my-8 block text-center">
{$LL.candidate.register.seventh.description()}
</p>
<div class="flex w-full flex-col">
<div class="field flex w-full">
<span class="w-[50%]">
<SelectField
error={$typedErrors['candidate']['citizenship']}
bind:value={$form.candidate.citizenship}
placeholder={$LL.input.citizenship()}
options={['Česká republika', 'Slovenská republika', 'Ukrajina', 'Jiné']}
/>
</span>
<span class="ml-2 w-[50%]">
<SelectField
error={$typedErrors['candidate']['testLanguage']}
bind:value={$form.candidate.testLanguage}
placeholder={$LL.input.testLanguage()}
options={['Čeština', 'Angličtina']}
/>
</span>
</div>
<div class="field flex flex-row">
<span>
{#if $form.candidate.citizenship === 'Česká republika' || !$form.candidate.citizenship}
<TextField
error={$typedErrors['candidate']['schoolName']}
type="number"
bind:value={$form.candidate.schoolName}
placeholder={$LL.input.schoolIzo()}
/>
{:else}
<TextField
error={$typedErrors['candidate']['schoolName']}
type="text"
bind:value={$form.candidate.schoolName}
placeholder={$LL.input.schoolName()}
/>
{/if}
</span>
<span class="ml-2">
<TextField
error={$typedErrors['candidate']['healthInsurance']}
type="text"
bind:value={$form.candidate.healthInsurance}
placeholder={$LL.input.insuranceNumber()}
/>
</span>
</div>
</div>
<div class="field flex items-center justify-center">
{#if $form.candidate.citizenship === 'Česká republika' || !$form.candidate.citizenship}
<IdField
error={$typedErrors['candidate']['personalIdNumber']}
bind:value={$form.candidate.personalIdNumber}
placeholder={$LL.input.personalIdentificationNumber()}
<div class="flex h-full flex-col justify-between">
<span class="field">
<SchoolSelect
error={$typedErrors['candidate']['firstSchool']['name'] ||
$typedErrors['candidate']['firstSchool']['field']}
bind:selectedSchool={$form.candidate.firstSchool}
/>
{:else}
<TextField
error={$typedErrors['candidate']['personalIdNumber']}
bind:value={$form.candidate.personalIdNumber}
placeholder={$LL.input.personalIdentificationNumber()}
</span>
<span class="field mt-10">
<SchoolSelect
error={$typedErrors['candidate']['secondSchool']['name'] ||
$typedErrors['candidate']['secondSchool']['field']}
bind:selectedSchool={$form.candidate.secondSchool}
/>
{/if}
</span>
</div>
{:else if pageIndex === 7}
{:else if pageIndex === 8}
<h1 class="title mt-8">{pageTexts[6]}</h1>
<p class="description mt-8 block text-center">
{$LL.candidate.register.eighth.description()}
@ -693,7 +724,7 @@
/>
{/if}
</div>
<div class="bottom-1/12 absolute w-full">
<div class="bottom-1/24 absolute w-full">
<div class="field">
<Submit
on:click={async (e) => {
@ -711,7 +742,7 @@
/>
</div>
<div class="mt-4 flex flex-row justify-center md:mt-8">
<div class="mt-4 flex flex-row justify-center md:mt-6">
{#each Array(pageCount + 1) as _, i}
<button
class:dotActive={i === pageIndex}

View file

@ -25,7 +25,11 @@
{$LL.candidate.auth.login.description()}
</p>
<div class="w-4/5 lg:w-3/5">
<TextField bind:value={applicationValue} placeholder={$LL.input.evidenceNumber()} type="number" />
<TextField
bind:value={applicationValue}
placeholder={$LL.input.evidenceNumber()}
type="number"
/>
</div>
<div class="mt-8 w-4/5 lg:w-3/5">
<Submit on:click={redirectToCode} value={$LL.input.submit()} />

View file

@ -5,7 +5,7 @@ import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ url }) => {
await loadAllLocalesAsync();
setLocale('cs');
return {
url: url.pathname
};

View file

@ -58,9 +58,9 @@ const cs: BaseTranslation = {
'Zde můžete zadat údaje o druhém zákonném zástupci. Škole tím umožníte lépe komunikovat.'
},
seventh: {
title: 'Dokončení registrace',
title: 'Přihlášky na školy',
description:
'Zadejte prosím své občanství, rodné číslo, či jeho alternativu Vaší země a obor na který se hlásíte.'
'Zde můžete vyplnit přihlášky na školy v pořadí Vašeho výběru. V případě, že jste podali přihlášku na více škol, vyplňte přihlášky na školy v pořadí Vašeho výběru.'
},
eighth: {
title: 'Poslední krok',
@ -100,7 +100,7 @@ const cs: BaseTranslation = {
},
missing: {
title: 'Soubory nebyly nahrány!',
description: 'Nahrajte včechny soubory prosím',
description: 'Nahrajte včechny soubory prosím'
}
}
},
@ -118,6 +118,12 @@ const cs: BaseTranslation = {
title2: 'Ne, přihlášku na SSPŠaG jsem podával více přihlášek'
}
},
personalIdConfirmCheckBox: {
ok: 'Vše je v pořádku',
whatHappened: 'Co se děje?',
titleOk: 'Potvrzuji, že moje rodné číslo je {personalId}',
titleErr: 'Ne, moje rodné číslo není {personalId}'
},
gdprCheckBox: {
title: 'Souhlasím se zpracováním osobních údajů',
description: 'Kliknutím vyjaďřujete souhlas se zpracováním osobních údajů',
@ -128,6 +134,7 @@ const cs: BaseTranslation = {
input: {
optional: 'nepovinné',
nameSurname: 'Jméno a příjmení',
birthSurname: 'Rodné příjmení',
email: 'E-mail',
telephone: 'Telefon',
address: 'Ulice a č. p.',
@ -147,6 +154,8 @@ const cs: BaseTranslation = {
password: 'Heslo',
submit: 'Odeslat',
continue: 'Pokračovat',
fieldOfStudy: 'Obor',
selectedSchool: 'Vybraná škola',
parent: {
nameSurname: 'Jméno a příjmení zákonného zástupce',
email: 'E-mail zákonného zástupce',

View file

@ -1,11 +1,10 @@
import type { FormattersInitializer } from 'typesafe-i18n'
import type { Locales, Formatters } from './i18n-types.js'
import type { FormattersInitializer } from 'typesafe-i18n';
import type { Locales, Formatters } from './i18n-types.js';
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
const formatters: Formatters = {
// add your formatter functions here
}
};
return formatters
}
return formatters;
};

View file

@ -129,11 +129,11 @@ type RootTranslation = {
}
seventh: {
/**
* Dokončení registrace
* Přihlášky na školy
*/
title: string
/**
* Zadejte prosím své občanství, rodné číslo, či jeho alternativu Vaší země a obor na který se hlásíte.
* Zde můžete vyplnit přihlášky na školy v pořadí Vašeho výběru. V případě, že jste podali přihlášku na více škol, vyplňte přihlášky na školy v pořadí Vašeho výběru.
*/
description: string
}
@ -272,6 +272,26 @@ type RootTranslation = {
title2: string
}
}
personalIdConfirmCheckBox: {
/**
* Vše je v pořádku
*/
ok: string
/**
* Co se děje?
*/
whatHappened: string
/**
* Potvrzuji, že moje rodné číslo je {personalId}
* @param {unknown} personalId
*/
titleOk: RequiredParams<'personalId'>
/**
* Ne, moje rodné číslo není {personalId}
* @param {unknown} personalId
*/
titleErr: RequiredParams<'personalId'>
}
gdprCheckBox: {
/**
* Souhlasím se zpracováním osobních údajů
@ -297,6 +317,10 @@ type RootTranslation = {
* Jméno a příjmení
*/
nameSurname: string
/**
* Rodné příjmení
*/
birthSurname: string
/**
* E-mail
*/
@ -373,6 +397,14 @@ type RootTranslation = {
* Pokračovat
*/
'continue': string
/**
* Obor
*/
fieldOfStudy: string
/**
* Vybraná škola
*/
selectedSchool: string
parent: {
/**
* Jméno a příjmení zákonného zástupce
@ -507,11 +539,11 @@ export type TranslationFunctions = {
}
seventh: {
/**
* Dokončení registrace
* Přihlášky na školy
*/
title: () => LocalizedString
/**
* Zadejte prosím své občanství, rodné číslo, či jeho alternativu Vaší země a obor na který se hlásíte.
* Zde můžete vyplnit přihlášky na školy v pořadí Vašeho výběru. V případě, že jste podali přihlášku na více škol, vyplňte přihlášky na školy v pořadí Vašeho výběru.
*/
description: () => LocalizedString
}
@ -645,6 +677,24 @@ export type TranslationFunctions = {
title2: () => LocalizedString
}
}
personalIdConfirmCheckBox: {
/**
* Vše je v pořádku
*/
ok: () => LocalizedString
/**
* Co se děje?
*/
whatHappened: () => LocalizedString
/**
* Potvrzuji, že moje rodné číslo je {personalId}
*/
titleOk: (arg: { personalId: unknown }) => LocalizedString
/**
* Ne, moje rodné číslo není {personalId}
*/
titleErr: (arg: { personalId: unknown }) => LocalizedString
}
gdprCheckBox: {
/**
* Souhlasím se zpracováním osobních údajů
@ -670,6 +720,10 @@ export type TranslationFunctions = {
* Jméno a příjmení
*/
nameSurname: () => LocalizedString
/**
* Rodné příjmení
*/
birthSurname: () => LocalizedString
/**
* E-mail
*/
@ -746,6 +800,14 @@ export type TranslationFunctions = {
* Pokračovat
*/
'continue': () => LocalizedString
/**
* Obor
*/
fieldOfStudy: () => LocalizedString
/**
* Vybraná škola
*/
selectedSchool: () => LocalizedString
parent: {
/**
* Jméno a příjmení zákonného zástupce

View file

@ -1,6 +1,6 @@
import adapter from '@sveltejs/adapter-node';
import preprocess from 'svelte-preprocess';
import path from "path";
import path from 'path';
import { windi } from 'svelte-windicss-preprocess';
/** @type {import('@sveltejs/kit').Config} */
@ -11,9 +11,8 @@ const config = {
kit: {
adapter: adapter({ out: 'build' }),
alias: {
$i18n: path.resolve('./src/translations'),
$i18n: path.resolve('./src/translations')
}
}
};

View file

@ -9,6 +9,6 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"strict": true
}
}