aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/views')
-rw-r--r--FrontEnd/src/views/about/index.tsx10
-rw-r--r--FrontEnd/src/views/admin/Admin.tsx5
-rw-r--r--FrontEnd/src/views/admin/AdminNav.tsx50
-rw-r--r--FrontEnd/src/views/admin/UserAdmin.tsx56
-rw-r--r--FrontEnd/src/views/center/CenterBoards.tsx25
-rw-r--r--FrontEnd/src/views/center/TimelineBoard.tsx36
-rw-r--r--FrontEnd/src/views/center/TimelineCreateDialog.tsx4
-rw-r--r--FrontEnd/src/views/center/index.tsx24
-rw-r--r--FrontEnd/src/views/common/AppBar.css95
-rw-r--r--FrontEnd/src/views/common/AppBar.tsx4
-rw-r--r--FrontEnd/src/views/common/Card.css2
-rw-r--r--FrontEnd/src/views/common/ConfirmDialog.tsx40
-rw-r--r--FrontEnd/src/views/common/ImageCropper.css38
-rw-r--r--FrontEnd/src/views/common/ImageCropper.tsx2
-rw-r--r--FrontEnd/src/views/common/LoadingButton.tsx29
-rw-r--r--FrontEnd/src/views/common/LoadingPage.tsx5
-rw-r--r--FrontEnd/src/views/common/SearchInput.css4
-rw-r--r--FrontEnd/src/views/common/SearchInput.tsx22
-rw-r--r--FrontEnd/src/views/common/Skeleton.css14
-rw-r--r--FrontEnd/src/views/common/Skeleton.tsx4
-rw-r--r--FrontEnd/src/views/common/Spinner.css13
-rw-r--r--FrontEnd/src/views/common/Spinner.tsx43
-rw-r--r--FrontEnd/src/views/common/ToggleIconButton.tsx30
-rw-r--r--FrontEnd/src/views/common/alert/AlertHost.tsx50
-rw-r--r--FrontEnd/src/views/common/alert/alert.css32
-rw-r--r--FrontEnd/src/views/common/alert/alert.sass15
-rw-r--r--FrontEnd/src/views/common/button/Button.css51
-rw-r--r--FrontEnd/src/views/common/button/Button.tsx33
-rw-r--r--FrontEnd/src/views/common/button/FlatButton.css38
-rw-r--r--FrontEnd/src/views/common/button/FlatButton.tsx31
-rw-r--r--FrontEnd/src/views/common/button/LoadingButton.tsx28
-rw-r--r--FrontEnd/src/views/common/button/TextButton.css36
-rw-r--r--FrontEnd/src/views/common/button/TextButton.tsx41
-rw-r--r--FrontEnd/src/views/common/button/common.ts35
-rw-r--r--FrontEnd/src/views/common/dailog/ConfirmDialog.tsx43
-rw-r--r--FrontEnd/src/views/common/dailog/Dialog.css35
-rw-r--r--FrontEnd/src/views/common/dailog/Dialog.tsx39
-rw-r--r--FrontEnd/src/views/common/dailog/FullPageDialog.css30
-rw-r--r--FrontEnd/src/views/common/dailog/FullPageDialog.tsx (renamed from FrontEnd/src/views/common/FullPage.tsx)17
-rw-r--r--FrontEnd/src/views/common/dailog/OperationDialog.css26
-rw-r--r--FrontEnd/src/views/common/dailog/OperationDialog.tsx (renamed from FrontEnd/src/views/common/OperationDialog.tsx)183
-rw-r--r--FrontEnd/src/views/common/index.css426
-rw-r--r--FrontEnd/src/views/common/menu/Menu.css24
-rw-r--r--FrontEnd/src/views/common/menu/Menu.tsx (renamed from FrontEnd/src/views/common/Menu.tsx)54
-rw-r--r--FrontEnd/src/views/common/menu/PopupMenu.css6
-rw-r--r--FrontEnd/src/views/common/menu/PopupMenu.tsx84
-rw-r--r--FrontEnd/src/views/common/tab/TabPages.tsx (renamed from FrontEnd/src/views/common/TabPages.tsx)49
-rw-r--r--FrontEnd/src/views/common/tab/Tabs.css31
-rw-r--r--FrontEnd/src/views/common/tab/Tabs.tsx62
-rw-r--r--FrontEnd/src/views/home/index.css6
-rw-r--r--FrontEnd/src/views/home/index.tsx2
-rw-r--r--FrontEnd/src/views/login/index.css7
-rw-r--r--FrontEnd/src/views/login/index.tsx141
-rw-r--r--FrontEnd/src/views/search/index.tsx9
-rw-r--r--FrontEnd/src/views/settings/ChangeAvatarDialog.tsx229
-rw-r--r--FrontEnd/src/views/settings/ChangeNicknameDialog.tsx4
-rw-r--r--FrontEnd/src/views/settings/ChangePasswordDialog.tsx4
-rw-r--r--FrontEnd/src/views/settings/index.tsx64
-rw-r--r--FrontEnd/src/views/timeline-common/CollapseButton.tsx2
-rw-r--r--FrontEnd/src/views/timeline-common/MarkdownPostEdit.css21
-rw-r--r--FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx34
-rw-r--r--FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx4
-rw-r--r--FrontEnd/src/views/timeline-common/TimelineMember.css8
-rw-r--r--FrontEnd/src/views/timeline-common/TimelineMember.tsx62
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx32
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx5
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx37
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePostEdit.css24
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx29
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePostView.tsx8
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx4
-rw-r--r--FrontEnd/src/views/timeline-common/index.css25
-rw-r--r--FrontEnd/src/views/timeline/TimelineCard.tsx14
-rw-r--r--FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx6
-rw-r--r--FrontEnd/src/views/user/UserCard.tsx10
-rw-r--r--FrontEnd/src/views/user/index.css9
76 files changed, 1716 insertions, 1138 deletions
diff --git a/FrontEnd/src/views/about/index.tsx b/FrontEnd/src/views/about/index.tsx
index 7a72d5ec..11618086 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="cru-color-primary">杨宇千</span>
</p>
<p>
<small>{t("about.author.nickname")}</small>
- <span className="text-primary">crupest</span>
+ <span className="cru-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="cru-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..4ceff8ab 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 TextButton from "../common/button/TextButton";
+import Button from "../common/button/Button";
+import Spinner from "../common/Spinner";
+import FlatButton from "../common/button/FlatButton";
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,25 +204,25 @@ 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"
+ className="bi-pencil-square float-end icon-button cru-color-warning"
onClick={() => setEditMaskVisible(true)}
/>
- <h4 className="text-primary">{user.username}</h4>
- <div className="text-secondary">
+ <h4 className="cru-color-primary">{user.username}</h4>
+ <div className="cru-color-secondary">
{t("admin:user.nickname")}
{user.nickname}
</div>
- <div className="text-secondary">
+ <div className="cru-color-secondary">
{t("admin:user.uniqueId")}
{user.uniqueId}
</div>
- <div className="text-secondary">
+ <div className="cru-color-secondary">
{t("admin:user.permissions")}
{user.permissions.map((permission) => {
return (
- <span key={permission} className="text-danger">
+ <span key={permission} className="cru-color-danger">
{permission}{" "}
</span>
);
@@ -231,18 +232,18 @@ const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
className={classnames("edit-mask", !editMaskVisible && "d-none")}
onClick={() => setEditMaskVisible(false)}
>
- <TextButton text="admin:user.modify" onClick={on[kModify]} />
- <TextButton
+ <FlatButton text="admin:user.modify" onClick={on[kModify]} />
+ <FlatButton
text="admin:user.modifyPermissions"
onClick={on[kModifyPermission]}
/>
- <TextButton
+ <FlatButton
text="admin:user.delete"
color="danger"
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..8c1f5fac 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;
@@ -48,16 +48,16 @@ const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({
<TimelineLogo className="icon" />
)}
<span className="title">{title}</span>
- <small className="ms-2 text-secondary">{name}</small>
+ <small className="ms-2 cru-color-secondary">{name}</small>
<span className="flex-grow-1"></span>
{actions != null ? (
<div className="right">
<i
- className="bi-trash icon-button text-danger px-2"
+ className="bi-trash icon-button cru-color-danger px-2"
onClick={actions.onDelete}
/>
<i
- className="bi-grip-vertical icon-button text-gray px-2 touch-action-none"
+ className="bi-grip-vertical icon-button px-2 touch-action-none"
onPointerDown={(e) => {
e.currentTarget.setPointerCapture(e.pointerId);
actions.onMove.start(e);
@@ -208,7 +208,8 @@ const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({
interface TimelineBoardUIProps {
title?: string;
- timelines: HttpTimelineInfo[] | "offline" | "loading";
+ state: "offline" | "loading" | "loaded";
+ timelines: HttpTimelineInfo[];
onReload: () => void;
className?: string;
editHandler?: {
@@ -218,7 +219,7 @@ interface TimelineBoardUIProps {
}
const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
- const { title, timelines, className, editHandler } = props;
+ const { title, state, timelines, className, editHandler } = props;
const editable = editHandler != null;
@@ -246,13 +247,13 @@ const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => {
))}
</div>
{(() => {
- if (timelines === "loading") {
+ if (state === "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") {
+ } else if (state === "offline") {
return (
<div className="d-flex flex-grow-1 justify-content-center align-items-center">
<LoadFailReload onReload={props.onReload} />
@@ -301,36 +302,39 @@ const TimelineBoard: React.FC<TimelineBoardProps> = ({
load,
editHandler,
}) => {
- const [timelines, setTimelines] = React.useState<
- HttpTimelineInfo[] | "offline" | "loading"
- >("loading");
+ const [state, setState] = React.useState<"offline" | "loading" | "loaded">(
+ "loading"
+ );
+ const [timelines, setTimelines] = React.useState<HttpTimelineInfo[]>([]);
React.useEffect(() => {
let subscribe = true;
- if (timelines === "loading") {
+ if (state === "loading") {
void load().then(
(timelines) => {
if (subscribe) {
+ setState("loaded");
setTimelines(timelines);
}
},
() => {
- setTimelines("offline");
+ setState("offline");
}
);
}
return () => {
subscribe = false;
};
- }, [load, timelines]);
+ }, [load, state]);
return (
<TimelineBoardUI
title={title}
className={className}
+ state={state}
timelines={timelines}
onReload={() => {
- setTimelines("loading");
+ setState("loaded");
}}
editHandler={
typeof timelines === "object" && editHandler != null
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..3ec4fa36
--- /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(--cru-primary-color);
+ transition: background-color 1s;
+}
+
+.app-bar .cru-avatar {
+ background-color: white;
+}
+
+.app-bar a {
+ color: var(--cru-primary-t1-color);
+ text-decoration: none;
+ margin: 0 1em;
+ transition: color 1s;
+}
+.app-bar a:hover {
+ color: var(--cru-primary-t-color);
+}
+.app-bar a.active {
+ color: var(--cru-primary-t-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(--cru-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(--cru-primary-t-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/Card.css b/FrontEnd/src/views/common/Card.css
index fb90bd59..6de0dd8e 100644
--- a/FrontEnd/src/views/common/Card.css
+++ b/FrontEnd/src/views/common/Card.css
@@ -11,5 +11,5 @@
}
.cru-card:hover {
- border-color: var(--tl-primary-color);
+ border-color: var(--cru-primary-color);
}
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/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/Skeleton.css b/FrontEnd/src/views/common/Skeleton.css
new file mode 100644
index 00000000..db1a1c34
--- /dev/null
+++ b/FrontEnd/src/views/common/Skeleton.css
@@ -0,0 +1,14 @@
+.cru-skeleton {
+ padding: 0 1em;
+}
+
+.cru-skeleton-line {
+ height: 1em;
+ background-color: #e6e6e6;
+ margin: 0.7em 0;
+ border-radius: 0.2em;
+}
+
+.cru-skeleton-line.last {
+ width: 50%;
+}
diff --git a/FrontEnd/src/views/common/Skeleton.tsx b/FrontEnd/src/views/common/Skeleton.tsx
index 14886c71..58d34215 100644
--- a/FrontEnd/src/views/common/Skeleton.tsx
+++ b/FrontEnd/src/views/common/Skeleton.tsx
@@ -1,6 +1,8 @@
import React from "react";
import classnames from "classnames";
-import { range } from "lodash";
+import range from "lodash/range";
+
+import "./Skeleton.css";
export interface SkeletonProps {
lineNumber?: number;
diff --git a/FrontEnd/src/views/common/Spinner.css b/FrontEnd/src/views/common/Spinner.css
new file mode 100644
index 00000000..a1de68d2
--- /dev/null
+++ b/FrontEnd/src/views/common/Spinner.css
@@ -0,0 +1,13 @@
+@keyframes cru-spinner-animation {
+ from {
+ transform: scale(0,0);
+ }
+}
+
+.cru-spinner {
+ display: inline-block;
+ animation: cru-spinner-animation 0.5s infinite alternate;
+ background-color: currentColor;
+ border-radius: 50%;
+ transform-origin: center;
+}
diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx
new file mode 100644
index 00000000..4c735fef
--- /dev/null
+++ b/FrontEnd/src/views/common/Spinner.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import classnames from "classnames";
+
+import { PaletteColorType } from "@/palette";
+
+import "./Spinner.css";
+
+export interface SpinnerProps {
+ size?: "sm" | "md" | "lg" | number | string;
+ color?: PaletteColorType;
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+export default function Spinner(
+ props: SpinnerProps
+): React.ReactElement | null {
+ const { size, color, className, style } = props;
+ const calculatedSize =
+ size === "sm"
+ ? "18px"
+ : size === "md"
+ ? "30px"
+ : size === "lg"
+ ? "42px"
+ : typeof size === "number"
+ ? size
+ : size == null
+ ? "20px"
+ : size;
+ const calculatedColor = color ?? "primary";
+
+ return (
+ <span
+ className={classnames(
+ "cru-spinner",
+ `cru-color-${calculatedColor}`,
+ className
+ )}
+ style={{ width: calculatedSize, height: calculatedSize, ...style }}
+ />
+ );
+}
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..ba6d6a0f 100644
--- a/FrontEnd/src/views/common/alert/AlertHost.tsx
+++ b/FrontEnd/src/views/common/alert/AlertHost.tsx
@@ -1,16 +1,13 @@
import React from "react";
import without from "lodash/without";
import { useTranslation } from "react-i18next";
-import { Alert } from "react-bootstrap";
+import classNames from "classnames";
-import {
- alertService,
- AlertInfoEx,
- kAlertHostId,
- AlertInfo,
-} from "@/services/alert";
+import { alertService, AlertInfoEx, AlertInfo } from "@/services/alert";
import { convertI18nText } from "@/common";
+import "./alert.css";
+
interface AutoCloseAlertProps {
alert: AlertInfo;
close: () => void;
@@ -52,29 +49,36 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => {
};
return (
- <Alert
- className="m-3"
- variant={alert.type ?? "primary"}
+ <div
+ className={classNames(
+ "m-3 cru-alert",
+ "cru-" + (alert.type ?? "primary")
+ )}
onClick={cancelTimer}
- onClose={close}
- dismissible
>
- {(() => {
- const { message } = alert;
- if (typeof message === "function") {
- const Message = message;
- return <Message />;
- } else return convertI18nText(message, t);
- })()}
- </Alert>
+ <div className="cru-alert-content">
+ {(() => {
+ const { message, customMessage } = alert;
+ if (customMessage != null) {
+ return customMessage;
+ } else {
+ return convertI18nText(message, t);
+ }
+ })()}
+ </div>
+ <div className="cru-alert-close-button-container">
+ <i
+ className={classNames("icon-button bi-x cru-alert-close-button")}
+ onClick={close}
+ />
+ </div>
+ </div>
);
};
const AlertHost: React.FC = () => {
const [alerts, setAlerts] = React.useState<AlertInfoEx[]>([]);
- // react guarantee that state setters are stable, so we don't need to add it to dependency list
-
React.useEffect(() => {
const consume = (alert: AlertInfoEx): void => {
setAlerts((old) => [...old, alert]);
@@ -87,7 +91,7 @@ const AlertHost: React.FC = () => {
}, []);
return (
- <div id={kAlertHostId} className="alert-container">
+ <div className="alert-container">
{alerts.map((alert) => {
return (
<AutoCloseAlert
diff --git a/FrontEnd/src/views/common/alert/alert.css b/FrontEnd/src/views/common/alert/alert.css
new file mode 100644
index 00000000..328f5f9b
--- /dev/null
+++ b/FrontEnd/src/views/common/alert/alert.css
@@ -0,0 +1,32 @@
+.alert-container {
+ position: fixed;
+ z-index: 1040;
+}
+
+.cru-alert {
+ border-radius: 5px;
+ border: var(--cru-theme-color) 1px solid;
+ color: var(--cru-theme-t-color);
+ background-color: var(--cru-theme-r1-color);
+
+ display: flex;
+ overflow: hidden;
+}
+
+.cru-alert-content {
+ padding: 0.5em 2em;
+}
+
+.cru-alert-close-button-container {
+ margin-left: auto;
+ width: 2em;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--cru-theme-t-color);
+}
+
+.cru-alert-close-button {
+ color: var(--cru-theme-color);
+}
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..c34176f6
--- /dev/null
+++ b/FrontEnd/src/views/common/button/Button.css
@@ -0,0 +1,51 @@
+.cru-button:not(.outline) {
+ color: var(--cru-theme-t-color);
+ cursor: pointer;
+ padding: 0.2em 0.5em;
+ border-radius: 0.2em;
+ border: none;
+ transition: all 0.5s;
+ background-color: var(--cru-theme-color);
+}
+
+.cru-button:not(.outline):hover {
+ background-color: var(--cru-theme-f1-color);
+}
+
+.cru-button:not(.outline):active {
+ background-color: var(--cru-theme-f2-color);
+}
+
+.cru-button:not(.outline):disabled {
+ background-color: var(--cru-disable-color);
+ cursor: auto;
+}
+
+.cru-button.outline {
+ color: var(--cru-theme-color);
+ border: var(--cru-theme-color) 1px solid;
+ cursor: pointer;
+ padding: 0.2em 0.5em;
+ border-radius: 0.2em;
+ transition: all 0.6s;
+ background-color: white;
+}
+
+.cru-button.outline:hover {
+ color: var(--cru-theme-f1-color);
+ border-color: var(--cru-theme-f1-color);
+ background-color: var(--cru-background-color);
+}
+
+.cru-button.outline:active {
+ color: var(--cru-theme-f2-color);
+ border-color: var(--cru-theme-f2-color);
+ background-color: var(--cru-background-1-color);
+}
+
+.cru-button.outline:disabled {
+ color: var(--cru-disable-color);
+ border-color: var(--cru-disable-color);
+ background-color: white;
+ cursor: auto;
+}
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..f0d33153 100644
--- a/FrontEnd/src/views/common/button/FlatButton.css
+++ b/FrontEnd/src/views/common/button/FlatButton.css
@@ -5,44 +5,14 @@
border: none;
background-color: transparent;
transition: all 0.6s;
-}
-
-.cru-flat-button:hover:not(.disabled) {
- background-color: #e9ecef;
+ color: var(--cru-theme-color);
}
.cru-flat-button.disabled {
+ color: var(--cru-theme-l1-color);
cursor: default;
}
-.cru-flat-button.primary {
- color: var(--tl-primary-color);
-}
-
-.cru-flat-button.primary.disabled {
- color: var(--tl-primary-lighter-color);
-}
-
-.cru-flat-button.secondary {
- color: var(--tl-secondary-color);
-}
-
-.cru-flat-button.secondary.disabled {
- color: var(--tl-secondary-lighter-color);
-}
-
-.cru-flat-button.success {
- color: var(--tl-success-color);
-}
-
-.cru-flat-button.success.disabled {
- color: var(--tl-success-lighter-color);
-}
-
-.cru-flat-button.danger {
- color: var(--tl-danger-color);
-}
-
-.cru-flat-button.danger.disabled {
- color: var(--tl-danger-ligher-color);
+.cru-flat-button:hover:not(.disabled) {
+ background-color: #e9ecef;
}
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..a7e34f91
--- /dev/null
+++ b/FrontEnd/src/views/common/button/LoadingButton.tsx
@@ -0,0 +1,28 @@
+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}
+ outline
+ disabled={disabled || loading}
+ {...otherProps}
+ >
+ {otherProps.children}
+ {loading ? (
+ <Spinner className="cru-align-text-bottom ms-1" 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
deleted file mode 100644
index dc5abaaa..00000000
--- a/FrontEnd/src/views/common/button/TextButton.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.cru-text-button {
- background: transparent;
- border: none;
-}
-
-.cru-text-button.primary {
- color: var(--tl-primary-color);
-}
-
-.cru-text-button.primary:hover {
- color: var(--tl-primary-lighter-color);
-}
-
-.cru-text-button.secondary {
- color: var(--tl-secondary-color);
-}
-
-.cru-text-button.secondary:hover {
- color: var(--tl-secondary-lighter-color);
-}
-
-.cru-text-button.success {
- color: var(--tl-success-color);
-}
-
-.cru-text-button.success:hover {
- color: var(--tl-success-lighter-color);
-}
-
-.cru-text-button.danger {
- color: var(--tl-danger-color);
-}
-
-.cru-text-button.danger:hover {
- color: var(--tl-danger-lighter-color);
-}
diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx
deleted file mode 100644
index 1a2bac94..00000000
--- a/FrontEnd/src/views/common/button/TextButton.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import classNames from "classnames";
-
-import { convertI18nText, I18nText } from "@/common";
-import { PaletteColorType } from "@/palette";
-
-import "./TextButton.css";
-
-function _TextButton(
- {
- text,
- color,
- onClick,
- className,
- style,
- }: {
- text: I18nText;
- color?: PaletteColorType;
- onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
- className?: string;
- style?: React.CSSProperties;
- },
- 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>
- );
-}
-
-const TextButton = React.forwardRef(_TextButton);
-export default TextButton;
diff --git a/FrontEnd/src/views/common/button/common.ts b/FrontEnd/src/views/common/button/common.ts
new file mode 100644
index 00000000..0d84bae0
--- /dev/null
+++ b/FrontEnd/src/views/common/button/common.ts
@@ -0,0 +1,35 @@
+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 != null ? "cru-" + color : "cru-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..c10b1cdb
--- /dev/null
+++ b/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx
@@ -0,0 +1,43 @@
+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="cru-color-danger">{convertI18nText(title, t)}</h3>
+ <hr />
+ <p>{convertI18nText(body, t)}</p>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <Button
+ text="operationDialog.cancel"
+ color="secondary"
+ outline
+ 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..22b420fc
--- /dev/null
+++ b/FrontEnd/src/views/common/dailog/Dialog.css
@@ -0,0 +1,35 @@
+.cru-dialog-overlay {
+ position: fixed;
+ z-index: 1040;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(255, 255, 255, 0.92);
+
+ display: flex;
+ padding: 2em 0;
+
+ overflow: auto;
+}
+
+.cru-dialog-container {
+ max-width: 100%;
+ min-width: 30vw;
+
+ margin: auto;
+
+ border: var(--cru-primary-color) 1px solid;
+ border-radius: 5px;
+ padding: 1.5em;
+ background-color: white;
+}
+
+.cru-dialog-bottom-area {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.cru-dialog-bottom-area > * {
+ margin: 0 0.5em;
+}
diff --git a/FrontEnd/src/views/common/dailog/Dialog.tsx b/FrontEnd/src/views/common/dailog/Dialog.tsx
new file mode 100644
index 00000000..ee58080f
--- /dev/null
+++ b/FrontEnd/src/views/common/dailog/Dialog.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import ReactDOM from "react-dom";
+
+import "./Dialog.css";
+
+export interface DialogProps {
+ onClose: () => void;
+ open?: boolean;
+ children?: React.ReactNode;
+ disableCloseOnClickOnOverlay?: boolean;
+}
+
+export default function Dialog(props: DialogProps): React.ReactElement | null {
+ const { open, onClose, children, disableCloseOnClickOnOverlay } = props;
+
+ return open
+ ? ReactDOM.createPortal(
+ <div
+ className="cru-dialog-overlay"
+ onClick={
+ disableCloseOnClickOnOverlay
+ ? undefined
+ : () => {
+ onClose();
+ }
+ }
+ >
+ <div
+ className="cru-dialog-container"
+ onClick={(e) => e.stopPropagation()}
+ >
+ {children}
+ </div>
+ </div>,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ document.getElementById("portal")!
+ )
+ : null;
+}
diff --git a/FrontEnd/src/views/common/dailog/FullPageDialog.css b/FrontEnd/src/views/common/dailog/FullPageDialog.css
new file mode 100644
index 00000000..a196981c
--- /dev/null
+++ b/FrontEnd/src/views/common/dailog/FullPageDialog.css
@@ -0,0 +1,30 @@
+.cru-full-page {
+ position: fixed;
+ z-index: 1030;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ background-color: white;
+ padding-top: 56px;
+}
+
+.cru-full-page-top-bar {
+ height: 56px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1;
+ background-color: var(--cru-primary-color);
+ display: flex;
+ align-items: center;
+}
+
+.cru-full-page-content-container {
+ overflow: scroll;
+}
+
+.cru-full-page-back-button {
+ color: var(--cru-primary-t-color);
+}
diff --git a/FrontEnd/src/views/common/FullPage.tsx b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx
index 1b59045a..2e77dbb0 100644
--- a/FrontEnd/src/views/common/FullPage.tsx
+++ b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx
@@ -1,26 +1,29 @@
import React from "react";
+import { createPortal } from "react-dom";
import classnames from "classnames";
-export interface FullPageProps {
+import "./FullPageDialog.css";
+
+export interface FullPageDialogProps {
show: boolean;
onBack: () => void;
contentContainerClassName?: string;
}
-const FullPage: React.FC<FullPageProps> = ({
+const FullPageDialog: React.FC<FullPageDialogProps> = ({
show,
onBack,
children,
contentContainerClassName,
}) => {
- return (
+ return createPortal(
<div
className="cru-full-page"
style={{ display: show ? undefined : "none" }}
>
<div className="cru-full-page-top-bar">
<i
- className="icon-button bi-arrow-left text-white ms-3"
+ className="icon-button bi-arrow-left ms-3 cru-full-page-back-button"
onClick={onBack}
/>
</div>
@@ -32,8 +35,10 @@ const FullPage: React.FC<FullPageProps> = ({
>
{children}
</div>
- </div>
+ </div>,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ document.getElementById("portal")!
);
};
-export default FullPage;
+export default FullPageDialog;
diff --git a/FrontEnd/src/views/common/dailog/OperationDialog.css b/FrontEnd/src/views/common/dailog/OperationDialog.css
new file mode 100644
index 00000000..26c3920b
--- /dev/null
+++ b/FrontEnd/src/views/common/dailog/OperationDialog.css
@@ -0,0 +1,26 @@
+.cru-operation-dialog-group {
+ display: block;
+ margin: 0.4em 0;
+}
+
+.cru-operation-dialog-label {
+ display: block;
+ font-size: 0.8em;
+ color: var(--cru-primary-color);
+}
+
+.cru-operation-dialog-inline-label {
+ margin-inline-start: 0.5em;
+}
+
+.cru-operation-dialog-error-text {
+ display: block;
+ font-size: 0.8em;
+ color: var(--cru-danger-color);
+}
+
+.cru-operation-dialog-helper-text {
+ display: block;
+ font-size: 0.8em;
+ color: var(--cru-primary-color);
+}
diff --git a/FrontEnd/src/views/common/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx
index ac4c51b9..6bc846dd 100644
--- a/FrontEnd/src/views/common/OperationDialog.tsx
+++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx
@@ -1,12 +1,18 @@
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";
+
+import "./OperationDialog.css";
+import classNames from "classnames";
interface DefaultErrorPromptProps {
error?: string;
@@ -15,13 +21,13 @@ interface DefaultErrorPromptProps {
const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => {
const { t } = useTranslation();
- let result = <p className="text-danger">{t("operationDialog.error")}</p>;
+ let result = <p className="cru-color-danger">{t("operationDialog.error")}</p>;
if (props.error != null) {
result = (
<>
{result}
- <p className="text-danger">{props.error}</p>
+ <p className="cru-color-danger">{props.error}</p>
</>
);
}
@@ -45,6 +51,7 @@ export interface OperationDialogBoolInput {
type: "bool";
label: I18nText;
initValue?: boolean;
+ helperText?: string;
}
export interface OperationDialogSelectInputOption {
@@ -71,6 +78,7 @@ export interface OperationDialogDateTimeInput {
type: "datetime";
label?: I18nText;
initValue?: string;
+ helperText?: string;
}
export type OperationDialogInput =
@@ -141,9 +149,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 +212,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 +286,7 @@ const OperationDialog = <
body = (
<>
- <Modal.Body>
+ <div>
{inputPrompt}
{inputScheme.map((item, index) => {
const value = values[index];
@@ -289,50 +297,84 @@ const OperationDialog = <
if (item.type === "text") {
return (
- <Form.Group key={index}>
+ <div
+ key={index}
+ className={classNames(
+ "cru-operation-dialog-group",
+ error != null ? "error" : null
+ )}
+ >
{item.label && (
- <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
+ <label className="cru-operation-dialog-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">
+ <div className="cru-operation-dialog-error-text">
{error}
- </Form.Control.Feedback>
+ </div>
)}
{item.helperText && (
- <Form.Text>{t(item.helperText)}</Form.Text>
+ <div className="cru-operation-dialog-helper-text">
+ {t(item.helperText)}
+ </div>
)}
- </Form.Group>
+ </div>
);
} else if (item.type === "bool") {
return (
- <Form.Group key={index}>
- <Form.Check<"input">
+ <div
+ key={index}
+ className={classNames(
+ "cru-operation-dialog-group",
+ error != null ? "error" : null
+ )}
+ >
+ <input
type="checkbox"
checked={value as boolean}
onChange={(event) => {
updateValue(index, event.currentTarget.checked);
}}
- label={convertI18nText(item.label, t)}
disabled={process}
/>
- </Form.Group>
+ <label className="cru-operation-dialog-inline-label">
+ {convertI18nText(item.label, t)}
+ </label>
+ {error != null && (
+ <div className="cru-operation-dialog-error-text">
+ {error}
+ </div>
+ )}
+ {item.helperText && (
+ <div className="cru-operation-dialog-helper-text">
+ {t(item.helperText)}
+ </div>
+ )}
+ </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}
+ className={classNames(
+ "cru-operation-dialog-group",
+ error != null ? "error" : null
+ )}
+ >
+ <label className="cru-operation-dialog-label">
+ {convertI18nText(item.label, t)}
+ </label>
+ <select
value={value as string}
onChange={(event) => {
updateValue(index, event.target.value);
@@ -347,14 +389,20 @@ const OperationDialog = <
</option>
);
})}
- </Form.Control>
- </Form.Group>
+ </select>
+ </div>
);
} else if (item.type === "color") {
return (
- <Form.Group key={index}>
+ <div
+ key={index}
+ className={classNames(
+ "cru-operation-dialog-group",
+ error != null ? "error" : null
+ )}
+ >
{item.canBeNull ? (
- <Form.Check<"input">
+ <input
type="checkbox"
checked={value !== null}
onChange={(event) => {
@@ -364,52 +412,61 @@ const OperationDialog = <
updateValue(index, null);
}
}}
- label={convertI18nText(item.label, t)}
disabled={process}
/>
- ) : (
- <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
- )}
+ ) : null}
+ <label className="cru-operation-dialog-inline-label">
+ {convertI18nText(item.label, t)}
+ </label>
{value !== null && (
<TwitterPicker
color={value as string}
+ triangle="hide"
onChange={(result) => updateValue(index, result.hex)}
/>
)}
- </Form.Group>
+ </div>
);
} else if (item.type === "datetime") {
return (
- <Form.Group key={index}>
+ <div
+ key={index}
+ className={classNames(
+ "cru-operation-dialog-group",
+ error != null ? "error" : null
+ )}
+ >
{item.label && (
- <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
+ <label className="cru-operation-dialog-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>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <Button
+ text="operationDialog.cancel"
+ color="secondary"
+ outline
+ onClick={close}
+ disabled={process}
+ />
<LoadingButton
- variant={props.themeColor}
+ color={props.themeColor}
loading={process}
disabled={!canProcess}
onClick={() => {
@@ -421,7 +478,7 @@ const OperationDialog = <
>
{t("operationDialog.confirm")}
</LoadingButton>
- </Modal.Footer>
+ </div>
</>
);
} else {
@@ -431,7 +488,7 @@ const OperationDialog = <
content =
props.successPrompt?.(result.data) ?? t("operationDialog.success");
if (typeof content === "string")
- content = <p className="text-success">{content}</p>;
+ content = <p className="cru-color-success">{content}</p>;
} else {
content = props.failurePrompt?.(result.data) ?? <DefaultErrorPrompt />;
if (typeof content === "string")
@@ -439,12 +496,11 @@ const OperationDialog = <
}
body = (
<>
- <Modal.Body>{content}</Modal.Body>
- <Modal.Footer>
- <Button variant="primary" onClick={close}>
- {t("operationDialog.ok")}
- </Button>
- </Modal.Footer>
+ <div>{content}</div>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <Button text="operationDialog.ok" color="primary" onClick={close} />
+ </div>
</>
);
}
@@ -455,16 +511,19 @@ 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
+ props.themeColor != null
+ ? "cru-color-" + props.themeColor
+ : "cru-color-primary"
}
>
{title}
- </Modal.Header>
+ </h3>
+ <hr />
{body}
- </Modal>
+ </Dialog>
);
};
diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css
index bfd82b58..a4ce8cf3 100644
--- a/FrontEnd/src/views/common/index.css
+++ b/FrontEnd/src/views/common/index.css
@@ -1,245 +1,272 @@
-.image-cropper-container {
- position: relative;
- box-sizing: border-box;
- user-select: none;
-}
+:root {
+ --cru-background-color: #f8f9fa;
+ --cru-background-1-color: #e9ecef;
+ --cru-background-2-color: #dee2e6;
-.image-cropper-container img {
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
-}
+ --cru-disable-color: #ced4da;
-.image-cropper-mask-container {
- position: absolute;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- overflow: hidden;
+ --cru-primary-color: rgb(0, 123, 255);
+ --cru-primary-l1-color: rgb(26, 136, 255);
+ --cru-primary-l2-color: rgb(51, 149, 255);
+ --cru-primary-l3-color: rgb(77, 163, 255);
+ --cru-primary-d1-color: rgb(0, 111, 230);
+ --cru-primary-d2-color: rgb(0, 98, 204);
+ --cru-primary-d3-color: rgb(0, 86, 179);
+ --cru-primary-f1-color: rgb(0, 111, 230);
+ --cru-primary-f2-color: rgb(0, 98, 204);
+ --cru-primary-f3-color: rgb(0, 86, 179);
+ --cru-primary-r1-color: rgb(26, 136, 255);
+ --cru-primary-r2-color: rgb(51, 149, 255);
+ --cru-primary-r3-color: rgb(77, 163, 255);
+ --cru-primary-t-color: rgb(255, 255, 255);
+ --cru-primary-t1-color: rgb(230, 230, 230);
+ --cru-primary-t2-color: rgb(204, 204, 204);
+ --cru-primary-t3-color: rgb(179, 179, 179);
+ --cru-primary-enhance-color: rgb(77, 163, 255);
+ --cru-primary-enhance-l1-color: rgb(94, 172, 255);
+ --cru-primary-enhance-l2-color: rgb(112, 181, 255);
+ --cru-primary-enhance-l3-color: rgb(130, 190, 255);
+ --cru-primary-enhance-d1-color: rgb(43, 145, 255);
+ --cru-primary-enhance-d2-color: rgb(10, 128, 255);
+ --cru-primary-enhance-d3-color: rgb(0, 112, 232);
+ --cru-primary-enhance-f1-color: rgb(94, 172, 255);
+ --cru-primary-enhance-f2-color: rgb(112, 181, 255);
+ --cru-primary-enhance-f3-color: rgb(130, 190, 255);
+ --cru-primary-enhance-r1-color: rgb(43, 145, 255);
+ --cru-primary-enhance-r2-color: rgb(10, 128, 255);
+ --cru-primary-enhance-r3-color: rgb(0, 112, 232);
+ --cru-primary-enhance-t-color: rgb(0, 0, 0);
+ --cru-primary-enhance-t1-color: rgb(26, 26, 26);
+ --cru-primary-enhance-t2-color: rgb(51, 51, 51);
+ --cru-primary-enhance-t3-color: rgb(77, 77, 77);
+ --cru-secondary-color: rgb(128, 128, 128);
+ --cru-secondary-l1-color: rgb(141, 141, 141);
+ --cru-secondary-l2-color: rgb(153, 153, 153);
+ --cru-secondary-l3-color: rgb(166, 166, 166);
+ --cru-secondary-d1-color: rgb(115, 115, 115);
+ --cru-secondary-d2-color: rgb(102, 102, 102);
+ --cru-secondary-d3-color: rgb(90, 90, 90);
+ --cru-secondary-f1-color: rgb(115, 115, 115);
+ --cru-secondary-f2-color: rgb(102, 102, 102);
+ --cru-secondary-f3-color: rgb(90, 90, 90);
+ --cru-secondary-r1-color: rgb(141, 141, 141);
+ --cru-secondary-r2-color: rgb(153, 153, 153);
+ --cru-secondary-r3-color: rgb(166, 166, 166);
+ --cru-secondary-t-color: rgb(255, 255, 255);
+ --cru-secondary-t1-color: rgb(230, 230, 230);
+ --cru-secondary-t2-color: rgb(204, 204, 204);
+ --cru-secondary-t3-color: rgb(179, 179, 179);
+ --cru-danger-color: rgb(255, 0, 0);
+ --cru-danger-l1-color: rgb(255, 26, 26);
+ --cru-danger-l2-color: rgb(255, 51, 51);
+ --cru-danger-l3-color: rgb(255, 77, 77);
+ --cru-danger-d1-color: rgb(230, 0, 0);
+ --cru-danger-d2-color: rgb(204, 0, 0);
+ --cru-danger-d3-color: rgb(179, 0, 0);
+ --cru-danger-f1-color: rgb(230, 0, 0);
+ --cru-danger-f2-color: rgb(204, 0, 0);
+ --cru-danger-f3-color: rgb(179, 0, 0);
+ --cru-danger-r1-color: rgb(255, 26, 26);
+ --cru-danger-r2-color: rgb(255, 51, 51);
+ --cru-danger-r3-color: rgb(255, 77, 77);
+ --cru-danger-t-color: rgb(255, 255, 255);
+ --cru-danger-t1-color: rgb(230, 230, 230);
+ --cru-danger-t2-color: rgb(204, 204, 204);
+ --cru-danger-t3-color: rgb(179, 179, 179);
+ --cru-success-color: rgb(0, 128, 0);
+ --cru-success-l1-color: rgb(0, 166, 0);
+ --cru-success-l2-color: rgb(0, 204, 0);
+ --cru-success-l3-color: rgb(0, 243, 0);
+ --cru-success-d1-color: rgb(0, 115, 0);
+ --cru-success-d2-color: rgb(0, 102, 0);
+ --cru-success-d3-color: rgb(0, 90, 0);
+ --cru-success-f1-color: rgb(0, 115, 0);
+ --cru-success-f2-color: rgb(0, 102, 0);
+ --cru-success-f3-color: rgb(0, 90, 0);
+ --cru-success-r1-color: rgb(0, 166, 0);
+ --cru-success-r2-color: rgb(0, 204, 0);
+ --cru-success-r3-color: rgb(0, 243, 0);
+ --cru-success-t-color: rgb(255, 255, 255);
+ --cru-success-t1-color: rgb(230, 230, 230);
+ --cru-success-t2-color: rgb(204, 204, 204);
+ --cru-success-t3-color: rgb(179, 179, 179);
}
-.image-cropper-mask {
- position: absolute;
- box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8);
- touch-action: none;
+.cru-primary {
+ --cru-theme-color: var(--cru-primary-color);
+ --cru-theme-l1-color: var(--cru-primary-l1-color);
+ --cru-theme-l2-color: var(--cru-primary-l2-color);
+ --cru-theme-l3-color: var(--cru-primary-l3-color);
+ --cru-theme-d1-color: var(--cru-primary-d1-color);
+ --cru-theme-d2-color: var(--cru-primary-d2-color);
+ --cru-theme-d3-color: var(--cru-primary-d3-color);
+ --cru-theme-f1-color: var(--cru-primary-f1-color);
+ --cru-theme-f2-color: var(--cru-primary-f2-color);
+ --cru-theme-f3-color: var(--cru-primary-f3-color);
+ --cru-theme-r1-color: var(--cru-primary-r1-color);
+ --cru-theme-r2-color: var(--cru-primary-r2-color);
+ --cru-theme-r3-color: var(--cru-primary-r3-color);
+ --cru-theme-t-color: var(--cru-primary-t-color);
+ --cru-theme-t1-color: var(--cru-primary-t1-color);
+ --cru-theme-t2-color: var(--cru-primary-t2-color);
+ --cru-theme-t3-color: var(--cru-primary-t3-color);
}
-.image-cropper-handler {
- position: absolute;
- width: 26px;
- height: 26px;
- border: black solid 2px;
- border-radius: 50%;
- background: white;
- touch-action: none;
+.cru-primary-enhance {
+ --cru-theme-color: var(--cru-primary-enhance-color);
+ --cru-theme-l1-color: var(--cru-primary-enhance-l1-color);
+ --cru-theme-l2-color: var(--cru-primary-enhance-l2-color);
+ --cru-theme-l3-color: var(--cru-primary-enhance-l3-color);
+ --cru-theme-d1-color: var(--cru-primary-enhance-d1-color);
+ --cru-theme-d2-color: var(--cru-primary-enhance-d2-color);
+ --cru-theme-d3-color: var(--cru-primary-enhance-d3-color);
+ --cru-theme-f1-color: var(--cru-primary-enhance-f1-color);
+ --cru-theme-f2-color: var(--cru-primary-enhance-f2-color);
+ --cru-theme-f3-color: var(--cru-primary-enhance-f3-color);
+ --cru-theme-r1-color: var(--cru-primary-enhance-r1-color);
+ --cru-theme-r2-color: var(--cru-primary-enhance-r2-color);
+ --cru-theme-r3-color: var(--cru-primary-enhance-r3-color);
+ --cru-theme-t-color: var(--cru-primary-enhance-t-color);
+ --cru-theme-t1-color: var(--cru-primary-enhance-t1-color);
+ --cru-theme-t2-color: var(--cru-primary-enhance-t2-color);
+ --cru-theme-t3-color: var(--cru-primary-enhance-t3-color);
}
-.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);
+.cru-secondary {
+ --cru-theme-color: var(--cru-secondary-color);
+ --cru-theme-l1-color: var(--cru-secondary-l1-color);
+ --cru-theme-l2-color: var(--cru-secondary-l2-color);
+ --cru-theme-l3-color: var(--cru-secondary-l3-color);
+ --cru-theme-d1-color: var(--cru-secondary-d1-color);
+ --cru-theme-d2-color: var(--cru-secondary-d2-color);
+ --cru-theme-d3-color: var(--cru-secondary-d3-color);
+ --cru-theme-f1-color: var(--cru-secondary-f1-color);
+ --cru-theme-f2-color: var(--cru-secondary-f2-color);
+ --cru-theme-f3-color: var(--cru-secondary-f3-color);
+ --cru-theme-r1-color: var(--cru-secondary-r1-color);
+ --cru-theme-r2-color: var(--cru-secondary-r2-color);
+ --cru-theme-r3-color: var(--cru-secondary-r3-color);
+ --cru-theme-t-color: var(--cru-secondary-t-color);
+ --cru-theme-t1-color: var(--cru-secondary-t1-color);
+ --cru-theme-t2-color: var(--cru-secondary-t2-color);
+ --cru-theme-t3-color: var(--cru-secondary-t3-color);
}
-.app-bar-brand {
- display: flex;
- align-items: center;
+.cru-success {
+ --cru-theme-color: var(--cru-success-color);
+ --cru-theme-l1-color: var(--cru-success-l1-color);
+ --cru-theme-l2-color: var(--cru-success-l2-color);
+ --cru-theme-l3-color: var(--cru-success-l3-color);
+ --cru-theme-d1-color: var(--cru-success-d1-color);
+ --cru-theme-d2-color: var(--cru-success-d2-color);
+ --cru-theme-d3-color: var(--cru-success-d3-color);
+ --cru-theme-f1-color: var(--cru-success-f1-color);
+ --cru-theme-f2-color: var(--cru-success-f2-color);
+ --cru-theme-f3-color: var(--cru-success-f3-color);
+ --cru-theme-r1-color: var(--cru-success-r1-color);
+ --cru-theme-r2-color: var(--cru-success-r2-color);
+ --cru-theme-r3-color: var(--cru-success-r3-color);
+ --cru-theme-t-color: var(--cru-success-t-color);
+ --cru-theme-t1-color: var(--cru-success-t1-color);
+ --cru-theme-t2-color: var(--cru-success-t2-color);
+ --cru-theme-t3-color: var(--cru-success-t3-color);
}
-.app-bar-brand-icon {
- height: 2em;
+.cru-danger {
+ --cru-theme-color: var(--cru-danger-color);
+ --cru-theme-l1-color: var(--cru-danger-l1-color);
+ --cru-theme-l2-color: var(--cru-danger-l2-color);
+ --cru-theme-l3-color: var(--cru-danger-l3-color);
+ --cru-theme-d1-color: var(--cru-danger-d1-color);
+ --cru-theme-d2-color: var(--cru-danger-d2-color);
+ --cru-theme-d3-color: var(--cru-danger-d3-color);
+ --cru-theme-f1-color: var(--cru-danger-f1-color);
+ --cru-theme-f2-color: var(--cru-danger-f2-color);
+ --cru-theme-f3-color: var(--cru-danger-f3-color);
+ --cru-theme-r1-color: var(--cru-danger-r1-color);
+ --cru-theme-r2-color: var(--cru-danger-r2-color);
+ --cru-theme-r3-color: var(--cru-danger-r3-color);
+ --cru-theme-t-color: var(--cru-danger-t-color);
+ --cru-theme-t1-color: var(--cru-danger-t1-color);
+ --cru-theme-t2-color: var(--cru-danger-t2-color);
+ --cru-theme-t3-color: var(--cru-danger-t3-color);
}
-.app-bar-main-area {
- display: flex;
- flex-grow: 1;
+.cru-color-primary {
+ color: var(--cru-primary-color);
}
-.app-bar-link-area {
- display: flex;
- align-items: center;
- flex-shrink: 0;
+.cru-color-secondary {
+ color: var(--cru-secondary-color);
}
-.app-bar-user-area {
- display: flex;
- align-items: center;
- flex-shrink: 0;
- margin-left: auto;
+.cru-color-success {
+ color: var(--cru-success-color);
}
-.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;
+.cru-color-danger {
+ color: var(--cru-danger-color);
}
-.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-text-center {
+ text-align: center;
}
-.cru-skeleton {
- padding: 0 1em;
+.cru-text-end {
+ text-align: end;
}
-.cru-skeleton-line {
- height: 1em;
- background-color: #e6e6e6;
- margin: 0.7em 0;
- border-radius: 0.2em;
-}
-.cru-skeleton-line.last {
- width: 50%;
+.cru-float-right {
+ float: right;
}
-.cru-full-page {
- position: fixed;
- z-index: 1031;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- background-color: white;
- padding-top: 56px;
+.cru-align-text-bottom {
+ vertical-align: text-bottom;
}
-.cru-full-page-top-bar {
- height: 56px;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- z-index: 1;
- background-color: var(--tl-primary-color);
- display: flex;
- align-items: center;
+.cru-align-middle {
+ vertical-align: middle;
}
-.cru-full-page-content-container {
- overflow: scroll;
+.cru-clearfix::after {
+ clear: both;
}
-.cru-menu {
- min-width: 200px;
+.cru-fill-parent {
+ width: 100%;
+ height: 100%;
}
-.cru-menu-item {
- font-size: 1.2em;
- padding: 0.5em 1.5em;
+.icon-button {
+ font-size: 1.4rem;
cursor: pointer;
}
-.cru-menu-item.color-primary {
- color: #0d6efd;
-}
-.cru-menu-item.color-primary:hover {
- color: white;
- background-color: #0d6efd;
-}
-.cru-menu-item.color-secondary {
- color: #6c757d;
-}
-.cru-menu-item.color-secondary:hover {
- color: white;
- background-color: #6c757d;
-}
-.cru-menu-item.color-success {
- color: #198754;
-}
-.cru-menu-item.color-success:hover {
- color: white;
- background-color: #198754;
-}
-.cru-menu-item.color-info {
- color: #0dcaf0;
-}
-.cru-menu-item.color-info:hover {
- color: white;
- background-color: #0dcaf0;
-}
-.cru-menu-item.color-warning {
- color: #ffc107;
-}
-.cru-menu-item.color-warning:hover {
- color: white;
- background-color: #ffc107;
-}
-.cru-menu-item.color-danger {
- color: #dc3545;
-}
-.cru-menu-item.color-danger:hover {
- color: white;
- background-color: #dc3545;
-}
-.cru-menu-item.color-light {
- color: #f8f9fa;
+
+.icon-button.large {
+ font-size: 1.6rem;
}
-.cru-menu-item.color-light:hover {
- color: white;
- background-color: #f8f9fa;
+
+.icon-button.primary-enhance {
+ color: var(--cru-primary-enhance-color);
}
-.cru-menu-item.color-dark {
- color: #212529;
+
+.cru-avatar {
+ width: 60px;
+ height: 60px;
}
-.cru-menu-item.color-dark:hover {
- color: white;
- background-color: #212529;
+
+.cru-avatar.large {
+ width: 100px;
+ height: 100px;
}
-.cru-menu-item-icon {
- margin-right: 1em;
+.cru-avatar.small {
+ width: 40px;
+ height: 40px;
}
-.cru-menu-divider {
- border-top: 1px solid #e9ecef;
+.cru-round {
+ border-radius: 50%;
}
.cru-tab-pages-action-area {
@@ -247,11 +274,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/menu/Menu.css b/FrontEnd/src/views/common/menu/Menu.css
new file mode 100644
index 00000000..c3fa82c4
--- /dev/null
+++ b/FrontEnd/src/views/common/menu/Menu.css
@@ -0,0 +1,24 @@
+.cru-menu {
+ min-width: 200px;
+}
+
+.cru-menu-item {
+ font-size: 1em;
+ padding: 0.5em 1.5em;
+ cursor: pointer;
+ transition: all 0.5s;
+ color: var(--cru-theme-color);
+}
+
+.cru-menu-item:hover {
+ color: var(--cru-theme-t-color);
+ background-color: var(--cru-theme-color);
+}
+
+.cru-menu-item-icon {
+ margin-right: 1em;
+}
+
+.cru-menu-divider {
+ border-top: 1px solid #e9ecef;
+}
diff --git a/FrontEnd/src/views/common/Menu.tsx b/FrontEnd/src/views/common/menu/Menu.tsx
index ae73a331..d2f65391 100644
--- a/FrontEnd/src/views/common/Menu.tsx
+++ b/FrontEnd/src/views/common/menu/Menu.tsx
@@ -1,9 +1,11 @@
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";
+
+import "./Menu.css";
export type MenuItem =
| {
@@ -13,23 +15,29 @@ export type MenuItem =
type: "button";
text: I18nText;
iconClassName?: string;
- color?: BootstrapThemeColor;
+ color?: PaletteColorType;
onClick: () => void;
};
export type MenuItems = MenuItem[];
-export interface MenuProps {
+export type MenuProps = {
items: MenuItems;
- className?: string;
onItemClicked?: () => void;
-}
+ className?: string;
+ style?: React.CSSProperties;
+};
-const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => {
+export default function _Menu({
+ items,
+ onItemClicked,
+ className,
+ style,
+}: MenuProps): React.ReactElement | null {
const { t } = useTranslation();
return (
- <div className={classnames("cru-menu", className)}>
+ <div className={classnames("cru-menu", className)} style={style}>
{items.map((item, index) => {
if (item.type === "divider") {
return <div key={index} className="cru-menu-divider" />;
@@ -39,7 +47,7 @@ const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => {
key={index}
className={classnames(
"cru-menu-item",
- `color-${item.color ?? "primary"}`
+ `cru-${item.color ?? "primary"}`
)}
onClick={() => {
item.onClick();
@@ -61,32 +69,4 @@ const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => {
})}
</div>
);
-};
-
-export default Menu;
-
-export interface PopupMenuProps {
- items: MenuItems;
- children: OverlayTriggerProps["children"];
}
-
-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>
- );
-};
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.css b/FrontEnd/src/views/common/menu/PopupMenu.css
new file mode 100644
index 00000000..f6654f68
--- /dev/null
+++ b/FrontEnd/src/views/common/menu/PopupMenu.css
@@ -0,0 +1,6 @@
+.cru-popup-menu-menu-container {
+ z-index: 1040;
+ border-radius: 5px;
+ border: var(--cru-primary-color) 1px solid;
+ background-color: white;
+}
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.tsx b/FrontEnd/src/views/common/menu/PopupMenu.tsx
new file mode 100644
index 00000000..d7b81f49
--- /dev/null
+++ b/FrontEnd/src/views/common/menu/PopupMenu.tsx
@@ -0,0 +1,84 @@
+import classNames from "classnames";
+import React from "react";
+import { createPortal } from "react-dom";
+import { usePopper } from "react-popper";
+
+import Menu, { MenuItems } from "./Menu";
+
+import "./PopupMenu.css";
+
+export interface PopupMenuProps {
+ items: MenuItems;
+ children?: React.ReactNode;
+ containerClassName?: string;
+ containerStyle?: React.CSSProperties;
+}
+
+const PopupMenu: React.FC<PopupMenuProps> = ({
+ items,
+ children,
+ containerClassName,
+ containerStyle,
+}) => {
+ const [show, setShow] = React.useState<boolean>(false);
+
+ const [referenceElement, setReferenceElement] =
+ React.useState<HTMLDivElement | null>(null);
+ const [popperElement, setPopperElement] =
+ React.useState<HTMLDivElement | null>(null);
+ const { styles, attributes } = usePopper(referenceElement, popperElement);
+
+ React.useEffect(() => {
+ const handler = (event: MouseEvent): void => {
+ let element: HTMLElement | null = event.target as HTMLElement;
+ while (element) {
+ if (element == referenceElement || element == popperElement) {
+ return;
+ }
+ element = element.parentElement;
+ }
+ setShow(false);
+ };
+ document.addEventListener("click", handler);
+ return () => {
+ document.removeEventListener("click", handler);
+ };
+ }, [referenceElement, popperElement]);
+
+ return (
+ <>
+ <div
+ ref={setReferenceElement}
+ className={classNames(
+ "cru-popup-menu-trigger-container",
+ containerClassName
+ )}
+ style={containerStyle}
+ onClick={() => setShow(true)}
+ >
+ {children}
+ </div>
+ {show
+ ? createPortal(
+ <div
+ ref={setPopperElement}
+ className="cru-popup-menu-menu-container"
+ style={styles.popper}
+ {...attributes.popper}
+ >
+ <Menu
+ items={items}
+ onItemClicked={() => {
+ setShow(false);
+ }}
+ />
+ </div>,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ document.getElementById("portal")!
+ )
+ : null}
+ </>
+ );
+};
+
+export default PopupMenu;
diff --git a/FrontEnd/src/views/common/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx
index 2b1d91cb..677f558a 100644
--- a/FrontEnd/src/views/common/TabPages.tsx
+++ b/FrontEnd/src/views/common/tab/TabPages.tsx
@@ -1,18 +1,19 @@
import React from "react";
-import { Nav } from "react-bootstrap";
-import { useTranslation } from "react-i18next";
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
+import { I18nText, UiLogicError } from "@/common";
+
+import Tabs from "./Tabs";
export interface TabPage {
- id: string;
- tabText: I18nText;
+ name: string;
+ text: I18nText;
page: React.ReactNode;
}
export interface TabPagesProps {
pages: TabPage[];
actions?: React.ReactNode;
+ dense?: boolean;
className?: string;
style?: React.CSSProperties;
navClassName?: string;
@@ -24,6 +25,7 @@ export interface TabPagesProps {
const TabPages: React.FC<TabPagesProps> = ({
pages,
actions,
+ dense,
className,
style,
navClassName,
@@ -35,11 +37,9 @@ const TabPages: React.FC<TabPagesProps> = ({
throw new UiLogicError("Page list can't be empty.");
}
- const { t } = useTranslation();
-
- const [tab, setTab] = React.useState<string>(pages[0].id);
+ const [tab, setTab] = React.useState<string>(pages[0].name);
- const currentPage = pages.find((p) => p.id === tab);
+ const currentPage = pages.find((p) => p.name === tab);
if (currentPage == null) {
throw new UiLogicError("Current tab value is bad.");
@@ -47,23 +47,20 @@ 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>
+ <Tabs
+ tabs={pages.map((page) => ({
+ name: page.name,
+ text: page.text,
+ onClick: () => {
+ setTab(page.name);
+ },
+ }))}
+ dense={dense}
+ activeTabName={tab}
+ className={navClassName}
+ style={navStyle}
+ actions={actions}
+ />
<div className={pageContainerClassName} style={pageContainerStyle}>
{currentPage.page}
</div>
diff --git a/FrontEnd/src/views/common/tab/Tabs.css b/FrontEnd/src/views/common/tab/Tabs.css
new file mode 100644
index 00000000..53505a3c
--- /dev/null
+++ b/FrontEnd/src/views/common/tab/Tabs.css
@@ -0,0 +1,31 @@
+.cru-nav {
+ border-bottom: var(--cru-background-2-color) 1px solid;
+ display: flex;
+}
+
+.cru-nav-item {
+ color: var(--cru-primary-color);
+ border: var(--cru-background-2-color) 0.5px solid;
+ border-bottom: none;
+ padding: 0.5em 1.5em;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ transition: all 0.5s;
+ cursor: pointer;
+}
+
+.cru-nav.dense .cru-nav-item {
+ padding: 0.2em 1em;
+}
+
+.cru-nav-item:hover {
+ background-color: var(--cru-background-1-color);
+}
+
+.cru-nav-item:not(.active) {
+ color: var(--cru-primary-r2-color);
+}
+
+.cru-nav-action-area {
+ margin-left: auto;
+}
diff --git a/FrontEnd/src/views/common/tab/Tabs.tsx b/FrontEnd/src/views/common/tab/Tabs.tsx
new file mode 100644
index 00000000..701b4073
--- /dev/null
+++ b/FrontEnd/src/views/common/tab/Tabs.tsx
@@ -0,0 +1,62 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import classnames from "classnames";
+
+import { convertI18nText, I18nText } from "@/common";
+
+import "./Tabs.css";
+
+export interface Tab {
+ name: string;
+ text: I18nText;
+ link?: string;
+ onClick?: () => void;
+}
+
+export interface TabsProps {
+ activeTabName?: string;
+ actions?: React.ReactNode;
+ dense?: boolean;
+ tabs: Tab[];
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+export default function Tabs(props: TabsProps): React.ReactElement | null {
+ const { tabs, activeTabName, className, style, dense, actions } = props;
+
+ const { t } = useTranslation();
+
+ return (
+ <div
+ className={classnames("cru-nav", dense && "dense", className)}
+ style={style}
+ >
+ {tabs.map((tab) => {
+ const active = activeTabName === tab.name;
+ const className = classnames("cru-nav-item", active && "active");
+
+ if (tab.link != null) {
+ return (
+ <Link
+ key={tab.name}
+ to={tab.link}
+ onClick={tab.onClick}
+ className={className}
+ >
+ {convertI18nText(tab.text, t)}
+ </Link>
+ );
+ } else {
+ return (
+ <span key={tab.name} onClick={tab.onClick} className={className}>
+ {convertI18nText(tab.text, t)}
+ </span>
+ );
+ }
+ })}
+ <div className="cru-nav-action-area">{actions}</div>
+ </div>
+ );
+}
diff --git a/FrontEnd/src/views/home/index.css b/FrontEnd/src/views/home/index.css
index 516aba52..098bb017 100644
--- a/FrontEnd/src/views/home/index.css
+++ b/FrontEnd/src/views/home/index.css
@@ -71,3 +71,9 @@
.home-timeline-list-loading-head {
animation: 1s infinite home-timeline-list-loading-head-animation;
}
+
+@media (min-width: 576px) {
+ .home-search {
+ float: right;
+ }
+}
diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx
index ddb72e76..2e23654e 100644
--- a/FrontEnd/src/views/home/index.tsx
+++ b/FrontEnd/src/views/home/index.tsx
@@ -56,7 +56,7 @@ const HomeV2: React.FC = () => {
return (
<>
<SearchInput
- className="mx-2 my-3 float-sm-end"
+ className="mx-2 my-3 home-search"
value={navText}
onChange={setNavText}
onButtonClick={() => {
diff --git a/FrontEnd/src/views/login/index.css b/FrontEnd/src/views/login/index.css
index dca7054d..aefe57e8 100644
--- a/FrontEnd/src/views/login/index.css
+++ b/FrontEnd/src/views/login/index.css
@@ -1,3 +1,8 @@
.login-container {
- max-width: 600px;
+ max-width: 25em;
+}
+
+.login-container input[type="text"],
+.login-container input[type="password"] {
+ width: 100%;
}
diff --git a/FrontEnd/src/views/login/index.tsx b/FrontEnd/src/views/login/index.tsx
index a39a9972..ed696437 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,76 @@ const LoginPage: React.FC = (_) => {
};
return (
- <Container fluid className="login-container 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 className="login-container container-fluid mt-2">
+ <h1 className="cru-text-center cru-color-primary">{t("welcome")}</h1>
+ <div className="cru-operation-dialog-group">
+ <label className="cru-operation-dialog-label" htmlFor="username">
+ {t("user.username")}
+ </label>
+ <input
+ id="username"
+ type="text"
+ disabled={process}
+ onChange={(e) => {
+ setUsername(e.target.value);
+ setUsernameDirty(true);
+ }}
+ value={username}
+ />
+ {usernameDirty && username === "" && (
+ <div className="cru-operation-dialog-error-text">
+ {t("login.emptyUsername")}
+ </div>
+ )}
+ </div>
+ <div className="cru-operation-dialog-group">
+ <label className="cru-operation-dialog-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 className="cru-operation-dialog-error-text">
+ {t("login.emptyPassword")}
+ </div>
+ )}
+ </div>
+ <div className="cru-operation-dialog-group">
+ <input
+ id="remember-me"
+ type="checkbox"
+ checked={rememberMe}
+ onChange={(e) => {
+ setRememberMe(e.currentTarget.checked);
+ }}
+ />
+ <label className="cru-operation-dialog-inline-label">
+ {t("user.rememberMe")}
+ </label>
+ </div>
+ {error ? <p className="text-danger">{t(error)}</p> : null}
+ <div className="cru-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..c33687df 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,49 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
throw new UiLogicError();
}
return (
- <Row className="justify-content-center">
- <img
- className="change-avatar-img"
- src={resultUrl}
- alt={t("settings.dialogChangeAvatar.previewImgAlt")}
- />
- </Row>
+ <div className="row justify-content-center">
+ <div className="col col-auto">
+ <img
+ className="change-avatar-img"
+ src={resultUrl}
+ alt={t("settings.dialogChangeAvatar.previewImgAlt")}
+ />
+ </div>
+ </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 className="cru-color-primary">
+ {t("settings.dialogChangeAvatar.title")}
+ </h3>
+ <hr />
{(() => {
if (state === "select") {
return (
<>
- <Modal.Body className="container">
- <Row>{t("settings.dialogChangeAvatar.prompt.select")}</Row>
- <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 className="container">
+ <div className="row">
+ {t("settings.dialogChangeAvatar.prompt.select")}
+ </div>
+ <div className="row">
+ <input
+ className="px-0"
+ type="file"
+ accept="image/*"
+ onChange={onSelectFile}
+ />
+ </div>
+ </div>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <Button
+ text="operationDialog.cancel"
+ color="secondary"
+ onClick={close}
+ />
+ </div>
</>
);
} else if (state === "crop") {
@@ -186,119 +200,154 @@ 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>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <Button
+ text="operationDialog.cancel"
+ color="secondary"
+ outline
+ onClick={close}
+ />
+ <Button
+ text="operationDialog.previousStep"
+ color="secondary"
+ outline
+ 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>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <Button
+ text="operationDialog.cancel"
+ color="secondary"
+ onClick={close}
+ outline
+ />
+ <Button
+ text="operationDialog.previousStep"
+ color="secondary"
+ onClick={onPreviewPrevious}
+ outline
+ />
+ </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>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <Button
+ text="operationDialog.cancel"
+ color="secondary"
+ outline
+ onClick={close}
+ />
+ <Button
+ text="operationDialog.previousStep"
+ color="secondary"
+ outline
+ 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>
</>
);
} 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>
+ <hr />
+ <div className="cru-dialog-bottom-area">
+ <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>
+ <hr />
+ <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..69a74327 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,10 +25,10 @@ const SettingsPage: React.FC = (_) => {
return (
<>
- <Container>
+ <div className="container">
{user ? (
<Card className="my-3 py-3">
- <h3 className="px-3 mb-3 text-primary">
+ <h3 className="px-3 mb-3 cru-color-primary">
{t("settings.subheaders.account")}
</h3>
<div
@@ -70,13 +44,13 @@ const SettingsPage: React.FC = (_) => {
{t("settings.changeNickname")}
</div>
<div
- className="settings-item clickable text-danger"
+ className="settings-item clickable cru-color-danger"
onClick={() => setDialog("changepassword")}
>
{t("settings.changePassword")}
</div>
<div
- className="settings-item clickable text-danger"
+ className="settings-item clickable cru-color-danger"
onClick={() => {
setDialog("logout");
}}
@@ -86,19 +60,18 @@ const SettingsPage: React.FC = (_) => {
</Card>
) : null}
<Card className="my-3 py-3">
- <h3 className="px-3 mb-3 text-primary">
+ <h3 className="px-3 mb-3 cru-color-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">
+ <small className="d-block cru-color-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,19 +79,22 @@ 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)}
+ open
onConfirm={() => {
void userService.logout().then(() => {
history.push("/");
diff --git a/FrontEnd/src/views/timeline-common/CollapseButton.tsx b/FrontEnd/src/views/timeline-common/CollapseButton.tsx
index 12a3b710..31976228 100644
--- a/FrontEnd/src/views/timeline-common/CollapseButton.tsx
+++ b/FrontEnd/src/views/timeline-common/CollapseButton.tsx
@@ -12,7 +12,7 @@ const CollapseButton: React.FC<{
onClick={onClick}
className={classnames(
collapse ? "bi-arrows-angle-expand" : "bi-arrows-angle-contract",
- "text-primary icon-button",
+ "cru-color-primary icon-button",
className
)}
style={style}
diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.css b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.css
new file mode 100644
index 00000000..e36be992
--- /dev/null
+++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.css
@@ -0,0 +1,21 @@
+.timeline-markdown-post-edit-page {
+ overflow: auto;
+ max-height: 300px;
+}
+
+.timeline-markdown-post-edit-image-container {
+ position: relative;
+ text-align: center;
+ margin-bottom: 1em;
+}
+
+.timeline-markdown-post-edit-image {
+ max-width: 100%;
+ max-height: 200px;
+}
+
+.timeline-markdown-post-edit-image-delete-button {
+ position: absolute;
+ right: 10px;
+ top: 2px;
+}
diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
index 005da933..6d0fbedd 100644
--- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
@@ -1,15 +1,19 @@
+/* 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";
+
+import "./MarkdownPostEdit.css";
export interface MarkdownPostEditProps {
timeline: string;
@@ -100,9 +104,10 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
className={className}
style={style}
pageContainerClassName="py-2"
+ dense
actions={
process ? (
- <Spinner variant="primary" animation="border" size="sm" />
+ <Spinner />
) : (
<>
<FlatButton
@@ -123,13 +128,13 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
}
pages={[
{
- id: "text",
- tabText: "edit",
+ name: "text",
+ text: "edit",
page: (
- <Form.Control
- as="textarea"
+ <textarea
value={text}
disabled={process}
+ className="cru-fill-parent"
onChange={(event) => {
getBuilder().setMarkdownText(event.currentTarget.value);
}}
@@ -137,8 +142,8 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
),
},
{
- id: "images",
- tabText: "image",
+ name: "images",
+ text: "image",
page: (
<div className="timeline-markdown-post-edit-page">
{images.map((image, index) => (
@@ -161,7 +166,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>) => {
@@ -176,8 +181,8 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
),
},
{
- id: "preview",
- tabText: "preview",
+ name: "preview",
+ text: "preview",
page: (
<div
className="markdown-container timeline-markdown-post-edit-page"
@@ -191,6 +196,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
<ConfirmDialog
onClose={() => setShowLeaveConfirmDialog(false)}
onConfirm={onClose}
+ open
title="timeline.dropDraft"
body="timeline.confirmLeave"
/>
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.css b/FrontEnd/src/views/timeline-common/TimelineMember.css
new file mode 100644
index 00000000..adb78764
--- /dev/null
+++ b/FrontEnd/src/views/timeline-common/TimelineMember.css
@@ -0,0 +1,8 @@
+.timeline-member-item {
+ border: var(--cru-background-1-color) solid;
+ border-width: 0.5px 1px;
+}
+
+.timeline-member-item > div {
+ padding: 0.5em;
+}
diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx
index 299d6a53..0ebecbb9 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";
+
+import "./TimelineMember.css";
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">
- <UserAvatar username={user.username} className="avatar small" />
- </Col>
- <Col>
- <Row>{user.nickname}</Row>
- <Row>
- <small>{"@" + user.username}</small>
- </Row>
- </Col>
+ <div className="container timeline-member-item">
+ <div className="row">
+ <div className="col col-auto">
+ <UserAvatar username={user.username} className="cru-avatar small" />
+ </div>
+ <div className="col">
+ <div className="row">{user.nickname}</div>
+ <small className="row">{"@" + user.username}</small>
+ </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,12 +125,12 @@ const TimelineMemberUserSearch: React.FC<{
}}
/>
))}
- </ListGroup>
+ </div>
);
}
} else if (userSearchState.type === "error") {
return (
- <div className="text-danger">
+ <div className="cru-color-danger">
{convertI18nText(userSearchState.data, t)}
</div>
);
@@ -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..5c2fb275 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx
@@ -17,12 +17,13 @@ import CollapseButton from "./CollapseButton";
import { TimelineMemberDialog } from "./TimelineMember";
import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog";
import ConnectionStatusBadge from "./ConnectionStatusBadge";
-import { MenuItems, PopupMenu } from "../common/Menu";
-import FullPage from "../common/FullPage";
+import { MenuItems } from "../common/menu/Menu";
+import PopupMenu from "../common/menu/PopupMenu";
+import FullPageDialog from "../common/dailog/FullPageDialog";
import Card from "../common/Card";
export interface TimelineCardTemplateProps extends TimelinePageCardProps {
- infoArea: React.ReactElement;
+ infoArea: React.ReactNode;
manageItems?: MenuItems;
dialog: string | "property" | "member" | null;
setDialog: (dialog: "property" | "member" | null) => void;
@@ -53,11 +54,11 @@ const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({
<small className="mt-1 d-block">
{t(timelineVisibilityTooltipTranslationMap[timeline.visibility])}
</small>
- <div className="text-end mt-2">
+ <div className="mt-2 cru-text-end">
<i
className={classnames(
timeline.isHighlight ? "bi-star-fill" : "bi-star",
- "icon-button text-yellow me-3"
+ "icon-button cru-color-primary me-3"
)}
onClick={
user?.hasHighlightTimelineAdministrationPermission
@@ -80,7 +81,7 @@ const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({
<i
className={classnames(
timeline.isBookmark ? "bi-bookmark-fill" : "bi-bookmark",
- "icon-button text-yellow me-3"
+ "icon-button cru-color-primary me-3"
)}
onClick={() => {
getHttpBookmarkClient()
@@ -97,12 +98,12 @@ const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({
/>
) : null}
<i
- className={"icon-button bi-people text-primary me-3"}
+ className={"icon-button bi-people cru-color-primary me-3"}
onClick={() => setDialog("member")}
/>
{manageItems != null ? (
- <PopupMenu items={manageItems}>
- <i className="icon-button bi-three-dots-vertical text-primary" />
+ <PopupMenu items={manageItems} containerClassName="d-inline">
+ <i className="icon-button bi-three-dots-vertical cru-color-primary" />
</PopupMenu>
) : null}
</div>
@@ -111,24 +112,21 @@ const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({
return (
<>
- <Card
- className={classnames("p-2 clearfix", className)}
- style={{ zIndex: collapse ? 1029 : 1031 }}
- >
- <div className="float-end d-flex align-items-center">
+ <Card className={classnames("p-2 cru-clearfix", className)}>
+ <div className="cru-float-right ms-3 d-flex align-items-center">
<ConnectionStatusBadge status={connectionStatus} className="me-2" />
<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>
+ <div style={{ display: collapse ? "none" : "inline" }}>{content}</div>
)}
</Card>
{(() => {
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.css b/FrontEnd/src/views/timeline-common/TimelinePostEdit.css
index 0c7deaa2..4ce98383 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.css
+++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.css
@@ -9,7 +9,7 @@
}
.timeline-post-edit {
- position: sticky;
+ position: sticky !important;
bottom: 0;
z-index: 1;
}
@@ -18,25 +18,3 @@
max-width: 100px;
max-height: 100px;
}
-
-.timeline-markdown-post-edit-page {
- overflow: scroll;
- max-height: 300px;
-}
-
-.timeline-markdown-post-edit-image-container {
- position: relative;
- text-align: center;
- margin-bottom: 1em;
-}
-
-.timeline-markdown-post-edit-image {
- max-width: 100%;
- max-height: 200px;
-}
-
-.timeline-markdown-post-edit-image-delete-button {
- position: absolute;
- right: 10px;
- top: 2px;
-}
diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
index 14cd50d4..9c48c7c8 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,8 +15,8 @@ import { pushAlert } from "@/services/alert";
import { base64 } from "@/http/common";
import BlobImage from "../common/BlobImage";
-import LoadingButton from "../common/LoadingButton";
-import { PopupMenu } from "../common/Menu";
+import LoadingButton from "../common/button/LoadingButton";
+import PopupMenu from "../common/menu/PopupMenu";
import Card from "../common/Card";
import MarkdownPostEdit from "./MarkdownPostEdit";
import TimelineLine from "./TimelineLine";
@@ -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/*"
@@ -205,20 +203,20 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
<Card className="timeline-item-card">
{showMarkdown ? (
<MarkdownPostEdit
- className="w-100"
+ className="cru-fill-parent"
onClose={() => setShowMarkdown(false)}
timeline={timeline.name}
onPosted={onPosted}
onPostError={onPostError}
/>
) : (
- <Row>
- <Col className="px-1 py-1">
+ <div className="row">
+ <div className="col px-1 py-1">
{(() => {
if (kind === "text") {
return (
<TimelinePostEditText
- className="w-100 h-100 timeline-post-edit"
+ className="cru-fill-parent timeline-post-edit"
text={text}
disabled={process}
onChange={(t) => {
@@ -239,9 +237,9 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
);
}
})()}
- </Col>
- <Col xs="auto" className="align-self-end m-1">
- <div className="d-block text-center mt-1 mb-2">
+ </div>
+ <div className="col col-auto align-self-end m-1">
+ <div className="d-block cru-text-center mt-1 mb-2">
<PopupMenu
items={(["text", "image", "markdown"] as const).map(
(kind) => ({
@@ -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..652ff9c9 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 {
@@ -64,7 +64,7 @@ const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => {
>
{post.editable ? (
<i
- className="bi-chevron-down icon-button primary-enhance float-end"
+ className="bi-chevron-down icon-button primary-enhance cru-float-right"
onClick={(e) => {
setOperationMaskVisible(true);
e.stopPropagation();
@@ -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..6929f9ae 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(--cru-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(--cru-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(--cru-primary-l1-color);
}
}
@@ -79,7 +79,7 @@
.timeline-line .segment {
width: 7px;
- background: var(--tl-primary-color);
+ background: var(--cru-primary-color);
}
.timeline-line .segment.start {
height: 1.8em;
@@ -91,7 +91,7 @@
.timeline-line .segment.current-end {
height: 2em;
flex: 0 0 auto;
- background: linear-gradient(var(--tl-primary-enhance-color), white);
+ background: linear-gradient(var(--cru-primary-enhance-color), white);
}
.timeline-line .node-container {
flex: 0 0 auto;
@@ -103,7 +103,7 @@
width: 20px;
height: 20px;
position: absolute;
- background: var(--tl-primary-color);
+ background: var(--cru-primary-color);
left: -1px;
top: -1px;
border-radius: 50%;
@@ -113,7 +113,7 @@
animation-name: timeline-line-node-noncurrent;
}
.timeline-line .node-loading-edge {
- color: var(--tl-primary-color);
+ color: var(--cru-primary-color);
width: 38px;
height: 38px;
position: absolute;
@@ -125,22 +125,22 @@
}
.timeline-line.current .segment.start {
background: linear-gradient(
- var(--tl-primary-color),
- var(--tl-primary-enhance-color)
+ var(--cru-primary-color),
+ var(--cru-primary-enhance-color)
);
}
.timeline-line.current .segment.end {
- background: var(--tl-primary-enhance-color);
+ background: var(--cru-primary-enhance-color);
}
.timeline-line.current .node {
- background: var(--tl-primary-enhance-color);
+ background: var(--cru-primary-enhance-color);
animation-name: timeline-line-node-current;
}
.timeline-line.loading .node {
- background: var(--tl-primary-color);
+ background: var(--cru-primary-color);
animation-name: timeline-line-node-loading;
}
@@ -239,6 +239,7 @@
.timeline-template-card {
position: fixed;
+ z-index: 1029;
top: 56px;
right: 0;
margin: 0.5em;
diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx
index 86063843..56057560 100644
--- a/FrontEnd/src/views/timeline/TimelineCard.tsx
+++ b/FrontEnd/src/views/timeline/TimelineCard.tsx
@@ -18,18 +18,20 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => {
<TimelinePageCardTemplate
infoArea={
<>
- <h3 className="tl-color-primary d-inline-block align-middle">
+ <h3 className="cru-color-primary d-inline-block align-middle">
{timeline.title}
- <small className="ms-3 text-secondary">{timeline.name}</small>
+ <small className="ms-3 cru-color-secondary">
+ {timeline.name}
+ </small>
</h3>
- <div className="align-middle">
+ <div>
<UserAvatar
username={timeline.owner.username}
- className="avatar small rounded-circle me-3"
+ className="cru-avatar small cru-round me-3"
/>
{timeline.owner.nickname}
- <small className="ms-3 text-secondary">
- src{timeline.owner.username}
+ <small className="ms-3 cru-color-secondary">
+ @{timeline.owner.username}
</small>
</div>
</>
diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx
index dbca62ca..68dedf86 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,13 +20,13 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
return (
<OperationDialog
open={props.open}
- close={props.close}
+ onClose={props.close}
title="timeline.deleteDialog.title"
themeColor="danger"
inputPrompt={() => {
return (
<Trans i18nKey="timeline.deleteDialog.inputPrompt">
- 0<code className="mx-2">{{ name }}</code>2
+ 0<code className="mx-2">{{ name: timeline.name }}</code>2
</Trans>
);
}}
diff --git a/FrontEnd/src/views/user/UserCard.tsx b/FrontEnd/src/views/user/UserCard.tsx
index e7e4252e..739d26ee 100644
--- a/FrontEnd/src/views/user/UserCard.tsx
+++ b/FrontEnd/src/views/user/UserCard.tsx
@@ -16,14 +16,16 @@ const UserCard: React.FC<TimelinePageCardProps> = (props) => {
<TimelinePageCardTemplate
infoArea={
<>
- <h3 className="tl-color-primary d-inline-block align-middle">
+ <h3 className="cru-color-primary d-inline-block">
{timeline.title}
- <small className="ms-3 text-secondary">{timeline.name}</small>
+ <small className="ms-3 cru-color-secondary">
+ {timeline.name}
+ </small>
</h3>
- <div className="align-middle">
+ <div>
<UserAvatar
username={timeline.owner.username}
- className="avatar small rounded-circle me-3"
+ className="cru-avatar small cru-round me-3"
/>
{timeline.owner.nickname}
</div>
diff --git a/FrontEnd/src/views/user/index.css b/FrontEnd/src/views/user/index.css
index 35f01d38..e69de29b 100644
--- a/FrontEnd/src/views/user/index.css
+++ b/FrontEnd/src/views/user/index.css
@@ -1,9 +0,0 @@
-.change-avatar-cropper-row {
- max-height: 400px;
-}
-
-.change-avatar-img {
- min-width: 50%;
- max-width: 100%;
- max-height: 400px;
-}