diff --git a/next.config.ts b/next.config.ts
index 916db6c..78c4b29 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -27,11 +27,43 @@ const nextConfig: NextConfig = {
],
},
output: 'standalone',
- webpack(config) {
+ webpack(config, { isServer }) {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
});
+
+ if (!isServer) {
+ // Split heavy third-party vendor libraries into their own dedicated chunks
+ config.optimization.splitChunks.cacheGroups = {
+ ...config.optimization.splitChunks.cacheGroups,
+ maplibre: {
+ test: /[\\/]node_modules[\\/]maplibre-gl[\\/]/,
+ name: "maplibre",
+ chunks: "all",
+ priority: 40,
+ },
+ quill: {
+ test: /[\\/]node_modules[\\/](react-quill-new|quill|quill-blot-formatter)[\\/]/,
+ name: "quill",
+ chunks: "all",
+ priority: 35,
+ },
+ charts: {
+ test: /[\\/]node_modules[\\/](apexcharts|react-apexcharts)[\\/]/,
+ name: "charts",
+ chunks: "all",
+ priority: 30,
+ },
+ calendar: {
+ test: /[\\/]node_modules[\\/]@fullcalendar[\\/]/,
+ name: "calendar",
+ chunks: "all",
+ priority: 25,
+ },
+ };
+ }
+
return config;
},
diff --git a/src/uhm/components/preview/MapPlaceholder.tsx b/src/uhm/components/preview/MapPlaceholder.tsx
index a83e903..f2d421a 100644
--- a/src/uhm/components/preview/MapPlaceholder.tsx
+++ b/src/uhm/components/preview/MapPlaceholder.tsx
@@ -3,98 +3,10 @@
import React from "react";
interface MapPlaceholderProps {
- isLoaderOnly?: boolean;
onEnter?: () => void;
}
-export default function MapPlaceholder({ isLoaderOnly = true, onEnter }: MapPlaceholderProps) {
- if (isLoaderOnly) {
- return (
-
- {/* Background Image */}
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

-
- {/* Dark overlay & Spinner */}
-
-
-
-
- ĐANG TẢI DỮ LIỆU...
-
-
-
- );
- }
-
+export default function MapPlaceholder({ onEnter }: MapPlaceholderProps) {
return (
void;
+ instantLoad?: boolean;
+ onToggleInstantLoad?: (val: boolean) => void;
};
export default function PreviewMapShell({
@@ -88,6 +90,8 @@ export default function PreviewMapShell({
overlay,
children,
onLoad,
+ instantLoad = true,
+ onToggleInstantLoad,
}: Props) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -238,6 +242,22 @@ export default function PreviewMapShell({
+
+
{linkEntityPopup ? (
diff --git a/src/uhm/components/preview/PublicPreviewWrapper.tsx b/src/uhm/components/preview/PublicPreviewWrapper.tsx
index eeaf8d3..102c5a9 100644
--- a/src/uhm/components/preview/PublicPreviewWrapper.tsx
+++ b/src/uhm/components/preview/PublicPreviewWrapper.tsx
@@ -11,14 +11,31 @@ import MapPlaceholder from "./MapPlaceholder";
const LoaderComponent = () => {
const mapEntered = useSelector((state: RootState) => state.ui.mapEntered);
const dispatch = useDispatch();
+ const [instantLoad, setInstantLoad] = useState(true);
+
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ const saved = localStorage.getItem("instant-load");
+ if (saved !== null) {
+ setInstantLoad(saved === "true");
+ }
+ }
+ }, []);
const handleEnter = () => {
dispatch(setMapEntered(true));
};
+ const toggleInstantLoad = (val: boolean) => {
+ setInstantLoad(val);
+ if (typeof window !== "undefined") {
+ localStorage.setItem("instant-load", String(val));
+ }
+ window.dispatchEvent(new Event("instant-load-changed"));
+ };
+
return (
);
@@ -35,12 +52,36 @@ const PublicPreviewClientPage = dynamic(
export default function PublicPreviewWrapper() {
const mapEntered = useSelector((state: RootState) => state.ui.mapEntered);
const dispatch = useDispatch();
+ const [instantLoad, setInstantLoad] = useState(true);
- const [loadInteractive, setLoadInteractive] = useState(() => mapEntered);
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ const saved = localStorage.getItem("instant-load");
+ if (saved !== null) {
+ setInstantLoad(saved === "true");
+ }
+
+ const handleSync = () => {
+ const updated = localStorage.getItem("instant-load");
+ if (updated !== null) {
+ setInstantLoad(updated === "true");
+ }
+ };
+ window.addEventListener("instant-load-changed", handleSync);
+ return () => window.removeEventListener("instant-load-changed", handleSync);
+ }
+ }, []);
+
+ const toggleInstantLoad = (val: boolean) => {
+ setInstantLoad(val);
+ if (typeof window !== "undefined") {
+ localStorage.setItem("instant-load", String(val));
+ }
+ window.dispatchEvent(new Event("instant-load-changed"));
+ };
const handleEnter = () => {
dispatch(setMapEntered(true));
- setLoadInteractive(true);
};
useEffect(() => {
@@ -54,36 +95,27 @@ export default function PublicPreviewWrapper() {
window.addEventListener("touchstart", handleInteraction, { passive: true, once: true });
window.addEventListener("keydown", handleInteraction, { passive: true, once: true });
- // Tải ngầm bản đồ sau 2.5 giây khi trình duyệt rảnh rỗi (idle)
- let idleId: any = null;
- const prefetchTimer = setTimeout(() => {
- const runPrefetch = () => {
- setLoadInteractive(true);
- };
-
- if (typeof window !== "undefined") {
- if ("requestIdleCallback" in window) {
- idleId = (window as any).requestIdleCallback(runPrefetch, { timeout: 3000 });
- } else {
- setTimeout(runPrefetch, 200);
- }
- }
- }, 2000);
-
return () => {
window.removeEventListener("click", handleInteraction);
window.removeEventListener("touchstart", handleInteraction);
window.removeEventListener("keydown", handleInteraction);
- clearTimeout(prefetchTimer);
- if (idleId && typeof window !== "undefined" && "cancelIdleCallback" in window) {
- (window as any).cancelIdleCallback(idleId);
- }
};
}, [mapEntered]);
- if (!loadInteractive) {
- return ;
+ if (!mapEntered && !instantLoad) {
+ return (
+
+ );
}
- return ;
+ return (
+
+ );
}
diff --git a/src/uhm/components/wiki/PublicWikiSidebar.tsx b/src/uhm/components/wiki/PublicWikiSidebar.tsx
index 51db12d..f0b160e 100644
--- a/src/uhm/components/wiki/PublicWikiSidebar.tsx
+++ b/src/uhm/components/wiki/PublicWikiSidebar.tsx
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo, useRef, useState, memo } from "react";
-import "react-quill-new/dist/quill.snow.css";
+// Loaded dynamically inside the component to prevent render-blocking
import type { Entity } from "@/uhm/api/entities";
import type { Wiki } from "@/uhm/api/wikis";
@@ -139,6 +139,10 @@ function PublicWikiSidebar({
const contentRootRef = useRef(null);
const tocContainerRef = useRef(null);
+ useEffect(() => {
+ import("react-quill-new/dist/quill.snow.css");
+ }, []);
+
const [localWidth, setLocalWidth] = useState(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("public-wiki-sidebar-width");