Merge pull request 'feature/management' (#19) from feature/management into master
This commit is contained in:
commit
8e1fe48393
7
deploy-dev.sh
Normal file
7
deploy-dev.sh
Normal file
@ -0,0 +1,7 @@
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
npx webpack --mode production --progress || exit $?
|
||||
|
||||
rsync -azv $BASEDIR/public/* system-praktyk@kadet.net:~/dev/front
|
||||
rsync -azv $BASEDIR/build/* system-praktyk@kadet.net:~/dev/front
|
||||
|
7
deploy-stg.sh
Normal file
7
deploy-stg.sh
Normal file
@ -0,0 +1,7 @@
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
npx webpack --mode production --progress || exit $?
|
||||
|
||||
rsync -azv $BASEDIR/public/* system-praktyk@kadet.net:~/stg/front
|
||||
rsync -azv $BASEDIR/build/* system-praktyk@kadet.net:~/stg/front
|
||||
|
@ -5,11 +5,14 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-typescript": "^7.10.1",
|
||||
"@ckeditor/ckeditor5-build-classic": "^23.1.0",
|
||||
"@ckeditor/ckeditor5-react": "^3.0.0",
|
||||
"@date-io/moment": "^1.3.13",
|
||||
"@material-ui/core": "^4.10.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.55",
|
||||
"@material-ui/pickers": "^3.2.10",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.0",
|
||||
@ -38,9 +41,9 @@
|
||||
"i18next": "^19.6.0",
|
||||
"i18next-browser-languagedetector": "^5.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"material-table": "^1.69.1",
|
||||
"material-ui-dropzone": "^3.3.0",
|
||||
"mdi-material-ui": "^6.17.0",
|
||||
"moment-timezone": "^2.26.0",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"node-sass": "^4.14.1",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
|
53
public/img/pg-logo.svg
Normal file
53
public/img/pg-logo.svg
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="prefix__PGLogotyp"
|
||||
viewBox="0 0 143 64"
|
||||
version="1.1">
|
||||
<metadata
|
||||
id="metadata935">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs905">
|
||||
<style
|
||||
id="style903">.prefix__cls-1{fill:#fff}</style>
|
||||
</defs>
|
||||
<path
|
||||
id="prefix__Path_108"
|
||||
d="m 30.446,9.626 -3.339,2.357 1.375,1.866 2.652,-1.964 h 3.045 l 0.982,-7.17 -6.777,-1.277 -0.687,3.732 2.259,0.393 0.295,-1.473 2.357,0.491 -0.393,3.045 z"
|
||||
class="prefix__cls-1"
|
||||
data-name="Path 108" />
|
||||
<path
|
||||
id="prefix__Path_109"
|
||||
d="m 111.866,11.884 2.652,1.964 1.375,-1.866 -3.241,-2.357 h -1.866 l -0.393,-3.045 2.455,-0.491 0.2,1.473 2.259,-0.393 -0.593,-3.731 -6.875,1.277 0.982,7.17 z"
|
||||
class="prefix__cls-1"
|
||||
data-name="Path 109" />
|
||||
<path
|
||||
id="prefix__Path_110"
|
||||
d="m 130.036,44.884 h -2.259 v 2.259 h 7.464 l 2.159,3.045 -5.4,10.705 h -7.857 l 1.67,-3.339 h 3.241 l 3.536,-7.17 h -8.741 V 39.777 l -9.92,-7.464 2.161,-2.848 -1.866,-1.375 -3.339,4.42 v 2.554 l -11.2,2.455 -3.437,-0.982 V 34.571 H 93.991 V 49.4 L 71.4,58.045 48.813,49.4 V 32.214 h -2.259 v 1.964 l -3.634,1.08 h -10.9 v -2.847 l -3.339,-4.42 -1.866,1.375 2.161,2.848 -9.92,7.563 v 10.607 h -8.743 l 3.536,7.17 h 3.241 l 1.67,3.339 H 10.9 L 5.6,50.286 7.761,47.241 h 7.464 V 44.982 H 12.964 V 35.75 l 4.321,-3.241 V 27.795 L 21.8,24.357 20.523,22.589 27.3,17.482 35.259,21.8 v 8.45 L 44.1,31.527 45.473,27.5 h 3.536 V 8.643 h 45.179 v 21.214 h 3.536 l 1.473,4.321 8.741,-4.125 V 21.8 l 7.955,-4.321 6.777,5.107 -1.375,1.768 4.518,3.438 v 4.714 l 4.321,3.241 v 9.134 z m -86.136,-27.5 1.473,-4.42 h 1.179 v 8.054 H 44 l -6.58,4.911 v -5.3 z m -6.384,10.9 z M 46.65,25.338 H 43.9 l -1.277,3.732 -4.616,-0.589 6.875,-5.107 h 1.768 z m 58.732,3.339 -5.009,2.357 L 99.2,27.6 h -2.75 v -4.323 h 1.768 z m -7.759,-15.714 1.473,4.42 6.384,3.143 v 5.3 L 98.9,20.92 h -2.554 v -8.054 h 1.277 z m 39.977,12.573 -1.377,1.864 3.634,2.75 3.143,-4.125 v -1.864 l -6.384,-4.714 -5.009,6.482 v 3.536 l 4.223,3.143 v 4.518 l -7.857,-5.893 v -4.715 l -4.714,-3.536 1.375,-1.768 -6.679,-5.009 2.063,-1.08 5.893,4.42 -1.375,1.768 4.223,3.241 1.375,-1.866 -4.223,-3.143 1.375,-1.768 -5.107,-3.83 v -2.652 l 6.286,4.714 -1.375,1.768 3.438,2.554 1.375,-1.866 -3.437,-2.554 1.375,-1.768 -7.761,-5.795 V 5.795 l 9.036,6.777 -1.375,1.768 2.554,1.964 1.375,-1.866 -2.554,-1.964 1.375,-1.768 -10.214,-7.76 V 0 h -2.259 v 2.455 l -3.634,0.688 0.393,2.259 3.241,-0.589 v 7.857 l -6.187,3.339 -2.848,-2.161 -1.375,1.866 1.964,1.473 -3.732,2.063 -6.973,-3.437 L 99.1,10.705 H 96.25 V 6.384 h -49.6 v 4.321 H 43.8 L 42.032,15.812 35.063,19.25 31.33,17.188 33.294,15.715 31.92,13.848 29.072,16.009 22.884,12.67 V 4.813 L 26.125,5.402 26.518,3.143 22.884,2.456 V 0 h -2.259 v 2.946 l -10.214,7.854 1.375,1.768 -2.554,1.964 1.375,1.868 2.554,-1.964 -1.375,-1.766 9.036,-6.777 v 2.652 l -7.759,5.795 1.375,1.768 -3.438,2.553 1.375,1.866 3.438,-2.554 -1.375,-1.768 6.286,-4.714 v 2.652 l -5.107,3.83 1.375,1.768 -4.223,3.143 1.375,1.67 4.223,-3.241 -1.277,-1.67 5.893,-4.42 2.063,1.08 -6.679,5.009 1.375,1.768 -4.714,3.536 V 31.33 L 7.17,37.223 v -4.518 l 4.223,-3.143 V 25.83 L 6.384,19.348 0,24.063 v 1.866 L 3.143,30.054 6.777,27.304 5.5,25.536 3.634,26.911 2.357,25.143 5.893,22.491 9.036,26.616 v 1.768 l -4.223,3.143 v 6.482 l 1.866,2.455 4.027,-3.045 v 7.563 H 6.58 L 2.946,50.089 9.429,63.25 h 11.785 v -2.357 l -2.75,-5.6 h -3.241 l -1.277,-2.554 h 7.268 V 40.955 L 23.669,39.187 33,43.9 v 6.286 l -2.161,3.144 2.061,4.224 h 3.241 l 1.67,3.339 h -7.856 l -4.027,-8.152 1.964,-2.848 v -5.009 h -4.321 v 2.259 h 2.063 v 2.063 l -2.357,3.339 5.3,10.607 h 11.789 v -2.357 l -2.75,-5.6 h -3.241 l -0.884,-1.768 1.866,-2.652 V 42.33 l -9.527,-4.91 3.929,-2.946 v 2.848 H 43.411 L 46.652,36.34 V 50.679 L 71.5,60.205 96.348,50.678 V 38.795 l 3.438,0.982 13.554,-2.946 V 34.67 l 3.929,2.946 -9.527,4.911 v 8.446 l 1.866,2.652 -0.884,1.768 h -3.241 l -2.75,5.6 v 2.357 h 11.786 l 5.3,-10.607 -2.355,-3.343 v -2.061 h 2.062 V 45.08 h -4.321 v 5.009 l 1.964,2.848 -4.027,8.152 h -7.857 l 1.67,-3.339 H 110.2 L 112.263,53.527 110,50.384 V 44.1 l 9.33,-4.714 2.455,1.768 v 11.784 h 7.268 l -1.277,2.554 h -3.241 l -2.75,5.6 v 2.357 h 11.786 l 6.482,-13.161 -3.634,-5.107 h -4.125 v -7.565 l 4.027,3.045 1.866,-2.455 v -6.483 l -4.223,-3.143 v -1.768 l 3.143,-4.125 3.536,2.652 -1.277,1.67 z"
|
||||
class="prefix__cls-1"
|
||||
data-name="Path 110" />
|
||||
<path
|
||||
id="prefix__Path_111"
|
||||
d="m 63.25,20.822 a 1.2,1.2 0 0 0 -0.884,-0.295 h -2.357 v 3.045 h 2.357 a 1.85,1.85 0 0 0 0.786,-0.2 0.658,0.658 0 0 0 0.393,-0.687 V 21.607 A 0.658,0.658 0 0 0 63.25,20.821"
|
||||
class="prefix__cls-1"
|
||||
data-name="Path 111" />
|
||||
<path
|
||||
id="prefix__Path_112"
|
||||
d="m 86.331,21.215 v 2.063 h -8.349 v -2.063 l -0.393,-3.437 2.063,1.375 -0.491,1.964 h 2.455 l -0.786,-1.964 1.375,-2.259 1.375,2.259 -0.786,1.964 h 2.456 l -0.491,-1.964 2.063,-1.375 z m -0.589,10.116 -2.652,-0.295 0.393,2.652 h -2.554 l 0.393,-2.652 -2.652,0.295 v -2.554 l 2.652,0.393 -0.393,-2.652 h 2.554 l -0.393,2.554 2.652,-0.295 z m 0,10.313 -2.652,-0.393 0.393,2.652 h -2.554 l 0.393,-2.652 -2.652,0.295 v -2.555 l 2.652,0.393 -0.393,-2.554 h 2.554 l -0.393,2.554 2.652,-0.295 v 2.554 z M 72.286,44.102 H 70.813 V 18.759 h 1.473 z M 65.411,22.786 a 3.531,3.531 0 0 1 -0.2,1.08 2.648,2.648 0 0 1 -0.687,0.786 3.072,3.072 0 0 1 -0.982,0.491 4.221,4.221 0 0 1 -1.179,0.2 h -2.354 v 3.536 h -1.768 v -9.727 h 4.129 a 4.93,4.93 0 0 1 1.179,0.2 3.072,3.072 0 0 1 0.982,0.491 1.527,1.527 0 0 1 0.589,0.786 2.127,2.127 0 0 1 0.2,1.179 v 0.982 z m -0.295,18.955 a 3.981,3.981 0 0 1 -0.884,0.491 c -0.295,0.1 -0.589,0.295 -0.884,0.393 a 2.868,2.868 0 0 1 -0.884,0.2 3.028,3.028 0 0 1 -0.982,0.1 5.944,5.944 0 0 1 -1.473,-0.2 2.24,2.24 0 0 1 -1.179,-0.589 3.845,3.845 0 0 1 -0.786,-0.982 3.137,3.137 0 0 1 -0.295,-1.375 v -3.734 a 3.137,3.137 0 0 1 0.295,-1.375 3.845,3.845 0 0 1 0.786,-0.982 3.319,3.319 0 0 1 1.179,-0.589 7.581,7.581 0 0 1 1.473,-0.2 4.758,4.758 0 0 1 1.768,0.295 3.7,3.7 0 0 1 1.473,0.884 l -0.687,1.277 a 7.326,7.326 0 0 0 -1.179,-0.688 3.024,3.024 0 0 0 -1.277,-0.295 2.163,2.163 0 0 0 -0.786,0.1 4.787,4.787 0 0 0 -0.687,0.295 c -0.2,0.1 -0.295,0.295 -0.491,0.491 a 1.42,1.42 0 0 0 -0.2,0.688 v 3.732 a 1.42,1.42 0 0 0 0.2,0.688 1.184,1.184 0 0 0 0.491,0.491 4.788,4.788 0 0 0 0.688,0.295 3.585,3.585 0 0 0 1.67,0 3.489,3.489 0 0 0 0.884,-0.393 V 38.602 H 61.97 v -1.28 h 3.241 v 4.42 z M 51.17,10.902 v 36.83 l 20.33,7.857 20.33,-7.857 v -36.83 z"
|
||||
class="prefix__cls-1"
|
||||
data-name="Path 112" />
|
||||
</svg>
|
After Width: | Height: | Size: 6.9 KiB |
@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap&subset=latin,latin-ext"/>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
|
||||
<title>Zgłoszenie praktyki studenckiej</title>
|
||||
<base href="/">
|
||||
</head>
|
||||
|
@ -17,7 +17,6 @@ export const courseDtoTransformer: Transformer<CourseDTO, Course> = {
|
||||
id: subject.id,
|
||||
name: subject.name,
|
||||
desiredSemesters: [],
|
||||
possibleProgramEntries: [], // todo
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,9 @@ export const internshipTypeDtoTransformer: Transformer<InternshipTypeDTO, Intern
|
||||
description: subject.description ? {
|
||||
pl: subject.description,
|
||||
en: subject.descriptionEng || ""
|
||||
} : undefined
|
||||
} : undefined,
|
||||
requiresDeanApproval: parseInt(subject.id || "0") == 4,
|
||||
requiresInsurance: parseInt(subject.id || "0") >= 4,
|
||||
}
|
||||
},
|
||||
reverseTransform(subject: InternshipType, context?: unknown): InternshipTypeDTO {
|
||||
|
@ -13,7 +13,7 @@ import * as internship from "./internship";
|
||||
import * as upload from "./upload";
|
||||
|
||||
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://${window.location.hostname}/api/`,
|
||||
})
|
||||
|
||||
axios.interceptors.request.use(config => {
|
||||
|
@ -21,20 +21,21 @@ export class ValidationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
interface ApiError {
|
||||
key: string;
|
||||
parameters: { [name: string]: string },
|
||||
}
|
||||
|
||||
interface UpdateResponse {
|
||||
status: SubmissionState;
|
||||
errors?: string[];
|
||||
errors?: ApiError[];
|
||||
}
|
||||
|
||||
export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<SubmissionState> {
|
||||
const response = (await axios.put<UpdateResponse>(INTERNSHIP_REGISTRATION_ENDPOINT, internship)).data;
|
||||
|
||||
if (response.status == SubmissionState.Draft) {
|
||||
throw new ValidationError(
|
||||
response.errors?.map(
|
||||
msg => ({ key: msg, parameters: {} })
|
||||
) || []
|
||||
);
|
||||
throw new ValidationError(response.errors || []);
|
||||
}
|
||||
|
||||
return response.status;
|
||||
|
@ -3,7 +3,7 @@ import { PageDTO, pageDtoTransformer } from "./dto/page"
|
||||
import { axios } from "@/api/index";
|
||||
import { prepare } from "@/routing";
|
||||
|
||||
const STATIC_PAGE_ENDPOINT = "/staticPage/:slug"
|
||||
export const STATIC_PAGE_ENDPOINT = "/staticPage/:slug"
|
||||
|
||||
export async function get(slug: string): Promise<Page> {
|
||||
const response = await axios.get<PageDTO>(prepare(STATIC_PAGE_ENDPOINT, { slug }))
|
||||
|
@ -2,7 +2,7 @@ import { InternshipType } from "@/data";
|
||||
import { axios } from "@/api/index";
|
||||
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||
|
||||
const AVAILABLE_INTERNSHIP_TYPES = '/internshipTypes';
|
||||
const AVAILABLE_INTERNSHIP_TYPES = '/internshipTypes/current';
|
||||
|
||||
export async function available(): Promise<InternshipType[]> {
|
||||
const response = await axios.get<InternshipTypeDTO[]>(AVAILABLE_INTERNSHIP_TYPES);
|
||||
|
@ -2,6 +2,7 @@ import { axios } from "@/api/index";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
import { prepare } from "@/routing";
|
||||
import { Identifiable } from "@/data";
|
||||
import store from "@/state/store";
|
||||
|
||||
export enum UploadType {
|
||||
Ipp = "IppScan",
|
||||
@ -17,6 +18,7 @@ export interface DocumentFileInfo extends Identifiable {
|
||||
|
||||
const CREATE_DOCUMENT_ENDPOINT = '/document';
|
||||
const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan';
|
||||
const DOCUMENT_DOWNLOAD_ENDPOINT = 'document/:id/scan/download';
|
||||
|
||||
export async function create(type: UploadType) {
|
||||
const response = await axios.post<InternshipDocument>(CREATE_DOCUMENT_ENDPOINT, { type });
|
||||
@ -35,3 +37,7 @@ export async function fileinfo(document: InternshipDocument): Promise<DocumentFi
|
||||
const response = await axios.get<DocumentFileInfo>(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }));
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function link(document: InternshipDocument): string {
|
||||
return axios.defaults.baseURL + prepare(DOCUMENT_DOWNLOAD_ENDPOINT, { id: document.id as string }) + "?auth=" + store.getState().user.token;
|
||||
}
|
||||
|
16
src/app.tsx
16
src/app.tsx
@ -15,6 +15,7 @@ import { getLocale, Locale } from "@/state/reducer/settings";
|
||||
import i18n from "@/i18n";
|
||||
import moment from "moment-timezone";
|
||||
import { Container } from "@material-ui/core";
|
||||
import { useCurrentUser } from "@/hooks";
|
||||
|
||||
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
||||
const student = useSelector<AppState, Student>(state => state.student as Student);
|
||||
@ -62,6 +63,7 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>)
|
||||
function App() {
|
||||
const { t } = useTranslation();
|
||||
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
|
||||
const user = useCurrentUser();
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(locale);
|
||||
@ -96,13 +98,19 @@ function App() {
|
||||
</header>
|
||||
<main id="content">
|
||||
{ <Switch>
|
||||
{ routes.map(({ name, content, middlewares = [], ...route }) => <Route { ...route } key={ name }>
|
||||
{ processMiddlewares([ ...middlewares, content ]) }
|
||||
</Route>) }
|
||||
{ routes.map(({ name, content, middlewares = [], ...route }) =>
|
||||
<Route { ...route } key={ name } render={ () => {
|
||||
const Next = () => processMiddlewares([ ...middlewares, content ])
|
||||
return <Next />
|
||||
} } />
|
||||
) }
|
||||
</Switch> }
|
||||
</main>
|
||||
<footer className="footer">
|
||||
<Container>
|
||||
<Container style={{ display: 'flex', alignItems: "center" }}>
|
||||
<ul className="footer__menu">
|
||||
{ user?.isManager && <li><Link to="/management">{ t("management") }</Link></li> }
|
||||
</ul>
|
||||
<div className="footer__copyright">{ t('copyright', { date: moment() }) }</div>
|
||||
</Container>
|
||||
</footer>
|
||||
|
@ -1,8 +1,12 @@
|
||||
import React, { HTMLProps } from "react";
|
||||
import { useHorizontalSpacing } from "@/styles";
|
||||
|
||||
export const Actions = (props: HTMLProps<HTMLDivElement>) => {
|
||||
const classes = useHorizontalSpacing(2);
|
||||
export type ActionsProps = {
|
||||
spacing?: number;
|
||||
} & HTMLProps<HTMLDivElement>;
|
||||
|
||||
return <div className={ classes.root } { ...props } style={{ display: "flex", alignItems: "center" }}/>
|
||||
export const Actions = ({ spacing = 2, ...props }: ActionsProps) => {
|
||||
const classes = useHorizontalSpacing(spacing);
|
||||
|
||||
return <div className={ classes.root } { ...props } style={{ display: "flex", alignItems: "center", ...props.style }}/>
|
||||
}
|
||||
|
@ -9,21 +9,22 @@ type AsyncProps<TValue, TError = any> = {
|
||||
children: (value: TValue) => JSX.Element,
|
||||
loading?: () => JSX.Element,
|
||||
error?: (error: TError) => JSX.Element,
|
||||
keepValue?: boolean;
|
||||
}
|
||||
|
||||
const defaultLoading = () => <Loading />;
|
||||
const defaultError = (error: any) => <Alert severity="error">{ error.message }</Alert>;
|
||||
|
||||
export function Async<TValue, TError = any>(
|
||||
{ async, children: render, loading = defaultLoading, error = defaultError }: AsyncProps<TValue, TError>
|
||||
{ async, children: render, loading = defaultLoading, error = defaultError, keepValue = false }: AsyncProps<TValue, TError>
|
||||
) {
|
||||
if (async.isLoading || (!async.error && !async.value)) {
|
||||
return loading();
|
||||
if (async.value && (!async.isLoading || keepValue)) {
|
||||
return render(async.value as TValue);
|
||||
}
|
||||
|
||||
if (typeof async.error !== "undefined") {
|
||||
return error(async.error);
|
||||
}
|
||||
|
||||
return render(async.value as TValue);
|
||||
return loading();
|
||||
}
|
||||
|
46
src/components/confirm.tsx
Normal file
46
src/components/confirm.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button, ButtonProps, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type ConfirmProps = {
|
||||
children: (action: () => void) => React.ReactNode,
|
||||
title?: string,
|
||||
content?: React.ReactNode,
|
||||
onConfirm?: () => void,
|
||||
onCancel?: () => void,
|
||||
confirm?: (props: Pick<ButtonProps, 'onClick'>) => React.ReactNode,
|
||||
}
|
||||
|
||||
export function Confirm({ children, title, content, confirm, onConfirm, onCancel }: ConfirmProps) {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
setOpen(false);
|
||||
onConfirm?.();
|
||||
}
|
||||
|
||||
return <>
|
||||
{ children(() => { setOpen(true) }) }
|
||||
{ createPortal(
|
||||
<Dialog open={ open } onClose={ handleCancel }>
|
||||
{ title && <DialogTitle>{ title }</DialogTitle>}
|
||||
<DialogContent>
|
||||
<DialogContentText>{ content || t('confirmation') }</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{ confirm ? confirm({ onClick: handleConfirm }) : <Button color="primary" variant="contained" autoFocus onClick={ handleConfirm }>{ t('confirm') }</Button> }
|
||||
<Button onClick={ handleCancel }>{ t('cancel') }</Button>
|
||||
</DialogActions>
|
||||
</Dialog>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>
|
||||
}
|
74
src/components/contact.tsx
Normal file
74
src/components/contact.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { Field, Form, Formik } from "formik";
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle, Typography } from "@material-ui/core";
|
||||
import { CKEditorField } from "@/field/ckeditor";
|
||||
import { Actions } from "@/components/actions";
|
||||
import { Cancel, Send } from "mdi-material-ui";
|
||||
import { createPortal } from "react-dom";
|
||||
import { capitalize } from "@/helpers";
|
||||
|
||||
export type ContactFormValues = {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const initialContactFormValues: ContactFormValues = {
|
||||
content: "",
|
||||
}
|
||||
|
||||
export type ContactDialogProps = {
|
||||
onSend: (values: ContactFormValues) => void;
|
||||
} & DialogProps;
|
||||
|
||||
export function ContactForm() {
|
||||
const { t } = useTranslation();
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
return <div className={ spacing.vertical } style={{ overflow: 'hidden' }}>
|
||||
<Field label={ t("forms.contact.field.content") } name="content" component={ CKEditorField }/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function ContactDialog({ onSend, ...props }: ContactDialogProps) {
|
||||
const spacing = useSpacing(2);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Dialog { ...props } maxWidth="lg">
|
||||
<Formik initialValues={ initialContactFormValues } onSubmit={ onSend }>
|
||||
<Form className={ spacing.vertical }>
|
||||
<DialogTitle>{ capitalize(t("forms.contact.title")) }</DialogTitle>
|
||||
<DialogContent>
|
||||
<ContactForm />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Actions>
|
||||
<Button variant="contained" color="primary" startIcon={ <Send /> } type="submit">{ t("send") }</Button>
|
||||
<Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button>
|
||||
</Actions>
|
||||
</DialogActions>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Dialog>
|
||||
}
|
||||
|
||||
export type ContactActionProps = {
|
||||
children: (props: { action: () => void }) => React.ReactNode
|
||||
};
|
||||
|
||||
export function ContactAction({ children }: ContactActionProps) {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleClose = () => { setOpen(false) };
|
||||
const handleSubmit = (values: ContactFormValues) => {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
{ children({ action: () => setOpen(true) }) }
|
||||
{ createPortal(
|
||||
<ContactDialog open={ open } onSend={ handleSubmit } onClose={ handleClose }/>,
|
||||
document.getElementById("modals") as HTMLElement
|
||||
) }
|
||||
</>
|
||||
}
|
@ -84,7 +84,7 @@ export const FileInfo = ({ document, ...props }: FileInfoProps) => {
|
||||
</Typography>
|
||||
|
||||
<Actions className={ classes.actions }>
|
||||
<Button className={ classes.download } startIcon={ <FileDownloadOutline /> }>{ t("download") }</Button>
|
||||
<Button className={ classes.download } startIcon={ <FileDownloadOutline /> } href={ api.upload.link(document) }>{ t("download") }</Button>
|
||||
</Actions>
|
||||
</aside>
|
||||
</div> }
|
||||
|
@ -6,6 +6,8 @@ import { Company, Office } from "@/data/company";
|
||||
export interface InternshipType extends Identifiable {
|
||||
label: Multilingual<string>,
|
||||
description?: Multilingual<string>,
|
||||
requiresDeanApproval: boolean,
|
||||
requiresInsurance: boolean,
|
||||
}
|
||||
|
||||
export interface InternshipProgramEntry extends Identifiable {
|
||||
|
@ -5,3 +5,5 @@ export interface Page extends Identifiable {
|
||||
content: Multilingual<string>;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
23
src/field/ckeditor.tsx
Normal file
23
src/field/ckeditor.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { FieldProps } from "formik";
|
||||
|
||||
// @ts-ignore
|
||||
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||
// @ts-ignore
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||
import { FormControl, FormControlLabel, FormControlProps, FormLabel, TextFieldProps } from "@material-ui/core";
|
||||
import React from "react";
|
||||
|
||||
export type CKEditorFieldProps = FieldProps & FormControlProps & { label?: string };
|
||||
|
||||
export function CKEditorField({ field, form, error, label, ...props }: CKEditorFieldProps) {
|
||||
const handleChange = (_: unknown, editor: any) => {
|
||||
const data = editor.getData();
|
||||
form.setFieldValue(field.name, data);
|
||||
form.setFieldTouched(field.name);
|
||||
}
|
||||
|
||||
return <FormControl { ...props }>
|
||||
<FormLabel style={{ marginBottom: "0.5rem" }}>{ label }</FormLabel>
|
||||
<CKEditor data={ field.value } editor={ ClassicEditor } onChange={ handleChange }/>
|
||||
</FormControl>
|
||||
}
|
@ -112,7 +112,7 @@ const InternshipProgramForm = () => {
|
||||
if (ev.target.checked) {
|
||||
setSelectedProgramEntries([ ...selectedProgramEntries, entry ]);
|
||||
} else {
|
||||
setSelectedProgramEntries(selectedProgramEntries.filter(cur => cur != entry));
|
||||
setSelectedProgramEntries(selectedProgramEntries.filter(cur => cur.id != entry.id));
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,6 +133,7 @@ const InternshipProgramForm = () => {
|
||||
onBlur={ handleBlur }
|
||||
/>
|
||||
</Grid>
|
||||
{ values.kind?.requiresDeanApproval && <Grid item xs={ 12 }><Alert severity="warning">{ t("internship.kind-requires-dean-approval") }</Alert></Grid> }
|
||||
{/*<Grid item md={ 8 }>*/}
|
||||
{/* {*/}
|
||||
{/* values.kind === InternshipType.Other &&*/}
|
||||
@ -159,6 +160,7 @@ const InternshipProgramForm = () => {
|
||||
|
||||
const InternshipDurationForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const edition = useCurrentEdition();
|
||||
const {
|
||||
values: { startDate, endDate, workingHours },
|
||||
errors,
|
||||
@ -174,6 +176,8 @@ const InternshipDurationForm = () => {
|
||||
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
|
||||
const weeks = useMemo(() => hours !== null ? Math.floor(hours / workingHours) : null, [ hours ]);
|
||||
|
||||
const requiresDeanApproval = useMemo(() => edition?.startDate?.isAfter(startDate) || edition?.endDate?.isBefore(endDate), [ startDate, endDate ])
|
||||
|
||||
useUpdateEffect(() => {
|
||||
setFieldTouched("hours", true);
|
||||
setFieldValue("hours", hours, true);
|
||||
@ -200,6 +204,9 @@ const InternshipDurationForm = () => {
|
||||
minDate={ startDate }
|
||||
/>
|
||||
</Grid>
|
||||
{ requiresDeanApproval && <Grid item xs={ 12 }>
|
||||
<Alert severity="warning">{ t("internship.duration-requires-dean-approval") }</Alert>
|
||||
</Grid> }
|
||||
<Grid item md={ 4 }>
|
||||
<Field component={ TextFieldFormik }
|
||||
name="workingHours"
|
||||
@ -387,7 +394,7 @@ export const InternshipForm: React.FunctionComponent = () => {
|
||||
{ errors.length > 0 && <Alert severity="warning">
|
||||
<AlertTitle>{ t('internship.validation.has-errors') }</AlertTitle>
|
||||
<ul style={{ paddingLeft: 0 }}>
|
||||
{ errors.map(message => <li key={ message.key }>{ t(`internship.validation.${message.key}`, message.parameters) }</li>) }
|
||||
{ errors.map(message => <li key={ message.key }>{ t(`validation.api.${message.key}`, message.parameters) }</li>) }
|
||||
</ul>
|
||||
</Alert> }
|
||||
<Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography>
|
||||
|
@ -32,9 +32,10 @@ export const PlanForm = () => {
|
||||
|
||||
if (!destination) {
|
||||
destination = await api.upload.create(UploadType.Ipp);
|
||||
dispatch({ type: InternshipPlanActions.Send, document: destination });
|
||||
}
|
||||
|
||||
dispatch({ type: InternshipPlanActions.Send, document: destination });
|
||||
|
||||
await api.upload.upload(destination, file);
|
||||
|
||||
history.push("/");
|
||||
|
@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useFormikContext } from "formik";
|
||||
import { InternshipFormValues } from "@/forms/internship";
|
||||
import { useCurrentEdition } from "@/hooks";
|
||||
import { ContactAction } from "@/components/contact";
|
||||
|
||||
export const StudentForm = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -36,8 +37,10 @@ export const StudentForm = () => {
|
||||
<TextField label={ t("forms.internship.fields.semester") } value={ student.semester || "" } disabled fullWidth/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }>
|
||||
Powyższe dane nie są poprawne?
|
||||
<Alert severity="warning" action={ <ContactAction>{
|
||||
({ action }) => <Button color="inherit" size="small" onClick={ action }>{ t("contact") }</Button>
|
||||
}</ContactAction> }>
|
||||
{ t("incorrect-data-question") }
|
||||
</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -2,6 +2,7 @@ export type Nullable<T> = { [P in keyof T]: T[P] | null }
|
||||
|
||||
export type Subset<T> = { [K in keyof T]?: Subset<T[K]> }
|
||||
export type Dictionary<T> = { [key: string]: T };
|
||||
export type OneOrMany<T> = T | T[];
|
||||
|
||||
export type Index = string | symbol | number;
|
||||
|
||||
@ -26,3 +27,23 @@ export function throttle<TArgs extends any[]>(decorated: (...args: TArgs) => voi
|
||||
}, time);
|
||||
}
|
||||
}
|
||||
|
||||
export function encapsulate<T>(value: OneOrMany<T>): T[] {
|
||||
if (value instanceof Array) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return [ value ];
|
||||
}
|
||||
|
||||
export function one<T>(value: OneOrMany<T>): T {
|
||||
if (value instanceof Array) {
|
||||
return value[0];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function capitalize(value: string): string {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { AppState } from "@/state/reducer";
|
||||
import { Edition, getEditionDeadlines } from "@/data/edition";
|
||||
import { editionSerializationTransformer } from "@/serialization";
|
||||
import { Student } from "@/data";
|
||||
import { UserState } from "@/state/reducer/user";
|
||||
|
||||
export const useCurrentStudent = () => useSelector<AppState, Student | null>(
|
||||
state => state.student
|
||||
@ -16,3 +17,7 @@ export const useDeadlines = () => {
|
||||
const edition = useCurrentEdition() as Edition;
|
||||
return getEditionDeadlines(edition);
|
||||
}
|
||||
|
||||
export const useCurrentUser = () => useSelector<AppState, UserState>(
|
||||
state => state.user
|
||||
)
|
||||
|
@ -19,7 +19,6 @@ export function useAsync<T, TError = any>(supplier: Promise<T> | (() => Promise<
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
setValue(undefined);
|
||||
|
||||
const myMagicNumber = semaphore.value + 1;
|
||||
semaphore.value = myMagicNumber;
|
||||
@ -54,9 +53,9 @@ export function useAsync<T, TError = any>(supplier: Promise<T> | (() => Promise<
|
||||
};
|
||||
}
|
||||
|
||||
export function useAsyncState<T, TError = any>(initial: Promise<T> | undefined): AsyncState<T, TError> {
|
||||
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);
|
||||
const asyncState = useAsync<T, TError>(promise);
|
||||
|
||||
return [ asyncState, setPromise ];
|
||||
}
|
||||
|
@ -10,9 +10,11 @@ import { convertToRoman } from "@/utils/numbers";
|
||||
const resources = {
|
||||
en: {
|
||||
translation: require('../translations/en.yaml'),
|
||||
management: require('../translations/management.en.yaml'),
|
||||
},
|
||||
pl: {
|
||||
translation: require('../translations/pl.yaml'),
|
||||
management: require('../translations/management.pl.yaml'),
|
||||
}
|
||||
}
|
||||
|
||||
|
17
src/management/api/edition.ts
Normal file
17
src/management/api/edition.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { axios } from "@/api";
|
||||
import { EditionDTO, editionDtoTransformer } from "@/api/dto/edition";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { prepare } from "@/routing";
|
||||
|
||||
const MANAGEMENT_EDITION_INDEX_ENDPOINT = '/management/editions';
|
||||
const MANAGEMENT_EDITION_ENDPOINT = '/management/editions/:edition';
|
||||
|
||||
export async function all(): Promise<Edition[]> {
|
||||
const response = await axios.get<EditionDTO[]>(MANAGEMENT_EDITION_INDEX_ENDPOINT);
|
||||
return response.data.map(dto => editionDtoTransformer.transform(dto));
|
||||
}
|
||||
|
||||
export async function details(edition: string): Promise<Edition> {
|
||||
const response = await axios.get<EditionDTO>(prepare(MANAGEMENT_EDITION_ENDPOINT, { edition }));
|
||||
return editionDtoTransformer.transform(response.data);
|
||||
}
|
11
src/management/api/index.ts
Normal file
11
src/management/api/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as edition from "./edition"
|
||||
import * as page from "./page"
|
||||
import * as type from "./type"
|
||||
|
||||
export const api = {
|
||||
edition,
|
||||
page,
|
||||
type
|
||||
}
|
||||
|
||||
export default api;
|
30
src/management/api/page.ts
Normal file
30
src/management/api/page.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Page } from "@/data/page";
|
||||
import pageDtoTransformer, { PageDTO } from "@/api/dto/page";
|
||||
import { axios } from "@/api";
|
||||
import { STATIC_PAGE_ENDPOINT } from "@/api/page";
|
||||
import { prepare } from "@/routing";
|
||||
import { encapsulate, OneOrMany } from "@/helpers";
|
||||
|
||||
const STATIC_PAGE_INDEX_ENDPOINT = "/staticPage";
|
||||
|
||||
export { get, STATIC_PAGE_ENDPOINT } from "@/api/page"
|
||||
|
||||
export async function all(): Promise<Page[]> {
|
||||
const response = await axios.get<PageDTO[]>(STATIC_PAGE_INDEX_ENDPOINT);
|
||||
return response.data.map(dto => pageDtoTransformer.transform(dto));
|
||||
}
|
||||
|
||||
export async function remove(page: OneOrMany<Pick<Page, "slug">>): Promise<void> {
|
||||
const pages = encapsulate(page);
|
||||
|
||||
await Promise.all(pages.map(page => axios.delete(prepare(STATIC_PAGE_ENDPOINT, { slug: page.slug }))));
|
||||
}
|
||||
|
||||
export async function save(page: Page): Promise<Page> {
|
||||
const response = await axios.put<PageDTO>(
|
||||
STATIC_PAGE_INDEX_ENDPOINT,
|
||||
pageDtoTransformer.reverseTransform(page),
|
||||
);
|
||||
|
||||
return pageDtoTransformer.transform(response.data);
|
||||
}
|
28
src/management/api/type.ts
Normal file
28
src/management/api/type.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { InternshipType } from "@/data";
|
||||
import { axios } from "@/api";
|
||||
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||
import { encapsulate, OneOrMany } from "@/helpers";
|
||||
import { prepare } from "@/routing";
|
||||
|
||||
const INTERNSHIP_TYPE_INDEX_ENDPOINT = '/internshipTypes'
|
||||
const INTERNSHIP_TYPE_ENDPOINT = INTERNSHIP_TYPE_INDEX_ENDPOINT + "/:id";
|
||||
|
||||
export async function all(): Promise<InternshipType[]> {
|
||||
const response = await axios.get<InternshipTypeDTO[]>(INTERNSHIP_TYPE_INDEX_ENDPOINT);
|
||||
return response.data.map(dto => internshipTypeDtoTransformer.transform(dto))
|
||||
}
|
||||
|
||||
export async function remove(type: OneOrMany<InternshipType>): Promise<void> {
|
||||
await Promise.all(encapsulate(type).map(
|
||||
type => axios.delete(prepare(INTERNSHIP_TYPE_ENDPOINT, { id: type.id as string }))
|
||||
));
|
||||
}
|
||||
|
||||
export async function save(type: InternshipType): Promise<InternshipType> {
|
||||
await axios.put<InternshipType>(
|
||||
INTERNSHIP_TYPE_INDEX_ENDPOINT,
|
||||
internshipTypeDtoTransformer.reverseTransform(type)
|
||||
);
|
||||
|
||||
return type;
|
||||
}
|
15
src/management/common/BulkActions.tsx
Normal file
15
src/management/common/BulkActions.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Actions, ActionsProps } from "@/components";
|
||||
import React from "react";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type BulkActionsProps = ActionsProps;
|
||||
|
||||
export const BulkActions = ({ children, ...props }: BulkActionsProps) => {
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
return <Actions { ...props }>
|
||||
<Typography variant="subtitle2">{ t("actions.bulk") }: </Typography>
|
||||
{ children }
|
||||
</Actions>;
|
||||
};
|
56
src/management/common/DeleteResourceAction.tsx
Normal file
56
src/management/common/DeleteResourceAction.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { OneOrMany } from "@/helpers";
|
||||
import useTheme from "@material-ui/core/styles/useTheme";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Confirm } from "@/components/confirm";
|
||||
import { Button, IconButton, Tooltip } from "@material-ui/core";
|
||||
import { Delete } from "mdi-material-ui";
|
||||
import { createBoundComponent } from "@/management/common/helpers";
|
||||
|
||||
export type DeleteResourceActionProps<T> = {
|
||||
onDelete: (resource: OneOrMany<T>) => void;
|
||||
resource: OneOrMany<T>;
|
||||
label: (resource: T) => string;
|
||||
children?: (action: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function DeleteResourceAction<T>({ onDelete, resource, children, label }: DeleteResourceActionProps<T>) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
const confirmation = <>
|
||||
{ !Array.isArray(resource)
|
||||
? <Trans i18nKey="confirm.delete">
|
||||
Czy na pewno chcesz usunąć <strong>{ label(resource) }</strong>?
|
||||
</Trans>
|
||||
: <>
|
||||
{ t("confirm.bulk-delete") }
|
||||
<ul>
|
||||
{ resource.map(current => <li key={ label(current) }>{ label(current) }</li>) }
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
</>;
|
||||
|
||||
return <Confirm
|
||||
onConfirm={ () => onDelete(resource) }
|
||||
content={ confirmation }
|
||||
confirm={ props =>
|
||||
<Button variant="contained" startIcon={ <Delete /> }
|
||||
style={{
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
color: theme.palette.error.contrastText,
|
||||
}}
|
||||
{ ...props }>
|
||||
{ t("actions.delete") }
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{ action => children ? children(action) : <Tooltip title={ t("actions.delete") as string }><IconButton onClick={ action }><Delete /></IconButton></Tooltip> }
|
||||
</Confirm>;
|
||||
}
|
||||
|
||||
export function createDeleteAction<T>(props: Pick<DeleteResourceActionProps<T>, 'label' | 'onDelete'>) {
|
||||
return createBoundComponent(DeleteResourceAction, props);
|
||||
}
|
||||
|
28
src/management/common/LabelWithIcon.tsx
Normal file
28
src/management/common/LabelWithIcon.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
},
|
||||
icon: {
|
||||
marginRight: theme.spacing(1),
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
}
|
||||
}))
|
||||
|
||||
export type LabelWithIconProps = {
|
||||
icon: React.ReactNode,
|
||||
children: React.ReactChildren,
|
||||
}
|
||||
|
||||
export function LabelWithIcon({ icon, children }: LabelWithIconProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return <div className={ classes.root }>
|
||||
<div className={ classes.icon }>{ icon }</div>
|
||||
{ children }
|
||||
</div>
|
||||
}
|
11
src/management/common/MaterialTableTitle.tsx
Normal file
11
src/management/common/MaterialTableTitle.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import { AsyncResult } from "@/hooks";
|
||||
import { CircularProgress } from "@material-ui/core";
|
||||
|
||||
export type MaterialTableTitleProps = { result: AsyncResult<any>, label: React.ReactNode } & React.HTMLProps<HTMLDivElement>;
|
||||
|
||||
export const MaterialTableTitle = ({ label, result, style, ...props }: MaterialTableTitleProps) =>
|
||||
<div style={ { display: "flex", alignItems: "center", ...style } } { ...props }>
|
||||
{ label }
|
||||
{ result.isLoading && <CircularProgress size="1.5rem" style={ { marginLeft: "1rem" } }/> }
|
||||
</div>
|
15
src/management/common/MultilangualCell.tsx
Normal file
15
src/management/common/MultilangualCell.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Multilingual } from "@/data";
|
||||
import React from "react";
|
||||
import { Chip } from "@material-ui/core";
|
||||
|
||||
export type MultilingualCellProps = { value: Multilingual<React.ReactNode> }
|
||||
|
||||
export const MultilingualCell = ({ value }: MultilingualCellProps) => {
|
||||
return <>
|
||||
{ Object.keys(value).map(language => <div>
|
||||
<Chip size="small" label={ language.toUpperCase() } style={ { marginRight: "0.5rem" } }/>
|
||||
{ value[language as keyof Multilingual<any>] }
|
||||
</div>) }
|
||||
</>
|
||||
}
|
||||
|
35
src/management/common/helpers.tsx
Normal file
35
src/management/common/helpers.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { Column } from "material-table";
|
||||
import { Actions } from "@/components";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Multilingual } from "@/data";
|
||||
|
||||
export function actionsColumn<T extends Object>(render: (value: T) => React.ReactNode): Column<T> {
|
||||
return {
|
||||
title: <Trans i18nKey="management:actions.label" />,
|
||||
render: value => <Actions style={{ margin: "-1rem" }} spacing={ 0 }>{ render(value) }</Actions>,
|
||||
sorting: false,
|
||||
width: 0,
|
||||
resizable: false,
|
||||
removable: false,
|
||||
searchable: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function createBoundComponent<T, TBoundProps extends keyof T>(Component: React.ComponentType<T>, bound: Pick<T, TBoundProps>) {
|
||||
return (props: Omit<T, TBoundProps>) => <Component { ...bound as any } { ...props } />;
|
||||
}
|
||||
|
||||
export type Comparator<T> = (a: T, b: T) => number;
|
||||
export type MultilingualComparator<T> = Comparator<Multilingual<T>>;
|
||||
|
||||
export function createMultilingualComparator<T>(comparator: Comparator<T>): MultilingualComparator<T> {
|
||||
return (a, b) => comparator(a.pl, b.pl);
|
||||
}
|
||||
|
||||
export const multilingualStringComparator = createMultilingualComparator<string>((a, b) => a && b ? a.localeCompare(b) : 0)
|
||||
export const multilingualNumberComparator = createMultilingualComparator<number>((a, b) => a - b)
|
||||
|
||||
export function fieldComparator<T, K extends keyof T>(field: K, comparator: Comparator<T[K]>): Comparator<T> {
|
||||
return (a, b) => comparator(a[field], b[field])
|
||||
}
|
81
src/management/edition/list.tsx
Normal file
81
src/management/edition/list.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Page } from "@/pages/base";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsync, useAsyncState } from "@/hooks";
|
||||
import api from "@/management/api";
|
||||
import { Async } from "@/components/async";
|
||||
import { Container, Typography } from "@material-ui/core";
|
||||
import MaterialTable, { Action, Column } from "material-table";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { Pencil } from "mdi-material-ui";
|
||||
import { Management } from "../main";
|
||||
import { createPortal } from "react-dom";
|
||||
import { EditStaticPageDialog } from "@/management/page/create";
|
||||
|
||||
export type EditionDetailsProps = {
|
||||
edition: string;
|
||||
}
|
||||
|
||||
export function EditionDetails({ edition, ...props }: EditionDetailsProps) {
|
||||
const result = useAsync(useCallback(() => api.edition.details(edition), [ edition ]));
|
||||
|
||||
return <Async async={ result }>{ edition => <pre>{ JSON.stringify(edition, null, 2) }</pre> }</Async>
|
||||
}
|
||||
|
||||
export function EditionsManagement() {
|
||||
const { t } = useTranslation("management");
|
||||
const editions = useAsync(useCallback(api.edition.all, []));
|
||||
|
||||
const columns: Column<Edition>[] = [
|
||||
{
|
||||
title: t("edition.field.id"),
|
||||
field: "id",
|
||||
cellStyle: { whiteSpace: "nowrap" }
|
||||
},
|
||||
{
|
||||
title: t("edition.field.start"),
|
||||
render: edition => edition.startDate.format("DD.MM.yyyy"),
|
||||
customSort: (a, b) => b.startDate.unix() - a.startDate.unix(),
|
||||
},
|
||||
{
|
||||
title: t("edition.field.end"),
|
||||
render: edition => edition.endDate.format("DD.MM.yyyy"),
|
||||
customSort: (a, b) => b.endDate.unix() - a.endDate.unix(),
|
||||
},
|
||||
{
|
||||
title: t("edition.field.course"),
|
||||
customSort: (a, b) => a.course.name.localeCompare(b.course.name),
|
||||
render: edition => edition.course.name,
|
||||
},
|
||||
]
|
||||
|
||||
const actions: Action<Edition>[] = [
|
||||
{
|
||||
icon: () => <Pencil />,
|
||||
onClick: () => {},
|
||||
}
|
||||
]
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="lg">
|
||||
<Management.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t("edition.index.title") }</Typography>
|
||||
</Management.Breadcrumbs>
|
||||
<Page.Title>{ t("edition.index.title") }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="lg">
|
||||
<Async async={ editions }>
|
||||
{ editions =>
|
||||
<MaterialTable
|
||||
columns={ columns }
|
||||
data={ editions }
|
||||
actions={ actions }
|
||||
detailPanel={ edition => <EditionDetails edition={ edition.id as string } /> }
|
||||
title={ t("edition.index.title") }
|
||||
options={{ search: false, actionsColumnIndex: -1 }}
|
||||
/>
|
||||
}
|
||||
</Async>
|
||||
</Container>
|
||||
</Page>;
|
||||
}
|
54
src/management/main.tsx
Normal file
54
src/management/main.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { BreadcrumbsProps, Container, Link, List, ListItem, ListItemIcon, ListItemText, Paper } from "@material-ui/core";
|
||||
import { Page } from "@/pages/base";
|
||||
import React from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { route } from "@/routing";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CalendarClock, FileCertificateOutline, FileDocumentMultipleOutline } from "mdi-material-ui";
|
||||
|
||||
export const Management = {
|
||||
Breadcrumbs: ({ children, ...props }: BreadcrumbsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Page.Breadcrumbs { ...props }>
|
||||
<Link component={ RouterLink } to={ route("management:index") }>{ t("management:title") }</Link>
|
||||
{ children }
|
||||
</Page.Breadcrumbs>;
|
||||
}
|
||||
}
|
||||
|
||||
type ManagementLinkProps = React.PropsWithChildren<{
|
||||
icon: JSX.Element,
|
||||
route: string,
|
||||
}>;
|
||||
|
||||
const ManagementLink = ({ icon, route, children }: ManagementLinkProps) =>
|
||||
<ListItem button component={ RouterLink } to={ route }>
|
||||
<ListItemIcon>{ icon }</ListItemIcon>
|
||||
<ListItemText>{ children }</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
export const ManagementIndex = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Page>
|
||||
<Page.Header>
|
||||
<Page.Title>{ t("management:title") }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container>
|
||||
<Paper elevation={ 2 }>
|
||||
<List>
|
||||
<ManagementLink icon={ <CalendarClock /> } route={ route("management:editions") }>
|
||||
{ t("management:edition.index.title") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <FileCertificateOutline /> } route={ route("management:types") }>
|
||||
{ t("management:type.index.title") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <FileDocumentMultipleOutline /> } route={ route("management:static_pages") }>
|
||||
{ t("management:page.index.title") }
|
||||
</ManagementLink>
|
||||
</List>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
14
src/management/middleware.tsx
Normal file
14
src/management/middleware.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { isLoggedInMiddleware } from "@/middleware";
|
||||
import { useCurrentUser } from "@/hooks";
|
||||
import React from "react";
|
||||
import { Middleware } from "@/routing";
|
||||
|
||||
export const isManagerMiddleware: Middleware<any, any> = Next => isLoggedInMiddleware(() => {
|
||||
const user = useCurrentUser();
|
||||
|
||||
if (user.isManager) {
|
||||
return <Next />;
|
||||
}
|
||||
|
||||
return <div />;
|
||||
})
|
46
src/management/page/edit.tsx
Normal file
46
src/management/page/edit.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { Form, Formik } from "formik";
|
||||
import { initialStaticPageFormValues, StaticPageForm, StaticPageFormValues, staticPageFormValuesTransformer } from "@/management/page/form";
|
||||
import { Actions } from "@/components";
|
||||
import { Save } from "@material-ui/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Cancel } from "mdi-material-ui";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { default as StaticPage } from "@/data/page";
|
||||
|
||||
export type EditStaticPageDialogProps = {
|
||||
onSave?: (page: StaticPage) => void;
|
||||
page?: StaticPage;
|
||||
} & DialogProps;
|
||||
|
||||
export function EditStaticPageDialog({ onSave, page, ...props }: EditStaticPageDialogProps) {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(3);
|
||||
|
||||
const handleSubmit = (values: StaticPageFormValues) => {
|
||||
onSave?.(staticPageFormValuesTransformer.reverseTransform(values));
|
||||
};
|
||||
|
||||
const initialValues = page
|
||||
? staticPageFormValuesTransformer.transform(page)
|
||||
: initialStaticPageFormValues;
|
||||
|
||||
return <Dialog { ...props } maxWidth="lg">
|
||||
<Formik initialValues={ initialValues } onSubmit={ handleSubmit }>
|
||||
<Form className={ spacing.vertical }>
|
||||
<DialogTitle>{ t(page ? "page.edit.title" : "page.create.title") }</DialogTitle>
|
||||
<DialogContent>
|
||||
<StaticPageForm />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Actions>
|
||||
<Button variant="contained" color="primary" startIcon={ <Save /> } type="submit">{ t("save") }</Button>
|
||||
<Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button>
|
||||
</Actions>
|
||||
</DialogActions>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Dialog>
|
||||
}
|
||||
|
40
src/management/page/form.tsx
Normal file
40
src/management/page/form.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { default as StaticPage } from "@/data/page";
|
||||
import { identityTransformer, Transformer } from "@/serialization";
|
||||
import { Field, Form, FormikFormProps } from "formik";
|
||||
import React from "react";
|
||||
import { TextField as TextFieldFormik } from "formik-material-ui";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { CKEditorField } from "@/field/ckeditor";
|
||||
|
||||
export type StaticPageFormValues = StaticPage;
|
||||
|
||||
export const initialStaticPageFormValues: StaticPageFormValues = {
|
||||
slug: "",
|
||||
title: {
|
||||
en: "",
|
||||
pl: "",
|
||||
},
|
||||
content: {
|
||||
en: "",
|
||||
pl: "",
|
||||
}
|
||||
}
|
||||
|
||||
export const staticPageFormValuesTransformer: Transformer<StaticPage, StaticPageFormValues> = identityTransformer;
|
||||
|
||||
export function StaticPageForm() {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
return <div className={ spacing.vertical }>
|
||||
<Field label={ t("page.field.slug") } name="slug" fullWidth component={ TextFieldFormik }/>
|
||||
<Typography variant="subtitle2">{ t("page.field.title") }</Typography>
|
||||
<Field label={ t("translation:language.pl") } name="title.pl" fullWidth component={ TextFieldFormik }/>
|
||||
<Field label={ t("translation:language.en") } name="title.en" fullWidth component={ TextFieldFormik }/>
|
||||
<Typography variant="subtitle2">{ t("page.field.content") }</Typography>
|
||||
<Field label={ t("translation:language.pl") } name="content.pl" fullWidth component={ CKEditorField }/>
|
||||
<Field label={ t("translation:language.en") } name="content.en" fullWidth component={ CKEditorField }/>
|
||||
</div>
|
||||
}
|
157
src/management/page/list.tsx
Normal file
157
src/management/page/list.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { Page } from "@/pages/base";
|
||||
import { Management } from "@/management/main";
|
||||
import { Box, Button, CircularProgress, Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useAsyncState } from "@/hooks";
|
||||
import api from "@/management/api";
|
||||
import { Async } from "@/components/async";
|
||||
import MaterialTable, { Action, Column } from "material-table";
|
||||
import { default as StaticPage } from "@/data/page";
|
||||
import { Delete, FileFind, Pencil, Refresh } from "mdi-material-ui";
|
||||
import { encapsulate, one, OneOrMany } from "@/helpers";
|
||||
import { Actions } from "@/components";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Add, Edit } from "@material-ui/icons";
|
||||
import { createPortal } from "react-dom";
|
||||
import { EditStaticPageDialog } from "@/management/page/edit";
|
||||
import { Confirm } from "@/components/confirm";
|
||||
import useTheme from "@material-ui/core/styles/useTheme";
|
||||
import { BulkActions } from "@/management/common/BulkActions";
|
||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||
import { actionsColumn, fieldComparator, multilingualStringComparator } from "@/management/common/helpers";
|
||||
import { createDeleteAction } from "@/management/common/DeleteResourceAction";
|
||||
import { MultilingualCell } from "@/management/common/MultilangualCell";
|
||||
|
||||
const label = (page: StaticPage) => page.title.pl;
|
||||
|
||||
export const StaticPageManagement = () => {
|
||||
const { t } = useTranslation("management");
|
||||
const [ result, setPagesPromise ] = useAsyncState<StaticPage[]>();
|
||||
const [ selected, setSelected ] = useState<StaticPage[]>([]);
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const updatePageList = () => {
|
||||
setPagesPromise(api.page.all());
|
||||
}
|
||||
|
||||
useEffect(updatePageList, []);
|
||||
|
||||
const EditStaticPageAction = ({ page }: { page: StaticPage }) => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handlePageCreation = async (page: StaticPage) => {
|
||||
await api.page.save(page);
|
||||
setOpen(false);
|
||||
updatePageList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("actions.edit") as any }>
|
||||
<IconButton onClick={ () => setOpen(true) }><Edit /></IconButton>
|
||||
</Tooltip>
|
||||
{ open && createPortal(
|
||||
<EditStaticPageDialog open={ open } onSave={ handlePageCreation } page={ page } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const CreateStaticPageAction = () => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handlePageCreation = async (page: StaticPage) => {
|
||||
await api.page.save(page);
|
||||
setOpen(false);
|
||||
updatePageList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => setOpen(true) }>{ t("create") }</Button>
|
||||
{ createPortal(
|
||||
<EditStaticPageDialog open={ open } onSave={ handlePageCreation } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const handlePageDeletion = async (page: OneOrMany<StaticPage>) => {
|
||||
await api.page.remove(page);
|
||||
updatePageList();
|
||||
}
|
||||
|
||||
const DeleteStaticPageAction = createDeleteAction<StaticPage>({ label, onDelete: handlePageDeletion })
|
||||
|
||||
const PreviewStaticPageAction = ({ page }: { page: StaticPage }) => {
|
||||
const history = useHistory();
|
||||
const handlePagePreview = async () => history.push(`/${page.slug}`);
|
||||
|
||||
return <Tooltip title={ t("actions.preview") as string }>
|
||||
<IconButton onClick={ handlePagePreview }><FileFind /></IconButton>
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
const columns: Column<StaticPage>[] = [
|
||||
{
|
||||
render: page => <MultilingualCell value={ page.title }/>,
|
||||
title: t("page.field.title"),
|
||||
customSort: fieldComparator("title", multilingualStringComparator),
|
||||
},
|
||||
{
|
||||
field: "slug",
|
||||
title: t("page.field.slug"),
|
||||
},
|
||||
actionsColumn(page => <>
|
||||
<EditStaticPageAction page={ page } />
|
||||
<DeleteStaticPageAction resource={ page } />
|
||||
<PreviewStaticPageAction page={ page } />
|
||||
</>)
|
||||
];
|
||||
|
||||
const PagePreview = ({ page }: { page: StaticPage }) =>
|
||||
<Box className={ spacing.vertical } p={ 3 }>
|
||||
<div>
|
||||
<Typography variant="subtitle2">Polski</Typography>
|
||||
<Typography variant="h2">{ page.title.pl }</Typography>
|
||||
<div dangerouslySetInnerHTML={{ __html: page.content.pl }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="subtitle2">English</Typography>
|
||||
<Typography variant="h2">{ page.title.en }</Typography>
|
||||
<div dangerouslySetInnerHTML={{ __html: page.content.en }} />
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="lg">
|
||||
<Management.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t("page.index.title") }</Typography>
|
||||
</Management.Breadcrumbs>
|
||||
<Page.Title>{ t("page.index.title") }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||
<Actions>
|
||||
<CreateStaticPageAction />
|
||||
<Button onClick={ updatePageList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
{ selected.length > 0 && <BulkActions>
|
||||
<DeleteStaticPageAction resource={ selected }>
|
||||
{ action => <Button startIcon={ <Delete /> } onClick={ action }>{ t("actions.delete") }</Button> }
|
||||
</DeleteStaticPageAction>
|
||||
</BulkActions> }
|
||||
<Async async={ result } keepValue>{
|
||||
pages => <MaterialTable
|
||||
title={ <MaterialTableTitle result={ result } label={ t("page.index.title") }/> }
|
||||
columns={ columns }
|
||||
data={ pages }
|
||||
detailPanel={ page => <PagePreview page={ page } /> }
|
||||
onSelectionChange={ pages => setSelected(pages) }
|
||||
options={{ selection: true }}
|
||||
/>
|
||||
}</Async>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
||||
|
||||
export default StaticPageManagement;
|
22
src/management/routing.tsx
Normal file
22
src/management/routing.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Route } from "@/routing";
|
||||
import { isManagerMiddleware } from "@/management/middleware";
|
||||
import { EditionsManagement } from "@/management/edition/list";
|
||||
import React from "react";
|
||||
import { ManagementIndex } from "@/management/main";
|
||||
import StaticPageManagement from "@/management/page/list";
|
||||
import { InternshipTypeManagement } from "@/management/type/list";
|
||||
|
||||
export const managementRoutes: Route[] = ([
|
||||
{ name: "index", path: "/", content: ManagementIndex, exact: true },
|
||||
|
||||
{ name: "editions", path: "/editions", content: EditionsManagement },
|
||||
{ name: "types", path: "/types", content: InternshipTypeManagement },
|
||||
{ name: "static_pages", path: "/static-pages", content: StaticPageManagement }
|
||||
] as Route[]).map(
|
||||
({ name, path, middlewares = [], ...route }): Route => ({
|
||||
name: `management:${ name }`,
|
||||
path: `/management${ path }`,
|
||||
middlewares: [ isManagerMiddleware, ...middlewares ],
|
||||
...route
|
||||
})
|
||||
);
|
47
src/management/type/edit.tsx
Normal file
47
src/management/type/edit.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { Form, Formik } from "formik";
|
||||
import { initialStaticPageFormValues, StaticPageForm, StaticPageFormValues, staticPageFormValuesTransformer } from "@/management/page/form";
|
||||
import { Actions } from "@/components";
|
||||
import { Save } from "@material-ui/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Cancel } from "mdi-material-ui";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { initialInternshipTypeFormValues, InternshipTypeForm, InternshipTypeFormValues, internshipTypeFormValuesTransformer } from "@/management/type/form";
|
||||
import { InternshipType } from "@/data";
|
||||
|
||||
export type EditInternshipTypeDialogProps = {
|
||||
onSave?: (page: InternshipType) => void;
|
||||
value?: InternshipType;
|
||||
} & DialogProps;
|
||||
|
||||
export function EditInternshipTypeDialog({ onSave, value, ...props }: EditInternshipTypeDialogProps) {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(3);
|
||||
|
||||
const handleSubmit = (values: InternshipTypeFormValues) => {
|
||||
onSave?.(internshipTypeFormValuesTransformer.reverseTransform(values));
|
||||
};
|
||||
|
||||
const initialValues = value
|
||||
? internshipTypeFormValuesTransformer.transform(value)
|
||||
: initialInternshipTypeFormValues;
|
||||
|
||||
return <Dialog { ...props } maxWidth="lg">
|
||||
<Formik initialValues={ initialValues } onSubmit={ handleSubmit }>
|
||||
<Form className={ spacing.vertical }>
|
||||
<DialogTitle>{ t(value ? "type.edit.title" : "type.create.title") }</DialogTitle>
|
||||
<DialogContent>
|
||||
<InternshipTypeForm />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Actions>
|
||||
<Button variant="contained" color="primary" startIcon={ <Save /> } type="submit">{ t("save") }</Button>
|
||||
<Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button>
|
||||
</Actions>
|
||||
</DialogActions>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Dialog>
|
||||
}
|
||||
|
55
src/management/type/form.tsx
Normal file
55
src/management/type/form.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { InternshipType } from "@/data";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { Field } from "formik";
|
||||
import { TextField as TextFieldFormik, Checkbox as CheckboxFormik } from "formik-material-ui";
|
||||
import { FormControlLabel, FormGroup, Typography } from "@material-ui/core";
|
||||
import { CKEditorField } from "@/field/ckeditor";
|
||||
import { AccountCheck, ShieldCheck } from "mdi-material-ui";
|
||||
import { identityTransformer, Transformer } from "@/serialization";
|
||||
import { LabelWithIcon } from "@/management/common/LabelWithIcon";
|
||||
|
||||
export type InternshipTypeFormValues = Omit<InternshipType, 'id'>;
|
||||
|
||||
export const initialInternshipTypeFormValues: InternshipTypeFormValues = {
|
||||
label: {
|
||||
pl: "",
|
||||
en: "",
|
||||
},
|
||||
description: {
|
||||
pl: "",
|
||||
en: "",
|
||||
},
|
||||
requiresInsurance: false,
|
||||
requiresDeanApproval: false,
|
||||
}
|
||||
|
||||
export const internshipTypeFormValuesTransformer: Transformer<InternshipType, InternshipTypeFormValues> = identityTransformer;
|
||||
|
||||
export function InternshipTypeForm() {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
return <div className={ spacing.vertical }>
|
||||
<Typography variant="subtitle2">{ t("type.field.label") }</Typography>
|
||||
<Field label={ t("translation:language.pl") } name="label.pl" fullWidth component={ TextFieldFormik }/>
|
||||
<Field label={ t("translation:language.en") } name="label.en" fullWidth component={ TextFieldFormik }/>
|
||||
<Typography variant="subtitle2">{ t("type.field.description") }</Typography>
|
||||
<Field label={ t("translation:language.pl") } name="description.pl" fullWidth component={ TextFieldFormik }/>
|
||||
<Field label={ t("translation:language.en") } name="description.en" fullWidth component={ TextFieldFormik }/>
|
||||
|
||||
<Typography variant="subtitle2">{ t("type.field.flags") }</Typography>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={ <Field name="requiresDeanApproval" component={ CheckboxFormik }/> }
|
||||
label={ <LabelWithIcon icon={ <AccountCheck /> }>{ t("type.flag.dean-approval") }</LabelWithIcon> }
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={ <Field name="requiresInsurance" component={ CheckboxFormik }/> }
|
||||
label={ <LabelWithIcon icon={ <ShieldCheck /> }>{ t("type.flag.insurance") }</LabelWithIcon> }
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
}
|
149
src/management/type/list.tsx
Normal file
149
src/management/type/list.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { Page } from "@/pages/base";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncState } from "@/hooks";
|
||||
import { InternshipType } from "@/data";
|
||||
import api from "@/management/api";
|
||||
import { Management } from "@/management/main";
|
||||
import { Button, Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||
import { Async } from "@/components/async";
|
||||
import MaterialTable, { Column } from "material-table";
|
||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||
import { actionsColumn, fieldComparator, multilingualStringComparator } from "@/management/common/helpers";
|
||||
import { AccountCheck, Delete, Refresh, ShieldCheck } from "mdi-material-ui";
|
||||
import { OneOrMany } from "@/helpers";
|
||||
import { createDeleteAction } from "@/management/common/DeleteResourceAction";
|
||||
import { BulkActions } from "@/management/common/BulkActions";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { Actions } from "@/components";
|
||||
import { MultilingualCell } from "@/management/common/MultilangualCell";
|
||||
import { default as StaticPage } from "@/data/page";
|
||||
import { Add, Edit } from "@material-ui/icons";
|
||||
import { createPortal } from "react-dom";
|
||||
import { EditStaticPageDialog } from "@/management/page/edit";
|
||||
import { EditInternshipTypeDialog } from "@/management/type/edit";
|
||||
|
||||
const title = "type.index.title";
|
||||
|
||||
const label = (type: InternshipType) => type?.label?.pl;
|
||||
|
||||
export const InternshipTypeManagement = () => {
|
||||
const { t } = useTranslation("management");
|
||||
const [result, setTypesPromise] = useAsyncState<InternshipType[]>();
|
||||
const [selected, setSelected] = useState<InternshipType[]>([]);
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const updateTypeList = () => {
|
||||
setTypesPromise(api.type.all());
|
||||
}
|
||||
|
||||
const handleTypeDelete = async (type: OneOrMany<InternshipType>) => {
|
||||
await api.type.remove(type);
|
||||
updateTypeList();
|
||||
}
|
||||
|
||||
useEffect(updateTypeList, []);
|
||||
|
||||
const DeleteTypeAction = createDeleteAction({ label, onDelete: handleTypeDelete });
|
||||
|
||||
const CreateTypeAction = () => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handleTypeCreation = async (value: InternshipType) => {
|
||||
await api.type.save(value);
|
||||
setOpen(false);
|
||||
updateTypeList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => setOpen(true) }>{ t("create") }</Button>
|
||||
{ open && createPortal(
|
||||
<EditInternshipTypeDialog open={ open } onSave={ handleTypeCreation } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const EditTypeAction = ({ resource }: { resource: InternshipType }) => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handleTypeCreation = async (value: InternshipType) => {
|
||||
await api.type.save(value);
|
||||
setOpen(false);
|
||||
updateTypeList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("actions.edit") as any }>
|
||||
<IconButton onClick={ () => setOpen(true) }><Edit /></IconButton>
|
||||
</Tooltip>
|
||||
{ open && createPortal(
|
||||
<EditInternshipTypeDialog open={ open } onSave={ handleTypeCreation } value={ resource } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const columns: Column<InternshipType>[] = [
|
||||
{
|
||||
field: "id",
|
||||
title: "ID",
|
||||
width: 0,
|
||||
defaultSort: "asc",
|
||||
filtering: false,
|
||||
},
|
||||
{
|
||||
title: t("type.field.label"),
|
||||
render: type => <MultilingualCell value={ type.label }/>,
|
||||
customSort: fieldComparator("label", multilingualStringComparator),
|
||||
},
|
||||
{
|
||||
title: t("type.field.description"),
|
||||
render: type => type.description && <MultilingualCell value={ type.description }/>,
|
||||
sorting: false,
|
||||
},
|
||||
{
|
||||
title: t("type.field.flags"),
|
||||
render: type => <div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{ type.requiresDeanApproval && <Tooltip title={ t("type.flag.dean-approval") as string }><AccountCheck/></Tooltip> }
|
||||
{ type.requiresInsurance && <Tooltip title={ t("type.flag.insurance") as string }><ShieldCheck/></Tooltip> }
|
||||
</div>,
|
||||
width: 0,
|
||||
filtering: true,
|
||||
sorting: false,
|
||||
},
|
||||
actionsColumn(type => <>
|
||||
<DeleteTypeAction resource={ type }/>
|
||||
<EditTypeAction resource={ type }/>
|
||||
</>)
|
||||
];
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="lg">
|
||||
<Management.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||
</Management.Breadcrumbs>
|
||||
<Page.Title>{ t(title) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||
<Actions>
|
||||
<CreateTypeAction />
|
||||
<Button onClick={ updateTypeList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
{ selected.length > 0 && <BulkActions>
|
||||
<DeleteTypeAction resource={ selected }>
|
||||
{ action => <Button startIcon={ <Delete /> } onClick={ action }>{ t("actions.delete") }</Button> }
|
||||
</DeleteTypeAction>
|
||||
</BulkActions> }
|
||||
<Async async={ result } keepValue>{
|
||||
pages => <MaterialTable
|
||||
title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
|
||||
columns={ columns }
|
||||
data={ pages }
|
||||
onSelectionChange={ pages => setSelected(pages) }
|
||||
options={ { selection: true, pageSize: 10 } }
|
||||
/>
|
||||
}</Async>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { Middleware, route } from "@/routing";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState, isReady } from "@/state/reducer";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import React from "react";
|
||||
import { Redirect, useRouteMatch } from "react-router-dom";
|
||||
import React, { useEffect } from "react";
|
||||
import { UserState } from "@/state/reducer/user";
|
||||
|
||||
export const isReadyMiddleware: Middleware<any, any> = Next => isLoggedInMiddleware(() => {
|
||||
@ -22,5 +22,7 @@ export const isLoggedInMiddleware: Middleware<any, any> = Next => {
|
||||
return <Next />;
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem('back-path', window.location.pathname);
|
||||
|
||||
return <Redirect to={ route("user_login") } />;
|
||||
}
|
||||
|
@ -12,18 +12,19 @@ import { Alert } from "@material-ui/lab";
|
||||
import { Subset } from "@/helpers";
|
||||
import { useDispatch } from "@/state/actions";
|
||||
import { loginToEdition } from "@/pages/edition/pick";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
import { useDebouncedEffect } from "@/hooks/useDebouncedEffect";
|
||||
|
||||
export const RegisterEditionPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [key, setKey] = useState<string>("");
|
||||
const [{ value: edition, isLoading }, setEdition] = useAsyncState<Subset<Edition> | null>(undefined);
|
||||
|
||||
const classes = useVerticalSpacing(3);
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch<any>();
|
||||
|
||||
const [key, setKey] = useState<string>(match.params['edition'] || "");
|
||||
const [{ value: edition, isLoading }, setEdition] = useAsyncState<Subset<Edition> | null>(undefined);
|
||||
|
||||
useDebouncedEffect(() => {
|
||||
setEdition(api.edition.get(key));
|
||||
|
@ -4,6 +4,7 @@ import { createStyles, makeStyles } from "@material-ui/core/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import { CommentQuestion } from "mdi-material-ui/index";
|
||||
import { ContactAction } from "@/components/contact";
|
||||
|
||||
export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
@ -46,8 +47,12 @@ export const Status = ({ submission } : SubmissionStatusProps) => {
|
||||
return <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>;
|
||||
}
|
||||
|
||||
export const ContactAction = (props: ButtonProps) => {
|
||||
export const ContactButton = (props: ButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
|
||||
return <ContactAction>{
|
||||
({ action }) => <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props } onClick={ action}>
|
||||
{ t('contact') }
|
||||
</Button> }
|
||||
</ContactAction>
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { InsuranceState } from "@/state/reducer/insurance";
|
||||
import { Actions, Step } from "@/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import { ContactAction } from "@/pages/steps/common";
|
||||
import { ContactButton } from "@/pages/steps/common";
|
||||
import { useDeadlines } from "@/hooks";
|
||||
import { StepProps } from "@material-ui/core";
|
||||
|
||||
@ -17,7 +17,7 @@ export const InsuranceStep = (props: StepProps) => {
|
||||
return <Step { ...props } label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }>
|
||||
<p>{ t(`steps.insurance.instructions`) }</p>
|
||||
<Actions>
|
||||
<ContactAction />
|
||||
<ContactButton />
|
||||
</Actions>
|
||||
</Step>
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { Link as RouterLink, useHistory } from "react-router-dom";
|
||||
import { Actions, Step } from "@/components";
|
||||
import React, { HTMLProps } from "react";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||
import { ContactAction, Status } from "@/pages/steps/common";
|
||||
import { ContactButton, Status } from "@/pages/steps/common";
|
||||
import { Description as DescriptionIcon } from "@material-ui/icons";
|
||||
import { useDeadlines } from "@/hooks";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
@ -56,9 +56,9 @@ const PlanActions = () => {
|
||||
</Actions>
|
||||
case "declined":
|
||||
return <Actions>
|
||||
<FormAction>{ t('fix-errors') }</FormAction>
|
||||
<FormAction>{ t('send-again') }</FormAction>
|
||||
<TemplateAction />
|
||||
<ContactAction/>
|
||||
<ContactButton/>
|
||||
</Actions>
|
||||
case "draft":
|
||||
return <Actions>
|
||||
|
@ -10,7 +10,7 @@ import { Actions, Step } from "@/components";
|
||||
import { route } from "@/routing";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index";
|
||||
import { ContactAction, Status } from "@/pages/steps/common";
|
||||
import { ContactButton, Status } from "@/pages/steps/common";
|
||||
import { useDeadlines } from "@/hooks";
|
||||
|
||||
const ProposalActions = () => {
|
||||
@ -43,7 +43,7 @@ const ProposalActions = () => {
|
||||
case "declined":
|
||||
return <Actions>
|
||||
<FormAction>{ t('fix-errors') }</FormAction>
|
||||
<ContactAction />
|
||||
<ContactButton />
|
||||
</Actions>
|
||||
case "draft":
|
||||
return <Actions>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Dispatch, useEffect } from "react";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Button, CircularProgress, Container, Typography } from "@material-ui/core";
|
||||
import { Button, CircularProgress, Container, SvgIcon, Typography } 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";
|
||||
@ -12,13 +12,22 @@ import { UserActions } from "@/state/actions/user";
|
||||
import { getAuthorizeUrl } from "@/api/user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Loading } from "@/components/loading";
|
||||
import GUTLogo from "!@svgr/webpack!@/../public/img/pg-logo.svg";
|
||||
|
||||
const authorizeUser = (code?: string) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
|
||||
|
||||
type AuthorizeUserOptions = {
|
||||
isStudent?: boolean;
|
||||
isManager?: boolean;
|
||||
}
|
||||
|
||||
const authorizeUser = (code?: string, { isStudent = false, isManager = false }: AuthorizeUserOptions = { isStudent: true }) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
|
||||
const token = await api.user.login(code);
|
||||
|
||||
dispatch({
|
||||
type: UserActions.Login,
|
||||
token,
|
||||
isStudent,
|
||||
isManager,
|
||||
})
|
||||
|
||||
const student = await api.student.current();
|
||||
@ -36,23 +45,32 @@ export const UserLoginPage = () => {
|
||||
const query = new URLSearchParams(useLocation().search);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSampleLogin = async () => {
|
||||
await dispatch(authorizeUser());
|
||||
const redirectAfterLogin = () => {
|
||||
history.push(window.sessionStorage.getItem('back-path') || "/");
|
||||
}
|
||||
|
||||
history.push(route("home"));
|
||||
const handleSampleAdminLogin = async () => {
|
||||
await dispatch(authorizeUser(undefined, { isManager: true }));
|
||||
|
||||
history.push(route("management:index"));
|
||||
}
|
||||
|
||||
const handleSampleStudentLogin = async () => {
|
||||
await dispatch(authorizeUser());
|
||||
redirectAfterLogin();
|
||||
}
|
||||
|
||||
const handlePgLogin = async () => {
|
||||
history.push(route("user_login") + "/pg");
|
||||
}
|
||||
|
||||
const classes = useVerticalSpacing(3);
|
||||
const classes = useVerticalSpacing(2);
|
||||
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
if (location.pathname === `${match.path}/check/pg`) {
|
||||
await dispatch(authorizeUser(query.get("code") as string));
|
||||
history.push("/");
|
||||
redirectAfterLogin();
|
||||
}
|
||||
})();
|
||||
}, [ match.path ]);
|
||||
@ -61,14 +79,21 @@ export const UserLoginPage = () => {
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="md">
|
||||
<Page.Title>Zaloguj się</Page.Title>
|
||||
<Page.Title>{ t("login") }</Page.Title>
|
||||
</Page.Header>
|
||||
<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>
|
||||
<Button fullWidth onClick={ handlePgLogin } variant="contained" color="primary" startIcon={
|
||||
// @ts-ignore
|
||||
<GUTLogo style={{ height: "1.25rem" }} viewBox="0 0 144 64" />
|
||||
}>
|
||||
{ t("login-as.gut-account") }
|
||||
</Button>
|
||||
<Typography variant="subtitle2">{ t("login-as.sample") }</Typography>
|
||||
<Button fullWidth onClick={ handleSampleStudentLogin } variant="contained" color="secondary">{ t("login-as.sample-student")}</Button>
|
||||
<Button fullWidth onClick={ handleSampleAdminLogin } variant="contained" color="secondary">{ t("login-as.sample-manager")}</Button>
|
||||
</Container>
|
||||
</Route>
|
||||
<Route path={`${match.path}/pg`} render={ () => {
|
||||
|
@ -10,8 +10,9 @@ import PickEditionPage from "@/pages/edition/pick";
|
||||
import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware";
|
||||
import UserFillPage from "@/pages/user/fill";
|
||||
import UserProfilePage from "@/pages/user/profile";
|
||||
import { managementRoutes } from "@/management/routing";
|
||||
|
||||
type Route = {
|
||||
export type Route = {
|
||||
name?: string;
|
||||
content: () => ReactComponentElement<any>,
|
||||
condition?: () => boolean,
|
||||
@ -36,6 +37,7 @@ export const routes: Route[] = [
|
||||
|
||||
// edition
|
||||
{ name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||
{ name: "edition_register_exact", path: "/edition/register/:edition", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||
{ name: "edition_pick", path: "/edition/pick", exact: true, content: () => <PickEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||
|
||||
// internship
|
||||
@ -48,8 +50,10 @@ export const routes: Route[] = [
|
||||
{ name: "user_fill", path: "/user/data", content: () => <UserFillPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||
{ name: "user_profile", path: "/user/profile", content: () => <UserProfilePage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||
|
||||
...managementRoutes,
|
||||
|
||||
// fallback route for 404 pages
|
||||
{ name: "fallback", path: "*", content: () => <FallbackPage/> }
|
||||
{ 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>
|
||||
|
@ -19,3 +19,8 @@ export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transfo
|
||||
export type OneWayTransformer<TFrom, TResult, TContext = never> = {
|
||||
transform(subject: TFrom, context?: TContext): TResult;
|
||||
}
|
||||
|
||||
export const identityTransformer: Transformer<any, any> = {
|
||||
transform: subject => subject,
|
||||
reverseTransform: subject => subject
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ export enum UserActions {
|
||||
|
||||
export interface LoginAction extends Action<UserActions.Login> {
|
||||
token: string;
|
||||
isStudent: boolean;
|
||||
isManager: boolean;
|
||||
}
|
||||
|
||||
export type LogoutAction = Action<UserActions.Logout>;
|
||||
|
@ -3,10 +3,14 @@ import { UserAction, UserActions } from "@/state/actions/user";
|
||||
export type UserState = {
|
||||
loggedIn: boolean;
|
||||
token?: string;
|
||||
isManager: boolean;
|
||||
isStudent: boolean;
|
||||
}
|
||||
|
||||
const initialUserState: UserState = {
|
||||
loggedIn: false,
|
||||
isManager: false,
|
||||
isStudent: false,
|
||||
}
|
||||
|
||||
const userReducer = (state: UserState = initialUserState, action: UserAction): UserState => {
|
||||
@ -16,12 +20,12 @@ const userReducer = (state: UserState = initialUserState, action: UserAction): U
|
||||
...state,
|
||||
loggedIn: true,
|
||||
token: action.token,
|
||||
isManager: action.isManager,
|
||||
isStudent: action.isManager,
|
||||
}
|
||||
|
||||
case UserActions.Logout:
|
||||
return {
|
||||
loggedIn: false
|
||||
};
|
||||
return initialUserState;
|
||||
}
|
||||
|
||||
return state;
|
||||
|
@ -10,5 +10,34 @@
|
||||
}
|
||||
|
||||
.footer__copyright {
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.footer__menu {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: .5rem;
|
||||
&::before {
|
||||
content: "•";
|
||||
opacity: 0.6;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
0
translations/management.en.yaml
Normal file
0
translations/management.en.yaml
Normal file
51
translations/management.pl.yaml
Normal file
51
translations/management.pl.yaml
Normal file
@ -0,0 +1,51 @@
|
||||
title: Zarządzanie
|
||||
|
||||
create: utwórz
|
||||
refresh: $t(translation:refresh)
|
||||
save: zapisz
|
||||
cancel: anuluj
|
||||
|
||||
actions:
|
||||
label: Akcje
|
||||
bulk: Akcje masowe
|
||||
preview: Podgląd
|
||||
delete: Usuń
|
||||
edit: Edytuj
|
||||
|
||||
edition:
|
||||
index:
|
||||
title: "Edycje praktyk"
|
||||
field:
|
||||
id: Identyfikator
|
||||
start: Początek
|
||||
end: Koniec
|
||||
course: Kierunek
|
||||
|
||||
type:
|
||||
index:
|
||||
title: "Rodzeje praktyki"
|
||||
edit:
|
||||
title: "Edytuj rodzaj praktyki"
|
||||
create:
|
||||
title: "Utwórz rodzaj praktyki"
|
||||
field:
|
||||
label: "Rodzaj praktyki"
|
||||
description: "Opis"
|
||||
flags: "Wymogi"
|
||||
flag:
|
||||
dean-approval: "Wymaga zgody dziekana"
|
||||
insurance: "Wymaga ubezpieczenia"
|
||||
|
||||
page:
|
||||
index:
|
||||
title: Strony statyczne
|
||||
field:
|
||||
title: Tytuł
|
||||
content: Treść
|
||||
slug: Adres
|
||||
create:
|
||||
title: Utwórz stronę statyczną
|
||||
edit:
|
||||
title: Zmień stronę statyczną
|
||||
confirm:
|
||||
bulk-delete: Czy na pewno chcesz usunąć wszystkie wybrane strony?
|
@ -5,6 +5,11 @@ login: zaloguj się
|
||||
login-in-progress: Logowanie w toku, proszę czekać...
|
||||
logout: wyloguj się
|
||||
logged-in-as: zalogowany jako <1>{{ name }}</1>
|
||||
login-as:
|
||||
sample: "Przykładowe konta"
|
||||
gut-account: "Zaloguj z pomocą konta politechnicznego"
|
||||
sample-student: "Zaloguj jako przykładowy student"
|
||||
sample-manager: "Zaloguj jako przykładowy pełnomocnik/administrator"
|
||||
|
||||
until: do {{ date, DD MMMM YYYY }}
|
||||
not-before: od {{ date, DD MMMM YYYY }}
|
||||
@ -21,12 +26,15 @@ contact: skontaktuj się z pełnomocnikiem
|
||||
comments: Zgłoszone uwagi
|
||||
send-again: wyślij ponownie
|
||||
cancel: anuluj
|
||||
send: wyślij
|
||||
|
||||
accept: zaakceptuj
|
||||
accept-with-comments: zaakceptuj z uwagami
|
||||
accept-without-comments: zaakceptuj bez uwag
|
||||
discard: zgłoś uwagi
|
||||
|
||||
incorrect-data-question: "Powyższe dane nie są poprawne?"
|
||||
|
||||
dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
|
||||
|
||||
pages:
|
||||
@ -58,6 +66,10 @@ forms:
|
||||
sections:
|
||||
personal: "Dane osobowe"
|
||||
studies: "Dane kierunkowe"
|
||||
contact:
|
||||
title: $t(contact)
|
||||
field:
|
||||
content: "Treść"
|
||||
internship:
|
||||
fields:
|
||||
start-date: Data rozpoczęcia praktyki
|
||||
@ -123,6 +135,8 @@ internship:
|
||||
intern:
|
||||
semester: semestr {{ semester, roman }}
|
||||
album: "numer albumu {{ album }}"
|
||||
kind-requires-dean-approval: "Ten rodzaj praktyki/umowy wymaga akceptacji przez dziekana!"
|
||||
duration-requires-dean-approval: "Taki okres trwania praktyki wymaga akceptacji przez dziekana!"
|
||||
date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}"
|
||||
duration_2: "{{ duration, weeks }} tygodni"
|
||||
duration_0: "{{ duration, weeks }} tydzień"
|
||||
@ -190,7 +204,7 @@ steps:
|
||||
draft: >
|
||||
W porozumieniu z firmą w której odbywają się praktyki należy sporządzić Indywidualny Plan Praktyk zgodnie z
|
||||
załączonym szablonem a następnie wysłać go do weryfikacji. Indywidualny Plan Praktyk musi zostać zatwierdzony
|
||||
oraz podpisany przez Twojego zakłądowego opiekuna praktyki.
|
||||
oraz podpisany przez Twojego zakładowego opiekuna praktyki.
|
||||
awaiting: >
|
||||
Twój indywidualny program praktyki został poprawnie zapisany w systemie. Musi on jeszcze zostać zweryfikowany i
|
||||
zatwierdzony. Po weryfikacji zostaniesz poinformowany o akceptacji bądź konieczności wprowadzenia zmian.
|
||||
@ -211,7 +225,16 @@ steps:
|
||||
instructions: >
|
||||
Należy zgłosić się do pełnomocnika ds. praktyk Twojego kierunku i podpisać umowę ubezpieczenia. (TODO)
|
||||
|
||||
language:
|
||||
pl: Polski
|
||||
en: Angielski
|
||||
|
||||
validation:
|
||||
api:
|
||||
GreaterThanOrEqualValidator: Wartość pola "{{ PropertyName }}" musi być większa bądź równa {{ ComparisonValue }}.
|
||||
NotEmptyValidator: Wartosć pola "{{ PropertyName }}" nie może być pusta.
|
||||
NotNullValidator: Wartosć pola "{{ PropertyName }}" nie może być pusta.
|
||||
PredicateValidator: Wartosć pola "{{ PropertyName }}" nie spełnia warunków walidacji.
|
||||
required: "To pole jest wymagane"
|
||||
email: "Wprowadź poprawny adres e-mail"
|
||||
phone: "Wprowadź poprawny numer telefonu"
|
||||
@ -220,3 +243,5 @@ validation:
|
||||
|
||||
contact-coordinator: "Skontaktuj się z koordynatorem"
|
||||
download: "pobierz"
|
||||
management: "zarządzanie"
|
||||
refresh: "odśwież"
|
||||
|
@ -59,7 +59,7 @@ const config = {
|
||||
port: parseInt(process.env.APP_PORT || "3000"),
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "https://system-praktyk.stg.kadet.net/api/",
|
||||
target: "https://system-praktyk.dev.kadet.net/api/",
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
"^/api": ''
|
||||
|
Loading…
Reference in New Issue
Block a user