fix feedback bug
This commit is contained in:
@@ -63,6 +63,12 @@ export default function LandingPage() {
|
||||
<div className="text-xl font-bold tracking-widest text-[#2D3A3A] uppercase">
|
||||
<span className="text-[#A88B4C]">Geo</span>History
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-xl border border-[#A88B4C]/30 bg-[#FDFBF7]/80 px-4 py-2 text-sm font-bold text-[#2D3A3A] transition hover:bg-[#A88B4C] hover:text-white"
|
||||
>
|
||||
Quay lại bản đồ
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<main className="max-w-6xl mx-auto px-6 pt-8 pb-24 flex flex-col gap-32 w-full relative">
|
||||
@@ -94,6 +100,12 @@ export default function LandingPage() {
|
||||
>
|
||||
Khám phá sứ mệnh
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="px-8 py-4 border border-[#A88B4C]/30 bg-[#FDFBF7]/80 text-[#2D3A3A] font-bold rounded-xl hover:bg-[#2D3A3A] hover:text-white"
|
||||
>
|
||||
Về bản đồ
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -2696,7 +2696,7 @@ function EditorPageContent() {
|
||||
onImageOverlayChange={setImageOverlay}
|
||||
onBindGeometries={handleBindGeometries}
|
||||
localFeatureIds={localFeatureIds}
|
||||
showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible}
|
||||
showViewportControls={!isAnyPreviewMode}
|
||||
isPreviewMode={isAnyPreviewMode}
|
||||
onEnterPreview={handleEnterPreviewClick}
|
||||
onExitPreview={isReplayPreviewMode ? exitReplayPreview : exitViewerPreview}
|
||||
|
||||
+21
-3
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
type GuideSection = {
|
||||
id: string;
|
||||
@@ -139,6 +139,18 @@ const troubleshooting = [
|
||||
export default function Page() {
|
||||
const [openSection, setOpenSection] = useState<string>('start');
|
||||
const [openTrouble, setOpenTrouble] = useState<number | null>(0);
|
||||
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
const handleGuideNavClick = (sectionId: string) => {
|
||||
setOpenSection(sectionId);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
sectionRefs.current[sectionId]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-50 text-slate-950">
|
||||
@@ -169,7 +181,7 @@ export default function Page() {
|
||||
<button
|
||||
key={section.id}
|
||||
type="button"
|
||||
onClick={() => setOpenSection(section.id)}
|
||||
onClick={() => handleGuideNavClick(section.id)}
|
||||
className={`rounded-md px-3 py-2 text-left text-sm font-medium transition ${
|
||||
openSection === section.id
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
@@ -202,7 +214,13 @@ export default function Page() {
|
||||
{guideSections.map((section) => {
|
||||
const isOpen = openSection === section.id;
|
||||
return (
|
||||
<div key={section.id} className="border-b border-slate-200 last:border-b-0">
|
||||
<div
|
||||
key={section.id}
|
||||
ref={(element) => {
|
||||
sectionRefs.current[section.id] = element;
|
||||
}}
|
||||
className="scroll-mt-6 border-b border-slate-200 last:border-b-0"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenSection(isOpen ? '' : section.id)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { MapHoverPopupContent } from "@/uhm/components/map/useMapHoverPopup
|
||||
import PresentPlaceSearch, { type HistoricalGeometryFocusPayload, type PresentPlaceSelection } from "@/uhm/components/editor/PresentPlaceSearch";
|
||||
import ReplayPreviewOverlay from "@/uhm/components/editor/ReplayPreviewOverlay";
|
||||
import ReplayPreviewLayerPanel from "@/uhm/components/editor/ReplayPreviewLayerPanel";
|
||||
import { PublicMapZoomPanel } from "@/uhm/components/preview/PublicPreviewClientPage";
|
||||
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
|
||||
import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
||||
import RelatedEntityPopup from "./RelatedEntityPopup";
|
||||
@@ -82,6 +83,8 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
||||
onGeometryVisibilityChange,
|
||||
activeReplay,
|
||||
autoplayMode = null,
|
||||
viewMode = "local",
|
||||
onViewModeChange,
|
||||
|
||||
replayPreview,
|
||||
mapHandleRef,
|
||||
@@ -663,6 +666,8 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
||||
right: (isReplayPreviewRightPanelOpen && isLargeScreen) ? previewSidebarWidth + 32 : 18,
|
||||
zIndex: 18,
|
||||
display: "flex",
|
||||
flexWrap: "wrap" as const,
|
||||
gap: "10px",
|
||||
alignItems: "flex-start",
|
||||
pointerEvents: "none" as const,
|
||||
maxWidth: "calc(100vw - 102px)",
|
||||
@@ -717,6 +722,24 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
||||
width: searchBarWidth,
|
||||
}}
|
||||
/>
|
||||
{isLargeScreen && (!isReplayPreviewMode || replayPreview.zoomPanelVisible) ? (
|
||||
<>
|
||||
<PublicMapZoomPanel
|
||||
mapHandleRef={mapHandleRef || { current: null }}
|
||||
onPlayPreviewReplay={
|
||||
!isReplayPreviewMode && currentActiveReplay
|
||||
? () => handlePlaySelectedReplay(currentActiveReplay)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<EditorPreviewModePanel
|
||||
isPreviewMode={isReplayPreviewMode}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={onViewModeChange}
|
||||
onExitPreview={onExitPreview}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{isReplayPreviewMode ? (
|
||||
@@ -887,6 +910,82 @@ PreviewLayout.displayName = "PreviewLayout";
|
||||
|
||||
export default PreviewLayout;
|
||||
|
||||
function EditorPreviewModePanel({
|
||||
isPreviewMode,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
onExitPreview,
|
||||
}: {
|
||||
isPreviewMode: boolean;
|
||||
viewMode: "local" | "global";
|
||||
onViewModeChange?: (mode: "local" | "global") => void;
|
||||
onExitPreview: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
flexShrink: 0,
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: 50,
|
||||
padding: "8px 10px",
|
||||
color: "#f8fafc",
|
||||
background: "linear-gradient(135deg, rgba(30, 30, 30, 0.72) 0%, rgba(20, 20, 20, 0.85) 100%)",
|
||||
boxShadow: "0 10px 30px -10px rgba(0, 0, 0, 0.5), inset 0 1px 1px 0 rgba(255, 255, 255, 0.05)",
|
||||
backdropFilter: "blur(8px)",
|
||||
WebkitBackdropFilter: "blur(8px)",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{onViewModeChange ? (
|
||||
<div style={{ display: "flex", gap: 2, borderRadius: 999, padding: 2, background: "rgba(255, 255, 255, 0.08)", border: "1px solid rgba(255, 255, 255, 0.15)" }}>
|
||||
{(["local", "global"] as const).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
onClick={() => onViewModeChange(mode)}
|
||||
style={{
|
||||
border: 0,
|
||||
borderRadius: 999,
|
||||
padding: "4px 10px",
|
||||
background: viewMode === mode ? "#2563eb" : "transparent",
|
||||
color: viewMode === mode ? "#ffffff" : "#94a3b8",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{mode === "local" ? "CỤC BỘ" : "TOÀN CỤC"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onExitPreview}
|
||||
style={{
|
||||
height: 28,
|
||||
minWidth: 76,
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
background: isPreviewMode ? "rgba(51, 65, 85, 0.6)" : "rgba(16, 185, 129, 0.25)",
|
||||
color: isPreviewMode ? "#ffffff" : "#34d399",
|
||||
cursor: "pointer",
|
||||
padding: "0 12px",
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
aria-label="Thoát xem trước"
|
||||
title="Thoát xem trước"
|
||||
>
|
||||
Trình sửa
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Helper functions
|
||||
// ==========================================
|
||||
|
||||
@@ -1009,7 +1009,7 @@ export default function PublicPreviewClientPage({
|
||||
);
|
||||
}
|
||||
|
||||
function PublicMapZoomPanel({
|
||||
export function PublicMapZoomPanel({
|
||||
mapHandleRef,
|
||||
onPlayPreviewReplay,
|
||||
onResumePreviewReplay,
|
||||
|
||||
@@ -161,6 +161,10 @@ function prepareWikiHtml(inputHtml: string): { html: string; toc: TocItem[] } {
|
||||
return { html: doc.body.innerHTML, toc };
|
||||
}
|
||||
|
||||
function getWikiContentScrollContainer(root: HTMLElement | null): HTMLElement | null {
|
||||
return root?.closest<HTMLElement>(".uhm-public-wiki-sidebar-content") ?? null;
|
||||
}
|
||||
|
||||
function PublicWikiSidebar({
|
||||
entity,
|
||||
wiki,
|
||||
@@ -249,7 +253,6 @@ function PublicWikiSidebar({
|
||||
|
||||
|
||||
|
||||
const [activeHeadingId, setActiveHeadingId] = useState<string | null>(null);
|
||||
const processedWiki = useMemo(() => {
|
||||
if (!wiki) return { html: "", toc: [] as TocItem[] };
|
||||
|
||||
@@ -263,59 +266,13 @@ function PublicWikiSidebar({
|
||||
}, [wiki]);
|
||||
const renderHtml = processedWiki.html;
|
||||
const toc = processedWiki.toc;
|
||||
const effectiveActiveHeadingId = toc.some((item) => item.id === activeHeadingId)
|
||||
? activeHeadingId
|
||||
: (toc[0]?.id ?? null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const firstHeadingId = toc[0]?.id ?? null;
|
||||
setActiveHeadingId(firstHeadingId);
|
||||
|
||||
const scrollContainer = contentRootRef.current?.parentElement;
|
||||
const scrollContainer = getWikiContentScrollContainer(contentRootRef.current);
|
||||
scrollContainer?.scrollTo({ top: 0, behavior: "auto" });
|
||||
tocContainerRef.current?.scrollTo({ left: 0, behavior: "auto" });
|
||||
}, [wiki?.id, wiki?.slug, renderHtml, toc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toc.length) return;
|
||||
const root = contentRootRef.current;
|
||||
if (!root) return;
|
||||
|
||||
const headings = toc
|
||||
.map((item) => root.querySelector<HTMLElement>(`#${CSS.escape(item.id)}`))
|
||||
.filter((item): item is HTMLElement => Boolean(item));
|
||||
if (!headings.length) return;
|
||||
|
||||
const scrollContainer = root.parentElement;
|
||||
const updateActiveHeading = () => {
|
||||
const containerRect = scrollContainer?.getBoundingClientRect();
|
||||
const topBoundary = (containerRect?.top ?? 0) + (containerRect?.height ?? window.innerHeight) * 0.18;
|
||||
const bottomBoundary = (containerRect?.top ?? 0) + (containerRect?.height ?? window.innerHeight) * 0.82;
|
||||
const visibleHeadings = headings
|
||||
.map((heading) => ({ heading, rect: heading.getBoundingClientRect() }))
|
||||
.filter(({ rect }) => rect.bottom >= topBoundary && rect.top <= bottomBoundary)
|
||||
.sort((a, b) => {
|
||||
const aDistance = Math.abs(a.rect.top - topBoundary);
|
||||
const bDistance = Math.abs(b.rect.top - topBoundary);
|
||||
return aDistance - bDistance;
|
||||
});
|
||||
const nextHeading = visibleHeadings[0]?.heading || headings[0];
|
||||
if (nextHeading?.id) setActiveHeadingId(nextHeading.id);
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
updateActiveHeading,
|
||||
{ root: scrollContainer || null, rootMargin: "-18% 0px -70% 0px", threshold: [0, 1] }
|
||||
);
|
||||
|
||||
for (const heading of headings) observer.observe(heading);
|
||||
scrollContainer?.addEventListener("scroll", updateActiveHeading, { passive: true });
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
scrollContainer?.removeEventListener("scroll", updateActiveHeading);
|
||||
};
|
||||
}, [toc]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = tocContainerRef.current;
|
||||
if (!container) return;
|
||||
@@ -621,18 +578,16 @@ function PublicWikiSidebar({
|
||||
}}
|
||||
>
|
||||
{toc.slice(0, 8).map((item) => {
|
||||
const isActive = effectiveActiveHeadingId === item.id;
|
||||
return (
|
||||
<a
|
||||
key={item.id}
|
||||
href={`#${item.id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setActiveHeadingId(item.id);
|
||||
const root = contentRootRef.current;
|
||||
if (root) {
|
||||
const targetElement = root.querySelector(`#${CSS.escape(item.id)}`) as HTMLElement | null;
|
||||
const scrollContainer = root.parentElement;
|
||||
const scrollContainer = getWikiContentScrollContainer(root);
|
||||
if (targetElement && scrollContainer) {
|
||||
const containerTop = scrollContainer.getBoundingClientRect().top;
|
||||
const targetTop = targetElement.getBoundingClientRect().top;
|
||||
@@ -652,15 +607,11 @@ function PublicWikiSidebar({
|
||||
fontWeight: 650,
|
||||
textDecoration: "none",
|
||||
transition: "all 0.2s",
|
||||
background: isActive
|
||||
? "rgba(56, 189, 248, 0.15)"
|
||||
: "rgba(30, 41, 59, 0.4)",
|
||||
color: isActive ? "#38bdf8" : "#94a3b8",
|
||||
border: isActive
|
||||
? "1px solid rgba(56, 189, 248, 0.3)"
|
||||
: "1px solid rgba(148, 163, 184, 0.1)",
|
||||
background: "rgba(30, 41, 59, 0.4)",
|
||||
color: "#94a3b8",
|
||||
border: "1px solid rgba(148, 163, 184, 0.1)",
|
||||
}}
|
||||
className={isActive ? "" : "hover:bg-slate-700/40 hover:text-slate-200"}
|
||||
className="hover:bg-slate-700/40 hover:text-slate-200"
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user