diff --git a/app/(home)/mobile/ButtonTools.tsx b/app/(home)/mobile/ButtonTools.tsx index 16430e1..cf6b801 100644 --- a/app/(home)/mobile/ButtonTools.tsx +++ b/app/(home)/mobile/ButtonTools.tsx @@ -6,57 +6,41 @@ type Props = { }; const ButtonTools = ({ switchCallback, sigListCallback }: Props) => { - const [buttonStyle, setButtonStyle] = useState("left-0"); - const [leftStyle, setLeftStyle] = useState( - "w-[5rem] text-center duration-500 relative text-white h-[2.5rem]", - ); - const [rightStyle, setRightStyle] = useState( - "w-[5rem] text-center duration-500 relative text-md-dark-green h-[2.5rem]", - ); + const [activeTab, setActiveTab] = useState<"latest" | "top">("latest"); return ( -
-
+
+ {/* Toggle Button */} +
+ className={`absolute w-20 h-10 bg-md-dark-green rounded-full transition-all duration-500 ${activeTab === "latest" ? "left-0" : "left-20" + }`} + />
+ + {/* SIGs Button */} +
+ + {/* SIG Grid */} +
+ {isLoading ? ( +
+
+
+ ) : ( +
+ {sigs.map((item: any) => { + if (item._id !== "652d60b842cdf6a660c2b778") { + return ( + + ); + } + })} +
+ )} +
+ + {/* Footer */} +
+

+ 選擇一個 SIG 來探索更多內容 +

); }; -export default Information; + +export default SigList; \ No newline at end of file diff --git a/app/(home)/mobile/Threads.module.scss b/app/(home)/mobile/Threads.module.scss deleted file mode 100644 index 325634f..0000000 --- a/app/(home)/mobile/Threads.module.scss +++ /dev/null @@ -1,23 +0,0 @@ -@use "@/app/styles/variables.scss"; - -.messagePage { - font-size: 3rem; - height: 100%; - width: 100%; - user-select: none; - display: flex; -} - -.threadWrap { - width: 100dvw; - height: 100%; - - padding: variables.$mobile-headerbar-height variables.$mobile-border-margin variables.$mobile-toolbar-height variables.$mobile-border-margin; - - position: relative; - overflow-y: auto; - - &::-webkit-scrollbar { - display: none; - } -} \ No newline at end of file diff --git a/app/(home)/mobile/ThreadsList.tsx b/app/(home)/mobile/ThreadsList.tsx index 5e4c521..ffbd19d 100644 --- a/app/(home)/mobile/ThreadsList.tsx +++ b/app/(home)/mobile/ThreadsList.tsx @@ -1,29 +1,27 @@ "use client"; import { useState } from "react"; -//Components +// Components import { InfinityThreadsList, ThreadsListSkeleton, } from "@/components/Threads/mobile/ThreadsList"; import SigList from "./SigList"; - -// Styles -import styles from "./Threads.module.scss"; +import SwitchButton from "./ButtonTools"; // Interfaces import { useAllPost, useSigPost } from "@/utils/usePost"; -import SwitchButton from "./ButtonTools"; const ThreadsList = () => { const [showList, setShowList] = useState(false); - const [dataType, setDataType] = useState("latest"); - const pageSize = 15; + const pageSize = 10; + const { data, fetchNextPage, isFetchingNextPage, isLoading } = useAllPost({ pageSize, sort: dataType, }); + const { data: announcementData } = useSigPost("652d60b842cdf6a660c2b778", { pageSize: 1, sort: "latest", @@ -38,12 +36,14 @@ const ThreadsList = () => { } return ( -
- {showList ? : <>} +
+ {showList && } + + /> + {isLoading ? ( ) : ( @@ -59,4 +59,4 @@ const ThreadsList = () => { ); }; -export default ThreadsList; +export default ThreadsList; \ No newline at end of file diff --git a/app/new/page.tsx b/app/new/page.tsx index 1793591..bd22b0d 100644 --- a/app/new/page.tsx +++ b/app/new/page.tsx @@ -1,31 +1,25 @@ "use client"; -import { type ChangeEvent, useEffect, useState } from "react"; +import { type ChangeEvent, useEffect, useState, useRef } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import Swal from "sweetalert2"; // Desktop Components import PostEditorDesktop from "@/components/PostEditor/desktop/PostEditor"; - // Mobile Components import PostEditorMobile from "@/components/PostEditor/mobile/PostEditor"; - // Types import type { TPostAPI } from "../../components/PostEditor/types/postAPI"; - // Configs import { alertMessageConfigs } from "../../components/PostEditor/config/alertMessages"; import { markdownGuide } from "../../components/PostEditor/config/markdownGuide"; - // APIs Request Function import { postAPI } from "./(new)/apis/postAPI"; - // Utils import useIsMobile from "@/utils/useIsMobile"; import assert from "assert"; import { useUserAccount } from "@/utils/useUserAccount"; - // Modules import { imageUpload } from "@/modules/imageUploadAPI"; @@ -34,7 +28,10 @@ export default function NewPostPage() { const { token } = useUserAccount(); const route = useRouter(); const isMobile = useIsMobile(); - // Form data states + + // ✅ 使用 ref 追蹤是否已經初始化,避免重複清除 + const hasLoadedFromStorage = useRef(false); + const [postButtonDisable, setPostButtonDisable] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [data, setPostData] = useState({ @@ -55,41 +52,55 @@ export default function NewPostPage() { ); } + // ✅ 修復:只在真正有內容變化時才保存 useEffect(() => { - if (!isInitialized) return; // 初始化前不執行 + if (!isInitialized || !hasLoadedFromStorage.current) return; - if (data.title !== "" || data.sig !== "" || data.hashtag?.length !== 0) { + const hasContent = + data.title !== "" || + data.sig !== "" || + (data.hashtag && data.hashtag.length > 0) || + (data.content !== markdownGuide && data.content !== ""); + + if (hasContent) { localStorage.setItem("postData", JSON.stringify(data)); - } else { - localStorage.removeItem("postData"); } + // ⚠️ 不要在這裡刪除 postData,讓用戶手動 discard }, [data, isInitialized]); + // ✅ 修復:確保只在組件首次掛載時讀取一次 useEffect(() => { + if (hasLoadedFromStorage.current) return; + const storedContent = localStorage?.getItem("editorContent"); const storedData = localStorage?.getItem("postData"); + let loadedData: Partial = {}; + if (storedContent) { - setPostData( - (prev: TPostAPI | undefined) => - ({ - ...prev, - content: storedContent, - }) as TPostAPI, - ); + loadedData.content = storedContent; } + if (storedData) { - setPostData( - (prev: TPostAPI | undefined) => - ({ - ...prev, - ...JSON.parse(storedData), - }) as TPostAPI, - ); + try { + const parsed = JSON.parse(storedData); + loadedData = { ...loadedData, ...parsed }; + } catch (error) { + console.error("Failed to parse stored post data:", error); + } + } + + // 如果有存儲的數據,則加載 + if (Object.keys(loadedData).length > 0) { + setPostData(prev => ({ + ...prev, + ...loadedData, + })); } - setIsInitialized(true); // 標記初始化完成 - }, []); + hasLoadedFromStorage.current = true; + setIsInitialized(true); + }, []); // 空依賴,只執行一次 async function NewPostAPI() { setPostButtonDisable(true); @@ -107,16 +118,18 @@ export default function NewPostPage() { } try { - assert(data); // Check whether data was defined - assert(token !== ""); // Check whether token was loaded + assert(data); + assert(token !== ""); const res = await postAPI(data, token!); console.debug(res); if (res.status === 2000) { return Swal.fire(alertMessageConfigs.Success).then(() => { setPostButtonDisable(false); + // ✅ 成功發布後才清除 localStorage.removeItem("editorContent"); localStorage.removeItem("postData"); + hasLoadedFromStorage.current = false; // 重置標記 route.push(`/post/${res.data._id}`); }); } else if (res.status === 4001) { @@ -136,15 +149,29 @@ export default function NewPostPage() { function discard(e: ChangeEvent) { e.preventDefault(); - setPostData({ - title: "", - sig: "", - content: markdownGuide, - cover: "", - hashtag: [], + + Swal.fire({ + title: "確定要放棄嗎?", + text: "所有未保存的內容將會丟失", + icon: "warning", + showCancelButton: true, + confirmButtonText: "確定放棄", + cancelButtonText: "取消", + confirmButtonColor: "#dc0032", + }).then((result) => { + if (result.isConfirmed) { + setPostData({ + title: "", + sig: "", + content: markdownGuide, + cover: "", + hashtag: [], + }); + localStorage.removeItem("editorContent"); + localStorage.removeItem("postData"); + hasLoadedFromStorage.current = false; + } }); - localStorage.removeItem("editorContent"); - localStorage.removeItem("postData"); } async function handleFileChange(e: ChangeEvent) { @@ -161,7 +188,7 @@ export default function NewPostPage() { if (!validImageTypes.includes(file.type)) { Swal.fire( "File type not supported", - "You can only upload png, jpg, webp, tiff", + "You can only upload png, jpg, webp, tiff", "error", ); return; @@ -198,12 +225,7 @@ export default function NewPostPage() { } if (status === "loading") { - return ( - //
- //

Loading...

- //
- <> - ); + return
; } return isMobile ? ( @@ -229,4 +251,4 @@ export default function NewPostPage() { handleFileChange={handleFileChange} /> ); -} +} \ No newline at end of file diff --git a/app/styles/globals.css b/app/styles/globals.css index 7256483..6f32b86 100644 --- a/app/styles/globals.css +++ b/app/styles/globals.css @@ -4,6 +4,8 @@ /* 自定義顏色 */ --color-md-light-green: #009BB0; --color-md-dark-green: #002024; + --color-thread-bg: rgba(255, 255, 255, 0.6); + --color-hashtag-bg: #5FCDF5; /* 自定義背景圖像 */ --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops)); @@ -43,6 +45,7 @@ body { background: linear-gradient(127deg, rgba(255, 217, 103, 1) 0%, rgb(205, 205, 205) 25%, rgba(116, 197, 233, 1) 50%, rgba(13, 154, 217, 1) 75%, rgba(81, 147, 224, 1) 100%); } +/* Custom Scrollbar Styles */ ::-webkit-scrollbar { width: 10px; margin-left: 5px; @@ -58,20 +61,21 @@ body { background: #004c64; } -.remove-scrollbar::-webkit-scrollbar { - display: none; -} - -.small-scrollbar::-webkit-scrollbar { +.custom-scrollbar::-webkit-scrollbar { width: 5px; - margin-left: 2px; } -.small-scrollbar::-webkit-scrollbar-track { +.custom-scrollbar::-webkit-scrollbar-track { border-radius: 5px; background: rgba(0, 76, 100, 0.29); } +.custom-scrollbar::-webkit-scrollbar-thumb { + border-radius: 5px; + background: #004c64; +} + +/* Editor specific styles */ .editor-wrapper>div { border: none !important; height: 100%; @@ -85,7 +89,7 @@ body { padding: 0 15px; } - +/* Animations */ @keyframes fadeIn { 0% { opacity: 0; @@ -96,28 +100,57 @@ body { } } -@layer utilities { - - /* Custom animation */ - @keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } +@keyframes shimmer { + 100% { + transform: translateX(100%); } +} - /* Scrollbar hide utility */ +@layer utilities { + + /* Scrollbar utilities */ .scrollbar-hide::-webkit-scrollbar { width: 0; height: 0; + display: none; + } + + /* Scrollbar Firefox */ + .scrollbar-hide { + scrollbar-width: none; + } + + /* Line clamp utilities */ + .line-clamp-1 { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + /* Custom backdrop blur */ + .backdrop-blur-custom { + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); } - /* If you're using specific background colors from variables */ + /* Panel background */ .panel-bg { - background-color: #ffffff; - /* Replace with your actual panel background color */ + background-color: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(10px); } } \ No newline at end of file diff --git a/components/Threads/mobile/Skeleton.module.scss b/components/Threads/mobile/Skeleton.module.scss deleted file mode 100644 index d387a20..0000000 --- a/components/Threads/mobile/Skeleton.module.scss +++ /dev/null @@ -1,184 +0,0 @@ -@use "@/app/styles/variables.scss"; - -.thread { - --thread-height: 8rem; - background: variables.$panel-background-color; - border-radius: variables.$mobile-border-radius; - max-height: var(--thread-height); - height: var(--thread-height); - margin-bottom: 0.5rem; - - animation-name: breath; - animation-duration: 0.8s; - animation-timing-function: linear; - animation-iteration-count: infinite; - animation-direction: alternate; - - @keyframes breath { - from { - background-color: rgba(255, 255, 255, 0.5); - } - to { - background-color: rgba(255, 255, 255, 0.2); - } - } -} - -.preview { - position: relative; - padding: 1rem; - box-sizing: border-box; - height: var(--thread-height); - width: 100%; - overflow: hidden; -} - -.info { - font-size: 0.75rem; - width: auto; - display: flex; - flex-direction: initial; - margin-bottom: 0.3rem; - - .user { - width: 3rem; - height: 0.75rem; - position: relative; - background-color: variables.$skeleton-background-color; - overflow: hidden; - border-radius: 0.3rem; - - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - transform: translateX(-100%); - background-image: linear-gradient(90deg, - rgba(#fff, 0) 0, - rgba(#fff, 0.2) 20%, - rgba(#fff, 0.5) 60%, - rgba(#fff, 0)); - animation: shimmer 2s infinite; - content: ''; - } - } - - > span { - padding: 0 0.1rem; - } - - .sig { - width: 4rem; - height: 0.75rem; - position: relative; - background-color: variables.$skeleton-background-color; - overflow: hidden; - border-radius: 0.3rem; - - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - transform: translateX(-100%); - background-image: linear-gradient(90deg, - rgba(#fff, 0) 0, - rgba(#fff, 0.2) 20%, - rgba(#fff, 0.5) 60%, - rgba(#fff, 0)); - animation: shimmer 2s infinite; - content: ''; - } - } - - @keyframes shimmer { - 100% { - transform: translateX(100%); - } - } -} - -.title { - width: 60%; - height: 1rem; - position: relative; - background-color: variables.$skeleton-background-color; - overflow: hidden; - border-radius: 0.3rem; - margin-bottom: 0.5rem; - - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - transform: translateX(-100%); - background-image: linear-gradient(90deg, - rgba(#fff, 0) 0, - rgba(#fff, 0.2) 20%, - rgba(#fff, 0.5) 60%, - rgba(#fff, 0)); - animation: shimmer 2s infinite; - content: ''; - } -} - -.content { - height: 0.9rem; - margin-bottom: 0.5rem; - position: relative; - background-color: variables.$skeleton-background-color; - overflow: hidden; - border-radius: 0.3rem; - - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - transform: translateX(-100%); - background-image: linear-gradient(90deg, - rgba(#fff, 0) 0, - rgba(#fff, 0.2) 20%, - rgba(#fff, 0.5) 60%, - rgba(#fff, 0)); - animation: shimmer 2s infinite; - content: ''; - } -} - -.hashtags { - display: flex; - gap: 0.4rem; - margin-top: 1rem; -} - -.hashtag { - width: 2.5rem; - height: 1.1rem; - position: relative; - background-color: variables.$skeleton-background-color; - overflow: hidden; - border-radius: 0.6rem; - - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - transform: translateX(-100%); - background-image: linear-gradient(90deg, - rgba(#fff, 0) 0, - rgba(#fff, 0.2) 20%, - rgba(#fff, 0.5) 60%, - rgba(#fff, 0)); - animation: shimmer 2s infinite; - content: ''; - } -} \ No newline at end of file diff --git a/components/Threads/mobile/ThreadsList.module.scss b/components/Threads/mobile/ThreadsList.module.scss deleted file mode 100644 index 91534a8..0000000 --- a/components/Threads/mobile/ThreadsList.module.scss +++ /dev/null @@ -1,163 +0,0 @@ -@use "@/app/styles/variables.scss"; - -.threads { - position: relative; - width: 100%; - border-radius: variables.$mobile-border-radius variables.$mobile-border-radius 0 0; - display: flex; - flex-direction: column; - overflow-y: auto; - scrollbar-width: none; - - > p { - width: 100%; - text-align: center; - padding-bottom: 0.5rem; - } - - &::-webkit-scrollbar { - display: none; - } -} - -.thread { - --thread-height: 8rem; - background: variables.$panel-background-color; - border-radius: variables.$mobile-border-radius; - max-height: var(--thread-height); - height: var(--thread-height); - margin-bottom: 0.5rem; -} - -.preview { - position: relative; - padding: 1rem; - box-sizing: border-box; - height: var(--thread-height); - overflow: hidden; -} - -.previewTitle { - font-size: 1rem; - line-height: 1rem; - font-weight: 500; - margin-bottom: 0.5rem; - color: black; - width: 100%; - height: 1rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -} - -.previewContent { - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - color: black; - font-size: 0.75rem; - line-height: 1.2; - min-height: calc(0.75rem * 1.2 * 1); - margin-bottom: 0.5rem; -} - -.hashtags { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin-top: 1rem; -} - -.hashtag { - padding: 0.2rem 0.5rem; - background: rgba(95, 205, 245, 1); - color: #ffffff; - border-radius: 0.6rem; - font-size: 0.65rem; - font-weight: 600; - transition: all 0.2s ease; - white-space: nowrap; - - &:hover { - background: #4ab8e0; - } -} - -.cover { - position: relative; - min-width: 25%; - border-radius: variables.$border-radius; - overflow: hidden; -} - -.more { - color: cadetblue; - font-weight: bolder; -} - -.title_bar { - display: flex; -} - -.info { - font-size: 0.75rem; - width: auto; - display: flex; - flex-wrap: wrap; - flex-direction: initial; - margin-bottom: 0.3rem; - - .user_sig { - display: flex; - flex: auto; - margin-right: 0.5rem; - - .user { - opacity: 60%; - } - - & > span { - padding: 0 0.1rem; - } - } - - .statist { - display: flex; - align-items: center; - margin-left: 0; - gap: 0.5rem; - opacity: 60%; - justify-content: flex-end; - flex: content; - - & > span { - padding: 0 0.1rem; - } - - .likes { - display: flex; - align-items: center; - gap: 0.25rem; - } - - .comments { - display: flex; - align-items: center; - gap: 0.25rem; - } - } -} - -.noPost { - height: 100%; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - - > h1 { - font-size: variables.$info-title-font-size; - } -} \ No newline at end of file diff --git a/components/Threads/mobile/ThreadsList.tsx b/components/Threads/mobile/ThreadsList.tsx index bcba546..67a3332 100644 --- a/components/Threads/mobile/ThreadsList.tsx +++ b/components/Threads/mobile/ThreadsList.tsx @@ -7,10 +7,6 @@ import type { InfiniteData, } from "@tanstack/react-query"; -// Styles -import style from "./ThreadsList.module.scss"; -import skeleton from "./Skeleton.module.scss"; - // Interfaces, Types import type { TThread } from "@/interfaces/Thread"; import type { User } from "@/interfaces/User"; @@ -26,7 +22,24 @@ import { announcementStayTime, } from "../configs/announcement"; -const Thread = ({ threadData }: { threadData: TThread }) => { +// Helper function to fix cover URL +function fixCoverUrl(cover: string) { + if (cover.startsWith("http://") || cover.startsWith("https://")) { + if (cover.includes("lazco.dev")) { + const pathPart = cover.split("lazco.dev/")[1] || ""; + return `${process.env.NEXT_PUBLIC_API_URL}/${pathPart}`; + } + return cover; + } + + if (cover.startsWith("/image/")) { + return `${process.env.NEXT_PUBLIC_API_URL}${cover}`; + } + + return `${process.env.NEXT_PUBLIC_API_URL}/image/${cover}`; +} + +const Thread = ({ threadData, priority = false }: { threadData: TThread; priority?: boolean }) => { const user = threadData.user as User; const sig = threadData.sig as Sig; const isAnnouncement = sig._id === announcementSigId; @@ -34,75 +47,105 @@ const Thread = ({ threadData }: { threadData: TThread }) => { return ( -
-
-
-

