diff --git a/src/components/actions.tsx b/src/components/actions.tsx index 79ce5e2..310b241 100644 --- a/src/components/actions.tsx +++ b/src/components/actions.tsx @@ -1,8 +1,12 @@ import React, { HTMLProps } from "react"; import { useHorizontalSpacing } from "@/styles"; -export const Actions = (props: HTMLProps) => { - const classes = useHorizontalSpacing(2); +type ActionsProps = { + spacing?: number; +} & HTMLProps; - return
+export const Actions = ({ spacing = 2, ...props }: ActionsProps) => { + const classes = useHorizontalSpacing(spacing); + + return
} diff --git a/src/components/async.tsx b/src/components/async.tsx index 6b90ce6..f99fbca 100644 --- a/src/components/async.tsx +++ b/src/components/async.tsx @@ -9,21 +9,22 @@ type AsyncProps = { children: (value: TValue) => JSX.Element, loading?: () => JSX.Element, error?: (error: TError) => JSX.Element, + keepValue?: boolean; } const defaultLoading = () => ; const defaultError = (error: any) => { error.message }; export function Async( - { async, children: render, loading = defaultLoading, error = defaultError }: AsyncProps + { async, children: render, loading = defaultLoading, error = defaultError, keepValue = false }: AsyncProps ) { - 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(); } diff --git a/src/components/confirm.tsx b/src/components/confirm.tsx new file mode 100644 index 0000000..7a8cf42 --- /dev/null +++ b/src/components/confirm.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import React from "react"; +import { createPortal } from "react-dom"; +import { Button, 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, +} + +export function Confirm({ children, title, content, onConfirm, onCancel }: ConfirmProps) { + const [ open, setOpen ] = useState(false); + const { t } = useTranslation(); + + const handleCancel = () => { + setOpen(false); + onCancel?.(); + } + + const handleConfirm = () => { + setOpen(false); + onConfirm?.(); + } + + return <> + { children(() => { setOpen(true) }) } + { createPortal( + + { title && { title }} + + { content || t('confirmation') } + + + + + + , + document.getElementById("modals") as Element, + ) } + +} diff --git a/src/helpers.ts b/src/helpers.ts index 34dae24..a37de8c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -26,3 +26,19 @@ export function throttle(decorated: (...args: TArgs) => voi }, time); } } + +export function encapsulate(value: T|T[]): T[] { + if (value instanceof Array) { + return value; + } + + return [ value ]; +} + +export function one(value: T|T[]): T { + if (value instanceof Array) { + return value[0]; + } + + return value; +} diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index d7fbf9b..775d8fc 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -19,7 +19,6 @@ export function useAsync(supplier: Promise | (() => Promise< useEffect(() => { setLoading(true); setError(undefined); - setValue(undefined); const myMagicNumber = semaphore.value + 1; semaphore.value = myMagicNumber; @@ -54,9 +53,9 @@ export function useAsync(supplier: Promise | (() => Promise< }; } -export function useAsyncState(initial: Promise | undefined): AsyncState { +export function useAsyncState(initial?: Promise | undefined): AsyncState { const [promise, setPromise] = useState | undefined>(initial); - const asyncState = useAsync(promise); + const asyncState = useAsync(promise); return [ asyncState, setPromise ]; } diff --git a/src/management/api/page.ts b/src/management/api/page.ts index 155d607..c615845 100644 --- a/src/management/api/page.ts +++ b/src/management/api/page.ts @@ -1,6 +1,8 @@ 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"; const STATIC_PAGE_INDEX_ENDPOINT = "/staticPage"; @@ -10,3 +12,16 @@ export async function all(): Promise { const response = await axios.get(STATIC_PAGE_INDEX_ENDPOINT); return response.data.map(dto => pageDtoTransformer.transform(dto)); } + +export async function remove(page: Pick): Promise { + await axios.delete(prepare(STATIC_PAGE_ENDPOINT, { slug: page.slug })); +} + +export async function save(page: Page): Promise { + const response = await axios.put( + STATIC_PAGE_INDEX_ENDPOINT, + pageDtoTransformer.reverseTransform(page), + ); + + return pageDtoTransformer.transform(response.data); +} diff --git a/src/management/edition/list.tsx b/src/management/edition/list.tsx index 846ef35..535450b 100644 --- a/src/management/edition/list.tsx +++ b/src/management/edition/list.tsx @@ -1,7 +1,7 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect } from "react"; import { Page } from "@/pages/base"; import { useTranslation } from "react-i18next"; -import { useAsync } from "@/hooks"; +import { useAsync, useAsyncState } from "@/hooks"; import api from "@/management/api"; import { Async } from "@/components/async"; import { Container, Typography } from "@material-ui/core"; @@ -9,6 +9,8 @@ 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 { CreateStaticPageDialog } from "@/management/page/create"; export type EditionDetailsProps = { edition: string; diff --git a/src/management/page/create.tsx b/src/management/page/create.tsx new file mode 100644 index 0000000..59c3870 --- /dev/null +++ b/src/management/page/create.tsx @@ -0,0 +1,40 @@ +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 CreateStaticPageDialogProps = { + onSave?: (page: StaticPage) => void; +} & DialogProps; + +export function CreateStaticPageDialog({ onSave, ...props }: CreateStaticPageDialogProps) { + const { t } = useTranslation("management"); + const spacing = useSpacing(3); + + const handleSubmit = (values: StaticPageFormValues) => { + onSave?.(staticPageFormValuesTransformer.reverseTransform(values)); + }; + + return + +
+ { t("page.create.title") } + + + + + + + + + +
+
+
+} diff --git a/src/management/page/form.tsx b/src/management/page/form.tsx new file mode 100644 index 0000000..c0dfb3a --- /dev/null +++ b/src/management/page/form.tsx @@ -0,0 +1,39 @@ +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"; + +export type StaticPageFormValues = StaticPage; + +export const initialStaticPageFormValues: StaticPageFormValues = { + slug: "", + title: { + en: "", + pl: "", + }, + content: { + en: "", + pl: "", + } +} + +export const staticPageFormValuesTransformer: Transformer = identityTransformer; + +export function StaticPageForm() { + const { t } = useTranslation("management"); + const spacing = useSpacing(2); + + return
+ + { t("page.field.title") } + + + { t("page.field.content") } + + +
+} diff --git a/src/management/page/list.tsx b/src/management/page/list.tsx new file mode 100644 index 0000000..8d21234 --- /dev/null +++ b/src/management/page/list.tsx @@ -0,0 +1,137 @@ +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 } from "@/helpers"; +import { Actions } from "@/components"; +import { useSpacing } from "@/styles"; +import { useHistory } from "react-router-dom"; +import { Add } from "@material-ui/icons"; +import { createPortal } from "react-dom"; +import { CreateStaticPageDialog } from "@/management/page/create"; +import { Confirm } from "@/components/confirm"; + +export const StaticPageManagement = () => { + const { t } = useTranslation("management"); + const [ result, setPagesPromise ] = useAsyncState(); + const spacing = useSpacing(2); + + const updatePageList = () => { + setPagesPromise(api.page.all()); + } + + useEffect(updatePageList, []); + + const CreateStaticPageAction = () => { + const [ open, setOpen ] = useState(false); + + const handlePageCreation = async (page: StaticPage) => { + await api.page.save(page); + setOpen(false); + updatePageList(); + } + + return <> + + { createPortal( + setOpen(false) }/>, + document.getElementById("modals") as Element + ) } + + } + + const DeleteStaticPageAction = ({ page }: { page: StaticPage }) => { + const handlePageDeletion = async () => { + await api.page.remove(page); + updatePageList(); + } + + const confirmation = <> + + Czy na pewno chcesz usunąć stronę { page.title.pl }? + + ; + + return + { action => } + ; + } + + const PreviewStaticPageAction = ({ page }: { page: StaticPage }) => { + const history = useHistory(); + const handlePagePreview = async () => history.push(`/${page.slug}`); + + return + + ; + } + + const columns: Column[] = [ + { + render: page => page.title.pl, + title: t("page.field.title"), + }, + { + field: "slug", + title: t("page.field.slug"), + }, + { + title: t("actions.label"), + render: page => + + + , + sorting: false, + width: 0, + resizable: false, + removable: false, + searchable: false, + }, + ]; + + const PagePreview = ({ page }: { page: StaticPage }) => + +
+ Polski + { page.title.pl } +
+
+
+ English + { page.title.en } +
+
+ + + return + + + { t("page.index.title") } + + { t("page.index.title") } + + + + + + + { + pages => { t("page.index.title") } { result.isLoading && }
} + columns={ columns } + data={ pages } + detailPanel={ page => } + /> + } + + +} + +export default StaticPageManagement; diff --git a/src/management/pages/list.tsx b/src/management/pages/list.tsx deleted file mode 100644 index 3a2648a..0000000 --- a/src/management/pages/list.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Page } from "@/pages/base"; -import { Management } from "@/management/main"; -import { Box, Container, Typography } from "@material-ui/core"; -import React, { useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { useAsync } 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 { Edition } from "@/data/edition"; -import { Delete, Pencil } from "mdi-material-ui"; - -export const StaticPageManagement = () => { - const { t } = useTranslation("management"); - const pages = useAsync(useCallback(api.page.all, [])); - - const columns: Column[] = [ - { - render: page => page.title.pl, - title: t("page.field.title"), - }, - { - field: "slug", - title: t("page.field.slug"), - }, - ]; - - const actions: Action[] = [ - { - icon: () => , - onClick: () => {}, - }, - { - icon: () => , - onClick: () => {}, - }, - ] - - const PagePreview = ({ page }: { page: StaticPage }) => - <> - - Polski - { page.title.pl } -
- - - English - { page.title.en } -
- - - - return - - - { t("page.index.title") } - - { t("page.index.title") } - - - { - pages => } - options={{ actionsColumnIndex: -1 }} - /> - } - - -} - -export default StaticPageManagement; diff --git a/src/management/routing.tsx b/src/management/routing.tsx index bbdc7b8..6c02c77 100644 --- a/src/management/routing.tsx +++ b/src/management/routing.tsx @@ -3,7 +3,7 @@ import { isManagerMiddleware } from "@/management/middleware"; import { EditionsManagement } from "@/management/edition/list"; import React from "react"; import { ManagementIndex } from "@/management/main"; -import StaticPageManagement from "@/management/pages/list"; +import StaticPageManagement from "@/management/page/list"; export const managementRoutes: Route[] = ([ { name: "index", path: "/", content: ManagementIndex, exact: true }, diff --git a/src/serialization/types.ts b/src/serialization/types.ts index 424786d..23b1ae1 100644 --- a/src/serialization/types.ts +++ b/src/serialization/types.ts @@ -19,3 +19,8 @@ export type SerializationTransformer> = Transfo export type OneWayTransformer = { transform(subject: TFrom, context?: TContext): TResult; } + +export const identityTransformer: Transformer = { + transform: subject => subject, + reverseTransform: subject => subject +} diff --git a/translations/management.pl.yaml b/translations/management.pl.yaml index 83e57f7..7eb655c 100644 --- a/translations/management.pl.yaml +++ b/translations/management.pl.yaml @@ -1,5 +1,15 @@ title: Zarządzanie +create: utwórz +refresh: $t(translation:refresh) +save: zapisz +cancel: anuluj + +actions: + label: Akcje + preview: Podgląd + delete: Usuń + edition: index: title: "Edycje praktyk" @@ -16,3 +26,5 @@ page: title: Tytuł content: Treść slug: Adres + create: + title: Utwórz stronę statyczną diff --git a/translations/pl.yaml b/translations/pl.yaml index 020723c..187d1e0 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -211,6 +211,10 @@ 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 }}. @@ -224,3 +228,4 @@ validation: contact-coordinator: "Skontaktuj się z koordynatorem" download: "pobierz" management: "zarządzanie" +refresh: "odśwież"