mirror of
https://github.com/usememos/memos.git
synced 2025-01-01 10:01:54 +08:00
chore: update dropdown component
This commit is contained in:
parent
7a6eb53e0f
commit
004713d4cd
9 changed files with 139 additions and 163 deletions
|
@ -1,82 +0,0 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { userService } from "../services";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
||||
import showResourcesDialog from "./ResourcesDialog";
|
||||
import "../less/menu-btns-popup.less";
|
||||
|
||||
interface Props {
|
||||
shownStatus: boolean;
|
||||
setShownStatus: (status: boolean) => void;
|
||||
}
|
||||
|
||||
const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
const { shownStatus, setShownStatus } = props;
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const popupElRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (shownStatus) {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!popupElRef.current?.contains(event.target as Node)) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
setShownStatus(false);
|
||||
};
|
||||
window.addEventListener("click", handleClickOutside, {
|
||||
capture: true,
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
}, [shownStatus]);
|
||||
|
||||
const handleResourcesBtnClick = () => {
|
||||
showResourcesDialog();
|
||||
};
|
||||
|
||||
const handleArchivedBtnClick = () => {
|
||||
showArchivedMemoDialog();
|
||||
};
|
||||
|
||||
const handleAboutBtnClick = () => {
|
||||
showAboutSiteDialog();
|
||||
};
|
||||
|
||||
const handleSignOutBtnClick = async () => {
|
||||
userService
|
||||
.doSignOut()
|
||||
.then(() => {
|
||||
navigate("/auth");
|
||||
})
|
||||
.catch(() => {
|
||||
// do nth
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleResourcesBtnClick}>
|
||||
<span className="icon">🌄</span> {t("sidebar.resources")}
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handleArchivedBtnClick}>
|
||||
<span className="icon">🗂</span> {t("sidebar.archived")}
|
||||
</button>
|
||||
</Only>
|
||||
<button className="btn action-btn" onClick={handleAboutBtnClick}>
|
||||
<span className="icon">🤠</span> {t("common.about")}
|
||||
</button>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleSignOutBtnClick}>
|
||||
<span className="icon">👋</span> {t("common.sign-out")}
|
||||
</button>
|
||||
</Only>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuBtnsPopup;
|
|
@ -154,13 +154,31 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||
<span className="field-text name-text">{resource.filename}</span>
|
||||
<span className="field-text">{resource.type}</span>
|
||||
<div className="buttons-container">
|
||||
<Dropdown className="actions-dropdown">
|
||||
<button onClick={() => handlPreviewBtnClick(resource)}>{t("resources.preview")}</button>
|
||||
<button onClick={() => handleCopyResourceLinkBtnClick(resource)}>{t("resources.copy-link")}</button>
|
||||
<button className="delete-btn" onClick={() => handleDeleteResourceBtnClick(resource)}>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
actionsClassName="!w-32"
|
||||
actions={
|
||||
<>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
||||
onClick={() => handlPreviewBtnClick(resource)}
|
||||
>
|
||||
{t("resources.preview")}
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
||||
onClick={() => handleCopyResourceLinkBtnClick(resource)}
|
||||
>
|
||||
{t("resources.copy-link")}
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100"
|
||||
onClick={() => handleDeleteResourceBtnClick(resource)}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
|
|
@ -139,18 +139,36 @@ const PreferencesSection = () => {
|
|||
{currentUser?.id === user.id ? (
|
||||
<span className="tip-text">{t("common.yourself")}</span>
|
||||
) : (
|
||||
<Dropdown className="actions-dropdown">
|
||||
{user.rowStatus === "NORMAL" ? (
|
||||
<button onClick={() => handleArchiveUserClick(user)}>{t("common.archive")}</button>
|
||||
) : (
|
||||
<Dropdown
|
||||
actionsClassName="!w-24"
|
||||
actions={
|
||||
<>
|
||||
<button onClick={() => handleRestoreUserClick(user)}>{t("common.restore")}</button>
|
||||
<button className="delete" onClick={() => handleDeleteUserClick(user)}>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
{user.rowStatus === "NORMAL" ? (
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
||||
onClick={() => handleArchiveUserClick(user)}
|
||||
>
|
||||
{t("common.archive")}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
||||
onClick={() => handleRestoreUserClick(user)}
|
||||
>
|
||||
{t("common.restore")}
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100"
|
||||
onClick={() => handleDeleteUserClick(user)}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as utils from "../helpers/utils";
|
||||
import userService from "../services/userService";
|
||||
import { locationService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import Icon from "./Icon";
|
||||
import MenuBtnsPopup from "./MenuBtnsPopup";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import showResourcesDialog from "./ResourcesDialog";
|
||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||
import "../less/user-banner.less";
|
||||
|
||||
const UserBanner = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { user, owner } = useAppSelector((state) => state.user);
|
||||
const { memos, tags } = useAppSelector((state) => state.memo);
|
||||
const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false);
|
||||
const [username, setUsername] = useState("Memos");
|
||||
const [createdDays, setCreatedDays] = useState(0);
|
||||
const isVisitorMode = userService.isVisitorMode();
|
||||
|
@ -34,8 +39,27 @@ const UserBanner = () => {
|
|||
locationService.clearQuery();
|
||||
}, []);
|
||||
|
||||
const handlePopupBtnClick = () => {
|
||||
setShouldShowPopupBtns(true);
|
||||
const handleResourcesBtnClick = () => {
|
||||
showResourcesDialog();
|
||||
};
|
||||
|
||||
const handleArchivedBtnClick = () => {
|
||||
showArchivedMemoDialog();
|
||||
};
|
||||
|
||||
const handleAboutBtnClick = () => {
|
||||
showAboutSiteDialog();
|
||||
};
|
||||
|
||||
const handleSignOutBtnClick = async () => {
|
||||
userService
|
||||
.doSignOut()
|
||||
.then(() => {
|
||||
navigate("/auth");
|
||||
})
|
||||
.catch(() => {
|
||||
// do nth
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -45,10 +69,39 @@ const UserBanner = () => {
|
|||
<span className="username-text">{username}</span>
|
||||
{!isVisitorMode && user?.role === "HOST" ? <span className="tag">MOD</span> : null}
|
||||
</div>
|
||||
<button className="action-btn menu-popup-btn" onClick={handlePopupBtnClick}>
|
||||
<Icon.MoreHorizontal className="icon-img" />
|
||||
</button>
|
||||
<MenuBtnsPopup shownStatus={shouldShowPopupBtns} setShownStatus={setShouldShowPopupBtns} />
|
||||
<Dropdown
|
||||
trigger={<Icon.MoreHorizontal className="w-5 h-auto cursor-pointer" />}
|
||||
actionsClassName="!w-36"
|
||||
actions={
|
||||
<>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
||||
onClick={handleResourcesBtnClick}
|
||||
>
|
||||
<span className="mr-1">🌄</span> {t("sidebar.resources")}
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
||||
onClick={handleArchivedBtnClick}
|
||||
>
|
||||
<span className="mr-1">🗂</span> {t("sidebar.archived")}
|
||||
</button>
|
||||
</Only>
|
||||
<button className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100" onClick={handleAboutBtnClick}>
|
||||
<span className="mr-1">🤠</span> {t("common.about")}
|
||||
</button>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button
|
||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
||||
onClick={handleSignOutBtnClick}
|
||||
>
|
||||
<span className="mr-1">👋</span> {t("common.sign-out")}
|
||||
</button>
|
||||
</Only>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="amount-text-container">
|
||||
<div className="status-text memos-text">
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import useToggle from "../../hooks/useToggle";
|
||||
import Icon from "../Icon";
|
||||
import "../../less/common/dropdown.less";
|
||||
|
||||
interface DropdownProps {
|
||||
children?: ReactNode;
|
||||
interface Props {
|
||||
trigger?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
className?: string;
|
||||
actionsClassName?: string;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = (props: DropdownProps) => {
|
||||
const { children, className } = props;
|
||||
const Dropdown: React.FC<Props> = (props: Props) => {
|
||||
const { trigger, actions, className, actionsClassName } = props;
|
||||
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
|
||||
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -28,11 +29,25 @@ const Dropdown: React.FC<DropdownProps> = (props: DropdownProps) => {
|
|||
}, [dropdownStatus]);
|
||||
|
||||
return (
|
||||
<div ref={dropdownWrapperRef} className={`dropdown-wrapper ${className ?? ""}`} onClick={() => toggleDropdownStatus()}>
|
||||
<span className="trigger-button">
|
||||
<Icon.MoreHorizontal className="icon-img" />
|
||||
</span>
|
||||
<div className={`action-buttons-container ${dropdownStatus ? "" : "!hidden"}`}>{children}</div>
|
||||
<div
|
||||
ref={dropdownWrapperRef}
|
||||
className={`relative flex flex-col justify-start items-start select-none ${className ?? ""}`}
|
||||
onClick={() => toggleDropdownStatus()}
|
||||
>
|
||||
{trigger ? (
|
||||
trigger
|
||||
) : (
|
||||
<button className="flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80">
|
||||
<Icon.MoreHorizontal className="w-4 h-auto" />
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`w-auto mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded-md shadow ${
|
||||
actionsClassName ?? ""
|
||||
} ${dropdownStatus ? "" : "!hidden"}`}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
@import "../mixin.less";
|
||||
|
||||
.dropdown-wrapper {
|
||||
@apply relative flex flex-col justify-start items-start select-none;
|
||||
|
||||
> .trigger-button {
|
||||
@apply flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .action-buttons-container {
|
||||
@apply w-28 mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded shadow;
|
||||
|
||||
> button {
|
||||
@apply w-full text-left px-2 text-sm leading-7 rounded hover:bg-gray-100;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
@import "./mixin.less";
|
||||
|
||||
.menu-btns-popup {
|
||||
@apply absolute right-2 top-6 flex flex-col justify-start items-start mt-4 p-1 w-36 rounded-lg z-10 shadow bg-white;
|
||||
|
||||
> .btn {
|
||||
@apply flex flex-row justify-start items-center w-full py-2 px-3 text-base rounded text-left hover:bg-gray-100;
|
||||
|
||||
> .icon {
|
||||
@apply block w-6 text-center mr-2 text-base;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,12 +45,6 @@
|
|||
|
||||
> .buttons-container {
|
||||
@apply w-full flex flex-row justify-end items-center;
|
||||
|
||||
> .actions-dropdown {
|
||||
.delete-btn {
|
||||
@apply text-red-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,12 +52,6 @@
|
|||
> .tip-text {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
|
||||
> .actions-dropdown {
|
||||
.delete {
|
||||
@apply text-red-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue