mirror of
				https://github.com/usememos/memos.git
				synced 2025-11-01 01:06:04 +08:00 
			
		
		
		
	feat: update home layout (#1242)
This commit is contained in:
		
							parent
							
								
									9d4bb5b3af
								
							
						
					
					
						commit
						6ab58f294e
					
				
					 26 changed files with 610 additions and 721 deletions
				
			
		|  | @ -63,7 +63,7 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => { | |||
|     <> | ||||
|       <div className="dialog-header-container"> | ||||
|         <p className="title-text" onClick={() => toggleShowDatePicker()}> | ||||
|           <span className="icon-text">📅</span> {t("sidebar.daily-review")} | ||||
|           <span className="icon-text">📅</span> {t("common.daily-review")} | ||||
|         </p> | ||||
|         <div className="btns-container"> | ||||
|           <button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}> | ||||
|  |  | |||
							
								
								
									
										92
									
								
								web/src/components/Header.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								web/src/components/Header.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | |||
| import { isUndefined } from "lodash-es"; | ||||
| import { useEffect } from "react"; | ||||
| import { Link } from "react-router-dom"; | ||||
| import { useTranslation } from "react-i18next"; | ||||
| import { useLocationStore, useUserStore } from "../store/module"; | ||||
| import Icon from "./Icon"; | ||||
| import showDailyReviewDialog from "./DailyReviewDialog"; | ||||
| import showResourcesDialog from "./ResourcesDialog"; | ||||
| import showSettingDialog from "./SettingDialog"; | ||||
| import UserBanner from "./UserBanner"; | ||||
| import "../less/header.less"; | ||||
| 
 | ||||
| const Header = () => { | ||||
|   const { t } = useTranslation(); | ||||
|   const userStore = useUserStore(); | ||||
|   const locationStore = useLocationStore(); | ||||
|   const query = locationStore.state.query; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     toggleHeader(false); | ||||
|   }, [query]); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="mask" onClick={() => toggleHeader(false)}></div> | ||||
|       <header className="header-wrapper"> | ||||
|         <UserBanner /> | ||||
|         <div className="w-full px-2 my-2 mt-4 flex flex-col justify-start items-start shrink-0 space-y-2"> | ||||
|           <Link | ||||
|             to="/" | ||||
|             className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|           > | ||||
|             <Icon.Home className="mr-4 w-6 h-auto opacity-80" /> {t("common.home")} | ||||
|           </Link> | ||||
|           <button | ||||
|             className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|             onClick={() => showDailyReviewDialog()} | ||||
|           > | ||||
|             <Icon.Calendar className="mr-4 w-6 h-auto opacity-80" /> {t("common.daily-review")} | ||||
|           </button> | ||||
|           <Link | ||||
|             to="/explore" | ||||
|             className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|           > | ||||
|             <Icon.Hash className="mr-4 w-6 h-auto opacity-80" /> {t("common.explore")} | ||||
|           </Link> | ||||
|           {!userStore.isVisitorMode() && ( | ||||
|             <> | ||||
|               <button | ||||
|                 className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|                 onClick={() => showResourcesDialog()} | ||||
|               > | ||||
|                 <Icon.Paperclip className="mr-4 w-6 h-auto opacity-80" /> {t("common.resources")} | ||||
|               </button> | ||||
|               <button | ||||
|                 className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|                 onClick={() => showDailyReviewDialog()} | ||||
|               > | ||||
|                 <Icon.Archive className="mr-4 w-6 h-auto opacity-80" /> {t("common.archive")} | ||||
|               </button> | ||||
|               <button | ||||
|                 className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|                 onClick={() => showSettingDialog()} | ||||
|               > | ||||
|                 <Icon.Settings className="mr-4 w-6 h-auto opacity-80" /> {t("common.settings")} | ||||
|               </button> | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|       </header> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const toggleHeader = (show?: boolean) => { | ||||
|   const headerEl = document.body.querySelector(".header-wrapper") as HTMLDivElement; | ||||
|   const maskEl = headerEl.previousSibling as HTMLDivElement; | ||||
| 
 | ||||
|   if (isUndefined(show)) { | ||||
|     show = !headerEl.classList.contains("show"); | ||||
|   } | ||||
| 
 | ||||
|   if (show) { | ||||
|     headerEl.classList.add("show"); | ||||
|     maskEl.classList.add("show"); | ||||
|   } else { | ||||
|     headerEl.classList.remove("show"); | ||||
|     maskEl.classList.remove("show"); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export default Header; | ||||
							
								
								
									
										48
									
								
								web/src/components/HomeSidebar.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								web/src/components/HomeSidebar.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| import { isUndefined } from "lodash-es"; | ||||
| import { useEffect } from "react"; | ||||
| import { useLocationStore } from "../store/module"; | ||||
| import ShortcutList from "./ShortcutList"; | ||||
| import TagList from "./TagList"; | ||||
| import SearchBar from "./SearchBar"; | ||||
| import "../less/home-sidebar.less"; | ||||
| 
 | ||||
| const HomeSidebar = () => { | ||||
|   const locationStore = useLocationStore(); | ||||
|   const query = locationStore.state.query; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     toggleHomeSidebar(false); | ||||
|   }, [query]); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="mask" onClick={() => toggleHomeSidebar(false)}></div> | ||||
|       <aside className="sidebar-wrapper"> | ||||
|         <div className="pl-6 pr-2 mb-4 w-full"> | ||||
|           <SearchBar /> | ||||
|         </div> | ||||
|         <ShortcutList /> | ||||
|         <TagList /> | ||||
|       </aside> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const toggleHomeSidebar = (show?: boolean) => { | ||||
|   const sidebarEl = document.body.querySelector(".sidebar-wrapper") as HTMLDivElement; | ||||
|   const maskEl = sidebarEl.previousSibling as HTMLDivElement; | ||||
| 
 | ||||
|   if (isUndefined(show)) { | ||||
|     show = !sidebarEl.classList.contains("show"); | ||||
|   } | ||||
| 
 | ||||
|   if (show) { | ||||
|     sidebarEl.classList.add("show"); | ||||
|     maskEl.classList.add("show"); | ||||
|   } else { | ||||
|     sidebarEl.classList.remove("show"); | ||||
|     maskEl.classList.remove("show"); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export default HomeSidebar; | ||||
|  | @ -17,6 +17,7 @@ import "../less/memo.less"; | |||
| 
 | ||||
| interface Props { | ||||
|   memo: Memo; | ||||
|   readonly?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => { | ||||
|  | @ -28,7 +29,7 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => { | |||
| }; | ||||
| 
 | ||||
| const Memo: React.FC<Props> = (props: Props) => { | ||||
|   const { memo } = props; | ||||
|   const { memo, readonly } = props; | ||||
|   const { t, i18n } = useTranslation(); | ||||
|   const navigate = useNavigate(); | ||||
|   const editorStore = useEditorStore(); | ||||
|  | @ -37,7 +38,7 @@ const Memo: React.FC<Props> = (props: Props) => { | |||
|   const memoStore = useMemoStore(); | ||||
|   const [createdTimeStr, setCreatedTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language)); | ||||
|   const memoContainerRef = useRef<HTMLDivElement>(null); | ||||
|   const isVisitorMode = userStore.isVisitorMode(); | ||||
|   const isVisitorMode = userStore.isVisitorMode() || readonly; | ||||
|   const updatedTimeStr = getFormatedMemoTimeStr(memo.updatedTs, i18n.language); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|  | @ -67,6 +68,10 @@ const Memo: React.FC<Props> = (props: Props) => { | |||
|   }; | ||||
| 
 | ||||
|   const handleTogglePinMemoBtnClick = async () => { | ||||
|     if (isVisitorMode) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       if (memo.pinned) { | ||||
|         await memoStore.unpinMemo(memo.id); | ||||
|  | @ -79,10 +84,18 @@ const Memo: React.FC<Props> = (props: Props) => { | |||
|   }; | ||||
| 
 | ||||
|   const handleEditMemoClick = () => { | ||||
|     if (isVisitorMode) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     editorStore.setEditMemoWithId(memo.id); | ||||
|   }; | ||||
| 
 | ||||
|   const handleArchiveMemoClick = async () => { | ||||
|     if (isVisitorMode) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       await memoStore.patchMemo({ | ||||
|         id: memo.id, | ||||
|  | @ -114,7 +127,7 @@ const Memo: React.FC<Props> = (props: Props) => { | |||
|         locationStore.setTagQuery(tagName); | ||||
|       } | ||||
|     } else if (targetEl.classList.contains("todo-block")) { | ||||
|       if (userStore.isVisitorMode()) { | ||||
|       if (isVisitorMode) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|  | @ -153,6 +166,10 @@ const Memo: React.FC<Props> = (props: Props) => { | |||
|   }; | ||||
| 
 | ||||
|   const handleMemoContentDoubleClick = (e: React.MouseEvent) => { | ||||
|     if (isVisitorMode) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const loginUser = userStore.state.user; | ||||
|     if (loginUser && !loginUser.localSetting.enableDoubleClickEditing) { | ||||
|       return; | ||||
|  | @ -191,6 +208,11 @@ const Memo: React.FC<Props> = (props: Props) => { | |||
|               {createdTimeStr} | ||||
|             </span> | ||||
|           </Tooltip> | ||||
|           {isVisitorMode && ( | ||||
|             <a className="ml-2 opacity-60 text-sm" href={`/u/${memo.creatorId}`}> | ||||
|               @{memo.creatorName} | ||||
|             </a> | ||||
|           )} | ||||
|           {memo.visibility !== "PRIVATE" && !isVisitorMode && ( | ||||
|             <span | ||||
|               className={`status-text ${memo.visibility.toLocaleLowerCase()}`} | ||||
|  |  | |||
|  | @ -1,62 +0,0 @@ | |||
| import { useCallback, useEffect, useState } from "react"; | ||||
| import { useLocationStore, useMemoStore, useShortcutStore, useUserStore } from "../store/module"; | ||||
| import Icon from "./Icon"; | ||||
| import SearchBar from "./SearchBar"; | ||||
| import { toggleSidebar } from "./Sidebar"; | ||||
| import "../less/memos-header.less"; | ||||
| 
 | ||||
| let prevRequestTimestamp = Date.now(); | ||||
| 
 | ||||
| const MemosHeader = () => { | ||||
|   const locationStore = useLocationStore(); | ||||
|   const memoStore = useMemoStore(); | ||||
|   const shortcutStore = useShortcutStore(); | ||||
|   const userStore = useUserStore(); | ||||
|   const user = userStore.state.user; | ||||
|   const query = locationStore.state.query; | ||||
|   const shortcuts = shortcutStore.state.shortcuts; | ||||
|   const [titleText, setTitleText] = useState("MEMOS"); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!query?.shortcutId) { | ||||
|       setTitleText("MEMOS"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const shortcut = shortcutStore.getShortcutById(query?.shortcutId); | ||||
|     if (shortcut) { | ||||
|       setTitleText(shortcut.title); | ||||
|     } | ||||
|   }, [query, shortcuts]); | ||||
| 
 | ||||
|   const handleTitleTextClick = useCallback(() => { | ||||
|     const now = Date.now(); | ||||
|     if (now - prevRequestTimestamp > 1 * 1000) { | ||||
|       prevRequestTimestamp = now; | ||||
|       memoStore.fetchMemos().catch(() => { | ||||
|         // do nth
 | ||||
|       }); | ||||
|     } | ||||
|   }, []); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="memos-header-container"> | ||||
|       <div className="title-container"> | ||||
|         <div className="action-btn" onClick={() => toggleSidebar(true)}> | ||||
|           <Icon.Menu className="icon-img" /> | ||||
|         </div> | ||||
|         <span className="title-text" onClick={handleTitleTextClick}> | ||||
|           {titleText} | ||||
|         </span> | ||||
|         {user && ( | ||||
|           <a className="dark:text-white" href={"/u/" + user.id + "/rss.xml"} target="_blank" rel="noreferrer"> | ||||
|             <Icon.Rss className="w-4 h-auto opacity-40 hover:opacity-60" /> | ||||
|           </a> | ||||
|         )} | ||||
|       </div> | ||||
|       <SearchBar /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default MemosHeader; | ||||
							
								
								
									
										62
									
								
								web/src/components/MobileHeader.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								web/src/components/MobileHeader.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| import { useCallback, useEffect, useState } from "react"; | ||||
| import { useLocationStore, useMemoStore, useShortcutStore } from "../store/module"; | ||||
| import Icon from "./Icon"; | ||||
| import { toggleHeader } from "./Header"; | ||||
| import { toggleHomeSidebar } from "./HomeSidebar"; | ||||
| 
 | ||||
| let prevRequestTimestamp = Date.now(); | ||||
| 
 | ||||
| const MobileHeader = () => { | ||||
|   const locationStore = useLocationStore(); | ||||
|   const memoStore = useMemoStore(); | ||||
|   const shortcutStore = useShortcutStore(); | ||||
|   const query = locationStore.state.query; | ||||
|   const shortcuts = shortcutStore.state.shortcuts; | ||||
|   const [titleText, setTitleText] = useState("MEMOS"); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!query?.shortcutId) { | ||||
|       setTitleText("MEMOS"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const shortcut = shortcutStore.getShortcutById(query?.shortcutId); | ||||
|     if (shortcut) { | ||||
|       setTitleText(shortcut.title); | ||||
|     } | ||||
|   }, [query, shortcuts]); | ||||
| 
 | ||||
|   const handleTitleTextClick = useCallback(() => { | ||||
|     const now = Date.now(); | ||||
|     if (now - prevRequestTimestamp > 1 * 1000) { | ||||
|       prevRequestTimestamp = now; | ||||
|       memoStore.fetchMemos().catch(() => { | ||||
|         // do nth
 | ||||
|       }); | ||||
|     } | ||||
|   }, []); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="sticky top-0 pt-4 pb-1 mb-1 backdrop-blur-sm flex sm:hidden flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-10"> | ||||
|       <div className="flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden"> | ||||
|         <div | ||||
|           className="flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent" | ||||
|           onClick={() => toggleHeader(true)} | ||||
|         > | ||||
|           <Icon.Menu className="w-5 h-auto dark:text-gray-200" /> | ||||
|         </div> | ||||
|         <span | ||||
|           className="font-bold text-lg leading-10 mr-1 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700 dark:text-gray-200" | ||||
|           onClick={handleTitleTextClick} | ||||
|         > | ||||
|           {titleText} | ||||
|         </span> | ||||
|       </div> | ||||
|       <div className="flex flex-row justify-end items-center pr-1"> | ||||
|         <Icon.Search className="w-5 h-auto dark:text-gray-200" onClick={() => toggleHomeSidebar(true)} /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default MobileHeader; | ||||
|  | @ -104,7 +104,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => { | |||
|   return ( | ||||
|     <> | ||||
|       <div className="dialog-header-container"> | ||||
|         <p className="title-text">{t("sidebar.resources")}</p> | ||||
|         <p className="title-text">{t("common.resources")}</p> | ||||
|         <button className="btn close-btn" onClick={destroy}> | ||||
|           <Icon.X className="icon-img" /> | ||||
|         </button> | ||||
|  |  | |||
|  | @ -79,7 +79,7 @@ const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => { | |||
|   return ( | ||||
|     <> | ||||
|       <div className="dialog-header-container"> | ||||
|         <p className="title-text">{t("sidebar.resources")}</p> | ||||
|         <p className="title-text">{t("common.resources")}</p> | ||||
|         <button className="btn close-btn" onClick={destroy}> | ||||
|           <Icon.X className="icon-img" /> | ||||
|         </button> | ||||
|  |  | |||
|  | @ -86,7 +86,7 @@ const SearchBar = () => { | |||
|           onBlur={handleBlur} | ||||
|         /> | ||||
|       </div> | ||||
|       <div className="quickly-action-wrapper"> | ||||
|       <div className="quickly-action-wrapper !hidden"> | ||||
|         <div className="quickly-action-container"> | ||||
|           <p className="title-text">{t("search.quickly-filter").toUpperCase()}</p> | ||||
|           <div className="section-container types-container"> | ||||
|  |  | |||
|  | @ -148,7 +148,7 @@ const SystemSection = () => { | |||
|         </span> | ||||
|         <Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button> | ||||
|       </div> | ||||
|       <p className="title-text">{t("sidebar.setting")}</p> | ||||
|       <p className="title-text">{t("common.settings")}</p> | ||||
|       <div className="form-label"> | ||||
|         <span className="normal-text">{t("setting.system-section.allow-user-signup")}</span> | ||||
|         <Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} /> | ||||
|  |  | |||
|  | @ -1,93 +0,0 @@ | |||
| import { isUndefined } from "lodash-es"; | ||||
| import { useEffect } from "react"; | ||||
| import { Link } from "react-router-dom"; | ||||
| import { useTranslation } from "react-i18next"; | ||||
| import { useLocationStore, useUserStore } from "../store/module"; | ||||
| import showDailyReviewDialog from "./DailyReviewDialog"; | ||||
| import showResourcesDialog from "./ResourcesDialog"; | ||||
| import showSettingDialog from "./SettingDialog"; | ||||
| import UserBanner from "./UserBanner"; | ||||
| import UsageHeatMap from "./UsageHeatMap"; | ||||
| import ShortcutList from "./ShortcutList"; | ||||
| import TagList from "./TagList"; | ||||
| import "../less/siderbar.less"; | ||||
| 
 | ||||
| const Sidebar = () => { | ||||
|   const { t } = useTranslation(); | ||||
|   const userStore = useUserStore(); | ||||
|   const locationStore = useLocationStore(); | ||||
|   const query = locationStore.state.query; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     toggleSidebar(false); | ||||
|   }, [query]); | ||||
| 
 | ||||
|   const handleSettingBtnClick = () => { | ||||
|     showSettingDialog(); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="mask" onClick={() => toggleSidebar(false)}></div> | ||||
|       <aside className="sidebar-wrapper"> | ||||
|         <UserBanner /> | ||||
|         <UsageHeatMap /> | ||||
|         <div className="w-full px-2 my-2 flex flex-col justify-start items-start shrink-0"> | ||||
|           <button | ||||
|             className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|             onClick={() => showDailyReviewDialog()} | ||||
|           > | ||||
|             <span className="mr-1">📅</span> {t("sidebar.daily-review")} | ||||
|           </button> | ||||
|           <Link | ||||
|             to="/explore" | ||||
|             className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|           > | ||||
|             <span className="mr-1">🏂</span> {t("common.explore")} | ||||
|           </Link> | ||||
|           <button | ||||
|             className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|             onClick={() => showResourcesDialog()} | ||||
|           > | ||||
|             <span className="mr-1">🗂️</span> {t("sidebar.resources")} | ||||
|           </button> | ||||
|           {!userStore.isVisitorMode() && ( | ||||
|             <> | ||||
|               <button | ||||
|                 className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700" | ||||
|                 onClick={handleSettingBtnClick} | ||||
|               > | ||||
|                 <span className="mr-1">⚙️</span> {t("sidebar.setting")} | ||||
|               </button> | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|         {!userStore.isVisitorMode() && ( | ||||
|           <> | ||||
|             <ShortcutList /> | ||||
|             <TagList /> | ||||
|           </> | ||||
|         )} | ||||
|       </aside> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const toggleSidebar = (show?: boolean) => { | ||||
|   const sidebarEl = document.body.querySelector(".sidebar-wrapper") as HTMLDivElement; | ||||
|   const maskEl = document.body.querySelector(".mask") as HTMLDivElement; | ||||
| 
 | ||||
|   if (isUndefined(show)) { | ||||
|     show = !sidebarEl.classList.contains("show"); | ||||
|   } | ||||
| 
 | ||||
|   if (show) { | ||||
|     sidebarEl.classList.add("show"); | ||||
|     maskEl.classList.add("show"); | ||||
|   } else { | ||||
|     sidebarEl.classList.remove("show"); | ||||
|     maskEl.classList.remove("show"); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export default Sidebar; | ||||
|  | @ -83,7 +83,6 @@ const TagList = () => { | |||
|         {tags.map((t, idx) => ( | ||||
|           <TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={query?.tag} /> | ||||
|         ))} | ||||
|         {tags.length <= 3 && <p className="tip-text">{t("tag-list.tip-text")}</p>} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
|  |  | |||
|  | @ -1,10 +1,7 @@ | |||
| import { useEffect, useState } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | ||||
| import { useMemoStore, useTagStore, useUserStore } from "../store/module"; | ||||
| import { getMemoStats } from "../helpers/api"; | ||||
| import * as utils from "../helpers/utils"; | ||||
| import { useUserStore } from "../store/module"; | ||||
| import Dropdown from "./common/Dropdown"; | ||||
| import showArchivedMemoDialog from "./ArchivedMemoDialog"; | ||||
| import showAboutSiteDialog from "./AboutSiteDialog"; | ||||
| import UserAvatar from "./UserAvatar"; | ||||
| import showSettingDialog from "./SettingDialog"; | ||||
|  | @ -12,14 +9,8 @@ import showSettingDialog from "./SettingDialog"; | |||
| const UserBanner = () => { | ||||
|   const { t } = useTranslation(); | ||||
|   const userStore = useUserStore(); | ||||
|   const memoStore = useMemoStore(); | ||||
|   const tagStore = useTagStore(); | ||||
|   const { user, owner } = userStore.state; | ||||
|   const { memos } = memoStore.state; | ||||
|   const tags = tagStore.state.tags; | ||||
|   const [username, setUsername] = useState("Memos"); | ||||
|   const [memoAmount, setMemoAmount] = useState(0); | ||||
|   const [createdDays, setCreatedDays] = useState(0); | ||||
|   const isVisitorMode = userStore.isVisitorMode(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|  | @ -28,31 +19,15 @@ const UserBanner = () => { | |||
|         return; | ||||
|       } | ||||
|       setUsername(owner.nickname || owner.username); | ||||
|       setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(owner.createdTs)) / 1000 / 3600 / 24)); | ||||
|     } else if (user) { | ||||
|       setUsername(user.nickname || user.username); | ||||
|       setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24)); | ||||
|     } | ||||
|   }, [isVisitorMode, user, owner]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     getMemoStats(userStore.getCurrentUserId()) | ||||
|       .then(({ data: { data } }) => { | ||||
|         setMemoAmount(data.length); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error(error); | ||||
|       }); | ||||
|   }, [memos]); | ||||
| 
 | ||||
|   const handleMyAccountClick = () => { | ||||
|     showSettingDialog("my-account"); | ||||
|   }; | ||||
| 
 | ||||
|   const handleArchivedBtnClick = () => { | ||||
|     showArchivedMemoDialog(); | ||||
|   }; | ||||
| 
 | ||||
|   const handleAboutBtnClick = () => { | ||||
|     showAboutSiteDialog(); | ||||
|   }; | ||||
|  | @ -63,7 +38,6 @@ const UserBanner = () => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|     <div className="flex flex-row justify-between items-center relative w-full h-auto px-3 flex-nowrap shrink-0"> | ||||
|       <Dropdown | ||||
|         className="w-full" | ||||
|  | @ -88,12 +62,6 @@ const UserBanner = () => { | |||
|                 > | ||||
|                   <span className="mr-1">🤠</span> {t("setting.my-account")} | ||||
|                 </button> | ||||
|                   <button | ||||
|                     className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800" | ||||
|                     onClick={handleArchivedBtnClick} | ||||
|                   > | ||||
|                     <span className="mr-1">🗃️</span> {t("sidebar.archived")} | ||||
|                   </button> | ||||
|               </> | ||||
|             )} | ||||
|             <button | ||||
|  | @ -114,21 +82,6 @@ const UserBanner = () => { | |||
|         } | ||||
|       /> | ||||
|     </div> | ||||
|       <div className="flex flex-row justify-between items-start w-full px-6 select-none shrink-0 pb-2"> | ||||
|         <div className="flex flex-col justify-start items-start"> | ||||
|           <span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{memoAmount}</span> | ||||
|           <span className="text-gray-400 text-xs font-mono">{t("amount-text.memo", { count: memoAmount })}</span> | ||||
|         </div> | ||||
|         <div className="flex flex-col justify-start items-start"> | ||||
|           <span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{tags.length}</span> | ||||
|           <span className="text-gray-400 text-xs font-mono">{t("amount-text.tag", { count: tags.length })}</span> | ||||
|         </div> | ||||
|         <div className="flex flex-col justify-start items-start"> | ||||
|           <span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{createdDays}</span> | ||||
|           <span className="text-gray-400 text-xs font-mono">{t("amount-text.day", { count: createdDays })}</span> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,11 +17,6 @@ | |||
|     overflow-wrap: anywhere; | ||||
|     word-break: normal; | ||||
|   } | ||||
|   @media screen and (min-width: 1024px) { | ||||
|     .ml-calc { | ||||
|       margin-left: calc(100vw - 100%); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @layer components { | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|   @apply flex flex-row justify-center items-center w-full h-full dark:bg-zinc-800; | ||||
| 
 | ||||
|   > .page-container { | ||||
|     @apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center ml-calc; | ||||
|     @apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center; | ||||
| 
 | ||||
|     > .auth-form-wrapper { | ||||
|       @apply w-full py-4 grow flex flex-col justify-center items-center; | ||||
|  |  | |||
|  | @ -1,53 +0,0 @@ | |||
| .page-wrapper.explore { | ||||
|   @apply w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800; | ||||
| 
 | ||||
|   > .page-container { | ||||
|     @apply relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8; | ||||
| 
 | ||||
|     > .page-header { | ||||
|       @apply sticky top-0 z-10 max-w-2xl w-full h-auto flex flex-row justify-between backdrop-blur-sm items-center px-4 sm:pr-6 pt-6 mb-2 ml-calc; | ||||
| 
 | ||||
|       > .title-container { | ||||
|         @apply flex flex-row justify-start items-center; | ||||
| 
 | ||||
|         > .logo-img { | ||||
|           @apply h-12 w-auto rounded-md mr-2; | ||||
|         } | ||||
| 
 | ||||
|         > .title-text { | ||||
|           @apply text-xl sm:text-4xl text-gray-700 dark:text-gray-200; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     > .memos-wrapper { | ||||
|       @apply relative flex-grow max-w-2xl w-full h-auto flex flex-col justify-start items-start px-4 sm:pr-6 ml-calc; | ||||
| 
 | ||||
|       > .memo-container { | ||||
|         @apply relative flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-600; | ||||
| 
 | ||||
|         &.pinned { | ||||
|           @apply border-gray-200 border-2 dark:border-zinc-600; | ||||
|         } | ||||
| 
 | ||||
|         > .corner-container { | ||||
|           @apply absolute top-0 right-0 z-1; | ||||
| 
 | ||||
|           &::after { | ||||
|             @apply rounded-tr-md absolute top-0 right-0 border-transparent border-t-green-600 border-r-green-600; | ||||
|             content: ""; | ||||
|             border-width: 6px; | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         > .memo-header { | ||||
|           @apply mb-2 w-full flex flex-row justify-start items-center text-sm text-gray-400; | ||||
| 
 | ||||
|           > .name-text { | ||||
|             @apply ml-2 hover:text-green-600 hover:underline; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| .sidebar-wrapper { | ||||
|   @apply fixed sm:sticky top-0 z-30 sm:z-0 -translate-x-64 sm:translate-x-0 sm:flex flex-col justify-start items-start w-64 h-auto max-h-screen py-4 pl-2 bg-white dark:bg-zinc-800 sm:bg-transparent overflow-x-hidden overflow-y-auto transition-transform duration-300 overscroll-contain hide-scrollbar; | ||||
| .header-wrapper { | ||||
|   @apply fixed sm:sticky top-0 z-30 sm:z-0 -translate-x-64 sm:translate-x-0 sm:flex flex-col justify-start items-start w-56 h-full py-4 pl-2 bg-white dark:bg-zinc-800 sm:bg-transparent overflow-x-hidden overflow-y-auto transition-transform duration-300 overscroll-contain hide-scrollbar; | ||||
| 
 | ||||
|   &.show { | ||||
|     @apply translate-x-0 shadow-2xl sm:shadow-none; | ||||
							
								
								
									
										15
									
								
								web/src/less/home-sidebar.less
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								web/src/less/home-sidebar.less
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| .sidebar-wrapper { | ||||
|   @apply flex-shrink-0 fixed sm:sticky top-0 z-30 sm:z-0 translate-x-56 sm:translate-x-0 hidden md:flex flex-col justify-start items-start w-56 h-full py-4 bg-white dark:bg-zinc-800 sm:bg-transparent overflow-x-hidden overflow-y-auto transition-transform duration-300 overscroll-contain hide-scrollbar; | ||||
| 
 | ||||
|   &.show { | ||||
|     @apply flex translate-x-0 right-0 shadow-2xl sm:shadow-none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .mask { | ||||
|   @apply fixed top-0 left-0 w-full h-full bg-black opacity-0 transition-opacity duration-300 pointer-events-none z-20 sm:hidden; | ||||
| 
 | ||||
|   &.show { | ||||
|     @apply opacity-60 pointer-events-auto; | ||||
|   } | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| .page-wrapper.home { | ||||
|   @apply w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800; | ||||
|   @apply w-full h-full overflow-y-auto bg-zinc-100 dark:bg-zinc-800; | ||||
| 
 | ||||
|   > .banner-wrapper { | ||||
|     @apply w-full flex flex-col justify-start items-center; | ||||
|  | @ -8,28 +8,16 @@ | |||
|   > .page-container { | ||||
|     @apply relative w-full h-auto mx-auto flex flex-row justify-start sm:justify-center items-start; | ||||
| 
 | ||||
|     > .sidebar-wrapper { | ||||
|       @apply flex-shrink-0 h-full ml-calc; | ||||
|     > .header-wrapper { | ||||
|       @apply flex-shrink-0 h-full; | ||||
|     } | ||||
| 
 | ||||
|     > .memos-wrapper { | ||||
|       @apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 sm:pr-6; | ||||
|       @apply relative flex-grow max-w-2xl w-full h-auto flex flex-col justify-start items-start px-2 sm:pt-4; | ||||
| 
 | ||||
|       > .memos-editor-wrapper { | ||||
|         @apply w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg; | ||||
|       } | ||||
| 
 | ||||
|       > .addition-btn-container { | ||||
|         @apply fixed bottom-12 left-1/2 -translate-x-1/2; | ||||
| 
 | ||||
|         > .btn { | ||||
|           @apply bg-blue-600 dark:bg-blue-800 text-white dark:text-gray-200 px-4 py-2 rounded-3xl shadow-2xl hover:opacity-80; | ||||
| 
 | ||||
|           > .icon { | ||||
|             @apply text-lg mr-1; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
|     @apply relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8; | ||||
| 
 | ||||
|     > .page-header { | ||||
|       @apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 pt-6 mb-2 bg-zinc-100 dark:bg-zinc-800 ml-calc; | ||||
|       @apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 pt-6 mb-2 bg-zinc-100 dark:bg-zinc-800; | ||||
| 
 | ||||
|       > .title-container { | ||||
|         @apply flex flex-row justify-start items-center; | ||||
|  | @ -35,7 +35,7 @@ | |||
|     } | ||||
| 
 | ||||
|     > .memos-wrapper { | ||||
|       @apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 ml-calc; | ||||
|       @apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4; | ||||
| 
 | ||||
|       > .memo-container { | ||||
|         @apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700; | ||||
|  |  | |||
|  | @ -1,23 +0,0 @@ | |||
| .memos-header-container { | ||||
|   @apply sticky top-0 pt-4 pb-1 mb-1 backdrop-blur-sm flex flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-10; | ||||
| 
 | ||||
|   > .title-container { | ||||
|     @apply flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden; | ||||
| 
 | ||||
|     > .action-btn { | ||||
|       @apply flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent; | ||||
| 
 | ||||
|       > .icon-img { | ||||
|         @apply w-5 h-auto dark:text-gray-200; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     > .title-text { | ||||
|       @apply font-bold text-lg leading-10 mr-1 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700 dark:text-gray-200; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   > .btns-container { | ||||
|     @apply flex flex-row justify-end items-center; | ||||
|   } | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| .search-bar-container { | ||||
|   @apply relative w-auto; | ||||
|   @apply relative w-full; | ||||
| 
 | ||||
|   &:hover, | ||||
|   &:active { | ||||
|  |  | |||
|  | @ -1,15 +1,17 @@ | |||
| { | ||||
|   "common": { | ||||
|     "about": "About", | ||||
|     "home": "Home", | ||||
|     "resources": "Resources", | ||||
|     "settings": "Settings", | ||||
|     "daily-review": "Daily Review", | ||||
|     "email": "Email", | ||||
|     "password": "Password", | ||||
|     "repeat-password-short": "Repeat", | ||||
|     "repeat-password": "Repeat the password", | ||||
|     "new-password": "New password", | ||||
|     "repeat-new-password": "Repeat the new password", | ||||
|     "avatar": "Avatar", | ||||
|     "username": "Username", | ||||
|     "nickname": "Nickname", | ||||
|     "new-password": "New password", | ||||
|     "repeat-new-password": "Repeat the new password", | ||||
|     "save": "Save", | ||||
|     "close": "Close", | ||||
|     "cancel": "Cancel", | ||||
|  | @ -57,12 +59,6 @@ | |||
|     "host-tip": "You are registering as the Site Host.", | ||||
|     "not-host-tip": "If you don't have an account, please contact the site host." | ||||
|   }, | ||||
|   "sidebar": { | ||||
|     "daily-review": "Daily Review", | ||||
|     "resources": "Resources", | ||||
|     "setting": "Settings", | ||||
|     "archived": "Archived" | ||||
|   }, | ||||
|   "daily-review": { | ||||
|     "oops-nothing": "Oops, there is nothing." | ||||
|   }, | ||||
|  |  | |||
|  | @ -1,14 +1,16 @@ | |||
| { | ||||
|   "common": { | ||||
|     "about": "关于", | ||||
|     "home": "主页", | ||||
|     "resources": "资源库", | ||||
|     "settings": "设置", | ||||
|     "daily-review": "每日回顾", | ||||
|     "email": "邮箱", | ||||
|     "password": "密码", | ||||
|         "repeat-password-short": "重复密码", | ||||
|         "repeat-password": "重复密码", | ||||
|         "new-password": "新密码", | ||||
|         "repeat-new-password": "重复新密码", | ||||
|     "username": "用户名", | ||||
|     "nickname": "昵称", | ||||
|     "new-password": "新密码", | ||||
|     "repeat-new-password": "重复新密码", | ||||
|     "save": "保存", | ||||
|     "close": "关闭", | ||||
|     "cancel": "退出", | ||||
|  | @ -57,12 +59,6 @@ | |||
|     "host-tip": "你正在注册为 Host 用户账号。", | ||||
|     "not-host-tip": "如果你没有账号,请联系站点 Host" | ||||
|   }, | ||||
|     "sidebar": { | ||||
|         "daily-review": "每日回顾", | ||||
|         "resources": "资源库", | ||||
|         "setting": "设置", | ||||
|         "archived": "已归档" | ||||
|     }, | ||||
|   "daily-review": { | ||||
|     "oops-nothing": "啊哦,空空荡荡。" | ||||
|   }, | ||||
|  |  | |||
|  | @ -1,24 +1,21 @@ | |||
| import dayjs from "dayjs"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module"; | ||||
| import { TAG_REG } from "../labs/marked/parser"; | ||||
| import { DEFAULT_MEMO_LIMIT } from "../helpers/consts"; | ||||
| import useLoading from "../hooks/useLoading"; | ||||
| import toastHelper from "../components/Toast"; | ||||
| import MemoContent from "../components/MemoContent"; | ||||
| import MemoResources from "../components/MemoResources"; | ||||
| import MemoFilter from "../components/MemoFilter"; | ||||
| import Icon from "../components/Icon"; | ||||
| import { TAG_REG } from "../labs/marked/parser"; | ||||
| import "../less/explore.less"; | ||||
| import MemoFilter from "../components/MemoFilter"; | ||||
| import Memo from "../components/Memo"; | ||||
| 
 | ||||
| interface State { | ||||
|   memos: Memo[]; | ||||
| } | ||||
| 
 | ||||
| const Explore = () => { | ||||
|   const { t, i18n } = useTranslation(); | ||||
|   const { t } = useTranslation(); | ||||
|   const navigate = useNavigate(); | ||||
|   const globalStore = useGlobalStore(); | ||||
|   const locationStore = useLocationStore(); | ||||
|  | @ -91,20 +88,6 @@ const Explore = () => { | |||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleMemoContentClick = async (e: React.MouseEvent) => { | ||||
|     const targetEl = e.target as HTMLElement; | ||||
| 
 | ||||
|     if (targetEl.className === "tag-span") { | ||||
|       const tagName = targetEl.innerText.slice(1); | ||||
|       const currTagQuery = locationStore.getState().query?.tag; | ||||
|       if (currTagQuery === tagName) { | ||||
|         locationStore.setTagQuery(undefined); | ||||
|       } else { | ||||
|         locationStore.setTagQuery(tagName); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleTitleClick = () => { | ||||
|     if (user) { | ||||
|       navigate("/"); | ||||
|  | @ -114,12 +97,11 @@ const Explore = () => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <section className="page-wrapper explore"> | ||||
|       <div className="page-container"> | ||||
|         <div className="page-header"> | ||||
|           <div className="title-container cursor-pointer hover:opacity-80" onClick={handleTitleClick}> | ||||
|             <img className="logo-img" src={customizedProfile.logoUrl} alt="" /> | ||||
|             <span className="title-text">{customizedProfile.name}</span> | ||||
|     <section className="w-full min-h-full flex flex-col justify-start items-center pb-8 bg-zinc-100 dark:bg-zinc-800"> | ||||
|       <div className="sticky top-0 z-10 max-w-2xl w-full h-auto flex flex-row justify-between backdrop-blur-sm items-center px-4 sm:pr-6 pt-6 mb-2"> | ||||
|         <div className="flex flex-row justify-start items-center cursor-pointer hover:opacity-80" onClick={handleTitleClick}> | ||||
|           <img className="h-12 w-auto rounded-md mr-2" src={customizedProfile.logoUrl} alt="" /> | ||||
|           <span className="text-xl sm:text-4xl text-gray-700 dark:text-gray-200">{customizedProfile.name}</span> | ||||
|         </div> | ||||
|         <div className="flex flex-row justify-end items-center"> | ||||
|           <a | ||||
|  | @ -133,39 +115,22 @@ const Explore = () => { | |||
|         </div> | ||||
|       </div> | ||||
|       {!loadingState.isLoading && ( | ||||
|           <main className="memos-wrapper"> | ||||
|         <main className="relative flex-grow max-w-2xl w-full h-auto flex flex-col justify-start items-start px-4 sm:pr-6"> | ||||
|           <MemoFilter /> | ||||
|           {sortedMemos.map((memo) => { | ||||
|               const createdAtStr = dayjs(memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss"); | ||||
|               return ( | ||||
|                 <div className={`memo-container ${memo.pinned ? "pinned" : ""}`} key={memo.id}> | ||||
|                   {memo.pinned && <div className="corner-container"></div>} | ||||
|                   <div className="memo-header"> | ||||
|                     <span className="time-text">{createdAtStr}</span> | ||||
|                     <a className="name-text" href={`/u/${memo.creatorId}`}> | ||||
|                       @{memo.creatorName} | ||||
|                     </a> | ||||
|                   </div> | ||||
|                   <MemoContent className="memo-content" content={memo.content} onMemoContentClick={handleMemoContentClick} /> | ||||
|                   <MemoResources resourceList={memo.resourceList} /> | ||||
|                 </div> | ||||
|               ); | ||||
|             return <Memo key={`${memo.id}-${memo.createdTs}`} memo={memo} readonly={true} />; | ||||
|           })} | ||||
|           {isComplete ? ( | ||||
|             state.memos.length === 0 ? ( | ||||
|               <p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p> | ||||
|             ) : null | ||||
|           ) : ( | ||||
|               <p | ||||
|                 className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600" | ||||
|                 onClick={handleFetchMoreClick} | ||||
|               > | ||||
|             <p className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600" onClick={handleFetchMoreClick}> | ||||
|               {t("memo-list.fetch-more")} | ||||
|             </p> | ||||
|           )} | ||||
|         </main> | ||||
|       )} | ||||
|       </div> | ||||
|     </section> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -3,12 +3,13 @@ import { useTranslation } from "react-i18next"; | |||
| import { useLocation } from "react-router-dom"; | ||||
| import { useGlobalStore, useUserStore } from "../store/module"; | ||||
| import toastHelper from "../components/Toast"; | ||||
| import Sidebar from "../components/Sidebar"; | ||||
| import MemosHeader from "../components/MemosHeader"; | ||||
| import Header from "../components/Header"; | ||||
| import MemoEditor from "../components/MemoEditor"; | ||||
| import MemoFilter from "../components/MemoFilter"; | ||||
| import MemoList from "../components/MemoList"; | ||||
| import UpdateVersionBanner from "../components/UpdateVersionBanner"; | ||||
| import MobileHeader from "../components/MobileHeader"; | ||||
| import HomeSidebar from "../components/HomeSidebar"; | ||||
| import "../less/home.less"; | ||||
| 
 | ||||
| function Home() { | ||||
|  | @ -40,28 +41,16 @@ function Home() { | |||
|         <UpdateVersionBanner /> | ||||
|       </div> | ||||
|       <div className="page-container"> | ||||
|         <Sidebar /> | ||||
|         <Header /> | ||||
|         <main className="memos-wrapper"> | ||||
|           <MemosHeader /> | ||||
|           <MobileHeader /> | ||||
|           <div className="memos-editor-wrapper"> | ||||
|             {!userStore.isVisitorMode() && <MemoEditor />} | ||||
|             <MemoFilter /> | ||||
|           </div> | ||||
|           <MemoList /> | ||||
|           {userStore.isVisitorMode() && ( | ||||
|             <div className="addition-btn-container"> | ||||
|               {user ? ( | ||||
|                 <button className="btn" onClick={() => (window.location.href = "/")}> | ||||
|                   <span className="icon">🏠</span> {t("common.back-to-home")} | ||||
|                 </button> | ||||
|               ) : ( | ||||
|                 <button className="btn" onClick={() => (window.location.href = "/auth")}> | ||||
|                   <span className="icon">👉</span> {t("common.sign-in")} | ||||
|                 </button> | ||||
|               )} | ||||
|             </div> | ||||
|           )} | ||||
|         </main> | ||||
|         {!userStore.isVisitorMode() && <HomeSidebar />} | ||||
|       </div> | ||||
|     </section> | ||||
|   ); | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue