Add step info
This commit is contained in:
parent
a3db797a06
commit
e012d015db
98
src/app.tsx
98
src/app.tsx
@ -1,13 +1,8 @@
|
|||||||
import React, { Dispatch, HTMLProps } from 'react';
|
import React, { Dispatch, HTMLProps, useEffect } from 'react';
|
||||||
import { MuiThemeProvider as ThemeProvider, StylesProvider } from "@material-ui/core/styles";
|
import { Link, Route, Switch } from "react-router-dom"
|
||||||
import { studentTheme } from "./ui/theme";
|
import moment from "moment";
|
||||||
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
|
|
||||||
import MomentUtils from "@date-io/moment";
|
|
||||||
import { BrowserRouter, Link, Route, Switch } from "react-router-dom"
|
|
||||||
import moment, { Moment } from "moment";
|
|
||||||
import { route, routes } from "@/routing";
|
import { route, routes } from "@/routing";
|
||||||
import { Provider, useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import store, { persistor } from "@/state/store";
|
|
||||||
import { AppState } from "@/state/reducer";
|
import { AppState } from "@/state/reducer";
|
||||||
import { StudentAction, StudentActions } from "@/state/actions/student";
|
import { StudentAction, StudentActions } from "@/state/actions/student";
|
||||||
import { sampleStudent } from "@/provider/dummy/student";
|
import { sampleStudent } from "@/provider/dummy/student";
|
||||||
@ -16,13 +11,9 @@ import { Student } from "@/data";
|
|||||||
import '@/styles/overrides.scss'
|
import '@/styles/overrides.scss'
|
||||||
import '@/styles/header.scss'
|
import '@/styles/header.scss'
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { PersistGate } from 'redux-persist/integration/react';
|
import { EditionAction, EditionActions } from "@/state/actions/edition";
|
||||||
|
import { sampleEdition } from "@/provider/dummy/edition";
|
||||||
class LocalizedMomentUtils extends MomentUtils {
|
import { Edition } from "@/data/edition";
|
||||||
getDatePickerHeaderText(date: Moment): string {
|
|
||||||
return this.format(date, "d MMM yyyy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
||||||
const student = useSelector<AppState, Student>(state => state.student as Student);
|
const student = useSelector<AppState, Student>(state => state.student as Student);
|
||||||
@ -40,14 +31,14 @@ const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
|||||||
dispatch({ type: StudentActions.Logout })
|
dispatch({ type: StudentActions.Logout })
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ul {...props}>
|
return <ul { ...props }>
|
||||||
{
|
{
|
||||||
student ? <>
|
student ? <>
|
||||||
<Trans t={ t } i18nKey="logged-in-as">logged in as <strong>{{ name: `${student.name} ${student.surname}` }}</strong></Trans>
|
<Trans t={ t } i18nKey="logged-in-as">logged in as <strong>{ { name: `${ student.name } ${ student.surname }` } }</strong></Trans>
|
||||||
{' '}
|
{ ' ' }
|
||||||
(<Link to={'#'} onClick={ handleUserLogout }>{ t('logout') }</Link>)
|
(<Link to={ '#' } onClick={ handleUserLogout }>{ t('logout') }</Link>)
|
||||||
</> : <>
|
</> : <>
|
||||||
<Link to={'#'} onClick={ handleUserLogin }>{ t('login') }</Link>
|
<Link to={ '#' } onClick={ handleUserLogin }>{ t('login') }</Link>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</ul>;
|
</ul>;
|
||||||
@ -75,39 +66,38 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
const dispatch = useDispatch<Dispatch<EditionAction>>();
|
||||||
<Provider store={ store }>
|
const edition = useSelector<AppState, Edition | null>(state => state.edition);
|
||||||
<PersistGate loading={ null } persistor={ persistor }>
|
|
||||||
<StylesProvider injectFirst>
|
useEffect(() => {
|
||||||
<MuiPickersUtilsProvider utils={ LocalizedMomentUtils } libInstance={ moment }>
|
if (!edition) {
|
||||||
<ThemeProvider theme={ studentTheme }>
|
dispatch({ type: EditionActions.Set, edition: sampleEdition });
|
||||||
<BrowserRouter>
|
}
|
||||||
<header className="header">
|
})
|
||||||
<div id="logo" className="header__logo">
|
|
||||||
<Link to={ route('home') }>
|
const isReady = !!edition;
|
||||||
<img src="img/pg-logotyp.svg"/>
|
|
||||||
</Link>
|
return <>
|
||||||
</div>
|
<header className="header">
|
||||||
<div className="header__nav">
|
<div id="logo" className="header__logo">
|
||||||
<nav className="header__top">
|
<Link to={ route('home') }>
|
||||||
<ul className="header__menu"></ul>
|
<img src="img/pg-logotyp.svg"/>
|
||||||
<UserMenu className="header__user"/>
|
</Link>
|
||||||
<div className="header__divider" />
|
</div>
|
||||||
<LanguageSwitcher className="header__language-switcher"/>
|
<div className="header__nav">
|
||||||
</nav>
|
<nav className="header__top">
|
||||||
<nav className="header__bottom">
|
<ul className="header__menu"></ul>
|
||||||
<ul className="header__menu header__menu--main"></ul>
|
<UserMenu className="header__user"/>
|
||||||
</nav>
|
<div className="header__divider"/>
|
||||||
</div>
|
<LanguageSwitcher className="header__language-switcher"/>
|
||||||
</header>
|
</nav>
|
||||||
<Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch>
|
<nav className="header__bottom">
|
||||||
</BrowserRouter>
|
<ul className="header__menu header__menu--main"></ul>
|
||||||
</ThemeProvider>
|
</nav>
|
||||||
</MuiPickersUtilsProvider>
|
</div>
|
||||||
</StylesProvider>
|
</header>
|
||||||
</PersistGate>
|
{ isReady && <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> }
|
||||||
</Provider>
|
</>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
21
src/data/edition.ts
Normal file
21
src/data/edition.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Moment } from "moment";
|
||||||
|
|
||||||
|
export type Edition = {
|
||||||
|
startDate: Moment;
|
||||||
|
endDate: Moment;
|
||||||
|
proposalDeadline: Moment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Deadlines = {
|
||||||
|
personalData?: Moment;
|
||||||
|
proposal?: Moment;
|
||||||
|
personalPlan?: Moment;
|
||||||
|
report?: Moment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEditionDeadlines(edition: Edition): Deadlines {
|
||||||
|
return {
|
||||||
|
proposal: edition.proposalDeadline,
|
||||||
|
personalPlan: edition.proposalDeadline,
|
||||||
|
}
|
||||||
|
}
|
@ -11,3 +11,18 @@ export interface Student extends Identifiable {
|
|||||||
semester: Semester;
|
semester: Semester;
|
||||||
course: Course;
|
course: Course;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isStudentDataComplete(student: Student): boolean {
|
||||||
|
return getMissingStudentData(student).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMissingStudentData(student: Student): (keyof Student)[] {
|
||||||
|
return [
|
||||||
|
!!student.name || "name",
|
||||||
|
!!student.surname || "surname",
|
||||||
|
!!student.email || "email",
|
||||||
|
!!student.albumNumber || "albumNumber",
|
||||||
|
!!student.semester || "semester",
|
||||||
|
!!student.course || "course",
|
||||||
|
].filter(x => x !== true) as (keyof Student)[];
|
||||||
|
}
|
||||||
|
@ -2,10 +2,37 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import "./i18n"
|
import "./i18n"
|
||||||
import App from './app';
|
import App from './app';
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import store, { persistor } from "@/state/store";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import { MuiThemeProvider as ThemeProvider, StylesProvider } from "@material-ui/core/styles";
|
||||||
|
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
|
||||||
|
import moment, { Moment } from "moment";
|
||||||
|
import { studentTheme } from "@/ui/theme";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import MomentUtils from "@date-io/moment";
|
||||||
|
|
||||||
|
class LocalizedMomentUtils extends MomentUtils {
|
||||||
|
getDatePickerHeaderText(date: Moment): string {
|
||||||
|
return this.format(date, "d MMM yyyy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<Provider store={ store }>
|
||||||
|
<PersistGate loading={ null } persistor={ persistor }>
|
||||||
|
<StylesProvider injectFirst>
|
||||||
|
<MuiPickersUtilsProvider utils={ LocalizedMomentUtils } libInstance={ moment }>
|
||||||
|
<ThemeProvider theme={ studentTheme }>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
</MuiPickersUtilsProvider>
|
||||||
|
</StylesProvider>
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
@ -5,6 +5,10 @@ import { Link as RouterLink } from "react-router-dom";
|
|||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { AppState } from "@/state/reducer";
|
||||||
|
import { getMissingStudentData, Student } from "@/data";
|
||||||
|
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
|
||||||
|
|
||||||
type StepProps = StepperStepProps & {
|
type StepProps = StepperStepProps & {
|
||||||
until?: Moment;
|
until?: Moment;
|
||||||
@ -18,7 +22,7 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
|
const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
|
||||||
const left = useMemo(() => moment.duration(now.diff(until)), [until]);
|
const left = useMemo(() => moment.duration(now.diff(until)), [until]);
|
||||||
|
|
||||||
return <StepperStep { ...props } completed={ completed || !!completedOn }>
|
return <StepperStep { ...props } completed={ completed || !!completedOn }>
|
||||||
<StepLabel>
|
<StepLabel>
|
||||||
@ -26,7 +30,8 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step
|
|||||||
{ until && <Box>
|
{ until && <Box>
|
||||||
<Typography variant="subtitle2" color="textSecondary">
|
<Typography variant="subtitle2" color="textSecondary">
|
||||||
{ t('until', { date: until.format("DD MMMM YYYY") }) }
|
{ t('until', { date: until.format("DD MMMM YYYY") }) }
|
||||||
{ isLate && <Typography color="error" display="inline" variant="body2"> - { t('late', { by: moment.duration(now.diff(until)).humanize() }) }</Typography> }
|
{ isLate && <Typography color="error" display="inline"
|
||||||
|
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)).humanize() }) }</Typography> }
|
||||||
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left.humanize() }) }</Typography> }
|
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left.humanize() }) }</Typography> }
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box> }
|
</Box> }
|
||||||
@ -38,19 +43,47 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step
|
|||||||
export const MainPage = () => {
|
export const MainPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const student = useSelector<AppState, Student | null>(state => state.student);
|
||||||
|
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
|
||||||
|
|
||||||
|
const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]);
|
||||||
|
|
||||||
return <Page my={ 6 }>
|
return <Page my={ 6 }>
|
||||||
<Container>
|
<Container>
|
||||||
<Typography variant="h2">{ t("sections.my-internship.header") }</Typography>
|
<Typography variant="h2">{ t("sections.my-internship.header") }</Typography>
|
||||||
<Stepper orientation="vertical" nonLinear>
|
<Stepper orientation="vertical" nonLinear>
|
||||||
<Step label={ t('steps.personal-data.header') } until={ moment("2020-07-01") }/>
|
<Step label={ t('steps.personal-data.header') } completed={ missingStudentData.length === 0 } until={ deadlines.personalData }>
|
||||||
<Step label={ t('steps.internship-proposal.header') }>
|
{ missingStudentData.length > 0 && <>
|
||||||
|
<p>{ t('steps.personal-data.info') }</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{ missingStudentData.map(field => <li key={ field }>{ t(`student.${field}`) }</li>) }
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }>
|
||||||
|
{ t('steps.personal-data.form') }
|
||||||
|
</Button>
|
||||||
|
</> }
|
||||||
|
</Step>
|
||||||
|
<Step label={ t('steps.internship-proposal.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }>
|
||||||
|
<p>{ t('steps.internship-proposal.info') }</p>
|
||||||
|
|
||||||
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }>
|
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }>
|
||||||
{ t('steps.internship-proposal.form') }
|
{ t('steps.internship-proposal.form') }
|
||||||
</Button>
|
</Button>
|
||||||
</Step>
|
</Step>
|
||||||
<Step label={ t('steps.plan.header') } until={ moment("2020-07-22") }/>
|
<Step label={ t('steps.plan.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }>
|
||||||
<Step label={ t('steps.insurance.header') }/>
|
<p>{ t('steps.plan.info') }</p>
|
||||||
<Step label={ t('steps.report.header') }/>
|
|
||||||
|
<Button to={ route("internship_proposal") } component={ RouterLink }>
|
||||||
|
{ t('steps.plan.template') }
|
||||||
|
</Button>
|
||||||
|
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }>
|
||||||
|
{ t('steps.plan.submit') }
|
||||||
|
</Button>
|
||||||
|
</Step>
|
||||||
|
<Step label={ t('steps.insurance.header') } />
|
||||||
|
<Step label={ t('steps.report.header') } until={ deadlines.report }/>
|
||||||
<Step label={ t('steps.grade.header') }/>
|
<Step label={ t('steps.grade.header') }/>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</Container>
|
</Container>
|
||||||
|
8
src/provider/dummy/edition.ts
Normal file
8
src/provider/dummy/edition.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Edition } from "@/data/edition";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
export const sampleEdition: Edition = {
|
||||||
|
startDate: moment("2020-07-01"),
|
||||||
|
endDate: moment("2020-09-30"),
|
||||||
|
proposalDeadline: moment("2020-07-31")
|
||||||
|
}
|
13
src/state/actions/edition.ts
Normal file
13
src/state/actions/edition.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Action } from "@/state/actions/base";
|
||||||
|
import { Edition } from "@/data/edition";
|
||||||
|
|
||||||
|
export enum EditionActions {
|
||||||
|
Set = 'SET',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetAction extends Action<EditionActions.Set> {
|
||||||
|
edition: Edition,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EditionAction = SetAction;
|
||||||
|
|
17
src/state/reducer/edition.ts
Normal file
17
src/state/reducer/edition.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Edition } from "@/data/edition";
|
||||||
|
import { EditionAction, EditionActions } from "@/state/actions/edition";
|
||||||
|
|
||||||
|
export type EditionState = Edition | null;
|
||||||
|
|
||||||
|
const initialEditionState: EditionState = null;
|
||||||
|
|
||||||
|
const editionReducer = (state: EditionState = initialEditionState, action: EditionAction): EditionState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case EditionActions.Set:
|
||||||
|
return action.edition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default editionReducer;
|
@ -1,9 +1,11 @@
|
|||||||
import { combineReducers } from "redux";
|
import { combineReducers } from "redux";
|
||||||
|
|
||||||
import studentReducer from "./student"
|
import studentReducer from "./student"
|
||||||
|
import editionReducer from "@/state/reducer/edition";
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
student: studentReducer,
|
student: studentReducer,
|
||||||
|
edition: editionReducer,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AppState = ReturnType<typeof rootReducer>;
|
export type AppState = ReturnType<typeof rootReducer>;
|
||||||
|
@ -8,7 +8,8 @@ const store = createStore(
|
|||||||
persistReducer(
|
persistReducer(
|
||||||
{
|
{
|
||||||
key: 'state',
|
key: 'state',
|
||||||
storage: sessionStorage
|
storage: sessionStorage,
|
||||||
|
blacklist: ['edition']
|
||||||
},
|
},
|
||||||
rootReducer
|
rootReducer
|
||||||
),
|
),
|
||||||
|
@ -7,6 +7,14 @@ until: until {{ date }}
|
|||||||
late: late by {{ by }}
|
late: late by {{ by }}
|
||||||
left: '{{ left }} left'
|
left: '{{ left }} left'
|
||||||
|
|
||||||
|
student:
|
||||||
|
name: first name
|
||||||
|
surname: last name
|
||||||
|
course: course
|
||||||
|
semester: semester
|
||||||
|
email: e-mail
|
||||||
|
albumNumber: album number
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
my-internship:
|
my-internship:
|
||||||
header: "My internship"
|
header: "My internship"
|
||||||
@ -14,8 +22,21 @@ sections:
|
|||||||
steps:
|
steps:
|
||||||
personal-data:
|
personal-data:
|
||||||
header: "Fill personal data"
|
header: "Fill personal data"
|
||||||
|
info: >
|
||||||
|
Your profile is incomplete. In order to continue your internship you have to supply information given below. In
|
||||||
|
case of problem with providing those information - please contact with your internship coordinator of your course.
|
||||||
internship-proposal:
|
internship-proposal:
|
||||||
header: "Internship proposal"
|
header: "Internship proposal"
|
||||||
form: "Internship proposal form"
|
form: "Internship proposal form"
|
||||||
|
info: ""
|
||||||
plan:
|
plan:
|
||||||
header: "Individual Internship Plan"
|
header: "Individual Internship Plan"
|
||||||
|
info: ""
|
||||||
|
template: "Download template"
|
||||||
|
submit: "Submit Individual Internship Plan"
|
||||||
|
insurance:
|
||||||
|
header: "Insurance"
|
||||||
|
report:
|
||||||
|
header: "Internship report"
|
||||||
|
grade:
|
||||||
|
header: "Your grade"
|
||||||
|
@ -11,17 +11,36 @@ sections:
|
|||||||
my-internship:
|
my-internship:
|
||||||
header: "Moja praktyka"
|
header: "Moja praktyka"
|
||||||
|
|
||||||
|
student:
|
||||||
|
name: imię
|
||||||
|
surname: mazwisko
|
||||||
|
course: kierunek
|
||||||
|
semester: semestr
|
||||||
|
email: adres e-mail
|
||||||
|
albumNumber: numer albumu
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
personal-data:
|
personal-data:
|
||||||
header: "Uzupełnienie informacji"
|
header: "Uzupełnienie informacji"
|
||||||
|
info: >
|
||||||
|
Twój profil jest niekompletny. W celu kontynuacji praktyki musisz uzupełnić informacje podane poniżej. Jeżeli masz
|
||||||
|
problem z uzupełnieniem tych informacji - skontaktuj się z koordynatorem praktyk dla Twojego kierunku.
|
||||||
|
form: "Uzupełnij dane"
|
||||||
internship-proposal:
|
internship-proposal:
|
||||||
header: "Zgłoszenie praktyki"
|
header: "Zgłoszenie praktyki"
|
||||||
|
info: >
|
||||||
|
Przed podjęciem praktyki należy ją zgłosić.
|
||||||
form: "Formularz zgłaszania praktyki"
|
form: "Formularz zgłaszania praktyki"
|
||||||
plan:
|
plan:
|
||||||
header: "Indywidualny Program Praktyki"
|
header: "Indywidualny Program Praktyki"
|
||||||
|
info: ""
|
||||||
|
template: "Pobierz szablon"
|
||||||
|
submit: "Wyślij Indywidualny Plan Praktyki"
|
||||||
report:
|
report:
|
||||||
header: "Raport z praktyki"
|
header: "Raport z praktyki"
|
||||||
grade:
|
grade:
|
||||||
header: "Ocena z praktyki"
|
header: "Ocena z praktyki"
|
||||||
insurance:
|
insurance:
|
||||||
header: "Ubezpieczenie NWW"
|
header: "Ubezpieczenie NNW"
|
||||||
|
|
||||||
|
contact-coordinator: "Skontaktuj się z koordynatorem"
|
||||||
|
Loading…
Reference in New Issue
Block a user