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">
|
<div className="text-xl font-bold tracking-widest text-[#2D3A3A] uppercase">
|
||||||
<span className="text-[#A88B4C]">Geo</span>History
|
<span className="text-[#A88B4C]">Geo</span>History
|
||||||
</div>
|
</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>
|
</header>
|
||||||
|
|
||||||
<main className="max-w-6xl mx-auto px-6 pt-8 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">
|
||||||
@@ -94,6 +100,12 @@ export default function LandingPage() {
|
|||||||
>
|
>
|
||||||
Khám phá sứ mệnh
|
Khám phá sứ mệnh
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -2696,7 +2696,7 @@ function EditorPageContent() {
|
|||||||
onImageOverlayChange={setImageOverlay}
|
onImageOverlayChange={setImageOverlay}
|
||||||
onBindGeometries={handleBindGeometries}
|
onBindGeometries={handleBindGeometries}
|
||||||
localFeatureIds={localFeatureIds}
|
localFeatureIds={localFeatureIds}
|
||||||
showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible}
|
showViewportControls={!isAnyPreviewMode}
|
||||||
isPreviewMode={isAnyPreviewMode}
|
isPreviewMode={isAnyPreviewMode}
|
||||||
onEnterPreview={handleEnterPreviewClick}
|
onEnterPreview={handleEnterPreviewClick}
|
||||||
onExitPreview={isReplayPreviewMode ? exitReplayPreview : exitViewerPreview}
|
onExitPreview={isReplayPreviewMode ? exitReplayPreview : exitViewerPreview}
|
||||||
|
|||||||
+21
-3
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
type GuideSection = {
|
type GuideSection = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -139,6 +139,18 @@ const troubleshooting = [
|
|||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [openSection, setOpenSection] = useState<string>('start');
|
const [openSection, setOpenSection] = useState<string>('start');
|
||||||
const [openTrouble, setOpenTrouble] = useState<number | null>(0);
|
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 (
|
return (
|
||||||
<main className="min-h-screen bg-slate-50 text-slate-950">
|
<main className="min-h-screen bg-slate-50 text-slate-950">
|
||||||
@@ -169,7 +181,7 @@ export default function Page() {
|
|||||||
<button
|
<button
|
||||||
key={section.id}
|
key={section.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenSection(section.id)}
|
onClick={() => handleGuideNavClick(section.id)}
|
||||||
className={`rounded-md px-3 py-2 text-left text-sm font-medium transition ${
|
className={`rounded-md px-3 py-2 text-left text-sm font-medium transition ${
|
||||||
openSection === section.id
|
openSection === section.id
|
||||||
? 'bg-blue-50 text-blue-700'
|
? 'bg-blue-50 text-blue-700'
|
||||||
@@ -202,7 +214,13 @@ export default function Page() {
|
|||||||
{guideSections.map((section) => {
|
{guideSections.map((section) => {
|
||||||
const isOpen = openSection === section.id;
|
const isOpen = openSection === section.id;
|
||||||
return (
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenSection(isOpen ? '' : section.id)}
|
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 PresentPlaceSearch, { type HistoricalGeometryFocusPayload, type PresentPlaceSelection } from "@/uhm/components/editor/PresentPlaceSearch";
|
||||||
import ReplayPreviewOverlay from "@/uhm/components/editor/ReplayPreviewOverlay";
|
import ReplayPreviewOverlay from "@/uhm/components/editor/ReplayPreviewOverlay";
|
||||||
import ReplayPreviewLayerPanel from "@/uhm/components/editor/ReplayPreviewLayerPanel";
|
import ReplayPreviewLayerPanel from "@/uhm/components/editor/ReplayPreviewLayerPanel";
|
||||||
|
import { PublicMapZoomPanel } from "@/uhm/components/preview/PublicPreviewClientPage";
|
||||||
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
|
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
|
||||||
import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
||||||
import RelatedEntityPopup from "./RelatedEntityPopup";
|
import RelatedEntityPopup from "./RelatedEntityPopup";
|
||||||
@@ -82,6 +83,8 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
|||||||
onGeometryVisibilityChange,
|
onGeometryVisibilityChange,
|
||||||
activeReplay,
|
activeReplay,
|
||||||
autoplayMode = null,
|
autoplayMode = null,
|
||||||
|
viewMode = "local",
|
||||||
|
onViewModeChange,
|
||||||
|
|
||||||
replayPreview,
|
replayPreview,
|
||||||
mapHandleRef,
|
mapHandleRef,
|
||||||
@@ -663,6 +666,8 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
|||||||
right: (isReplayPreviewRightPanelOpen && isLargeScreen) ? previewSidebarWidth + 32 : 18,
|
right: (isReplayPreviewRightPanelOpen && isLargeScreen) ? previewSidebarWidth + 32 : 18,
|
||||||
zIndex: 18,
|
zIndex: 18,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
flexWrap: "wrap" as const,
|
||||||
|
gap: "10px",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
pointerEvents: "none" as const,
|
pointerEvents: "none" as const,
|
||||||
maxWidth: "calc(100vw - 102px)",
|
maxWidth: "calc(100vw - 102px)",
|
||||||
@@ -717,6 +722,24 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
|||||||
width: searchBarWidth,
|
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>
|
</div>
|
||||||
|
|
||||||
{isReplayPreviewMode ? (
|
{isReplayPreviewMode ? (
|
||||||
@@ -887,6 +910,82 @@ PreviewLayout.displayName = "PreviewLayout";
|
|||||||
|
|
||||||
export default 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
|
// Helper functions
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -1009,7 +1009,7 @@ export default function PublicPreviewClientPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PublicMapZoomPanel({
|
export function PublicMapZoomPanel({
|
||||||
mapHandleRef,
|
mapHandleRef,
|
||||||
onPlayPreviewReplay,
|
onPlayPreviewReplay,
|
||||||
onResumePreviewReplay,
|
onResumePreviewReplay,
|
||||||
|
|||||||
@@ -161,6 +161,10 @@ function prepareWikiHtml(inputHtml: string): { html: string; toc: TocItem[] } {
|
|||||||
return { html: doc.body.innerHTML, toc };
|
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({
|
function PublicWikiSidebar({
|
||||||
entity,
|
entity,
|
||||||
wiki,
|
wiki,
|
||||||
@@ -249,7 +253,6 @@ function PublicWikiSidebar({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [activeHeadingId, setActiveHeadingId] = useState<string | null>(null);
|
|
||||||
const processedWiki = useMemo(() => {
|
const processedWiki = useMemo(() => {
|
||||||
if (!wiki) return { html: "", toc: [] as TocItem[] };
|
if (!wiki) return { html: "", toc: [] as TocItem[] };
|
||||||
|
|
||||||
@@ -263,59 +266,13 @@ function PublicWikiSidebar({
|
|||||||
}, [wiki]);
|
}, [wiki]);
|
||||||
const renderHtml = processedWiki.html;
|
const renderHtml = processedWiki.html;
|
||||||
const toc = processedWiki.toc;
|
const toc = processedWiki.toc;
|
||||||
const effectiveActiveHeadingId = toc.some((item) => item.id === activeHeadingId)
|
|
||||||
? activeHeadingId
|
|
||||||
: (toc[0]?.id ?? null);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const firstHeadingId = toc[0]?.id ?? null;
|
const scrollContainer = getWikiContentScrollContainer(contentRootRef.current);
|
||||||
setActiveHeadingId(firstHeadingId);
|
|
||||||
|
|
||||||
const scrollContainer = contentRootRef.current?.parentElement;
|
|
||||||
scrollContainer?.scrollTo({ top: 0, behavior: "auto" });
|
scrollContainer?.scrollTo({ top: 0, behavior: "auto" });
|
||||||
tocContainerRef.current?.scrollTo({ left: 0, behavior: "auto" });
|
tocContainerRef.current?.scrollTo({ left: 0, behavior: "auto" });
|
||||||
}, [wiki?.id, wiki?.slug, renderHtml, toc]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const container = tocContainerRef.current;
|
const container = tocContainerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -621,18 +578,16 @@ function PublicWikiSidebar({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{toc.slice(0, 8).map((item) => {
|
{toc.slice(0, 8).map((item) => {
|
||||||
const isActive = effectiveActiveHeadingId === item.id;
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={item.id}
|
key={item.id}
|
||||||
href={`#${item.id}`}
|
href={`#${item.id}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setActiveHeadingId(item.id);
|
|
||||||
const root = contentRootRef.current;
|
const root = contentRootRef.current;
|
||||||
if (root) {
|
if (root) {
|
||||||
const targetElement = root.querySelector(`#${CSS.escape(item.id)}`) as HTMLElement | null;
|
const targetElement = root.querySelector(`#${CSS.escape(item.id)}`) as HTMLElement | null;
|
||||||
const scrollContainer = root.parentElement;
|
const scrollContainer = getWikiContentScrollContainer(root);
|
||||||
if (targetElement && scrollContainer) {
|
if (targetElement && scrollContainer) {
|
||||||
const containerTop = scrollContainer.getBoundingClientRect().top;
|
const containerTop = scrollContainer.getBoundingClientRect().top;
|
||||||
const targetTop = targetElement.getBoundingClientRect().top;
|
const targetTop = targetElement.getBoundingClientRect().top;
|
||||||
@@ -652,15 +607,11 @@ function PublicWikiSidebar({
|
|||||||
fontWeight: 650,
|
fontWeight: 650,
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
transition: "all 0.2s",
|
transition: "all 0.2s",
|
||||||
background: isActive
|
background: "rgba(30, 41, 59, 0.4)",
|
||||||
? "rgba(56, 189, 248, 0.15)"
|
color: "#94a3b8",
|
||||||
: "rgba(30, 41, 59, 0.4)",
|
border: "1px solid rgba(148, 163, 184, 0.1)",
|
||||||
color: isActive ? "#38bdf8" : "#94a3b8",
|
|
||||||
border: isActive
|
|
||||||
? "1px solid rgba(56, 189, 248, 0.3)"
|
|
||||||
: "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}
|
{item.text}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user