mirror of
				https://github.com/usememos/memos.git
				synced 2025-06-05 22:09:59 +02:00 
			
		
		
		
	feat: use dialog instead of page
This commit is contained in:
		
							
								
								
									
										79
									
								
								web/src/components/MemoTrashDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								web/src/components/MemoTrashDialog.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| import { useCallback, useEffect, useState } from "react"; | ||||
| import useLoading from "../hooks/useLoading"; | ||||
| import { locationService, memoService } from "../services"; | ||||
| import { showDialog } from "./Dialog"; | ||||
| import toastHelper from "./Toast"; | ||||
| import DeletedMemo from "./DeletedMemo"; | ||||
| import "../less/memo-trash-dialog.less"; | ||||
|  | ||||
| interface Props extends DialogProps {} | ||||
|  | ||||
| const MemoTrashDialog: React.FC<Props> = (props: Props) => { | ||||
|   const { destroy } = props; | ||||
|   const loadingState = useLoading(); | ||||
|   const [deletedMemos, setDeletedMemos] = useState<Model.Memo[]>([]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     memoService.fetchAllMemos(); | ||||
|     memoService | ||||
|       .fetchDeletedMemos() | ||||
|       .then((result) => { | ||||
|         if (result !== false) { | ||||
|           setDeletedMemos(result); | ||||
|         } | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         toastHelper.error("Failed to fetch deleted memos: ", error); | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         loadingState.setFinish(); | ||||
|       }); | ||||
|     locationService.clearQuery(); | ||||
|   }, []); | ||||
|  | ||||
|   const handleDeletedMemoAction = useCallback((memoId: string) => { | ||||
|     setDeletedMemos((deletedMemos) => deletedMemos.filter((memo) => memo.id !== memoId)); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="dialog-header-container"> | ||||
|         <p className="title-text"> | ||||
|           <span className="icon-text">🗑️</span> | ||||
|           Trash Bin | ||||
|         </p> | ||||
|         <button className="btn close-btn" onClick={destroy}> | ||||
|           <img className="icon-img" src="/icons/close.svg" /> | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="dialog-content-container"> | ||||
|         {loadingState.isLoading ? ( | ||||
|           <div className="tip-text-container"> | ||||
|             <p className="tip-text">fetching data...</p> | ||||
|           </div> | ||||
|         ) : deletedMemos.length === 0 ? ( | ||||
|           <div className="tip-text-container"> | ||||
|             <p className="tip-text">Here is No Zettels.</p> | ||||
|           </div> | ||||
|         ) : ( | ||||
|           <div className="deleted-memos-container"> | ||||
|             {deletedMemos.map((memo) => ( | ||||
|               <DeletedMemo key={`${memo.id}-${memo.updatedAt}`} memo={memo} handleDeletedMemoAction={handleDeletedMemoAction} /> | ||||
|             ))} | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default function showMemoTrashDialog(): void { | ||||
|   showDialog( | ||||
|     { | ||||
|       className: "memo-trash-dialog", | ||||
|       useAppContext: true, | ||||
|     }, | ||||
|     MemoTrashDialog, | ||||
|     {} | ||||
|   ); | ||||
| } | ||||
| @@ -1,6 +1,8 @@ | ||||
| import { useEffect, useRef } from "react"; | ||||
| import { locationService, userService } from "../services"; | ||||
| import showAboutSiteDialog from "./AboutSiteDialog"; | ||||
| import showSettingDialog from "./SettingDialog"; | ||||
| import showMemoTrashDialog from "./MemoTrashDialog"; | ||||
| import "../less/menu-btns-popup.less"; | ||||
|  | ||||
| interface Props { | ||||
| @@ -29,11 +31,11 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => { | ||||
|   }, [shownStatus]); | ||||
|  | ||||
|   const handleMyAccountBtnClick = () => { | ||||
|     locationService.pushHistory("/setting"); | ||||
|     showSettingDialog(); | ||||
|   }; | ||||
|  | ||||
|   const handleMemosTrashBtnClick = () => { | ||||
|     locationService.pushHistory("/trash"); | ||||
|     showMemoTrashDialog(); | ||||
|   }; | ||||
|  | ||||
|   const handleAboutBtnClick = () => { | ||||
| @@ -49,7 +51,7 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => { | ||||
|   return ( | ||||
|     <div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}> | ||||
|       <button className="btn action-btn" onClick={handleMyAccountBtnClick}> | ||||
|         <span className="icon">👤</span> Settings | ||||
|         <span className="icon">👤</span> Setting | ||||
|       </button> | ||||
|       <button className="btn action-btn" onClick={handleMemosTrashBtnClick}> | ||||
|         <span className="icon">🗑️</span> Recycle Bin | ||||
|   | ||||
							
								
								
									
										45
									
								
								web/src/components/SettingDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								web/src/components/SettingDialog.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import { useEffect } from "react"; | ||||
| import { memoService } from "../services"; | ||||
| import { showDialog } from "./Dialog"; | ||||
| import MyAccountSection from "./MyAccountSection"; | ||||
| import PreferencesSection from "./PreferencesSection"; | ||||
| import "../less/setting-dialog.less"; | ||||
|  | ||||
| interface Props extends DialogProps {} | ||||
|  | ||||
| const SettingDialog: React.FC<Props> = (props: Props) => { | ||||
|   const { destroy } = props; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     memoService.fetchAllMemos(); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="dialog-header-container"> | ||||
|         <p className="title-text"> | ||||
|           <span className="icon-text">👤</span> | ||||
|           Setting | ||||
|         </p> | ||||
|         <button className="btn close-btn" onClick={destroy}> | ||||
|           <img className="icon-img" src="/icons/close.svg" /> | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="dialog-content-container"> | ||||
|         <MyAccountSection /> | ||||
|         <PreferencesSection /> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default function showSettingDialog(): void { | ||||
|   showDialog( | ||||
|     { | ||||
|       className: "setting-dialog", | ||||
|       useAppContext: true, | ||||
|     }, | ||||
|     SettingDialog, | ||||
|     {} | ||||
|   ); | ||||
| } | ||||
| @@ -67,14 +67,11 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont | ||||
|   const { shortcut, isActive } = props; | ||||
|   const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false); | ||||
|  | ||||
|   console.log(props); | ||||
|  | ||||
|   const handleShortcutClick = () => { | ||||
|     console.log("here"); | ||||
|     if (isActive) { | ||||
|       locationService.setMemoShortcut(""); | ||||
|     } else { | ||||
|       if (!["/", "/trash"].includes(locationService.getState().pathname)) { | ||||
|       if (!["/"].includes(locationService.getState().pathname)) { | ||||
|         locationService.setPathname("/"); | ||||
|       } | ||||
|       locationService.setMemoShortcut(shortcut.id); | ||||
|   | ||||
| @@ -101,7 +101,7 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain | ||||
|       locationService.setTagQuery(""); | ||||
|     } else { | ||||
|       utils.copyTextToClipboard(`#${tag.text} `); | ||||
|       if (!["/", "/trash"].includes(locationService.getState().pathname)) { | ||||
|       if (!["/"].includes(locationService.getState().pathname)) { | ||||
|         locationService.setPathname("/"); | ||||
|       } | ||||
|       locationService.setTagQuery(tag.text); | ||||
|   | ||||
| @@ -76,7 +76,7 @@ const UsageHeatMap: React.FC<Props> = () => { | ||||
|       locationService.setFromAndToQuery(0, 0); | ||||
|       setCurrentStat(null); | ||||
|     } else if (item.count > 0) { | ||||
|       if (!["/", "/trash"].includes(locationService.getState().pathname)) { | ||||
|       if (!["/"].includes(locationService.getState().pathname)) { | ||||
|         locationService.setPathname("/"); | ||||
|       } | ||||
|       locationService.setFromAndToQuery(item.timestamp, item.timestamp + DAILY_TIMESTAMP); | ||||
|   | ||||
							
								
								
									
										23
									
								
								web/src/less/memo-trash-dialog.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								web/src/less/memo-trash-dialog.less
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| @import "./mixin.less"; | ||||
| @import "./memos-header.less"; | ||||
|  | ||||
| .memo-trash-dialog { | ||||
|   > .dialog-container { | ||||
|     @apply w-128 max-w-full mb-8; | ||||
|  | ||||
|     > .dialog-content-container { | ||||
|       .flex(column, flex-start, flex-start); | ||||
|       @apply w-full overflow-y-scroll; | ||||
|  | ||||
|       > .tip-text-container { | ||||
|         @apply w-full h-32; | ||||
|         .flex(column, center, center); | ||||
|       } | ||||
|  | ||||
|       > .deleted-memos-container { | ||||
|         .flex(column, flex-start, flex-start); | ||||
|         @apply w-full; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,39 +0,0 @@ | ||||
| @import "./mixin.less"; | ||||
| @import "./memos-header.less"; | ||||
|  | ||||
| .memo-trash-wrapper { | ||||
|   @apply px-8; | ||||
|   .flex(column, flex-start, flex-start); | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   flex-grow: 1; | ||||
|   overflow-y: scroll; | ||||
|   .hide-scroll-bar(); | ||||
|  | ||||
|   > .section-header-container { | ||||
|     width: 100%; | ||||
|     height: 40px; | ||||
|     margin-bottom: 0; | ||||
|  | ||||
|     > .title-text { | ||||
|       font-weight: bold; | ||||
|       font-size: 18px; | ||||
|       color: @text-black; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   > .tip-text-container { | ||||
|     width: 100%; | ||||
|     height: 128px; | ||||
|     .flex(column, center, center); | ||||
|   } | ||||
|  | ||||
|   > .deleted-memos-container { | ||||
|     .flex(column, flex-start, flex-start); | ||||
|     flex-grow: 1; | ||||
|     width: 100%; | ||||
|     overflow-y: scroll; | ||||
|     padding-bottom: 64px; | ||||
|     .hide-scroll-bar(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										33
									
								
								web/src/less/setting-dialog.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/src/less/setting-dialog.less
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| @import "./mixin.less"; | ||||
| @import "./memos-header.less"; | ||||
|  | ||||
| .setting-dialog { | ||||
|   > .dialog-container { | ||||
|     @apply w-3/5 max-w-full mb-8; | ||||
|  | ||||
|     > .dialog-content-container { | ||||
|       .flex(column, flex-start, flex-start); | ||||
|       @apply w-full overflow-y-scroll; | ||||
|       .hide-scroll-bar(); | ||||
|  | ||||
|       > .section-container { | ||||
|         .flex(column, flex-start, flex-start); | ||||
|         @apply w-full my-2; | ||||
|  | ||||
|         > .title-text { | ||||
|           @apply text-base font-bold mb-2; | ||||
|           color: @text-black; | ||||
|         } | ||||
|  | ||||
|         > .form-label { | ||||
|           .flex(row, flex-start, center); | ||||
|           @apply w-full text-sm mb-2; | ||||
|  | ||||
|           > .normal-text { | ||||
|             @apply shrink-0; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,47 +0,0 @@ | ||||
| @import "./mixin.less"; | ||||
| @import "./memos-header.less"; | ||||
|  | ||||
| .preference-wrapper { | ||||
|   .flex(column, flex-start, flex-start); | ||||
|   @apply w-full h-full grow overflow-y-scroll px-8; | ||||
|   .hide-scroll-bar(); | ||||
|  | ||||
|   > .section-header-container { | ||||
|     @apply w-full h-10 mb-0; | ||||
|  | ||||
|     > .title-text { | ||||
|       @apply font-bold text-lg; | ||||
|       color: @text-black; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   > .tip-text-container { | ||||
|     .flex(column, center, center); | ||||
|     @apply w-full h-32; | ||||
|   } | ||||
|  | ||||
|   > .sections-wrapper { | ||||
|     .flex(column, flex-start, flex-start); | ||||
|     @apply grow w-full overflow-y-scroll pb-16; | ||||
|     .hide-scroll-bar(); | ||||
|  | ||||
|     > .section-container { | ||||
|       .flex(column, flex-start, flex-start); | ||||
|       @apply w-full bg-white my-2 mx-0 p-4 pb-2 rounded-lg; | ||||
|  | ||||
|       > .title-text { | ||||
|         @apply text-base font-bold mb-2; | ||||
|         color: @text-black; | ||||
|       } | ||||
|  | ||||
|       > .form-label { | ||||
|         .flex(row, flex-start, center); | ||||
|         @apply w-full text-sm mb-2; | ||||
|  | ||||
|         > .normal-text { | ||||
|           @apply shrink-0; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,30 +0,0 @@ | ||||
| import { useEffect } from "react"; | ||||
| import { memoService } from "../services"; | ||||
| import MyAccountSection from "../components/MyAccountSection"; | ||||
| import PreferencesSection from "../components/PreferencesSection"; | ||||
| import "../less/setting.less"; | ||||
|  | ||||
| interface Props {} | ||||
|  | ||||
| const Setting: React.FC<Props> = () => { | ||||
|   useEffect(() => { | ||||
|     memoService.fetchAllMemos(); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <div className="preference-wrapper"> | ||||
|       <div className="section-header-container"> | ||||
|         <div className="title-text"> | ||||
|           <span className="normal-text">Settings</span> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div className="sections-wrapper"> | ||||
|         <MyAccountSection /> | ||||
|         <PreferencesSection /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Setting; | ||||
| @@ -1,129 +0,0 @@ | ||||
| import { useCallback, useContext, useEffect, useState } from "react"; | ||||
| import appContext from "../stores/appContext"; | ||||
| import useLoading from "../hooks/useLoading"; | ||||
| import { locationService, memoService, shortcutService } from "../services"; | ||||
| import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts"; | ||||
| import utils from "../helpers/utils"; | ||||
| import { checkShouldShowMemoWithFilters } from "../helpers/filter"; | ||||
| import toastHelper from "../components/Toast"; | ||||
| import DeletedMemo from "../components/DeletedMemo"; | ||||
| import MemoFilter from "../components/MemoFilter"; | ||||
| import "../less/memo-trash.less"; | ||||
|  | ||||
| interface Props {} | ||||
|  | ||||
| const Trash: React.FC<Props> = () => { | ||||
|   const { | ||||
|     locationState: { query }, | ||||
|   } = useContext(appContext); | ||||
|   const loadingState = useLoading(); | ||||
|   const [deletedMemos, setDeletedMemos] = useState<Model.Memo[]>([]); | ||||
|  | ||||
|   const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query; | ||||
|   const queryFilter = shortcutService.getShortcutById(shortcutId); | ||||
|   const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || queryFilter); | ||||
|  | ||||
|   const shownMemos = | ||||
|     showMemoFilter || queryFilter | ||||
|       ? deletedMemos.filter((memo) => { | ||||
|           let shouldShow = true; | ||||
|  | ||||
|           if (queryFilter) { | ||||
|             const filters = JSON.parse(queryFilter.payload) as Filter[]; | ||||
|             if (Array.isArray(filters)) { | ||||
|               shouldShow = checkShouldShowMemoWithFilters(memo, filters); | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           if (tagQuery) { | ||||
|             const tagsSet = new Set<string>(); | ||||
|             for (const t of Array.from(memo.content.match(TAG_REG) ?? [])) { | ||||
|               const tag = t.replace(TAG_REG, "$1").trim(); | ||||
|               const items = tag.split("/"); | ||||
|               let temp = ""; | ||||
|               for (const i of items) { | ||||
|                 temp += i; | ||||
|                 tagsSet.add(temp); | ||||
|                 temp += "/"; | ||||
|               } | ||||
|             } | ||||
|             if (!tagsSet.has(tagQuery)) { | ||||
|               shouldShow = false; | ||||
|             } | ||||
|           } | ||||
|           if ( | ||||
|             duration && | ||||
|             duration.from < duration.to && | ||||
|             (utils.getTimeStampByDate(memo.createdAt) < duration.from || utils.getTimeStampByDate(memo.createdAt) > duration.to) | ||||
|           ) { | ||||
|             shouldShow = false; | ||||
|           } | ||||
|           if (memoType) { | ||||
|             if (memoType === "NOT_TAGGED" && memo.content.match(TAG_REG) !== null) { | ||||
|               shouldShow = false; | ||||
|             } else if (memoType === "LINKED" && memo.content.match(LINK_REG) === null) { | ||||
|               shouldShow = false; | ||||
|             } else if (memoType === "IMAGED" && memo.content.match(IMAGE_URL_REG) === null) { | ||||
|               shouldShow = false; | ||||
|             } else if (memoType === "CONNECTED" && memo.content.match(MEMO_LINK_REG) === null) { | ||||
|               shouldShow = false; | ||||
|             } | ||||
|           } | ||||
|           if (textQuery && !memo.content.includes(textQuery)) { | ||||
|             shouldShow = false; | ||||
|           } | ||||
|  | ||||
|           return shouldShow; | ||||
|         }) | ||||
|       : deletedMemos; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     memoService.fetchAllMemos(); | ||||
|     memoService | ||||
|       .fetchDeletedMemos() | ||||
|       .then((result) => { | ||||
|         if (result !== false) { | ||||
|           setDeletedMemos(result); | ||||
|         } | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         toastHelper.error("Failed to fetch deleted memos: ", error); | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         loadingState.setFinish(); | ||||
|       }); | ||||
|     locationService.clearQuery(); | ||||
|   }, []); | ||||
|  | ||||
|   const handleDeletedMemoAction = useCallback((memoId: string) => { | ||||
|     setDeletedMemos((deletedMemos) => deletedMemos.filter((memo) => memo.id !== memoId)); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <div className="memo-trash-wrapper"> | ||||
|       <div className="section-header-container"> | ||||
|         <div className="title-text"> | ||||
|           <span className="normal-text">Recycle Bin</span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <MemoFilter /> | ||||
|       {loadingState.isLoading ? ( | ||||
|         <div className="tip-text-container"> | ||||
|           <p className="tip-text">fetching data...</p> | ||||
|         </div> | ||||
|       ) : deletedMemos.length === 0 ? ( | ||||
|         <div className="tip-text-container"> | ||||
|           <p className="tip-text">Here is No Zettels.</p> | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <div className="deleted-memos-container"> | ||||
|           {shownMemos.map((memo) => ( | ||||
|             <DeletedMemo key={`${memo.id}-${memo.updatedAt}`} memo={memo} handleDeletedMemoAction={handleDeletedMemoAction} /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Trash; | ||||
| @@ -1,10 +1,6 @@ | ||||
| import Memos from "../pages/Memos"; | ||||
| import Trash from "../pages/Trash"; | ||||
| import Setting from "../pages/Setting"; | ||||
|  | ||||
| const homeRouter = { | ||||
|   "/trash": <Trash />, | ||||
|   "/setting": <Setting />, | ||||
|   "*": <Memos />, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -185,7 +185,7 @@ class LocationService { | ||||
|   }; | ||||
|  | ||||
|   public getValidPathname = (pathname: string): AppRouter => { | ||||
|     if (["/", "/signin", "/trash", "/setting"].includes(pathname)) { | ||||
|     if (["/", "/signin"].includes(pathname)) { | ||||
|       return pathname as AppRouter; | ||||
|     } else { | ||||
|       return "/"; | ||||
|   | ||||
							
								
								
									
										2
									
								
								web/src/types/location.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								web/src/types/location.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -11,7 +11,7 @@ interface Query { | ||||
|   shortcutId: string; | ||||
| } | ||||
|  | ||||
| type AppRouter = "/" | "/signin" | "/trash" | "/setting"; | ||||
| type AppRouter = "/" | "/signin"; | ||||
|  | ||||
| interface AppLocation { | ||||
|   pathname: AppRouter; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user