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"