fix feedback bug

This commit is contained in:
taDuc
2026-06-08 21:56:53 +07:00
parent a4c4084f9b
commit 5595bc7371
6 changed files with 144 additions and 64 deletions
+12
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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,
+10 -59
View File
@@ -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>