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 { // MOCK export async function get(key: string): Promise { + 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 = `

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Bestiarum vero nullum iudicium puto. Quare ad ea primum, si videtur; Duo Reges: constructio interrete. Eam tum adesse, cum dolor omnis absit; Sed ad bona praeterita redeamus. Facillimum id quidem est, inquam. Apud ceteros autem philosophos, qui quaesivit aliquid, tacet;

+ +

Quorum altera prosunt, nocent altera. Eam stabilem appellas. Sed nimis multa. Quo plebiscito decreta a senatu est consuli quaestio Cn. Sin laboramus, quis est, qui alienae modum statuat industriae? Quod quidem nobis non saepe contingit. Si autem id non concedatur, non continuo vita beata tollitur. Illum mallem levares, quo optimum atque humanissimum virum, Cn. Id est enim, de quo quaerimus.

+ +

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.

+` + +export async function get(slug: string): Promise { + 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() {
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 = { + 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; + content: Multilingual; +} 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 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 = { + isLoading: boolean, + value: T | undefined, + error: TError | undefined +}; + +export type AsyncState = [AsyncResult, (promise: Promise | undefined) => void] + +export function useAsync(promise: Promise | undefined): AsyncResult { + const [isLoading, setLoading] = useState(true); + const [error, setError] = useState(undefined); + const [value, setValue] = useState(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(initial: Promise | undefined): AsyncState { + const [promise, setPromise] = useState | 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(""); - const [edition, setEdition] = useState(null); + const [{ value: edition, isLoading }, setEdition] = useAsyncState(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 + ?
+ + { edition.course.name } + + { t('internship.date-range', { start: edition.startDate, end: edition.endDate }) } + +
+ : { t("forms.edition-register.edition-not-found") } + + return { t("pages.edition.header") } @@ -42,16 +54,10 @@ export const RegisterEditionPage = () => { - { edition - ?
- - { edition.course.name } - - { t('internship.date-range', { start: edition.startDate, end: edition.endDate }) } - -
- : { t("forms.edition-register.edition-not-found") } - } + + + { isLoading ? : } + 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 - - 404 - Strona nie została znaleziona - - - - - - - - -} - -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 + } + + if (error) { + return + + 404 + Strona nie została znaleziona + + + + + + + + + } + + if (value) { + return + + { value.title.pl } + + +
+ + + } + + return ; +} + +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: () => }, // fallback route for 404 pages - { name: "fallback", path: "*", content: () => } + { name: "fallback", path: "*", content: () => } ] const routeNameMap = new Map(routes.filter(({ name }) => !!name).map(({ name, path }) => [name, path instanceof Array ? path[0] : path])) as Map 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;