diff --git a/package.json b/package.json index df24a9f..78023c1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "css-loader": "3.4.2", "date-holidays": "^1.5.3", "file-loader": "4.3.0", + "filesize": "^6.1.0", "formik": "^2.1.5", "formik-material-ui": "^3.0.0-alpha.0", "html-webpack-plugin": "4.0.0-beta.11", diff --git a/src/api/upload.ts b/src/api/upload.ts index 81fa7b1..4657fce 100644 --- a/src/api/upload.ts +++ b/src/api/upload.ts @@ -1,6 +1,7 @@ import { axios } from "@/api/index"; import { InternshipDocument } from "@/api/dto/internship-registration"; import { prepare } from "@/routing"; +import { Identifiable } from "@/data"; export enum UploadType { Ipp = "IppScan", @@ -8,6 +9,12 @@ export enum UploadType { Insurance = "NnwInsurance", } +export interface DocumentFileInfo extends Identifiable { + filename: string; + size: number; + mime: string; +} + const CREATE_DOCUMENT_ENDPOINT = '/document'; const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan'; @@ -23,3 +30,8 @@ export async function upload(document: InternshipDocument, file: File) { const response = await axios.put(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }), data); return true; } + +export async function fileinfo(document: InternshipDocument): Promise { + const response = await axios.get(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string })); + return response.data; +} diff --git a/src/app.tsx b/src/app.tsx index a3bb940..79092b4 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -62,7 +62,7 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps) function App() { const dispatch = useDispatch(); - const edition = useSelector(state => state.edition); + const edition = useSelector(state => state.edition as any); const { t } = useTranslation(); const locale = useSelector(state => getLocale(state.settings)); @@ -73,27 +73,29 @@ function App() { }, [ locale ]) return <> -
- -
- - -
+
+ + +
+ + +
+
{ diff --git a/src/components/async.tsx b/src/components/async.tsx new file mode 100644 index 0000000..c68d16d --- /dev/null +++ b/src/components/async.tsx @@ -0,0 +1,28 @@ +import { AsyncResult } from "@/hooks"; +import React from "react"; +import { CircularProgress } from "@material-ui/core"; +import { Alert } from "@material-ui/lab"; + +type AsyncProps = { + async: AsyncResult, + children: (value: TValue) => JSX.Element, + loading?: () => JSX.Element, + error?: (error: TError) => JSX.Element, +} + +const defaultLoading = () => ; +const defaultError = (error: any) => { error.message }; + +export function Async( + { async, children: render, loading = defaultLoading, error = defaultError }: AsyncProps +) { + if (async.isLoading || (!async.error && !async.value)) { + return loading(); + } + + if (typeof async.error !== "undefined") { + return error(async.error); + } + + return render(async.value as TValue); +} diff --git a/src/components/fileinfo.tsx b/src/components/fileinfo.tsx new file mode 100644 index 0000000..544baee --- /dev/null +++ b/src/components/fileinfo.tsx @@ -0,0 +1,93 @@ +import { InternshipDocument } from "@/api/dto/internship-registration"; +import { useAsync } from "@/hooks"; +import { DocumentFileInfo } from "@/api/upload"; +import React, { useCallback } from "react"; +import api from "@/api"; +import { Async } from "@/components/async"; +import { Button, Grid, Paper, PaperProps, SvgIconProps, Theme, Typography } from "@material-ui/core"; +import { makeStyles, createStyles } from "@material-ui/core/styles"; +import filesize from "filesize"; +import { Actions } from "@/components/actions"; +import { useTranslation } from "react-i18next"; +import { FileDownloadOutline, FileOutline, FileImageOutline, FilePdfOutline, FileWordOutline } from "mdi-material-ui"; +import classNames from "classnames"; + +const useStyles = makeStyles((theme: Theme) => createStyles({ + root: { + padding: theme.spacing(2), + backgroundColor: "#e9f0f5", + }, + header: { + color: theme.palette.primary.dark, + }, + download: { + color: theme.palette.primary.dark, + }, + actions: { + marginTop: theme.spacing(2), + }, + icon: { + fontSize: "6rem", + margin: "0 auto", + }, + grid: { + display: "flex", + alignItems: "center", + }, + iconColumn: { + flex: "0 1 auto", + marginRight: "1rem", + color: theme.palette.primary.dark + "af", + }, + asideColumn: { + flex: "1 1 auto" + } +})) + +export type FileInfoProps = { + document: InternshipDocument +} & PaperProps; + +export type FileIconProps = { + mime: string; +} & SvgIconProps; + +export function FileIcon({ mime, ...props }: FileIconProps) { + switch (true) { + case ["application/pdf", "application/x-pdf"].includes(mime): + return + case mime === "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + return + case mime.startsWith("image/"): + return + default: + return + } +} + +export const FileInfo = ({ document, ...props }: FileInfoProps) => { + const fileinfo = useAsync(useCallback(() => api.upload.fileinfo(document), [document.id])); + const classes = useStyles(); + + const { t } = useTranslation(); + + return + + { fileinfo =>
+
+ +
+ +
} +
+
+} diff --git a/src/forms/company.tsx b/src/forms/company.tsx index 246b1c2..d4896fd 100644 --- a/src/forms/company.tsx +++ b/src/forms/company.tsx @@ -194,7 +194,7 @@ export const CompanyForm: React.FunctionComponent = () => { return ( <> - + typeof option === "string" ? option : option.name } renderOption={ company => } diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index c36b3a3..9f591dc 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -141,7 +141,6 @@ const InternshipDurationForm = () => { format="DD MMMM yyyy" disableToolbar fullWidth variant="inline" label={ t("forms.internship.fields.start-date") } - minDate={ moment() } /> @@ -149,7 +148,7 @@ const InternshipDurationForm = () => { format="DD MMMM yyyy" disableToolbar fullWidth variant="inline" label={ t("forms.internship.fields.end-date") } - minDate={ startDate || moment() } + minDate={ startDate } /> @@ -252,12 +251,8 @@ export const InternshipForm: React.FunctionComponent = () => { }); const edition = useCurrentEdition(); - const { t } = useTranslation(); - const dispatch = useDispatch(); - const history = useHistory(); - const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const validationSchema = Yup.object>({ diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index b7f2021..57e560d 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -41,19 +41,19 @@ export const PlanForm = () => { } return - + { t('forms.plan.instructions') } - + - + setFile(files[0]) }/> { t('forms.plan.dropzone-help') } - + }> Powyższe dane nie są poprawne? diff --git a/src/forms/user.tsx b/src/forms/user.tsx index 9b450bc..cdf1e7f 100644 --- a/src/forms/user.tsx +++ b/src/forms/user.tsx @@ -10,6 +10,8 @@ import { Actions } from "@/components"; import { Nullable } from "@/helpers"; import * as Yup from "yup"; import { StudentActions, useDispatch } from "@/state/actions"; +import { route } from "@/routing"; +import { useHistory } from "react-router-dom"; interface StudentFormValues { firstName: string; @@ -48,6 +50,7 @@ const studentToFormValuesTransformer: Transformer, StudentForm export const StudentForm = ({ student }: StudentFormProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); + const history = useHistory(); const validationSchema = useMemo(() => Yup.object({ semester: Yup.number().required().min(1).max(10), @@ -71,6 +74,8 @@ export const StudentForm = ({ student }: StudentFormProps) => { type: StudentActions.Set, student: updated, }) + + history.push(route("home")); } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 6141ecd..c6b91a5 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -36,6 +36,10 @@ export const updateInternshipInfo = async (dispatch: AppDispatch) => { document: plan, state: plan.state, }) + } else { + dispatch({ + type: InternshipPlanActions.Reset, + }) } } diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index e6f6c4c..1e885ce 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -12,13 +12,14 @@ import { Alert, AlertTitle } from "@material-ui/lab"; import { ContactAction, Status } from "@/pages/steps/common"; import { Description as DescriptionIcon } from "@material-ui/icons"; import { useDeadlines } from "@/hooks"; +import { InternshipDocument } from "@/api/dto/internship-registration"; +import { FileInfo } from "@/components/fileinfo"; +import { useSpacing } from "@/styles"; const PlanActions = () => { const status = useSelector(state => getSubmissionStatus(state.plan)); - const { t } = useTranslation(); - const ReviewAction = (props: ButtonProps) => - + const { t } = useTranslation(); const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) => diff --git a/src/state/actions/plan.ts b/src/state/actions/plan.ts index 90a0cdb..cafc834 100644 --- a/src/state/actions/plan.ts +++ b/src/state/actions/plan.ts @@ -7,6 +7,7 @@ import { } from "@/state/actions/submission"; import { InternshipDocument, SubmissionState } from "@/api/dto/internship-registration"; +import { Action } from "@/state/actions/base"; export enum InternshipPlanActions { Send = "SEND_PLAN", @@ -14,8 +15,11 @@ export enum InternshipPlanActions { Approve = "RECEIVE_PLAN_APPROVE", Decline = "RECEIVE_PLAN_DECLINE", Receive = "RECEIVE_PLAN_STATE", + Reset = "RESET_PLAN", } +export interface ResetPlanAction extends Action {} + export interface SendPlanAction extends SendSubmissionAction { document: InternshipDocument; } @@ -40,4 +44,6 @@ export type InternshipPlanAction | SavePlanAction | ReceivePlanApproveAction | ReceivePlanDeclineAction - | ReceivePlanUpdateAction; + | ReceivePlanUpdateAction + | ResetPlanAction + ; diff --git a/src/state/reducer/plan.ts b/src/state/reducer/plan.ts index b89f661..7d27faf 100644 --- a/src/state/reducer/plan.ts +++ b/src/state/reducer/plan.ts @@ -1,4 +1,4 @@ -import { InternshipPlanAction, InternshipPlanActions, InternshipProposalActions } from "@/state/actions"; +import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions"; import { Serializable } from "@/serialization/types"; import { createSubmissionReducer, @@ -51,6 +51,9 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla document: action.document, } + case InternshipPlanActions.Reset: + return defaultInternshipPlanState; + default: return state; } diff --git a/src/styles/header.scss b/src/styles/header.scss index 60f2400..d05d182 100644 --- a/src/styles/header.scss +++ b/src/styles/header.scss @@ -1,9 +1,12 @@ @import "variables"; -.header { - height: 110px; +header { background: $main; +} + +.header { display: flex; + height: 110px; color: white; } diff --git a/src/styles/spacing.ts b/src/styles/spacing.ts index dc8863d..b6a3339 100644 --- a/src/styles/spacing.ts +++ b/src/styles/spacing.ts @@ -17,3 +17,16 @@ export const useHorizontalSpacing = makeStyles(theme => createStyles({ } } })) + +export const useSpacing = makeStyles(theme => createStyles({ + horizontal: { + "& > *:not(:last-child)": { + marginRight: (spacing: number = defaultSpacing) => theme.spacing(spacing) + } + }, + vertical: { + "& > *:not(:last-child)": { + marginBottom: (spacing: number = defaultSpacing) => theme.spacing(spacing) + } + } +})) diff --git a/src/ui/theme.ts b/src/ui/theme.ts index cf65aad..9a1f07b 100644 --- a/src/ui/theme.ts +++ b/src/ui/theme.ts @@ -4,7 +4,6 @@ export const studentTheme = responsiveFontSizes(createMuiTheme({ props: { MuiGrid: { spacing: 3, - xs: 12, }, MuiContainer: { maxWidth: "md" diff --git a/translations/pl.yaml b/translations/pl.yaml index 4ec3a5c..e46a78a 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -200,3 +200,4 @@ validation: minimum-hours: "Minimalna liczba godzin wynosi {{ hours }}" contact-coordinator: "Skontaktuj się z koordynatorem" +download: "pobierz" diff --git a/yarn.lock b/yarn.lock index e5383ca..281e4cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3858,6 +3858,11 @@ filesize@6.0.1: resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.0.1.tgz#f850b509909c7c86f7e450ea19006c31c2ed3d2f" integrity sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg== +filesize@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" + integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"