Add step info

This commit is contained in:
Kacper Donat 2020-07-22 00:00:27 +02:00
parent a3db797a06
commit e012d015db
12 changed files with 231 additions and 64 deletions

View File

@ -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;

21
src/data/edition.ts Normal file
View File

@ -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,
}
}

View File

@ -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)[];
}

View File

@ -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')
);

View File

@ -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>

View File

@ -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")
}

View File

@ -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;

View File

@ -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;

View File

@ -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>;

View File

@ -8,7 +8,8 @@ const store = createStore(
persistReducer(
{
key: 'state',
storage: sessionStorage
storage: sessionStorage,
blacklist: ['edition']
},
rootReducer
),

View File

@ -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"

View File

@ -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"