diff options
Diffstat (limited to 'FrontEnd/src')
62 files changed, 969 insertions, 900 deletions
diff --git a/FrontEnd/src/common.ts b/FrontEnd/src/common.ts index 1a4f6dda..91d58562 100644 --- a/FrontEnd/src/common.ts +++ b/FrontEnd/src/common.ts @@ -1,13 +1,5 @@ import { TFunction } from "i18next"; -export type BootstrapThemeColor = - | "primary" - | "secondary" - | "success" - | "danger" - | "warning" - | "info"; - // This error is thrown when ui goes wrong with bad logic. // Such as a variable should not be null, but it does. // This error should never occur. If it does, it indicates there is some logic bug in codes. diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index 9b9956a9..4adc1844 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -1,48 +1,125 @@ +@import "bootstrap/dist/css/bootstrap-reboot.css";
+@import "bootstrap/dist/css/bootstrap-grid.css";
+@import "bootstrap-icons/font/bootstrap-icons.css";
+
+@import "./views/common/index.css";
+
:root {
--tl-background-color: #f8f9fa;
--tl-primary-color: rgb(0, 123, 255);
- --tl-primary-inactive-color: rgb(26, 136, 255);
- --tl-primary-lighter-color: rgb(26, 136, 255);
- --tl-primary-darker-color: rgb(0, 111, 230);
+ --tl-primary-l1-color: rgb(26, 136, 255);
+ --tl-primary-l2-color: rgb(51, 149, 255);
+ --tl-primary-l3-color: rgb(77, 163, 255);
+ --tl-primary-d1-color: rgb(0, 111, 230);
+ --tl-primary-d2-color: rgb(0, 98, 204);
+ --tl-primary-d3-color: rgb(0, 86, 179);
+ --tl-primary-f1-color: rgb(0, 111, 230);
+ --tl-primary-f2-color: rgb(0, 98, 204);
+ --tl-primary-f3-color: rgb(0, 86, 179);
+ --tl-primary-r1-color: rgb(26, 136, 255);
+ --tl-primary-r2-color: rgb(51, 149, 255);
+ --tl-primary-r3-color: rgb(77, 163, 255);
--tl-primary-enhance-color: rgb(77, 163, 255);
- --tl-primary-enhance-inactive-color: rgb(43, 145, 255);
- --tl-primary-enhance-lighter-color: rgb(94, 172, 255);
- --tl-primary-enhance-darker-color: rgb(43, 145, 255);
+ --tl-primary-enhance-l1-color: rgb(94, 172, 255);
+ --tl-primary-enhance-l2-color: rgb(112, 181, 255);
+ --tl-primary-enhance-l3-color: rgb(130, 190, 255);
+ --tl-primary-enhance-d1-color: rgb(43, 145, 255);
+ --tl-primary-enhance-d2-color: rgb(10, 128, 255);
+ --tl-primary-enhance-d3-color: rgb(0, 112, 232);
+ --tl-primary-enhance-f1-color: rgb(94, 172, 255);
+ --tl-primary-enhance-f2-color: rgb(112, 181, 255);
+ --tl-primary-enhance-f3-color: rgb(130, 190, 255);
+ --tl-primary-enhance-r1-color: rgb(43, 145, 255);
+ --tl-primary-enhance-r2-color: rgb(10, 128, 255);
+ --tl-primary-enhance-r3-color: rgb(0, 112, 232);
--tl-secondary-color: rgb(255, 0, 250);
- --tl-secondary-inactive-color: rgb(255, 26, 251);
- --tl-secondary-lighter-color: rgb(255, 26, 251);
- --tl-secondary-darker-color: rgb(230, 0, 225);
+ --tl-secondary-l1-color: rgb(255, 26, 251);
+ --tl-secondary-l2-color: rgb(255, 51, 251);
+ --tl-secondary-l3-color: rgb(255, 77, 252);
+ --tl-secondary-d1-color: rgb(230, 0, 225);
+ --tl-secondary-d2-color: rgb(204, 0, 200);
+ --tl-secondary-d3-color: rgb(179, 0, 175);
+ --tl-secondary-f1-color: rgb(230, 0, 225);
+ --tl-secondary-f2-color: rgb(204, 0, 200);
+ --tl-secondary-f3-color: rgb(179, 0, 175);
+ --tl-secondary-r1-color: rgb(255, 26, 251);
+ --tl-secondary-r2-color: rgb(255, 51, 251);
+ --tl-secondary-r3-color: rgb(255, 77, 252);
--tl-text-primary-color: rgb(17, 17, 17);
- --tl-text-primary-inactive-color: rgb(41, 41, 41);
- --tl-text-primary-lighter-color: rgb(41, 41, 41);
- --tl-text-primary-darker-color: rgb(15, 15, 15);
+ --tl-text-primary-l1-color: rgb(41, 41, 41);
+ --tl-text-primary-l2-color: rgb(65, 65, 65);
+ --tl-text-primary-l3-color: rgb(88, 88, 88);
+ --tl-text-primary-d1-color: rgb(15, 15, 15);
+ --tl-text-primary-d2-color: rgb(14, 14, 14);
+ --tl-text-primary-d3-color: rgb(12, 12, 12);
+ --tl-text-primary-f1-color: rgb(15, 15, 15);
+ --tl-text-primary-f2-color: rgb(14, 14, 14);
+ --tl-text-primary-f3-color: rgb(12, 12, 12);
+ --tl-text-primary-r1-color: rgb(41, 41, 41);
+ --tl-text-primary-r2-color: rgb(65, 65, 65);
+ --tl-text-primary-r3-color: rgb(88, 88, 88);
--tl-text-on-primary-color: rgb(255, 255, 255);
- --tl-text-on-primary-inactive-color: rgb(230, 230, 230);
- --tl-text-on-primary-lighter-color: rgb(255, 255, 255);
- --tl-text-on-primary-darker-color: rgb(230, 230, 230);
+ --tl-text-on-primary-l1-color: rgb(255, 255, 255);
+ --tl-text-on-primary-l2-color: rgb(255, 255, 255);
+ --tl-text-on-primary-l3-color: rgb(255, 255, 255);
+ --tl-text-on-primary-d1-color: rgb(230, 230, 230);
+ --tl-text-on-primary-d2-color: rgb(204, 204, 204);
+ --tl-text-on-primary-d3-color: rgb(179, 179, 179);
+ --tl-text-on-primary-f1-color: rgb(255, 255, 255);
+ --tl-text-on-primary-f2-color: rgb(255, 255, 255);
+ --tl-text-on-primary-f3-color: rgb(255, 255, 255);
+ --tl-text-on-primary-r1-color: rgb(230, 230, 230);
+ --tl-text-on-primary-r2-color: rgb(204, 204, 204);
+ --tl-text-on-primary-r3-color: rgb(179, 179, 179);
--tl-danger-color: rgb(255, 0, 0);
- --tl-danger-inactive-color: rgb(255, 26, 26);
- --tl-danger-lighter-color: rgb(255, 26, 26);
- --tl-danger-darker-color: rgb(230, 0, 0);
+ --tl-danger-l1-color: rgb(255, 26, 26);
+ --tl-danger-l2-color: rgb(255, 51, 51);
+ --tl-danger-l3-color: rgb(255, 77, 77);
+ --tl-danger-d1-color: rgb(230, 0, 0);
+ --tl-danger-d2-color: rgb(204, 0, 0);
+ --tl-danger-d3-color: rgb(179, 0, 0);
+ --tl-danger-f1-color: rgb(230, 0, 0);
+ --tl-danger-f2-color: rgb(204, 0, 0);
+ --tl-danger-f3-color: rgb(179, 0, 0);
+ --tl-danger-r1-color: rgb(255, 26, 26);
+ --tl-danger-r2-color: rgb(255, 51, 51);
+ --tl-danger-r3-color: rgb(255, 77, 77);
--tl-success-color: rgb(0, 128, 0);
- --tl-success-inactive-color: rgb(0, 166, 0);
- --tl-success-lighter-color: rgb(0, 166, 0);
- --tl-success-darker-color: rgb(0, 115, 0);
-}
-
-body {
- background: var(--tl-background-color);
+ --tl-success-l1-color: rgb(0, 166, 0);
+ --tl-success-l2-color: rgb(0, 204, 0);
+ --tl-success-l3-color: rgb(0, 243, 0);
+ --tl-success-d1-color: rgb(0, 115, 0);
+ --tl-success-d2-color: rgb(0, 102, 0);
+ --tl-success-d3-color: rgb(0, 90, 0);
+ --tl-success-f1-color: rgb(0, 115, 0);
+ --tl-success-f2-color: rgb(0, 102, 0);
+ --tl-success-f3-color: rgb(0, 90, 0);
+ --tl-success-r1-color: rgb(0, 166, 0);
+ --tl-success-r2-color: rgb(0, 204, 0);
+ --tl-success-r3-color: rgb(0, 243, 0);
}
.tl-color-primary {
color: var(--tl-primary-color);
}
+.tl-color-secondary {
+ color: var(--tl-secondary-color);
+}
+
+.tl-color-success {
+ color: var(--tl-success-color);
+}
+
.tl-color-danger {
color: var(--tl-danger-color);
}
+body {
+ background: var(--tl-background-color);
+}
+
small {
line-height: 1.2;
}
@@ -52,21 +129,6 @@ small { 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;
@@ -101,14 +163,6 @@ textarea { align-items: center;
}
-.text-orange {
- color: #fd7e14;
-}
-
-.text-yellow {
- color: #ffc107;
-}
-
.touch-action-none {
touch-action: none;
}
diff --git a/FrontEnd/src/index.tsx b/FrontEnd/src/index.tsx index 28034601..e2132de0 100644 --- a/FrontEnd/src/index.tsx +++ b/FrontEnd/src/index.tsx @@ -3,9 +3,6 @@ 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"; diff --git a/FrontEnd/src/palette.ts b/FrontEnd/src/palette.ts index ab3d6b54..fa99364f 100644 --- a/FrontEnd/src/palette.ts +++ b/FrontEnd/src/palette.ts @@ -15,9 +15,18 @@ function darkenBy(color: Color, ratio: number): Color { export interface PaletteColor { color: string; - lighter: string; - darker: string; - inactive: string; + l1: string; + l2: string; + l3: string; + d1: string; + d2: string; + d3: string; + f1: string; + f2: string; + f3: string; + r1: string; + r2: string; + r3: string; [key: string]: string; } @@ -37,13 +46,34 @@ export type Palette = Record<PaletteColorType, PaletteColor>; export function generatePaletteColor(color: string): PaletteColor { const c = Color(color); + const light = c.lightness() > 60; + const l1 = lightenBy(c, 0.1).rgb().toString(); + const l2 = lightenBy(c, 0.2).rgb().toString(); + const l3 = lightenBy(c, 0.3).rgb().toString(); + const d1 = darkenBy(c, 0.1).rgb().toString(); + const d2 = darkenBy(c, 0.2).rgb().toString(); + const d3 = darkenBy(c, 0.3).rgb().toString(); + const f1 = light ? l1 : d1; + const f2 = light ? l2 : d2; + const f3 = light ? l3 : d3; + const r1 = light ? d1 : l1; + const r2 = light ? d2 : l2; + const r3 = light ? d3 : l3; + return { color: c.rgb().toString(), - inactive: (c.lightness() > 60 ? darkenBy(c, 0.1) : lightenBy(c, 0.1)) - .rgb() - .toString(), - lighter: lightenBy(c, 0.1).rgb().toString(), - darker: darkenBy(c, 0.1).rgb().toString(), + l1, + l2, + l3, + d1, + d2, + d3, + f1, + f2, + f3, + r1, + r2, + r3, }; } @@ -91,7 +121,10 @@ export function generatePaletteCSS(palette: Palette): string { } const paletteSubject: BehaviorSubject<Palette | null> = - new BehaviorSubject<Palette | null>(null); + new BehaviorSubject<Palette | null>( + // generatePalette({ primary: "rgb(0, 123, 255)" }) + null + ); export const palette$: Observable<Palette | null> = paletteSubject.asObservable(); diff --git a/FrontEnd/src/service-worker.tsx b/FrontEnd/src/service-worker.tsx index e40124fe..fb81650d 100644 --- a/FrontEnd/src/service-worker.tsx +++ b/FrontEnd/src/service-worker.tsx @@ -1,9 +1,10 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { Button } from "react-bootstrap"; import { pushAlert } from "./services/alert"; +import Button from "./views/common/button/Button"; + if (import.meta.env.PROD && "serviceWorker" in navigator) { let isThisTriggerUpgrade = false; @@ -37,12 +38,10 @@ if (import.meta.env.PROD && "serviceWorker" in navigator) { <> {t("serviceWorker.externalActivatedPrompt")} <Button - variant="outline-success" - size="sm" + text="serviceWorker.reloadNow" + color="success" onClick={upgradeReload} - > - {t("serviceWorker.reloadNow")} - </Button> + /> </> ); }; @@ -83,9 +82,11 @@ if (import.meta.env.PROD && "serviceWorker" in navigator) { return ( <> {t("serviceWorker.upgradePrompt")} - <Button variant="outline-success" size="sm" onClick={upgrade}> - {t("serviceWorker.upgradeNow")} - </Button> + <Button + text="serviceWorker.upgradeNow" + color="success" + onClick={upgrade} + /> </> ); }; diff --git a/FrontEnd/src/views/about/index.tsx b/FrontEnd/src/views/about/index.tsx index 7a72d5ec..8a358014 100644 --- a/FrontEnd/src/views/about/index.tsx +++ b/FrontEnd/src/views/about/index.tsx @@ -72,16 +72,16 @@ const AboutPage: React.FC = () => { <div className="d-flex"> <img src={authorAvatarUrl} - className="align-self-start avatar large rounded-circle" + className="align-self-start cru-avatar large cru-round" /> <div> <p> <small>{t("about.author.fullname")}</small> - <span className="text-primary">杨宇千</span> + <span className="tl-color-primary">杨宇千</span> </p> <p> <small>{t("about.author.nickname")}</small> - <span className="text-primary">crupest</span> + <span className="tl-color-primary">crupest</span> </p> <p> <small>{t("about.author.introduction")}</small> @@ -96,7 +96,7 @@ const AboutPage: React.FC = () => { target="_blank" rel="noopener noreferrer" > - <img src={githubLogoUrl} className="about-link-icon text-body" /> + <img src={githubLogoUrl} className="about-link-icon" /> </a> </p> </div> @@ -105,7 +105,7 @@ const AboutPage: React.FC = () => { <h4>{t("about.site.title")}</h4> <p> <Trans i18nKey="about.site.content"> - 0<span className="text-primary">1</span>2<b>3</b>4 + 0<span className="tl-color-primary">1</span>2<b>3</b>4 <a href="#author-info">5</a>6 </Trans> </p> diff --git a/FrontEnd/src/views/admin/Admin.tsx b/FrontEnd/src/views/admin/Admin.tsx index 34e7e2f6..9393a61f 100644 --- a/FrontEnd/src/views/admin/Admin.tsx +++ b/FrontEnd/src/views/admin/Admin.tsx @@ -1,6 +1,5 @@ import React, { Fragment } from "react"; import { Redirect, Route, Switch, useRouteMatch, match } from "react-router"; -import { Container } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { AuthUser } from "@/services/user"; @@ -29,7 +28,7 @@ const Admin: React.FC<AdminProps> = ({ user }) => { const match = p.match as match<{ name: string }>; const name = match.params["name"]; return ( - <Container> + <div className="container"> <AdminNav /> {(() => { if (name === "users") { @@ -38,7 +37,7 @@ const Admin: React.FC<AdminProps> = ({ user }) => { return <MoreAdmin user={user} />; } })()} - </Container> + </div> ); }} </Route> diff --git a/FrontEnd/src/views/admin/AdminNav.tsx b/FrontEnd/src/views/admin/AdminNav.tsx index 47e2138f..8b4c5fda 100644 --- a/FrontEnd/src/views/admin/AdminNav.tsx +++ b/FrontEnd/src/views/admin/AdminNav.tsx @@ -1,43 +1,29 @@ import React from "react"; -import { Nav } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; -import { useHistory, useRouteMatch } from "react-router"; +import { useRouteMatch } from "react-router"; + +import Tabs from "../common/tab/Tabs"; const AdminNav: React.FC = () => { const match = useRouteMatch<{ name: string }>(); - const history = useHistory(); - - const { t } = useTranslation(); const name = match.params.name; - function toggle(newTab: string): void { - history.push(`/admin/${newTab}`); - } - return ( - <Nav variant="tabs" className="my-2"> - <Nav.Item> - <Nav.Link - active={name === "users"} - onClick={() => { - toggle("users"); - }} - > - {t("admin:nav.users")} - </Nav.Link> - </Nav.Item> - <Nav.Item> - <Nav.Link - active={name === "more"} - onClick={() => { - toggle("more"); - }} - > - {t("admin:nav.more")} - </Nav.Link> - </Nav.Item> - </Nav> + <Tabs + activeTabName={name} + tabs={[ + { + name: "users", + text: "admin:nav.users", + link: "/admin/users", + }, + { + name: "more", + text: "admin:nav.more", + link: "/admin/more", + }, + ]} + /> ); }; diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index eb141520..481db1cc 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -1,10 +1,9 @@ import React, { useState, useEffect } from "react"; import classnames from "classnames"; -import { ListGroup, Row, Col, Spinner, Button } from "react-bootstrap"; import OperationDialog, { OperationDialogBoolInput, -} from "../common/OperationDialog"; +} from "../common/dailog/OperationDialog"; import { AuthUser } from "@/services/user"; import { @@ -14,7 +13,9 @@ import { UserPermission, } from "@/http/user"; import { Trans, useTranslation } from "react-i18next"; +import Button from "../common/button/Button"; import TextButton from "../common/button/TextButton"; +import Spinner from "../common/Spinner"; interface DialogProps<TData = undefined, TReturn = undefined> { open: boolean; @@ -45,7 +46,7 @@ const CreateUserDialog: React.FC<DialogProps<undefined, HttpUser>> = ({ password, }) } - close={close} + onClose={close} open={open} onSuccessAndClose={onSuccess} /> @@ -61,7 +62,7 @@ const UserDeleteDialog: React.FC<DialogProps<{ username: string }, unknown>> = return ( <OperationDialog open={open} - close={close} + onClose={close} title="admin:user.dialog.delete.title" themeColor="danger" inputPrompt={() => ( @@ -86,7 +87,7 @@ const UserModifyDialog: React.FC< return ( <OperationDialog open={open} - close={close} + onClose={close} title="admin:user.dialog.modify.title" themeColor="danger" inputPrompt={() => ( @@ -137,7 +138,7 @@ const UserPermissionModifyDialog: React.FC< return ( <OperationDialog open={open} - close={close} + onClose={close} title="admin:user.dialog.modifyPermissions.title" themeColor="danger" inputPrompt={() => ( @@ -203,7 +204,7 @@ const UserItem: React.FC<UserItemProps> = ({ user, on }) => { const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false); return ( - <ListGroup.Item className="admin-user-item"> + <div className="admin-user-item"> <i className="bi-pencil-square float-end icon-button text-warning" onClick={() => setEditMaskVisible(true)} @@ -242,7 +243,7 @@ const UserItem: React.FC<UserItemProps> = ({ user, on }) => { onClick={on[kDelete]} /> </div> - </ListGroup.Item> + </div> ); }; @@ -251,8 +252,6 @@ interface UserAdminProps { } const UserAdmin: React.FC<UserAdminProps> = () => { - const { t } = useTranslation(); - type DialogInfo = | null | { @@ -372,26 +371,25 @@ const UserAdmin: React.FC<UserAdminProps> = () => { return ( <> - <Row className="justify-content-end my-2"> - <Col xs="auto"> + <div className="row justify-content-end my-2"> + <div className="col col-auto"> <Button - variant="outline-success" + text="admin:create" + color="success" onClick={() => setDialog({ type: "create", }) } - > - {t("admin:create")} - </Button> - </Col> - </Row> + /> + </div> + </div> {userComponents} {dialogNode} </> ); } else { - return <Spinner animation="border" />; + return <Spinner />; } }; diff --git a/FrontEnd/src/views/center/CenterBoards.tsx b/FrontEnd/src/views/center/CenterBoards.tsx index f5200415..392c2d08 100644 --- a/FrontEnd/src/views/center/CenterBoards.tsx +++ b/FrontEnd/src/views/center/CenterBoards.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { pushAlert } from "@/services/alert"; @@ -18,10 +17,10 @@ const CenterBoards: React.FC = () => { return ( <> - <Row className="justify-content-center"> - <Col xs="12" md="6"> - <Row> - <Col xs="12" className="my-2"> + <div className="row justify-content-center"> + <div className="col col-12 col-md-6"> + <div className="row"> + <div className="col col-12 my-2"> <TimelineBoard title={t("home.bookmarkTimeline")} load={() => getHttpBookmarkClient().list()} @@ -52,8 +51,8 @@ const CenterBoards: React.FC = () => { }, }} /> - </Col> - <Col xs="12" className="my-2"> + </div> + <div className="col col-12 my-2"> <TimelineBoard title={t("home.highlightTimeline")} load={() => getHttpHighlightClient().list()} @@ -88,18 +87,18 @@ const CenterBoards: React.FC = () => { : undefined } /> - </Col> - </Row> - </Col> - <Col xs="12" md="6" className="my-2"> + </div> + </div> + </div> + <div className="col-12 col-md-6 my-2"> <TimelineBoard title={t("home.relatedTimeline")} load={() => getHttpTimelineClient().listTimeline({ relate: user.username }) } /> - </Col> - </Row> + </div> + </div> </> ); }; diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index d6f6228d..d7aa39ab 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 { Spinner } from "react-bootstrap"; import { HttpTimelineInfo } from "@/http/timeline"; @@ -10,6 +9,7 @@ import UserTimelineLogo from "../common/UserTimelineLogo"; import LoadFailReload from "../common/LoadFailReload"; import FlatButton from "../common/button/FlatButton"; import Card from "../common/Card"; +import Spinner from "../common/Spinner"; interface TimelineBoardItemProps { timeline: HttpTimelineInfo; @@ -249,7 +249,7 @@ const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => { if (timelines === "loading") { return ( <div className="d-flex flex-grow-1 justify-content-center align-items-center"> - <Spinner variant="primary" animation="border" /> + <Spinner /> </div> ); } else if (timelines === "offline") { diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx index b4e25ba1..4871a5e0 100644 --- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx +++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useHistory } from "react-router"; import { validateTimelineName } from "@/services/timeline"; -import OperationDialog from "../common/OperationDialog"; +import OperationDialog from "../common/dailog/OperationDialog"; import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; interface TimelineCreateDialogProps { @@ -16,7 +16,7 @@ const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => { return ( <OperationDialog open={props.open} - close={props.close} + onClose={props.close} themeColor="success" title="home.createDialog.title" inputScheme={ diff --git a/FrontEnd/src/views/center/index.tsx b/FrontEnd/src/views/center/index.tsx index 28d8b372..77bb6ec8 100644 --- a/FrontEnd/src/views/center/index.tsx +++ b/FrontEnd/src/views/center/index.tsx @@ -1,11 +1,10 @@ import React from "react"; import { useHistory } from "react-router"; -import { useTranslation } from "react-i18next"; -import { Row, Container, Button, Col } from "react-bootstrap"; import { useUserLoggedIn } from "@/services/user"; import SearchInput from "../common/SearchInput"; +import Button from "../common/button/Button"; import CenterBoards from "./CenterBoards"; import TimelineCreateDialog from "./TimelineCreateDialog"; @@ -14,8 +13,6 @@ import "./index.css"; const HomePage: React.FC = () => { const history = useHistory(); - const { t } = useTranslation(); - const user = useUserLoggedIn(); const [navText, setNavText] = React.useState<string>(""); @@ -24,9 +21,9 @@ const HomePage: React.FC = () => { return ( <> - <Container> - <Row className="my-3 justify-content-center"> - <Col xs={12} sm={8} lg={6}> + <div className="container"> + <div className="row my-3 justify-content-center"> + <div className="col col-12 col-sm-8 col-lg-6"> <SearchInput className="justify-content-center" value={navText} @@ -37,20 +34,19 @@ const HomePage: React.FC = () => { additionalButton={ user != null && ( <Button - variant="outline-success" + text="home.createButton" + color="success" onClick={() => { setDialog("create"); }} - > - {t("home.createButton")} - </Button> + /> ) } /> - </Col> - </Row> + </div> + </div> <CenterBoards /> - </Container> + </div> {dialog === "create" && ( <TimelineCreateDialog open diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css new file mode 100644 index 00000000..16572817 --- /dev/null +++ b/FrontEnd/src/views/common/AppBar.css @@ -0,0 +1,95 @@ +.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 .cru-avatar {
+ background-color: white;
+}
+
+.app-bar a {
+ color: var(--tl-text-on-primary-r1-color);
+ text-decoration: none;
+ margin: 0 1em;
+ transition: color 1s;
+}
+.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;
+}
diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx index ebc8bf0c..5d62a88d 100644 --- a/FrontEnd/src/views/common/AppBar.tsx +++ b/FrontEnd/src/views/common/AppBar.tsx @@ -9,7 +9,7 @@ import { useUser } from "@/services/user"; import TimelineLogo from "./TimelineLogo"; import UserAvatar from "./user/UserAvatar"; -import "./index.css"; +import "./AppBar.css"; const AppBar: React.FC = (_) => { const { t } = useTranslation(); @@ -68,7 +68,7 @@ const AppBar: React.FC = (_) => { "/", <UserAvatar username={user.username} - className="avatar small rounded-circle bg-white cursor-pointer ml-auto" + className="cru-avatar small cru-round cursor-pointer ml-auto" />, "app-bar-avatar" ) diff --git a/FrontEnd/src/views/common/ConfirmDialog.tsx b/FrontEnd/src/views/common/ConfirmDialog.tsx deleted file mode 100644 index 72940c51..00000000 --- a/FrontEnd/src/views/common/ConfirmDialog.tsx +++ /dev/null @@ -1,40 +0,0 @@ -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 ( - <Modal onHide={onClose} show centered> - <Modal.Header> - <Modal.Title className="text-danger"> - {convertI18nText(title, t)} - </Modal.Title> - </Modal.Header> - <Modal.Body>{convertI18nText(body, t)}</Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={onClose}> - {t("operationDialog.cancel")} - </Button> - <Button - variant="danger" - onClick={() => { - onConfirm(); - onClose(); - }} - > - {t("operationDialog.confirm")} - </Button> - </Modal.Footer> - </Modal> - ); -}; - -export default ConfirmDialog; diff --git a/FrontEnd/src/views/common/ImageCropper.css b/FrontEnd/src/views/common/ImageCropper.css new file mode 100644 index 00000000..2c4d0a8c --- /dev/null +++ b/FrontEnd/src/views/common/ImageCropper.css @@ -0,0 +1,38 @@ +.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;
+}
diff --git a/FrontEnd/src/views/common/ImageCropper.tsx b/FrontEnd/src/views/common/ImageCropper.tsx index 2ef5b7ed..be44200a 100644 --- a/FrontEnd/src/views/common/ImageCropper.tsx +++ b/FrontEnd/src/views/common/ImageCropper.tsx @@ -3,6 +3,8 @@ import classnames from "classnames"; import { UiLogicError } from "@/common"; +import "./ImageCropper.css"; + export interface Clip { left: number; top: number; diff --git a/FrontEnd/src/views/common/LoadingButton.tsx b/FrontEnd/src/views/common/LoadingButton.tsx deleted file mode 100644 index cd9f1adc..00000000 --- a/FrontEnd/src/views/common/LoadingButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { Button, ButtonProps, Spinner } from "react-bootstrap"; - -const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({ - loading, - variant, - disabled, - ...otherProps -}) => { - return ( - <Button - variant={variant != null ? `outline-${variant}` : "outline-primary"} - disabled={disabled || loading} - {...otherProps} - > - {otherProps.children} - {loading ? ( - <Spinner - className="ms-1" - variant={variant} - animation="grow" - size="sm" - /> - ) : null} - </Button> - ); -}; - -export default LoadingButton; diff --git a/FrontEnd/src/views/common/LoadingPage.tsx b/FrontEnd/src/views/common/LoadingPage.tsx index 590fafa0..8c1e681a 100644 --- a/FrontEnd/src/views/common/LoadingPage.tsx +++ b/FrontEnd/src/views/common/LoadingPage.tsx @@ -1,10 +1,11 @@ import React from "react"; -import { Spinner } from "react-bootstrap"; + +import Spinner from "./Spinner"; const LoadingPage: React.FC = () => { return ( <div className="position-fixed w-100 h-100 d-flex justify-content-center align-items-center"> - <Spinner variant="primary" animation="border" /> + <Spinner /> </div> ); }; diff --git a/FrontEnd/src/views/common/Menu.tsx b/FrontEnd/src/views/common/Menu.tsx index ae73a331..a5d2ec2c 100644 --- a/FrontEnd/src/views/common/Menu.tsx +++ b/FrontEnd/src/views/common/Menu.tsx @@ -1,9 +1,9 @@ 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"; +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; export type MenuItem = | { @@ -13,7 +13,7 @@ export type MenuItem = type: "button"; text: I18nText; iconClassName?: string; - color?: BootstrapThemeColor; + color?: PaletteColorType; onClick: () => void; }; @@ -67,26 +67,14 @@ export default Menu; export interface PopupMenuProps { items: MenuItems; - children: OverlayTriggerProps["children"]; + children: React.ReactElement; } export const PopupMenu: React.FC<PopupMenuProps> = ({ items, children }) => { const [show, setShow] = React.useState<boolean>(false); const toggle = (): void => setShow(!show); - return ( - <OverlayTrigger - trigger="click" - rootClose - overlay={ - <Popover id="menu-popover"> - <Menu items={items} onItemClicked={() => setShow(false)} /> - </Popover> - } - show={show} - onToggle={toggle} - > - {children} - </OverlayTrigger> - ); + // TODO: + + return <Menu items={items} onItemClicked={() => setShow(false)} />; }; diff --git a/FrontEnd/src/views/common/SearchInput.css b/FrontEnd/src/views/common/SearchInput.css new file mode 100644 index 00000000..2943b3a2 --- /dev/null +++ b/FrontEnd/src/views/common/SearchInput.css @@ -0,0 +1,4 @@ +.cru-search-input {
+ display: flex;
+ flex-wrap: wrap;
+}
diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx index ccb6dad6..da3f1c19 100644 --- a/FrontEnd/src/views/common/SearchInput.tsx +++ b/FrontEnd/src/views/common/SearchInput.tsx @@ -1,7 +1,10 @@ import React, { useCallback } from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { Spinner, Form, Button } from "react-bootstrap"; + +import LoadingButton from "./button/LoadingButton"; + +import "./SearchInput.css"; export interface SearchInputProps { value: string; @@ -38,14 +41,15 @@ const SearchInput: React.FC<SearchInputProps> = (props) => { ); return ( - <Form + <div className={classnames( "cru-search-input", alwaysOneline ? "flex-nowrap" : "flex-sm-nowrap", props.className )} > - <Form.Control + <input + type="text" className="me-sm-2 flex-grow-1" value={props.value} onChange={onInputChange} @@ -63,15 +67,11 @@ const SearchInput: React.FC<SearchInputProps> = (props) => { "flex-shrink-0" )} > - {props.loading ? ( - <Spinner variant="primary" animation="border" /> - ) : ( - <Button variant="outline-primary" onClick={props.onButtonClick}> - {props.buttonText ?? t("search")} - </Button> - )} + <LoadingButton loading={props.loading} onClick={props.onButtonClick}> + {props.buttonText ?? t("search")} + </LoadingButton> </div> - </Form> + </div> ); }; diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx new file mode 100644 index 00000000..783c9be2 --- /dev/null +++ b/FrontEnd/src/views/common/Spinner.tsx @@ -0,0 +1,13 @@ +import { PaletteColorType } from "@/palette"; +import React from "react"; + +export interface SpinnerProps { + size?: "sm" | "md" | "lg" | number; + color?: PaletteColorType; +} + +export default function Spinner( + props: SpinnerProps +): React.ReactElement | null { + return <span />; +} diff --git a/FrontEnd/src/views/common/ToggleIconButton.tsx b/FrontEnd/src/views/common/ToggleIconButton.tsx deleted file mode 100644 index c4d2d132..00000000 --- a/FrontEnd/src/views/common/ToggleIconButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import classnames from "classnames"; - -export interface ToggleIconButtonProps - extends React.HTMLAttributes<HTMLElement> { - state: boolean; - trueIconClassName: string; - falseIconClassName: string; -} - -const ToggleIconButton: React.FC<ToggleIconButtonProps> = ({ - state, - className, - trueIconClassName, - falseIconClassName, - ...otherProps -}) => { - return ( - <i - className={classnames( - state ? trueIconClassName : falseIconClassName, - "icon-button", - className - )} - {...otherProps} - /> - ); -}; - -export default ToggleIconButton; diff --git a/FrontEnd/src/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx index 949be7ed..21b9882d 100644 --- a/FrontEnd/src/views/common/alert/AlertHost.tsx +++ b/FrontEnd/src/views/common/alert/AlertHost.tsx @@ -1,7 +1,6 @@ import React from "react"; import without from "lodash/without"; import { useTranslation } from "react-i18next"; -import { Alert } from "react-bootstrap"; import { alertService, @@ -52,13 +51,7 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { }; return ( - <Alert - className="m-3" - variant={alert.type ?? "primary"} - onClick={cancelTimer} - onClose={close} - dismissible - > + <div className="m-3" onClick={cancelTimer}> {(() => { const { message } = alert; if (typeof message === "function") { @@ -66,7 +59,7 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { return <Message />; } else return convertI18nText(message, t); })()} - </Alert> + </div> ); }; diff --git a/FrontEnd/src/views/common/alert/alert.css b/FrontEnd/src/views/common/alert/alert.css new file mode 100644 index 00000000..12ce294e --- /dev/null +++ b/FrontEnd/src/views/common/alert/alert.css @@ -0,0 +1,4 @@ +.alert-container {
+ position: fixed;
+ z-index: 1040;
+}
diff --git a/FrontEnd/src/views/common/alert/alert.sass b/FrontEnd/src/views/common/alert/alert.sass deleted file mode 100644 index c3560b87..00000000 --- a/FrontEnd/src/views/common/alert/alert.sass +++ /dev/null @@ -1,15 +0,0 @@ -.alert-container
- position: fixed
- z-index: $zindex-popover
-
-@include media-breakpoint-up(sm)
- .alert-container
- bottom: 0
- right: 0
-
-@include media-breakpoint-down(sm)
- .alert-container
- bottom: 0
- right: 0
- left: 0
- text-align: center
diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css new file mode 100644 index 00000000..b6df222f --- /dev/null +++ b/FrontEnd/src/views/common/button/Button.css @@ -0,0 +1,72 @@ +.cru-button {
+ color: white;
+ cursor: pointer;
+ padding: 0.2em 0.5em;
+ border-radius: 0.2em;
+ border: none;
+ transition: all 0.6s;
+}
+
+.cru-button.primary {
+ background-color: var(--tl-primary-color);
+}
+
+.cru-button.primary:hover {
+ background-color: var(--tl-primary-f1-color);
+}
+
+.cru-button.primary:active {
+ background-color: var(--tl-primary-f2-color);
+}
+
+.cru-button.primary.disabled {
+ background-color: var(--tl-primary-f3-color);
+}
+
+.cru-button.secondary {
+ background-color: var(--tl-secondary-color);
+}
+
+.cru-button.secondary:hover {
+ background-color: var(--tl-secondary-f1-color);
+}
+
+.cru-button.secondary:active {
+ background-color: var(--tl-secondary-f2-color);
+}
+
+.cru-button.secondary.disabled {
+ background-color: var(--tl-secondary-f3-color);
+}
+
+.cru-button.success {
+ background-color: var(--tl-success-color);
+}
+
+.cru-button.success:hover {
+ background-color: var(--tl-success-f1-color);
+}
+
+.cru-button.success:active {
+ background-color: var(--tl-success-f2-color);
+}
+
+.cru-button.success.disabled {
+ background-color: var(--tl-success-f3-color);
+}
+
+.cru-button.danger {
+ background-color: var(--tl-danger-color);
+}
+
+.cru-button.danger:hover {
+ background-color: var(--tl-danger-f1-color);
+}
+
+.cru-button.danger:active {
+ background-color: var(--tl-danger-f2-color);
+}
+
+.cru-button.danger.disabled {
+ background-color: var(--tl-danger-f3-color);
+}
diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx new file mode 100644 index 00000000..a39ef8a7 --- /dev/null +++ b/FrontEnd/src/views/common/button/Button.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { calculateProps, CommonButtonProps } from "./common"; + +import "./Button.css"; + +function _Button( + props: CommonButtonProps & { + outline?: boolean; + customButtonClassName?: string; + }, + ref: React.ForwardedRef<HTMLButtonElement> +): React.ReactElement | null { + const { t } = useTranslation(); + + const { customButtonClassName, outline, ...otherProps } = props; + + const { newProps, children } = calculateProps( + otherProps, + customButtonClassName ?? "cru-button" + (outline ? " outline" : ""), + t + ); + + return ( + <button ref={ref} {...newProps}> + {children} + </button> + ); +} + +const Button = React.forwardRef(_Button); +export default Button; diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css index 522563b9..c3c0dbb3 100644 --- a/FrontEnd/src/views/common/button/FlatButton.css +++ b/FrontEnd/src/views/common/button/FlatButton.css @@ -20,7 +20,7 @@ }
.cru-flat-button.primary.disabled {
- color: var(--tl-primary-lighter-color);
+ color: var(--tl-primary-l1-color);
}
.cru-flat-button.secondary {
@@ -28,7 +28,7 @@ }
.cru-flat-button.secondary.disabled {
- color: var(--tl-secondary-lighter-color);
+ color: var(--tl-secondary-l1-color);
}
.cru-flat-button.success {
@@ -36,7 +36,7 @@ }
.cru-flat-button.success.disabled {
- color: var(--tl-success-lighter-color);
+ color: var(--tl-success-l1-color);
}
.cru-flat-button.danger {
@@ -44,5 +44,5 @@ }
.cru-flat-button.danger.disabled {
- color: var(--tl-danger-ligher-color);
+ color: var(--tl-danger-l1-color);
}
diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 6351971a..266ea908 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -1,39 +1,16 @@ import React from "react"; -import { useTranslation } from "react-i18next"; -import classNames from "classnames"; -import { convertI18nText, I18nText } from "@/common"; -import { PaletteColorType } from "@/palette"; +import { CommonButtonProps } from "./common"; +import Button from "./Button"; import "./FlatButton.css"; function _FlatButton( - { - text, - color, - onClick, - className, - style, - }: { - text: I18nText; - color?: PaletteColorType; - onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; - className?: string; - style?: React.CSSProperties; - }, + props: CommonButtonProps, ref: React.ForwardedRef<HTMLButtonElement> ): React.ReactElement | null { - const { t } = useTranslation(); - return ( - <button - ref={ref} - className={classNames("cru-flat-button", color ?? "primary", className)} - onClick={onClick} - style={style} - > - {convertI18nText(text, t)} - </button> + <Button ref={ref} customButtonClassName="cru-flat-button" {...props} /> ); } diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx new file mode 100644 index 00000000..aee83aa2 --- /dev/null +++ b/FrontEnd/src/views/common/button/LoadingButton.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import { CommonButtonProps } from "./common"; +import Button from "./Button"; +import Spinner from "../Spinner"; + +const LoadingButton: React.FC<{ loading?: boolean } & CommonButtonProps> = ({ + loading, + disabled, + color, + ...otherProps +}) => { + return ( + <Button color={color} disabled={disabled || loading} {...otherProps}> + {otherProps.children} + {loading ? <Spinner color={color} /> : null} + </Button> + ); +}; + +export default LoadingButton; diff --git a/FrontEnd/src/views/common/button/TextButton.css b/FrontEnd/src/views/common/button/TextButton.css index dc5abaaa..d267fb38 100644 --- a/FrontEnd/src/views/common/button/TextButton.css +++ b/FrontEnd/src/views/common/button/TextButton.css @@ -8,7 +8,7 @@ }
.cru-text-button.primary:hover {
- color: var(--tl-primary-lighter-color);
+ color: var(--tl-primary-l1-color);
}
.cru-text-button.secondary {
@@ -16,7 +16,7 @@ }
.cru-text-button.secondary:hover {
- color: var(--tl-secondary-lighter-color);
+ color: var(--tl-secondary-l1-color);
}
.cru-text-button.success {
@@ -24,7 +24,7 @@ }
.cru-text-button.success:hover {
- color: var(--tl-success-lighter-color);
+ color: var(--tl-success-l1-color);
}
.cru-text-button.danger {
@@ -32,5 +32,5 @@ }
.cru-text-button.danger:hover {
- color: var(--tl-danger-lighter-color);
+ color: var(--tl-danger-l1-color);
}
diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx index 1a2bac94..1d8e7a4b 100644 --- a/FrontEnd/src/views/common/button/TextButton.tsx +++ b/FrontEnd/src/views/common/button/TextButton.tsx @@ -1,39 +1,16 @@ import React from "react"; -import { useTranslation } from "react-i18next"; -import classNames from "classnames"; -import { convertI18nText, I18nText } from "@/common"; -import { PaletteColorType } from "@/palette"; +import { CommonButtonProps } from "./common"; +import Button from "./Button"; import "./TextButton.css"; function _TextButton( - { - text, - color, - onClick, - className, - style, - }: { - text: I18nText; - color?: PaletteColorType; - onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; - className?: string; - style?: React.CSSProperties; - }, + props: CommonButtonProps, ref: React.ForwardedRef<HTMLButtonElement> ): React.ReactElement | null { - const { t } = useTranslation(); - return ( - <button - ref={ref} - className={classNames("cru-text-button", color ?? "primary", className)} - onClick={onClick} - style={style} - > - {convertI18nText(text, t)} - </button> + <Button ref={ref} customButtonClassName="cru-flat-button" {...props} /> ); } diff --git a/FrontEnd/src/views/common/button/common.ts b/FrontEnd/src/views/common/button/common.ts new file mode 100644 index 00000000..a9db959e --- /dev/null +++ b/FrontEnd/src/views/common/button/common.ts @@ -0,0 +1,31 @@ +import React from "react"; +import classNames from "classnames"; +import { TFunction } from "i18next"; + +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; + +export type CommonButtonProps = { + text?: I18nText; + color?: PaletteColorType; +} & React.ButtonHTMLAttributes<HTMLButtonElement>; + +export function calculateProps( + props: CommonButtonProps, + buttonClassName: string, + t: TFunction +): { + children: React.ReactNode; + newProps: React.ButtonHTMLAttributes<HTMLButtonElement>; +} { + const { text, color, className, children, ...otherProps } = props; + const newProps = { + className: classNames(buttonClassName, color ?? "primary", className), + ...otherProps, + }; + + return { + children: text != null ? convertI18nText(text, t) : children, + newProps: newProps, + }; +} diff --git a/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx b/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx new file mode 100644 index 00000000..1ad52350 --- /dev/null +++ b/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx @@ -0,0 +1,40 @@ +import { convertI18nText, I18nText } from "@/common"; +import React from "react"; +import { useTranslation } from "react-i18next"; + +import Button from "../button/Button"; +import Dialog from "./Dialog"; + +const ConfirmDialog: React.FC<{ + open?: boolean; + onClose: () => void; + onConfirm: () => void; + title: I18nText; + body: I18nText; +}> = ({ open, onClose, onConfirm, title, body }) => { + const { t } = useTranslation(); + + return ( + <Dialog onClose={onClose} open={open}> + <h3 className="text-danger">{convertI18nText(title, t)}</h3> + <p>{convertI18nText(body, t)}</p> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={onClose} + /> + <Button + text="operationDialog.confirm" + color="danger" + onClick={() => { + onConfirm(); + onClose(); + }} + /> + </div> + </Dialog> + ); +}; + +export default ConfirmDialog; diff --git a/FrontEnd/src/views/common/dailog/Dialog.css b/FrontEnd/src/views/common/dailog/Dialog.css new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/FrontEnd/src/views/common/dailog/Dialog.css diff --git a/FrontEnd/src/views/common/dailog/Dialog.tsx b/FrontEnd/src/views/common/dailog/Dialog.tsx new file mode 100644 index 00000000..5a3902c4 --- /dev/null +++ b/FrontEnd/src/views/common/dailog/Dialog.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +export interface DialogProps { + onClose: () => void; + open?: boolean; + children?: React.ReactNode; +} + +export default function Dialog(props: DialogProps): React.ReactElement | null { + const { open, onClose, children } = props; + + return <div>{children}</div>; +} diff --git a/FrontEnd/src/views/common/FullPage.tsx b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx index 1b59045a..88c90bbc 100644 --- a/FrontEnd/src/views/common/FullPage.tsx +++ b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx @@ -1,13 +1,13 @@ import React from "react"; import classnames from "classnames"; -export interface FullPageProps { +export interface FullPageDialogProps { show: boolean; onBack: () => void; contentContainerClassName?: string; } -const FullPage: React.FC<FullPageProps> = ({ +const FullPageDialog: React.FC<FullPageDialogProps> = ({ show, onBack, children, @@ -36,4 +36,4 @@ const FullPage: React.FC<FullPageProps> = ({ ); }; -export default FullPage; +export default FullPageDialog; diff --git a/FrontEnd/src/views/common/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx index ac4c51b9..1e765276 100644 --- a/FrontEnd/src/views/common/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx @@ -1,12 +1,15 @@ 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"; +import { PaletteColorType } from "@/palette"; + +import Button from "../button/Button"; +import LoadingButton from "../button/LoadingButton"; +import Dialog from "./Dialog"; interface DefaultErrorPromptProps { error?: string; @@ -141,9 +144,9 @@ export interface OperationDialogProps< OperationInputInfoList extends readonly OperationDialogInput[] > { open: boolean; - close: () => void; + onClose: () => void; title: I18nText | (() => React.ReactNode); - themeColor?: "danger" | "success" | string; + themeColor?: PaletteColorType; onProcess: ( inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList> ) => Promise<TData>; @@ -204,7 +207,7 @@ const OperationDialog = < const close = (): void => { if (step.type !== "process") { - props.close(); + props.onClose(); if (step.type === "success" && props.onSuccessAndClose) { props.onSuccessAndClose(step.data); } @@ -278,7 +281,7 @@ const OperationDialog = < body = ( <> - <Modal.Body> + <div> {inputPrompt} {inputScheme.map((item, index) => { const value = values[index]; @@ -289,50 +292,42 @@ const OperationDialog = < if (item.type === "text") { return ( - <Form.Group key={index}> + <div key={index}> {item.label && ( - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> + <label>{convertI18nText(item.label, t)}</label> )} - <Form.Control + <input type={item.password === true ? "password" : "text"} value={value as string} onChange={(e) => { const v = e.target.value; updateValue(index, v); }} - isInvalid={error != null} disabled={process} /> - {error != null && ( - <Form.Control.Feedback type="invalid"> - {error} - </Form.Control.Feedback> - )} - {item.helperText && ( - <Form.Text>{t(item.helperText)}</Form.Text> - )} - </Form.Group> + {error != null && <div>{error}</div>} + {item.helperText && <div>{t(item.helperText)}</div>} + </div> ); } else if (item.type === "bool") { return ( - <Form.Group key={index}> - <Form.Check<"input"> + <div key={index}> + <input type="checkbox" checked={value as boolean} onChange={(event) => { updateValue(index, event.currentTarget.checked); }} - label={convertI18nText(item.label, t)} disabled={process} /> - </Form.Group> + <label>{convertI18nText(item.label, t)}</label> + </div> ); } else if (item.type === "select") { return ( - <Form.Group key={index}> - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> - <Form.Control - as="select" + <div key={index}> + <label>{convertI18nText(item.label, t)}</label> + <select value={value as string} onChange={(event) => { updateValue(index, event.target.value); @@ -347,14 +342,14 @@ const OperationDialog = < </option> ); })} - </Form.Control> - </Form.Group> + </select> + </div> ); } else if (item.type === "color") { return ( - <Form.Group key={index}> + <div key={index}> {item.canBeNull ? ( - <Form.Check<"input"> + <input type="checkbox" checked={value !== null} onChange={(event) => { @@ -364,52 +359,47 @@ const OperationDialog = < updateValue(index, null); } }} - label={convertI18nText(item.label, t)} disabled={process} /> - ) : ( - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> - )} + ) : null} + <label>{convertI18nText(item.label, t)}</label> {value !== null && ( <TwitterPicker color={value as string} onChange={(result) => updateValue(index, result.hex)} /> )} - </Form.Group> + </div> ); } else if (item.type === "datetime") { return ( - <Form.Group key={index}> + <div key={index}> {item.label && ( - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> + <label>{convertI18nText(item.label, t)}</label> )} - <Form.Control + <input type="datetime-local" value={value as string} onChange={(e) => { const v = e.target.value; updateValue(index, v); }} - isInvalid={error != null} disabled={process} /> - {error != null && ( - <Form.Control.Feedback type="invalid"> - {error} - </Form.Control.Feedback> - )} - </Form.Group> + {error != null && <div>{error}</div>} + </div> ); } })} - </Modal.Body> - <Modal.Footer> - <Button variant="outline-secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> + </div> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={close} + /> <LoadingButton - variant={props.themeColor} + color={props.themeColor} loading={process} disabled={!canProcess} onClick={() => { @@ -421,7 +411,7 @@ const OperationDialog = < > {t("operationDialog.confirm")} </LoadingButton> - </Modal.Footer> + </div> </> ); } else { @@ -439,12 +429,10 @@ const OperationDialog = < } body = ( <> - <Modal.Body>{content}</Modal.Body> - <Modal.Footer> - <Button variant="primary" onClick={close}> - {t("operationDialog.ok")} - </Button> - </Modal.Footer> + <div>{content}</div> + <div> + <Button text="operationDialog.ok" color="primary" onClick={close} /> + </div> </> ); } @@ -455,16 +443,16 @@ const OperationDialog = < : convertI18nText(props.title, t); return ( - <Modal show={props.open} onHide={close}> - <Modal.Header + <Dialog open={props.open} onClose={close}> + <h3 className={ props.themeColor != null ? "text-" + props.themeColor : undefined } > {title} - </Modal.Header> + </h3> {body} - </Modal> + </Dialog> ); }; diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css index bfd82b58..aaaaad58 100644 --- a/FrontEnd/src/views/common/index.css +++ b/FrontEnd/src/views/common/index.css @@ -1,130 +1,20 @@ -.image-cropper-container {
- position: relative;
- box-sizing: border-box;
- user-select: none;
+.cru-avatar {
+ width: 60px;
+ height: 60px;
}
-.image-cropper-container img {
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
+.cru-avatar.large {
+ width: 100px;
+ height: 100px;
}
-.image-cropper-mask-container {
- position: absolute;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- overflow: hidden;
+.cru-avatar.small {
+ width: 40px;
+ height: 40px;
}
-.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;
+.cru-round {
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 {
@@ -247,11 +137,6 @@ align-items: center;
}
-.cru-search-input {
- display: flex;
- flex-wrap: wrap;
-}
-
.alert-container {
position: fixed;
z-index: 1070;
diff --git a/FrontEnd/src/views/common/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx index 2b1d91cb..b7a9fb36 100644 --- a/FrontEnd/src/views/common/TabPages.tsx +++ b/FrontEnd/src/views/common/tab/TabPages.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Nav } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { convertI18nText, I18nText, UiLogicError } from "@/common"; @@ -31,6 +30,8 @@ const TabPages: React.FC<TabPagesProps> = ({ pageContainerClassName, pageContainerStyle, }) => { + // TODO: + if (pages.length === 0) { throw new UiLogicError("Page list can't be empty."); } @@ -47,23 +48,6 @@ const TabPages: React.FC<TabPagesProps> = ({ return ( <div className={className} style={style}> - <Nav variant="tabs" className={navClassName} style={navStyle}> - {pages.map((page) => ( - <Nav.Item key={page.id}> - <Nav.Link - active={tab === page.id} - onClick={() => { - setTab(page.id); - }} - > - {convertI18nText(page.tabText, t)} - </Nav.Link> - </Nav.Item> - ))} - {actions != null && ( - <div className="ms-auto cru-tab-pages-action-area">{actions}</div> - )} - </Nav> <div className={pageContainerClassName} style={pageContainerStyle}> {currentPage.page} </div> diff --git a/FrontEnd/src/views/common/tab/Tabs.tsx b/FrontEnd/src/views/common/tab/Tabs.tsx new file mode 100644 index 00000000..29ebcbd8 --- /dev/null +++ b/FrontEnd/src/views/common/tab/Tabs.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import { I18nText } from "@/common"; + +export interface Tab { + name: string; + text: I18nText; + link?: string; + onClick?: () => void; +} + +export interface TabsProps { + activeTabName?: string; + tabs: Tab[]; +} + +export default function Tabs(props: TabsProps): React.ReactElement | null { + return <div></div>; +} diff --git a/FrontEnd/src/views/login/index.tsx b/FrontEnd/src/views/login/index.tsx index a39a9972..6d70c64a 100644 --- a/FrontEnd/src/views/login/index.tsx +++ b/FrontEnd/src/views/login/index.tsx @@ -1,12 +1,11 @@ import React from "react"; import { useHistory } from "react-router"; import { useTranslation } from "react-i18next"; -import { Container, Form } from "react-bootstrap"; import { useUser, userService } from "@/services/user"; import AppBar from "../common/AppBar"; -import LoadingButton from "../common/LoadingButton"; +import LoadingButton from "../common/button/LoadingButton"; import "./index.css"; @@ -79,74 +78,63 @@ const LoginPage: React.FC = (_) => { }; return ( - <Container fluid className="login-container mt-2"> + <div className="login-container container-fluid mt-2"> <h1 className="text-center">{t("welcome")}</h1> - <Form> - <Form.Group> - <Form.Label htmlFor="username">{t("user.username")}</Form.Label> - <Form.Control - id="username" - disabled={process} - onChange={(e) => { - setUsername(e.target.value); - setUsernameDirty(true); - }} - value={username} - isInvalid={usernameDirty && username === ""} - /> - {usernameDirty && username === "" && ( - <Form.Control.Feedback type="invalid"> - {t("login.emptyUsername")} - </Form.Control.Feedback> - )} - </Form.Group> - <Form.Group> - <Form.Label htmlFor="password">{t("user.password")}</Form.Label> - <Form.Control - id="password" - type="password" - disabled={process} - onChange={(e) => { - setPassword(e.target.value); - setPasswordDirty(true); - }} - value={password} - onKeyDown={onEnterPressInPassword} - isInvalid={passwordDirty && password === ""} - /> - {passwordDirty && password === "" && ( - <Form.Control.Feedback type="invalid"> - {t("login.emptyPassword")} - </Form.Control.Feedback> - )} - </Form.Group> - <Form.Group> - <Form.Check<"input"> - id="remember-me" - type="checkbox" - checked={rememberMe} - onChange={(e) => { - setRememberMe(e.currentTarget.checked); - }} - label={t("user.rememberMe")} - /> - </Form.Group> - {error ? <p className="text-danger">{t(error)}</p> : null} - <div className="text-end"> - <LoadingButton - loading={process} - variant="primary" - onClick={(e) => { - submit(); - e.preventDefault(); - }} - disabled={username === "" || password === "" ? true : undefined} - > - {t("user.login")} - </LoadingButton> - </div> - </Form> - </Container> + <div> + <label htmlFor="username">{t("user.username")}</label> + <input + id="username" + disabled={process} + onChange={(e) => { + setUsername(e.target.value); + setUsernameDirty(true); + }} + value={username} + /> + {usernameDirty && username === "" && ( + <div>{t("login.emptyUsername")}</div> + )} + <label htmlFor="password">{t("user.password")}</label> + <input + id="password" + type="password" + disabled={process} + onChange={(e) => { + setPassword(e.target.value); + setPasswordDirty(true); + }} + value={password} + onKeyDown={onEnterPressInPassword} + /> + {passwordDirty && password === "" && ( + <div>{t("login.emptyPassword")}</div> + )} + </div> + <div> + <input + id="remember-me" + type="checkbox" + checked={rememberMe} + onChange={(e) => { + setRememberMe(e.currentTarget.checked); + }} + /> + <label>{t("user.rememberMe")}</label> + </div> + {error ? <p className="text-danger">{t(error)}</p> : null} + <div className="text-end"> + <LoadingButton + loading={process} + onClick={(e) => { + submit(); + e.preventDefault(); + }} + disabled={username === "" || password === "" ? true : undefined} + > + {t("user.login")} + </LoadingButton> + </div> + </div> ); }; diff --git a/FrontEnd/src/views/search/index.tsx b/FrontEnd/src/views/search/index.tsx index e849a95d..509fd8c0 100644 --- a/FrontEnd/src/views/search/index.tsx +++ b/FrontEnd/src/views/search/index.tsx @@ -1,6 +1,5 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { Container, Row } from "react-bootstrap"; import { useHistory, useLocation } from "react-router"; import { Link } from "react-router-dom"; @@ -80,8 +79,8 @@ const SearchPage: React.FC = () => { }, [queryParam, forceResearchKey]); return ( - <Container className="my-3"> - <Row className="justify-content-center"> + <div className="container my-3"> + <div className="row justify-content-center"> <SearchInput className="col-12 col-sm-9 col-md-6" value={searchText} @@ -95,7 +94,7 @@ const SearchPage: React.FC = () => { } }} /> - </Row> + </div> {(() => { switch (state) { case "init": { @@ -123,7 +122,7 @@ const SearchPage: React.FC = () => { } } })()} - </Container> + </div> ); }; diff --git a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx index c4f6f492..0bf51c21 100644 --- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; -import { Modal, Row, Button } from "react-bootstrap"; import { UiLogicError } from "@/common"; @@ -10,6 +9,8 @@ import { useUserLoggedIn } from "@/services/user"; import { getHttpUserClient } from "@/http/user"; import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; +import Button from "../common/button/Button"; +import Dialog from "../common/dailog/Dialog"; export interface ChangeAvatarDialogProps { open: boolean; @@ -148,36 +149,38 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { throw new UiLogicError(); } return ( - <Row className="justify-content-center"> + <div className="row justify-content-center"> <img className="change-avatar-img" src={resultUrl} alt={t("settings.dialogChangeAvatar.previewImgAlt")} /> - </Row> + </div> ); }; return ( - <Modal show={props.open} onHide={close}> - <Modal.Header> - <Modal.Title> {t("settings.dialogChangeAvatar.title")}</Modal.Title> - </Modal.Header> + <Dialog open={props.open} onClose={close}> + <h3>{t("settings.dialogChangeAvatar.title")}</h3> {(() => { if (state === "select") { return ( <> - <Modal.Body className="container"> - <Row>{t("settings.dialogChangeAvatar.prompt.select")}</Row> - <Row> + <div className="container"> + <div className="row"> + {t("settings.dialogChangeAvatar.prompt.select")} + </div> + <div className="row"> <input type="file" accept="image/*" onChange={onSelectFile} /> - </Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - </Modal.Footer> + </div> + </div> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={close} + /> + </div> </> ); } else if (state === "crop") { @@ -186,119 +189,144 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { } return ( <> - <Modal.Body className="container"> - <Row className="justify-content-center"> + <div className="container"> + <div className="row justify-content-center"> <ImageCropper clip={clip} onChange={setClip} imageUrl={fileUrl} imageElementCallback={setCropImgElement} /> - </Row> - <Row>{t("settings.dialogChangeAvatar.prompt.crop")}</Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="secondary" onClick={onCropPrevious}> - {t("operationDialog.previousStep")} - </Button> + </div> + <div className="row"> + {t("settings.dialogChangeAvatar.prompt.crop")} + </div> + </div> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={close} + /> <Button + text="operationDialog.previousStep" + color="secondary" + onClick={onCropPrevious} + /> + <Button + text="operationDialog.nextStep" color="primary" onClick={onCropNext} disabled={ cropImgElement == null || clip == null || clip.width === 0 } - > - {t("operationDialog.nextStep")} - </Button> - </Modal.Footer> + /> + </div> </> ); } else if (state === "processcrop") { return ( <> - <Modal.Body className="container"> - <Row> + <div className="container"> + <div className="row"> {t("settings.dialogChangeAvatar.prompt.processingCrop")} - </Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="secondary" onClick={onPreviewPrevious}> - {t("operationDialog.previousStep")} - </Button> - </Modal.Footer> + </div> + </div> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={close} + /> + <Button + text="operationDialog.previousStep" + color="secondary" + onClick={onPreviewPrevious} + /> + </div> </> ); } else if (state === "preview") { return ( <> - <Modal.Body className="container"> + <div className="container"> {createPreviewRow()} - <Row>{t("settings.dialogChangeAvatar.prompt.preview")}</Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="secondary" onClick={onPreviewPrevious}> - {t("operationDialog.previousStep")} - </Button> - <Button variant="primary" onClick={upload}> - {t("settings.dialogChangeAvatar.upload")} - </Button> - </Modal.Footer> + <div className="row"> + {t("settings.dialogChangeAvatar.prompt.preview")} + </div> + </div> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={close} + /> + <Button + text="operationDialog.previousStep" + color="secondary" + onClick={onPreviewPrevious} + /> + <Button + text="settings.dialogChangeAvatar.upload" + color="primary" + onClick={upload} + /> + </div> </> ); } else if (state === "uploading") { return ( <> - <Modal.Body className="container"> + <div className="container"> {createPreviewRow()} - <Row>{t("settings.dialogChangeAvatar.prompt.uploading")}</Row> - </Modal.Body> - <Modal.Footer></Modal.Footer> + <div className="row"> + {t("settings.dialogChangeAvatar.prompt.uploading")} + </div> + </div> + <div></div> </> ); } else if (state === "success") { return ( <> - <Modal.Body className="container"> - <Row className="p-4 text-success"> + <div className="container"> + <div className="row p-4 text-success"> {t("operationDialog.success")} - </Row> - </Modal.Body> - <Modal.Footer> - <Button variant="success" onClick={close}> - {t("operationDialog.ok")} - </Button> - </Modal.Footer> + </div> + </div> + <div> + <Button + text="operationDialog.ok" + color="success" + onClick={close} + /> + </div> </> ); } else { return ( <> - <Modal.Body className="container"> + <div className="container"> {createPreviewRow()} - <Row className="text-danger">{trueMessage}</Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="primary" onClick={upload}> - {t("operationDialog.retry")} - </Button> - </Modal.Footer> + <div className="row text-danger">{trueMessage}</div> + </div> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={close} + /> + <Button + text="operationDialog.retry" + color="primary" + onClick={upload} + /> + </div> </> ); } })()} - </Modal> + </Dialog> ); }; diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx index 4b44cdd6..605796ca 100644 --- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx @@ -2,7 +2,7 @@ import { getHttpUserClient } from "@/http/user"; import { useUserLoggedIn } from "@/services/user"; import React from "react"; -import OperationDialog from "../common/OperationDialog"; +import OperationDialog from "../common/dailog/OperationDialog"; export interface ChangeNicknameDialogProps { open: boolean; @@ -24,7 +24,7 @@ const ChangeNicknameDialog: React.FC<ChangeNicknameDialogProps> = (props) => { nickname: newNickname, }); }} - close={props.close} + onClose={props.close} /> ); }; diff --git a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx b/FrontEnd/src/views/settings/ChangePasswordDialog.tsx index 21eeeb09..944fdaed 100644 --- a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx +++ b/FrontEnd/src/views/settings/ChangePasswordDialog.tsx @@ -3,7 +3,7 @@ import { useHistory } from "react-router"; import { userService } from "@/services/user"; -import OperationDialog from "../common/OperationDialog"; +import OperationDialog from "../common/dailog/OperationDialog"; export interface ChangePasswordDialogProps { open: boolean; @@ -55,7 +55,7 @@ const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => { await userService.changePassword(oldPassword, newPassword); setRedirect(true); }} - close={() => { + onClose={() => { props.close(); if (redirect) { history.push("/login"); diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx index 840bb7e8..f25911d7 100644 --- a/FrontEnd/src/views/settings/index.tsx +++ b/FrontEnd/src/views/settings/index.tsx @@ -1,43 +1,17 @@ import React, { useState } from "react"; import { useHistory } from "react-router"; import { useTranslation } from "react-i18next"; -import { Container, Form, Row, Col, Button, Modal } from "react-bootstrap"; import { useUser, userService } from "@/services/user"; import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; +import ConfirmDialog from "../common/dailog/ConfirmDialog"; import Card from "../common/Card"; import "./index.css"; -const ConfirmLogoutDialog: React.FC<{ - onClose: () => void; - onConfirm: () => void; -}> = ({ onClose, onConfirm }) => { - const { t } = useTranslation(); - - return ( - <Modal show centered onHide={onClose}> - <Modal.Header> - <Modal.Title className="text-danger"> - {t("settings.dialogConfirmLogout.title")} - </Modal.Title> - </Modal.Header> - <Modal.Body>{t("settings.dialogConfirmLogout.prompt")}</Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={onClose}> - {t("operationDialog.cancel")} - </Button> - <Button variant="danger" onClick={onConfirm}> - {t("operationDialog.confirm")} - </Button> - </Modal.Footer> - </Modal> - ); -}; - const SettingsPage: React.FC = (_) => { const { i18n, t } = useTranslation(); const user = useUser(); @@ -51,7 +25,7 @@ const SettingsPage: React.FC = (_) => { return ( <> - <Container> + <div className="container"> {user ? ( <Card className="my-3 py-3"> <h3 className="px-3 mb-3 text-primary"> @@ -89,16 +63,15 @@ const SettingsPage: React.FC = (_) => { <h3 className="px-3 mb-3 text-primary"> {t("settings.subheaders.customization")} </h3> - <Row className="settings-item first mx-0"> - <Col xs="12" sm="auto"> + <div className="row settings-item first mx-0"> + <div className="col col-12 col-sm-auto"> <div>{t("settings.languagePrimary")}</div> <small className="d-block text-secondary"> {t("settings.languageSecondary")} </small> - </Col> - <Col xs="auto" className="ms-auto"> - <Form.Control - as="select" + </div> + <div className="col col-12 col-sm-auto"> + <select value={language} onChange={(e) => { void i18n.changeLanguage(e.target.value); @@ -106,18 +79,20 @@ const SettingsPage: React.FC = (_) => { > <option value="zh">中文</option> <option value="en">English</option> - </Form.Control> - </Col> - </Row> + </select> + </div> + </div> </Card> - </Container> + </div> {(() => { switch (dialog) { case "changepassword": return <ChangePasswordDialog open close={() => setDialog(null)} />; case "logout": return ( - <ConfirmLogoutDialog + <ConfirmDialog + title="settings.dialogConfirmLogout.title" + body="settings.dialogConfirmLogout.prompt" onClose={() => setDialog(null)} onConfirm={() => { void userService.logout().then(() => { diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx index 005da933..b16bf43d 100644 --- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -1,15 +1,17 @@ +/* eslint-disable react/jsx-no-undef */ import React from "react"; import classnames from "classnames"; -import { Form, Spinner } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { Prompt } from "react-router"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; -import FlatButton from "../common/button/FlatButton"; -import TabPages from "../common/TabPages"; import TimelinePostBuilder from "@/services/TimelinePostBuilder"; -import ConfirmDialog from "../common/ConfirmDialog"; + +import FlatButton from "../common/button/FlatButton"; +import TabPages from "../common/tab/TabPages"; +import ConfirmDialog from "../common/dailog/ConfirmDialog"; +import Spinner from "../common/Spinner"; export interface MarkdownPostEditProps { timeline: string; @@ -102,7 +104,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ pageContainerClassName="py-2" actions={ process ? ( - <Spinner variant="primary" animation="border" size="sm" /> + <Spinner /> ) : ( <> <FlatButton @@ -126,8 +128,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ id: "text", tabText: "edit", page: ( - <Form.Control - as="textarea" + <textarea value={text} disabled={process} onChange={(event) => { @@ -161,7 +162,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ /> </div> ))} - <Form.Control + <input type="file" accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" onChange={(event: React.ChangeEvent<HTMLInputElement>) => { diff --git a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx index 001e52d7..988124b6 100644 --- a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; -import OperationDialog from "../common/OperationDialog"; +import OperationDialog from "../common/dailog/OperationDialog"; function PostPropertyChangeDialog(props: { onClose: () => void; @@ -14,7 +14,7 @@ function PostPropertyChangeDialog(props: { return ( <OperationDialog title="timeline.changePostPropertyDialog.title" - close={onClose} + onClose={onClose} open inputScheme={[ { diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx index 299d6a53..c8f26da7 100644 --- a/FrontEnd/src/views/timeline-common/TimelineMember.tsx +++ b/FrontEnd/src/views/timeline-common/TimelineMember.tsx @@ -1,49 +1,47 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -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 { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; +import Button from "../common/button/Button"; +import Dialog from "../common/dailog/Dialog"; const TimelineMemberItem: React.FC<{ user: HttpUser; add?: boolean; onAction?: (username: string) => void; }> = ({ user, add, onAction }) => { - const { t } = useTranslation(); - return ( - <ListGroup.Item className="container"> - <Row> - <Col xs="auto"> + <div className="container"> + <div className="row"> + <div className="col col-auto"> <UserAvatar username={user.username} className="avatar small" /> - </Col> - <Col> - <Row>{user.nickname}</Row> - <Row> + </div> + <div className="col"> + <div className="row">{user.nickname}</div> + <div className="row"> <small>{"@" + user.username}</small> - </Row> - </Col> + </div> + </div> {onAction ? ( - <Col xs="auto"> + <div className="col col-auto"> <Button - variant={add ? "success" : "danger"} + text={`timeline.member.${add ? "add" : "remove"}`} + color={add ? "success" : "danger"} onClick={() => { onAction(user.username); }} - > - {t(`timeline.member.${add ? "add" : "remove"}`)} - </Button> - </Col> + /> + </div> ) : null} - </Row> - </ListGroup.Item> + </div> + </div> ); }; @@ -110,7 +108,7 @@ const TimelineMemberUserSearch: React.FC<{ return <div>{t("timeline.member.noUserAvailableToAdd")}</div>; } else { return ( - <ListGroup className="mt-2"> + <div className="mt-2"> {users.map((user) => ( <TimelineMemberItem key={user.username} @@ -127,7 +125,7 @@ const TimelineMemberUserSearch: React.FC<{ }} /> ))} - </ListGroup> + </div> ); } } else if (userSearchState.type === "error") { @@ -152,8 +150,8 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { const members = [timeline.owner, ...timeline.members]; return ( - <Container className="px-4 py-3"> - <ListGroup> + <div className="container px-4 py-3"> + <div> {members.map((member, index) => ( <TimelineMemberItem key={member.username} @@ -169,11 +167,11 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { } /> ))} - </ListGroup> + </div> {timeline.manageable ? ( <TimelineMemberUserSearch timeline={timeline} onChange={onChange} /> ) : null} - </Container> + </div> ); }; @@ -188,8 +186,8 @@ export const TimelineMemberDialog: React.FC<TimelineMemberDialogProps> = ( props ) => { return ( - <Modal show centered onHide={props.onClose}> + <Dialog open onClose={props.onClose}> <TimelineMember {...props} /> - </Modal> + </Dialog> ); }; diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx index 851dfa55..78057be2 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx @@ -18,7 +18,7 @@ import { TimelineMemberDialog } from "./TimelineMember"; import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; import ConnectionStatusBadge from "./ConnectionStatusBadge"; import { MenuItems, PopupMenu } from "../common/Menu"; -import FullPage from "../common/FullPage"; +import FullPageDialog from "../common/dailog/FullPageDialog"; import Card from "../common/Card"; export interface TimelineCardTemplateProps extends TimelinePageCardProps { @@ -120,13 +120,13 @@ const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({ <CollapseButton collapse={collapse} onClick={toggleCollapse} /> </div> {isSmallScreen ? ( - <FullPage + <FullPageDialog onBack={toggleCollapse} show={!collapse} contentContainerClassName="p-2" > {content} - </FullPage> + </FullPageDialog> ) : ( <div style={{ display: collapse ? "none" : "block" }}>{content}</div> )} diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx index 6f032eae..ea6e8d40 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Container } from "react-bootstrap"; import { HubConnectionState } from "@microsoft/signalr"; import { HttpTimelineInfo } from "@/http/timeline"; @@ -75,7 +74,7 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => { connectionStatus={connectionStatus} /> ) : null} - <Container> + <div className="container"> <Timeline timelineName={timelineName} reloadKey={reloadKey} @@ -83,7 +82,7 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => { onTimelineLoaded={(t) => setTimeline(t)} onConnectionStateChanged={setConnectionStatus} /> - </Container> + </div> </> ); }; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx deleted file mode 100644 index b2c7a470..00000000 --- a/FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { Modal, Button } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; - -const TimelinePostDeleteConfirmDialog: React.FC<{ - onClose: () => void; - onConfirm: () => void; -}> = ({ onClose, onConfirm }) => { - const { t } = useTranslation(); - - return ( - <Modal onHide={onClose} show centered> - <Modal.Header> - <Modal.Title className="text-danger"> - {t("timeline.post.deleteDialog.title")} - </Modal.Title> - </Modal.Header> - <Modal.Body>{t("timeline.post.deleteDialog.prompt")}</Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={onClose}> - {t("operationDialog.cancel")} - </Button> - <Button - variant="danger" - onClick={() => { - onConfirm(); - onClose(); - }} - > - {t("operationDialog.confirm")} - </Button> - </Modal.Footer> - </Modal> - ); -}; - -export default TimelinePostDeleteConfirmDialog; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx index 14cd50d4..13aacb54 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx @@ -1,7 +1,6 @@ import React from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { Row, Col, Form } from "react-bootstrap"; import { UiLogicError } from "@/common"; @@ -16,7 +15,7 @@ import { pushAlert } from "@/services/alert"; import { base64 } from "@/http/common"; import BlobImage from "../common/BlobImage"; -import LoadingButton from "../common/LoadingButton"; +import LoadingButton from "../common/button/LoadingButton"; import { PopupMenu } from "../common/Menu"; import Card from "../common/Card"; import MarkdownPostEdit from "./MarkdownPostEdit"; @@ -36,8 +35,7 @@ const TimelinePostEditText: React.FC<TimelinePostEditTextProps> = (props) => { const { text, disabled, onChange, className, style } = props; return ( - <Form.Control - as="textarea" + <textarea value={text} disabled={disabled} onChange={(event) => { @@ -81,7 +79,7 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { return ( <> - <Form.Control + <input type="file" onChange={onInputChange} accept="image/*" @@ -212,8 +210,8 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { onPostError={onPostError} /> ) : ( - <Row> - <Col className="px-1 py-1"> + <div className="row"> + <div className="col px-1 py-1"> {(() => { if (kind === "text") { return ( @@ -239,8 +237,8 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { ); } })()} - </Col> - <Col xs="auto" className="align-self-end m-1"> + </div> + <div className="col col-auto align-self-end m-1"> <div className="d-block text-center mt-1 mb-2"> <PopupMenu items={(["text", "image", "markdown"] as const).map( @@ -267,15 +265,14 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { </PopupMenu> </div> <LoadingButton - variant="primary" onClick={onSend} disabled={!canSend} loading={process} > {t("timeline.send")} </LoadingButton> - </Col> - </Row> + </div> + </div> )} </Card> </div> diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index 7b16e898..995c43df 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -9,9 +9,9 @@ import { pushAlert } from "@/services/alert"; import UserAvatar from "../common/user/UserAvatar"; import Card from "../common/Card"; import FlatButton from "../common/button/FlatButton"; +import ConfirmDialog from "../common/dailog/ConfirmDialog"; import TimelineLine from "./TimelineLine"; import TimelinePostContentView from "./TimelinePostContentView"; -import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; import PostPropertyChangeDialog from "./PostPropertyChangeDialog"; export interface TimelinePostViewProps { @@ -116,7 +116,9 @@ const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => { ) : null} </Card> {dialog === "delete" ? ( - <TimelinePostDeleteConfirmDialog + <ConfirmDialog + title="timeline.post.deleteDialog.title" + body="timeline.post.deleteDialog.prompt" onClose={() => { setDialog(null); setOperationMaskVisible(false); diff --git a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx index 70f72025..64daa19b 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -8,7 +8,7 @@ import { TimelineVisibility, } from "@/http/timeline"; -import OperationDialog from "../common/OperationDialog"; +import OperationDialog from "../common/dailog/OperationDialog"; export interface TimelinePropertyChangeDialogProps { open: boolean; @@ -60,7 +60,7 @@ const TimelinePropertyChangeDialog: React.FC<TimelinePropertyChangeDialogProps> ] as const } open={props.open} - close={props.close} + onClose={props.close} onProcess={([newTitle, newVisibility, newDescription, newColor]) => { const req: HttpTimelinePatchRequest = {}; if (newTitle !== timeline.title) { diff --git a/FrontEnd/src/views/timeline-common/index.css b/FrontEnd/src/views/timeline-common/index.css index e38d0ba7..b78564a3 100644 --- a/FrontEnd/src/views/timeline-common/index.css +++ b/FrontEnd/src/views/timeline-common/index.css @@ -13,19 +13,19 @@ @keyframes timeline-line-node-noncurrent { to { - box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color); + box-shadow: 0 0 20px 3px var(--tl-primary-l1-color); } } @keyframes timeline-line-node-current { to { - box-shadow: 0 0 20px 3px var(--tl-primary-enhance-lighter-color); + box-shadow: 0 0 20px 3px var(--tl-primary-enhance-l1-color); } } @keyframes timeline-line-node-loading { to { - box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color); + box-shadow: 0 0 20px 3px var(--tl-primary-l1-color); } } diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx index dbca62ca..aedf4f29 100644 --- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx @@ -4,7 +4,7 @@ import { Trans } from "react-i18next"; import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; -import OperationDialog from "../common/OperationDialog"; +import OperationDialog from "../common/dailog/OperationDialog"; interface TimelineDeleteDialog { timeline: HttpTimelineInfo; @@ -20,7 +20,7 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => { return ( <OperationDialog open={props.open} - close={props.close} + onClose={props.close} title="timeline.deleteDialog.title" themeColor="danger" inputPrompt={() => { diff --git a/FrontEnd/src/views/user/UserCard.tsx b/FrontEnd/src/views/user/UserCard.tsx index e7e4252e..06bb2143 100644 --- a/FrontEnd/src/views/user/UserCard.tsx +++ b/FrontEnd/src/views/user/UserCard.tsx @@ -23,7 +23,7 @@ const UserCard: React.FC<TimelinePageCardProps> = (props) => { <div className="align-middle"> <UserAvatar username={timeline.owner.username} - className="avatar small rounded-circle me-3" + className="cru-avatar small cru-round me-3" /> {timeline.owner.nickname} </div> |