From 47587812b809fee2a95c76266d9d0e42fc4ac1ca Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 14:14:28 +0800 Subject: ... --- FrontEnd/src/views/common/AppBar.tsx | 80 +++++ FrontEnd/src/views/common/BlobImage.tsx | 27 ++ FrontEnd/src/views/common/ConfirmDialog.tsx | 40 +++ FrontEnd/src/views/common/FlatButton.tsx | 36 ++ FrontEnd/src/views/common/FullPage.tsx | 39 ++ FrontEnd/src/views/common/ImageCropper.tsx | 306 ++++++++++++++++ FrontEnd/src/views/common/LoadFailReload.tsx | 37 ++ FrontEnd/src/views/common/LoadingButton.tsx | 29 ++ FrontEnd/src/views/common/LoadingPage.tsx | 12 + FrontEnd/src/views/common/Menu.tsx | 92 +++++ FrontEnd/src/views/common/OperationDialog.tsx | 471 +++++++++++++++++++++++++ FrontEnd/src/views/common/SearchInput.tsx | 78 ++++ FrontEnd/src/views/common/Skeleton.tsx | 30 ++ FrontEnd/src/views/common/TabPages.tsx | 74 ++++ FrontEnd/src/views/common/TimelineLogo.tsx | 26 ++ FrontEnd/src/views/common/ToggleIconButton.tsx | 30 ++ FrontEnd/src/views/common/UserTimelineLogo.tsx | 26 ++ FrontEnd/src/views/common/alert/AlertHost.tsx | 106 ++++++ FrontEnd/src/views/common/alert/alert.sass | 15 + FrontEnd/src/views/common/common.sass | 191 ++++++++++ FrontEnd/src/views/common/user/UserAvatar.tsx | 19 + 21 files changed, 1764 insertions(+) create mode 100644 FrontEnd/src/views/common/AppBar.tsx create mode 100644 FrontEnd/src/views/common/BlobImage.tsx create mode 100644 FrontEnd/src/views/common/ConfirmDialog.tsx create mode 100644 FrontEnd/src/views/common/FlatButton.tsx create mode 100644 FrontEnd/src/views/common/FullPage.tsx create mode 100644 FrontEnd/src/views/common/ImageCropper.tsx create mode 100644 FrontEnd/src/views/common/LoadFailReload.tsx create mode 100644 FrontEnd/src/views/common/LoadingButton.tsx create mode 100644 FrontEnd/src/views/common/LoadingPage.tsx create mode 100644 FrontEnd/src/views/common/Menu.tsx create mode 100644 FrontEnd/src/views/common/OperationDialog.tsx create mode 100644 FrontEnd/src/views/common/SearchInput.tsx create mode 100644 FrontEnd/src/views/common/Skeleton.tsx create mode 100644 FrontEnd/src/views/common/TabPages.tsx create mode 100644 FrontEnd/src/views/common/TimelineLogo.tsx create mode 100644 FrontEnd/src/views/common/ToggleIconButton.tsx create mode 100644 FrontEnd/src/views/common/UserTimelineLogo.tsx create mode 100644 FrontEnd/src/views/common/alert/AlertHost.tsx create mode 100644 FrontEnd/src/views/common/alert/alert.sass create mode 100644 FrontEnd/src/views/common/common.sass create mode 100644 FrontEnd/src/views/common/user/UserAvatar.tsx (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx new file mode 100644 index 00000000..91dfbee9 --- /dev/null +++ b/FrontEnd/src/views/common/AppBar.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Link, NavLink } from "react-router-dom"; +import classnames from "classnames"; +import { useMediaQuery } from "react-responsive"; + +import { useUser } from "@/services/user"; + +import TimelineLogo from "./TimelineLogo"; +import UserAvatar from "./user/UserAvatar"; + +const AppBar: React.FC = (_) => { + const { t } = useTranslation(); + + const user = useUser(); + const hasAdministrationPermission = user && user.hasAdministrationPermission; + + const isSmallScreen = useMediaQuery({ maxWidth: 576 }); + + const [expand, setExpand] = React.useState(false); + const collapse = (): void => setExpand(false); + const toggleExpand = (): void => setExpand(!expand); + + const createLink = ( + link: string, + label: React.ReactNode, + className?: string + ): React.ReactNode => ( + + {label} + + ); + + return ( + + ); +}; + +export default AppBar; diff --git a/FrontEnd/src/views/common/BlobImage.tsx b/FrontEnd/src/views/common/BlobImage.tsx new file mode 100644 index 00000000..0dd25c52 --- /dev/null +++ b/FrontEnd/src/views/common/BlobImage.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +const BlobImage: React.FC< + Omit, "src"> & { + blob?: Blob | unknown; + } +> = (props) => { + const { blob, ...otherProps } = props; + + const [url, setUrl] = React.useState(undefined); + + React.useEffect(() => { + if (blob instanceof Blob) { + const url = URL.createObjectURL(blob); + setUrl(url); + return () => { + URL.revokeObjectURL(url); + }; + } else { + setUrl(undefined); + } + }, [blob]); + + return ; +}; + +export default BlobImage; diff --git a/FrontEnd/src/views/common/ConfirmDialog.tsx b/FrontEnd/src/views/common/ConfirmDialog.tsx new file mode 100644 index 00000000..72940c51 --- /dev/null +++ b/FrontEnd/src/views/common/ConfirmDialog.tsx @@ -0,0 +1,40 @@ +import { convertI18nText, I18nText } from "@/common"; +import React from "react"; +import { Modal, Button } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +const ConfirmDialog: React.FC<{ + onClose: () => void; + onConfirm: () => void; + title: I18nText; + body: I18nText; +}> = ({ onClose, onConfirm, title, body }) => { + const { t } = useTranslation(); + + return ( + + + + {convertI18nText(title, t)} + + + {convertI18nText(body, t)} + + + + + + ); +}; + +export default ConfirmDialog; diff --git a/FrontEnd/src/views/common/FlatButton.tsx b/FrontEnd/src/views/common/FlatButton.tsx new file mode 100644 index 00000000..b1f7a051 --- /dev/null +++ b/FrontEnd/src/views/common/FlatButton.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import classnames from "classnames"; + +import { BootstrapThemeColor } from "@/common"; + +export interface FlatButtonProps { + variant?: BootstrapThemeColor | string; + disabled?: boolean; + className?: string; + style?: React.CSSProperties; + onClick?: () => void; +} + +const FlatButton: React.FC = (props) => { + const { disabled, className, style } = props; + const variant = props.variant ?? "primary"; + + const onClick = disabled ? undefined : props.onClick; + + return ( +
+ {props.children} +
+ ); +}; + +export default FlatButton; diff --git a/FrontEnd/src/views/common/FullPage.tsx b/FrontEnd/src/views/common/FullPage.tsx new file mode 100644 index 00000000..1b59045a --- /dev/null +++ b/FrontEnd/src/views/common/FullPage.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import classnames from "classnames"; + +export interface FullPageProps { + show: boolean; + onBack: () => void; + contentContainerClassName?: string; +} + +const FullPage: React.FC = ({ + show, + onBack, + children, + contentContainerClassName, +}) => { + return ( +
+
+ +
+
+ {children} +
+
+ ); +}; + +export default FullPage; diff --git a/FrontEnd/src/views/common/ImageCropper.tsx b/FrontEnd/src/views/common/ImageCropper.tsx new file mode 100644 index 00000000..2ef5b7ed --- /dev/null +++ b/FrontEnd/src/views/common/ImageCropper.tsx @@ -0,0 +1,306 @@ +import React from "react"; +import classnames from "classnames"; + +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( + null + ); + const [imageInfo, setImageInfo] = React.useState(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(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) => { + 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 ( +
+ to crop +
+
+
+
+
+ ); +}; + +export default ImageCropper; + +export function applyClipToImage( + image: HTMLImageElement, + clip: Clip, + mimeType: string +): Promise { + 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/FrontEnd/src/views/common/LoadFailReload.tsx b/FrontEnd/src/views/common/LoadFailReload.tsx new file mode 100644 index 00000000..a80e7b76 --- /dev/null +++ b/FrontEnd/src/views/common/LoadFailReload.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { Trans } from "react-i18next"; + +export interface LoadFailReloadProps { + className?: string; + style?: React.CSSProperties; + onReload: () => void; +} + +const LoadFailReload: React.FC = ({ + onReload, + className, + style, +}) => { + return ( + + 0 + { + onReload(); + e.preventDefault(); + }} + > + 1 + + 2 + + ); +}; + +export default LoadFailReload; diff --git a/FrontEnd/src/views/common/LoadingButton.tsx b/FrontEnd/src/views/common/LoadingButton.tsx new file mode 100644 index 00000000..cd9f1adc --- /dev/null +++ b/FrontEnd/src/views/common/LoadingButton.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Button, ButtonProps, Spinner } from "react-bootstrap"; + +const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({ + loading, + variant, + disabled, + ...otherProps +}) => { + return ( + + ); +}; + +export default LoadingButton; diff --git a/FrontEnd/src/views/common/LoadingPage.tsx b/FrontEnd/src/views/common/LoadingPage.tsx new file mode 100644 index 00000000..590fafa0 --- /dev/null +++ b/FrontEnd/src/views/common/LoadingPage.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Spinner } from "react-bootstrap"; + +const LoadingPage: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default LoadingPage; diff --git a/FrontEnd/src/views/common/Menu.tsx b/FrontEnd/src/views/common/Menu.tsx new file mode 100644 index 00000000..ae73a331 --- /dev/null +++ b/FrontEnd/src/views/common/Menu.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import classnames from "classnames"; +import { OverlayTrigger, OverlayTriggerProps, Popover } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { BootstrapThemeColor, convertI18nText, I18nText } from "@/common"; + +export type MenuItem = + | { + type: "divider"; + } + | { + type: "button"; + text: I18nText; + iconClassName?: string; + color?: BootstrapThemeColor; + onClick: () => void; + }; + +export type MenuItems = MenuItem[]; + +export interface MenuProps { + items: MenuItems; + className?: string; + onItemClicked?: () => void; +} + +const Menu: React.FC = ({ items, className, onItemClicked }) => { + const { t } = useTranslation(); + + return ( +
+ {items.map((item, index) => { + if (item.type === "divider") { + return
; + } else { + return ( +
{ + item.onClick(); + onItemClicked?.(); + }} + > + {item.iconClassName != null ? ( + + ) : null} + {convertI18nText(item.text, t)} +
+ ); + } + })} +
+ ); +}; + +export default Menu; + +export interface PopupMenuProps { + items: MenuItems; + children: OverlayTriggerProps["children"]; +} + +export const PopupMenu: React.FC = ({ items, children }) => { + const [show, setShow] = React.useState(false); + const toggle = (): void => setShow(!show); + + return ( + + setShow(false)} /> + + } + show={show} + onToggle={toggle} + > + {children} + + ); +}; diff --git a/FrontEnd/src/views/common/OperationDialog.tsx b/FrontEnd/src/views/common/OperationDialog.tsx new file mode 100644 index 00000000..ac4c51b9 --- /dev/null +++ b/FrontEnd/src/views/common/OperationDialog.tsx @@ -0,0 +1,471 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Button, Modal } from "react-bootstrap"; +import { TwitterPicker } from "react-color"; +import moment from "moment"; + +import { convertI18nText, I18nText, UiLogicError } from "@/common"; + +import LoadingButton from "./LoadingButton"; + +interface DefaultErrorPromptProps { + error?: string; +} + +const DefaultErrorPrompt: React.FC = (props) => { + const { t } = useTranslation(); + + let result =

{t("operationDialog.error")}

; + + if (props.error != null) { + result = ( + <> + {result} +

{props.error}

+ + ); + } + + return result; +}; + +export interface OperationDialogTextInput { + type: "text"; + label?: I18nText; + password?: boolean; + initValue?: string; + textFieldProps?: Omit< + React.InputHTMLAttributes, + "type" | "value" | "onChange" | "aria-relevant" + >; + helperText?: string; +} + +export interface OperationDialogBoolInput { + type: "bool"; + label: I18nText; + initValue?: boolean; +} + +export interface OperationDialogSelectInputOption { + value: string; + label: I18nText; + icon?: React.ReactElement; +} + +export interface OperationDialogSelectInput { + type: "select"; + label: I18nText; + options: OperationDialogSelectInputOption[]; + initValue?: string; +} + +export interface OperationDialogColorInput { + type: "color"; + label?: I18nText; + initValue?: string | null; + canBeNull?: boolean; +} + +export interface OperationDialogDateTimeInput { + type: "datetime"; + label?: I18nText; + initValue?: string; +} + +export type OperationDialogInput = + | OperationDialogTextInput + | OperationDialogBoolInput + | OperationDialogSelectInput + | OperationDialogColorInput + | OperationDialogDateTimeInput; + +interface OperationInputTypeStringToValueTypeMap { + text: string; + bool: boolean; + select: string; + color: string | null; + datetime: string; +} + +type MapOperationInputTypeStringToValueType = + Type extends keyof OperationInputTypeStringToValueTypeMap + ? OperationInputTypeStringToValueTypeMap[Type] + : never; + +type MapOperationInputInfoValueType = T extends OperationDialogInput + ? MapOperationInputTypeStringToValueType + : T; + +const initValueMapperMap: { + [T in OperationDialogInput as T["type"]]: ( + item: T + ) => MapOperationInputInfoValueType; +} = { + bool: (item) => item.initValue ?? false, + color: (item) => item.initValue ?? null, + datetime: (item) => { + if (item.initValue != null) { + return moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss"); + } else { + return ""; + } + }, + select: (item) => item.initValue ?? item.options[0].value, + text: (item) => item.initValue ?? "", +}; + +type MapOperationInputInfoValueTypeList< + Tuple extends readonly OperationDialogInput[] +> = { + [Index in keyof Tuple]: MapOperationInputInfoValueType; +} & { length: Tuple["length"] }; + +export type OperationInputError = + | { + [index: number]: I18nText | null | undefined; + } + | null + | undefined; + +const isNoError = (error: OperationInputError): boolean => { + if (error == null) return true; + for (const key in error) { + if (error[key] != null) return false; + } + return true; +}; + +export interface OperationDialogProps< + TData, + OperationInputInfoList extends readonly OperationDialogInput[] +> { + open: boolean; + close: () => void; + title: I18nText | (() => React.ReactNode); + themeColor?: "danger" | "success" | string; + onProcess: ( + inputs: MapOperationInputInfoValueTypeList + ) => Promise; + inputScheme?: OperationInputInfoList; + inputValidator?: ( + inputs: MapOperationInputInfoValueTypeList + ) => OperationInputError; + inputPrompt?: I18nText | (() => React.ReactNode); + processPrompt?: () => React.ReactNode; + successPrompt?: (data: TData) => React.ReactNode; + failurePrompt?: (error: unknown) => React.ReactNode; + onSuccessAndClose?: (data: TData) => void; +} + +const OperationDialog = < + TData, + OperationInputInfoList extends readonly OperationDialogInput[] +>( + props: OperationDialogProps +): React.ReactElement => { + const inputScheme = (props.inputScheme ?? + []) as readonly OperationDialogInput[]; + + const { t } = useTranslation(); + + type Step = + | { type: "input" } + | { type: "process" } + | { + type: "success"; + data: TData; + } + | { + type: "failure"; + data: unknown; + }; + const [step, setStep] = useState({ type: "input" }); + + type ValueType = boolean | string | null | undefined; + + const [values, setValues] = useState( + inputScheme.map((item) => { + if (item.type in initValueMapperMap) { + return ( + initValueMapperMap[item.type] as ( + i: OperationDialogInput + ) => ValueType + )(item); + } else { + throw new UiLogicError("Unknown input scheme."); + } + }) + ); + const [dirtyList, setDirtyList] = useState(() => + inputScheme.map(() => false) + ); + const [inputError, setInputError] = useState(); + + const close = (): void => { + if (step.type !== "process") { + props.close(); + if (step.type === "success" && props.onSuccessAndClose) { + props.onSuccessAndClose(step.data); + } + } else { + console.log("Attempt to close modal when processing."); + } + }; + + const onConfirm = (): void => { + setStep({ type: "process" }); + props + .onProcess( + values.map((v, index) => { + if (inputScheme[index].type === "datetime" && v !== "") + return new Date(v as string).toISOString(); + else return v; + }) as unknown as MapOperationInputInfoValueTypeList + ) + .then( + (d) => { + setStep({ + type: "success", + data: d, + }); + }, + (e: unknown) => { + setStep({ + type: "failure", + data: e, + }); + } + ); + }; + + let body: React.ReactNode; + if (step.type === "input" || step.type === "process") { + const process = step.type === "process"; + + let inputPrompt = + typeof props.inputPrompt === "function" + ? props.inputPrompt() + : convertI18nText(props.inputPrompt, t); + inputPrompt =
{inputPrompt}
; + + const validate = (values: ValueType[]): boolean => { + const { inputValidator } = props; + if (inputValidator != null) { + const result = inputValidator( + values as unknown as MapOperationInputInfoValueTypeList + ); + setInputError(result); + return isNoError(result); + } + return true; + }; + + const updateValue = (index: number, newValue: ValueType): void => { + const oldValues = values; + const newValues = oldValues.slice(); + newValues[index] = newValue; + setValues(newValues); + if (dirtyList[index] === false) { + const newDirtyList = dirtyList.slice(); + newDirtyList[index] = true; + setDirtyList(newDirtyList); + } + validate(newValues); + }; + + const canProcess = isNoError(inputError); + + body = ( + <> + + {inputPrompt} + {inputScheme.map((item, index) => { + const value = values[index]; + const error: string | null = + dirtyList[index] && inputError != null + ? convertI18nText(inputError[index], t) + : null; + + if (item.type === "text") { + return ( + + {item.label && ( + {convertI18nText(item.label, t)} + )} + { + const v = e.target.value; + updateValue(index, v); + }} + isInvalid={error != null} + disabled={process} + /> + {error != null && ( + + {error} + + )} + {item.helperText && ( + {t(item.helperText)} + )} + + ); + } else if (item.type === "bool") { + return ( + + + type="checkbox" + checked={value as boolean} + onChange={(event) => { + updateValue(index, event.currentTarget.checked); + }} + label={convertI18nText(item.label, t)} + disabled={process} + /> + + ); + } else if (item.type === "select") { + return ( + + {convertI18nText(item.label, t)} + { + updateValue(index, event.target.value); + }} + disabled={process} + > + {item.options.map((option, i) => { + return ( + + ); + })} + + + ); + } else if (item.type === "color") { + return ( + + {item.canBeNull ? ( + + type="checkbox" + checked={value !== null} + onChange={(event) => { + if (event.currentTarget.checked) { + updateValue(index, "#007bff"); + } else { + updateValue(index, null); + } + }} + label={convertI18nText(item.label, t)} + disabled={process} + /> + ) : ( + {convertI18nText(item.label, t)} + )} + {value !== null && ( + updateValue(index, result.hex)} + /> + )} + + ); + } else if (item.type === "datetime") { + return ( + + {item.label && ( + {convertI18nText(item.label, t)} + )} + { + const v = e.target.value; + updateValue(index, v); + }} + isInvalid={error != null} + disabled={process} + /> + {error != null && ( + + {error} + + )} + + ); + } + })} + + + + { + setDirtyList(inputScheme.map(() => true)); + if (validate(values)) { + onConfirm(); + } + }} + > + {t("operationDialog.confirm")} + + + + ); + } 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 =

