diff --git a/src/api/edition.ts b/src/api/edition.ts index 966a036..bce8982 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -1,6 +1,7 @@ import { axios } from "@/api/index"; import { Edition } from "@/data/edition"; import { sampleEdition } from "@/provider/dummy"; +import { delay } from "@/helpers"; const EDITIONS_ENDPOINT = "/editions"; const EDITION_INFO_ENDPOINT = "/editions/:key"; @@ -24,6 +25,8 @@ export async function join(key: string): Promise<boolean> { // MOCK export async function get(key: string): Promise<Edition | null> { + await delay(Math.random() * 200 + 100); + if (key == "inf2020") { return sampleEdition; } diff --git a/src/api/index.ts b/src/api/index.ts index 2fab26e..6d9942d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,6 +5,7 @@ import { UserState } from "@/state/reducer/user"; import * as user from "./user"; import * as edition from "./edition"; +import * as page from "./page" export const axios = Axios.create({ baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/", @@ -29,7 +30,8 @@ axios.interceptors.request.use(config => { const api = { user, - edition + edition, + page } export default api; diff --git a/src/api/page.tsx b/src/api/page.tsx new file mode 100644 index 0000000..f1c69ee --- /dev/null +++ b/src/api/page.tsx @@ -0,0 +1,27 @@ +// MOCK +import { Page } from "@/data/page"; + +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> +` + +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", + }, + } + } + + throw new Error(); +} diff --git a/src/app.tsx b/src/app.tsx index d879206..c05b38c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -91,13 +91,17 @@ function App() { </div> <div className="header__nav"> <nav className="header__top"> - <ul className="header__menu"></ul> + <ul className="header__menu"> + </ul> <UserMenu className="header__user"/> <div className="header__divider"/> <LanguageSwitcher className="header__language-switcher"/> </nav> <nav className="header__bottom"> - <ul className="header__menu header__menu--main"></ul> + <ul className="header__menu header__menu--main"> + <li><Link to={ route("home") }>{ t("pages.my-internship.header") }</Link></li> + <li><Link to="/regulamin">Regulamin</Link></li> + </ul> </nav> </div> </header> diff --git a/src/data/common.ts b/src/data/common.ts index 5a735c4..da78cf8 100644 --- a/src/data/common.ts +++ b/src/data/common.ts @@ -3,3 +3,8 @@ export type Identifier = string; export interface Identifiable { id?: Identifier } + +export type Multilingual<T> = { + pl: T, + en: T +} diff --git a/src/data/page.ts b/src/data/page.ts new file mode 100644 index 0000000..a474e82 --- /dev/null +++ b/src/data/page.ts @@ -0,0 +1,6 @@ +import { Identifiable, Multilingual } from "@/data/common"; + +export interface Page extends Identifiable { + title: Multilingual<string>; + content: Multilingual<string>; +} diff --git a/src/helpers.ts b/src/helpers.ts index b24217f..ae9da92 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -8,3 +8,7 @@ export type Index = string | symbol | number; export interface DOMEvent<TTarget extends EventTarget> extends Event { target: TTarget; } + +export function delay(time: number) { + return new Promise(resolve => setTimeout(resolve, time)); +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 9e7f847..848cfd7 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1,3 @@ export * from "./useProxyState" export * from "./useUpdateEffect" +export * from "./useAsync" diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts new file mode 100644 index 0000000..91e1de2 --- /dev/null +++ b/src/hooks/useAsync.ts @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; + +export type AsyncResult<T, TError = any> = { + isLoading: boolean, + value: T | undefined, + error: TError | undefined +}; + +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> { + 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 }) + + useEffect(() => { + setLoading(true); + setError(undefined); + setValue(undefined); + + const myMagicNumber = semaphore.value + 1; + semaphore.value = myMagicNumber; + + promise && promise.then(value => { + if (semaphore.value == myMagicNumber) { + setValue(value); + setLoading(false); + } + }).catch(error => { + if (semaphore.value == myMagicNumber) { + setError(error); + setLoading(false); + } + }) + }, [ promise ]) + + return { + isLoading, + value, + error, + }; +} + +export function useAsyncState<T, TError = any>(initial: Promise<T> | undefined): AsyncState<T, TError> { + const [promise, setPromise] = useState<Promise<T> | undefined>(initial); + const asyncState = useAsync(promise); + + return [ asyncState, setPromise ]; +} diff --git a/src/pages/edition/register.tsx b/src/pages/edition/register.tsx index a10a105..7c69834 100644 --- a/src/pages/edition/register.tsx +++ b/src/pages/edition/register.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from "react"; import { Page } from "@/pages/base"; import { useTranslation } from "react-i18next"; -import { Button, Container, TextField, Typography } from "@material-ui/core"; +import { Box, Button, CircularProgress, Container, TextField, Typography } from "@material-ui/core"; import api from "@/api"; import { useVerticalSpacing } from "@/styles"; import { Edition } from "@/data/edition"; +import { useAsyncState } from "@/hooks"; import { Label, Section } from "@/components/section"; import { Alert } from "@material-ui/lab"; @@ -13,12 +14,12 @@ export const RegisterEditionPage = () => { const { t } = useTranslation(); const [key, setKey] = useState<string>(""); - const [edition, setEdition] = useState<Edition | null>(null); + const [{ value: edition, isLoading }, setEdition] = useAsyncState<Edition | null>(undefined); const classes = useVerticalSpacing(3); useEffect(() => { - (async () => setEdition(await api.edition.get(key)))(); + setEdition(api.edition.get(key)); }, [ key ]) const handleRegister = () => { @@ -33,6 +34,17 @@ export const RegisterEditionPage = () => { setKey(ev.currentTarget.value); } + const Edition = () => edition + ? <Section> + <Label>{ t("forms.edition-register.edition" ) }</Label> + <Typography className="proposal__primary">{ edition.course.name }</Typography> + <Typography className="proposal__secondary"> + { t('internship.date-range', { start: edition.startDate, end: edition.endDate }) } + </Typography> + </Section> + : <Alert severity="warning">{ t("forms.edition-register.edition-not-found") }</Alert> + + return <Page> <Page.Header maxWidth="md"> <Page.Title>{ t("pages.edition.header") }</Page.Title> @@ -42,16 +54,10 @@ export const RegisterEditionPage = () => { <TextField label={ t("forms.edition-register.fields.key") } fullWidth onChange={ handleKeyChange } value={ key } /> - { edition - ? <Section> - <Label>{ t("forms.edition-register.edition" ) }</Label> - <Typography className="proposal__primary">{ edition.course.name }</Typography> - <Typography className="proposal__secondary"> - { t('internship.date-range', { start: edition.startDate, end: edition.endDate }) } - </Typography> - </Section> - : <Alert severity="warning">{ t("forms.edition-register.edition-not-found") }</Alert> - } + + <Box> + { isLoading ? <CircularProgress /> : <Edition /> } + </Box> <Button onClick={ handleRegister } variant="contained" color="primary" disabled={ !edition }>{ t("forms.edition-register.register") }</Button> </Container> diff --git a/src/pages/errors/not-found.tsx b/src/pages/errors/not-found.tsx deleted file mode 100644 index 0f6456b..0000000 --- a/src/pages/errors/not-found.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Page } from "@/pages/base"; -import { Box, Button, Container, Divider, Typography } from "@material-ui/core"; -import { route } from "@/routing"; -import { Link as RouterLink } from "react-router-dom"; -import React from "react"; - -export const NotFoundPage = () => { - return <Page title="Strona nie została znaleziona"> - <Container> - <Typography variant="h1">404</Typography> - <Typography variant="h2">Strona nie została znaleziona</Typography> - - <Box my={ 4 }> - <Divider variant="fullWidth"/> - </Box> - - <Button to={ route("home") } color="primary" component={ RouterLink }>strona główna</Button> - </Container> - </Page> -} - -export default NotFoundPage; diff --git a/src/pages/fallback.tsx b/src/pages/fallback.tsx new file mode 100644 index 0000000..ff1584f --- /dev/null +++ b/src/pages/fallback.tsx @@ -0,0 +1,51 @@ +import { Page } from "@/pages/base"; +import { Box, Button, CircularProgress, Container, Divider, Typography } from "@material-ui/core"; +import { Link as RouterLink, useLocation } from "react-router-dom"; +import React, { useMemo } from "react"; +import { route } from "@/routing"; +import { useAsync } from "@/hooks"; +import api from "@/api"; + +export const FallbackPage = () => { + const location = useLocation(); + + const promise = useMemo(() => api.page.get(location.pathname), [ location.pathname ]); + + const { isLoading, value, error } = useAsync(promise); + + console.log({ isLoading, value, error, location }); + + if (isLoading) { + return <CircularProgress /> + } + + if (error) { + return <Page title="Strona nie została znaleziona"> + <Container> + <Typography variant="h1">404</Typography> + <Typography variant="h2">Strona nie została znaleziona</Typography> + + <Box my={ 4 }> + <Divider variant="fullWidth"/> + </Box> + + <Button to={ route("home") } color="primary" component={ RouterLink }>strona główna</Button> + </Container> + </Page> + } + + if (value) { + return <Page title={ value.title.pl }> + <Page.Header maxWidth="md"> + <Page.Title>{ value.title.pl }</Page.Title> + </Page.Header> + <Container> + <div dangerouslySetInnerHTML={{ __html: value.content.pl }} /> + </Container> + </Page> + } + + return <Page/>; +} + +export default FallbackPage; diff --git a/src/pages/index.ts b/src/pages/index.ts index c5f71a3..b4836aa 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,3 +1,3 @@ export * from "./internship/proposal"; -export * from "./errors/not-found" export * from "./main" +export * from "./fallback" diff --git a/src/routing.tsx b/src/routing.tsx index 233b423..f57249d 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -2,7 +2,7 @@ import React, { ReactComponentElement } from "react"; import { MainPage } from "@/pages/main"; import { RouteProps } from "react-router-dom"; import { InternshipProposalFormPage, InternshipProposalPreviewPage } from "@/pages/internship/proposal"; -import { NotFoundPage } from "@/pages/errors/not-found"; +import { FallbackPage } from "@/pages/fallback"; import SubmitPlanPage from "@/pages/internship/plan"; import { UserLoginPage } from "@/pages/user/login"; import { RegisterEditionPage } from "@/pages/edition/register"; @@ -28,7 +28,7 @@ export const routes: Route[] = [ { name: "user_login", path: "/user/login", exact: true, content: () => <UserLoginPage /> }, // fallback route for 404 pages - { name: "fallback", path: "*", content: () => <NotFoundPage/> } + { name: "fallback", path: "*", content: () => <FallbackPage/> } ] const routeNameMap = new Map(routes.filter(({ name }) => !!name).map(({ name, path }) => [name, path instanceof Array ? path[0] : path])) as Map<string, string> diff --git a/src/styles/header.scss b/src/styles/header.scss index 9750554..60f2400 100644 --- a/src/styles/header.scss +++ b/src/styles/header.scss @@ -45,12 +45,56 @@ .header__nav { flex: 1 1 auto; + display: flex; + flex-direction: column; } .header__top .header__menu { margin-right: auto; } +.header__bottom { + display: flex; + flex: 1 1 auto; +} + +.header__menu--main { + display: flex; + list-style: none; + margin: 0; + font-size: 1.25rem; + padding-left: 0.75rem; + + > li { + display: flex; + + > a { + padding: 16px; + color: white; + text-decoration: none; + font-weight: bold; + display: flex; + align-items: center; + position: relative; + + &:hover { + background: $brand; + + &::before { + display: block; + bottom: 0; + left: 0; + right: 0; + height: 4px; + position: absolute; + background: white; + content: ''; + } + } + } + } +} + .header__language-switcher { padding: 0; display: flex;