From c3f17f1dd1099c244e36d09b14c3e131d703830e Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sat, 26 Jun 2021 19:54:24 +0800
Subject: ...

---
 FrontEnd/src/views/common/tab/TabPages.tsx | 58 ++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)
 create mode 100644 FrontEnd/src/views/common/tab/TabPages.tsx

(limited to 'FrontEnd/src/views/common/tab/TabPages.tsx')

diff --git a/FrontEnd/src/views/common/tab/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx
new file mode 100644
index 00000000..b7a9fb36
--- /dev/null
+++ b/FrontEnd/src/views/common/tab/TabPages.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+
+import { convertI18nText, I18nText, UiLogicError } from "@/common";
+
+export interface TabPage {
+  id: string;
+  tabText: I18nText;
+  page: React.ReactNode;
+}
+
+export interface TabPagesProps {
+  pages: TabPage[];
+  actions?: React.ReactNode;
+  className?: string;
+  style?: React.CSSProperties;
+  navClassName?: string;
+  navStyle?: React.CSSProperties;
+  pageContainerClassName?: string;
+  pageContainerStyle?: React.CSSProperties;
+}
+
+const TabPages: React.FC<TabPagesProps> = ({
+  pages,
+  actions,
+  className,
+  style,
+  navClassName,
+  navStyle,
+  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 currentPage = pages.find((p) => p.id === tab);
+
+  if (currentPage == null) {
+    throw new UiLogicError("Current tab value is bad.");
+  }
+
+  return (
+    <div className={className} style={style}>
+      <div className={pageContainerClassName} style={pageContainerStyle}>
+        {currentPage.page}
+      </div>
+    </div>
+  );
+};
+
+export default TabPages;
-- 
cgit v1.2.3


From 88851e84f070207581f5dfa78a94e52194a2281b Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Thu, 1 Jul 2021 17:54:04 +0800
Subject: ...

---
 FrontEnd/src/views/common/Skeleton.css             | 14 +++++++
 FrontEnd/src/views/common/Skeleton.tsx             |  4 +-
 FrontEnd/src/views/common/index.css                | 23 +++++------
 FrontEnd/src/views/common/tab/TabPages.tsx         | 33 ++++++++++-----
 FrontEnd/src/views/common/tab/Tabs.css             | 31 ++++++++++++++
 FrontEnd/src/views/common/tab/Tabs.tsx             | 47 +++++++++++++++++++++-
 FrontEnd/src/views/settings/ChangeAvatarDialog.tsx | 35 ++++++++++++----
 .../src/views/timeline-common/MarkdownPostEdit.css | 21 ++++++++++
 .../src/views/timeline-common/MarkdownPostEdit.tsx | 16 +++++---
 .../src/views/timeline-common/TimelinePostEdit.css | 24 +----------
 .../src/views/timeline-common/TimelinePostEdit.tsx |  6 +--
 11 files changed, 187 insertions(+), 67 deletions(-)
 create mode 100644 FrontEnd/src/views/common/Skeleton.css
 create mode 100644 FrontEnd/src/views/common/tab/Tabs.css
 create mode 100644 FrontEnd/src/views/timeline-common/MarkdownPostEdit.css

(limited to 'FrontEnd/src/views/common/tab/TabPages.tsx')

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) => ({
-- 
cgit v1.2.3

