Merge pull request #121 from EETagent/more_prod_fixes

(frontend) more prod fixes
This commit is contained in:
Vojtěch Jungmann 2023-01-03 15:53:14 +01:00 committed by GitHub
commit 1367ffcf7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 370 additions and 306 deletions

View file

@ -10,4 +10,5 @@ COPY --from=builder /portfolio/target/release/portfolio /usr/local/bin/portfolio
VOLUME ["/portfolio"] VOLUME ["/portfolio"]
WORKDIR /portfolio WORKDIR /portfolio
EXPOSE 8000
ENTRYPOINT ["portfolio"] ENTRYPOINT ["portfolio"]

View file

@ -1,18 +1,24 @@
declare type FileDropEvent = import("filedrop-svelte/event").FileDropEvent; declare type FileDropEvent = import('filedrop-svelte/event').FileDropEvent;
declare type FileDropSelectEvent = import("filedrop-svelte/event").FileDropSelectEvent; declare type FileDropSelectEvent = import('filedrop-svelte/event').FileDropSelectEvent;
declare type FileDropDragEvent = import("filedrop-svelte/event").FileDropDragEvent; declare type FileDropDragEvent = import('filedrop-svelte/event').FileDropDragEvent;
declare namespace svelte.JSX { declare namespace svelte.JSX {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLAttributes<T> { interface HTMLAttributes<T> {
onfiledrop?: (event: CustomEvent<FileDropSelectEvent> & { target: EventTarget & T }) => void; onfiledrop?: (event: CustomEvent<FileDropSelectEvent> & { target: EventTarget & T }) => void;
onfiledragenter?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void; onfiledragenter?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void;
onfiledragleave?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void; onfiledragleave?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void;
onfiledragover?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void; onfiledragover?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void;
onfiledialogcancel?: (event: CustomEvent<FileDropEvent> & { target: EventTarget & T }) => void; onfiledialogcancel?: (event: CustomEvent<FileDropEvent> & { target: EventTarget & T }) => void;
onfiledialogclose?: (event: CustomEvent<FileDropEvent> & { target: EventTarget & T }) => void; onfiledialogclose?: (event: CustomEvent<FileDropEvent> & { target: EventTarget & T }) => void;
onfiledialogopen?: (event: CustomEvent<FileDropEvent> & { target: EventTarget & T }) => void; onfiledialogopen?: (event: CustomEvent<FileDropEvent> & { target: EventTarget & T }) => void;
onwindowfiledragenter?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void; onwindowfiledragenter?: (
onwindowfiledragleave?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void; event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }
onwindowfiledragover?: (event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }) => void; ) => void;
} onwindowfiledragleave?: (
} event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }
) => void;
onwindowfiledragover?: (
event: CustomEvent<FileDropDragEvent> & { target: EventTarget & T }
) => void;
}
}

View file

