From 7e025fb449953f2f8824a459f7e637d1c5988c91 Mon Sep 17 00:00:00 2001 From: bokhonglo Date: Wed, 20 May 2026 18:02:10 +0700 Subject: [PATCH] feat: implement resizable public wiki sidebar with improved content fetching, map zoom limits, and layout adjustments --- src/app/page.tsx | 77 +++++++++++++-- src/app/user/about-us/page.tsx | 4 +- src/app/user/layout.tsx | 2 +- src/uhm/components/map/mapUtils.ts | 6 +- src/uhm/components/map/useMapSync.ts | 10 +- src/uhm/components/ui/TimelineBar.tsx | 9 +- src/uhm/components/wiki/PublicWikiSidebar.tsx | 95 ++++++++++++++++++- src/uhm/lib/map/constants.ts | 2 +- .../lib/map/styles/shared/styleBuilders.ts | 15 ++- 9 files changed, 191 insertions(+), 29 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index ca3f0e7..35500d4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,7 @@ import TimelineBar from "@/uhm/components/ui/TimelineBar"; import { fetchEntities, type Entity } from "@/uhm/api/entities"; import { fetchGeometriesByBBox } from "@/uhm/api/geometries"; import { ApiError } from "@/uhm/api/http"; -import { fetchWikiBySlug, searchWikisByTitle, type Wiki } from "@/uhm/api/wikis"; +import { fetchWikiBySlug, getContentByVersionWikiId, searchWikisByTitle, type Wiki } from "@/uhm/api/wikis"; import { BACKGROUND_LAYER_OPTIONS, type BackgroundLayerId, @@ -88,6 +88,34 @@ export default function Page() { const [linkEntityPopup, setLinkEntityPopup] = useState(null); const [entityFocusToken, setEntityFocusToken] = useState(0); + const [sidebarWidth, setSidebarWidth] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("public-wiki-sidebar-width"); + if (saved) { + const parsed = parseInt(saved, 10); + if (!isNaN(parsed) && parsed >= 320 && parsed <= 800) { + return parsed; + } + } + } + return 420; + }); + const [isLargeScreen, setIsLargeScreen] = useState(false); + + useEffect(() => { + if (typeof window === "undefined") return; + const handleResize = () => { + setIsLargeScreen(window.innerWidth >= 1024); + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + const maxDragWidth = typeof window !== "undefined" + ? Math.min(800, window.innerWidth - 340) + : 800; + const timelineFetchRequestRef = useRef(0); const hoverHideTimerRef = useRef(null); const hoverPopupHoveredRef = useRef(false); @@ -339,7 +367,7 @@ export default function Page() { selectEntity(onlyEntityId, { sourceFeatureId: selectedFeatureIds[0], - focusMap: false, + focusMap: true, selectGeometry: false, }); }, [activeEntityId, relations.geometryEntityIds, selectEntity, selectedFeatureIds]); @@ -386,6 +414,8 @@ export default function Page() { }; }, [linkEntityPopup]); + const cachedWiki = activeWikiSlug ? (wikiCache[activeWikiSlug] as Wiki & { __fetched?: boolean }) : undefined; + useEffect(() => { if (!activeWikiSlug) { setIsActiveWikiLoading(false); @@ -393,10 +423,13 @@ export default function Page() { return; } - const cached = wikiCache[activeWikiSlug] || relations.wikiBySlug[activeWikiSlug] || null; - if (cached?.content) { + if (cachedWiki && (cachedWiki.__fetched || cachedWiki.id === "__not_found__")) { setIsActiveWikiLoading(false); - setActiveWikiError(null); + if (cachedWiki.id === "__not_found__") { + setActiveWikiError("Không tìm thấy wiki cho entity đã chọn."); + } else { + setActiveWikiError(null); + } return; } @@ -407,9 +440,30 @@ export default function Page() { try { const row = await fetchWikiBySlug(activeWikiSlug); if (disposed) return; + if (row) { - setWikiCache((prev) => ({ ...prev, [activeWikiSlug]: row })); + let versionContent = row.content; + try { + if (row.content_sample?.[0]?.id) { + const res = await getContentByVersionWikiId(row.content_sample[0].id); + if (res?.data?.content) { + versionContent = res.data.content; + } + } + } catch (err) { + console.error("Failed to fetch version content:", err); + } + + if (disposed) return; + setWikiCache((prev) => ({ + ...prev, + [activeWikiSlug]: { ...row, content: versionContent, __fetched: true } as any, + })); } else { + setWikiCache((prev) => ({ + ...prev, + [activeWikiSlug]: { id: "__not_found__", project_id: "" }, + })); setActiveWikiError("Không tìm thấy wiki cho entity đã chọn."); } } catch (err) { @@ -423,7 +477,7 @@ export default function Page() { return () => { disposed = true; }; - }, [activeWikiSlug, relations.wikiBySlug, wikiCache]); + }, [activeWikiSlug, cachedWiki]); const handleWikiLinkRequest = useCallback(async ({ slug, rect }: { slug: string; rect: DOMRect }) => { const linkedEntityIds = relations.wikiEntityIdsBySlug[slug] || []; @@ -482,7 +536,7 @@ export default function Page() { highlightFeatures={activeEntityGeometries} focusFeatureCollection={activeEntityGeometries} focusRequestKey={entityFocusToken} - focusPadding={activeEntityId ? { top: 84, right: 500, bottom: 116, left: 84 } : { top: 84, right: 84, bottom: 116, left: 84 }} + focusPadding={activeEntityId && isLargeScreen ? { top: 84, right: sidebarWidth + 80, bottom: 116, left: 84 } : { top: 84, right: 84, bottom: 116, left: 84 }} /> ) : (
@@ -496,6 +550,7 @@ export default function Page() { isLoading={isTimelineLoading} disabled={false} statusText={timelineStatus} + style={activeEntityId && isLargeScreen ? { right: `${sidebarWidth + 32}px` } : undefined} />
@@ -632,7 +687,7 @@ export default function Page() { ) : null} {activeEntity ? ( -