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/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 |
12 files changed, 0 insertions, 1167 deletions
diff --git a/Timeline/ClientApp/src/app/common/AlertHost.tsx b/Timeline/ClientApp/src/app/common/AlertHost.tsx deleted file mode 100644 index 23b6c5f4..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 { - alertService, - AlertInfoEx, - kAlertHostId, - AlertInfo, -} from './alert-service'; -import { useTranslation } from 'react-i18next'; - -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; -} - -export 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 f75fe08f..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 } from '../data/user'; -import { useOptionalVersionedAvatarUrl } from '../user/api'; - -import TimelineLogo from './TimelineLogo'; - -const AppBar: React.FC = (_) => { - const history = useHistory(); - const user = useUser(); - const avatarUrl = useOptionalVersionedAvatarUrl(user?._links?.avatar); - - 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}`}> - <img - className="avatar small rounded-circle bg-white" - src={avatarUrl} - /> - </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/FileInput.tsx b/Timeline/ClientApp/src/app/common/FileInput.tsx deleted file mode 100644 index 20da7b71..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 '../type-utilities'; - -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 7cb8d3cf..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 81bc74cf..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 30db4053..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 50c252fa..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 8dd9e97b..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 58a429d8..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 79eecc82..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 |