From bbf3b864e74f88c614f7a855bcc539968b2d2dfe Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 18 Oct 2020 14:22:05 +0200 Subject: [PATCH 1/3] Fix label and error handling --- src/api/edition.ts | 18 +++++++++++++----- translations/pl.yaml | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/api/edition.ts b/src/api/edition.ts index 1a04c9b..ec7975f 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -10,9 +10,13 @@ const EDITION_REGISTER_ENDPOINT = "/register"; const EDITION_LOGIN_ENDPOINT = "/access/loginEdition"; export async function available() { - const response = await axios.get(EDITIONS_ENDPOINT); + try { + const response = await axios.get(EDITIONS_ENDPOINT); - return (response.data || []).map(editionTeaserDtoTransformer.transform); + return (response.data || []).map(editionTeaserDtoTransformer.transform); + } catch (e) { + return []; + } } export async function join(key: string): Promise { @@ -26,10 +30,14 @@ export async function join(key: string): Promise { } export async function get(key: string): Promise { - const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); - const dto = response.data; + try { + const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); + const dto = response.data; - return editionDtoTransformer.transform(dto); + return editionDtoTransformer.transform(dto); + } catch (error) { + return null; + } } export async function current(): Promise { diff --git a/translations/pl.yaml b/translations/pl.yaml index d077211..4ec3a5c 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -39,6 +39,7 @@ pages: my-editions: "Moje praktyki" pick: "wybierz" register: "Zapisz się do edycji praktyk" + no-editions: "Brak edycji do wyboru, zarejestruj się do edycji praktyk przyciskiem poniżej." user-fill: title: "Uzupełnij swoje dane" user-profile: From d9902702db33e071133853afe12c68ba7f9bdb5a Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 18 Oct 2020 14:52:38 +0200 Subject: [PATCH 2/3] Fix useAsync --- src/api/edition.ts | 10 +++------- src/hooks/useAsync.ts | 4 +++- src/pages/edition/pick.tsx | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/api/edition.ts b/src/api/edition.ts index ec7975f..5f7951e 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -30,14 +30,10 @@ export async function join(key: string): Promise { } export async function get(key: string): Promise { - try { - const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); - const dto = response.data; + const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); + const dto = response.data; - return editionDtoTransformer.transform(dto); - } catch (error) { - return null; - } + return editionDtoTransformer.transform(dto); } export async function current(): Promise { diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index a8293d4..8b83d9b 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -40,8 +40,10 @@ export function useAsync(supplier: Promise | (() => Promise< useEffect(() => { if (typeof supplier === "function") { setPromise(supplier()); + } else { + setPromise(supplier); } - }, []) + }, [ supplier ]) return { isLoading, diff --git a/src/pages/edition/pick.tsx b/src/pages/edition/pick.tsx index a1261c0..ea04030 100644 --- a/src/pages/edition/pick.tsx +++ b/src/pages/edition/pick.tsx @@ -1,5 +1,5 @@ import { Page } from "@/pages/base"; -import React from "react"; +import React, { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Box, Button, CircularProgress, Container, Typography } from "@material-ui/core"; import { Actions } from "@/components"; @@ -15,7 +15,7 @@ import { EditionActions, useDispatch, UserActions } from "@/state/actions"; export const PickEditionPage = () => { const { t } = useTranslation(); - const { value: editions, isLoading } = useAsync(() => api.edition.available()); + const { value: editions, isLoading } = useAsync(useCallback(() => api.edition.available(), [])); const dispatch = useDispatch(); const history = useHistory(); From 52bda87494e05bb49903aa44821f13b66040bdb0 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Fri, 6 Nov 2020 20:03:14 +0100 Subject: [PATCH 3/3] Fix internship selection --- src/api/dto/edition.ts | 2 +- src/api/dto/internship-registration.ts | 8 +++++ src/api/edition.ts | 13 +++++--- src/api/internship.ts | 5 +--- src/api/upload.ts | 18 ++++++----- src/data/internship.ts | 4 --- src/forms/plan.tsx | 31 ++++++++++++++----- src/helpers.ts | 14 +++++++++ src/hooks/useAsync.ts | 2 ++ src/hooks/useDebouncedEffect.ts | 10 +++++++ src/pages/edition/pick.tsx | 41 ++++++++++++++------------ src/pages/edition/register.tsx | 23 ++++++++++----- src/pages/main.tsx | 33 ++++++++++++++------- src/state/actions/index.ts | 3 +- src/state/actions/plan.ts | 9 ++++-- src/state/reducer/plan.ts | 24 ++++++++++----- 16 files changed, 164 insertions(+), 76 deletions(-) create mode 100644 src/hooks/useDebouncedEffect.ts diff --git a/src/api/dto/edition.ts b/src/api/dto/edition.ts index 94c3101..2753f39 100644 --- a/src/api/dto/edition.ts +++ b/src/api/dto/edition.ts @@ -20,7 +20,7 @@ export interface EditionTeaserDTO extends Identifiable { export const editionTeaserDtoTransformer: OneWayTransformer> = { transform(subject: EditionTeaserDTO, context?: undefined): Subset { - return { + return subject && { id: subject.id, startDate: moment(subject.editionStart), endDate: moment(subject.editionFinish), diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts index b415db9..34c61b0 100644 --- a/src/api/dto/internship-registration.ts +++ b/src/api/dto/internship-registration.ts @@ -5,6 +5,7 @@ import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor"; import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type"; import { Moment } from "moment"; import { sampleStudent } from "@/provider/dummy"; +import { UploadType } from "@/api/upload"; export enum SubmissionState { Draft = "Draft", @@ -48,10 +49,17 @@ export interface InternshipRegistrationDTO extends Identifiable { declaredHours: number, } +export interface InternshipDocument extends Identifiable { + description: null, + type: UploadType, + state: SubmissionState, +} + const reference = (subject: Identifiable | null): Identifiable | null => subject && { id: subject.id }; export interface InternshipInfoDTO { internshipRegistration: InternshipRegistrationDTO; + documentation: InternshipDocument[], } export const internshipRegistrationUpdateTransformer: OneWayTransformer, Nullable> = { diff --git a/src/api/edition.ts b/src/api/edition.ts index 5f7951e..b420620 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -1,7 +1,8 @@ import { axios } from "@/api/index"; import { Edition } from "@/data/edition"; import { prepare } from "@/routing"; -import { EditionDTO, editionDtoTransformer, editionTeaserDtoTransformer } from "@/api/dto/edition"; +import { EditionDTO, editionDtoTransformer, EditionTeaserDTO, editionTeaserDtoTransformer } from "@/api/dto/edition"; +import { Subset } from "@/helpers"; const EDITIONS_ENDPOINT = "/editions"; const EDITION_INFO_ENDPOINT = "/editions/:key"; @@ -29,11 +30,15 @@ export async function join(key: string): Promise { } } -export async function get(key: string): Promise { - const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); +export async function get(key: string): Promise | null> { + if (!key) { + return null; + } + + const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); const dto = response.data; - return editionDtoTransformer.transform(dto); + return editionTeaserDtoTransformer.transform(dto); } export async function current(): Promise { diff --git a/src/api/internship.ts b/src/api/internship.ts index 5741950..961c789 100644 --- a/src/api/internship.ts +++ b/src/api/internship.ts @@ -6,15 +6,12 @@ const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration'; const INTERNSHIP_ENDPOINT = '/internship'; export async function update(internship: Nullable): Promise { - const response = await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); + await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); return true; } export async function get(): Promise { const response = await axios.get(INTERNSHIP_ENDPOINT); - - console.log(response); - return response.data; } diff --git a/src/api/upload.ts b/src/api/upload.ts index e27735a..81fa7b1 100644 --- a/src/api/upload.ts +++ b/src/api/upload.ts @@ -1,5 +1,6 @@ -import { Identifiable } from "@/data"; import { axios } from "@/api/index"; +import { InternshipDocument } from "@/api/dto/internship-registration"; +import { prepare } from "@/routing"; export enum UploadType { Ipp = "IppScan", @@ -10,14 +11,15 @@ export enum UploadType { const CREATE_DOCUMENT_ENDPOINT = '/document'; const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan'; -interface Document extends Identifiable { - description?: string; - type: UploadType; +export async function create(type: UploadType) { + const response = await axios.post(CREATE_DOCUMENT_ENDPOINT, { type }); + return response.data; } -export async function create(type: UploadType, content: File) -{ - const response = await axios.post(CREATE_DOCUMENT_ENDPOINT, { type }); +export async function upload(document: InternshipDocument, file: File) { + const data = new FormData(); + data.append('documentScan', file) - console.log(response.data); + const response = await axios.put(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }), data); + return true; } diff --git a/src/data/internship.ts b/src/data/internship.ts index 29733f4..d3e946e 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -26,10 +26,6 @@ export interface Internship extends Identifiable { office: Office; } -export interface Plan extends Identifiable { - -} - export interface Mentor { name: string; surname: string; diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index 1626d03..b7f2021 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -5,24 +5,39 @@ import { Actions } from "@/components"; import { Link as RouterLink, useHistory } from "react-router-dom"; import { route } from "@/routing"; import React, { useState } from "react"; -import { Plan } from "@/data"; import { useTranslation } from "react-i18next"; -import { useDispatch } from "@/state/actions"; +import { InternshipPlanActions, useDispatch } from "@/state/actions"; import { UploadType } from "@/api/upload"; import api from "@/api"; +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { InternshipDocument } from "@/api/dto/internship-registration"; export const PlanForm = () => { const { t } = useTranslation(); - const [plan, setPlan] = useState({}); + const [file, setFile] = useState(); const dispatch = useDispatch(); const history = useHistory(); - const handleSubmit = () => { - api.upload.create(UploadType.Ipp, null as any); - // dispatch({ type: InternshipPlanActions.Send, plan }); - history.push(route("home")) + const document = useSelector(state => state.plan.document); + + const handleSubmit = async () => { + if (!file) { + return; + } + + let destination: InternshipDocument = document as any; + + if (!destination) { + destination = await api.upload.create(UploadType.Ipp); + dispatch({ type: InternshipPlanActions.Send, document: destination }); + } + + await api.upload.upload(destination, file); + + history.push("/"); } return @@ -35,7 +50,7 @@ export const PlanForm = () => { - + setFile(files[0]) }/> { t('forms.plan.dropzone-help') } diff --git a/src/helpers.ts b/src/helpers.ts index fc5e38e..34dae24 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -12,3 +12,17 @@ export interface DOMEvent extends Event { export function delay(time: number) { return new Promise(resolve => setTimeout(resolve, time)); } + +export function throttle(decorated: (...args: TArgs) => void, time: number = 150) { + let timeout: number | undefined; + return function (this: any, ...args: TArgs): void { + if (typeof timeout !== 'undefined') { + window.clearTimeout(timeout); + } + + timeout = window.setTimeout(() => { + timeout = undefined; + decorated.call(this, ...args); + }, time); + } +} diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index 8b83d9b..d7fbf9b 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -30,6 +30,8 @@ export function useAsync(supplier: Promise | (() => Promise< setLoading(false); } }).catch(error => { + console.error(error) + if (semaphore.value == myMagicNumber) { setError(error); setLoading(false); diff --git a/src/hooks/useDebouncedEffect.ts b/src/hooks/useDebouncedEffect.ts new file mode 100644 index 0000000..d50c368 --- /dev/null +++ b/src/hooks/useDebouncedEffect.ts @@ -0,0 +1,10 @@ +import { DependencyList, EffectCallback, useCallback, useEffect } from "react"; + +export function useDebouncedEffect(effect: EffectCallback, deps: DependencyList, time: number = 150) { + const callback = useCallback(effect, deps); + + useEffect(() => { + const timeout = window.setTimeout(() => callback(), time); + return () => window.clearTimeout(timeout); + }, [ callback, time ]) +} diff --git a/src/pages/edition/pick.tsx b/src/pages/edition/pick.tsx index ea04030..635d225 100644 --- a/src/pages/edition/pick.tsx +++ b/src/pages/edition/pick.tsx @@ -11,7 +11,27 @@ import api from "@/api"; import { Section } from "@/components/section"; import { useVerticalSpacing } from "@/styles"; import { Alert } from "@material-ui/lab"; -import { EditionActions, useDispatch, UserActions } from "@/state/actions"; +import { AppDispatch, EditionActions, useDispatch, UserActions } from "@/state/actions"; + +export const loginToEdition = (id: string) => async (dispatch: AppDispatch) => { + const token = await api.edition.login(id); + + if (!token) { + return; + } + + await dispatch({ + type: UserActions.Login, + token, + }) + + const edition = await api.edition.current(); + + dispatch({ + type: EditionActions.Set, + edition + }) +} export const PickEditionPage = () => { const { t } = useTranslation(); @@ -23,24 +43,7 @@ export const PickEditionPage = () => { const classes = useVerticalSpacing(3); const pickEditionHandler = (id: string) => async () => { - const token = await api.edition.login(id); - - if (!token) { - return; - } - - await dispatch({ - type: UserActions.Login, - token, - }) - - const edition = await api.edition.current(); - - dispatch({ - type: EditionActions.Set, - edition - }) - + await dispatch(loginToEdition(id)); history.push("/"); } diff --git a/src/pages/edition/register.tsx b/src/pages/edition/register.tsx index 7c69834..634636d 100644 --- a/src/pages/edition/register.tsx +++ b/src/pages/edition/register.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Page } from "@/pages/base"; import { useTranslation } from "react-i18next"; import { Box, Button, CircularProgress, Container, TextField, Typography } from "@material-ui/core"; @@ -9,24 +9,33 @@ import { Edition } from "@/data/edition"; import { useAsyncState } from "@/hooks"; import { Label, Section } from "@/components/section"; import { Alert } from "@material-ui/lab"; +import { Subset } from "@/helpers"; +import { useDispatch } from "@/state/actions"; +import { loginToEdition } from "@/pages/edition/pick"; +import { useHistory } from "react-router-dom"; +import { useDebouncedEffect } from "@/hooks/useDebouncedEffect"; export const RegisterEditionPage = () => { const { t } = useTranslation(); const [key, setKey] = useState(""); - const [{ value: edition, isLoading }, setEdition] = useAsyncState(undefined); + const [{ value: edition, isLoading }, setEdition] = useAsyncState | null>(undefined); const classes = useVerticalSpacing(3); + const dispatch = useDispatch(); + const history = useHistory(); - useEffect(() => { + useDebouncedEffect(() => { setEdition(api.edition.get(key)); }, [ key ]) - const handleRegister = () => { + const handleRegister = async () => { try { - api.edition.join(key); + await api.edition.join(key); + await dispatch(loginToEdition(key)); + history.push("/"); } catch (error) { - + console.log(error); } } @@ -37,7 +46,7 @@ export const RegisterEditionPage = () => { const Edition = () => edition ?
- { edition.course.name } + { edition.course?.name } { t('internship.date-range', { start: edition.startDate, end: edition.endDate }) } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index e204644..6141ecd 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -15,8 +15,29 @@ import { InsuranceStep } from "@/pages/steps/insurance"; import { StudentStep } from "@/pages/steps/student"; import { useDeadlines } from "@/hooks"; import api from "@/api"; -import { InternshipProposalActions, useDispatch } from "@/state/actions"; +import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions"; import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration"; +import { UploadType } from "@/api/upload"; + +export const updateInternshipInfo = async (dispatch: AppDispatch) => { + const internship = await api.internship.get(); + + dispatch({ + type: InternshipProposalActions.Receive, + state: internship.internshipRegistration.state, + internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration), + }) + + const plan = internship.documentation.find(doc => doc.type === UploadType.Ipp); + + if (plan) { + dispatch({ + type: InternshipPlanActions.Receive, + document: plan, + state: plan.state, + }) + } +} export const MainPage = () => { const { t } = useTranslation(); @@ -28,15 +49,7 @@ export const MainPage = () => { const dispatch = useDispatch(); useEffect(() => { - (async () => { - const internship = await api.internship.get(); - - dispatch({ - type: InternshipProposalActions.Receive, - state: internship.internshipRegistration.state, - internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration), - }) - })() + dispatch(updateInternshipInfo); }, []) if (!student) { diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 043b99b..0fc0534 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -37,7 +37,8 @@ export const Actions = { ...StudentActions, } export type Actions = typeof Actions; +export type AppDispatch = ThunkDispatch; -export const useDispatch = () => useReduxDispatch>() +export const useDispatch = () => useReduxDispatch() export default Actions; diff --git a/src/state/actions/plan.ts b/src/state/actions/plan.ts index fcabfbe..90a0cdb 100644 --- a/src/state/actions/plan.ts +++ b/src/state/actions/plan.ts @@ -1,4 +1,3 @@ -import { Plan } from "@/data"; import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, @@ -7,6 +6,8 @@ import { SendSubmissionAction } from "@/state/actions/submission"; +import { InternshipDocument, SubmissionState } from "@/api/dto/internship-registration"; + export enum InternshipPlanActions { Send = "SEND_PLAN", Save = "SAVE_PLAN", @@ -16,7 +17,7 @@ export enum InternshipPlanActions { } export interface SendPlanAction extends SendSubmissionAction { - plan: Plan; + document: InternshipDocument; } export interface ReceivePlanApproveAction extends ReceiveSubmissionApproveAction { @@ -26,10 +27,12 @@ export interface ReceivePlanDeclineAction extends ReceiveSubmissionDeclineAction } export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction { + document: InternshipDocument; + state: SubmissionState; } export interface SavePlanAction extends SaveSubmissionAction { - plan: Plan; + document: InternshipDocument; } export type InternshipPlanAction diff --git a/src/state/reducer/plan.ts b/src/state/reducer/plan.ts index d092583..b89f661 100644 --- a/src/state/reducer/plan.ts +++ b/src/state/reducer/plan.ts @@ -1,5 +1,4 @@ -import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions"; -import { Plan } from "@/data"; +import { InternshipPlanAction, InternshipPlanActions, InternshipProposalActions } from "@/state/actions"; import { Serializable } from "@/serialization/types"; import { createSubmissionReducer, @@ -10,19 +9,18 @@ import { } from "@/state/reducer/submission"; import { Reducer } from "react"; import { SubmissionAction } from "@/state/actions/submission"; +import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration"; export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & { - plan: Serializable | null; + document: Serializable | null; } const defaultInternshipPlanState: InternshipPlanState = { ...defaultDeanApprovalsState, ...defaultSubmissionState, - plan: null, + document: null, } -export const getInternshipPlan = ({ plan }: InternshipPlanState): Plan | null => plan; - const internshipPlanSubmissionReducer: Reducer = createSubmissionReducer({ [InternshipPlanActions.Approve]: SubmissionAction.Approve, [InternshipPlanActions.Decline]: SubmissionAction.Decline, @@ -39,8 +37,20 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla case InternshipPlanActions.Send: return { ...state, - plan: action.plan, + document: action.document, } + case InternshipPlanActions.Receive: + return { + ...state, + accepted: action.state === ApiSubmissionState.Accepted, + sent: [ + ApiSubmissionState.Accepted, + ApiSubmissionState.Rejected, + ApiSubmissionState.Submitted + ].includes(action.state), + document: action.document, + } + default: return state; }