Merge pull request 'feature/pages_from_api' (#16) from feature/pages_from_api into master

This commit is contained in:
Kacper Donat 2020-09-27 22:07:25 +02:00
commit 7a74ac5b2a
30 changed files with 436 additions and 84 deletions

23
src/api/dto/course.ts Normal file
View File

@ -0,0 +1,23 @@
import { Course, Identifiable } from "@/data";
import { Transformer } from "@/serialization";
export interface CourseDTO extends Identifiable {
name: string;
}
export const courseDtoTransformer: Transformer<CourseDTO, Course> = {
reverseTransform(subject: Course, context: undefined): CourseDTO {
return {
id: subject.id,
name: subject.name,
};
},
transform(subject: CourseDTO, context: undefined): Course {
return {
id: subject.id,
name: subject.name,
desiredSemesters: [],
possibleProgramEntries: [], // todo
};
}
}

57
src/api/dto/edition.ts Normal file
View File

@ -0,0 +1,57 @@
import { Identifiable } from "@/data";
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
import { OneWayTransformer, Transformer } from "@/serialization";
import { Edition } from "@/data/edition";
import moment from "moment";
import { Subset } from "@/helpers";
export interface EditionDTO extends Identifiable {
editionStart: string,
editionFinish: string,
reportingStart: string,
course: CourseDTO,
}
export interface EditionTeaserDTO extends Identifiable {
editionStart: string,
editionFinish: string,
courseName: string,
}
export const editionTeaserDtoTransformer: OneWayTransformer<EditionTeaserDTO, Subset<Edition>> = {
transform(subject: EditionTeaserDTO, context?: undefined): Subset<Edition> {
return {
id: subject.id,
startDate: moment(subject.editionStart),
endDate: moment(subject.editionFinish),
course: {
name: subject.courseName,
}
}
}
}
export const editionDtoTransformer: Transformer<EditionDTO, Edition> = {
reverseTransform(subject: Edition, context: undefined): EditionDTO {
return {
id: subject.id,
editionFinish: subject.endDate.toISOString(),
editionStart: subject.startDate.toISOString(),
course: courseDtoTransformer.reverseTransform(subject.course),
reportingStart: subject.reportingStart.toISOString(),
};
},
transform(subject: EditionDTO, context: undefined): Edition {
return {
id: subject.id,
course: courseDtoTransformer.transform(subject.course),
startDate: moment(subject.editionStart),
endDate: moment(subject.editionFinish),
minimumInternshipHours: 40,
maximumInternshipHours: 160,
proposalDeadline: moment(subject.reportingStart),
reportingStart: moment(subject.reportingStart),
reportingEnd: moment(subject.reportingStart).add(1, 'month'),
};
}
}

40
src/api/dto/page.ts Normal file
View File

@ -0,0 +1,40 @@
import { Identifiable } from "@/data";
import { Page } from "@/data/page";
import { Transformer } from "@/serialization";
export interface PageDTO extends Identifiable {
accessName: string;
title: string;
titleEng: string;
content: string;
contentEng: string;
}
export const pageDtoTransformer: Transformer<PageDTO, Page> = {
reverseTransform(subject: Page, context: undefined): PageDTO {
return {
id: subject.id,
accessName: subject.slug,
content: subject.content.pl,
contentEng: subject.content.en,
title: subject.title.pl,
titleEng: subject.title.en,
}
},
transform(subject: PageDTO, context: undefined): Page {
return {
slug: subject.accessName,
id: subject.id,
content: {
pl: subject.content,
en: subject.contentEng
},
title: {
pl: subject.title,
en: subject.titleEng
},
};
}
}
export default pageDtoTransformer;

34
src/api/dto/student.ts Normal file
View File

@ -0,0 +1,34 @@
import { Identifiable, Student } from "@/data";
import { Transformer } from "@/serialization";
export interface StudentDTO extends Identifiable {
albumNumber: number,
course: any,
email: string,
firstName: string,
lastName: string,
semester: number,
}
export const studentDtoTransfer: Transformer<StudentDTO, Student> = {
reverseTransform(subject: Student, context: undefined): StudentDTO {
return {
albumNumber: subject.albumNumber,
course: subject.course,
email: subject.email,
firstName: subject.name,
lastName: subject.surname,
semester: subject.semester
};
},
transform(subject: StudentDTO, context: undefined): Student {
return {
albumNumber: subject.albumNumber,
course: subject.course,
email: subject.email,
name: subject.firstName,
semester: subject.semester,
surname: subject.lastName
};
}
}

View File

@ -1,16 +1,16 @@
import { axios } from "@/api/index"; import { axios } from "@/api/index";
import { Edition } from "@/data/edition"; import { Edition } from "@/data/edition";
import { sampleEdition } from "@/provider/dummy"; import { prepare } from "@/routing";
import { delay } from "@/helpers"; import { EditionDTO, editionDtoTransformer, editionTeaserDtoTransformer } from "@/api/dto/edition";
const EDITIONS_ENDPOINT = "/editions"; const EDITIONS_ENDPOINT = "/editions";
const EDITION_INFO_ENDPOINT = "/editions/:key"; const EDITION_INFO_ENDPOINT = "/editions/:key";
const REGISTER_ENDPOINT = "/register"; const REGISTER_ENDPOINT = "/register";
export async function editions() { export async function available() {
const response = await axios.get(EDITIONS_ENDPOINT); const response = await axios.get(EDITIONS_ENDPOINT);
return response.data; return (response.data || []).map(editionTeaserDtoTransformer.transform);
} }
export async function join(key: string): Promise<boolean> { export async function join(key: string): Promise<boolean> {
@ -23,13 +23,9 @@ export async function join(key: string): Promise<boolean> {
} }
} }
// MOCK
export async function get(key: string): Promise<Edition | null> { export async function get(key: string): Promise<Edition | null> {
await delay(Math.random() * 200 + 100); const response = await axios.get<EditionDTO>(prepare(EDITION_INFO_ENDPOINT, { key }));
const dto = response.data;
if (key == "inf2020") { return editionDtoTransformer.transform(dto);
return sampleEdition;
}
return null;
} }