{user?.name}

- -

{sig.name}

-
-
-

- {new Date(threadData.createdAt!).toLocaleString("zh-TW").split(" ")[0]} -

-
- likes -

{threadData.likes}

+ {/* Cover Image */} + {threadData.cover && ( +
+ {threadData.title} +
+ )} + + {/* Content Container */} +
+ {/* Header: User info and Date */} + {!isAnnouncement && user && ( +
+
+ + {user?.name} + + + {sig.name} +
-
- comments -

{threadData.comments}

+
+ {new Date(threadData.createdAt!).toLocaleString("zh-TW").split(" ")[0]}
-
+ )} -
-

- {isAnnouncement && "🔔 公告 - "} - {threadData.title} -

-
+ {/* Title */} +

+ {isAnnouncement && "🔔 公告 - "} + {threadData.title} +

+ {/* Content Preview - Dynamic lines based on hashtags */}

0 + ? "line-clamp-2" + : "line-clamp-4" + }`} > {markdownToPlainText(threadData.content)}

+ {/* Hashtags - Horizontal scroll without scrollbar */} {threadData.hashtag && threadData.hashtag.length > 0 && !isAnnouncement && ( -
- {threadData.hashtag.slice(0, 3).map((tag, index) => ( - +
+ {threadData.hashtag.map((tag, index) => ( + #{tag} ))}
)} + + {/* Spacer to push footer to bottom */} +
+ + {/* Footer: Likes and Comments - No border */} + {!isAnnouncement && ( +
+
+ likes + {threadData.likes} +
+
+ comments + {threadData.comments} +
+
+ )}
); @@ -110,19 +153,30 @@ const Thread = ({ threadData }: { threadData: TThread }) => { const ThreadSkeleton = () => { return ( -
-
-
-

- -

+
+ {/* Cover Skeleton */} +
+ + {/* Content Container */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-

-
-
-
-
+
+
+
@@ -151,7 +205,8 @@ export const InfinityThreadsList = ({ const onScroll = useCallback(() => { if (postList.current) { const { scrollTop, scrollHeight, clientHeight } = postList.current; - const isNearBottom = scrollTop + clientHeight >= scrollHeight - 600; + // 距離底部 1200px 時就開始加載(更早觸發) + const isNearBottom = scrollTop + clientHeight >= scrollHeight - 1200; if (isNearBottom && !isFetchingNextPage) { fetchNextPage(); @@ -163,16 +218,25 @@ export const InfinityThreadsList = ({ const listInnerElement: HTMLElement = postList.current!; if (listInnerElement) { + // 監聽滾輪滾動(桌面) + listInnerElement.addEventListener("scroll", onScroll); + // 監聽觸控滑動(手機) listInnerElement.addEventListener("touchmove", onScroll); return () => { + listInnerElement.removeEventListener("scroll", onScroll); listInnerElement.removeEventListener("touchmove", onScroll); }; } }, [onScroll]); return data && data.pages[0].length >= 1 ? ( -
+
+ {/* Announcement Section */} {announcementData && announcementData.pages[0].length >= 1 && announcementData.pages.map((page: TThread[], index: number) => { @@ -183,25 +247,42 @@ export const InfinityThreadsList = ({ ); if (diffDays < announcementStayTime) { - return ; + return ( +
+ +
+ ); } })} - {data.pages.map((page: TThread[], index: number) => ( - - {page.map((item, index) => { - const sig = item.sig as Sig; - const isAnnouncement = sig._id === announcementSigId; - if (!isAnnouncement) { - return ; - } - })} - - ))} - {isFetchingNextPage && } + + {/* Threads Grid */} +
+ {data.pages.map((page: TThread[], pageIndex: number) => ( + + {page.map((item, itemIndex) => { + const sig = item.sig as Sig; + const isAnnouncement = sig._id === announcementSigId; + // Calculate global index for priority + const globalIndex = pageIndex * page.length + itemIndex; + const shouldPrioritize = globalIndex < 4; + + if (!isAnnouncement) { + return ; + } + })} + + ))} + {isFetchingNextPage && ( + <> + + + + )} +
) : ( -
-

No Post Yet

+
+

No Post Yet

); }; @@ -214,10 +295,12 @@ export const ThreadsListSkeleton = ({ height?: string; }) => { return ( -
- {[...Array(repeat)].map((_, index) => ( - - ))} +
+
+ {[...Array(repeat)].map((_, index) => ( + + ))} +
); -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index 6171aad..0644bce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mdsig-frontend", - "version": "2.18.0", + "version": "2.18.2", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/utils/useUserAccount/index.ts b/utils/useUserAccount/index.ts index cab8a93..4efb73a 100644 --- a/utils/useUserAccount/index.ts +++ b/utils/useUserAccount/index.ts @@ -1,6 +1,5 @@ import { useState, useEffect, useCallback } from "react"; import { useSession, signIn, signOut } from "next-auth/react"; - import type { User } from "@/interfaces/User"; const API_URL = process.env.NEXT_PUBLIC_API_URL; @@ -15,10 +14,9 @@ export function useUserAccount() { useEffect(() => { (async () => { - const userLocalStorage = localStorage.getItem("user"); const sessionLocalStorage = localStorage.getItem("session"); - // console.log("OAuth status:", OAuth); + // 優先使用 session 登入 if (sessionLocalStorage) { try { const { token, data } = await platformLoginWithSession( @@ -30,18 +28,22 @@ export function useUserAccount() { setUserData(data); setIsLogin(true); setIsLoading(false); - return; + return; // 成功登入後直接返回,不再檢查 OAuth } catch (error) { - console.error(error); - setIsLogin(false); - setIsLoading(false); - return; + console.error("Session login failed:", error); + // session 失效,清除 session 但不立即登出 + localStorage.removeItem("session"); + // 繼續檢查 OAuth } } - if (OAuth === "loading" || (!userLocalStorage && !sessionLocalStorage)) + // 如果 OAuth 正在載入,等待 + if (OAuth === "loading") { setIsLoading(true); + return; + } + // OAuth 登入成功 if (OAuth === "authenticated") { try { const accessToken = (session as any)?.accessToken; @@ -53,25 +55,39 @@ export function useUserAccount() { setIsLogin(true); setIsLoading(false); } catch (error) { - console.error(error); + console.error("OAuth login failed:", error); setIsLogin(false); + setIsLoading(false); signOut(); } - } else if (OAuth !== "loading") { - setIsLogin(false); + return; + } + + // ✅ 修復:只有在沒有 session 且 OAuth 未認證時才登出 + // 而且不要在每次 OAuth 狀態變化時都重置狀態 + if (!sessionLocalStorage && OAuth === "unauthenticated") { + // 只有在真正需要登出時才設置 + if (isLogin) { + setIsLogin(false); + } setIsLoading(false); } })(); - }, [OAuth, session]); + }, [OAuth, session]); // ⚠️ 移除 isLogin 依賴避免循環 + // ✅ 修復:更安全的清除邏輯 useEffect(() => { - if (!isLogin) { - localStorage.removeItem("token"); - localStorage.removeItem("user"); - setToken(null); - setUserData(null); + if (!isLogin && !isLoading) { + // 只在確定登出時才清除 + const sessionExists = localStorage.getItem("session"); + if (!sessionExists) { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + setToken(null); + setUserData(null); + } } - }, [isLogin]); + }, [isLogin, isLoading]); const login = useCallback(() => { signIn("google");