Merge pull request 'feature/pages_from_api' (#16) from feature/pages_from_api into master
This commit is contained in:
commit
7a74ac5b2a
23
src/api/dto/course.ts
Normal file
23
src/api/dto/course.ts
Normal 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
57
src/api/dto/edition.ts
Normal 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
40
src/api/dto/page.ts
Normal 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
34
src/api/dto/student.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
import { axios } from "@/api/index";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { sampleEdition } from "@/provider/dummy";
|
||||
import { delay } from "@/helpers";
|
||||
import { prepare } from "@/routing";
|
||||
import { EditionDTO, editionDtoTransformer, editionTeaserDtoTransformer } from "@/api/dto/edition";
|
||||
|
||||
const EDITIONS_ENDPOINT = "/editions";
|
||||
const EDITION_INFO_ENDPOINT = "/editions/:key";
|
||||
const REGISTER_ENDPOINT = "/register";
|
||||
|
||||
export async function editions() {
|
||||
export async function available() {
|
||||
const response = await axios.get(EDITIONS_ENDPOINT);
|
||||
|
||||
return response.data;
|
||||
return (response.data || []).map(editionTeaserDtoTransformer.transform);
|
||||
}
|
||||
|
||||
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> {
|
||||
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 sampleEdition;
|
||||
}
|
||||
|
||||
return null;
|
||||
return editionDtoTransformer.transform(dto);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { UserState } from "@/state/reducer/user";
|
||||
import * as user from "./user";
|
||||
import * as edition from "./edition";
|
||||
import * as page from "./page"
|
||||
import * as student from "./student"
|
||||
|
||||
export const axios = Axios.create({
|
||||
baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/",
|
||||
@ -31,7 +32,8 @@ axios.interceptors.request.use(config => {
|
||||
const api = {
|
||||
user,
|
||||
edition,
|
||||
page
|
||||
page,
|
||||
student
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
@ -1,27 +1,13 @@
|
||||
// MOCK
|
||||
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>
|
||||
|
||||
<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>
|
||||
`
|
||||
const STATIC_PAGE_ENDPOINT = "/staticPage/:slug"
|
||||
|
||||
export async function get(slug: string): Promise<Page> {
|
||||
if (slug === "/regulamin" || slug === "/rules") {
|
||||
return {
|
||||
id: "tak",
|
||||
content: {
|
||||
pl: tos,
|
||||
en: tos,
|
||||
},
|
||||
title: {
|
||||
pl: "Regulamin Praktyk",
|
||||
en: "Terms of Internship",
|
||||
},
|
||||
}
|
||||
}
|
||||
const response = await axios.get<PageDTO>(prepare(STATIC_PAGE_ENDPOINT, { slug }))
|
||||
const page = response.data;
|
||||
|
||||
throw new Error();
|
||||
return pageDtoTransformer.transform(page);
|
||||
}
|
||||
|
13
src/api/student.ts
Normal file
13
src/api/student.ts
Normal 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);
|
||||
}
|
||||
|
@ -1,9 +1,22 @@
|
||||
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 response = await axios.get<string>(AUTHORIZE_ENDPOINT, { params: { code }});
|
||||
const CLIENT_ID = process.env.LOGIN_CLIENT_ID || "PraktykiClientId";
|
||||
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;
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
20
src/app.tsx
20
src/app.tsx
@ -1,16 +1,14 @@
|
||||
import React, { HTMLProps, useEffect } from 'react';
|
||||
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 { AppState, isReady } from "@/state/reducer";
|
||||
import { AppState } from "@/state/reducer";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Student } from "@/data";
|
||||
import '@/styles/overrides.scss'
|
||||
import '@/styles/header.scss'
|
||||
import '@/styles/footer.scss'
|
||||
import classNames from "classnames";
|
||||
import { EditionActions } from "@/state/actions/edition";
|
||||
import { sampleEdition } from "@/provider/dummy/edition";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { SettingActions } from "@/state/actions/settings";
|
||||
import { useDispatch, UserActions } from "@/state/actions";
|
||||
@ -68,20 +66,12 @@ function App() {
|
||||
const { t } = useTranslation();
|
||||
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
|
||||
|
||||
useEffect(() => {
|
||||
if (!edition) {
|
||||
dispatch({ type: EditionActions.Set, edition: sampleEdition });
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(locale);
|
||||
document.documentElement.lang = locale;
|
||||
moment.locale(locale)
|
||||
}, [ locale ])
|
||||
|
||||
const ready = useSelector(isReady);
|
||||
|
||||
return <>
|
||||
<header className="header">
|
||||
<div id="logo" className="header__logo">
|
||||
@ -106,7 +96,11 @@ function App() {
|
||||
</div>
|
||||
</header>
|
||||
<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>
|
||||
<footer className="footer">
|
||||
<Container>
|
||||
|
@ -20,3 +20,5 @@ export const Section = ({ children, ...props }: PaperProps) => {
|
||||
export const Label = ({ children }: TypographyProps) => {
|
||||
return <Typography variant="subtitle2" className="proposal__header">{ children }</Typography>
|
||||
}
|
||||
|
||||
Section.Label = Label;
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { Moment } from "moment";
|
||||
import { Course } from "@/data/course";
|
||||
import { Identifiable } from "@/data/common";
|
||||
|
||||
export type Edition = {
|
||||
course: Course;
|
||||
startDate: Moment;
|
||||
endDate: Moment;
|
||||
proposalDeadline: Moment;
|
||||
reportingStart: Moment,
|
||||
reportingEnd: Moment,
|
||||
minimumInternshipHours: number;
|
||||
maximumInternshipHours?: number;
|
||||
}
|
||||
} & Identifiable
|
||||
|
||||
export type Deadlines = {
|
||||
personalData?: Moment;
|
||||
|
@ -3,4 +3,5 @@ import { Identifiable, Multilingual } from "@/data/common";
|
||||
export interface Page extends Identifiable {
|
||||
title: Multilingual<string>;
|
||||
content: Multilingual<string>;
|
||||
slug: string;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export interface Student extends Identifiable {
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
albumNumber: string;
|
||||
albumNumber: number;
|
||||
semester: Semester;
|
||||
course: Course;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 Index = string | symbol | number;
|
||||
|
@ -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 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 [error, setError] = useState<TError | undefined>(undefined);
|
||||
const [value, setValue] = useState<T | undefined>(undefined);
|
||||
const [semaphore] = useState<{ value: number }>({ value: 0 })
|
||||
|
||||
const [promise, setPromise] = useState(typeof supplier === "function" ? null : supplier)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
@ -35,6 +37,12 @@ export function useAsync<T, TError = any>(promise: Promise<T> | undefined): Asyn
|
||||
})
|
||||
}, [ promise ])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof supplier === "function") {
|
||||
setPromise(supplier());
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
value,
|
||||
|
15
src/middleware.tsx
Normal file
15
src/middleware.tsx
Normal 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") } />;
|
||||
}
|
72
src/pages/edition/pick.tsx
Normal file
72
src/pages/edition/pick.tsx
Normal 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;
|
@ -25,7 +25,7 @@ export const MainPage = () => {
|
||||
|
||||
const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]);
|
||||
|
||||
useEffect(() => void api.edition.editions())
|
||||
useEffect(() => void api.edition.available())
|
||||
|
||||
if (!student) {
|
||||
return <Redirect to={ route("user_login") }/>;
|
||||
|
@ -1,46 +1,78 @@
|
||||
import React, { Dispatch } from "react";
|
||||
import React, { Dispatch, useEffect } from "react";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Button, Container, Typography } from "@material-ui/core";
|
||||
import { Action, useDispatch } from "@/state/actions";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Button, Container } from "@material-ui/core";
|
||||
import { Action, StudentActions, useDispatch } from "@/state/actions";
|
||||
import { Route, Switch, useHistory, useLocation, useRouteMatch } from "react-router-dom";
|
||||
import { route } from "@/routing";
|
||||
import { useVerticalSpacing } from "@/styles";
|
||||
import { AppState } from "@/state/reducer";
|
||||
|
||||
import api from "@/api";
|
||||
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 token = await api.user.authorize("test");
|
||||
const authorizeUser = (code: string) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
|
||||
const token = await api.user.login(code);
|
||||
|
||||
dispatch({
|
||||
type: UserActions.Login,
|
||||
token,
|
||||
student: sampleStudent,
|
||||
})
|
||||
|
||||
const student = await api.student.current();
|
||||
dispatch({
|
||||
type: StudentActions.Set,
|
||||
student: student,
|
||||
})
|
||||
}
|
||||
|
||||
export const UserLoginPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
const location = useLocation();
|
||||
const query = new URLSearchParams(useLocation().search);
|
||||
|
||||
const handleSampleLogin = async () => {
|
||||
await dispatch(authorizeUser);
|
||||
await dispatch(authorizeUser("test"));
|
||||
|
||||
history.push(route("home"));
|
||||
}
|
||||
|
||||
const handlePgLogin = async () => {
|
||||
history.push(route("user_login") + "/pg");
|
||||
}
|
||||
|
||||
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>
|
||||
<Page.Header maxWidth="md">
|
||||
<Page.Title>Tu miało być przekierowanie do logowania PG...</Page.Title>
|
||||
<Page.Title>Zaloguj się</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="md" className={ classes.root }>
|
||||
<Typography variant="h3">... ale wciąż czekamy na dostęp :(</Typography>
|
||||
|
||||
<Button fullWidth onClick={ handleSampleLogin } variant="contained" color="primary">Zaloguj jako przykładowy student</Button>
|
||||
<Container>
|
||||
<Switch>
|
||||
<Route path={match.path} exact>
|
||||
<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>
|
||||
</Page>;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export const sampleStudent: Student = {
|
||||
id: studentIdSequence(),
|
||||
name: "Jan",
|
||||
surname: "Kowalski",
|
||||
albumNumber: "123456",
|
||||
albumNumber: 123456,
|
||||
email: "s123456@student.pg.edu.pl",
|
||||
course: sampleCourse,
|
||||
semester: 6,
|
||||
|
@ -6,26 +6,43 @@ import { FallbackPage } from "@/pages/fallback";
|
||||
import SubmitPlanPage from "@/pages/internship/plan";
|
||||
import { UserLoginPage } from "@/pages/user/login";
|
||||
import { RegisterEditionPage } from "@/pages/edition/register";
|
||||
import PickEditionPage from "@/pages/edition/pick";
|
||||
import { isReadyMiddleware } from "@/middleware";
|
||||
|
||||
type Route = {
|
||||
name?: string;
|
||||
content: () => ReactComponentElement<any>,
|
||||
condition?: () => boolean,
|
||||
middlewares?: Middleware<any, any>[],
|
||||
} & 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[] = [
|
||||
{ name: "home", path: "/", exact: true, content: () => <MainPage/> },
|
||||
{ name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isReadyMiddleware ] },
|
||||
|
||||
// edition
|
||||
{ name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/> },
|
||||
{ name: "edition_pick", path: "/edition/pick", exact: true, content: () => <PickEditionPage/> },
|
||||
|
||||
// internship
|
||||
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/> },
|
||||
{ name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/> },
|
||||
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/>, middlewares: [ isReadyMiddleware ] },
|
||||
{ name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/>, middlewares: [ isReadyMiddleware ] },
|
||||
{ name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/> },
|
||||
|
||||
// user
|
||||
{ name: "user_login", path: "/user/login", exact: true, content: () => <UserLoginPage /> },
|
||||
{ name: "user_login", path: "/user/login", content: () => <UserLoginPage/> },
|
||||
|
||||
// fallback route for 404 pages
|
||||
{ name: "fallback", path: "*", content: () => <FallbackPage/> }
|
||||
@ -44,3 +61,9 @@ export function route(name: string, params: URLParams = {}) {
|
||||
|
||||
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 }` : '');
|
||||
}
|
||||
|
@ -11,8 +11,11 @@ type Simplify<T> = string |
|
||||
|
||||
export type Serializable<T> = { [K in keyof T]: Simplify<T[K]> }
|
||||
export type Transformer<TFrom, TResult, TContext = never> = {
|
||||
transform(subject: TFrom, context?: TContext): TResult;
|
||||
reverseTransform(subject: TResult, context?: TContext): TFrom;
|
||||
}
|
||||
} & OneWayTransformer<TFrom, TResult, TContext>
|
||||
|
||||
export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized>
|
||||
|
||||
export type OneWayTransformer<TFrom, TResult, TContext = never> = {
|
||||
transform(subject: TFrom, context?: TContext): TResult;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Action } from "@/state/actions/base";
|
||||
import { Edition } from "@/data/edition";
|
||||
|
||||
export enum EditionActions {
|
||||
Set = 'SET',
|
||||
Set = 'SET_EDITION',
|
||||
}
|
||||
|
||||
export interface SetAction extends Action<EditionActions.Set> {
|
||||
|
@ -8,6 +8,7 @@ import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance";
|
||||
import { UserAction, UserActions } from "@/state/actions/user";
|
||||
import { ThunkDispatch } from "redux-thunk";
|
||||
import { AppState } from "@/state/reducer";
|
||||
import { StudentAction, StudentActions } from "@/state/actions/student";
|
||||
|
||||
export * from "./base"
|
||||
export * from "./edition"
|
||||
@ -15,10 +16,26 @@ export * from "./settings"
|
||||
export * from "./proposal"
|
||||
export * from "./plan"
|
||||
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 const useDispatch = () => useReduxDispatch<ThunkDispatch<AppState, any, Action>>()
|
||||
|
13
src/state/actions/student.ts
Normal file
13
src/state/actions/student.ts
Normal 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;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Action } from "@/state/actions/base";
|
||||
import { Student } from "@/data";
|
||||
|
||||
export enum UserActions {
|
||||
Login = 'LOGIN',
|
||||
@ -8,7 +7,6 @@ export enum UserActions {
|
||||
|
||||
export interface LoginAction extends Action<UserActions.Login> {
|
||||
token: string;
|
||||
student: Student;
|
||||
}
|
||||
|
||||
export type LogoutAction = Action<UserActions.Logout>;
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { Student } from "@/data";
|
||||
import { UserAction, UserActions } from "@/state/actions/user";
|
||||
import { StudentAction, StudentActions } from "@/state/actions/student";
|
||||
|
||||
export type StudentState = Student | 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) {
|
||||
case UserActions.Login:
|
||||
case StudentActions.Set:
|
||||
return action.student;
|
||||
|
||||
case UserActions.Logout:
|
||||
|
@ -34,6 +34,11 @@ pages:
|
||||
header: "Zgłoszenie praktyki"
|
||||
edition:
|
||||
header: "Zapisz się do edycji"
|
||||
pick-edition:
|
||||
title: "Wybór edycji"
|
||||
my-editions: "Moje praktyki"
|
||||
pick: "wybierz"
|
||||
register: "Zapisz się do edycji praktyk"
|
||||
|
||||
forms:
|
||||
internship:
|
||||
|
@ -51,11 +51,12 @@ const config = {
|
||||
],
|
||||
devServer: {
|
||||
contentBase: path.resolve("./public/"),
|
||||
port: 3000,
|
||||
host: 'system-praktyk-front.localhost',
|
||||
host: process.env.APP_HOST || 'system-praktyk-front.localhost',
|
||||
disableHostCheck: true,
|
||||
historyApiFallback: true,
|
||||
overlay: true,
|
||||
https: !!process.env.APP_HTTPS || false,
|
||||
port: parseInt(process.env.APP_PORT || "3000"),
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://system-praktyk-front.localhost:8080/",
|
||||
|
Loading…
Reference in New Issue
Block a user