feature/edition #15

Merged
kadet merged 2 commits from feature/edition into master 2020-09-08 21:39:30 +02:00
15 changed files with 222 additions and 41 deletions

View File

@ -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;
}

View File

@ -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;

27
src/api/page.tsx Normal file
View File

@ -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();
}

View File

@ -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>

View File

@ -3,3 +3,8 @@ export type Identifier = string;
export interface Identifiable {
id?: Identifier
}
export type Multilingual<T> = {
pl: T,
en: T
}

6
src/data/page.ts Normal file
View File

@ -0,0 +1,6 @@
import { Identifiable, Multilingual } from "@/data/common";
export interface Page extends Identifiable {
title: Multilingual<string>;
content: Multilingual<string>;
}

View File

@ -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));
}

View File

@ -1,2 +1,3 @@
export * from "./useProxyState"
export * from "./useUpdateEffect"
export * from "./useAsync"

50
src/hooks/useAsync.ts Normal file
View File

@ -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 ];
}

View File

@ -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>

View File

@ -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;

51
src/pages/fallback.tsx Normal file
View File

@ -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;

View File

@ -1,3 +1,3 @@
export * from "./internship/proposal";
export * from "./errors/not-found"
export * from "./main"
export * from "./fallback"

View File

@ -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>

View File

@ -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;