@ -123,9 +123,7 @@ export const apiFetchCandidate = async (id: number, fetchSsr?: Fetch): Promise<C
// SSR compatible // SSR compatible
// List all candidates /admin/list/candidates // List all candidates /admin/list/candidates
export const apiListCandidatesCSV = async ( export const apiListCandidatesCSV = async (fetchSsr?: Fetch): Promise<Blob> => {
fetchSsr?: Fetch,
): Promise<Blob> => {
const apiFetch = fetchSsr || fetch; const apiFetch = fetchSsr || fetch;
try { try {
const res = await apiFetch(API_URL + '/admin/list/candidates_csv', { const res = await apiFetch(API_URL + '/admin/list/candidates_csv', {

View file

@ -26,6 +26,7 @@
try { try {
login = await apiCreateCandidate(data); login = await apiCreateCandidate(data);
dispatch('created'); dispatch('created');
error = '';
} catch (e: unknown) { } catch (e: unknown) {
console.error(e); console.error(e);
error = (e as ApiError).msg; error = (e as ApiError).msg;
@ -47,7 +48,10 @@
{:else} {:else}
<h1 class="text-sspsBlue text-3xl font-semibold">Registrace nového uchazeče</h1> <h1 class="text-sspsBlue text-3xl font-semibold">Registrace nového uchazeče</h1>
{#if error} {#if error}
<div class="my-2 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert"> <div
class="relative my-2 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
role="alert"
>
<span class="block sm:inline">{error}</span> <span class="block sm:inline">{error}</span>
</div> </div>
{/if} {/if}

View file

@ -1,13 +1,18 @@
<script lang="ts"> <script lang="ts">
import debounce from 'just-debounce-it'; import debounce from 'just-debounce-it';
import { apiDeltePortfolio, apiGetPortfolio, apiLogout, apiSubmitPortfolio } from '$lib/@api/candidate'; import {
apiDeltePortfolio,
apiGetPortfolio,
apiLogout,
apiSubmitPortfolio
} from '$lib/@api/candidate';
import Circles from '$lib/components/icons/Circles.svelte'; import Circles from '$lib/components/icons/Circles.svelte';
import { fetchSubmProgress, type Status } from '$lib/stores/portfolio'; import { fetchSubmProgress, type Status } from '$lib/stores/portfolio';
import StatusNotificationBig from './StatusNotificationBig.svelte'; import StatusNotificationBig from './StatusNotificationBig.svelte';
import InfoButton from './InfoButton.svelte'; import InfoButton from './InfoButton.svelte';
import { baseCandidateData, candidateData } from '$lib/stores/candidate'; import { baseCandidateData, candidateData } from '$lib/stores/candidate';
import tippy, {sticky} from 'tippy.js'; import tippy, { sticky } from 'tippy.js';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
export let title: string; export let title: string;
@ -61,19 +66,19 @@
}; };
const editDetails = async () => { const editDetails = async () => {
goto('/register?edit=true') goto('/register?edit=true');
} };
const logout = async () => { const logout = async () => {
await apiLogout(); await apiLogout();
goto("/auth/login"); goto('/auth/login');
} };
</script> </script>
<div class="card flex flex-col"> <div class="card flex flex-col">
<div class="infoBar <2xl:flex-col flex flex-row-reverse"> <div class="infoBar <2xl:flex-col flex flex-row-reverse">
<StatusNotificationBig {loading} {status} on:click={debounce(handleNotificationClick, 150)} /> <StatusNotificationBig {loading} {status} on:click={debounce(handleNotificationClick, 150)} />
<div class="mr-4 <2xl:mr-1"> <div class="<2xl:mr-1 mr-4">
<div on:click on:keydown class="flex flex-col"> <div on:click on:keydown class="flex flex-col">
<div class="<2xl:ml-auto <2xl:flex-row <2xl:my-2 flex flex-col"> <div class="<2xl:ml-auto <2xl:flex-row <2xl:my-2 flex flex-col">
<InfoButton <InfoButton
@ -89,7 +94,7 @@
<div class="relative my-2 flex flex-col overflow-hidden"> <div class="relative my-2 flex flex-col overflow-hidden">
<div> <div>
<div class="mt-[5%] flex flex-col"> <div class="mt-[5%] flex flex-col">
<div class="flex justify-between"> <div class="flex justify-between">
<h3>{title}</h3> <h3>{title}</h3>
<span <span
on:click={logout} on:click={logout}
@ -101,15 +106,28 @@
sticky: true, sticky: true,
plugins: [sticky] plugins: [sticky]
}} }}
class="<2xl:hidden hover:cursor-pointer"> class="<2xl:hidden hover:cursor-pointer"
<svg class="w-10 h-10 stroke-sspsBlueDark" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg> >
<svg
class="stroke-sspsBlueDark h-10 w-10"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/></svg
>
</span> </span>
</div> </div>
<slot /> <slot />
</div> </div>
</div> </div>
{#if showDetails} {#if showDetails}
<div class="overflow-scroll flex justify-between"> <div class="flex justify-between overflow-scroll">
<div> <div>
<div <div
use:tippy={{ use:tippy={{
@ -121,7 +139,10 @@
}} }}
class="mt-4 flex flex-col justify-between leading-10" class="mt-4 flex flex-col justify-between leading-10"
> >
<span>Ev. č. přihlášky: <span class="font-bold">{$baseCandidateData.applicationId}</span></span> <span
>Ev. č. přihlášky: <span class="font-bold">{$baseCandidateData.applicationId}</span
></span
>
<span>Obor: <span class="font-bold">{$candidateData.candidate.study}</span></span> <span>Obor: <span class="font-bold">{$candidateData.candidate.study}</span></span>
<span>Adresa: <span class="font-bold">{$candidateData.candidate.address}</span></span> <span>Adresa: <span class="font-bold">{$candidateData.candidate.address}</span></span>
<span <span
@ -133,10 +154,12 @@
></span ></span
> >
<span <span
>Rodné číslo: <span class="font-bold">{$candidateData.candidate.personalIdNumber}</span >Rodné číslo: <span class="font-bold"
>{$candidateData.candidate.personalIdNumber}</span
></span ></span
> >
<span>Telefon: <span class="font-bold">{$candidateData.candidate.telephone}</span></span> <span>Telefon: <span class="font-bold">{$candidateData.candidate.telephone}</span></span
>
</div> </div>
<div <div
use:tippy={{ use:tippy={{
@ -167,8 +190,23 @@
sticky: true, sticky: true,
plugins: [sticky] plugins: [sticky]
}} }}
on:click={(_) => editDetails()} on:keydown={(_) => editDetails()} class="mt-4 hover:cursor-pointer"> on:click={(_) => editDetails()}
<svg class="w-10 h-10 stroke-sspsBlue" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg> on:keydown={(_) => editDetails()}
class="mt-4 hover:cursor-pointer"
>
<svg
class="stroke-sspsBlue h-10 w-10"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/></svg
>
</span> </span>
</div> </div>
{/if} {/if}
@ -182,7 +220,7 @@
@apply bg-[#f8fbfc]; @apply bg-[#f8fbfc];
@apply rounded-3xl; @apply rounded-3xl;
@apply px-7 py-10 <2xl:px-5 <2xl:py-5; @apply <2xl:px-5 <2xl:py-5 px-7 py-10;
@apply transition-all duration-300; @apply transition-all duration-300;
} }

View file

@ -68,7 +68,6 @@
clearInterval(dashAnimationInterval); clearInterval(dashAnimationInterval);
}; };
const onFileDrop = (dropped: Files) => { const onFileDrop = (dropped: Files) => {
console.log(dropped); console.log(dropped);
if (dropped.accepted.length > 0) { if (dropped.accepted.length > 0) {

View file

@ -88,8 +88,21 @@
}} }}
on:click={(_) => logout()} on:click={(_) => logout()}
on:keydown={(_) => logout()} on:keydown={(_) => logout()}
class="icon logoutIcon ml-1 hover:cursor-pointer"> class="icon logoutIcon ml-1 hover:cursor-pointer"
<svg class="w-10 h-10 icon logoutIcon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg> >
<svg
class="icon logoutIcon h-10 w-10"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/></svg
>
</span> </span>
<style lang="postcss"> <style lang="postcss">

View file

@ -49,8 +49,7 @@
} }
.view { .view {
@apply z-10 overflow-scroll; @apply z-10 overflow-scroll;
@apply rounded-3xl md:rounded-none; @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 absolute top-0 right-0 bottom-0 left-0 m-auto h-[90vh] w-[90vw] 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-[50vw];
@apply md:my-auto; @apply md:my-auto;
@apply bg-white; @apply bg-white;

View file

@ -43,7 +43,7 @@ export interface CreateCandidateLogin extends CreateCandidate {
password: string; password: string;
} }
export const baseCandidateData= writable<CreateCandidate>({ export const baseCandidateData = writable<CreateCandidate>({
applicationId: 0, applicationId: 0,
personalIdNumber: '' personalIdNumber: ''
}); });

View file

@ -8,7 +8,7 @@
import type { PageServerData } from './$types'; import type { PageServerData } from './$types';
import Table from '$lib/components/admin/table/Table.svelte'; import Table from '$lib/components/admin/table/Table.svelte';
import bacgkround from "$lib/assets/background.jpg"; import bacgkround from '$lib/assets/background.jpg';
export let data: PageServerData; export let data: PageServerData;
@ -87,7 +87,7 @@
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
} };
</script> </script>
{#if createCandidateModal} {#if createCandidateModal}
@ -98,8 +98,8 @@
{/if} {/if}
<div> <div>
<header class="w-full h-14"> <header class="h-14 w-full">
<img class="h-12 w-full object-cover filter blur-sm" src={bacgkround} alt="Background"> <img class="h-12 w-full object-cover blur-sm filter" src={bacgkround} alt="Background" />
</header> </header>
<div class="flex flex-row"> <div class="flex flex-row">
<div class="list fixed"> <div class="list fixed">
@ -116,12 +116,12 @@
<TextField on:keyup={search} bind:value={searchValue} placeholder="Hledat" /> <TextField on:keyup={search} bind:value={searchValue} placeholder="Hledat" />
<button <button
on:click={openCreateCandidateModal} on:click={openCreateCandidateModal}
class="bg-gray-500 hover:bg-gray-600 ml-3 w-2/5 rounded-lg p-3 py-4 text-xl font-semibold text-white transition-colors duration-300" class="ml-3 w-2/5 rounded-lg bg-gray-500 p-3 py-4 text-xl font-semibold text-white transition-colors duration-300 hover:bg-gray-600"
>Nový uchazeč</button >Nový uchazeč</button
> >
<button <button
on:click={downloadCSV} on:click={downloadCSV}
class="bg-gray-500 hover:bg-gray-600 ml-3 w-2/5 rounded-lg p-3 py-4 text-xl font-semibold text-white transition-colors duration-300" class="ml-3 w-2/5 rounded-lg bg-gray-500 p-3 py-4 text-xl font-semibold text-white transition-colors duration-300 hover:bg-gray-600"
>CSV</button >CSV</button
> >
</div> </div>

View file

@ -34,7 +34,7 @@
<p class="text-sspsGray mt-8 text-center font-light"> <p class="text-sspsGray mt-8 text-center font-light">
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.<br /> Fusce suscipit libero eget elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.<br /> Fusce suscipit libero eget elit.
</p> </p>
<div class="mt-8 flex w-3/5 flex-col"> <div class="mt-8 flex w-4/5 flex-col lg:w-3/5">
<span> <span>
<TextField bind:value={adminIdValue} placeholder="Admin id" type="number" /> <TextField bind:value={adminIdValue} placeholder="Admin id" type="number" />
</span> </span>
@ -42,7 +42,7 @@
<PasswordField bind:value={adminPasswordValue} placeholder="Heslo" /> <PasswordField bind:value={adminPasswordValue} placeholder="Heslo" />
</span> </span>
</div> </div>
<div class="mt-8 w-3/5"> <div class="mt-8 w-4/5 lg:w-3/5">
<Submit value="Odeslat" on:click={login} /> <Submit value="Odeslat" on:click={login} />
</div> </div>
</div> </div>

View file

@ -12,7 +12,7 @@ export const load: LayoutServerLoad = async ({ cookies, fetch }) => {
}); });
return { return {
whoami: whoami whoami: whoami
} };
} else { } else {
throw redirect(302, '/auth/logout'); throw redirect(302, '/auth/logout');
} }

