mirror of
https://github.com/danbulant/Portfolio
synced 2026-05-24 12:35:31 +00:00
Merge pull request #104 from EETagent/feature_dashboard_add_endpoints
(frontend) Improve dashboard's InfoCard
This commit is contained in:
commit
cf94bc7a8b
11 changed files with 317 additions and 49 deletions
|
|
@ -26,7 +26,7 @@ export const apiFetchDetails = async (fetchSsr?: Fetch): Promise<CandidateData>
|
|||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.status != 200) {
|
||||
if (!res.ok) {
|
||||
throw new Error(await res.text());
|
||||
}
|
||||
return await res.json();
|
||||
|
|
@ -202,6 +202,17 @@ export const apiSubmitPortfolio = async (): Promise<boolean> => {
|
|||
}
|
||||
};
|
||||
|
||||
export const apiGetPortfolio = async (): Promise<Blob> => {
|
||||
const res = await fetch(API_URL + '/candidate/portfolio/download', {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw errorHandler(await res.text(), 'Failed to download portfolio');
|
||||
}
|
||||
return await res.blob();
|
||||
};
|
||||
|
||||
export const apiDeltePortfolio = async (): Promise<boolean> => {
|
||||
try {
|
||||
await axios.post(API_URL + '/candidate/portfolio/delete', {}, { withCredentials: true });
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@
|
|||
class:error
|
||||
on:change
|
||||
type="checkbox"
|
||||
id="gdpr-option"
|
||||
id="gdpr"
|
||||
bind:checked={value}
|
||||
class="peer hidden"
|
||||
/>
|
||||
<label
|
||||
for="gdpr-option"
|
||||
for="gdpr"
|
||||
class="peer-checked:border-sspsBlue peer-checked:text-gray-600"
|
||||
class:error
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
<script lang="ts">
|
||||
import debounce from 'just-debounce-it';
|
||||
|
||||
import { apiDeltePortfolio, apiSubmitPortfolio } from '$lib/@api/candidate';
|
||||
import { apiDeltePortfolio, apiGetPortfolio, apiSubmitPortfolio } from '$lib/@api/candidate';
|
||||
import Circles from '$lib/components/icons/Circles.svelte';
|
||||
import { fetchSubmProgress, type Status } from '$lib/stores/portfolio';
|
||||
import StatusNotificationBig from './StatusNotificationBig.svelte';
|
||||
import InfoButton from './InfoButton.svelte';
|
||||
import { candidateData } from '$lib/stores/candidate';
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
export let title: string;
|
||||
export let status: Status;
|
||||
|
||||
export let showDetails = false;
|
||||
let loading = false;
|
||||
|
||||
const submitPortfolio = async () => {
|
||||
|
|
@ -32,22 +36,102 @@
|
|||
await deletePortfolio();
|
||||
}
|
||||
};
|
||||
|
||||
const downloadPortfolio = async () => {
|
||||
try {
|
||||
const portfolioBlob = await apiGetPortfolio();
|
||||
const url = window.URL.createObjectURL(new Blob([portfolioBlob]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute(
|
||||
'download',
|
||||
'PORTFOLIO' +
|
||||
'_' +
|
||||
$candidateData.candidate.name +
|
||||
'_' +
|
||||
$candidateData.candidate.surname +
|
||||
'.zip'
|
||||
);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="card flex flex-col">
|
||||
<div class="infoBar flex flex-row-reverse">
|
||||
<div class="infoBar flex flex-row-reverse <2xl:flex-col">
|
||||
<StatusNotificationBig {loading} {status} on:click={debounce(handleNotificationClick, 150)} />
|
||||
<div class="mr-4">
|
||||
<div on:click on:keydown class="flex flex-col">
|
||||
<div class="flex flex-col <2xl:ml-auto <2xl:flex-row <2xl:my-2">
|
||||
<InfoButton
|
||||
bind:showDetails
|
||||
on:download={downloadPortfolio}
|
||||
on:showInfo={(_) => (showDetails = !showDetails)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex flex-row justify-between">
|
||||
<div class="relative flex flex-col my-2 overflow-hidden">
|
||||
<div>
|
||||
<span class="absolute -left-16 -top-36">
|
||||
<Circles />
|
||||
</span>
|
||||
<div class="mt-8 flex flex-col lg:mt-12">
|
||||
<div class="flex flex-col mt-[5%]">
|
||||
<h3>{title}</h3>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{#if showDetails}
|
||||
<div class="overflow-scroll">
|
||||
<div
|
||||
use:tippy={{
|
||||
content: '<span>Vámi vyplněné osobní údaje</span>',
|
||||
allowHTML: true,
|
||||
placement: 'top',
|
||||
showOnCreate: false,
|
||||
delay: 0
|
||||
}}
|
||||
class="flex flex-col justify-between leading-10 mt-4"
|
||||
>
|
||||
<span>Adresa: <span class="font-bold">{$candidateData.candidate.address}</span></span>
|
||||
<span
|
||||
>Datum narození: <span class="font-bold">{$candidateData.candidate.birthdate}</span></span
|
||||
>
|
||||
<span
|
||||
>Místo narození: <span class="font-bold">{$candidateData.candidate.birthplace}</span
|
||||
></span
|
||||
>
|
||||
<span
|
||||
>Rodné číslo: <span class="font-bold">{$candidateData.candidate.personalIdNumber}</span
|
||||
></span
|
||||
>
|
||||
<span>Telefon: <span class="font-bold">{$candidateData.candidate.telephone}</span></span>
|
||||
</div>
|
||||
<div
|
||||
use:tippy={{
|
||||
content: '<span>Vámi vyplněné osobní údaje</span>',
|
||||
allowHTML: true,
|
||||
placement: 'top',
|
||||
showOnCreate: false,
|
||||
delay: 0
|
||||
}}
|
||||
class="flex flex-col leading-10 mt-4"
|
||||
>
|
||||
{#each $candidateData.parents as parent}
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sspsBlue text-xl font-bold">{parent.name + ' ' + parent.surname}</span
|
||||
>
|
||||
<span>Email: <span class="font-bold">{parent.email}</span></span>
|
||||
<span>Telefon: <span class="font-bold">{parent.telephone}</span></span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
<StatusNotificationDot {status} />
|
||||
</div>
|
||||
</div>
|
||||
{#if fileDropped && error === null}
|
||||
{#if fileDropped && error === null}
|
||||
<div class="body uploaded flex content-around items-center justify-between">
|
||||
<div class="w-24">
|
||||
<img
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
>
|
||||
<div class="items-center text-center">
|
||||
<h2 class="text-sspsBlueDark mb-2 text-2xl font-bold">{Math.round(progress * 100)} %</h2>
|
||||
<ProgressBar {progress} />
|
||||
<ProgressBar submitted={status === 'submitted'} {progress} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
|||
87
frontend/src/lib/components/dashboard/InfoButton.svelte
Normal file
87
frontend/src/lib/components/dashboard/InfoButton.svelte
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts">
|
||||
import { tippy } from 'svelte-tippy';
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { submissionProgress, UploadStatus } from '$lib/stores/portfolio';
|
||||
import Document from '../icons/Document.svelte';
|
||||
import Download from '../icons/Download.svelte';
|
||||
|
||||
export let showDetails: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const showInfo = () => {
|
||||
dispatch('showInfo');
|
||||
};
|
||||
|
||||
const download = () => {
|
||||
dispatch('download');
|
||||
};
|
||||
</script>
|
||||
|
||||
<span
|
||||
on:click={(_) => showInfo()}
|
||||
on:keydown={(_) => showInfo()}
|
||||
use:tippy={{
|
||||
content: (showDetails ? 'Skrýt' : 'Zobrazit') + ' osobní údaje',
|
||||
placement: 'top',
|
||||
showOnCreate: false,
|
||||
delay: 0
|
||||
}}
|
||||
class="icon"
|
||||
class:showDetails
|
||||
>
|
||||
<Document />
|
||||
</span>
|
||||
|
||||
{#if $submissionProgress.status === UploadStatus.Submitted}
|
||||
<span
|
||||
on:click={(_) => download()}
|
||||
on:keydown={(_) => download()}
|
||||
use:tippy={{
|
||||
content: 'Stáhnout portfolio',
|
||||
placement: 'top',
|
||||
showOnCreate: false,
|
||||
delay: 0
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="icon"
|
||||
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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/></svg
|
||||
>
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
use:tippy={{
|
||||
content: 'Nelze stáhnout, portfolio nebylo odevzdáno',
|
||||
placement: 'top',
|
||||
showOnCreate: false,
|
||||
delay: 0
|
||||
}}
|
||||
class="icon disabledIcon"
|
||||
>
|
||||
<Download />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
@apply text-sspsBlueDark h-10 w-10 transition-colors duration-300 hover:cursor-pointer;
|
||||
@apply hover:text-sspsBlue;
|
||||
}
|
||||
.showDetails {
|
||||
@apply text-sspsBlue;
|
||||
}
|
||||
.disabledIcon {
|
||||
@apply text-gray-300 hover:cursor-not-allowed hover:text-gray-300;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
export let progress: number;
|
||||
export let submitted: boolean = false;
|
||||
</script>
|
||||
|
||||
<div class="progress-bar">
|
||||
|
|
@ -13,27 +14,30 @@
|
|||
>
|
||||
<line x1="5" y1="3" x2="45" y2="3" stroke="#e6e6e6" stroke-width="6" stroke-linecap="round" />
|
||||
|
||||
{#if progress === 1}
|
||||
<line
|
||||
x1="5"
|
||||
y1="3"
|
||||
x2={progress * 45}
|
||||
y2="3"
|
||||
stroke="#35e000ff"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
{:else}
|
||||
<line
|
||||
x1="5"
|
||||
y1="3"
|
||||
x2={progress * 45}
|
||||
y2="3"
|
||||
stroke="#75bff8ff"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
{/if}
|
||||
<line
|
||||
x1="5"
|
||||
y1="3"
|
||||
x2={progress * 45}
|
||||
y2="3"
|
||||
class:submitted={progress === 1 && submitted}
|
||||
class:not-submitted={progress === 1 && !submitted}
|
||||
class:uploading={progress !== 1}
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
|
||||
>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.submitted {
|
||||
@apply stroke-[#35e000ff];
|
||||
}
|
||||
.not-submitted {
|
||||
@apply stroke-[#ef8b46];
|
||||
}
|
||||
.uploading {
|
||||
@apply stroke-[#75bff8ff];
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
<div on:click on:keydown class="flex flex-col">
|
||||
<div class="info flex flex-col {status}">
|
||||
<span class="text-xl font-bold text-white">{title}</span>
|
||||
<span class="2xl:text-xl font-bold text-white">{title}</span>
|
||||
{#if loading}
|
||||
<div role="status">
|
||||
<svg
|
||||
|
|
|
|||
16
frontend/src/lib/components/icons/Document.svelte
Normal file
16
frontend/src/lib/components/icons/Document.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/></svg
|
||||
>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
@apply fill-none stroke-current;
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 337 B |
16
frontend/src/lib/components/icons/Download.svelte
Normal file
16
frontend/src/lib/components/icons/Download.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/></svg
|
||||
>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
@apply fill-none stroke-current;
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 283 B |
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { flip } from 'svelte/animate';
|
||||
import FullLayout from '$lib/components/layout/FullLayout.svelte';
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/svelte';
|
||||
|
|
@ -9,16 +10,24 @@
|
|||
import PortfolioLetterUploadCard from '$lib/components/dashboard/PortfolioLetterUploadCard.svelte';
|
||||
import PortfolioZipUploadCard from '$lib/components/dashboard/PortfolioZipUploadCard.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { fetchSubmProgress, submissionProgress, UploadStatus, type Status } from '$lib/stores/portfolio';
|
||||
import {
|
||||
fetchSubmProgress,
|
||||
submissionProgress,
|
||||
UploadStatus,
|
||||
type Status
|
||||
} from '$lib/stores/portfolio';
|
||||
import { candidateData } from '$lib/stores/candidate';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
// TODO: transition
|
||||
let showDetails = false;
|
||||
|
||||
// @ts-ignore
|
||||
$: candidateData.set(data.candidate);
|
||||
// @ts-ignore
|
||||
$: submissionProgress.set(data.submission);
|
||||
|
||||
|
||||
const getUploadStatus = (progressStatus: UploadStatus | undefined): Status => {
|
||||
switch (progressStatus) {
|
||||
case 3:
|
||||
|
|
@ -28,30 +37,57 @@
|
|||
default:
|
||||
return 'missing';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullLayout>
|
||||
<div class="dashboard dashboardDesktop">
|
||||
<div class="name col-span-3 <2xl:col-span-4">
|
||||
<DashboardInfoCard status={getUploadStatus($submissionProgress.status)} title={$candidateData.candidate.name + ' ' + $candidateData.candidate.surname ?? ''}>
|
||||
<span class="text-sspsBlue mt-3 truncate">{$candidateData.candidate.email}</span>
|
||||
<span class="text-sspsGray mt-3 text-xs">Uchazeč na SSPŠ</span>
|
||||
</DashboardInfoCard>
|
||||
</div>
|
||||
<div class="coverletter col-span-5 <2xl:col-span-4">
|
||||
{#each [0] as animated (animated)}
|
||||
<div
|
||||
class="movable name col-span-3 row-span-4"
|
||||
animate:flip={{ duration: 400}}
|
||||
class:showDetailsInfoCard={showDetails}
|
||||
>
|
||||
<DashboardInfoCard
|
||||
bind:showDetails
|
||||
status={getUploadStatus($submissionProgress.status)}
|
||||
title={$candidateData.candidate.name + ' ' + $candidateData.candidate.surname ?? ''}
|
||||
>
|
||||
<span class="text-sspsBlue mt-3 truncate">{$candidateData.candidate.email}</span>
|
||||
<span class="text-sspsGray mt-3 text-xs">Uchazeč na SSPŠ</span>
|
||||
</DashboardInfoCard>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="movable coverletter col-span-5 row-span-4">
|
||||
<CoverLetterUploadCard />
|
||||
</div>
|
||||
<div class="portfolio col-span-4">
|
||||
<PortfolioLetterUploadCard />
|
||||
</div>
|
||||
<div class="moreData col-span-4">
|
||||
<PortfolioZipUploadCard />
|
||||
</div>
|
||||
{#each [0] as animated (animated)}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="portfolio col-span-4 row-span-4"
|
||||
class:showDetailsPortfolio={showDetails}
|
||||
>
|
||||
<PortfolioLetterUploadCard />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#each [0] as animated (animated)}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="moreData col-span-4 row-span-4"
|
||||
class:showDetailsMoreData={showDetails}
|
||||
>
|
||||
<PortfolioZipUploadCard />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="dashboard dashboardMobile">
|
||||
<div class="name my-10 mx-auto w-[90%]">
|
||||
<DashboardInfoCard status={getUploadStatus($submissionProgress.status)} title={$candidateData.candidate.name + ' ' + $candidateData.candidate.surname ?? ''}>
|
||||
<DashboardInfoCard
|
||||
status={getUploadStatus($submissionProgress.status)}
|
||||
title={$candidateData.candidate.name + ' ' + $candidateData.candidate.surname ?? ''}
|
||||
>
|
||||
<span class="text-sspsBlue mt-3 truncate">{$candidateData.candidate.email}</span>
|
||||
<span class="text-sspsGray mt-3 text-xs">Uchazeč na SSPŠ</span>
|
||||
</DashboardInfoCard>
|
||||
|
|
@ -77,9 +113,18 @@
|
|||
</FullLayout>
|
||||
|
||||
<style>
|
||||
.showDetailsInfoCard {
|
||||
@apply md:row-span-8;
|
||||
}
|
||||
.showDetailsPortfolio {
|
||||
@apply md:hidden;
|
||||
}
|
||||
.showDetailsMoreData {
|
||||
@apply md:col-span-5;
|
||||
}
|
||||
.dashboardDesktop {
|
||||
@apply h-[85vh] w-[85vw];
|
||||
@apply hidden grid-cols-8 grid-rows-2 gap-10 md:grid;
|
||||
@apply grid-rows-8 hidden grid-cols-8 gap-10 md:grid;
|
||||
}
|
||||
|
||||
.dashboardMobile {
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@
|
|||
console.log(values.parents);
|
||||
console.log(values);
|
||||
if (pageIndex === pageCount) {
|
||||
// clone values to oldValues
|
||||
let oldValues = JSON.parse(JSON.stringify(values));
|
||||
try {
|
||||
console.log('submit');
|
||||
// @ts-ignore // love javascript
|
||||
|
|
@ -131,9 +133,12 @@
|
|||
|
||||
values.candidate.birthdate = birthdate_formttted;
|
||||
|
||||
values.parents.filter((x) => x.name !== '' && x.surname !== '' && x.email !== '' && x.telephone !== '');
|
||||
|
||||
await apiFillDetails(values);
|
||||
goto('/dashboard');
|
||||
} catch (e) {
|
||||
values = oldValues;
|
||||
console.error('error while submitting data: ' + e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue