From e012d015db3a2b2d06d5c5b39e667b8f632f5cb3 Mon Sep 17 00:00:00 2001 From: Kacper Donat <kadet1090@gmail.com> Date: Wed, 22 Jul 2020 00:00:27 +0200 Subject: [PATCH] Add step info --- src/app.tsx | 98 ++++++++++++++++------------------- src/data/edition.ts | 21 ++++++++ src/data/student.ts | 15 ++++++ src/index.tsx | 29 ++++++++++- src/pages/main.tsx | 47 ++++++++++++++--- src/provider/dummy/edition.ts | 8 +++ src/state/actions/edition.ts | 13 +++++ src/state/reducer/edition.ts | 17 ++++++ src/state/reducer/index.ts | 2 + src/state/store.ts | 3 +- translations/en.yaml | 21 ++++++++ translations/pl.yaml | 21 +++++++- 12 files changed, 231 insertions(+), 64 deletions(-) create mode 100644 src/data/edition.ts create mode 100644 src/provider/dummy/edition.ts create mode 100644 src/state/actions/edition.ts create mode 100644 src/state/reducer/edition.ts diff --git a/src/app.tsx b/src/app.tsx index a334733..0f42d36 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,13 +1,8 @@ -import React, { Dispatch, HTMLProps } from 'react'; -import { MuiThemeProvider as ThemeProvider, StylesProvider } from "@material-ui/core/styles"; -import { studentTheme } from "./ui/theme"; -import { MuiPickersUtilsProvider } from "@material-ui/pickers"; -import MomentUtils from "@date-io/moment"; -import { BrowserRouter, Link, Route, Switch } from "react-router-dom" -import moment, { Moment } from "moment"; +import React, { Dispatch, HTMLProps, useEffect } from 'react'; +import { Link, Route, Switch } from "react-router-dom" +import moment from "moment"; import { route, routes } from "@/routing"; -import { Provider, useDispatch, useSelector } from "react-redux"; -import store, { persistor } from "@/state/store"; +import { useDispatch, useSelector } from "react-redux"; import { AppState } from "@/state/reducer"; import { StudentAction, StudentActions } from "@/state/actions/student"; import { sampleStudent } from "@/provider/dummy/student"; @@ -16,13 +11,9 @@ import { Student } from "@/data"; import '@/styles/overrides.scss' import '@/styles/header.scss' import classNames from "classnames"; -import { PersistGate } from 'redux-persist/integration/react'; - -class LocalizedMomentUtils extends MomentUtils { - getDatePickerHeaderText(date: Moment): string { - return this.format(date, "d MMM yyyy"); - } -} +import { EditionAction, EditionActions } from "@/state/actions/edition"; +import { sampleEdition } from "@/provider/dummy/edition"; +import { Edition } from "@/data/edition"; const UserMenu = (props: HTMLProps<HTMLUListElement>) => { const student = useSelector<AppState, Student>(state => state.student as Student); @@ -40,14 +31,14 @@ const UserMenu = (props: HTMLProps<HTMLUListElement>) => { dispatch({ type: StudentActions.Logout }) } - return <ul {...props}> + return <ul { ...props }> { student ? <> - <Trans t={ t } i18nKey="logged-in-as">logged in as <strong>{{ name: `${student.name} ${student.surname}` }}</strong></Trans> - {' '} - (<Link to={'#'} onClick={ handleUserLogout }>{ t('logout') }</Link>) + <Trans t={ t } i18nKey="logged-in-as">logged in as <strong>{ { name: `${ student.name } ${ student.surname }` } }</strong></Trans> + { ' ' } + (<Link to={ '#' } onClick={ handleUserLogout }>{ t('logout') }</Link>) </> : <> - <Link to={'#'} onClick={ handleUserLogin }>{ t('login') }</Link> + <Link to={ '#' } onClick={ handleUserLogin }>{ t('login') }</Link> </> } </ul>; @@ -75,39 +66,38 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>) } function App() { - return ( - <Provider store={ store }> - <PersistGate loading={ null } persistor={ persistor }> - <StylesProvider injectFirst> - <MuiPickersUtilsProvider utils={ LocalizedMomentUtils } libInstance={ moment }> - <ThemeProvider theme={ studentTheme }> - <BrowserRouter> - <header className="header"> - <div id="logo" className="header__logo"> - <Link to={ route('home') }> - <img src="img/pg-logotyp.svg"/> - </Link> - </div> - <div className="header__nav"> - <nav className="header__top"> - <ul className="header__menu"></ul> - <UserMenu className="header__user"/> - <div className="header__divider" /> - <LanguageSwitcher className="header__language-switcher"/> - </nav> - <nav className="header__bottom"> - <ul className="header__menu header__menu--main"></ul> - </nav> - </div> - </header> - <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> - </BrowserRouter> - </ThemeProvider> - </MuiPickersUtilsProvider> - </StylesProvider> - </PersistGate> - </Provider> - ); + const dispatch = useDispatch<Dispatch<EditionAction>>(); + const edition = useSelector<AppState, Edition | null>(state => state.edition); + + useEffect(() => { + if (!edition) { + dispatch({ type: EditionActions.Set, edition: sampleEdition }); + } + }) + + const isReady = !!edition; + + return <> + <header className="header"> + <div id="logo" className="header__logo"> + <Link to={ route('home') }> + <img src="img/pg-logotyp.svg"/> + </Link> + </div> + <div className="header__nav"> + <nav className="header__top"> + <ul className="header__menu"></ul> + <UserMenu className="header__user"/> + <div className="header__divider"/> + <LanguageSwitcher className="header__language-switcher"/> + </nav> + <nav className="header__bottom"> + <ul className="header__menu header__menu--main"></ul> + </nav> + </div> + </header> + { isReady && <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> } + </>; } export default App; diff --git a/src/data/edition.ts b/src/data/edition.ts new file mode 100644 index 0000000..8a29c94 --- /dev/null +++ b/src/data/edition.ts @@ -0,0 +1,21 @@ +import { Moment } from "moment"; + +export type Edition = { + startDate: Moment; + endDate: Moment; + proposalDeadline: Moment; +} + +export type Deadlines = { + personalData?: Moment; + proposal?: Moment; + personalPlan?: Moment; + report?: Moment; +} + +export function getEditionDeadlines(edition: Edition): Deadlines { + return { + proposal: edition.proposalDeadline, + personalPlan: edition.proposalDeadline, + } +} diff --git a/src/data/student.ts b/src/data/student.ts index 5fb3221..b8eabfc 100644 --- a/src/data/student.ts +++ b/src/data/student.ts @@ -11,3 +11,18 @@ export interface Student extends Identifiable { semester: Semester; course: Course; } + +export function isStudentDataComplete(student: Student): boolean { + return getMissingStudentData(student).length === 0; +} + +export function getMissingStudentData(student: Student): (keyof Student)[] { + return [ + !!student.name || "name", + !!student.surname || "surname", + !!student.email || "email", + !!student.albumNumber || "albumNumber", + !!student.semester || "semester", + !!student.course || "course", + ].filter(x => x !== true) as (keyof Student)[]; +} diff --git a/src/index.tsx b/src/index.tsx index 4bbd47a..2c53768 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,10 +2,37 @@ import React from 'react'; import ReactDOM from 'react-dom'; import "./i18n" import App from './app'; +import { Provider } from "react-redux"; +import store, { persistor } from "@/state/store"; +import { PersistGate } from "redux-persist/integration/react"; +import { MuiThemeProvider as ThemeProvider, StylesProvider } from "@material-ui/core/styles"; +import { MuiPickersUtilsProvider } from "@material-ui/pickers"; +import moment, { Moment } from "moment"; +import { studentTheme } from "@/ui/theme"; +import { BrowserRouter } from "react-router-dom"; +import MomentUtils from "@date-io/moment"; + +class LocalizedMomentUtils extends MomentUtils { + getDatePickerHeaderText(date: Moment): string { + return this.format(date, "d MMM yyyy"); + } +} ReactDOM.render( <React.StrictMode> - <App /> + <Provider store={ store }> + <PersistGate loading={ null } persistor={ persistor }> + <StylesProvider injectFirst> + <MuiPickersUtilsProvider utils={ LocalizedMomentUtils } libInstance={ moment }> + <ThemeProvider theme={ studentTheme }> + <BrowserRouter> + <App /> + </BrowserRouter> + </ThemeProvider> + </MuiPickersUtilsProvider> + </StylesProvider> + </PersistGate> + </Provider> </React.StrictMode>, document.getElementById('root') ); diff --git a/src/pages/main.tsx b/src/pages/main.tsx index a2c371f..2e6bb4b 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -5,6 +5,10 @@ import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; import moment, { Moment } from "moment"; +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { getMissingStudentData, Student } from "@/data"; +import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; type StepProps = StepperStepProps & { until?: Moment; @@ -18,7 +22,7 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step const { t } = useTranslation(); const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]); - const left = useMemo(() => moment.duration(now.diff(until)), [until]); + const left = useMemo(() => moment.duration(now.diff(until)), [until]); return <StepperStep { ...props } completed={ completed || !!completedOn }> <StepLabel> @@ -26,7 +30,8 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step { until && <Box> <Typography variant="subtitle2" color="textSecondary"> { t('until', { date: until.format("DD MMMM YYYY") }) } - { isLate && <Typography color="error" display="inline" variant="body2"> - { t('late', { by: moment.duration(now.diff(until)).humanize() }) }</Typography> } + { isLate && <Typography color="error" display="inline" + variant="body2"> - { t('late', { by: moment.duration(now.diff(until)).humanize() }) }</Typography> } { !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left.humanize() }) }</Typography> } </Typography> </Box> } @@ -38,19 +43,47 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step export const MainPage = () => { const { t } = useTranslation(); + const student = useSelector<AppState, Student | null>(state => state.student); + const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + + const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); + return <Page my={ 6 }> <Container> <Typography variant="h2">{ t("sections.my-internship.header") }</Typography> <Stepper orientation="vertical" nonLinear> - <Step label={ t('steps.personal-data.header') } until={ moment("2020-07-01") }/> - <Step label={ t('steps.internship-proposal.header') }> + <Step label={ t('steps.personal-data.header') } completed={ missingStudentData.length === 0 } until={ deadlines.personalData }> + { missingStudentData.length > 0 && <> + <p>{ t('steps.personal-data.info') }</p> + + <ul> + { missingStudentData.map(field => <li key={ field }>{ t(`student.${field}`) }</li>) } + </ul> + + <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }> + { t('steps.personal-data.form') } + </Button> + </> } + </Step> + <Step label={ t('steps.internship-proposal.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }> + <p>{ t('steps.internship-proposal.info') }</p> + <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }> { t('steps.internship-proposal.form') } </Button> </Step> - <Step label={ t('steps.plan.header') } until={ moment("2020-07-22") }/> - <Step label={ t('steps.insurance.header') }/> - <Step label={ t('steps.report.header') }/> + <Step label={ t('steps.plan.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }> + <p>{ t('steps.plan.info') }</p> + + <Button to={ route("internship_proposal") } component={ RouterLink }> + { t('steps.plan.template') } + </Button> + <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }> + { t('steps.plan.submit') } + </Button> + </Step> + <Step label={ t('steps.insurance.header') } /> + <Step label={ t('steps.report.header') } until={ deadlines.report }/> <Step label={ t('steps.grade.header') }/> </Stepper> </Container> diff --git a/src/provider/dummy/edition.ts b/src/provider/dummy/edition.ts new file mode 100644 index 0000000..290a3ac --- /dev/null +++ b/src/provider/dummy/edition.ts @@ -0,0 +1,8 @@ +import { Edition } from "@/data/edition"; +import moment from "moment"; + +export const sampleEdition: Edition = { + startDate: moment("2020-07-01"), + endDate: moment("2020-09-30"), + proposalDeadline: moment("2020-07-31") +} diff --git a/src/state/actions/edition.ts b/src/state/actions/edition.ts new file mode 100644 index 0000000..142220d --- /dev/null +++ b/src/state/actions/edition.ts @@ -0,0 +1,13 @@ +import { Action } from "@/state/actions/base"; +import { Edition } from "@/data/edition"; + +export enum EditionActions { + Set = 'SET', +} + +export interface SetAction extends Action<EditionActions.Set> { + edition: Edition, +} + +export type EditionAction = SetAction; + diff --git a/src/state/reducer/edition.ts b/src/state/reducer/edition.ts new file mode 100644 index 0000000..ef696f9 --- /dev/null +++ b/src/state/reducer/edition.ts @@ -0,0 +1,17 @@ +import { Edition } from "@/data/edition"; +import { EditionAction, EditionActions } from "@/state/actions/edition"; + +export type EditionState = Edition | null; + +const initialEditionState: EditionState = null; + +const editionReducer = (state: EditionState = initialEditionState, action: EditionAction): EditionState => { + switch (action.type) { + case EditionActions.Set: + return action.edition; + } + + return state; +} + +export default editionReducer; diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index 0bd70e0..c839818 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -1,9 +1,11 @@ import { combineReducers } from "redux"; import studentReducer from "./student" +import editionReducer from "@/state/reducer/edition"; const rootReducer = combineReducers({ student: studentReducer, + edition: editionReducer, }) export type AppState = ReturnType<typeof rootReducer>; diff --git a/src/state/store.ts b/src/state/store.ts index 9fafb4b..f35ec6d 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -8,7 +8,8 @@ const store = createStore( persistReducer( { key: 'state', - storage: sessionStorage + storage: sessionStorage, + blacklist: ['edition'] }, rootReducer ), diff --git a/translations/en.yaml b/translations/en.yaml index d0bc877..fd6824a 100644 --- a/translations/en.yaml +++ b/translations/en.yaml @@ -7,6 +7,14 @@ until: until {{ date }} late: late by {{ by }} left: '{{ left }} left' +student: + name: first name + surname: last name + course: course + semester: semester + email: e-mail + albumNumber: album number + sections: my-internship: header: "My internship" @@ -14,8 +22,21 @@ sections: steps: personal-data: header: "Fill personal data" + info: > + Your profile is incomplete. In order to continue your internship you have to supply information given below. In + case of problem with providing those information - please contact with your internship coordinator of your course. internship-proposal: header: "Internship proposal" form: "Internship proposal form" + info: "" plan: header: "Individual Internship Plan" + info: "" + template: "Download template" + submit: "Submit Individual Internship Plan" + insurance: + header: "Insurance" + report: + header: "Internship report" + grade: + header: "Your grade" diff --git a/translations/pl.yaml b/translations/pl.yaml index b931255..a08a967 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -11,17 +11,36 @@ sections: my-internship: header: "Moja praktyka" +student: + name: imię + surname: mazwisko + course: kierunek + semester: semestr + email: adres e-mail + albumNumber: numer albumu + steps: personal-data: header: "Uzupełnienie informacji" + info: > + Twój profil jest niekompletny. W celu kontynuacji praktyki musisz uzupełnić informacje podane poniżej. Jeżeli masz + problem z uzupełnieniem tych informacji - skontaktuj się z koordynatorem praktyk dla Twojego kierunku. + form: "Uzupełnij dane" internship-proposal: header: "Zgłoszenie praktyki" + info: > + Przed podjęciem praktyki należy ją zgłosić. form: "Formularz zgłaszania praktyki" plan: header: "Indywidualny Program Praktyki" + info: "" + template: "Pobierz szablon" + submit: "Wyślij Indywidualny Plan Praktyki" report: header: "Raport z praktyki" grade: header: "Ocena z praktyki" insurance: - header: "Ubezpieczenie NWW" + header: "Ubezpieczenie NNW" + +contact-coordinator: "Skontaktuj się z koordynatorem"