View file

@ -82,7 +82,7 @@
{/each} {/each}
</div> </div>
<div class="dashboard dashboardMobile"> <div class="dashboard dashboardMobile">
<div class="name md:my-10 mx-auto w-[90%]"> <div class="name mx-auto w-[90%] md:my-10">
<DashboardInfoCard <DashboardInfoCard
status={getUploadStatus($submissionProgress.status)} status={getUploadStatus($submissionProgress.status)}
title={$candidateData.candidate.name + ' ' + $candidateData.candidate.surname ?? ''} title={$candidateData.candidate.name + ' ' + $candidateData.candidate.surname ?? ''}

View file

@ -5,10 +5,10 @@ import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => { export const load: PageServerLoad = async ({ fetch }) => {
const details: CandidateData | undefined = await apiFetchDetails(fetch).catch((e) => { const details: CandidateData | undefined = await apiFetchDetails(fetch).catch((e) => {
console.error(e); console.error(e);
return undefined; return undefined;
}); });
return { return {
candidate: details, candidate: details
}; };
}; };

View file

@ -35,7 +35,6 @@
export let data: PageData; export let data: PageData;
let details = data.candidate; let details = data.candidate;
const formInitialValues = { const formInitialValues = {
gdpr: false, gdpr: false,
candidate: { candidate: {
@ -140,11 +139,10 @@
// TODO: validate on admin dashboard, move somewhere // TODO: validate on admin dashboard, move somewhere
// TODO: nefunguje pro lidi nar. pred 1.1.1954 :D // TODO: nefunguje pro lidi nar. pred 1.1.1954 :D
const isPersonalIdNumberValid = (personalIdNumber: string): boolean => { const isPersonalIdNumberValid = (personalIdNumber: string): boolean => {
const idFmt = personalIdNumber const idFmt = personalIdNumber.split('/').join('');
.split('/')
.join('');
const lastDigitCheck = Number(idFmt.slice(0, 9)) % 11 === Number(idFmt.at(-1)) || 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 Number(idFmt.slice(0, 9)) % 11 === 10; // an edge case that could occur
const divisibleBy11 = Number(idFmt) % 11 === 0; const divisibleBy11 = Number(idFmt) % 11 === 0;
@ -153,26 +151,30 @@
} else { } else {
return false; return false;
} }
} };
const isPersonalIdNumberWithBirthdateValid = (personalIdNumber: string, birthdate: string): boolean => { const isPersonalIdNumberWithBirthdateValid = (
personalIdNumber: string,
birthdate: string
): boolean => {
const dateFmt = birthdate const dateFmt = birthdate
.split('.') .split('.')
.map((x) => x.padStart(2, '0')) .map((x) => x.padStart(2, '0'))
.reverse() .reverse()
.join('') .join('')
.slice(2); .slice(2);
const idFmt = personalIdNumber const idFmt = personalIdNumber.split('/').join('');
.split('/')
.join('');
const divisionValid = isPersonalIdNumberValid(personalIdNumber); const divisionValid = isPersonalIdNumberValid(personalIdNumber);
const idMonth = Number(idFmt.slice(2, 4)); const idMonth = Number(idFmt.slice(2, 4));
const dateMonth = Number(dateFmt.slice(2, 4)); const dateMonth = Number(dateFmt.slice(2, 4));
const monthValid = idMonth === dateMonth || idMonth === dateMonth + 50 || const monthValid =
idMonth === dateMonth + 20 || idMonth === dateMonth + 70; idMonth === dateMonth ||
idMonth === dateMonth + 50 ||
idMonth === dateMonth + 20 ||
idMonth === dateMonth + 70;
if ( if (
idFmt.slice(0, 2) === dateFmt.slice(0, 2) && idFmt.slice(0, 2) === dateFmt.slice(0, 2) &&
monthValid && monthValid &&
@ -183,7 +185,6 @@
} else { } else {
return false; return false;
} }
}; };
const onSubmit = async (values: CandidateData) => { const onSubmit = async (values: CandidateData) => {
@ -192,7 +193,12 @@
let oldValues = JSON.parse(JSON.stringify(values)); let oldValues = JSON.parse(JSON.stringify(values));
try { try {
if (values.candidate.citizenship === 'Česká republika') { if (values.candidate.citizenship === 'Česká republika') {
if (!isPersonalIdNumberWithBirthdateValid(values.candidate.personalIdNumber, values.candidate.birthdate)) { if (
!isPersonalIdNumberWithBirthdateValid(
values.candidate.personalIdNumber,
values.candidate.birthdate
)
) {
alert('Rodné číslo neodpovídá oficiální specifikaci či datumu narození'); // TODO: alerts alert('Rodné číslo neodpovídá oficiální specifikaci či datumu narození'); // TODO: alerts
throw new Error('Rodné číslo neodpovídá datumu narození'); throw new Error('Rodné číslo neodpovídá datumu narození');
} }
@ -212,7 +218,6 @@
(x) => x.name !== '' && x.surname !== '' && x.email !== '' && x.telephone !== '' (x) => x.name !== '' && x.surname !== '' && x.email !== '' && x.telephone !== ''
); );
await apiFillDetails(values); await apiFillDetails(values);
goto('/dashboard'); goto('/dashboard');
} catch (e) { } catch (e) {
@ -220,7 +225,7 @@
console.error('error while submitting data: ' + e); console.error('error while submitting data: ' + e);
} }
} }
} };
const { form, errors, handleSubmit, handleChange } = createForm({ const { form, errors, handleSubmit, handleChange } = createForm({
initialValues: formInitialValues, initialValues: formInitialValues,
@ -292,267 +297,269 @@
}; };
const formatTelephone = (telephone: string) => { const formatTelephone = (telephone: string) => {
return '+' + telephone return '+' + telephone.match(/[0-9]{1,3}/g)!.join(' ');
.match(/[0-9]{1,3}/g)! };
.join(' ');
}
if (details !== undefined) { if (details !== undefined) {
details.candidate.birthdate = details.candidate.birthdate details.candidate.birthdate = details.candidate.birthdate.split('-').reverse().join('.');
.split('-')
.reverse() details.candidate.telephone = formatTelephone(details.candidate.telephone);
.join('.'); details.parents.map(
(x) => (x.telephone = x.telephone != '' ? formatTelephone(x.telephone) : '')
details.candidate.telephone = formatTelephone(details.candidate.telephone); );
details.parents.map((x) => x.telephone = x.telephone != '' ? formatTelephone(x.telephone) : ''); form.set({
form.set({ gdpr: true,
gdpr: true, candidate: {
candidate: { ...details.candidate
...details.candidate },
}, parents: [
parents: [
{ {
...details.parents[0] ...details.parents[0]
}, },
{ {
...details.parents[1] ?? { ...(details.parents[1] ?? {
name: '', name: '',
surname: '', surname: '',
email: '', email: '',
telephone: '' telephone: ''
} })
} }
] ]
}); });
pageIndex = 1; // skip gdpr page pageIndex = 1; // skip gdpr page
pageTexts[1] = 'Úprava osobních údajů' pageTexts[1] = 'Úprava osobních údajů';
} }
</script> </script>
<SplitLayout> <SplitLayout>
<div class="form relative"> <div class="form relative">
<div class="overflow-scroll h-[65%] md:h-auto absolute bottom-3/12 flex flex-col w-full"> <div class="bottom-3/12 absolute flex w-full flex-col overflow-scroll md:h-auto">
<div class="h-32 w-32 <md:hidden self-center mb-4"> <div class="<md:h-24 <md:w-24 mb-4 h-32 w-32 self-center">
<SchoolBadge /> <SchoolBadge />
</div> </div>
<form on:submit={handleSubmit} id="triggerForm" class="invisible hidden"></form> <form on:submit={handleSubmit} id="triggerForm" class="invisible hidden" />
{#if pageIndex === 0} {#if pageIndex === 0}
<form on:submit={handleSubmit}> <form on:submit={handleSubmit}>
<h1 class="title mt-8">{pageTexts[0]}</h1> <h1 class="title mt-8">{pageTexts[0]}</h1>
<p class="description mt-8 block text-center">
V rámci portálu pro přijímací řízení zpracováváme mnoho osobních údajů. Proto je nutný
Váš souhlas s jejich zpracováním. O bezpečnosti zpracování Vašich osobních údajů si
můžete přečíst
<a href="/bezpecnost" class="text-sspsBlue underline"> zde</a>.
</p>
<div class="field">
<GdprCheckBox
on:change={handleChange}
bind:value={$form.gdpr}
error={$typedErrors['gdpr']}
/>
</div>
</form>
{:else if pageIndex === 1}
<form on:submit={handleSubmit}>
<h1 class="title mt-8">{pageTexts[1]}</h1>
<p class="description mt-8 block text-center">
V rámci usnadnění přijímacího řízení jsme připravili online formulář, který Vám pomůže s
vyplněním potřebných údajů.
</p>
<div class="flex flex-col">
<span class="field">
<NameField
error={$typedErrors['candidate']['name'] || $typedErrors['candidate']['surname']}
on:change={handleChange}
bind:valueName={$form.candidate.name}
bind:valueSurname={$form.candidate.surname}
placeholder="Jméno a příjmení"
/>
</span>
<span class="field">
<EmailField
error={$typedErrors['candidate']['email']}
on:change={handleChange}
bind:value={$form.candidate.email}
placeholder="E-mail"
/>
</span>
<span class="field">
<TelephoneField
error={$typedErrors['candidate']['telephone']}
on:change={handleChange}
bind:value={$form.candidate.telephone}
placeholder="Telefon"
/>
</span>
</div>
</form>
{:else if pageIndex === 2}
<h1 class="title mt-8">{pageTexts[2]}</h1>
<p class="description mt-8 block text-center"> <p class="description mt-8 block text-center">
V rámci portálu pro přijímací řízení zpracováváme mnoho osobních údajů. Proto je nutný Váš Pro registraci je potřeba vyplnit několik údajů o Vás. Tyto údaje budou použity pro
souhlas s jejich zpracováním. O bezpečnosti zpracování Vašich osobních údajů si můžete přečíst přijímací řízení. Všechny údaje jsou důležité.
<a href="/bezpecnost" class="text-sspsBlue underline"> zde</a>.
</p> </p>
<div class="field"> <div class="flex w-full flex-col">
<GdprCheckBox <span class="field">
on:change={handleChange} <TextField
bind:value={$form.gdpr} error={$typedErrors['candidate']['address']}
error={$typedErrors['gdpr']} on:change={handleChange}
/> bind:value={$form.candidate.address}
type="text"
placeholder="Adresa trvalého bydliště"
helperText="Uveďte ulici, č.p., město, PSČ"
/>
</span>
<span class="field">
<TextField
error={$typedErrors['candidate']['birthplace']}
on:change={handleChange}
bind:value={$form.candidate.birthplace}
type="text"
placeholder="Místo narození"
helperText="Uveďte město"
icon
>
<div slot="icon" class="text-sspsBlue flex items-center justify-center">
<Home />
</div>
</TextField>
</span>
</div> </div>
</form>
{:else if pageIndex === 1} <div class="field flex items-center">
<form on:submit={handleSubmit}> <TextField
<h1 class="title mt-8">{pageTexts[1]}</h1> error={$typedErrors['candidate']['birthdate']}
on:change={handleChange}
bind:value={$form.candidate.birthdate}
type="text"
placeholder="Datum narození"
helperText="TODO: (Uveďte ve formátu DD.MM.RRRR)"
/>
<div class="ml-2">
<SelectField
error={$typedErrors['candidate']['sex']}
on:change={handleChange}
bind:value={$form.candidate.sex}
options={['Žena', 'Muž']}
placeholder="Pohlaví"
/>
</div>
</div>
{:else if pageIndex === 3}
<h1 class="title mt-8">{pageTexts[3]}</h1>
<p class="description mt-8 block text-center"> <p class="description mt-8 block text-center">
V rámci usnadnění přijímacího řízení jsme připravili online formulář, který Vám pomůže s Sběr dat o zákonném zástupci je klíčový pro získání důležitých kontaktů a informací.
vyplněním potřebných údajů.
</p> </p>
<div class="flex flex-col"> <div class="flex w-full flex-col">
<span class="field"> <span class="field">
<NameField <NameField
error={$typedErrors['candidate']['name'] || $typedErrors['candidate']['surname']} error={$typedErrors['parents'][0]['name'] || $typedErrors['parents'][0]['surname']}
on:change={handleChange} on:change={handleChange}
bind:valueName={$form.candidate.name} bind:valueName={$form.parents[0].name}
bind:valueSurname={$form.candidate.surname} bind:valueSurname={$form.parents[0].surname}
placeholder="Jméno a příjmení" placeholder="Jméno a příjmení zákonného zástupce"
/> />
</span> </span>
<span class="field"> <span class="field">
<EmailField <EmailField
error={$typedErrors['candidate']['email']} error={$typedErrors['parents'][0]['email']}
on:change={handleChange} on:change={handleChange}
bind:value={$form.candidate.email} bind:value={$form.parents[0].email}
placeholder="E-mail" placeholder="E-mail zákonného zástupce"
/> />
</span> </span>
<span class="field"> <span class="field">
<TelephoneField <TelephoneField
error={$typedErrors['candidate']['telephone']} error={$typedErrors['parents'][0]['telephone']}
on:change={handleChange} on:change={handleChange}
bind:value={$form.candidate.telephone} bind:value={$form.parents[0].telephone}
placeholder="Telefon" placeholder="Telefon zákonného zástupce"
/> />
</span> </span>
</div> </div>
</form> {:else if pageIndex === 4}
{:else if pageIndex === 2} <h1 class="title mt-8">{pageTexts[4]}</h1>
<h1 class="title mt-8">{pageTexts[2]}</h1> <p class="description mt-8 block text-center">
<p class="description mt-8 block text-center"> Zde můžete zadat údaje o druhém zákonném zástupci. Škole tím umožníte lépe komunikovat.
Pro registraci je potřeba vyplnit několik údajů o Vás. Tyto údaje budou použity pro </p>
přijímací řízení. Všechny údaje jsou důležité. <div class="flex w-full flex-col">
</p> <span class="field">
<div class="flex w-full flex-col"> <NameField
<span class="field"> error={$typedErrors['parents'][1]['name'] || $typedErrors['parents'][1]['surname']}
<TextField on:change={handleChange}
error={$typedErrors['candidate']['address']} bind:valueName={$form.parents[1].name}
on:change={handleChange} bind:valueSurname={$form.parents[1].surname}
bind:value={$form.candidate.address} placeholder="Jméno a příjmení zákonného zástupce (nepovinné)"
type="text" />
placeholder="Adresa trvalého bydliště"
helperText="Uveďte ulici, č.p., město, PSČ"
/>
</span>
<span class="field">
<TextField
error={$typedErrors['candidate']['birthplace']}
on:change={handleChange}
bind:value={$form.candidate.birthplace}
type="text"
placeholder="Místo narození"
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']}
on:change={handleChange}
bind:value={$form.candidate.birthdate}
type="text"
placeholder="Datum narození"
helperText="TODO: (Uveďte ve formátu DD.MM.RRRR)"
/>
<div class="ml-2">
<SelectField
error={$typedErrors['candidate']['sex']}
on:change={handleChange}
bind:value={$form.candidate.sex}
options={['Žena', 'Muž']}
placeholder="Pohlaví"
/>
</div>
</div>
{:else if pageIndex === 3}
<h1 class="title mt-8">{pageTexts[3]}</h1>
<p class="description mt-8 block text-center">
Sběr dat o zákonném zástupci je klíčový pro získání důležitých kontaktů a informací.
</p>
<div class="flex w-full flex-col">
<span class="field">
<NameField
error={$typedErrors['parents'][0]['name'] || $typedErrors['parents'][0]['surname']}
on:change={handleChange}
bind:valueName={$form.parents[0].name}
bind:valueSurname={$form.parents[0].surname}
placeholder="Jméno a příjmení zákonného zástupce"
/>
</span>
<span class="field">
<EmailField
error={$typedErrors['parents'][0]['email']}
on:change={handleChange}
bind:value={$form.parents[0].email}
placeholder="E-mail zákonného zástupce"
/>
</span>
<span class="field">
<TelephoneField
error={$typedErrors['parents'][0]['telephone']}
on:change={handleChange}
bind:value={$form.parents[0].telephone}
placeholder="Telefon zákonného zástupce"
/>
</span>
</div>
{:else if pageIndex === 4}
<h1 class="title mt-8">{pageTexts[4]}</h1>
<p class="description mt-8 block text-center">
Zde můžete zadat údaje o druhém zákonném zástupci. Škole tím umožníte lépe komunikovat.
</p>
<div class="flex w-full flex-col">
<span class="field">
<NameField
error={$typedErrors['parents'][1]['name'] || $typedErrors['parents'][1]['surname']}
on:change={handleChange}
bind:valueName={$form.parents[1].name}
bind:valueSurname={$form.parents[1].surname}
placeholder="Jméno a příjmení zákonného zástupce (nepovinné)"
/>
</span>
<span class="field">
<EmailField
error={$typedErrors['parents'][1]['email']}
on:change={handleChange}
bind:value={$form.parents[1].email}
placeholder="E-mail zákonného zástupce (nepovinné)"
/>
</span>
<span class="field">
<TelephoneField
error={$typedErrors['parents'][1]['telephone']}
on:change={handleChange}
bind:value={$form.parents[1].telephone}
placeholder="Telefon zákonného zástupce (nepovinné)"
/>
</span> </span>
</div> <span class="field">
{:else if pageIndex === 5} <EmailField
<h1 class="title mt-8">{pageTexts[5]}</h1> error={$typedErrors['parents'][1]['email']}
<p class="description mt-8 block text-center"> on:change={handleChange}
Zadejte prosím své občanství, rodné číslo, či jeho alternativu Vaší země a obor na který se hlásíte. bind:value={$form.parents[1].email}
</p> placeholder="E-mail zákonného zástupce (nepovinné)"
<div class="flex w-full flex-row md:flex-col"> />
<span class="field"> </span>
<SelectField <span class="field">
error={$typedErrors['candidate']['citizenship']} <TelephoneField
on:change={handleChange} error={$typedErrors['parents'][1]['telephone']}
bind:value={$form.candidate.citizenship} on:change={handleChange}
placeholder="Občanství" bind:value={$form.parents[1].telephone}
options={['Česká republika', 'Slovenská republika', 'Ukrajina', 'Jiné']} placeholder="Telefon zákonného zástupce (nepovinné)"
/> />
</span> </span>
<span class="field ml-2 md:ml-0"> </div>
<TextField on:change={handleChange} type="text" placeholder="Evidenční číslo přihlášky" /> {:else if pageIndex === 5}
</span> <h1 class="title mt-8">{pageTexts[5]}</h1>
</div> <p class="description mt-8 block text-center">
<div class="field flex items-center justify-center"> Zadejte prosím své občanství, rodné číslo, či jeho alternativu Vaší země a obor na který
{#if $form.candidate.citizenship === 'Česká republika' || !$form.candidate.citizenship} se hlásíte.
<IdField </p>
error={$typedErrors['candidate']['personalIdNumber']} <div class="flex w-full flex-row md:flex-col">
on:change={handleChange} <span class="field">
bind:value={$form.candidate.personalIdNumber} <SelectField
placeholder="Rodné číslo" error={$typedErrors['candidate']['citizenship']}
/> on:change={handleChange}
{:else} bind:value={$form.candidate.citizenship}
<TextField placeholder="Občanství"
error={$typedErrors['candidate']['personalIdNumber']} options={['Česká republika', 'Slovenská republika', 'Ukrajina', 'Jiné']}
on:change={handleChange} />
bind:value={$form.candidate.personalIdNumber} </span>
placeholder="Rodné číslo" <span class="field ml-2 md:ml-0">
/> <TextField
{/if} on:change={handleChange}
<span class="ml-2"> type="text"
<SelectField placeholder="Evidenční číslo přihlášky"
error={$typedErrors['candidate']['study']} />
on:change={handleChange} </span>
bind:value={$form.candidate.study} </div>
placeholder="Obor" <div class="field flex items-center justify-center">
options={['KB', 'IT', 'G']} {#if $form.candidate.citizenship === 'Česká republika' || !$form.candidate.citizenship}
/> <IdField
</span> error={$typedErrors['candidate']['personalIdNumber']}
</div> on:change={handleChange}
{/if} bind:value={$form.candidate.personalIdNumber}
placeholder="Rodné číslo"
/>
{:else}
<TextField
error={$typedErrors['candidate']['personalIdNumber']}
on:change={handleChange}
bind:value={$form.candidate.personalIdNumber}
placeholder="Rodné číslo"
/>
{/if}
<span class="ml-2">
<SelectField
error={$typedErrors['candidate']['study']}
on:change={handleChange}
bind:value={$form.candidate.study}
placeholder="Obor"
options={['KB', 'IT', 'G']}
/>
</span>
</div>
{/if}
</div> </div>
<div class="controls w-full absolute bottom-1/12"> <div class="controls bottom-1/12 absolute w-full">
<div class="field"> <div class="field">
<Submit <Submit
on:click={async (e) => { on:click={async (e) => {
@ -569,8 +576,8 @@
value={pageIndex === pageCount ? 'Odeslat' : 'Pokračovat'} value={pageIndex === pageCount ? 'Odeslat' : 'Pokračovat'}
/> />
</div> </div>
<div class="mt-4 md:mt-8 flex flex-row justify-center"> <div class="mt-4 flex flex-row justify-center md:mt-8">
{#each Array(pageCount + 1) as _, i} {#each Array(pageCount + 1) as _, i}
<button <button
class:dotActive={i === pageIndex} class:dotActive={i === pageIndex}
@ -578,7 +585,7 @@
pageIndex -= pageIndex === pageCount ? 1 : 0; pageIndex -= pageIndex === pageCount ? 1 : 0;
await handleSubmit(e); await handleSubmit(e);
pagesFilled = pagesFilled.map((_, i) => !isPageInvalid(i)); pagesFilled = pagesFilled.map((_, i) => !isPageInvalid(i));
const progress = pagesFilled.slice(0, i).every((item) => item === true); const progress = pagesFilled.slice(0, i).every((item) => item === true);
if (progress) { if (progress) {
pageIndex = i; pageIndex = i;
@ -594,7 +601,7 @@
<style lang="postcss"> <style lang="postcss">
.field { .field {
@apply mt-4 md:mt-8 w-full; @apply mt-4 w-full md:mt-8 lg:mx-auto lg:w-4/5;
} }
.form { .form {
@apply flex flex-col; @apply flex flex-col;
@ -618,6 +625,6 @@
@apply text-gray-500; @apply text-gray-500;
} }
.title { .title {
@apply text-sspsBlue text-4xl font-semibold text-center; @apply text-sspsBlue text-center text-4xl font-semibold;
} }
</style> </style>

View file

@ -21,12 +21,13 @@
<SchoolBadge /> <SchoolBadge />
<h1 class="text-sspsBlue mt-8 text-4xl font-semibold">Přihlášení</h1> <h1 class="text-sspsBlue mt-8 text-4xl font-semibold">Přihlášení</h1>
<p class="text-sspsGray my-8 text-center font-light"> <p class="text-sspsGray my-8 text-center font-light">
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.<br /> Fusce suscipit libero eget elit. Evidenční číslo je jedinečné číslo přidělené uchazeči, které slouží k jeho identifikaci<br /> a
přihlášení se do systému.
</p> </p>
<div class="w-3/5"> <div class="w-4/5 lg:w-3/5">
<TextField bind:value={applicationValue} placeholder="Ev. číslo" type="number" /> <TextField bind:value={applicationValue} placeholder="Ev. číslo" type="number" />
</div> </div>
<div class="mt-8 w-3/5"> <div class="mt-8 w-4/5 lg:w-3/5">
<Submit on:click={redirectToCode} value="Odeslat" /> <Submit on:click={redirectToCode} value="Odeslat" />
</div> </div>
</div> </div>

View file

@ -123,14 +123,14 @@
.modal { .modal {
@apply flex flex-col items-center justify-center; @apply flex flex-col items-center justify-center;
@apply mx-auto my-auto; @apply mx-auto my-auto;
@apply h-[90vh] w-[90vw] md:h-4/5 md:w-4/5; @apply h-full w-full md:h-4/5 md:w-4/5;
@apply rounded-3xl; @apply md:rounded-3xl;
@apply bg-white; @apply bg-white;
} }
input { input {
@apply text-sspsBlue text-center font-semibold; @apply text-sspsBlue text-center font-semibold;
@apply transition-colors duration-300; @apply transition-colors duration-300;
@apply focus:border-sspsBlue hover:border-sspsBlue rounded-xl border border-2 bg-[#f8fafb] p-3 md:caret-transparent shadow-lg outline-none; @apply focus:border-sspsBlue hover:border-sspsBlue rounded-xl border border-2 bg-[#f8fafb] p-3 shadow-lg outline-none md:caret-transparent;
} }
.separater { .separater {
@apply bg-sspsBlue mr-2 hidden h-2 w-8 md:block; @apply bg-sspsBlue mr-2 hidden h-2 w-8 md:block;

View file

@ -1,2 +0,0 @@
<h1 class="text-6xl">Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>