From e70429fd08b82995f8a65d530bf0235e2384ddae Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 3 Nov 2020 23:03:35 +0800 Subject: feat: Cursor on icon button is now pointer(hand). --- FrontEnd/src/app/index.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass index 0202e633..5a0d0cc2 100644 --- a/FrontEnd/src/app/index.sass +++ b/FrontEnd/src/app/index.sass @@ -35,6 +35,7 @@ small .icon-button font-size: 1.4em + cursor: pointer &.large font-size: 1.6em -- cgit v1.2.3 From a581cf642fa0ff06c27e3d3d95af02aec3abd87d Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 3 Nov 2020 23:37:28 +0800 Subject: feat: Enhance home page layout. --- FrontEnd/src/app/views/common/SearchInput.tsx | 12 +++++++----- FrontEnd/src/app/views/home/BoardWithUser.tsx | 8 ++++---- FrontEnd/src/app/views/home/BoardWithoutUser.tsx | 2 +- FrontEnd/src/app/views/home/index.tsx | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/FrontEnd/src/app/views/common/SearchInput.tsx b/FrontEnd/src/app/views/common/SearchInput.tsx index 9833d515..1373bd68 100644 --- a/FrontEnd/src/app/views/common/SearchInput.tsx +++ b/FrontEnd/src/app/views/common/SearchInput.tsx @@ -36,7 +36,7 @@ const SearchInput: React.FC = (props) => { ); return ( -
+ = (props) => { onKeyPress={onInputKeyPress} placeholder={props.placeholder} /> -
- {props.additionalButton} -
-
+ {props.additionalButton ? ( +
+ {props.additionalButton} +
+ ) : null} +
{props.loading ? ( ) : ( diff --git a/FrontEnd/src/app/views/home/BoardWithUser.tsx b/FrontEnd/src/app/views/home/BoardWithUser.tsx index dcd39cbe..fbe1dd89 100644 --- a/FrontEnd/src/app/views/home/BoardWithUser.tsx +++ b/FrontEnd/src/app/views/home/BoardWithUser.tsx @@ -62,9 +62,9 @@ const BoardWithUser: React.FC<{ user: UserWithToken }> = ({ user }) => { }, [user, joinTimelines]); return ( - + {ownTimelines === "offline" && joinTimelines === "offline" ? ( - + { setOwnTimelines("loading"); @@ -74,7 +74,7 @@ const BoardWithUser: React.FC<{ user: UserWithToken }> = ({ user }) => { ) : ( <> - + = ({ user }) => { }} /> - + { }, [publicTimelines]); return ( - + {publicTimelines === "offline" ? ( { return ( <> - - + + Date: Wed, 4 Nov 2020 10:31:34 +0800 Subject: refactor: Refactor operation dialog. --- FrontEnd/src/app/common.ts | 26 +++ FrontEnd/src/app/views/admin/UserAdmin.tsx | 22 +- FrontEnd/src/app/views/common/OperationDialog.tsx | 222 +++++++++++---------- .../src/app/views/home/TimelineCreateDialog.tsx | 39 ++-- FrontEnd/src/app/views/settings/index.tsx | 55 ++--- .../TimelinePropertyChangeDialog.tsx | 18 +- .../app/views/timeline/TimelineDeleteDialog.tsx | 14 +- .../src/app/views/user/ChangeNicknameDialog.tsx | 2 +- 8 files changed, 209 insertions(+), 189 deletions(-) diff --git a/FrontEnd/src/app/common.ts b/FrontEnd/src/app/common.ts index 0a2d345f..681568bb 100644 --- a/FrontEnd/src/app/common.ts +++ b/FrontEnd/src/app/common.ts @@ -1,5 +1,6 @@ import React from "react"; import { Observable, Subject } from "rxjs"; +import { TFunction } from "i18next"; // This error is thrown when ui goes wrong with bad logic. // Such as a variable should not be null, but it does. @@ -42,3 +43,28 @@ export function useValueEventEmiiter(): [ return [getter, trigger]; }, []); } + +export type I18nText = + | string + | { type: "custom"; value: string } + | { type: "i18n"; value: string }; + +export function convertI18nText(text: I18nText, t: TFunction): string; +export function convertI18nText( + text: I18nText | null | undefined, + t: TFunction +): string | null; +export function convertI18nText( + text: I18nText | null | undefined, + t: TFunction +): string | null { + if (text == null) { + return null; + } else if (typeof text === "string") { + return t(text); + } else if (text.type === "i18n") { + return t(text.value); + } else { + return text.value; + } +} diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 18b77ca8..0f5f8796 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -152,16 +152,18 @@ const CreateUserDialog: React.FC = (props) => { title="Create" titleColor="create" inputPrompt="You are creating a new user." - inputScheme={[ - { type: "text", label: "Username" }, - { type: "text", label: "Password" }, - { type: "bool", label: "Administrator" }, - ]} + inputScheme={ + [ + { type: "text", label: "Username" }, + { type: "text", label: "Password" }, + { type: "bool", label: "Administrator" }, + ] as const + } onProcess={([username, password, administrator]) => props.process({ - username: username as string, - password: password as string, - administrator: administrator as boolean, + username: username, + password: password, + administrator: administrator, }) } close={props.close} @@ -221,7 +223,7 @@ const UserChangeUsernameDialog: React.FC> = ( )} inputScheme={[{ type: "text", label: "New Username" }]} onProcess={([newUsername]) => { - return props.process(newUsername as string); + return props.process(newUsername); }} /> ); @@ -245,7 +247,7 @@ const UserChangePasswordDialog: React.FC> = ( )} inputScheme={[{ type: "text", label: "New Password" }]} onProcess={([newPassword]) => { - return props.process(newPassword as string); + return props.process(newPassword); }} /> ); diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx index 841392a6..e32e9277 100644 --- a/FrontEnd/src/app/views/common/OperationDialog.tsx +++ b/FrontEnd/src/app/views/common/OperationDialog.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Form, Button, Modal } from "react-bootstrap"; -import { UiLogicError } from "@/common"; +import { convertI18nText, I18nText, UiLogicError } from "@/common"; import LoadingButton from "./LoadingButton"; @@ -27,45 +27,33 @@ const DefaultErrorPrompt: React.FC = (props) => { return result; }; -export type OperationInputOptionalError = undefined | null | string; - -export interface OperationInputErrorInfo { - [index: number]: OperationInputOptionalError; -} - -export type OperationInputValidator = ( - value: TValue, - values: (string | boolean)[] -) => OperationInputOptionalError | OperationInputErrorInfo; - export interface OperationTextInputInfo { type: "text"; + label?: I18nText; password?: boolean; - label?: string; initValue?: string; textFieldProps?: Omit< React.InputHTMLAttributes, "type" | "value" | "onChange" | "aria-relevant" >; helperText?: string; - validator?: OperationInputValidator; } export interface OperationBoolInputInfo { type: "bool"; - label: string; + label: I18nText; initValue?: boolean; } export interface OperationSelectInputInfoOption { value: string; - label: string; + label: I18nText; icon?: React.ReactElement; } export interface OperationSelectInputInfo { type: "select"; - label: string; + label: I18nText; options: OperationSelectInputInfoOption[]; initValue?: string; } @@ -75,27 +63,67 @@ export type OperationInputInfo = | OperationBoolInputInfo | OperationSelectInputInfo; +type MapOperationInputInfoValueType = T extends OperationTextInputInfo + ? string + : T extends OperationBoolInputInfo + ? boolean + : T extends OperationSelectInputInfo + ? string + : never; + +type MapOperationInputInfoValueTypeList< + Tuple extends readonly OperationInputInfo[] +> = { + [Index in keyof Tuple]: MapOperationInputInfoValueType; +} & { length: Tuple["length"] }; + interface OperationResult { type: "success" | "failure"; data: unknown; } -interface OperationDialogProps { +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< + OperationInputInfoList extends readonly OperationInputInfo[] +> { open: boolean; close: () => void; - title: React.ReactNode; + title: I18nText | (() => React.ReactNode); titleColor?: "default" | "dangerous" | "create" | string; - onProcess: (inputs: (string | boolean)[]) => Promise; - inputScheme?: OperationInputInfo[]; - inputPrompt?: string | (() => React.ReactNode); + onProcess: ( + inputs: MapOperationInputInfoValueTypeList + ) => Promise; + inputScheme?: OperationInputInfoList; + inputValidator?: ( + inputs: MapOperationInputInfoValueTypeList + ) => OperationInputError; + inputPrompt?: I18nText | (() => React.ReactNode); processPrompt?: () => React.ReactNode; successPrompt?: (data: unknown) => React.ReactNode; failurePrompt?: (error: unknown) => React.ReactNode; onSuccessAndClose?: () => void; } -const OperationDialog: React.FC = (props) => { - const inputScheme = props.inputScheme ?? []; +const OperationDialog = < + OperationInputInfoList extends readonly OperationInputInfo[] +>( + props: OperationDialogProps +): React.ReactElement => { + const inputScheme = props.inputScheme as readonly OperationInputInfo[]; const { t } = useTranslation(); @@ -112,7 +140,10 @@ const OperationDialog: React.FC = (props) => { } }) ); - const [inputError, setInputError] = useState({}); + const [dirtyList, setDirtyList] = useState(() => + inputScheme.map(() => false) + ); + const [inputError, setInputError] = useState(); const close = (): void => { if (step !== "process") { @@ -131,20 +162,26 @@ const OperationDialog: React.FC = (props) => { const onConfirm = (): void => { setStep("process"); - props.onProcess(values).then( - (d: unknown) => { - setStep({ - type: "success", - data: d, - }); - }, - (e: unknown) => { - setStep({ - type: "failure", - data: e, - }); - } - ); + props + .onProcess( + (values as unknown) as MapOperationInputInfoValueTypeList< + OperationInputInfoList + > + ) + .then( + (d: unknown) => { + setStep({ + type: "success", + data: d, + }); + }, + (e: unknown) => { + setStep({ + type: "failure", + data: e, + }); + } + ); }; let body: React.ReactNode; @@ -154,65 +191,37 @@ const OperationDialog: React.FC = (props) => { let inputPrompt = typeof props.inputPrompt === "function" ? props.inputPrompt() - : props.inputPrompt; + : convertI18nText(props.inputPrompt, t); inputPrompt =
{inputPrompt}
; - const updateValue = ( - index: number, - newValue: string | boolean - ): (string | boolean)[] => { + const validate = (values: (string | boolean)[]): boolean => { + const { inputValidator } = props; + if (inputValidator != null) { + const result = inputValidator( + (values as unknown) as MapOperationInputInfoValueTypeList< + OperationInputInfoList + > + ); + setInputError(result); + return isNoError(result); + } + return true; + }; + + const updateValue = (index: number, newValue: string | boolean): void => { const oldValues = values; const newValues = oldValues.slice(); newValues[index] = newValue; setValues(newValues); - return newValues; - }; - - const testErrorInfo = (errorInfo: OperationInputErrorInfo): boolean => { - for (let i = 0; i < inputScheme.length; i++) { - if (inputScheme[i].type === "text" && errorInfo[i] != null) { - return true; - } - } - return false; - }; - - const calculateError = ( - oldError: OperationInputErrorInfo, - index: number, - newError: OperationInputOptionalError | OperationInputErrorInfo - ): OperationInputErrorInfo => { - if (newError === undefined) { - return oldError; - } else if (newError === null || typeof newError === "string") { - return { ...oldError, [index]: newError }; - } else { - const newInputError: OperationInputErrorInfo = { ...oldError }; - for (const [index, error] of Object.entries(newError)) { - if (error !== undefined) { - newInputError[+index] = error as OperationInputOptionalError; - } - } - return newInputError; + if (dirtyList[index] === false) { + const newDirtyList = dirtyList.slice(); + newDirtyList[index] = true; + setDirtyList(newDirtyList); } + validate(newValues); }; - const validateAll = (): boolean => { - let newInputError = inputError; - for (let i = 0; i < inputScheme.length; i++) { - const item = inputScheme[i]; - if (item.type === "text") { - newInputError = calculateError( - newInputError, - i, - item.validator?.(values[i] as string, values) - ); - } - } - const result = !testErrorInfo(newInputError); - setInputError(newInputError); - return result; - }; + const canProcess = isNoError(inputError); body = ( <> @@ -220,26 +229,23 @@ const OperationDialog: React.FC = (props) => { {inputPrompt} {inputScheme.map((item, index) => { const value = values[index]; - const error: string | undefined = ((e) => - typeof e === "string" ? t(e) : undefined)(inputError?.[index]); + const error: string | null = + dirtyList[index] && inputError != null + ? convertI18nText(inputError[index], t) + : null; if (item.type === "text") { return ( - {item.label && {t(item.label)}} + {item.label && ( + {convertI18nText(item.label, t)} + )} { const v = e.target.value; - const newValues = updateValue(index, v); - setInputError( - calculateError( - inputError, - index, - item.validator?.(v, newValues) - ) - ); + updateValue(index, v); }} isInvalid={error != null} disabled={process} @@ -263,7 +269,7 @@ const OperationDialog: React.FC = (props) => { onChange={(event) => { updateValue(index, event.currentTarget.checked); }} - label={t(item.label)} + label={convertI18nText(item.label, t)} disabled={process} /> @@ -271,7 +277,7 @@ const OperationDialog: React.FC = (props) => { } else if (item.type === "select") { return ( - {t(item.label)} + {convertI18nText(item.label, t)} = (props) => { return ( ); })} @@ -301,9 +307,10 @@ const OperationDialog: React.FC = (props) => { { - if (validateAll()) { + setDirtyList(inputScheme.map(() => true)); + if (validate(values)) { onConfirm(); } }} @@ -338,7 +345,10 @@ const OperationDialog: React.FC = (props) => { ); } - const title = typeof props.title === "string" ? t(props.title) : props.title; + const title = + typeof props.title === "function" + ? props.title() + : convertI18nText(props.title, t); return ( diff --git a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx index d9467719..786ebb5d 100644 --- a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx +++ b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx @@ -20,27 +20,28 @@ const TimelineCreateDialog: React.FC = (props) => { close={props.close} titleColor="success" title="home.createDialog.title" - inputScheme={[ - { - type: "text", - label: "home.createDialog.name", - helperText: "home.createDialog.nameFormat", - validator: (name) => { - if (name.length === 0) { - return "home.createDialog.noEmpty"; - } else if (name.length > 26) { - return "home.createDialog.tooLong"; - } else if (!validateTimelineName(name)) { - return "home.createDialog.badFormat"; - } else { - return null; - } + inputScheme={ + [ + { + type: "text", + label: "home.createDialog.name", + helperText: "home.createDialog.nameFormat", }, - }, - ]} + ] as const + } + inputValidator={([name]) => { + if (name.length === 0) { + return { 0: "home.createDialog.noEmpty" }; + } else if (name.length > 26) { + return { 0: "home.createDialog.tooLong" }; + } else if (!validateTimelineName(name)) { + return { 0: "home.createDialog.badFormat" }; + } else { + return null; + } + }} onProcess={([name]) => { - nameSaved = name as string; - return timelineService.createTimeline(nameSaved).toPromise(); + return timelineService.createTimeline(name).toPromise(); }} onSuccessAndClose={() => { history.push(`timelines/${nameSaved}`); diff --git a/FrontEnd/src/app/views/settings/index.tsx b/FrontEnd/src/app/views/settings/index.tsx index 964e7442..4d4f18b5 100644 --- a/FrontEnd/src/app/views/settings/index.tsx +++ b/FrontEnd/src/app/views/settings/index.tsx @@ -4,9 +4,7 @@ import { useTranslation } from "react-i18next"; import { Form, Container, Row, Col, Button, Modal } from "react-bootstrap"; import { useUser, userService } from "@/services/user"; -import OperationDialog, { - OperationInputErrorInfo, -} from "../common/OperationDialog"; +import OperationDialog from "../common/OperationDialog"; interface ChangePasswordDialogProps { open: boolean; @@ -15,60 +13,47 @@ interface ChangePasswordDialogProps { const ChangePasswordDialog: React.FC = (props) => { const history = useHistory(); - const { t } = useTranslation(); const [redirect, setRedirect] = useState(false); return ( - v === "" - ? "settings.dialogChangePassword.errorEmptyOldPassword" - : null, }, { type: "text", - label: t("settings.dialogChangePassword.inputNewPassword"), + label: "settings.dialogChangePassword.inputNewPassword", password: true, - validator: (v, values) => { - const error: OperationInputErrorInfo = {}; - error[1] = - v === "" - ? "settings.dialogChangePassword.errorEmptyNewPassword" - : null; - if (v === values[2]) { - error[2] = null; - } else { - if (values[2] !== "") { - error[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; - } - } - return error; - }, }, { type: "text", - label: t("settings.dialogChangePassword.inputRetypeNewPassword"), + label: "settings.dialogChangePassword.inputRetypeNewPassword", password: true, - validator: (v, values) => - v !== values[1] - ? "settings.dialogChangePassword.errorRetypeNotMatch" - : null, }, ]} + inputValidator={([oldPassword, newPassword, retypedNewPassword]) => { + const result: Record = {}; + if (oldPassword === "") { + result[0] = "settings.dialogChangePassword.errorEmptyOldPassword"; + } + if (newPassword === "") { + result[1] = "settings.dialogChangePassword.errorEmptyNewPassword"; + } + if (retypedNewPassword !== newPassword) { + result[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; + } + return result; + }} onProcess={async ([oldPassword, newPassword]) => { - await userService - .changePassword(oldPassword as string, newPassword as string) - .toPromise(); + await userService.changePassword(oldPassword, newPassword).toPromise(); await userService.logout(); setRedirect(true); }} diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx index 223525f9..ee49586e 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -6,9 +6,7 @@ import { TimelineChangePropertyRequest, } from "@/services/timeline"; -import OperationDialog, { - OperationSelectInputInfoOption, -} from "../common/OperationDialog"; +import OperationDialog from "../common/OperationDialog"; export interface TimelinePropertyInfo { title: string; @@ -45,12 +43,10 @@ const TimelinePropertyChangeDialog: React.FC { type: "select", label: "timeline.dialogChangeProperty.visibility", - options: kTimelineVisibilities.map( - (v) => ({ - label: labelMap[v], - value: v, - }) - ), + options: kTimelineVisibilities.map((v) => ({ + label: labelMap[v], + value: v, + })), initValue: props.oldInfo.visibility, }, { @@ -64,13 +60,13 @@ const TimelinePropertyChangeDialog: React.FC onProcess={([newTitle, newVisibility, newDescription]) => { const req: TimelineChangePropertyRequest = {}; if (newTitle !== props.oldInfo.title) { - req.title = newTitle as string; + req.title = newTitle; } if (newVisibility !== props.oldInfo.visibility) { req.visibility = newVisibility as TimelineVisibility; } if (newDescription !== props.oldInfo.description) { - req.description = newDescription as string; + req.description = newDescription; } return props.onProcess(req); }} diff --git a/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx index 894b8195..33609158 100644 --- a/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx @@ -33,15 +33,15 @@ const TimelineDeleteDialog: React.FC = (props) => { inputScheme={[ { type: "text", - validator: (value) => { - if (value !== name) { - return "timeline.deleteDialog.notMatch"; - } else { - return null; - } - }, }, ]} + inputValidator={([value]) => { + if (value !== name) { + return { 0: "timeline.deleteDialog.notMatch" }; + } else { + return null; + } + }} onProcess={() => { return timelineService.deleteTimeline(name).toPromise(); }} diff --git a/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx b/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx index 251b18c5..0e95b05b 100644 --- a/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx @@ -18,7 +18,7 @@ const ChangeNicknameDialog: React.FC = (props) => { { type: "text", label: "userPage.dialogChangeNickname.inputLabel" }, ]} onProcess={([newNickname]) => { - return props.onProcess(newNickname as string); + return props.onProcess(newNickname); }} close={props.close} /> -- cgit v1.2.3 From 8e836f4bee040d83a195255073fd318c07277744 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 4 Nov 2020 10:52:14 +0800 Subject: fix: Fix bug in app bar. --- FrontEnd/src/app/views/common/AppBar.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/FrontEnd/src/app/views/common/AppBar.tsx b/FrontEnd/src/app/views/common/AppBar.tsx index ee4ead8f..699c596e 100644 --- a/FrontEnd/src/app/views/common/AppBar.tsx +++ b/FrontEnd/src/app/views/common/AppBar.tsx @@ -2,6 +2,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { LinkContainer } from "react-router-bootstrap"; import { Navbar, Nav } from "react-bootstrap"; +import { NavLink } from "react-router-dom"; import { useUser, useAvatar } from "@/services/user"; @@ -28,18 +29,17 @@ const AppBar: React.FC = (_) => { -- cgit v1.2.3 From 34221c8c6d34e565c4c9d82413ac4a20356d8f78 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 4 Nov 2020 11:49:15 +0800 Subject: feat: Redesign timeline page. --- .../src/app/views/timeline-common/Timeline.tsx | 2 + .../src/app/views/timeline-common/TimelineItem.tsx | 58 +++++++++++----------- .../timeline-common/TimelinePageTemplateUI.tsx | 2 - .../views/timeline-common/timeline-background.svg | 4 ++ .../app/views/timeline-common/timeline-common.sass | 20 ++++++-- 5 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 FrontEnd/src/app/views/timeline-common/timeline-background.svg diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx index fd051d45..6bd1b96d 100644 --- a/FrontEnd/src/app/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/app/views/timeline-common/Timeline.tsx @@ -4,6 +4,7 @@ import clsx from "clsx"; import { TimelinePostInfo } from "@/services/timeline"; import TimelineItem from "./TimelineItem"; +import TimelineTop from "./TimelineTop"; export interface TimelinePostInfoEx extends TimelinePostInfo { deletable: boolean; @@ -52,6 +53,7 @@ const Timeline: React.FC = (props) => { return (
+ {(() => { const length = posts.length; return posts.map((post, i) => { diff --git a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx index 4db23371..5ccc5523 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx @@ -93,12 +93,12 @@ const TimelineItem: React.FC = (props) => { {current &&
}
-
+
- + {props.post.time.toLocaleString(i18n.languages)} - + {props.post.author.nickname} {more != null ? ( @@ -138,33 +138,33 @@ const TimelineItem: React.FC = (props) => { } })()}
+ {more != null && more.isOpen ? ( + <> +
+ { + toggleDeleteDialog(); + e.stopPropagation(); + }} + /> +
+ {deleteDialog ? ( + { + toggleDeleteDialog(); + more.toggle(); + }} + onConfirm={more.onDelete} + /> + ) : null} + + ) : null}
- {more != null && more.isOpen ? ( - <> -
- { - toggleDeleteDialog(); - e.stopPropagation(); - }} - /> -
- {deleteDialog ? ( - { - toggleDeleteDialog(); - more.toggle(); - }} - onConfirm={more.onDelete} - /> - ) : null} - - ) : null}
); }; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx index 6c2c43c1..35c1a65d 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -16,7 +16,6 @@ import Timeline, { TimelinePostInfoEx, TimelineDeleteCallback, } from "./Timeline"; -import TimelineTop from "./TimelineTop"; import TimelinePostEdit, { TimelinePostSendCallback } from "./TimelinePostEdit"; import { TimelineSyncStatus } from "./SyncStatusBadge"; @@ -226,7 +225,6 @@ export default function TimelinePageTemplateUI( collapse={cardCollapse} toggleCollapse={toggleCardCollapse} /> - {timelineBody} ); diff --git a/FrontEnd/src/app/views/timeline-common/timeline-background.svg b/FrontEnd/src/app/views/timeline-common/timeline-background.svg new file mode 100644 index 00000000..b72c448b --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/timeline-background.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass index 4151bfcc..286f6867 100644 --- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass +++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass @@ -4,8 +4,13 @@ z-index: 0 position: relative + background-image: url("views/timeline-common/timeline-background.svg") + background-size: 100% auto + background-repeat: no-repeat repeat + &-item - display: flex + position: relative + padding: 0.5em $timeline-line-width: 7px $timeline-line-node-radius: 18px @@ -30,12 +35,17 @@ $timeline-line-color-current: #36c2e6 .timeline-line &-area-container + position: absolute display: flex justify-content: flex-end padding-right: 5px + z-index: 1 flex: 0 0 auto + + left: 0.5em width: 60px + height: 100% &-area display: flex @@ -97,9 +107,11 @@ $timeline-line-color-current: #36c2e6 &-node animation-name: timeline-line-node-current -.timeline-content-area - padding: 10px 0 - flex-grow: 1 +.timeline-item-card + @extend .cru-card + @extend .clearfix + position: relative + padding: 0.5em 2em 0.5em 60px .timeline-item-delete-button position: absolute -- cgit v1.2.3 From 0bc6b4495ae63b652877a8145d0acca9213fa348 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 4 Nov 2020 12:01:50 +0800 Subject: feat: Enhance design. --- FrontEnd/src/app/views/timeline-common/Timeline.tsx | 7 ++++++- .../app/views/timeline-common/TimelinePageTemplateUI.tsx | 15 +++++++++------ FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx | 2 +- FrontEnd/src/app/views/user/UserInfoCard.tsx | 5 +---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx index 6bd1b96d..9047919c 100644 --- a/FrontEnd/src/app/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/app/views/timeline-common/Timeline.tsx @@ -14,6 +14,7 @@ export type TimelineDeleteCallback = (index: number, id: number) => void; export interface TimelineProps { className?: string; + style?: React.CSSProperties; posts: TimelinePostInfoEx[]; onDelete: TimelineDeleteCallback; onResize?: () => void; @@ -52,7 +53,11 @@ const Timeline: React.FC = (props) => { }, [posts, onDelete]); return ( -
+
{(() => { const length = posts.length; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx index 35c1a65d..036577b1 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -47,13 +47,10 @@ export default function TimelinePageTemplateUI( const { t } = useTranslation(); - const bottomSpaceRef = React.useRef(null); + const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState(0); const onPostEditHeightChange = React.useCallback((height: number): void => { - const { current: bottomSpaceDiv } = bottomSpaceRef; - if (bottomSpaceDiv != null) { - bottomSpaceDiv.style.height = `${height}px`; - } + setBottomSpaceHeight(height); if (height === 0) { const alertHost = getAlertHost(); if (alertHost != null) { @@ -177,6 +174,9 @@ export default function TimelinePageTemplateUI( timelineBody = ( ( timelineBody = ( <> {timelineBody} -
+
= (props) => { collapse={collapse} toggleCollapse={toggleCollapse} > -

+

{timeline.title} {timeline.name}

diff --git a/FrontEnd/src/app/views/user/UserInfoCard.tsx b/FrontEnd/src/app/views/user/UserInfoCard.tsx index e4edd80d..4a0c9e87 100644 --- a/FrontEnd/src/app/views/user/UserInfoCard.tsx +++ b/FrontEnd/src/app/views/user/UserInfoCard.tsx @@ -35,16 +35,13 @@ const UserInfoCard: React.FC = (props) => { collapse={collapse} toggleCollapse={toggleCollapse} > -

+

{timeline.title} {timeline.name}

{timeline.owner.nickname} - - @{timeline.owner.username} -

{timeline.description}

-- cgit v1.2.3 From e28d90df6fffd20384234b1351e3d9dd56f549de Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 4 Nov 2020 12:10:43 +0800 Subject: feat: Add hover effect on timeline item. --- FrontEnd/src/app/index.sass | 2 +- FrontEnd/src/app/views/timeline-common/timeline-common.sass | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass index 5a0d0cc2..abe663d1 100644 --- a/FrontEnd/src/app/index.sass +++ b/FrontEnd/src/app/index.sass @@ -52,8 +52,8 @@ textarea @extend .shadow @extend .border @extend .rounded - @extend .bg-light border-color: $gray-200 + background: $light .full-viewport-center-child position: fixed diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass index 286f6867..dd5452de 100644 --- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass +++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass @@ -112,6 +112,10 @@ $timeline-line-color-current: #36c2e6 @extend .clearfix position: relative padding: 0.5em 2em 0.5em 60px + transition: background 0.5s + + &:hover + background: $gray-200 .timeline-item-delete-button position: absolute -- cgit v1.2.3 From d2d0f78bfff303db42cb22e48cf23f9c0e643478 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 4 Nov 2020 12:23:41 +0800 Subject: feat: ... --- .../src/app/views/timeline-common/timeline-common.sass | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass index dd5452de..f804834b 100644 --- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass +++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass @@ -41,11 +41,10 @@ $timeline-line-color-current: #36c2e6 padding-right: 5px z-index: 1 - flex: 0 0 auto - + top: 0em + bottom: 0em left: 0.5em width: 60px - height: 100% &-area display: flex @@ -58,14 +57,14 @@ $timeline-line-color-current: #36c2e6 background: $timeline-line-color &.start - height: 14px + height: 1.4em flex: 0 0 auto &.end flex: 1 1 auto &.current-end - height: 20px + height: 2em flex: 0 0 auto background: linear-gradient($timeline-line-color-current, transparent) @@ -88,14 +87,16 @@ $timeline-line-color-current: #36c2e6 animation-name: timeline-line-node-noncurrent .timeline-top - display: flex - justify-content: space-between + position: relative + text-align: right .timeline-line-segment flex: 1 1 auto .current .timeline-line + &-area-container + bottom: -2em &-segment &.start @@ -137,9 +138,6 @@ $timeline-line-color-current: #36c2e6 background: change-color($color: white, $alpha: 0.8) z-index: 100 -.timeline-page-top-space - transition: height 0.5s - .timeline-sync-state-badge font-size: 0.8em padding: 3px 8px -- cgit v1.2.3 From 1ba32f233677cad56636462b1f6552ff6e800801 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 4 Nov 2020 12:26:56 +0800 Subject: feat: ... --- FrontEnd/src/app/views/timeline-common/timeline-common.sass | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass index f804834b..8d9ee04d 100644 --- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass +++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass @@ -94,9 +94,10 @@ $timeline-line-color-current: #36c2e6 flex: 1 1 auto .current + &.timeline-item + padding-bottom: 2.5em + .timeline-line - &-area-container - bottom: -2em &-segment &.start -- cgit v1.2.3