diff --git a/src/api/dto/course.ts b/src/api/dto/course.ts new file mode 100644 index 0000000..8b13f1f --- /dev/null +++ b/src/api/dto/course.ts @@ -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 + }; + } +} diff --git a/src/api/dto/edition.ts b/src/api/dto/edition.ts new file mode 100644 index 0000000..94c3101 --- /dev/null +++ b/src/api/dto/edition.ts @@ -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'), + }; + } +} diff --git a/src/api/dto/page.ts b/src/api/dto/page.ts new file mode 100644 index 0000000..390fa03 --- /dev/null +++ b/src/api/dto/page.ts @@ -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; diff --git a/src/api/dto/student.ts b/src/api/dto/student.ts new file mode 100644 index 0000000..269e1aa --- /dev/null +++ b/src/api/dto/student.ts @@ -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 + }; + } +} diff --git a/src/api/edition.ts b/src/api/edition.ts index bce8982..6164634 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -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); } diff --git a/src/api/index.ts b/src/api/index.ts index 6d9942d..3432330 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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; diff --git a/src/api/page.tsx b/src/api/page.tsx index f1c69ee..1a5cd02 100644 --- a/src/api/page.tsx +++ b/src/api/page.tsx @@ -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); } diff --git a/src/api/student.ts b/src/api/student.ts new file mode 100644 index 0000000..e93a861 --- /dev/null +++ b/src/api/student.ts @@ -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); +} + diff --git a/src/api/user.ts b/src/api/user.ts index ba70d5f..e930190 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -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", + }) +} diff --git a/src/app.tsx b/src/app.tsx index c05b38c..a3bb940 100644 --- a/src/app.tsx +++ b/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> diff --git a/src/components/section.tsx b/src/components/section.tsx index c91449d..f2a4189 100644 --- a/src/components/section.tsx +++ b/src/components/section.tsx @@ -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; diff --git a/src/data/edition.ts b/src/data/edition.ts index 8abaa77..6ca8725 100644 --- a/src/data/edition.ts +++ b/src/data/edition.ts @@ -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; diff --git a/src/data/page.ts b/src/data/page.ts index a474e82..ca7e3c9 100644 --- a/src/data/page.ts +++ b/src/data/page.ts @@ -3,4 +3,5 @@ import { Identifiable, Multilingual } from "@/data/common"; export interface Page extends Identifiable { title: Multilingual<string>; content: Multilingual<string>; + slug: string; } diff --git a/src/data/student.ts b/src/data/student.ts index b8eabfc..d649fbd 100644 --- a/src/data/student.ts +++ b/src/data/student.ts @@ -7,7 +7,7 @@ export interface Student extends Identifiable { name: string; surname: string; email: string; - albumNumber: string; + albumNumber: number; semester: Semester; course: Course; } diff --git a/src/helpers.ts b/src/helpers.ts index ae9da92..fc5e38e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -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; diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index 91e1de2..a8293d4 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -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, diff --git a/src/middleware.tsx b/src/middleware.tsx new file mode 100644 index 0000000..7d16ca3 --- /dev/null +++ b/src/middleware.tsx @@ -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") } />; +} diff --git a/src/pages/edition/pick.tsx b/src/pages/edition/pick.tsx new file mode 100644 index 0000000..5275408 --- /dev/null +++ b/src/pages/edition/pick.tsx @@ -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; diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 73d7cab..28215eb 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -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") }/>; diff --git a/src/pages/user/login.tsx b/src/pages/user/login.tsx index 89fa966..46a5a00 100644 --- a/src/pages/user/login.tsx +++ b/src/pages/user/login.tsx @@ -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>; } diff --git a/src/provider/dummy/student.ts b/src/provider/dummy/student.ts index 1e2387c..38272d7 100644 --- a/src/provider/dummy/student.ts +++ b/src/provider/dummy/student.ts @@ -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, diff --git a/src/routing.tsx b/src/routing.tsx index f57249d..f9ef81f 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -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 }` : ''); +} diff --git a/src/serialization/types.ts b/src/serialization/types.ts index b7528c6..befd035 100644 --- a/src/serialization/types.ts +++ b/src/serialization/types.ts @@ -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; +} diff --git a/src/state/actions/edition.ts b/src/state/actions/edition.ts index 142220d..f25b853 100644 --- a/src/state/actions/edition.ts +++ b/src/state/actions/edition.ts @@ -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> { diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index a629b04..043b99b 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -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>>() diff --git a/src/state/actions/student.ts b/src/state/actions/student.ts new file mode 100644 index 0000000..ab9c832 --- /dev/null +++ b/src/state/actions/student.ts @@ -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; + diff --git a/src/state/actions/user.ts b/src/state/actions/user.ts index 6ee23f1..655c1cb 100644 --- a/src/state/actions/user.ts +++ b/src/state/actions/user.ts @@ -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>; diff --git a/src/state/reducer/student.ts b/src/state/reducer/student.ts index 1851bac..f00f52d 100644 --- a/src/state/reducer/student.ts +++ b/src/state/reducer/student.ts @@ -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: diff --git a/translations/pl.yaml b/translations/pl.yaml index 2efc081..2d7554f 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -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: diff --git a/webpack.config.js b/webpack.config.js index c3636bf..6585967 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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/",