feat: implement resizable public wiki sidebar with improved content fetching, map zoom limits, and layout adjustments
Build and Release / release (push) Successful in 36s

This commit is contained in:
2026-05-20 18:02:10 +07:00
parent 8c0bff8082
commit 7e025fb449
9 changed files with 191 additions and 29 deletions
+68 -9
View File
@@ -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<LinkEntityPopupState | null>(null);
const [entityFocusToken, setEntityFocusToken] = useState(0);
const [sidebarWidth, setSidebarWidth] = useState<number>(() => {
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<number | null>(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 }}
/>
) : (
<div className="h-screen w-full bg-[#0b1220]" />
@@ -496,6 +550,7 @@ export default function Page() {
isLoading={isTimelineLoading}
disabled={false}
statusText={timelineStatus}
style={activeEntityId && isLargeScreen ? { right: `${sidebarWidth + 32}px` } : undefined}
/>
<div className="absolute left-4 top-4 z-20 w-[280px] max-w-[calc(100vw-2rem)] overflow-hidden rounded-xl border border-white/10 bg-slate-950/92 shadow-xl backdrop-blur">
@@ -632,7 +687,7 @@ export default function Page() {
) : null}
{activeEntity ? (
<aside className="absolute bottom-4 right-4 top-4 z-20 w-[420px] max-w-[calc(100vw-2rem)]">
<aside className="absolute bottom-4 right-4 top-4 z-20 max-w-[calc(100vw-2rem)]">
<PublicWikiSidebar
entity={activeEntity}
wiki={activeWiki}
@@ -643,8 +698,12 @@ export default function Page() {
setActiveWikiSlug(null);
setActiveWikiError(null);
setLinkEntityPopup(null);
setSelectedFeatureIds([]);
}}
onWikiLinkRequest={handleWikiLinkRequest}
sidebarWidth={sidebarWidth}
onSidebarWidthChange={setSidebarWidth}
maxDragWidth={maxDragWidth}
/>
</aside>
) : null}
+2 -2
View File
@@ -61,13 +61,13 @@ export default function LandingPage() {
<div className="fixed inset-0 bg-gradient-to-b from-[#FDFBF7]/80 via-[#FDFBF7]/70 to-[#FDFBF7]/90 -z-10 pointer-events-none"></div>
{/* --- HEADER NAVBAR --- */}
<header className="fixed top-0 w-full px-6 py-4 flex justify-between items-center backdrop-blur-sm bg-[#FDFBF7]/70 z-50 border-b border-[#A88B4C]/20">
<header className="sticky top-0 w-full px-6 py-4 flex justify-between items-center backdrop-blur-sm bg-[#FDFBF7]/70 z-40 border-b border-[#A88B4C]/20">
<div className="text-xl font-bold tracking-widest text-[#2D3A3A] uppercase">
<span className="text-[#A88B4C]">Geo</span>History
</div>
</header>
<main className="max-w-6xl mx-auto px-6 pt-32 pb-24 flex flex-col gap-32 w-full relative">
<main className="max-w-6xl mx-auto px-6 pt-8 pb-24 flex flex-col gap-32 w-full relative">
{/* --- PHẦN 1: GIỚI THIỆU TỔNG QUAN --- */}
<section className="min-h-[70vh] flex flex-col justify-center relative">
<h1 className="text-5xl md:text-7xl font-black leading-tight tracking-tight mb-6">
+1 -1
View File
@@ -34,7 +34,7 @@ export default function AdminLayout({
? "ml-0"
: isExpanded || isHovered
? "lg:ml-[290px]"
: "lg:ml-[0px]";
: "lg:ml-[88px]";
return (
<div className="min-h-screen xl:flex">