View File

@ -6,6 +6,7 @@ import { UserState } from "@/state/reducer/user";
import * as user from "./user"; import * as user from "./user";
import * as edition from "./edition"; import * as edition from "./edition";
import * as page from "./page" import * as page from "./page"
import * as student from "./student"
export const axios = Axios.create({ export const axios = Axios.create({
baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/", baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/",
@ -31,7 +32,8 @@ axios.interceptors.request.use(config => {
const api = { const api = {
user, user,
edition, edition,
page page,
student
} }
export default api; export default api;

View File

@ -1,27 +1,13 @@
// MOCK
import { Page } from "@/data/page"; import { Page } from "@/data/page";
import { PageDTO, pageDtoTransformer } from "./dto/page"
import { axios } from "@/api/index";
import { prepare } from "@/routing";
const tos = `<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Bestiarum vero nullum iudicium puto. Quare ad ea primum, si videtur; <b>Duo Reges: constructio interrete.</b> <i>Eam tum adesse, cum dolor omnis absit;</i> Sed ad bona praeterita redeamus. <mark>Facillimum id quidem est, inquam.</mark> Apud ceteros autem philosophos, qui quaesivit aliquid, tacet; </p> const STATIC_PAGE_ENDPOINT = "/staticPage/:slug"
<p><a href="http://loripsum.net/" target="_blank">Quorum altera prosunt, nocent altera.</a> Eam stabilem appellas. <i>Sed nimis multa.</i> Quo plebiscito decreta a senatu est consuli quaestio Cn. Sin laboramus, quis est, qui alienae modum statuat industriae? <mark>Quod quidem nobis non saepe contingit.</mark> Si autem id non concedatur, non continuo vita beata tollitur. <a href="http://loripsum.net/" target="_blank">Illum mallem levares, quo optimum atque humanissimum virum, Cn.</a> <i>Id est enim, de quo quaerimus.</i> </p>
<p>Ille vero, si insipiens-quo certe, quoniam tyrannus -, numquam beatus; Sin dicit obscurari quaedam nec apparere, quia valde parva sint, nos quoque concedimus; Et quod est munus, quod opus sapientiae? Ab hoc autem quaedam non melius quam veteres, quaedam omnino relicta. </p>
`
export async function get(slug: string): Promise<Page> { export async function get(slug: string): Promise<Page> {
if (slug === "/regulamin" || slug === "/rules") { const response = await axios.get<PageDTO>(prepare(STATIC_PAGE_ENDPOINT, { slug }))
return { const page = response.data;
id: "tak",
content: {
pl: tos,
en: tos,
},
title: {
pl: "Regulamin Praktyk",
en: "Terms of Internship",
},
}
}
throw new Error(); return pageDtoTransformer.transform(page);
} }

13
src/api/student.ts Normal file
View File

@ -0,0 +1,13 @@
import { axios } from "@/api/index";
import { Student } from "@/data/student";
import { StudentDTO, studentDtoTransfer } from "@/api/dto/student";
export const CURRENT_STUDENT_ENDPOINT = '/students/current';
export async function current(): Promise<Student> {
const response = await axios.get<StudentDTO>(CURRENT_STUDENT_ENDPOINT);
const dto = response.data;
return studentDtoTransfer.transform(dto);
}

View File

@ -1,9 +1,22 @@
import { axios } from "@/api/index"; import { axios } from "@/api/index";
import { query, route } from "@/routing";
const AUTHORIZE_ENDPOINT = "/access/login" const LOGIN_ENDPOINT = "/access/login"
export async function authorize(code: string): Promise<string> { const CLIENT_ID = process.env.LOGIN_CLIENT_ID || "PraktykiClientId";
const response = await axios.get<string>(AUTHORIZE_ENDPOINT, { params: { code }}); const AUTHORIZE_URL = process.env.AUTHORIZE || "https://logowanie.pg.edu.pl/oauth2.0/authorize";
export async function login(code: string): Promise<string> {
const response = await axios.get<string>(LOGIN_ENDPOINT, { params: { code }});
return response.data; return response.data;
} }
export function getAuthorizeUrl() {
return query(AUTHORIZE_URL, {
response_type: "code",
scope: "user_details",
client_id: CLIENT_ID,
redirect_uri: window.location.origin + route("user_login") + "/check/pg",
})
}

View File

@ -1,16 +1,14 @@
import React, { HTMLProps, useEffect } from 'react'; import React, { HTMLProps, useEffect } from 'react';
import { Link, Route, Switch } from "react-router-dom" import { Link, Route, Switch } from "react-router-dom"
import { route, routes } from "@/routing"; import { processMiddlewares, route, routes } from "@/routing";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { AppState, isReady } from "@/state/reducer"; import { AppState } from "@/state/reducer";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Student } from "@/data"; import { Student } from "@/data";
import '@/styles/overrides.scss' import '@/styles/overrides.scss'
import '@/styles/header.scss' import '@/styles/header.scss'
import '@/styles/footer.scss' import '@/styles/footer.scss'
import classNames from "classnames"; import classNames from "classnames";
import { EditionActions } from "@/state/actions/edition";
import { sampleEdition } from "@/provider/dummy/edition";
import { Edition } from "@/data/edition"; import { Edition } from "@/data/edition";
import { SettingActions } from "@/state/actions/settings"; import { SettingActions } from "@/state/actions/settings";
import { useDispatch, UserActions } from "@/state/actions"; import { useDispatch, UserActions } from "@/state/actions";
@ -68,20 +66,12 @@ function App() {
const { t } = useTranslation(); const { t } = useTranslation();
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings)); const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
useEffect(() => {
if (!edition) {
dispatch({ type: EditionActions.Set, edition: sampleEdition });
}
})
useEffect(() => { useEffect(() => {
i18n.changeLanguage(locale); i18n.changeLanguage(locale);
document.documentElement.lang = locale; document.documentElement.lang = locale;
moment.locale(locale) moment.locale(locale)
}, [ locale ]) }, [ locale ])
const ready = useSelector(isReady);
return <> return <>
<header className="header"> <header className="header">
<div id="logo" className="header__logo"> <div id="logo" className="header__logo">
@ -106,7 +96,11 @@ function App() {
</div> </div>
</header> </header>
<main id="content"> <main id="content">
{ ready && <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> } { <Switch>
{ routes.map(({ name, content, middlewares = [], ...route }) => <Route { ...route } key={ name }>
{ processMiddlewares([ ...middlewares, content ]) }
</Route>) }
</Switch> }
</main> </main>
<footer className="footer"> <footer className="footer">
<Container> <Container>

View File

@ -20,3 +20,5 @@ export const Section = ({ children, ...props }: PaperProps) => {
export const Label = ({ children }: TypographyProps) => { export const Label = ({ children }: TypographyProps) => {
return <Typography variant="subtitle2" className="proposal__header">{ children }</Typography> return <Typography variant="subtitle2" className="proposal__header">{ children }</Typography>
} }
Section.Label = Label;

View File

@ -1,14 +1,17 @@
import { Moment } from "moment"; import { Moment } from "moment";
import { Course } from "@/data/course"; import { Course } from "@/data/course";
import { Identifiable } from "@/data/common";
export type Edition = { export type Edition = {
course: Course; course: Course;
startDate: Moment; startDate: Moment;
endDate: Moment; endDate: Moment;
proposalDeadline: Moment; proposalDeadline: Moment;
reportingStart: Moment,
reportingEnd: Moment,
minimumInternshipHours: number; minimumInternshipHours: number;
maximumInternshipHours?: number; maximumInternshipHours?: number;
} } & Identifiable
export type Deadlines = { export type Deadlines = {
personalData?: Moment; personalData?: Moment;

View File

@ -3,4 +3,5 @@ import { Identifiable, Multilingual } from "@/data/common";
export interface Page extends Identifiable { export interface Page extends Identifiable {
title: Multilingual<string>; title: Multilingual<string>;
content: Multilingual<string>; content: Multilingual<string>;
slug: string;
} }

View File

@ -7,7 +7,7 @@ export interface Student extends Identifiable {
name: string; name: string;
surname: string; surname: string;
email: string; email: string;
albumNumber: string; albumNumber: number;
semester: Semester; semester: Semester;
course: Course; course: Course;
} }

View File

@ -1,6 +1,6 @@
export type Nullable<T> = { [P in keyof T]: T[P] | null } export type Nullable<T> = { [P in keyof T]: T[P] | null }
export type Partial<T> = { [K in keyof T]?: T[K] } export type Subset<T> = { [K in keyof T]?: Subset<T[K]> }
export type Dictionary<T> = { [key: string]: T }; export type Dictionary<T> = { [key: string]: T };
export type Index = string | symbol | number; export type Index = string | symbol | number;

View File

@ -8,12 +8,14 @@ export type AsyncResult<T, TError = any> = {
export type AsyncState<T, TError = any> = [AsyncResult<T, TError>, (promise: Promise<T> | undefined) => void] export type AsyncState<T, TError = any> = [AsyncResult<T, TError>, (promise: Promise<T> | undefined) => void]
export function useAsync<T, TError = any>(promise: Promise<T> | undefined): AsyncResult<T, TError> { export function useAsync<T, TError = any>(supplier: Promise<T> | (() => Promise<T>) | undefined): AsyncResult<T, TError> {
const [isLoading, setLoading] = useState<boolean>(true); const [isLoading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<TError | undefined>(undefined); const [error, setError] = useState<TError | undefined>(undefined);
const [value, setValue] = useState<T | undefined>(undefined); const [value, setValue] = useState<T | undefined>(undefined);
const [semaphore] = useState<{ value: number }>({ value: 0 }) const [semaphore] = useState<{ value: number }>({ value: 0 })
const [promise, setPromise] = useState(typeof supplier === "function" ? null : supplier)
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
setError(undefined); setError(undefined);
@ -35,6 +37,12 @@ export function useAsync<T, TError = any>(promise: Promise<T> | undefined): Asyn
}) })
}, [ promise ]) }, [ promise ])
useEffect(() => {
if (typeof supplier === "function") {
setPromise(supplier());
}
}, [])
return { return {
isLoading, isLoading,
value, value,

15
src/middleware.tsx Normal file
View File

@ -0,0 +1,15 @@
import { Middleware, route } from "@/routing";
import { useSelector } from "react-redux";
import { isReady } from "@/state/reducer";
import { Redirect } from "react-router-dom";
import React from "react";
export const isReadyMiddleware: Middleware<any, any> = next => {
const ready = useSelector(isReady);
if (ready) {
return next();
}
return <Redirect to={ route("edition_pick") } />;
}

View File

@ -0,0 +1,72 @@
import { Page } from "@/pages/base";
import React from "react";
import { useTranslation } from "react-i18next";
import { Box, Button, CircularProgress, Container, Typography } from "@material-ui/core";
import { Actions } from "@/components";
import { Link as RouterLink, useHistory } from "react-router-dom"
import { route } from "@/routing";
import { AccountArrowRight } from "mdi-material-ui";
import { useAsync } from "@/hooks";
import api from "@/api";
import { Section } from "@/components/section";
import { useVerticalSpacing } from "@/styles";
import { Alert } from "@material-ui/lab";
import { EditionActions, useDispatch } from "@/state/actions";
export const PickEditionPage = () => {
const { t } = useTranslation();
const { value: editions, isLoading } = useAsync(() => api.edition.available());
const dispatch = useDispatch();
const history = useHistory();
const classes = useVerticalSpacing(3);
const pickEditionHandler = (id: string) => async () => {
const edition = await api.edition.get(id);
if (!edition) {
return;
}
dispatch({
type: EditionActions.Set,
edition
})
history.push("/");
}
return <Page>
<Page.Header maxWidth="md">
<Page.Title>{ t("pages.pick-edition.title") }</Page.Title>
</Page.Header>
<Container className={ classes.root }>
<Typography variant="h3">{ t("pages.pick-edition.my-editions") }</Typography>
{ isLoading ? <CircularProgress /> : <div>
{ editions.length > 0 ? editions.map((edition: any) =>
<Section key={ edition.id }>
<Typography className="proposal__primary">{ edition.course.name }</Typography>
<Typography className="proposal__secondary">
{ t('internship.date-range', { start: edition.startDate, end: edition.endDate }) }
</Typography>
<Box mt={2}>
<Actions>
<Button variant="contained" color="primary" onClick={ pickEditionHandler(edition.id) }>{ t('pages.pick-edition.pick') }</Button>
</Actions>
</Box>
</Section>
) : <Alert severity="info">{ t("pages.pick-edition.no-editions") }</Alert> }
</div> }
<Actions>
<Button variant="contained" color="primary"
startIcon={ <AccountArrowRight /> }
component={ RouterLink } to={ route("edition_register") }>
{ t("pages.pick-edition.register") }
</Button>
</Actions>
</Container>
</Page>
}
export default PickEditionPage;

View File

@ -25,7 +25,7 @@ export const MainPage = () => {
const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]);
useEffect(() => void api.edition.editions()) useEffect(() => void api.edition.available())
if (!student) { if (!student) {
return <Redirect to={ route("user_login") }/>; return <Redirect to={ route("user_login") }/>;

View File

@ -1,46 +1,78 @@
import React, { Dispatch } from "react"; import React, { Dispatch, useEffect } from "react";
import { Page } from "@/pages/base"; import { Page } from "@/pages/base";
import { Button, Container, Typography } from "@material-ui/core"; import { Button, Container } from "@material-ui/core";
import { Action, useDispatch } from "@/state/actions"; import { Action, StudentActions, useDispatch } from "@/state/actions";
import { useHistory } from "react-router-dom"; import { Route, Switch, useHistory, useLocation, useRouteMatch } from "react-router-dom";
import { route } from "@/routing"; import { route } from "@/routing";
import { useVerticalSpacing } from "@/styles"; import { useVerticalSpacing } from "@/styles";
import { AppState } from "@/state/reducer"; import { AppState } from "@/state/reducer";
import api from "@/api"; import api from "@/api";
import { UserActions } from "@/state/actions/user"; import { UserActions } from "@/state/actions/user";
import { sampleStudent } from "@/provider/dummy"; import { getAuthorizeUrl } from "@/api/user";
const authorizeUser = async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => { const authorizeUser = (code: string) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
const token = await api.user.authorize("test"); const token = await api.user.login(code);
dispatch({ dispatch({
type: UserActions.Login, type: UserActions.Login,
token, token,
student: sampleStudent, })
const student = await api.student.current();
dispatch({
type: StudentActions.Set,
student: student,
}) })
} }
export const UserLoginPage = () => { export const UserLoginPage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const match = useRouteMatch();
const location = useLocation();
const query = new URLSearchParams(useLocation().search);
const handleSampleLogin = async () => { const handleSampleLogin = async () => {
await dispatch(authorizeUser); await dispatch(authorizeUser("test"));
history.push(route("home")); history.push(route("home"));
} }
const handlePgLogin = async () => {
history.push(route("user_login") + "/pg");
}
const classes = useVerticalSpacing(3); const classes = useVerticalSpacing(3);
useEffect(() => {
(async function() {
if (location.pathname === `${match.path}/check/pg`) {
await dispatch(authorizeUser(query.get("code") as string));
history.push("/");
}
})();
}, [ match.path ]);
return <Page> return <Page>
<Page.Header maxWidth="md"> <Page.Header maxWidth="md">
<Page.Title>Tu miało być przekierowanie do logowania PG...</Page.Title> <Page.Title>Zaloguj się</Page.Title>
</Page.Header> </Page.Header>
<Container maxWidth="md" className={ classes.root }> <Container>
<Typography variant="h3">... ale wciąż czekamy na dostęp :(</Typography> <Switch>
<Route path={match.path} exact>
<Button fullWidth onClick={ handleSampleLogin } variant="contained" color="primary">Zaloguj jako przykładowy student</Button> <Container maxWidth="md" className={ classes.root }>
<Button fullWidth onClick={ handlePgLogin } variant="contained" color="primary">Zaloguj się z pomocą konta PG</Button>
<Button fullWidth onClick={ handleSampleLogin } variant="contained" color="secondary">Zaloguj jako przykładowy student</Button>
</Container>
</Route>
<Route path={`${match.path}/pg`} render={
() => (window.location.href = getAuthorizeUrl())
} />
<Route path={`${match.path}/check/pg`}>
Kod: { query.get("code") }
</Route>
</Switch>
</Container> </Container>
</Page>; </Page>;
} }

View File

@ -32,7 +32,7 @@ export const sampleStudent: Student = {
id: studentIdSequence(), id: studentIdSequence(),
name: "Jan", name: "Jan",
surname: "Kowalski", surname: "Kowalski",
albumNumber: "123456", albumNumber: 123456,
email: "s123456@student.pg.edu.pl", email: "s123456@student.pg.edu.pl",
course: sampleCourse, course: sampleCourse,
semester: 6, semester: 6,

View File

@ -6,26 +6,43 @@ import { FallbackPage } from "@/pages/fallback";
import SubmitPlanPage from "@/pages/internship/plan"; import SubmitPlanPage from "@/pages/internship/plan";
import { UserLoginPage } from "@/pages/user/login"; import { UserLoginPage } from "@/pages/user/login";
import { RegisterEditionPage } from "@/pages/edition/register"; import { RegisterEditionPage } from "@/pages/edition/register";
import PickEditionPage from "@/pages/edition/pick";
import { isReadyMiddleware } from "@/middleware";
type Route = { type Route = {
name?: string; name?: string;
content: () => ReactComponentElement<any>, content: () => ReactComponentElement<any>,
condition?: () => boolean, condition?: () => boolean,
middlewares?: Middleware<any, any>[],
} & RouteProps; } & RouteProps;
export type Middleware<TReturn, TArgs extends any[]> = (next: () => any, ...args: TArgs) => TReturn;
export function processMiddlewares<TArgs extends any[]>(middleware: Middleware<any, TArgs>[], ...args: TArgs): any {
if (middleware.length == 0) {
return null;
}
const current = middleware.slice(0, 1)[0];
const left = middleware.slice(1);
return current(() => processMiddlewares(left, ...args), ...args);
}
export const routes: Route[] = [ export const routes: Route[] = [
{ name: "home", path: "/", exact: true, content: () => <MainPage/> }, { name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isReadyMiddleware ] },
// edition // edition
{ name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/> }, { name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/> },
{ name: "edition_pick", path: "/edition/pick", exact: true, content: () => <PickEditionPage/> },
// internship // internship
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/> }, { name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/>, middlewares: [ isReadyMiddleware ] },
{ name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/> }, { name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/>, middlewares: [ isReadyMiddleware ] },
{ name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/> }, { name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/> },
// user // user
{ name: "user_login", path: "/user/login", exact: true, content: () => <UserLoginPage /> }, { name: "user_login", path: "/user/login", content: () => <UserLoginPage/> },
// fallback route for 404 pages // fallback route for 404 pages
{ name: "fallback", path: "*", content: () => <FallbackPage/> } { name: "fallback", path: "*", content: () => <FallbackPage/> }
@ -44,3 +61,9 @@ export function route(name: string, params: URLParams = {}) {
return prepare(url, params) return prepare(url, params)
} }
export const query = (url: string, params: URLParams) => {
const query = Object.entries(params).map(([name, value]) => `${ name }=${ encodeURIComponent(value) }`).join("&");
return url + (query.length > 0 ? `?${ query }` : '');
}

View File

@ -11,8 +11,11 @@ type Simplify<T> = string |
export type Serializable<T> = { [K in keyof T]: Simplify<T[K]> } export type Serializable<T> = { [K in keyof T]: Simplify<T[K]> }
export type Transformer<TFrom, TResult, TContext = never> = { export type Transformer<TFrom, TResult, TContext = never> = {
transform(subject: TFrom, context?: TContext): TResult;
reverseTransform(subject: TResult, context?: TContext): TFrom; reverseTransform(subject: TResult, context?: TContext): TFrom;
} } & OneWayTransformer<TFrom, TResult, TContext>
export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized> export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized>
export type OneWayTransformer<TFrom, TResult, TContext = never> = {
transform(subject: TFrom, context?: TContext): TResult;
}

View File

@ -2,7 +2,7 @@ import { Action } from "@/state/actions/base";
import { Edition } from "@/data/edition"; import { Edition } from "@/data/edition";
export enum EditionActions { export enum EditionActions {
Set = 'SET', Set = 'SET_EDITION',
} }
export interface SetAction extends Action<EditionActions.Set> { export interface SetAction extends Action<EditionActions.Set> {

View File

@ -8,6 +8,7 @@ import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance";
import { UserAction, UserActions } from "@/state/actions/user"; import { UserAction, UserActions } from "@/state/actions/user";
import { ThunkDispatch } from "redux-thunk"; import { ThunkDispatch } from "redux-thunk";
import { AppState } from "@/state/reducer"; import { AppState } from "@/state/reducer";
import { StudentAction, StudentActions } from "@/state/actions/student";
export * from "./base" export * from "./base"
export * from "./edition" export * from "./edition"
@ -15,10 +16,26 @@ export * from "./settings"
export * from "./proposal" export * from "./proposal"
export * from "./plan" export * from "./plan"
export * from "./user" export * from "./user"
export * from "./student"
export type Action = UserAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction | InsuranceAction; export type Action
= UserAction
| EditionAction
| SettingsAction
| InternshipProposalAction
| StudentAction
| InternshipPlanAction
| InsuranceAction;
export const Actions = { ...UserActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions, ...InsuranceActions } export const Actions = {
...UserActions,
...EditionActions,
...SettingActions,
...InternshipProposalActions,
...InternshipPlanActions,
...InsuranceActions,
...StudentActions,
}
export type Actions = typeof Actions; export type Actions = typeof Actions;
export const useDispatch = () => useReduxDispatch<ThunkDispatch<AppState, any, Action>>() export const useDispatch = () => useReduxDispatch<ThunkDispatch<AppState, any, Action>>()

View File

@ -0,0 +1,13 @@
import { Action } from "@/state/actions/base";
import { Student } from "@/data";
export enum StudentActions {
Set = 'SET_STUDENT',
}
export interface SetStudentAction extends Action<StudentActions.Set> {
student: Student,
}
export type StudentAction = SetStudentAction;

View File

@ -1,5 +1,4 @@
import { Action } from "@/state/actions/base"; import { Action } from "@/state/actions/base";
import { Student } from "@/data";
export enum UserActions { export enum UserActions {
Login = 'LOGIN', Login = 'LOGIN',
@ -8,7 +7,6 @@ export enum UserActions {
export interface LoginAction extends Action<UserActions.Login> { export interface LoginAction extends Action<UserActions.Login> {
token: string; token: string;
student: Student;
} }
export type LogoutAction = Action<UserActions.Logout>; export type LogoutAction = Action<UserActions.Logout>;

View File

@ -1,13 +1,14 @@
import { Student } from "@/data"; import { Student } from "@/data";
import { UserAction, UserActions } from "@/state/actions/user"; import { UserAction, UserActions } from "@/state/actions/user";
import { StudentAction, StudentActions } from "@/state/actions/student";
export type StudentState = Student | null; export type StudentState = Student | null;
const initialStudentState: StudentState = null; const initialStudentState: StudentState = null;
const studentReducer = (state: StudentState = initialStudentState, action: UserAction): StudentState => { const studentReducer = (state: StudentState = initialStudentState, action: UserAction | StudentAction): StudentState => {
switch (action.type) { switch (action.type) {
case UserActions.Login: case StudentActions.Set:
return action.student; return action.student;
case UserActions.Logout: case UserActions.Logout:

View File

@ -34,6 +34,11 @@ pages:
header: "Zgłoszenie praktyki" header: "Zgłoszenie praktyki"
edition: edition:
header: "Zapisz się do edycji" header: "Zapisz się do edycji"
pick-edition:
title: "Wybór edycji"
my-editions: "Moje praktyki"
pick: "wybierz"
register: "Zapisz się do edycji praktyk"
forms: forms:
internship: internship:

View File

@ -51,11 +51,12 @@ const config = {
], ],
devServer: { devServer: {
contentBase: path.resolve("./public/"), contentBase: path.resolve("./public/"),
port: 3000, host: process.env.APP_HOST || 'system-praktyk-front.localhost',
host: 'system-praktyk-front.localhost',
disableHostCheck: true, disableHostCheck: true,
historyApiFallback: true, historyApiFallback: true,
overlay: true, overlay: true,
https: !!process.env.APP_HTTPS || false,
port: parseInt(process.env.APP_PORT || "3000"),
proxy: { proxy: {
"/api": { "/api": {
target: "http://system-praktyk-front.localhost:8080/", target: "http://system-praktyk-front.localhost:8080/",