{content}

; + } else { + content = props.failurePrompt?.(result.data) ?? ; + if (typeof content === "string") + content = ; + } + body = ( + <> + {content} + + + + + ); + } + + const title = + typeof props.title === "function" + ? props.title() + : convertI18nText(props.title, t); + + return ( + + + {title} + + {body} + + ); +}; + +export default OperationDialog; diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx new file mode 100644 index 00000000..ccb6dad6 --- /dev/null +++ b/FrontEnd/src/views/common/SearchInput.tsx @@ -0,0 +1,78 @@ +import React, { useCallback } from "react"; +import classnames from "classnames"; +import { useTranslation } from "react-i18next"; +import { Spinner, Form, Button } from "react-bootstrap"; + +export interface SearchInputProps { + value: string; + onChange: (value: string) => void; + onButtonClick: () => void; + className?: string; + loading?: boolean; + buttonText?: string; + placeholder?: string; + additionalButton?: React.ReactNode; + alwaysOneline?: boolean; +} + +const SearchInput: React.FC = (props) => { + const { onChange, onButtonClick, alwaysOneline } = props; + + const { t } = useTranslation(); + + const onInputChange = useCallback( + (event: React.ChangeEvent): void => { + onChange(event.currentTarget.value); + }, + [onChange] + ); + + const onInputKeyPress = useCallback( + (event: React.KeyboardEvent): void => { + if (event.key === "Enter") { + onButtonClick(); + event.preventDefault(); + } + }, + [onButtonClick] + ); + + return ( +
+ + {props.additionalButton ? ( +
+ {props.additionalButton} +
+ ) : null} +
+ {props.loading ? ( + + ) : ( + + )} +
+ + ); +}; + +export default SearchInput; diff --git a/FrontEnd/src/views/common/Skeleton.tsx b/FrontEnd/src/views/common/Skeleton.tsx new file mode 100644 index 00000000..14886c71 --- /dev/null +++ b/FrontEnd/src/views/common/Skeleton.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import classnames from "classnames"; +import { range } from "lodash"; + +export interface SkeletonProps { + lineNumber?: number; + className?: string; + style?: React.CSSProperties; +} + +const Skeleton: React.FC = (props) => { + const { lineNumber: lineNumberProps, className, style } = props; + const lineNumber = lineNumberProps ?? 3; + + return ( +
+ {range(lineNumber).map((i) => ( +
+ ))} +
+ ); +}; + +export default Skeleton; diff --git a/FrontEnd/src/views/common/TabPages.tsx b/FrontEnd/src/views/common/TabPages.tsx new file mode 100644 index 00000000..2b1d91cb --- /dev/null +++ b/FrontEnd/src/views/common/TabPages.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { Nav } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { convertI18nText, I18nText, UiLogicError } from "@/common"; + +export interface TabPage { + id: string; + tabText: I18nText; + page: React.ReactNode; +} + +export interface TabPagesProps { + pages: TabPage[]; + actions?: React.ReactNode; + className?: string; + style?: React.CSSProperties; + navClassName?: string; + navStyle?: React.CSSProperties; + pageContainerClassName?: string; + pageContainerStyle?: React.CSSProperties; +} + +const TabPages: React.FC = ({ + pages, + actions, + className, + style, + navClassName, + navStyle, + pageContainerClassName, + pageContainerStyle, +}) => { + if (pages.length === 0) { + throw new UiLogicError("Page list can't be empty."); + } + + const { t } = useTranslation(); + + const [tab, setTab] = React.useState(pages[0].id); + + const currentPage = pages.find((p) => p.id === tab); + + if (currentPage == null) { + throw new UiLogicError("Current tab value is bad."); + } + + return ( +
+ +
+ {currentPage.page} +
+
+ ); +}; + +export default TabPages; diff --git a/FrontEnd/src/views/common/TimelineLogo.tsx b/FrontEnd/src/views/common/TimelineLogo.tsx new file mode 100644 index 00000000..27d188fc --- /dev/null +++ b/FrontEnd/src/views/common/TimelineLogo.tsx @@ -0,0 +1,26 @@ +import React, { SVGAttributes } from "react"; + +export interface TimelineLogoProps extends SVGAttributes { + color?: string; +} + +const TimelineLogo: React.FC = (props) => { + const { color, ...forwardProps } = props; + const coercedColor = color ?? "currentcolor"; + return ( + + + + + + ); +}; + +export default TimelineLogo; diff --git a/FrontEnd/src/views/common/ToggleIconButton.tsx b/FrontEnd/src/views/common/ToggleIconButton.tsx new file mode 100644 index 00000000..c4d2d132 --- /dev/null +++ b/FrontEnd/src/views/common/ToggleIconButton.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import classnames from "classnames"; + +export interface ToggleIconButtonProps + extends React.HTMLAttributes { + state: boolean; + trueIconClassName: string; + falseIconClassName: string; +} + +const ToggleIconButton: React.FC = ({ + state, + className, + trueIconClassName, + falseIconClassName, + ...otherProps +}) => { + return ( + + ); +}; + +export default ToggleIconButton; diff --git a/FrontEnd/src/views/common/UserTimelineLogo.tsx b/FrontEnd/src/views/common/UserTimelineLogo.tsx new file mode 100644 index 00000000..19b9fee5 --- /dev/null +++ b/FrontEnd/src/views/common/UserTimelineLogo.tsx @@ -0,0 +1,26 @@ +import React, { SVGAttributes } from "react"; + +export interface UserTimelineLogoProps extends SVGAttributes { + color?: string; +} + +const UserTimelineLogo: React.FC = (props) => { + const { color, ...forwardProps } = props; + const coercedColor = color ?? "currentcolor"; + + return ( + + + + + + + + + + + + ); +}; + +export default UserTimelineLogo; diff --git a/FrontEnd/src/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx new file mode 100644 index 00000000..949be7ed --- /dev/null +++ b/FrontEnd/src/views/common/alert/AlertHost.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import without from "lodash/without"; +import { useTranslation } from "react-i18next"; +import { Alert } from "react-bootstrap"; + +import { + alertService, + AlertInfoEx, + kAlertHostId, + AlertInfo, +} from "@/services/alert"; +import { convertI18nText } from "@/common"; + +interface AutoCloseAlertProps { + alert: AlertInfo; + close: () => void; +} + +export const AutoCloseAlert: React.FC = (props) => { + const { alert, close } = props; + const { dismissTime } = alert; + + const { t } = useTranslation(); + + const timerTag = React.useRef(null); + const closeHandler = React.useRef<(() => void) | null>(null); + + React.useEffect(() => { + closeHandler.current = close; + }, [close]); + + React.useEffect(() => { + const tag = + dismissTime === "never" + ? null + : typeof dismissTime === "number" + ? window.setTimeout(() => closeHandler.current?.(), dismissTime) + : window.setTimeout(() => closeHandler.current?.(), 5000); + timerTag.current = tag; + return () => { + if (tag != null) { + window.clearTimeout(tag); + } + }; + }, [dismissTime]); + + const cancelTimer = (): void => { + const { current: tag } = timerTag; + if (tag != null) { + window.clearTimeout(tag); + } + }; + + return ( + + {(() => { + const { message } = alert; + if (typeof message === "function") { + const Message = message; + return ; + } else return convertI18nText(message, t); + })()} + + ); +}; + +const AlertHost: React.FC = () => { + const [alerts, setAlerts] = React.useState([]); + + // react guarantee that state setters are stable, so we don't need to add it to dependency list + + React.useEffect(() => { + const consume = (alert: AlertInfoEx): void => { + setAlerts((old) => [...old, alert]); + }; + + alertService.registerConsumer(consume); + return () => { + alertService.unregisterConsumer(consume); + }; + }, []); + + return ( +
+ {alerts.map((alert) => { + return ( + { + setAlerts((old) => without(old, alert)); + }} + /> + ); + })} +
+ ); +}; + +export default AlertHost; diff --git a/FrontEnd/src/views/common/alert/alert.sass b/FrontEnd/src/views/common/alert/alert.sass new file mode 100644 index 00000000..c3560b87 --- /dev/null +++ b/FrontEnd/src/views/common/alert/alert.sass @@ -0,0 +1,15 @@ +.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/FrontEnd/src/views/common/common.sass b/FrontEnd/src/views/common/common.sass new file mode 100644 index 00000000..cbf7292e --- /dev/null +++ b/FrontEnd/src/views/common/common.sass @@ -0,0 +1,191 @@ +.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 + +.app-bar + display: flex + align-items: center + height: 56px + + position: fixed + z-index: 1030 + top: 0 + left: 0 + right: 0 + + background-color: var(--tl-primary-color) + + transition: background-color 1s + + a + color: var(--tl-text-on-primary-inactive-color) + text-decoration: none + margin: 0 1em + + &:hover + color: var(--tl-text-on-primary-color) + + &.active + color: var(--tl-text-on-primary-color) + +.app-bar-brand + display: flex + align-items: center + +.app-bar-brand-icon + height: 2em + +.app-bar-main-area + display: flex + flex-grow: 1 + +.app-bar-link-area + display: flex + align-items: center + flex-shrink: 0 + +.app-bar-user-area + display: flex + align-items: center + flex-shrink: 0 + margin-left: auto + +.small-screen + .app-bar-main-area + position: absolute + top: 56px + left: 0 + right: 0 + + transform-origin: top + transition: transform 0.6s, background-color 1s + + background-color: var(--tl-primary-color) + + flex-direction: column + + &.app-bar-collapse + transform: scale(1,0) + + a + text-align: left + padding: 0.5em 0.5em + + .app-bar-link-area + flex-direction: column + align-items: stretch + + .app-bar-user-area + flex-direction: column + align-items: stretch + margin-left: unset + + .app-bar-avatar + align-self: flex-end + +.app-bar-toggler + margin-left: auto + font-size: 2em + margin-right: 1em + color: var(--tl-text-on-primary-color) + cursor: pointer + user-select: none + +.cru-skeleton + padding: 0 1em + +.cru-skeleton-line + height: 1em + background-color: #e6e6e6 + margin: 0.7em 0 + border-radius: 0.2em + + &.last + width: 50% + +.cru-full-page + position: fixed + z-index: 1031 + left: 0 + top: 0 + right: 0 + bottom: 0 + background-color: white + padding-top: 56px + +.cru-full-page-top-bar + height: 56px + + position: absolute + top: 0 + left: 0 + right: 0 + z-index: 1 + + background-color: var(--tl-primary-color) + + display: flex + align-items: center + +.cru-full-page-content-container + overflow: scroll + +.cru-menu + min-width: 200px + +.cru-menu-item + font-size: 1.2em + padding: 0.5em 1.5em + cursor: pointer + + @each $color, $value in $theme-colors + &.color-#{$color} + color: $value + + &:hover + color: white + background-color: $value + +.cru-menu-item-icon + margin-right: 1em + +.cru-menu-divider + border-top: 1px solid $gray-200 + +.cru-tab-pages-action-area + display: flex + align-items: center + +.cru-search-input + display: flex + flex-wrap: wrap diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx new file mode 100644 index 00000000..901697db --- /dev/null +++ b/FrontEnd/src/views/common/user/UserAvatar.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import { getHttpUserClient } from "http/user"; + +export interface UserAvatarProps + extends React.ImgHTMLAttributes { + username: string; +} + +const UserAvatar: React.FC = ({ username, ...otherProps }) => { + return ( + + ); +}; + +export default UserAvatar; -- cgit v1.2.3 From e0b766203d7576ab67b16ba556ba14120d0bc876 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 15:02:49 +0800 Subject: ... --- BackEnd/Timeline/FrontEndMode.cs | 1 - BackEnd/Timeline/Properties/launchSettings.json | 2 +- BackEnd/Timeline/Startup.cs | 4 --- FrontEnd/index.html | 29 ++++++++++++++++++++++ FrontEnd/package.json | 5 ++-- FrontEnd/postcss.config.js | 11 +++----- FrontEnd/src/index.ejs | 29 ---------------------- FrontEnd/src/services/TimelinePostBuilder.ts | 4 +-- FrontEnd/src/services/timeline.ts | 4 +-- FrontEnd/src/services/user.ts | 6 ++--- FrontEnd/src/tsconfig.json | 23 ----------------- FrontEnd/src/views/admin/UserAdmin.tsx | 2 +- FrontEnd/src/views/center/CenterBoards.tsx | 6 ++--- FrontEnd/src/views/center/TimelineBoard.tsx | 4 +-- FrontEnd/src/views/center/TimelineCreateDialog.tsx | 2 +- FrontEnd/src/views/common/user/UserAvatar.tsx | 2 +- FrontEnd/src/views/home/TimelineListView.tsx | 4 +-- FrontEnd/src/views/home/index.tsx | 4 +-- FrontEnd/src/views/search/index.tsx | 10 ++++---- FrontEnd/src/views/settings/ChangeAvatarDialog.tsx | 2 +- .../src/views/settings/ChangeNicknameDialog.tsx | 2 +- .../timeline-common/ConnectionStatusBadge.tsx | 2 +- .../src/views/timeline-common/MarkdownPostEdit.tsx | 2 +- .../timeline-common/PostPropertyChangeDialog.tsx | 2 +- FrontEnd/src/views/timeline-common/Timeline.tsx | 6 ++--- .../src/views/timeline-common/TimelineMember.tsx | 8 +++--- .../timeline-common/TimelinePageCardTemplate.tsx | 4 +-- .../views/timeline-common/TimelinePageTemplate.tsx | 4 +-- .../timeline-common/TimelinePagedPostListView.tsx | 2 +- .../timeline-common/TimelinePostContentView.tsx | 4 +-- .../src/views/timeline-common/TimelinePostEdit.tsx | 6 ++--- .../views/timeline-common/TimelinePostListView.tsx | 2 +- .../src/views/timeline-common/TimelinePostView.tsx | 2 +- .../TimelinePropertyChangeDialog.tsx | 2 +- FrontEnd/src/views/timeline/TimelineCard.tsx | 2 +- .../src/views/timeline/TimelineDeleteDialog.tsx | 2 +- FrontEnd/src/views/user/index.tsx | 2 +- FrontEnd/tsconfig.json | 25 +++++++++++++++++++ FrontEnd/vite.config.js | 3 +++ 39 files changed, 116 insertions(+), 120 deletions(-) create mode 100644 FrontEnd/index.html delete mode 100644 FrontEnd/src/index.ejs delete mode 100644 FrontEnd/src/tsconfig.json create mode 100644 FrontEnd/tsconfig.json (limited to 'FrontEnd/src/views/common') diff --git a/BackEnd/Timeline/FrontEndMode.cs b/BackEnd/Timeline/FrontEndMode.cs index 63503292..eb718028 100644 --- a/BackEnd/Timeline/FrontEndMode.cs +++ b/BackEnd/Timeline/FrontEndMode.cs @@ -4,7 +4,6 @@ { Disable, Mock, - Proxy, Normal } } diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json index f683ca2d..7f68b709 100644 --- a/BackEnd/Timeline/Properties/launchSettings.json +++ b/BackEnd/Timeline/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_FRONTEND": "Proxy" + "ASPNETCORE_FRONTEND": "Disable" } }, "Dev-Mock": { diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 18097e2c..26ffb4b1 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -166,10 +166,6 @@ namespace Timeline { app.UseSpa(spa => { - if (_frontEndMode == FrontEndMode.Proxy) - { - spa.UseProxyToSpaDevelopmentServer(new UriBuilder("http", "localhost", 3000).Uri); - } }); } } diff --git a/FrontEnd/index.html b/FrontEnd/index.html new file mode 100644 index 00000000..87e19743 --- /dev/null +++ b/FrontEnd/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + Timeline + + + +
+ + + diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 1afb5ad0..5d12899e 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -33,8 +33,9 @@ "xregexp": "^5.0.2" }, "scripts": { - "start": "webpack serve --config ./webpack.config.dev.js", - "build": "webpack --config ./webpack.config.prod.js", + "start": "vite", + "build": "tsc && vite build", + "preview": "vite preview", "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx", "lint:fix": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx --fix" }, diff --git a/FrontEnd/postcss.config.js b/FrontEnd/postcss.config.js index 74ee8155..9129aa1f 100644 --- a/FrontEnd/postcss.config.js +++ b/FrontEnd/postcss.config.js @@ -1,10 +1,5 @@ +import postcssPresetEnv from "postcss-preset-env"; + module.exports = { - plugins: [ - [ - "postcss-preset-env", - { - // Options - }, - ], - ], + plugins: [postcssPresetEnv()], }; diff --git a/FrontEnd/src/index.ejs b/FrontEnd/src/index.ejs deleted file mode 100644 index c2ff4182..00000000 --- a/FrontEnd/src/index.ejs +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - <%= htmlWebpackPlugin.options.title %> - - - -
- - - diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts index fe4c7a9a..40279eca 100644 --- a/FrontEnd/src/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/services/TimelinePostBuilder.ts @@ -2,8 +2,8 @@ import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; -import { base64 } from "http/common"; -import { HttpTimelinePostPostRequest } from "http/timeline"; +import { base64 } from "@/http/common"; +import { HttpTimelinePostPostRequest } from "@/http/timeline"; export default class TimelinePostBuilder { private _onChange: () => void; diff --git a/FrontEnd/src/services/timeline.ts b/FrontEnd/src/services/timeline.ts index 4ebb705d..d8c0ae00 100644 --- a/FrontEnd/src/services/timeline.ts +++ b/FrontEnd/src/services/timeline.ts @@ -1,9 +1,9 @@ -import { TimelineVisibility } from "http/timeline"; +import { TimelineVisibility } from "@/http/timeline"; import XRegExp from "xregexp"; import { Observable } from "rxjs"; import { HubConnectionBuilder, HubConnectionState } from "@microsoft/signalr"; -import { getHttpToken } from "http/common"; +import { getHttpToken } from "@/http/common"; const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts index 3375c88a..9a8e5687 100644 --- a/FrontEnd/src/services/user.ts +++ b/FrontEnd/src/services/user.ts @@ -3,12 +3,12 @@ import { BehaviorSubject, Observable } from "rxjs"; import { UiLogicError } from "@/common"; -import { HttpNetworkError, setHttpToken } from "http/common"; +import { HttpNetworkError, setHttpToken } from "@/http/common"; import { getHttpTokenClient, HttpCreateTokenBadCredentialError, -} from "http/token"; -import { getHttpUserClient, HttpUser, UserPermission } from "http/user"; +} from "@/http/token"; +import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user"; import { pushAlert } from "./alert"; diff --git a/FrontEnd/src/tsconfig.json b/FrontEnd/src/tsconfig.json deleted file mode 100644 index 817c50bd..00000000 --- a/FrontEnd/src/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react", - "types": ["vite/client"], - "sourceMap": true, - "baseUrl": "./", - "paths": { - "@/*": ["*"] - }, - "lib": ["dom", "dom.iterable", "esnext"] - }, - "include": ["."] -} diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index 4e9cd600..558d3aee 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -12,7 +12,7 @@ import { HttpUser, kUserPermissionList, UserPermission, -} from "http/user"; +} from "@/http/user"; import { Trans, useTranslation } from "react-i18next"; interface DialogProps { diff --git a/FrontEnd/src/views/center/CenterBoards.tsx b/FrontEnd/src/views/center/CenterBoards.tsx index 431d1e9a..f5200415 100644 --- a/FrontEnd/src/views/center/CenterBoards.tsx +++ b/FrontEnd/src/views/center/CenterBoards.tsx @@ -5,9 +5,9 @@ import { useTranslation } from "react-i18next"; import { pushAlert } from "@/services/alert"; import { useUserLoggedIn } from "@/services/user"; -import { getHttpTimelineClient } from "http/timeline"; -import { getHttpBookmarkClient } from "http/bookmark"; -import { getHttpHighlightClient } from "http/highlight"; +import { getHttpTimelineClient } from "@/http/timeline"; +import { getHttpBookmarkClient } from "@/http/bookmark"; +import { getHttpHighlightClient } from "@/http/highlight"; import TimelineBoard from "./TimelineBoard"; diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index bb80266b..840c0415 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -4,7 +4,7 @@ import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Spinner } from "react-bootstrap"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; @@ -34,7 +34,7 @@ const TimelineBoardItem: React.FC = ({ actions, }) => { const { name, title } = timeline; - const isPersonal = name.startsWith("@"); + const isPersonal = name.startsWith("src"); const url = isPersonal ? `/users/${timeline.owner.username}` : `/timelines/${name}`; diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx index a2437ae5..b4e25ba1 100644 --- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx +++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx @@ -3,7 +3,7 @@ import { useHistory } from "react-router"; import { validateTimelineName } from "@/services/timeline"; import OperationDialog from "../common/OperationDialog"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; interface TimelineCreateDialogProps { open: boolean; diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx index 901697db..9e822528 100644 --- a/FrontEnd/src/views/common/user/UserAvatar.tsx +++ b/FrontEnd/src/views/common/user/UserAvatar.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; export interface UserAvatarProps extends React.ImgHTMLAttributes { diff --git a/FrontEnd/src/views/home/TimelineListView.tsx b/FrontEnd/src/views/home/TimelineListView.tsx index 975875af..2fb54820 100644 --- a/FrontEnd/src/views/home/TimelineListView.tsx +++ b/FrontEnd/src/views/home/TimelineListView.tsx @@ -2,7 +2,7 @@ import React from "react"; import { convertI18nText, I18nText } from "@/common"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; @@ -13,7 +13,7 @@ interface TimelineListItemProps { const TimelineListItem: React.FC = ({ timeline }) => { const url = React.useMemo( () => - timeline.name.startsWith("@") + timeline.name.startsWith("src") ? `/users/${timeline.owner.username}` : `/timelines/${timeline.name}`, [timeline] diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx index efc364d7..0eca23ee 100644 --- a/FrontEnd/src/views/home/index.tsx +++ b/FrontEnd/src/views/home/index.tsx @@ -1,8 +1,8 @@ import React from "react"; import { useHistory } from "react-router"; -import { HttpTimelineInfo } from "http/timeline"; -import { getHttpHighlightClient } from "http/highlight"; +import { HttpTimelineInfo } from "@/http/timeline"; +import { getHttpHighlightClient } from "@/http/highlight"; import SearchInput from "../common/SearchInput"; import TimelineListView from "./TimelineListView"; diff --git a/FrontEnd/src/views/search/index.tsx b/FrontEnd/src/views/search/index.tsx index 14a9709c..9a26802d 100644 --- a/FrontEnd/src/views/search/index.tsx +++ b/FrontEnd/src/views/search/index.tsx @@ -4,9 +4,9 @@ import { Container, Row } from "react-bootstrap"; import { useHistory, useLocation } from "react-router"; import { Link } from "react-router-dom"; -import { HttpNetworkError } from "http/common"; -import { getHttpSearchClient } from "http/search"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpNetworkError } from "@/http/common"; +import { getHttpSearchClient } from "@/http/search"; +import { HttpTimelineInfo } from "@/http/timeline"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; @@ -14,7 +14,7 @@ import UserAvatar from "../common/user/UserAvatar"; const TimelineSearchResultItemView: React.FC<{ timeline: HttpTimelineInfo; }> = ({ timeline }) => { - const link = timeline.name.startsWith("@") + const link = timeline.name.startsWith("src") ? `users/${timeline.owner.username}` : `timelines/${timeline.name}`; @@ -33,7 +33,7 @@ const TimelineSearchResultItemView: React.FC<{ /> {timeline.owner.nickname} - @{timeline.owner.username} + src{timeline.owner.username}
diff --git a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx index 338d2112..c4f6f492 100644 --- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx @@ -7,7 +7,7 @@ import { UiLogicError } from "@/common"; import { useUserLoggedIn } from "@/services/user"; -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx index e6420f36..4b44cdd6 100644 --- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx @@ -1,4 +1,4 @@ -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; import { useUserLoggedIn } from "@/services/user"; import React from "react"; diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx index df43d8d2..1b9d6d2a 100644 --- a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx +++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx @@ -1,6 +1,6 @@ import React from "react"; import classnames from "classnames"; -import { HubConnectionState } from "@microsoft/signalr"; +import { HubConnectionState } from "srcmicrosoft/signalr"; import { useTranslation } from "react-i18next"; export interface ConnectionStatusBadgeProps { diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx index 1514d28f..685e17be 100644 --- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -4,7 +4,7 @@ import { Form, Spinner } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { Prompt } from "react-router"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import FlatButton from "../common/FlatButton"; import TabPages from "../common/TabPages"; diff --git a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx index 21c5272e..001e52d7 100644 --- a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx index 40619e64..31ea5870 100644 --- a/FrontEnd/src/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/views/timeline-common/Timeline.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { HubConnectionState } from "@microsoft/signalr"; +import { HubConnectionState } from "srcmicrosoft/signalr"; import { HttpForbiddenError, HttpNetworkError, HttpNotFoundError, -} from "http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +} from "@/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { getTimelinePostUpdate$ } from "@/services/timeline"; diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx index 3d4de8b8..1ef085a7 100644 --- a/FrontEnd/src/views/timeline-common/TimelineMember.tsx +++ b/FrontEnd/src/views/timeline-common/TimelineMember.tsx @@ -4,12 +4,12 @@ import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; import { convertI18nText, I18nText } from "@/common"; -import { HttpUser } from "http/user"; -import { getHttpSearchClient } from "http/search"; +import { HttpUser } from "@/http/user"; +import { getHttpSearchClient } from "@/http/search"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; const TimelineMemberItem: React.FC<{ user: HttpUser; @@ -27,7 +27,7 @@ const TimelineMemberItem: React.FC<{ {user.nickname} - {"@" + user.username} + {"src" + user.username} {onAction ? ( diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx index 038ea3ab..623d643f 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx @@ -2,8 +2,8 @@ import React from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { getHttpHighlightClient } from "http/highlight"; -import { getHttpBookmarkClient } from "http/bookmark"; +import { getHttpHighlightClient } from "@/http/highlight"; +import { getHttpBookmarkClient } from "@/http/bookmark"; import { useUser } from "@/services/user"; import { pushAlert } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx index 44926cc6..658ce502 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx @@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next"; import { Container } from "react-bootstrap"; import { HubConnectionState } from "@microsoft/signalr"; -import { HttpNetworkError, HttpNotFoundError } from "http/common"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { HttpNetworkError, HttpNotFoundError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import { getAlertHost } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx index d569a2d7..37f02a82 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { HttpTimelinePostInfo } from "http/timeline"; +import { HttpTimelinePostInfo } from "@/http/timeline"; import useScrollToTop from "@/utilities/useScrollToTop"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx index f1b53335..607b72c9 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx @@ -4,8 +4,8 @@ import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; -import { HttpNetworkError } from "http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { HttpNetworkError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { useUser } from "@/services/user"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx index 0f470fd6..1f9f02a5 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx @@ -10,10 +10,10 @@ import { HttpTimelineInfo, HttpTimelinePostInfo, HttpTimelinePostPostRequestData, -} from "http/timeline"; +} from "@/http/timeline"; import { pushAlert } from "@/services/alert"; -import { base64 } from "http/common"; +import { base64 } from "@/http/common"; import BlobImage from "../common/BlobImage"; import LoadingButton from "../common/LoadingButton"; @@ -138,7 +138,7 @@ const TimelinePostEdit: React.FC = (props) => { (kind === "text" && text.length !== 0) || (kind === "image" && image != null); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // eslint-disable-next-line srctypescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); const notifyHeightChange = (): void => { diff --git a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx index 49284720..ba204b72 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from "react"; import classnames from "classnames"; -import { HttpTimelinePostInfo } from "http/timeline"; +import { HttpTimelinePostInfo } from "@/http/timeline"; import TimelinePostView from "./TimelinePostView"; import TimelineDateLabel from "./TimelineDateLabel"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index e8b32c71..f7b81478 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { pushAlert } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx index 83b24d01..70f72025 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -6,7 +6,7 @@ import { HttpTimelinePatchRequest, kTimelineVisibilities, TimelineVisibility, -} from "http/timeline"; +} from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx index e031b565..86063843 100644 --- a/FrontEnd/src/views/timeline/TimelineCard.tsx +++ b/FrontEnd/src/views/timeline/TimelineCard.tsx @@ -29,7 +29,7 @@ const TimelineCard: React.FC = (props) => { /> {timeline.owner.nickname} - @{timeline.owner.username} + src{timeline.owner.username}
diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx index 8821507d..dbca62ca 100644 --- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useHistory } from "react-router"; import { Trans } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/user/index.tsx b/FrontEnd/src/views/user/index.tsx index 57454d0d..0013b254 100644 --- a/FrontEnd/src/views/user/index.tsx +++ b/FrontEnd/src/views/user/index.tsx @@ -14,7 +14,7 @@ const UserPage: React.FC = () => { return ( <> setReloadKey(reloadKey + 1)} diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json new file mode 100644 index 00000000..3afe2c3e --- /dev/null +++ b/FrontEnd/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react", + "noEmit": true, + "types": ["vite/client"], + "sourceMap": true, + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["./src"] +} diff --git a/FrontEnd/vite.config.js b/FrontEnd/vite.config.js index 6e8cde1b..2e85c36a 100644 --- a/FrontEnd/vite.config.js +++ b/FrontEnd/vite.config.js @@ -7,6 +7,9 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [reactRefresh()], + resolve: { + alias: [{ find: "@", replacement: "/src" }], + }, server: { port: 13000, proxy: { -- cgit v1.2.3 From ddce03a67708249eec129eb36744be460345bd75 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 16:23:44 +0800 Subject: ... --- FrontEnd/.eslintrc.js | 2 +- FrontEnd/package.json | 15 -- FrontEnd/postcss.config.js | 5 - FrontEnd/src/index.css | 155 +++++++++++ FrontEnd/src/index.sass | 120 --------- FrontEnd/src/index.tsx | 8 +- FrontEnd/src/palette.ts | 2 + FrontEnd/src/views/about/about.sass | 4 - FrontEnd/src/views/about/index.css | 4 + FrontEnd/src/views/about/index.tsx | 2 + FrontEnd/src/views/admin/Admin.tsx | 2 + FrontEnd/src/views/admin/admin.sass | 22 -- FrontEnd/src/views/admin/index.css | 19 ++ FrontEnd/src/views/center/TimelineBoard.tsx | 17 +- FrontEnd/src/views/center/center.sass | 36 --- FrontEnd/src/views/center/index.css | 73 ++++++ FrontEnd/src/views/center/index.tsx | 2 + FrontEnd/src/views/common/AppBar.tsx | 2 + FrontEnd/src/views/common/FlatButton.tsx | 36 --- FrontEnd/src/views/common/button/FlatButton.css | 45 ++++ FrontEnd/src/views/common/button/FlatButton.tsx | 36 +++ FrontEnd/src/views/common/common.sass | 191 -------------- FrontEnd/src/views/common/index.css | 273 +++++++++++++++++++ FrontEnd/src/views/home/home.sass | 29 --- FrontEnd/src/views/home/index.css | 73 ++++++ FrontEnd/src/views/home/index.tsx | 2 + FrontEnd/src/views/login/index.css | 3 + FrontEnd/src/views/login/index.tsx | 2 + FrontEnd/src/views/login/login.sass | 2 - FrontEnd/src/views/search/index.css | 15 ++ FrontEnd/src/views/search/index.tsx | 2 + FrontEnd/src/views/search/search.sass | 13 - FrontEnd/src/views/settings/index.css | 24 ++ FrontEnd/src/views/settings/index.tsx | 2 + FrontEnd/src/views/settings/settings.sass | 14 - FrontEnd/src/views/timeline-common/Timeline.tsx | 4 +- FrontEnd/src/views/timeline-common/index.css | 289 +++++++++++++++++++++ .../src/views/timeline-common/timeline-common.sass | 259 ------------------ FrontEnd/src/views/timeline/timeline.sass | 0 FrontEnd/src/views/user/index.css | 9 + FrontEnd/src/views/user/index.tsx | 2 + FrontEnd/src/views/user/user.sass | 7 - 42 files changed, 1054 insertions(+), 768 deletions(-) delete mode 100644 FrontEnd/postcss.config.js create mode 100644 FrontEnd/src/index.css delete mode 100644 FrontEnd/src/index.sass delete mode 100644 FrontEnd/src/views/about/about.sass create mode 100644 FrontEnd/src/views/about/index.css delete mode 100644 FrontEnd/src/views/admin/admin.sass create mode 100644 FrontEnd/src/views/admin/index.css delete mode 100644 FrontEnd/src/views/center/center.sass create mode 100644 FrontEnd/src/views/center/index.css delete mode 100644 FrontEnd/src/views/common/FlatButton.tsx create mode 100644 FrontEnd/src/views/common/button/FlatButton.css create mode 100644 FrontEnd/src/views/common/button/FlatButton.tsx delete mode 100644 FrontEnd/src/views/common/common.sass create mode 100644 FrontEnd/src/views/common/index.css delete mode 100644 FrontEnd/src/views/home/home.sass create mode 100644 FrontEnd/src/views/home/index.css create mode 100644 FrontEnd/src/views/login/index.css delete mode 100644 FrontEnd/src/views/login/login.sass create mode 100644 FrontEnd/src/views/search/index.css delete mode 100644 FrontEnd/src/views/search/search.sass create mode 100644 FrontEnd/src/views/settings/index.css delete mode 100644 FrontEnd/src/views/settings/settings.sass create mode 100644 FrontEnd/src/views/timeline-common/index.css delete mode 100644 FrontEnd/src/views/timeline-common/timeline-common.sass delete mode 100644 FrontEnd/src/views/timeline/timeline.sass create mode 100644 FrontEnd/src/views/user/index.css delete mode 100644 FrontEnd/src/views/user/user.sass (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/.eslintrc.js b/FrontEnd/.eslintrc.js index 611b965f..93b98978 100644 --- a/FrontEnd/.eslintrc.js +++ b/FrontEnd/.eslintrc.js @@ -18,7 +18,7 @@ module.exports = { }, parser: "@typescript-eslint/parser", parserOptions: { - project: ["./src/tsconfig.json"], + project: ["./tsconfig.json"], ecmaFeatures: { jsx: true, }, diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 5d12899e..38dd85ff 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -39,18 +39,6 @@ "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx", "lint:fix": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx --fix" }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, "devDependencies": { "@types/color": "^3.0.1", "@types/lodash": "^4.14.170", @@ -70,10 +58,7 @@ "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^4.2.0", - "postcss": "^8.3.0", - "postcss-preset-env": "^6.7.0", "prettier": "^2.3.1", - "sass": "^1.34.1", "typescript": "^4.3.2", "vite": "^2.3.7" } diff --git a/FrontEnd/postcss.config.js b/FrontEnd/postcss.config.js deleted file mode 100644 index 9129aa1f..00000000 --- a/FrontEnd/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import postcssPresetEnv from "postcss-preset-env"; - -module.exports = { - plugins: [postcssPresetEnv()], -}; diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css new file mode 100644 index 00000000..ca0d4829 --- /dev/null +++ b/FrontEnd/src/index.css @@ -0,0 +1,155 @@ +.tl-color-primary { + color: var(--tl-primary-color); +} + +.tl-color-danger { + color: var(--tl-danger-color); +} + +small { + line-height: 1.2; +} + +.flex-fix-length { + flex-grow: 0; + flex-shrink: 0; +} + +.avatar { + width: 60px; + height: 60px; +} + +.avatar.large { + width: 100px; + height: 100px; +} + +.avatar.small { + width: 40px; + height: 40px; +} + +.icon-button { + font-size: 1.4rem; + cursor: pointer; +} + +.icon-button.large { + font-size: 1.6rem; +} + + + +.cursor-pointer { + cursor: pointer; +} + +textarea { + resize: none; +} + +.white-space-no-wrap { + white-space: nowrap; +} + +.cru-card { + border: 1px solid; + border-color: #e9ecef; + background: #f8f9fa; + transition: all 0.3s; +} + +.cru-card:hover { + border-color: var(--tl-primary-color); +} + +.full-viewport-center-child { + position: fixed; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +.text-orange { + color: #fd7e14; +} + +.text-yellow { + color: #ffc107; +} + +.text-button { + background: transparent; + border: none; +} +.text-button.primary { + color: #0d6efd; +} +.text-button.primary:hover { + color: #599bfe; +} +.text-button.secondary { + color: #6c757d; +} +.text-button.secondary:hover { + color: #939ba2; +} +.text-button.success { + color: #198754; +} +.text-button.success:hover { + color: #25c87c; +} +.text-button.info { + color: #0dcaf0; +} +.text-button.info:hover { + color: #54dbf6; +} +.text-button.warning { + color: #ffc107; +} +.text-button.warning:hover { + color: #ffd454; +} +.text-button.danger { + color: #dc3545; +} +.text-button.danger:hover { + color: #e77681; +} +.text-button.light { + color: #f8f9fa; +} +.text-button.light:hover { + color: white; +} +.text-button.dark { + color: #212529; +} +.text-button.dark:hover { + color: #434b53; +} + +.touch-action-none { + touch-action: none; +} + +i { + line-height: 1; +} + +.markdown-container { + white-space: initial; +} +.markdown-container img { + max-height: 200px; + max-width: 100%; +} + +a { + text-decoration: none; +} diff --git a/FrontEnd/src/index.sass b/FrontEnd/src/index.sass deleted file mode 100644 index 4cee155f..00000000 --- a/FrontEnd/src/index.sass +++ /dev/null @@ -1,120 +0,0 @@ -@import 'bootstrap/scss/bootstrap' -@import 'bootstrap-icons/font/bootstrap-icons.css' - -@import './views/common/common' -@import './views/common/alert/alert' -@import './views/center/center' -@import './views/home/home' -@import './views/about/about' -@import './views/login/login' -@import './views/settings/settings' -@import './views/timeline-common/timeline-common' -@import './views/timeline/timeline' -@import './views/user/user' -@import './views/search/search' - -@import './views/admin/admin' - -.tl-color-primary - color: var(--tl-primary-color) - -.tl-color-danger - color: var(--tl-danger-color) - -small - line-height: 1.2 - -.flex-fix-length - flex-grow: 0 - flex-shrink: 0 - -.position-lt - left: 0 - top: 0 - -.avatar - width: 60px - height: 60px - &.large - width: 100px - height: 100px - &.small - width: 40px - height: 40px - -.icon-button - font-size: 1.4rem - cursor: pointer - &.large - font-size: 1.6rem - -.flat-button - cursor: pointer - padding: 0.2em 0.5em - border-radius: 0.2em - &:hover:not(.disabled) - background-color: $gray-200 - &.disabled - cursor: default - @each $color, $value in $theme-colors - &.#{$color} - color: $value - &.disabled - color: adjust-color($value, $lightness: +15%) - -.cursor-pointer - cursor: pointer - -textarea - resize: none - -.white-space-no-wrap - white-space: nowrap - -.cru-card - @extend .shadow - @extend .rounded - border: 1px solid - border-color: $gray-200 - background: $gray-100 - transition: all 0.3s - &:hover - border-color: var(--tl-primary-color) - -.full-viewport-center-child - position: fixed - width: 100vw - height: 100vh - display: flex - justify-content: center - align-items: center - -.text-orange - color: $orange - -.text-yellow - color: $yellow - -.text-button - background: transparent - border: none - @each $color, $value in $theme-colors - &.#{$color} - color: $value - &:hover - color: adjust-color($value, $lightness: +15%) - -.touch-action-none - touch-action: none - -i - line-height: 1 - -.markdown-container - white-space: initial - img - max-height: 200px - max-width: 100% - -a - text-decoration: none diff --git a/FrontEnd/src/index.tsx b/FrontEnd/src/index.tsx index fb0c8899..83c25792 100644 --- a/FrontEnd/src/index.tsx +++ b/FrontEnd/src/index.tsx @@ -3,17 +3,19 @@ import "core-js/modules/es.promise"; import "core-js/modules/es.array.iterator"; import "pepjs"; +import "bootstrap/dist/css/bootstrap.css"; +import "bootstrap-icons/font/bootstrap-icons.css"; + import React from "react"; import ReactDOM from "react-dom"; -import "./index.sass"; +import "./index.css"; import "./i18n"; +import "./palette"; import App from "./App"; -import "./palette"; - import { userService } from "./services/user"; void userService.checkLoginState(); diff --git a/FrontEnd/src/palette.ts b/FrontEnd/src/palette.ts index c4f4f4f9..11b3de85 100644 --- a/FrontEnd/src/palette.ts +++ b/FrontEnd/src/palette.ts @@ -30,6 +30,8 @@ export interface Palette { [key: string]: PaletteColor; } +export type PaletteColorType = keyof Palette; + export function generatePaletteColor(color: string): PaletteColor { const c = Color(color); return { diff --git a/FrontEnd/src/views/about/about.sass b/FrontEnd/src/views/about/about.sass deleted file mode 100644 index f4d00cae..00000000 --- a/FrontEnd/src/views/about/about.sass +++ /dev/null @@ -1,4 +0,0 @@ -.about-link-icon - @extend .mx-2 - width: 1.2em - height: 1.2em diff --git a/FrontEnd/src/views/about/index.css b/FrontEnd/src/views/about/index.css new file mode 100644 index 00000000..2574f4b7 --- /dev/null +++ b/FrontEnd/src/views/about/index.css @@ -0,0 +1,4 @@ +.about-link-icon { + width: 1.2em; + height: 1.2em; +} diff --git a/FrontEnd/src/views/about/index.tsx b/FrontEnd/src/views/about/index.tsx index a8a53a97..db4814c4 100644 --- a/FrontEnd/src/views/about/index.tsx +++ b/FrontEnd/src/views/about/index.tsx @@ -4,6 +4,8 @@ import { useTranslation, Trans } from "react-i18next"; import authorAvatarUrl from "./author-avatar.png"; import githubLogoUrl from "./github.png"; +import "./index.css"; + const frontendCredits: { name: string; url: string; diff --git a/FrontEnd/src/views/admin/Admin.tsx b/FrontEnd/src/views/admin/Admin.tsx index 0b6d1f05..34e7e2f6 100644 --- a/FrontEnd/src/views/admin/Admin.tsx +++ b/FrontEnd/src/views/admin/Admin.tsx @@ -9,6 +9,8 @@ import AdminNav from "./AdminNav"; import UserAdmin from "./UserAdmin"; import MoreAdmin from "./MoreAdmin"; +import "./index.css"; + interface AdminProps { user: AuthUser; } diff --git a/FrontEnd/src/views/admin/admin.sass b/FrontEnd/src/views/admin/admin.sass deleted file mode 100644 index 1ce010f8..00000000 --- a/FrontEnd/src/views/admin/admin.sass +++ /dev/null @@ -1,22 +0,0 @@ -.admin-user-item - position: relative - - .edit-mask - position: absolute - top: 0 - left: 0 - bottom: 0 - right: 0 - - background: #ffffffc5 - position: absolute - - display: flex - justify-content: center - align-items: center - - @include media-breakpoint-down(xs) - flex-direction: column - - button - margin: 0.5em 2em diff --git a/FrontEnd/src/views/admin/index.css b/FrontEnd/src/views/admin/index.css new file mode 100644 index 00000000..00917600 --- /dev/null +++ b/FrontEnd/src/views/admin/index.css @@ -0,0 +1,19 @@ +.admin-user-item { + position: relative; +} +.admin-user-item .edit-mask { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: #ffffffc5; + position: absolute; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} +.admin-user-item .edit-mask button { + margin: 0.5em 2em; +} diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index 840c0415..a6a60b3d 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -9,6 +9,7 @@ import { HttpTimelineInfo } from "@/http/timeline"; import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; import LoadFailReload from "../common/LoadFailReload"; +import FlatButton from "../common/button/FlatButton"; interface TimelineBoardItemProps { timeline: HttpTimelineInfo; @@ -231,23 +232,19 @@ const TimelineBoardUI: React.FC = (props) => { {title != null &&

{title}

} {editable && (editing ? ( -
{ setEditing(false); }} - > - {t("done")} -
+ /> ) : ( -
{ setEditing(true); }} - > - {t("edit")} -
+ /> ))}
{(() => { diff --git a/FrontEnd/src/views/center/center.sass b/FrontEnd/src/views/center/center.sass deleted file mode 100644 index c0dfb9c0..00000000 --- a/FrontEnd/src/views/center/center.sass +++ /dev/null @@ -1,36 +0,0 @@ -.timeline-board - @extend .cru-card - @extend .d-flex - @extend .flex-column - @extend .py-3 - min-height: 200px - height: 100% - position: relative - -.timeline-board-header - @extend .px-3 - display: flex - align-items: center - justify-content: space-between - -.timeline-board-item - font-size: 1.1em - @extend .px-3 - height: 48px - transition: background 0.3s - display: flex - align-items: center - .icon - height: 1.3em - color: black - @extend .me-2 - &:hover - background: $gray-300 - .right - display: flex - align-items: center - flex-shrink: 0 - .title - white-space: nowrap - overflow: hidden - text-overflow: ellipsis diff --git a/FrontEnd/src/views/center/index.css b/FrontEnd/src/views/center/index.css new file mode 100644 index 00000000..516aba52 --- /dev/null +++ b/FrontEnd/src/views/center/index.css @@ -0,0 +1,73 @@ +.timeline-board { + min-height: 200px; + height: 100%; + position: relative; +} + +.timeline-board-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.timeline-board-item { + font-size: 1.1em; + height: 48px; + transition: background 0.3s; + display: flex; + align-items: center; +} +.timeline-board-item .icon { + height: 1.3em; + color: black; +} +.timeline-board-item:hover { + background: #dee2e6; +} +.timeline-board-item .right { + display: flex; + align-items: center; + flex-shrink: 0; +} +.timeline-board-item .title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.home-timeline-list-item { + display: flex; + align-items: center; +} + +.home-timeline-list-item-timeline { + transition: background 0.8s; + animation: 0.8s home-timeline-list-item-timeline-enter; +} +.home-timeline-list-item-timeline:hover { + background: #e9ecef; +} + +@keyframes home-timeline-list-item-timeline-enter { + from { + transform: translate(-100%, 0); + opacity: 0; + } +} +.home-timeline-list-item-line { + width: 80px; + flex-shrink: 0; +} + +@keyframes home-timeline-list-loading-head-animation { + from { + transform: translate(0, -30px); + opacity: 1; + } + to { + opacity: 0; + } +} +.home-timeline-list-loading-head { + animation: 1s infinite home-timeline-list-loading-head-animation; +} diff --git a/FrontEnd/src/views/center/index.tsx b/FrontEnd/src/views/center/index.tsx index 0a2abb2c..28d8b372 100644 --- a/FrontEnd/src/views/center/index.tsx +++ b/FrontEnd/src/views/center/index.tsx @@ -9,6 +9,8 @@ import SearchInput from "../common/SearchInput"; import CenterBoards from "./CenterBoards"; import TimelineCreateDialog from "./TimelineCreateDialog"; +import "./index.css"; + const HomePage: React.FC = () => { const history = useHistory(); diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx index 91dfbee9..ebc8bf0c 100644 --- a/FrontEnd/src/views/common/AppBar.tsx +++ b/FrontEnd/src/views/common/AppBar.tsx @@ -9,6 +9,8 @@ import { useUser } from "@/services/user"; import TimelineLogo from "./TimelineLogo"; import UserAvatar from "./user/UserAvatar"; +import "./index.css"; + const AppBar: React.FC = (_) => { const { t } = useTranslation(); diff --git a/FrontEnd/src/views/common/FlatButton.tsx b/FrontEnd/src/views/common/FlatButton.tsx deleted file mode 100644 index b1f7a051..00000000 --- a/FrontEnd/src/views/common/FlatButton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import classnames from "classnames"; - -import { BootstrapThemeColor } from "@/common"; - -export interface FlatButtonProps { - variant?: BootstrapThemeColor | string; - disabled?: boolean; - className?: string; - style?: React.CSSProperties; - onClick?: () => void; -} - -const FlatButton: React.FC = (props) => { - const { disabled, className, style } = props; - const variant = props.variant ?? "primary"; - - const onClick = disabled ? undefined : props.onClick; - - return ( -
- {props.children} -
- ); -}; - -export default FlatButton; diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css new file mode 100644 index 00000000..779e3562 --- /dev/null +++ b/FrontEnd/src/views/common/button/FlatButton.css @@ -0,0 +1,45 @@ +.cru-flat-button { + cursor: pointer; + padding: 0.2em 0.5em; + border-radius: 0.2em; +} + +.cru-flat-button:hover:not(.disabled) { + background-color: #e9ecef; +} + +.cru-flat-button.disabled { + cursor: default; +} + +.cru-flat-button.primary { + color: var(--tl-primary-color); +} + +.cru-flat-button.primary.disabled { + color: var(--tl-primary-lighter-color); +} + +.cru-flat-button.secondary { + color: var(--tl-secondary-color); +} + +.cru-flat-button.secondary.disabled { + color: var(--tl-secondary-lighter-color); +} + +.cru-flat-button.success { + color: var(--tl-success-color); +} + +.cru-flat-button.success.disabled { + color: var(--tl-success-lighter-color); +} + +.cru-flat-button.danger { + color: var(--tl-danger-color); +} + +.cru-flat-button.danger.disabled { + color: var(--tl-danger-ligher-color); +} diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx new file mode 100644 index 00000000..0727eb88 --- /dev/null +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; + +import "./FlatButton.css"; +import classNames from "classnames"; + +function _FlatButton( + { + text, + color, + onClick, + }: { + text: I18nText; + color?: PaletteColorType; + onClick?: () => void; + }, + ref: React.ForwardedRef +): React.ReactElement | null { + const { t } = useTranslation(); + + return ( + + ); +} + +const FlatButton = React.forwardRef(_FlatButton); +export default FlatButton; diff --git a/FrontEnd/src/views/common/common.sass b/FrontEnd/src/views/common/common.sass deleted file mode 100644 index cbf7292e..00000000 --- a/FrontEnd/src/views/common/common.sass +++ /dev/null @@ -1,191 +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 - -.app-bar - display: flex - align-items: center - height: 56px - - position: fixed - z-index: 1030 - top: 0 - left: 0 - right: 0 - - background-color: var(--tl-primary-color) - - transition: background-color 1s - - a - color: var(--tl-text-on-primary-inactive-color) - text-decoration: none - margin: 0 1em - - &:hover - color: var(--tl-text-on-primary-color) - - &.active - color: var(--tl-text-on-primary-color) - -.app-bar-brand - display: flex - align-items: center - -.app-bar-brand-icon - height: 2em - -.app-bar-main-area - display: flex - flex-grow: 1 - -.app-bar-link-area - display: flex - align-items: center - flex-shrink: 0 - -.app-bar-user-area - display: flex - align-items: center - flex-shrink: 0 - margin-left: auto - -.small-screen - .app-bar-main-area - position: absolute - top: 56px - left: 0 - right: 0 - - transform-origin: top - transition: transform 0.6s, background-color 1s - - background-color: var(--tl-primary-color) - - flex-direction: column - - &.app-bar-collapse - transform: scale(1,0) - - a - text-align: left - padding: 0.5em 0.5em - - .app-bar-link-area - flex-direction: column - align-items: stretch - - .app-bar-user-area - flex-direction: column - align-items: stretch - margin-left: unset - - .app-bar-avatar - align-self: flex-end - -.app-bar-toggler - margin-left: auto - font-size: 2em - margin-right: 1em - color: var(--tl-text-on-primary-color) - cursor: pointer - user-select: none - -.cru-skeleton - padding: 0 1em - -.cru-skeleton-line - height: 1em - background-color: #e6e6e6 - margin: 0.7em 0 - border-radius: 0.2em - - &.last - width: 50% - -.cru-full-page - position: fixed - z-index: 1031 - left: 0 - top: 0 - right: 0 - bottom: 0 - background-color: white - padding-top: 56px - -.cru-full-page-top-bar - height: 56px - - position: absolute - top: 0 - left: 0 - right: 0 - z-index: 1 - - background-color: var(--tl-primary-color) - - display: flex - align-items: center - -.cru-full-page-content-container - overflow: scroll - -.cru-menu - min-width: 200px - -.cru-menu-item - font-size: 1.2em - padding: 0.5em 1.5em - cursor: pointer - - @each $color, $value in $theme-colors - &.color-#{$color} - color: $value - - &:hover - color: white - background-color: $value - -.cru-menu-item-icon - margin-right: 1em - -.cru-menu-divider - border-top: 1px solid $gray-200 - -.cru-tab-pages-action-area - display: flex - align-items: center - -.cru-search-input - display: flex - flex-wrap: wrap diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css new file mode 100644 index 00000000..bfd82b58 --- /dev/null +++ b/FrontEnd/src/views/common/index.css @@ -0,0 +1,273 @@ +.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, 0.8); + touch-action: none; +} + +.image-cropper-handler { + position: absolute; + width: 26px; + height: 26px; + border: black solid 2px; + border-radius: 50%; + background: white; + touch-action: none; +} + +.app-bar { + display: flex; + align-items: center; + height: 56px; + position: fixed; + z-index: 1030; + top: 0; + left: 0; + right: 0; + background-color: var(--tl-primary-color); + transition: background-color 1s; +} +.app-bar a { + color: var(--tl-text-on-primary-inactive-color); + text-decoration: none; + margin: 0 1em; +} +.app-bar a:hover { + color: var(--tl-text-on-primary-color); +} +.app-bar a.active { + color: var(--tl-text-on-primary-color); +} + +.app-bar-brand { + display: flex; + align-items: center; +} + +.app-bar-brand-icon { + height: 2em; +} + +.app-bar-main-area { + display: flex; + flex-grow: 1; +} + +.app-bar-link-area { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.app-bar-user-area { + display: flex; + align-items: center; + flex-shrink: 0; + margin-left: auto; +} + +.small-screen .app-bar-main-area { + position: absolute; + top: 56px; + left: 0; + right: 0; + transform-origin: top; + transition: transform 0.6s, background-color 1s; + background-color: var(--tl-primary-color); + flex-direction: column; +} +.small-screen .app-bar-main-area.app-bar-collapse { + transform: scale(1, 0); +} +.small-screen .app-bar-main-area a { + text-align: left; + padding: 0.5em 0.5em; +} +.small-screen .app-bar-link-area { + flex-direction: column; + align-items: stretch; +} +.small-screen .app-bar-user-area { + flex-direction: column; + align-items: stretch; + margin-left: unset; +} +.small-screen .app-bar-avatar { + align-self: flex-end; +} + +.app-bar-toggler { + margin-left: auto; + font-size: 2em; + margin-right: 1em; + color: var(--tl-text-on-primary-color); + cursor: pointer; + user-select: none; +} + +.cru-skeleton { + padding: 0 1em; +} + +.cru-skeleton-line { + height: 1em; + background-color: #e6e6e6; + margin: 0.7em 0; + border-radius: 0.2em; +} +.cru-skeleton-line.last { + width: 50%; +} + +.cru-full-page { + position: fixed; + z-index: 1031; + left: 0; + top: 0; + right: 0; + bottom: 0; + background-color: white; + padding-top: 56px; +} + +.cru-full-page-top-bar { + height: 56px; + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1; + background-color: var(--tl-primary-color); + display: flex; + align-items: center; +} + +.cru-full-page-content-container { + overflow: scroll; +} + +.cru-menu { + min-width: 200px; +} + +.cru-menu-item { + font-size: 1.2em; + padding: 0.5em 1.5em; + cursor: pointer; +} +.cru-menu-item.color-primary { + color: #0d6efd; +} +.cru-menu-item.color-primary:hover { + color: white; + background-color: #0d6efd; +} +.cru-menu-item.color-secondary { + color: #6c757d; +} +.cru-menu-item.color-secondary:hover { + color: white; + background-color: #6c757d; +} +.cru-menu-item.color-success { + color: #198754; +} +.cru-menu-item.color-success:hover { + color: white; + background-color: #198754; +} +.cru-menu-item.color-info { + color: #0dcaf0; +} +.cru-menu-item.color-info:hover { + color: white; + background-color: #0dcaf0; +} +.cru-menu-item.color-warning { + color: #ffc107; +} +.cru-menu-item.color-warning:hover { + color: white; + background-color: #ffc107; +} +.cru-menu-item.color-danger { + color: #dc3545; +} +.cru-menu-item.color-danger:hover { + color: white; + background-color: #dc3545; +} +.cru-menu-item.color-light { + color: #f8f9fa; +} +.cru-menu-item.color-light:hover { + color: white; + background-color: #f8f9fa; +} +.cru-menu-item.color-dark { + color: #212529; +} +.cru-menu-item.color-dark:hover { + color: white; + background-color: #212529; +} + +.cru-menu-item-icon { + margin-right: 1em; +} + +.cru-menu-divider { + border-top: 1px solid #e9ecef; +} + +.cru-tab-pages-action-area { + display: flex; + align-items: center; +} + +.cru-search-input { + display: flex; + flex-wrap: wrap; +} + +.alert-container { + position: fixed; + z-index: 1070; +} + +@media (min-width: 576px) { + .alert-container { + bottom: 0; + right: 0; + } +} +@media (max-width: 575.98px) { + .alert-container { + bottom: 0; + right: 0; + left: 0; + text-align: center; + } +} diff --git a/FrontEnd/src/views/home/home.sass b/FrontEnd/src/views/home/home.sass deleted file mode 100644 index b4cda586..00000000 --- a/FrontEnd/src/views/home/home.sass +++ /dev/null @@ -1,29 +0,0 @@ -.home-timeline-list-item - display: flex - align-items: center - -.home-timeline-list-item-timeline - transition: background 0.8s - animation: 0.8s home-timeline-list-item-timeline-enter - &:hover - background: $gray-200 - -@keyframes home-timeline-list-item-timeline-enter - from - transform: translate(-100%,0) - opacity: 0 - -.home-timeline-list-item-line - width: 80px - flex-shrink: 0 - -@keyframes home-timeline-list-loading-head-animation - from - transform: translate(0,-30px) - opacity: 1 - - to - opacity: 0 - -.home-timeline-list-loading-head - animation: 1s infinite home-timeline-list-loading-head-animation diff --git a/FrontEnd/src/views/home/index.css b/FrontEnd/src/views/home/index.css new file mode 100644 index 00000000..516aba52 --- /dev/null +++ b/FrontEnd/src/views/home/index.css @@ -0,0 +1,73 @@ +.timeline-board { + min-height: 200px; + height: 100%; + position: relative; +} + +.timeline-board-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.timeline-board-item { + font-size: 1.1em; + height: 48px; + transition: background 0.3s; + display: flex; + align-items: center; +} +.timeline-board-item .icon { + height: 1.3em; + color: black; +} +.timeline-board-item:hover { + background: #dee2e6; +} +.timeline-board-item .right { + display: flex; + align-items: center; + flex-shrink: 0; +} +.timeline-board-item .title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.home-timeline-list-item { + display: flex; + align-items: center; +} + +.home-timeline-list-item-timeline { + transition: background 0.8s; + animation: 0.8s home-timeline-list-item-timeline-enter; +} +.home-timeline-list-item-timeline:hover { + background: #e9ecef; +} + +@keyframes home-timeline-list-item-timeline-enter { + from { + transform: translate(-100%, 0); + opacity: 0; + } +} +.home-timeline-list-item-line { + width: 80px; + flex-shrink: 0; +} + +@keyframes home-timeline-list-loading-head-animation { + from { + transform: translate(0, -30px); + opacity: 1; + } + to { + opacity: 0; + } +} +.home-timeline-list-loading-head { + animation: 1s infinite home-timeline-list-loading-head-animation; +} diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx index 0eca23ee..ddb72e76 100644 --- a/FrontEnd/src/views/home/index.tsx +++ b/FrontEnd/src/views/home/index.tsx @@ -8,6 +8,8 @@ import SearchInput from "../common/SearchInput"; import TimelineListView from "./TimelineListView"; import WebsiteIntroduction from "./WebsiteIntroduction"; +import "./index.css"; + const highlightTimelineMessageMap = { loading: "home.loadingHighlightTimelines", done: "home.loadedHighlightTimelines", diff --git a/FrontEnd/src/views/login/index.css b/FrontEnd/src/views/login/index.css new file mode 100644 index 00000000..dca7054d --- /dev/null +++ b/FrontEnd/src/views/login/index.css @@ -0,0 +1,3 @@ +.login-container { + max-width: 600px; +} diff --git a/FrontEnd/src/views/login/index.tsx b/FrontEnd/src/views/login/index.tsx index 6adcef39..a39a9972 100644 --- a/FrontEnd/src/views/login/index.tsx +++ b/FrontEnd/src/views/login/index.tsx @@ -8,6 +8,8 @@ import { useUser, userService } from "@/services/user"; import AppBar from "../common/AppBar"; import LoadingButton from "../common/LoadingButton"; +import "./index.css"; + const LoginPage: React.FC = (_) => { const { t } = useTranslation(); const history = useHistory(); diff --git a/FrontEnd/src/views/login/login.sass b/FrontEnd/src/views/login/login.sass deleted file mode 100644 index 0bf385f5..00000000 --- a/FrontEnd/src/views/login/login.sass +++ /dev/null @@ -1,2 +0,0 @@ -.login-container - max-width: 600px diff --git a/FrontEnd/src/views/search/index.css b/FrontEnd/src/views/search/index.css new file mode 100644 index 00000000..6ff4d9fa --- /dev/null +++ b/FrontEnd/src/views/search/index.css @@ -0,0 +1,15 @@ +.timeline-search-result-item { + border: 1px solid; + border-color: #e9ecef; + background: #f8f9fa; + transition: all 0.3s; +} +.timeline-search-result-item:hover { + border-color: #0d6efd; +} + +.timeline-search-result-item-avatar { + width: 2em; + height: 2em; + border-radius: 50%; +} diff --git a/FrontEnd/src/views/search/index.tsx b/FrontEnd/src/views/search/index.tsx index 9a26802d..f5018c3e 100644 --- a/FrontEnd/src/views/search/index.tsx +++ b/FrontEnd/src/views/search/index.tsx @@ -11,6 +11,8 @@ import { HttpTimelineInfo } from "@/http/timeline"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; +import "./index.css"; + const TimelineSearchResultItemView: React.FC<{ timeline: HttpTimelineInfo; }> = ({ timeline }) => { diff --git a/FrontEnd/src/views/search/search.sass b/FrontEnd/src/views/search/search.sass deleted file mode 100644 index 83f297fe..00000000 --- a/FrontEnd/src/views/search/search.sass +++ /dev/null @@ -1,13 +0,0 @@ -.timeline-search-result-item - @extend .rounded - border: 1px solid - border-color: $gray-200 - background: $gray-100 - transition: all 0.3s - &:hover - border-color: $primary - -.timeline-search-result-item-avatar - width: 2em - height: 2em - border-radius: 50% diff --git a/FrontEnd/src/views/settings/index.css b/FrontEnd/src/views/settings/index.css new file mode 100644 index 00000000..566d501b --- /dev/null +++ b/FrontEnd/src/views/settings/index.css @@ -0,0 +1,24 @@ +.change-avatar-cropper-row { + max-height: 400px; +} + +.change-avatar-img { + min-width: 50%; + max-width: 100%; + max-height: 400px; +} + +.settings-item { + padding: 0.5em 1em; + transition: background 0.3s; + border-bottom: 1px solid #e9ecef; +} +.settings-item.first { + border-top: 1px solid #e9ecef; +} +.settings-item.clickable { + cursor: pointer; +} +.settings-item:hover { + background: #dee2e6; +} diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx index 04a2777a..f0bed222 100644 --- a/FrontEnd/src/views/settings/index.tsx +++ b/FrontEnd/src/views/settings/index.tsx @@ -9,6 +9,8 @@ import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; +import "./index.css"; + const ConfirmLogoutDialog: React.FC<{ onClose: () => void; onConfirm: () => void; diff --git a/FrontEnd/src/views/settings/settings.sass b/FrontEnd/src/views/settings/settings.sass deleted file mode 100644 index 8c6d24b8..00000000 --- a/FrontEnd/src/views/settings/settings.sass +++ /dev/null @@ -1,14 +0,0 @@ -.settings-item - padding: 0.5em 1em - transition: background 0.3s - border-bottom: 1px solid $gray-200 - - &.first - border-top: 1px solid $gray-200 - - &.clickable - cursor: pointer - - &:hover - background: $gray-300 - diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx index 31ea5870..21daa5e2 100644 --- a/FrontEnd/src/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/views/timeline-common/Timeline.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { HubConnectionState } from "srcmicrosoft/signalr"; +import { HubConnectionState } from "@microsoft/signalr"; import { HttpForbiddenError, @@ -14,6 +14,8 @@ import TimelinePagedPostListView from "./TimelinePagedPostListView"; import TimelineTop from "./TimelineTop"; import TimelineLoading from "./TimelineLoading"; +import "./index.css"; + export interface TimelineProps { className?: string; style?: React.CSSProperties; diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css new file mode 100644 index 00000000..89399961 --- /dev/null +++ b/FrontEnd/src/views/timeline-common/index.css @@ -0,0 +1,289 @@ +.timeline { + z-index: 0; + position: relative; + width: 100%; + overflow-wrap: break-word; + animation: 1s timeline-enter; +} + +@keyframes timeline-line-node-noncurrent { + to { + box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color); + } +} +@keyframes timeline-line-node-current { + to { + box-shadow: 0 0 20px 3px var(--tl-primary-enhance-lighter-color); + } +} +@keyframes timeline-line-node-loading { + to { + box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color); + } +} +@keyframes timeline-line-node-loading-edge { + from { + transform: rotate(0turn); + } + to { + transform: rotate(1turn); + } +} +@keyframes timeline-enter { + from { + transform: translate(0, -100vh); + } +} +@keyframes timeline-top-loading-enter { + from { + transform: translate(0, -100%); + } +} +@keyframes timeline-post-enter { + from { + transform: translate(0, -100%); + opacity: 0; + } + to { + opacity: 1; + } +} +.timeline-top-loading-enter { + animation: 1s timeline-top-loading-enter; +} + +.timeline-line { + display: flex; + flex-direction: column; + align-items: center; + width: 30px; + position: absolute; + z-index: 1; + left: 2em; + top: 0; + bottom: 0; + transition: left 0.5s; +} +@media (max-width: 575.98px) { + .timeline-line { + left: 1em; + } +} +.timeline-line .segment { + width: 7px; + background: var(--tl-primary-color); +} +.timeline-line .segment.start { + height: 1.8em; + flex: 0 0 auto; +} +.timeline-line .segment.end { + flex: 1 1 auto; +} +.timeline-line .segment.current-end { + height: 2em; + flex: 0 0 auto; + background: linear-gradient(var(--tl-primary-enhance-color), white); +} +.timeline-line .node-container { + flex: 0 0 auto; + position: relative; + width: 18px; + height: 18px; +} +.timeline-line .node { + width: 20px; + height: 20px; + position: absolute; + background: var(--tl-primary-color); + left: -1px; + top: -1px; + border-radius: 50%; + box-sizing: border-box; + z-index: 1; + animation: 1s infinite alternate; + animation-name: timeline-line-node-noncurrent; +} +.timeline-line .node-loading-edge { + color: var(--tl-primary-color); + width: 38px; + height: 38px; + position: absolute; + left: -10px; + top: -10px; + box-sizing: border-box; + z-index: 2; + animation: 1.5s linear infinite timeline-line-node-loading-edge; +} +.timeline-line.current .segment.start { + background: linear-gradient( + var(--tl-primary-color), + var(--tl-primary-enhance-color) + ); +} +.timeline-line.current .segment.end { + background: var(--tl-primary-enhance-color); +} +.timeline-line.current .node { + background: var(--tl-primary-enhance-color); + animation-name: timeline-line-node-current; +} +.timeline-line.loading .node { + background: var(--tl-primary-color); + animation-name: timeline-line-node-loading; +} + +.timeline-item.current { + padding-bottom: 2.5em; +} + +.timeline-top { + position: relative; + text-align: right; +} + +.timeline-item { + position: relative; + padding: 0.5em; +} + +.timeline-item-card { + position: relative; + padding: 0.3em 0.5em 1em 4em; + transition: background 0.5s, padding-left 0.5s; + animation: 0.6s forwards; + opacity: 0; +} +@media (max-width: 575.98px) { + .timeline-item-card { + padding-left: 3em; + } +} + +.timeline-item-header { + display: flex; + align-items: center; +} + +.timeline-avatar { + border-radius: 50%; + width: 2em; + height: 2em; +} + +.timeline-item-delete-button { + position: absolute; + right: 0; + bottom: 0; +} + +.timeline-content { + white-space: pre-line; +} + +.timeline-content-image { + max-width: 80%; + max-height: 200px; +} + +.timeline-date-item { + position: relative; + padding: 0.3em 0 0.3em 4em; +} + +.timeline-date-item-badge { + display: inline-block; + padding: 0.1em 0.4em; + border-radius: 0.4em; + background: #7c7c7c; + color: white; + font-size: 0.8em; +} + +.timeline-post-edit-image { + max-width: 100px; + max-height: 100px; +} + +.mask { + background: rgba(255, 255, 255, 0.8); + z-index: 100; +} + +.timeline-sync-state-badge { + font-size: 0.8em; + padding: 3px 8px; + border-radius: 5px; + background: #e8fbff; +} + +.timeline-sync-state-badge-pin { + display: inline-block; + width: 0.4em; + height: 0.4em; + border-radius: 50%; + vertical-align: middle; + margin-right: 0.6em; +} + +.timeline-template-card { + position: fixed; + top: 56px; + right: 0; + margin: 0.5em; +} + +.timeline-markdown-post-edit-page { + overflow: scroll; + max-height: 300px; +} + +.timeline-markdown-post-edit-image-container { + position: relative; + text-align: center; + margin-bottom: 1em; +} + +.timeline-markdown-post-edit-image { + max-width: 100%; + max-height: 200px; +} + +.timeline-markdown-post-edit-image-delete-button { + position: absolute; + right: 10px; + top: 2px; +} + +.connection-status-badge { + font-size: 0.8em; + border-radius: 5px; + padding: 0.1em 1em; + background-color: #eaf2ff; +} +.connection-status-badge::before { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + content: ""; + margin-right: 0.6em; +} +.connection-status-badge.success { + color: #006100; +} +.connection-status-badge.success::before { + background-color: #006100; +} +.connection-status-badge.warning { + color: #e4a700; +} +.connection-status-badge.warning::before { + background-color: #e4a700; +} +.connection-status-badge.danger { + color: #fd1616; +} +.connection-status-badge.danger::before { + background-color: #fd1616; +} diff --git a/FrontEnd/src/views/timeline-common/timeline-common.sass b/FrontEnd/src/views/timeline-common/timeline-common.sass deleted file mode 100644 index 4400fead..00000000 --- a/FrontEnd/src/views/timeline-common/timeline-common.sass +++ /dev/null @@ -1,259 +0,0 @@ -@use 'sass:color' - -.timeline - z-index: 0 - position: relative - width: 100% - overflow-wrap: break-word - animation: 1s timeline-enter - -$timeline-line-width: 7px -$timeline-line-node-radius: 18px -$timeline-line-color: var(--tl-primary-color) -$timeline-line-color-current: var(--tl-primary-enhance-color) - -@keyframes timeline-line-node-noncurrent - to - box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color) - -@keyframes timeline-line-node-current - to - box-shadow: 0 0 20px 3px var(--tl-primary-enhance-lighter-color) - -@keyframes timeline-line-node-loading - to - box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color) - -@keyframes timeline-line-node-loading-edge - from - transform: rotate(0turn) - to - transform: rotate(1turn) - -@keyframes timeline-enter - from - transform: translate(0, -100vh) - -@keyframes timeline-top-loading-enter - from - transform: translate(0, -100%) - -@keyframes timeline-post-enter - from - transform: translate(0, -100%) - opacity: 0 - - to - opacity: 1 - -.timeline-top-loading-enter - animation: 1s timeline-top-loading-enter - -.timeline-line - display: flex - flex-direction: column - align-items: center - width: 30px - - position: absolute - z-index: 1 - left: 2em - top: 0 - bottom: 0 - - transition: left 0.5s - - @include media-breakpoint-down(sm) - left: 1em - - .segment - width: $timeline-line-width - background: $timeline-line-color - - &.start - height: 1.8em - flex: 0 0 auto - - &.end - flex: 1 1 auto - - &.current-end - height: 2em - flex: 0 0 auto - background: linear-gradient($timeline-line-color-current, white) - - .node-container - flex: 0 0 auto - position: relative - width: $timeline-line-node-radius - height: $timeline-line-node-radius - - .node - width: $timeline-line-node-radius + 2 - height: $timeline-line-node-radius + 2 - position: absolute - background: $timeline-line-color - left: -1px - top: -1px - border-radius: 50% - box-sizing: border-box - z-index: 1 - animation: 1s infinite alternate - animation-name: timeline-line-node-noncurrent - - .node-loading-edge - color: $timeline-line-color - width: $timeline-line-node-radius + 20 - height: $timeline-line-node-radius + 20 - position: absolute - left: -10px - top: -10px - box-sizing: border-box - z-index: 2 - animation: 1.5s linear infinite timeline-line-node-loading-edge - - &.current - .segment - &.start - background: linear-gradient($timeline-line-color, $timeline-line-color-current) - &.end - background: $timeline-line-color-current - .node - background: $timeline-line-color-current - animation-name: timeline-line-node-current - - &.loading - .node - background: $timeline-line-color - animation-name: timeline-line-node-loading - -.timeline-item.current - padding-bottom: 2.5em - -.timeline-top - position: relative - text-align: right - -.timeline-item - position: relative - padding: 0.5em - -.timeline-item-card - @extend .cru-card - position: relative - padding: 0.3em 0.5em 1em 4em - transition: background 0.5s, padding-left 0.5s - animation: 0.6s forwards - opacity: 0 - - @include media-breakpoint-down(sm) - padding-left: 3em - -.timeline-item-header - display: flex - align-items: center - @extend .my-2 - -.timeline-avatar - border-radius: 50% - width: 2em - height: 2em - -.timeline-item-delete-button - position: absolute - right: 0 - bottom: 0 - -.timeline-content - white-space: pre-line - -.timeline-content-image - max-width: 80% - max-height: 200px - -.timeline-date-item - position: relative - padding: 0.3em 0 0.3em 4em - -.timeline-date-item-badge - display: inline-block - padding: 0.1em 0.4em - border-radius: 0.4em - background: #7c7c7c - color: white - font-size: 0.8em - -.timeline-post-edit-image - max-width: 100px - max-height: 100px - -.mask - background: change-color($color: white, $alpha: 0.8) - z-index: 100 - -.timeline-sync-state-badge - font-size: 0.8em - padding: 3px 8px - border-radius: 5px - background: #e8fbff - -.timeline-sync-state-badge-pin - display: inline-block - width: 0.4em - height: 0.4em - border-radius: 50% - vertical-align: middle - margin-right: 0.6em - -.timeline-template-card - position: fixed - top: 56px - right: 0 - margin: 0.5em - -.timeline-markdown-post-edit-page - overflow: scroll - max-height: 300px - -.timeline-markdown-post-edit-image-container - position: relative - text-align: center - margin-bottom: 1em - -.timeline-markdown-post-edit-image - max-width: 100% - max-height: 200px - -.timeline-markdown-post-edit-image-delete-button - position: absolute - right: 10px - top: 2px - -.connection-status-badge - font-size: 0.8em - border-radius: 5px - padding: 0.1em 1em - background-color: rgb(234 242 255) - - &::before - width: 10px - height: 10px - border-radius: 50% - display: inline-block - content: '' - margin-right: 0.6em - - &.success - color: #006100 - &::before - background-color: #006100 - - &.warning - color: #e4a700 - &::before - background-color: #e4a700 - - &.danger - color: #fd1616 - &::before - background-color: #fd1616 diff --git a/FrontEnd/src/views/timeline/timeline.sass b/FrontEnd/src/views/timeline/timeline.sass deleted file mode 100644 index e69de29b..00000000 diff --git a/FrontEnd/src/views/user/index.css b/FrontEnd/src/views/user/index.css new file mode 100644 index 00000000..35f01d38 --- /dev/null +++ b/FrontEnd/src/views/user/index.css @@ -0,0 +1,9 @@ +.change-avatar-cropper-row { + max-height: 400px; +} + +.change-avatar-img { + min-width: 50%; + max-width: 100%; + max-height: 400px; +} diff --git a/FrontEnd/src/views/user/index.tsx b/FrontEnd/src/views/user/index.tsx index 0013b254..1f2fe9ed 100644 --- a/FrontEnd/src/views/user/index.tsx +++ b/FrontEnd/src/views/user/index.tsx @@ -4,6 +4,8 @@ import { useParams } from "react-router"; import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; import UserCard from "./UserCard"; +import "./index.css"; + const UserPage: React.FC = () => { const { username } = useParams<{ username: string }>(); diff --git a/FrontEnd/src/views/user/user.sass b/FrontEnd/src/views/user/user.sass deleted file mode 100644 index 63a28e05..00000000 --- a/FrontEnd/src/views/user/user.sass +++ /dev/null @@ -1,7 +0,0 @@ -.change-avatar-cropper-row - max-height: 400px - -.change-avatar-img - min-width: 50% - max-width: 100% - max-height: 400px -- cgit v1.2.3 From 8c56e3fd388005bcb7aced75b73d7018511ceac8 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 16:41:03 +0800 Subject: ... --- FrontEnd/src/index.css | 53 ------------------------- FrontEnd/src/palette.ts | 43 ++++++++++---------- FrontEnd/src/views/admin/UserAdmin.tsx | 20 +++++----- FrontEnd/src/views/center/TimelineBoard.tsx | 3 -- FrontEnd/src/views/common/button/FlatButton.tsx | 2 +- FrontEnd/src/views/common/button/TextButton.css | 36 +++++++++++++++++ FrontEnd/src/views/common/button/TextButton.tsx | 36 +++++++++++++++++ 7 files changed, 105 insertions(+), 88 deletions(-) create mode 100644 FrontEnd/src/views/common/button/TextButton.css create mode 100644 FrontEnd/src/views/common/button/TextButton.tsx (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index ca0d4829..ff8c1866 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -81,59 +81,6 @@ textarea { color: #ffc107; } -.text-button { - background: transparent; - border: none; -} -.text-button.primary { - color: #0d6efd; -} -.text-button.primary:hover { - color: #599bfe; -} -.text-button.secondary { - color: #6c757d; -} -.text-button.secondary:hover { - color: #939ba2; -} -.text-button.success { - color: #198754; -} -.text-button.success:hover { - color: #25c87c; -} -.text-button.info { - color: #0dcaf0; -} -.text-button.info:hover { - color: #54dbf6; -} -.text-button.warning { - color: #ffc107; -} -.text-button.warning:hover { - color: #ffd454; -} -.text-button.danger { - color: #dc3545; -} -.text-button.danger:hover { - color: #e77681; -} -.text-button.light { - color: #f8f9fa; -} -.text-button.light:hover { - color: white; -} -.text-button.dark { - color: #212529; -} -.text-button.dark:hover { - color: #434b53; -} - .touch-action-none { touch-action: none; } diff --git a/FrontEnd/src/palette.ts b/FrontEnd/src/palette.ts index 11b3de85..6385df66 100644 --- a/FrontEnd/src/palette.ts +++ b/FrontEnd/src/palette.ts @@ -19,18 +19,19 @@ export interface PaletteColor { [key: string]: string; } -export interface Palette { - primary: PaletteColor; - primaryEnhance: PaletteColor; - secondary: PaletteColor; - textPrimary: PaletteColor; - textOnPrimary: PaletteColor; - danger: PaletteColor; - success: PaletteColor; - [key: string]: PaletteColor; -} +const paletteColorList = [ + "primary", + "primary-enhance", + "secondary", + "text-primary", + "text-on-primary", + "danger", + "success", +] as const; + +export type PaletteColorType = typeof paletteColorList[number]; -export type PaletteColorType = keyof Palette; +export type Palette = Record; export function generatePaletteColor(color: string): PaletteColor { const c = Color(color); @@ -60,26 +61,24 @@ export function generatePalette(options: { return { primary: generatePaletteColor(p.toString()), - primaryEnhance: generatePaletteColor(pe.toString()), + "primary-enhance": generatePaletteColor(pe.toString()), secondary: generatePaletteColor(s.toString()), - textPrimary: generatePaletteColor("#111111"), - textOnPrimary: generatePaletteColor(p.lightness() > 60 ? "black" : "white"), + "text-primary": generatePaletteColor("#111111"), + "text-on-primary": generatePaletteColor( + p.lightness() > 60 ? "black" : "white" + ), danger: generatePaletteColor("red"), success: generatePaletteColor("green"), }; } export function generatePaletteCSS(palette: Palette): string { - function toSnakeCase(s: string): string { - return s.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); - } - const colors: [string, string][] = []; - for (const paletteColorName in palette) { - const paletteColor = palette[paletteColorName]; + for (const colorType of paletteColorList) { + const paletteColor = palette[colorType]; for (const variant in paletteColor) { - let key = `--tl-${toSnakeCase(paletteColorName)}`; - if (variant !== "color") key += `-${toSnakeCase(variant)}`; + let key = `--tl-${colorType}`; + if (variant !== "color") key += `-${variant}`; key += "-color"; colors.push([key, paletteColor[variant]]); } diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index 558d3aee..eb141520 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -14,6 +14,7 @@ import { UserPermission, } from "@/http/user"; import { Trans, useTranslation } from "react-i18next"; +import TextButton from "../common/button/TextButton"; interface DialogProps { open: boolean; @@ -230,15 +231,16 @@ const UserItem: React.FC = ({ user, on }) => { className={classnames("edit-mask", !editMaskVisible && "d-none")} onClick={() => setEditMaskVisible(false)} > - - - + + + ); diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index a6a60b3d..e0a2d80e 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -1,7 +1,6 @@ import React from "react"; import classnames from "classnames"; import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; import { Spinner } from "react-bootstrap"; import { HttpTimelineInfo } from "@/http/timeline"; @@ -220,8 +219,6 @@ interface TimelineBoardUIProps { const TimelineBoardUI: React.FC = (props) => { const { title, timelines, className, editHandler } = props; - const { t } = useTranslation(); - const editable = editHandler != null; const [editing, setEditing] = React.useState(false); diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 0727eb88..24f47785 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -1,11 +1,11 @@ import React from "react"; import { useTranslation } from "react-i18next"; +import classNames from "classnames"; import { convertI18nText, I18nText } from "@/common"; import { PaletteColorType } from "@/palette"; import "./FlatButton.css"; -import classNames from "classnames"; function _FlatButton( { diff --git a/FrontEnd/src/views/common/button/TextButton.css b/FrontEnd/src/views/common/button/TextButton.css new file mode 100644 index 00000000..dc5abaaa --- /dev/null +++ b/FrontEnd/src/views/common/button/TextButton.css @@ -0,0 +1,36 @@ +.cru-text-button { + background: transparent; + border: none; +} + +.cru-text-button.primary { + color: var(--tl-primary-color); +} + +.cru-text-button.primary:hover { + color: var(--tl-primary-lighter-color); +} + +.cru-text-button.secondary { + color: var(--tl-secondary-color); +} + +.cru-text-button.secondary:hover { + color: var(--tl-secondary-lighter-color); +} + +.cru-text-button.success { + color: var(--tl-success-color); +} + +.cru-text-button.success:hover { + color: var(--tl-success-lighter-color); +} + +.cru-text-button.danger { + color: var(--tl-danger-color); +} + +.cru-text-button.danger:hover { + color: var(--tl-danger-lighter-color); +} diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx new file mode 100644 index 00000000..2014158a --- /dev/null +++ b/FrontEnd/src/views/common/button/TextButton.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import classNames from "classnames"; + +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; + +import "./TextButton.css"; + +function _TextButton( + { + text, + color, + onClick, + }: { + text: I18nText; + color?: PaletteColorType; + onClick?: () => void; + }, + ref: React.ForwardedRef +): React.ReactElement | null { + const { t } = useTranslation(); + + return ( + + ); +} + +const TextButton = React.forwardRef(_TextButton); +export default TextButton; -- cgit v1.2.3 From 9b2de947e6d16fe8478a3e8316ae4a1136e50948 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 16:46:07 +0800 Subject: ... --- FrontEnd/src/views/common/button/FlatButton.css | 3 +++ 1 file changed, 3 insertions(+) (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css index 779e3562..522563b9 100644 --- a/FrontEnd/src/views/common/button/FlatButton.css +++ b/FrontEnd/src/views/common/button/FlatButton.css @@ -2,6 +2,9 @@ cursor: pointer; padding: 0.2em 0.5em; border-radius: 0.2em; + border: none; + background-color: transparent; + transition: all 0.6s; } .cru-flat-button:hover:not(.disabled) { -- cgit v1.2.3 From 57a4aa9bc47d3d38f66819f01f41ab10e9673667 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 16:46:12 +0800 Subject: ... --- FrontEnd/src/views/common/button/FlatButton.tsx | 7 ++++++- FrontEnd/src/views/common/button/TextButton.tsx | 7 ++++++- FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx | 13 +++++-------- 3 files changed, 17 insertions(+), 10 deletions(-) (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 24f47785..f5349765 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -12,10 +12,14 @@ function _FlatButton( text, color, onClick, + className, + style, }: { text: I18nText; color?: PaletteColorType; onClick?: () => void; + className?: string; + style?: React.CSSProperties; }, ref: React.ForwardedRef ): React.ReactElement | null { @@ -24,8 +28,9 @@ function _FlatButton( return ( diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx index 2014158a..1e2b4873 100644 --- a/FrontEnd/src/views/common/button/TextButton.tsx +++ b/FrontEnd/src/views/common/button/TextButton.tsx @@ -12,10 +12,14 @@ function _TextButton( text, color, onClick, + className, + style, }: { text: I18nText; color?: PaletteColorType; onClick?: () => void; + className?: string; + style?: React.CSSProperties; }, ref: React.ForwardedRef ): React.ReactElement | null { @@ -24,8 +28,9 @@ function _TextButton( return ( diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx index 685e17be..005da933 100644 --- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -6,7 +6,7 @@ import { Prompt } from "react-router"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; -import FlatButton from "../common/FlatButton"; +import FlatButton from "../common/button/FlatButton"; import TabPages from "../common/TabPages"; import TimelinePostBuilder from "@/services/TimelinePostBuilder"; import ConfirmDialog from "../common/ConfirmDialog"; @@ -106,8 +106,9 @@ const MarkdownPostEdit: React.FC = ({ ) : ( <> { if (canLeave) { onClose(); @@ -115,12 +116,8 @@ const MarkdownPostEdit: React.FC = ({ setShowLeaveConfirmDialog(true); } }} - > - {t("operationDialog.cancel")} - - - {t("timeline.send")} - + /> + {canSend && } ) } -- cgit v1.2.3 From 1f242271a98900ca0a72a13ab05efbf65090df0d Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 17:04:25 +0800 Subject: ... --- FrontEnd/src/index.css | 10 ---------- FrontEnd/src/views/about/index.tsx | 14 ++++++++------ FrontEnd/src/views/center/TimelineBoard.tsx | 5 +++-- FrontEnd/src/views/common/Card.css | 11 +++++++++++ FrontEnd/src/views/common/Card.tsx | 22 ++++++++++++++++++++++ FrontEnd/src/views/settings/index.tsx | 9 +++++---- .../timeline-common/TimelinePageCardTemplate.tsx | 7 ++++--- 7 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 FrontEnd/src/views/common/Card.css create mode 100644 FrontEnd/src/views/common/Card.tsx (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index ff8c1866..8d428774 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -53,16 +53,6 @@ textarea { white-space: nowrap; } -.cru-card { - border: 1px solid; - border-color: #e9ecef; - background: #f8f9fa; - transition: all 0.3s; -} - -.cru-card:hover { - border-color: var(--tl-primary-color); -} .full-viewport-center-child { position: fixed; diff --git a/FrontEnd/src/views/about/index.tsx b/FrontEnd/src/views/about/index.tsx index db4814c4..7b0e50b0 100644 --- a/FrontEnd/src/views/about/index.tsx +++ b/FrontEnd/src/views/about/index.tsx @@ -4,6 +4,8 @@ import { useTranslation, Trans } from "react-i18next"; import authorAvatarUrl from "./author-avatar.png"; import githubLogoUrl from "./github.png"; +import Card from "../common/Card"; + import "./index.css"; const frontendCredits: { @@ -68,7 +70,7 @@ const AboutPage: React.FC = () => { return (
-
+

{t("about.author.title")}

@@ -102,8 +104,8 @@ const AboutPage: React.FC = () => {

-
-
+ +

{t("about.site.title")}

@@ -120,8 +122,8 @@ const AboutPage: React.FC = () => { {t("about.site.repo")}

-
-
+ +

{t("about.credits.title")}

{t("about.credits.content")}

{t("about.credits.frontend")}

@@ -150,7 +152,7 @@ const AboutPage: React.FC = () => { })}
  • ...
  • -
    +
    ); }; diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index e0a2d80e..7b9981e5 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -9,6 +9,7 @@ import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; import LoadFailReload from "../common/LoadFailReload"; import FlatButton from "../common/button/FlatButton"; +import Card from "../common/Card"; interface TimelineBoardItemProps { timeline: HttpTimelineInfo; @@ -224,7 +225,7 @@ const TimelineBoardUI: React.FC = (props) => { const [editing, setEditing] = React.useState(false); return ( -
    +
    {title != null &&

    {title}

    } {editable && @@ -280,7 +281,7 @@ const TimelineBoardUI: React.FC = (props) => { ); } })()} -
    +
    ); }; diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css new file mode 100644 index 00000000..daf4e12b --- /dev/null +++ b/FrontEnd/src/views/common/Card.css @@ -0,0 +1,11 @@ +.cru-card { + border: 1px solid; + border-color: #e9ecef; + border-radius: 8px; + background: #f8f9fa; + transition: all 0.3s; +} + +.cru-card:hover { + border-color: var(--tl-primary-color); +} diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx new file mode 100644 index 00000000..da2a1b68 --- /dev/null +++ b/FrontEnd/src/views/common/Card.tsx @@ -0,0 +1,22 @@ +import classNames from "classnames"; +import React from "react"; + +import "./Card.css"; + +function _Card( + { + className, + children, + }: React.PropsWithChildren>, + ref: React.ForwardedRef +): React.ReactElement | null { + return ( +
    + {children} +
    + ); +} + +const Card = React.forwardRef(_Card); + +export default Card; diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx index f0bed222..840bb7e8 100644 --- a/FrontEnd/src/views/settings/index.tsx +++ b/FrontEnd/src/views/settings/index.tsx @@ -8,6 +8,7 @@ import { useUser, userService } from "@/services/user"; import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; +import Card from "../common/Card"; import "./index.css"; @@ -52,7 +53,7 @@ const SettingsPage: React.FC = (_) => { <> {user ? ( -
    +

    {t("settings.subheaders.account")}

    @@ -82,9 +83,9 @@ const SettingsPage: React.FC = (_) => { > {t("settings.logout")}
    -
    + ) : null} -
    +

    {t("settings.subheaders.customization")}

    @@ -108,7 +109,7 @@ const SettingsPage: React.FC = (_) => { -
    + {(() => { switch (dialog) { diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx index 623d643f..851dfa55 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx @@ -19,6 +19,7 @@ import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; import ConnectionStatusBadge from "./ConnectionStatusBadge"; import { MenuItems, PopupMenu } from "../common/Menu"; import FullPage from "../common/FullPage"; +import Card from "../common/Card"; export interface TimelineCardTemplateProps extends TimelinePageCardProps { infoArea: React.ReactElement; @@ -110,8 +111,8 @@ const TimelinePageCardTemplate: React.FC = ({ return ( <> -
    @@ -129,7 +130,7 @@ const TimelinePageCardTemplate: React.FC = ({ ) : (
    {content}
    )} -
    + {(() => { if (dialog === "member") { return ( -- cgit v1.2.3 From 0be1f595578153765d081cdb4478140da9cb8fc9 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 17:14:04 +0800 Subject: ... --- FrontEnd/src/views/common/Card.css | 2 +- FrontEnd/src/views/common/button/FlatButton.tsx | 2 +- FrontEnd/src/views/common/button/TextButton.tsx | 2 +- .../src/views/timeline-common/TimelinePostView.tsx | 25 +++++++++++----------- FrontEnd/src/views/timeline-common/index.css | 8 ++++++- 5 files changed, 22 insertions(+), 17 deletions(-) (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css index daf4e12b..17c3c779 100644 --- a/FrontEnd/src/views/common/Card.css +++ b/FrontEnd/src/views/common/Card.css @@ -2,7 +2,7 @@ border: 1px solid; border-color: #e9ecef; border-radius: 8px; - background: #f8f9fa; + background: #fefeff; transition: all 0.3s; } diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index f5349765..6351971a 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -17,7 +17,7 @@ function _FlatButton( }: { text: I18nText; color?: PaletteColorType; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; className?: string; style?: React.CSSProperties; }, diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx index 1e2b4873..1a2bac94 100644 --- a/FrontEnd/src/views/common/button/TextButton.tsx +++ b/FrontEnd/src/views/common/button/TextButton.tsx @@ -17,7 +17,7 @@ function _TextButton( }: { text: I18nText; color?: PaletteColorType; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; className?: string; style?: React.CSSProperties; }, diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index f7b81478..e9dd3443 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -8,6 +8,8 @@ import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { pushAlert } from "@/services/alert"; import UserAvatar from "../common/user/UserAvatar"; +import Card from "../common/Card"; +import FlatButton from "../common/button/FlatButton"; import TimelineLine from "./TimelineLine"; import TimelinePostContentView from "./TimelinePostContentView"; import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; @@ -60,7 +62,7 @@ const TimelinePostView: React.FC = (props) => { style={style} > -
    + {post.editable ? ( = (props) => {
    {operationMaskVisible ? (
    { setOperationMaskVisible(false); }} > - { setDialog("changeproperty"); e.stopPropagation(); }} - > - {t("changeProperty")} - - + { setDialog("delete"); e.stopPropagation(); }} - > - {t("delete")} - + />
    ) : null} -
    + {dialog === "delete" ? ( { diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css index 89399961..f35a86c9 100644 --- a/FrontEnd/src/views/timeline-common/index.css +++ b/FrontEnd/src/views/timeline-common/index.css @@ -154,6 +154,7 @@ animation: 0.6s forwards; opacity: 0; } + @media (max-width: 575.98px) { .timeline-item-card { padding-left: 3em; @@ -205,9 +206,14 @@ max-height: 100px; } -.mask { +.timeline-post-item-options-mask { background: rgba(255, 255, 255, 0.8); z-index: 100; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; } .timeline-sync-state-badge { -- cgit v1.2.3