aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd')
-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/index.css23
-rw-r--r--FrontEnd/src/views/common/tab/TabPages.tsx33
-rw-r--r--FrontEnd/src/views/common/tab/Tabs.css31
-rw-r--r--FrontEnd/src/views/common/tab/Tabs.tsx47
-rw-r--r--FrontEnd/src/views/settings/ChangeAvatarDialog.tsx35
-rw-r--r--FrontEnd/src/views/timeline-common/MarkdownPostEdit.css21
-rw-r--r--FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx16
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePostEdit.css24
-rw-r--r--FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx6
11 files changed, 187 insertions, 67 deletions
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/index.css b/FrontEnd/src/views/common/index.css
index 62167cfc..529e0e51 100644
--- a/FrontEnd/src/views/common/index.css
+++ b/FrontEnd/src/views/common/index.css
@@ -108,6 +108,10 @@
color: var(--cru-danger-color);
}
+.cru-text-center {
+ text-align: center;
+}
+
.cru-text-end {
text-align: end;
}
@@ -128,6 +132,11 @@
clear: both;
}
+.cru-fill-parent {
+ width: 100%;
+ height: 100%;
+}
+
.icon-button {
font-size: 1.4rem;
cursor: pointer;
@@ -160,20 +169,6 @@
border-radius: 50%;
}
-.cru-skeleton {
- padding: 0 1em;
-}
-
-.cru-skeleton-line {
- height: 1em;
- background-color: #e6e6e6;
- margin: 0.7em 0;
- border-radius: 0.2em;
-}
-.cru-skeleton-line.last {
- width: 50%;
-}
-
.cru-tab-pages-action-area {
display: flex;
align-items: center;
diff --git a/FrontEnd/src/views/common/tab/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx
index b7a9fb36..677f558a 100644
--- a/FrontEnd/src/views/common/tab/TabPages.tsx
+++ b/FrontEnd/src/views/common/tab/TabPages.tsx
@@ -1,17 +1,19 @@
import React from "react";
-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;
@@ -23,6 +25,7 @@ export interface TabPagesProps {
const TabPages: React.FC<TabPagesProps> = ({
pages,
actions,
+ dense,
className,
style,
navClassName,
@@ -30,17 +33,13 @@ const TabPages: React.FC<TabPagesProps> = ({
pageContainerClassName,
pageContainerStyle,
}) => {
- // TODO:
-
if (pages.length === 0) {
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.");
@@ -48,6 +47,20 @@ const TabPages: React.FC<TabPagesProps> = ({
return (
<div className={className} style={style}>
+ <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
index 29ebcbd8..701b4073 100644
--- a/FrontEnd/src/views/common/tab/Tabs.tsx
+++ b/FrontEnd/src/views/common/tab/Tabs.tsx
@@ -1,6 +1,11 @@
import React from "react";
+import { Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import classnames from "classnames";
-import { I18nText } from "@/common";
+import { convertI18nText, I18nText } from "@/common";
+
+import "./Tabs.css";
export interface Tab {
name: string;
@@ -11,9 +16,47 @@ export interface Tab {
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 {
- return <div></div>;
+ 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/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
index 0bf51c21..784da2d7 100644
--- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx
@@ -161,7 +161,10 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
return (
<Dialog open={props.open} onClose={close}>
- <h3>{t("settings.dialogChangeAvatar.title")}</h3>
+ <h3 className="cru-color-primary">
+ {t("settings.dialogChangeAvatar.title")}
+ </h3>
+ <hr />
{(() => {
if (state === "select") {
return (
@@ -171,10 +174,16 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
{t("settings.dialogChangeAvatar.prompt.select")}
</div>
<div className="row">
- <input type="file" accept="image/*" onChange={onSelectFile} />
+ <input
+ className="px-0"
+ type="file"
+ accept="image/*"
+ onChange={onSelectFile}
+ />
</div>
</div>
- <div>
+ <hr />
+ <div className="cru-dialog-bottom-area">
<Button
text="operationDialog.cancel"
color="secondary"
@@ -202,15 +211,18 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
{t("settings.dialogChangeAvatar.prompt.crop")}
</div>
</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
@@ -232,16 +244,19 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
{t("settings.dialogChangeAvatar.prompt.processingCrop")}
</div>
</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>
</>
@@ -255,15 +270,18 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
{t("settings.dialogChangeAvatar.prompt.preview")}
</div>
</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
@@ -283,7 +301,6 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
{t("settings.dialogChangeAvatar.prompt.uploading")}
</div>
</div>
- <div></div>
</>
);
} else if (state === "success") {
@@ -294,7 +311,8 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
{t("operationDialog.success")}
</div>
</div>
- <div>
+ <hr />
+ <div className="cru-dialog-bottom-area">
<Button
text="operationDialog.ok"
color="success"
@@ -310,6 +328,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
{createPreviewRow()}
<div className="row text-danger">{trueMessage}</div>
</div>
+ <hr />
<div>
<Button
text="operationDialog.cancel"
diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.css b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.css
new file mode 100644
index 00000000..a303b227
--- /dev/null
+++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.css
@@ -0,0 +1,21 @@
+.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/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
index b16bf43d..0066701f 100644
--- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx
@@ -13,6 +13,8 @@ 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;
onPosted: (post: HttpTimelinePostInfo) => void;
@@ -102,6 +104,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
className={className}
style={style}
pageContainerClassName="py-2"
+ dense
actions={
process ? (
<Spinner />
@@ -125,12 +128,13 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({
}
pages={[
{
- id: "text",
- tabText: "edit",
+ name: "text",
+ text: "edit",
page: (
<textarea
value={text}
disabled={process}
+ className="cru-fill-parent"
onChange={(event) => {
getBuilder().setMarkdownText(event.currentTarget.value);
}}
@@ -138,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) => (
@@ -177,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"
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 e2045429..9c48c7c8 100644
--- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx
@@ -203,7 +203,7 @@ 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}
@@ -216,7 +216,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
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,7 +239,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
})()}
</div>
<div className="col col-auto align-self-end m-1">
- <div className="d-block text-center mt-1 mb-2">
+ <div className="d-block cru-text-center mt-1 mb-2">
<PopupMenu
items={(["text", "image", "markdown"] as const).map(
(kind) => ({