diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/common')
-rw-r--r-- | Timeline/ClientApp/src/app/common/AlertHost.tsx | 96 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/AppBar.tsx | 107 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/BlobImage.tsx | 29 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/FileInput.tsx | 41 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/ImageCropper.tsx | 306 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/LoadingPage.tsx | 12 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/OperationDialog.tsx | 381 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/SearchInput.tsx | 63 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/TimelineLogo.tsx | 26 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx | 26 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/alert-service.ts | 61 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/alert.sass | 15 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/common/common.sass | 33 |
13 files changed, 0 insertions, 1196 deletions
diff --git a/Timeline/ClientApp/src/app/common/AlertHost.tsx b/Timeline/ClientApp/src/app/common/AlertHost.tsx deleted file mode 100644 index bfcf5c00..00000000 --- a/Timeline/ClientApp/src/app/common/AlertHost.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useCallback } from "react"; -import { Alert } from "reactstrap"; -import without from "lodash/without"; -import concat from "lodash/concat"; -import { useTranslation } from "react-i18next"; - -import { - alertService, - AlertInfoEx, - kAlertHostId, - AlertInfo, -} from "./alert-service"; - -interface AutoCloseAlertProps { - alert: AlertInfo; - close: () => void; -} - -export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { - const { alert } = props; - const { dismissTime } = alert; - - const { t } = useTranslation(); - - React.useEffect(() => { - const tag = - dismissTime === "never" - ? null - : typeof dismissTime === "number" - ? window.setTimeout(props.close, dismissTime) - : window.setTimeout(props.close, 5000); - return () => { - if (tag != null) { - window.clearTimeout(tag); - } - }; - }, [dismissTime, props.close]); - - return ( - <Alert className="m-3" color={alert.type ?? "primary"} toggle={props.close}> - {(() => { - const { message } = alert; - if (typeof message === "function") { - const Message = message; - return <Message />; - } else if (typeof message === "object" && message.type === "i18n") { - return t(message.key); - } else return alert.message; - })()} - </Alert> - ); -}; - -// oh what a bad name! -interface AlertInfoExEx extends AlertInfoEx { - close: () => void; -} - -const AlertHost: React.FC = () => { - const [alerts, setAlerts] = React.useState<AlertInfoExEx[]>([]); - - // react guarantee that state setters are stable, so we don't need to add it to dependency list - - const consume = useCallback((alert: AlertInfoEx): void => { - const alertEx: AlertInfoExEx = { - ...alert, - close: () => { - setAlerts((oldAlerts) => { - return without(oldAlerts, alertEx); - }); - }, - }; - setAlerts((oldAlerts) => { - return concat(oldAlerts, alertEx); - }); - }, []); - - React.useEffect(() => { - alertService.registerConsumer(consume); - return () => { - alertService.unregisterConsumer(consume); - }; - }, [consume]); - - return ( - <div id={kAlertHostId} className="alert-container"> - {alerts.map((alert) => { - return ( - <AutoCloseAlert key={alert.id} alert={alert} close={alert.close} /> - ); - })} - </div> - ); -}; - -export default AlertHost; diff --git a/Timeline/ClientApp/src/app/common/AppBar.tsx b/Timeline/ClientApp/src/app/common/AppBar.tsx deleted file mode 100644 index 59239696..00000000 --- a/Timeline/ClientApp/src/app/common/AppBar.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from "react"; -import { useHistory, matchPath } from "react-router"; -import { Link, NavLink } from "react-router-dom"; -import { Navbar, NavbarToggler, Collapse, Nav, NavItem } from "reactstrap"; -import { useMediaQuery } from "react-responsive"; -import { useTranslation } from "react-i18next"; - -import { useUser, useAvatar } from "../data/user"; - -import TimelineLogo from "./TimelineLogo"; -import BlobImage from "./BlobImage"; - -const AppBar: React.FC = (_) => { - const history = useHistory(); - const user = useUser(); - const avatar = useAvatar(user?.username); - - const { t } = useTranslation(); - - const isUpMd = useMediaQuery({ - minWidth: getComputedStyle(document.documentElement).getPropertyValue( - "--breakpoint-md" - ), - }); - - const [isMenuOpen, setIsMenuOpen] = React.useState(false); - - const toggleMenu = React.useCallback((): void => { - setIsMenuOpen((oldIsMenuOpen) => !oldIsMenuOpen); - }, []); - - const isAdministrator = user && user.administrator; - - const rightArea = ( - <div className="ml-auto mr-2"> - {user != null ? ( - <NavLink to={`/users/${user.username}`}> - <BlobImage - className="avatar small rounded-circle bg-white" - blob={avatar} - /> - </NavLink> - ) : ( - <NavLink className="text-light" to="/login"> - {t("nav.login")} - </NavLink> - )} - </div> - ); - - return ( - <Navbar dark className="fixed-top w-100 bg-primary app-bar" expand="md"> - <Link to="/" className="navbar-brand d-flex align-items-center"> - <TimelineLogo style={{ height: "1em" }} /> - Timeline - </Link> - - {isUpMd ? null : rightArea} - - <NavbarToggler onClick={toggleMenu} /> - <Collapse isOpen={isMenuOpen} navbar> - <Nav className="mr-auto" navbar> - <NavItem - className={ - matchPath(history.location.pathname, "/settings") - ? "active" - : undefined - } - > - <NavLink className="nav-link" to="/settings"> - {t("nav.settings")} - </NavLink> - </NavItem> - - <NavItem - className={ - matchPath(history.location.pathname, "/about") - ? "active" - : undefined - } - > - <NavLink className="nav-link" to="/about"> - {t("nav.about")} - </NavLink> - </NavItem> - - {isAdministrator && ( - <NavItem - className={ - matchPath(history.location.pathname, "/admin") - ? "active" - : undefined - } - > - <NavLink className="nav-link" to="/admin"> - Administration - </NavLink> - </NavItem> - )} - </Nav> - {isUpMd ? rightArea : null} - </Collapse> - </Navbar> - ); -}; - -export default AppBar; diff --git a/Timeline/ClientApp/src/app/common/BlobImage.tsx b/Timeline/ClientApp/src/app/common/BlobImage.tsx deleted file mode 100644 index 8602f550..00000000 --- a/Timeline/ClientApp/src/app/common/BlobImage.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; - -import { ExcludeKey } from "../utilities/type"; - -const BlobImage: React.FC< - ExcludeKey<React.ImgHTMLAttributes<HTMLImageElement>, "src"> & { - blob?: Blob | unknown; - } -> = (props) => { - const { blob, ...otherProps } = props; - - const [url, setUrl] = React.useState<string | undefined>(undefined); - - React.useEffect(() => { - if (blob instanceof Blob) { - const url = URL.createObjectURL(blob); - setUrl(url); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setUrl(undefined); - } - }, [blob]); - - return <img {...otherProps} src={url} />; -}; - -export default BlobImage; diff --git a/Timeline/ClientApp/src/app/common/FileInput.tsx b/Timeline/ClientApp/src/app/common/FileInput.tsx deleted file mode 100644 index 3d1bc2b3..00000000 --- a/Timeline/ClientApp/src/app/common/FileInput.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -import { ExcludeKey } from "../utilities/type"; - -export interface FileInputProps - extends ExcludeKey< - React.InputHTMLAttributes<HTMLInputElement>, - "type" | "id" - > { - inputId?: string; - labelText: string; - color?: string; - className?: string; -} - -const FileInput: React.FC<FileInputProps> = (props) => { - const { inputId, labelText, color, className, ...otherProps } = props; - - const realInputId = React.useMemo<string>(() => { - if (inputId != null) return inputId; - return ( - "file-input-" + - (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1) - ); - }, [inputId]); - - return ( - <> - <input className="d-none" type="file" id={realInputId} {...otherProps} /> - <label - htmlFor={realInputId} - className={clsx("btn", "btn-" + (color ?? "primary"), className)} - > - {labelText} - </label> - </> - ); -}; - -export default FileInput; diff --git a/Timeline/ClientApp/src/app/common/ImageCropper.tsx b/Timeline/ClientApp/src/app/common/ImageCropper.tsx deleted file mode 100644 index cd510969..00000000 --- a/Timeline/ClientApp/src/app/common/ImageCropper.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import * as React from "react"; -import clsx from "clsx"; - -import { UiLogicError } from "../common"; - -export interface Clip { - left: number; - top: number; - width: number; -} - -interface NormailizedClip extends Clip { - height: number; -} - -interface ImageInfo { - width: number; - height: number; - landscape: boolean; - ratio: number; - maxClipWidth: number; - maxClipHeight: number; -} - -interface ImageCropperSavedState { - clip: NormailizedClip; - x: number; - y: number; - pointerId: number; -} - -export interface ImageCropperProps { - clip: Clip | null; - imageUrl: string; - onChange: (clip: Clip) => void; - imageElementCallback?: (element: HTMLImageElement | null) => void; - className?: string; -} - -const ImageCropper = (props: ImageCropperProps): React.ReactElement => { - const { clip, imageUrl, onChange, imageElementCallback, className } = props; - - const [oldState, setOldState] = React.useState<ImageCropperSavedState | null>( - null - ); - const [imageInfo, setImageInfo] = React.useState<ImageInfo | null>(null); - - const normalizeClip = (c: Clip | null | undefined): NormailizedClip => { - if (c == null) { - return { left: 0, top: 0, width: 0, height: 0 }; - } - - return { - left: c.left || 0, - top: c.top || 0, - width: c.width || 0, - height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0, - }; - }; - - const c = normalizeClip(clip); - - const imgElementRef = React.useRef<HTMLImageElement | null>(null); - - const onImageRef = React.useCallback( - (e: HTMLImageElement | null) => { - imgElementRef.current = e; - if (imageElementCallback != null && e == null) { - imageElementCallback(null); - } - }, - [imageElementCallback] - ); - - const onImageLoad = React.useCallback( - (e: React.SyntheticEvent<HTMLImageElement>) => { - const img = e.currentTarget; - const landscape = img.naturalWidth >= img.naturalHeight; - - const info = { - width: img.naturalWidth, - height: img.naturalHeight, - landscape, - ratio: img.naturalHeight / img.naturalWidth, - maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1, - maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight, - }; - setImageInfo(info); - onChange({ left: 0, top: 0, width: info.maxClipWidth }); - if (imageElementCallback != null) { - imageElementCallback(img); - } - }, - [onChange, imageElementCallback] - ); - - const onPointerDown = React.useCallback( - (e: React.PointerEvent) => { - if (oldState != null) return; - e.currentTarget.setPointerCapture(e.pointerId); - setOldState({ - x: e.clientX, - y: e.clientY, - clip: c, - pointerId: e.pointerId, - }); - }, - [oldState, c] - ); - - const onPointerUp = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null || oldState.pointerId !== e.pointerId) return; - e.currentTarget.releasePointerCapture(e.pointerId); - setOldState(null); - }, - [oldState] - ); - - const onPointerMove = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null) return; - - const oldClip = oldState.clip; - - const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; - - const { current: imgElement } = imgElementRef; - - if (imgElement == null) throw new UiLogicError("Image element is null."); - - const moveRatio = { - x: movement.x / imgElement.width, - y: movement.y / imgElement.height, - }; - - const newRatio = { - x: oldClip.left + moveRatio.x, - y: oldClip.top + moveRatio.y, - }; - if (newRatio.x < 0) { - newRatio.x = 0; - } else if (newRatio.x > 1 - oldClip.width) { - newRatio.x = 1 - oldClip.width; - } - if (newRatio.y < 0) { - newRatio.y = 0; - } else if (newRatio.y > 1 - oldClip.height) { - newRatio.y = 1 - oldClip.height; - } - - onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width }); - }, - [oldState, onChange] - ); - - const onHandlerPointerMove = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null) return; - - const oldClip = oldState.clip; - - const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; - - const ratio = imageInfo == null ? 1 : imageInfo.ratio; - - const { current: imgElement } = imgElementRef; - - if (imgElement == null) throw new UiLogicError("Image element is null."); - - const moveRatio = { - x: movement.x / imgElement.width, - y: movement.x / imgElement.width / ratio, - }; - - const newRatio = { - x: oldClip.width + moveRatio.x, - y: oldClip.height + moveRatio.y, - }; - - const maxRatio = { - x: Math.min(1 - oldClip.left, newRatio.x), - y: Math.min(1 - oldClip.top, newRatio.y), - }; - - const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio); - - let newWidth; - if (newRatio.x < 0) { - newWidth = 0; - } else if (newRatio.x > maxWidthRatio) { - newWidth = maxWidthRatio; - } else { - newWidth = newRatio.x; - } - - onChange({ left: oldClip.left, top: oldClip.top, width: newWidth }); - }, - [imageInfo, oldState, onChange] - ); - - const toPercentage = (n: number): string => `${n}%`; - - // fuck!!! I just can't find a better way to implement this in pure css - const containerStyle: React.CSSProperties = (() => { - if (imageInfo == null) { - return { width: "100%", paddingTop: "100%", height: 0 }; - } else { - if (imageInfo.ratio > 1) { - return { - width: toPercentage(100 / imageInfo.ratio), - paddingTop: "100%", - height: 0, - }; - } else { - return { - width: "100%", - paddingTop: toPercentage(100 * imageInfo.ratio), - height: 0, - }; - } - } - })(); - - return ( - <div - className={clsx("image-cropper-container", className)} - style={containerStyle} - > - <img ref={onImageRef} src={imageUrl} onLoad={onImageLoad} alt="to crop" /> - <div className="image-cropper-mask-container"> - <div - className="image-cropper-mask" - touch-action="none" - style={{ - left: toPercentage(c.left * 100), - top: toPercentage(c.top * 100), - width: toPercentage(c.width * 100), - height: toPercentage(c.height * 100), - }} - onPointerMove={onPointerMove} - onPointerDown={onPointerDown} - onPointerUp={onPointerUp} - /> - </div> - <div - className="image-cropper-handler" - touch-action="none" - style={{ - left: `calc(${(c.left + c.width) * 100}% - 15px)`, - top: `calc(${(c.top + c.height) * 100}% - 15px)`, - }} - onPointerMove={onHandlerPointerMove} - onPointerDown={onPointerDown} - onPointerUp={onPointerUp} - /> - </div> - ); -}; - -export default ImageCropper; - -export function applyClipToImage( - image: HTMLImageElement, - clip: Clip, - mimeType: string -): Promise<Blob> { - return new Promise((resolve, reject) => { - const naturalSize = { - width: image.naturalWidth, - height: image.naturalHeight, - }; - const clipArea = { - x: naturalSize.width * clip.left, - y: naturalSize.height * clip.top, - length: naturalSize.width * clip.width, - }; - - const canvas = document.createElement("canvas"); - canvas.width = clipArea.length; - canvas.height = clipArea.length; - const context = canvas.getContext("2d"); - - if (context == null) throw new Error("Failed to create context."); - - context.drawImage( - image, - clipArea.x, - clipArea.y, - clipArea.length, - clipArea.length, - 0, - 0, - clipArea.length, - clipArea.length - ); - - canvas.toBlob((blob) => { - if (blob == null) { - reject(new Error("canvas.toBlob returns null")); - } else { - resolve(blob); - } - }, mimeType); - }); -} diff --git a/Timeline/ClientApp/src/app/common/LoadingPage.tsx b/Timeline/ClientApp/src/app/common/LoadingPage.tsx deleted file mode 100644 index a849126d..00000000 --- a/Timeline/ClientApp/src/app/common/LoadingPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { Spinner } from "reactstrap"; - -const LoadingPage: React.FC = () => { - return ( - <div className="position-fixed w-100 h-100 d-flex justify-content-center align-items-center"> - <Spinner style={{ height: "2.5rem", width: "2.5rem" }} color="primary" /> - </div> - ); -}; - -export default LoadingPage; diff --git a/Timeline/ClientApp/src/app/common/OperationDialog.tsx b/Timeline/ClientApp/src/app/common/OperationDialog.tsx deleted file mode 100644 index bca4580c..00000000 --- a/Timeline/ClientApp/src/app/common/OperationDialog.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - Spinner, - Container, - ModalBody, - Label, - Input, - FormGroup, - FormFeedback, - ModalFooter, - Button, - Modal, - ModalHeader, - FormText, -} from "reactstrap"; - -import { UiLogicError } from "../common"; - -const DefaultProcessPrompt: React.FC = (_) => { - return ( - <Container className="justify-content-center align-items-center"> - <Spinner /> - </Container> - ); -}; - -interface DefaultErrorPromptProps { - error?: string; -} - -const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => { - const { t } = useTranslation(); - - let result = <p className="text-danger">{t("operationDialog.error")}</p>; - - if (props.error != null) { - result = ( - <> - {result} - <p className="text-danger">{props.error}</p> - </> - ); - } - - return result; -}; - -export type OperationInputOptionalError = undefined | null | string; - -export interface OperationInputErrorInfo { - [index: number]: OperationInputOptionalError; -} - -export type OperationInputValidator<TValue> = ( - value: TValue, - values: (string | boolean)[] -) => OperationInputOptionalError | OperationInputErrorInfo; - -export interface OperationTextInputInfo { - type: "text"; - password?: boolean; - label?: string; - initValue?: string; - textFieldProps?: Omit< - React.InputHTMLAttributes<HTMLInputElement>, - "type" | "value" | "onChange" - >; - helperText?: string; - validator?: OperationInputValidator<string>; -} - -export interface OperationBoolInputInfo { - type: "bool"; - label: string; - initValue?: boolean; -} - -export interface OperationSelectInputInfoOption { - value: string; - label: string; - icon?: React.ReactElement; -} - -export interface OperationSelectInputInfo { - type: "select"; - label: string; - options: OperationSelectInputInfoOption[]; - initValue?: string; -} - -export type OperationInputInfo = - | OperationTextInputInfo - | OperationBoolInputInfo - | OperationSelectInputInfo; - -interface OperationResult { - type: "success" | "failure"; - data: unknown; -} - -interface OperationDialogProps { - open: boolean; - close: () => void; - title: React.ReactNode; - titleColor?: "default" | "dangerous" | "create" | string; - onProcess: (inputs: (string | boolean)[]) => Promise<unknown>; - inputScheme?: OperationInputInfo[]; - inputPrompt?: string | (() => React.ReactNode); - processPrompt?: () => React.ReactNode; - successPrompt?: (data: unknown) => React.ReactNode; - failurePrompt?: (error: unknown) => React.ReactNode; - onSuccessAndClose?: () => void; -} - -const OperationDialog: React.FC<OperationDialogProps> = (props) => { - const inputScheme = props.inputScheme ?? []; - - const { t } = useTranslation(); - - type Step = "input" | "process" | OperationResult; - const [step, setStep] = useState<Step>("input"); - const [values, setValues] = useState<(boolean | string)[]>( - inputScheme.map((i) => { - if (i.type === "bool") { - return i.initValue ?? false; - } else if (i.type === "text" || i.type === "select") { - return i.initValue ?? ""; - } else { - throw new UiLogicError("Unknown input scheme."); - } - }) - ); - const [inputError, setInputError] = useState<OperationInputErrorInfo>({}); - - const close = (): void => { - if (step !== "process") { - props.close(); - if ( - typeof step === "object" && - step.type === "success" && - props.onSuccessAndClose - ) { - props.onSuccessAndClose(); - } - } else { - console.log("Attempt to close modal when processing."); - } - }; - - const onConfirm = (): void => { - setStep("process"); - props.onProcess(values).then( - (d: unknown) => { - setStep({ - type: "success", - data: d, - }); - }, - (e: unknown) => { - setStep({ - type: "failure", - data: e, - }); - } - ); - }; - - let body: React.ReactNode; - if (step === "input") { - let inputPrompt = - typeof props.inputPrompt === "function" - ? props.inputPrompt() - : props.inputPrompt; - inputPrompt = <h6>{inputPrompt}</h6>; - - const updateValue = ( - index: number, - newValue: string | boolean - ): (string | boolean)[] => { - const oldValues = values; - const newValues = oldValues.slice(); - newValues[index] = newValue; - setValues(newValues); - return newValues; - }; - - const testErrorInfo = (errorInfo: OperationInputErrorInfo): boolean => { - for (let i = 0; i < inputScheme.length; i++) { - if (inputScheme[i].type === "text" && errorInfo[i] != null) { - return true; - } - } - return false; - }; - - const calculateError = ( - oldError: OperationInputErrorInfo, - index: number, - newError: OperationInputOptionalError | OperationInputErrorInfo - ): OperationInputErrorInfo => { - if (newError === undefined) { - return oldError; - } else if (newError === null || typeof newError === "string") { - return { ...oldError, [index]: newError }; - } else { - const newInputError: OperationInputErrorInfo = { ...oldError }; - for (const [index, error] of Object.entries(newError)) { - if (error !== undefined) { - newInputError[+index] = error as OperationInputOptionalError; - } - } - return newInputError; - } - }; - - const validateAll = (): boolean => { - let newInputError = inputError; - for (let i = 0; i < inputScheme.length; i++) { - const item = inputScheme[i]; - if (item.type === "text") { - newInputError = calculateError( - newInputError, - i, - item.validator?.(values[i] as string, values) - ); - } - } - const result = !testErrorInfo(newInputError); - setInputError(newInputError); - return result; - }; - - body = ( - <> - <ModalBody> - {inputPrompt} - {inputScheme.map((item, index) => { - const value = values[index]; - const error: string | undefined = ((e) => - typeof e === "string" ? t(e) : undefined)(inputError?.[index]); - - if (item.type === "text") { - return ( - <FormGroup key={index}> - {item.label && <Label>{t(item.label)}</Label>} - <Input - type={item.password === true ? "password" : "text"} - value={value as string} - onChange={(e) => { - const v = e.target.value; - const newValues = updateValue(index, v); - setInputError( - calculateError( - inputError, - index, - item.validator?.(v, newValues) - ) - ); - }} - invalid={error != null} - {...item.textFieldProps} - /> - {error != null && <FormFeedback>{error}</FormFeedback>} - {item.helperText && <FormText>{t(item.helperText)}</FormText>} - </FormGroup> - ); - } else if (item.type === "bool") { - return ( - <FormGroup check key={index}> - <Input - type="checkbox" - value={value as string} - onChange={(e) => { - updateValue( - index, - (e.target as HTMLInputElement).checked - ); - }} - /> - <Label check>{t(item.label)}</Label> - </FormGroup> - ); - } else if (item.type === "select") { - return ( - <FormGroup key={index}> - <Label>{t(item.label)}</Label> - <Input - type="select" - value={value as string} - onChange={(event) => { - updateValue(index, event.target.value); - }} - > - {item.options.map((option, i) => { - return ( - <option value={option.value} key={i}> - {option.icon} - {t(option.label)} - </option> - ); - })} - </Input> - </FormGroup> - ); - } - })} - </ModalBody> - <ModalFooter> - <Button color="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button - color="primary" - disabled={testErrorInfo(inputError)} - onClick={() => { - if (validateAll()) { - onConfirm(); - } - }} - > - {t("operationDialog.confirm")} - </Button> - </ModalFooter> - </> - ); - } else if (step === "process") { - body = ( - <ModalBody> - {props.processPrompt?.() ?? <DefaultProcessPrompt />} - </ModalBody> - ); - } else { - let content: React.ReactNode; - const result = step; - if (result.type === "success") { - content = - props.successPrompt?.(result.data) ?? t("operationDialog.success"); - if (typeof content === "string") - content = <p className="text-success">{content}</p>; - } else { - content = props.failurePrompt?.(result.data) ?? <DefaultErrorPrompt />; - if (typeof content === "string") - content = <DefaultErrorPrompt error={content} />; - } - body = ( - <> - <ModalBody>{content}</ModalBody> - <ModalFooter> - <Button color="primary" onClick={close}> - {t("operationDialog.ok")} - </Button> - </ModalFooter> - </> - ); - } - - const title = typeof props.title === "string" ? t(props.title) : props.title; - - return ( - <Modal isOpen={props.open} toggle={close}> - <ModalHeader - className={ - props.titleColor != null - ? "text-" + - (props.titleColor === "create" - ? "success" - : props.titleColor === "dangerous" - ? "danger" - : props.titleColor) - : undefined - } - > - {title} - </ModalHeader> - {body} - </Modal> - ); -}; - -export default OperationDialog; diff --git a/Timeline/ClientApp/src/app/common/SearchInput.tsx b/Timeline/ClientApp/src/app/common/SearchInput.tsx deleted file mode 100644 index 5a0b0eaa..00000000 --- a/Timeline/ClientApp/src/app/common/SearchInput.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useCallback } from "react"; -import clsx from "clsx"; -import { Spinner, Input, Button } from "reactstrap"; -import { useTranslation } from "react-i18next"; - -export interface SearchInputProps { - value: string; - onChange: (value: string) => void; - onButtonClick: () => void; - className?: string; - loading?: boolean; - buttonText?: string; - placeholder?: string; - additionalButton?: React.ReactNode; -} - -const SearchInput: React.FC<SearchInputProps> = (props) => { - const { onChange, onButtonClick } = props; - - const { t } = useTranslation(); - - const onInputChange = useCallback( - (event: React.ChangeEvent<HTMLInputElement>): void => { - onChange(event.currentTarget.value); - }, - [onChange] - ); - - const onInputKeyPress = useCallback( - (event: React.KeyboardEvent<HTMLInputElement>): void => { - if (event.key === "Enter") { - onButtonClick(); - } - }, - [onButtonClick] - ); - - return ( - <div className={clsx("form-inline my-2", props.className)}> - <Input - className="mr-sm-2" - value={props.value} - onChange={onInputChange} - onKeyPress={onInputKeyPress} - placeholder={props.placeholder} - /> - <div className="mt-2 mt-sm-0 order-sm-last ml-sm-3"> - {props.additionalButton} - </div> - <div className="mt-2 mt-sm-0 ml-auto ml-sm-0"> - {props.loading ? ( - <Spinner /> - ) : ( - <Button outline color="primary" onClick={props.onButtonClick}> - {props.buttonText ?? t("search")} - </Button> - )} - </div> - </div> - ); -}; - -export default SearchInput; diff --git a/Timeline/ClientApp/src/app/common/TimelineLogo.tsx b/Timeline/ClientApp/src/app/common/TimelineLogo.tsx deleted file mode 100644 index 27d188fc..00000000 --- a/Timeline/ClientApp/src/app/common/TimelineLogo.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { SVGAttributes } from "react"; - -export interface TimelineLogoProps extends SVGAttributes<SVGElement> { - color?: string; -} - -const TimelineLogo: React.FC<TimelineLogoProps> = (props) => { - const { color, ...forwardProps } = props; - const coercedColor = color ?? "currentcolor"; - return ( - <svg - className={props.className} - viewBox="0 0 100 100" - fill="none" - strokeWidth="12" - stroke={coercedColor} - {...forwardProps} - > - <line x1="50" y1="0" x2="50" y2="25" /> - <circle cx="50" cy="50" r="22" /> - <line x1="50" y1="75" x2="50" y2="100" /> - </svg> - ); -}; - -export default TimelineLogo; diff --git a/Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx b/Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx deleted file mode 100644 index 29f6a69f..00000000 --- a/Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { SVGAttributes } from "react"; - -export interface UserTimelineLogoProps extends SVGAttributes<SVGElement> { - color?: string; -} - -const UserTimelineLogo: React.FC<UserTimelineLogoProps> = (props) => { - const { color, ...forwardProps } = props; - const coercedColor = color ?? "currentcolor"; - - return ( - <svg viewBox="0 0 100 100" {...forwardProps}> - <g fill="none" stroke={coercedColor} strokeWidth="12"> - <line x1="50" x2="50" y1="0" y2="25" /> - <circle cx="50" cy="50" r="22" /> - <line x1="50" x2="50" y1="75" y2="100" /> - </g> - <g fill={color}> - <circle cx="85" cy="75" r="10" /> - <path d="m70,100c0,0 15,-30 30,0.25" /> - </g> - </svg> - ); -}; - -export default UserTimelineLogo; diff --git a/Timeline/ClientApp/src/app/common/alert-service.ts b/Timeline/ClientApp/src/app/common/alert-service.ts deleted file mode 100644 index e4c0e653..00000000 --- a/Timeline/ClientApp/src/app/common/alert-service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; -import pull from "lodash/pull"; - -export interface AlertInfo { - type?: "primary" | "secondary" | "success" | "danger" | "warning" | "info"; - message: string | React.FC<unknown> | { type: "i18n"; key: string }; - dismissTime?: number | "never"; -} - -export interface AlertInfoEx extends AlertInfo { - id: number; -} - -export type AlertConsumer = (alerts: AlertInfoEx) => void; - -export class AlertService { - private consumers: AlertConsumer[] = []; - private savedAlerts: AlertInfoEx[] = []; - private currentId = 1; - - private produce(alert: AlertInfoEx): void { - for (const consumer of this.consumers) { - consumer(alert); - } - } - - registerConsumer(consumer: AlertConsumer): void { - this.consumers.push(consumer); - if (this.savedAlerts.length !== 0) { - for (const alert of this.savedAlerts) { - this.produce(alert); - } - this.savedAlerts = []; - } - } - - unregisterConsumer(consumer: AlertConsumer): void { - pull(this.consumers, consumer); - } - - push(alert: AlertInfo): void { - const newAlert: AlertInfoEx = { ...alert, id: this.currentId++ }; - if (this.consumers.length === 0) { - this.savedAlerts.push(newAlert); - } else { - this.produce(newAlert); - } - } -} - -export const alertService = new AlertService(); - -export function pushAlert(alert: AlertInfo): void { - alertService.push(alert); -} - -export const kAlertHostId = "alert-host"; - -export function getAlertHost(): HTMLElement | null { - return document.getElementById(kAlertHostId); -} diff --git a/Timeline/ClientApp/src/app/common/alert.sass b/Timeline/ClientApp/src/app/common/alert.sass deleted file mode 100644 index 5b6e65c2..00000000 --- a/Timeline/ClientApp/src/app/common/alert.sass +++ /dev/null @@ -1,15 +0,0 @@ -.alert-container - position: fixed - z-index: $zindex-popover - -@include media-breakpoint-up(sm) - .alert-container - bottom: 0 - right: 0 - -@include media-breakpoint-down(sm) - .alert-container - bottom: 0 - right: 0 - left: 0 - text-align: center diff --git a/Timeline/ClientApp/src/app/common/common.sass b/Timeline/ClientApp/src/app/common/common.sass deleted file mode 100644 index 15d34d7c..00000000 --- a/Timeline/ClientApp/src/app/common/common.sass +++ /dev/null @@ -1,33 +0,0 @@ -.image-cropper-container - position: relative - box-sizing: border-box - user-select: none - -.image-cropper-container img - position: absolute - left: 0 - top: 0 - width: 100% - height: 100% - -.image-cropper-mask-container - position: absolute - left: 0 - top: 0 - right: 0 - bottom: 0 - overflow: hidden - -.image-cropper-mask - position: absolute - box-shadow: 0 0 0 10000px rgba(255, 255, 255, 80%) - touch-action: none - -.image-cropper-handler - position: absolute - width: 26px - height: 26px - border: black solid 2px - border-radius: 50% - background: white - touch-action: none |