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 { 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;
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
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 { 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
20
src/app.tsx
20
src/app.tsx
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
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]);
|
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") }/>;
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 }` : '');
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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> {
|
||||||
|
@ -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>>()
|
||||||
|
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 { 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>;
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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/",
|
||||||
|
Loading…
Reference in New Issue
Block a user