feat: update bug fixing chunk
This commit is contained in:
parent
cabd73822f
commit
af9000498a
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import ChunkErrorBoundary from "@/components/layout/chunk-error-boundary";
|
||||||
|
import "@/utils/chunk-error-handler";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
|
|
@ -25,7 +27,9 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`} suppressHydrationWarning>
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`} suppressHydrationWarning>
|
||||||
|
<ChunkErrorBoundary>
|
||||||
{children}
|
{children}
|
||||||
|
</ChunkErrorBoundary>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,11 @@ export default function LatestandPopular() {
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<div className="relative w-full aspect-video mb-3">
|
<div className="relative w-full aspect-video mb-3">
|
||||||
<Image
|
<Image
|
||||||
src={article.thumbnailUrl}
|
src={
|
||||||
|
articles[0]?.thumbnailUrl ||
|
||||||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
"/default-image.jpg"
|
||||||
|
}
|
||||||
alt={"article.title"}
|
alt={"article.title"}
|
||||||
fill
|
fill
|
||||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||||
|
|
@ -407,7 +411,11 @@ export default function LatestandPopular() {
|
||||||
<div key={index} className="flex gap-3">
|
<div key={index} className="flex gap-3">
|
||||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={article?.thumbnailUrl}
|
src={
|
||||||
|
articles[0]?.thumbnailUrl ||
|
||||||
|
articles[0]?.files?.[0]?.file_url ||
|
||||||
|
"/default-image.jpg"
|
||||||
|
}
|
||||||
alt={"article?.title"}
|
alt={"article?.title"}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChunkErrorBoundary extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
// Check if it's a chunk loading error
|
||||||
|
if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
return { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error('Chunk loading error:', error, errorInfo);
|
||||||
|
|
||||||
|
// If it's a chunk loading error, try to reload the page
|
||||||
|
if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) {
|
||||||
|
this.setState({ hasError: true, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry = () => {
|
||||||
|
// Clear the error state and reload the page
|
||||||
|
this.setState({ hasError: false, error: undefined });
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-white to-slate-50">
|
||||||
|
<div className="text-center p-8 max-w-md">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<RefreshCw className="w-8 h-8 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
Chunk Loading Error
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
There was an issue loading a part of the application. This usually happens when the application has been updated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button
|
||||||
|
onClick={this.handleRetry}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
Reload Application
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => window.location.href = '/'}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Go to Homepage
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||||
|
<details className="mt-6 text-left">
|
||||||
|
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
|
||||||
|
Error Details (Development)
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto">
|
||||||
|
{this.state.error.message}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChunkErrorBoundary;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { getStatisticMonthly } from "@/service/article";
|
import { getStatisticMonthly } from "@/service/article";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import { SafeReactApexChart } from "@/utils/dynamic-import";
|
||||||
|
|
||||||
type WeekData = {
|
type WeekData = {
|
||||||
week: number;
|
week: number;
|
||||||
|
|
@ -53,9 +53,6 @@ const ApexChartColumn = (props: {
|
||||||
const [seriesComment, setSeriesComment] = useState<number[]>([]);
|
const [seriesComment, setSeriesComment] = useState<number[]>([]);
|
||||||
const [seriesView, setSeriesView] = useState<number[]>([]);
|
const [seriesView, setSeriesView] = useState<number[]>([]);
|
||||||
const [seriesShare, setSeriesShare] = useState<number[]>([]);
|
const [seriesShare, setSeriesShare] = useState<number[]>([]);
|
||||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initFetch();
|
initFetch();
|
||||||
|
|
@ -132,7 +129,7 @@ const ApexChartColumn = (props: {
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<div id="chart" className="h-full">
|
<div id="chart" className="h-full">
|
||||||
<ReactApexChart
|
<SafeReactApexChart
|
||||||
options={{
|
options={{
|
||||||
chart: {
|
chart: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,53 @@ const nextConfig: NextConfig = {
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
|
webpack: (config, { isServer }) => {
|
||||||
|
// Handle chunk loading errors
|
||||||
|
config.optimization = {
|
||||||
|
...config.optimization,
|
||||||
|
splitChunks: {
|
||||||
|
...config.optimization.splitChunks,
|
||||||
|
chunks: 'all',
|
||||||
|
cacheGroups: {
|
||||||
|
...config.optimization.splitChunks?.cacheGroups,
|
||||||
|
vendor: {
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
name: 'vendors',
|
||||||
|
chunks: 'all',
|
||||||
|
},
|
||||||
|
// Separate CKEditor chunks
|
||||||
|
ckeditor: {
|
||||||
|
test: /[\\/]node_modules[\\/]@ckeditor[\\/]/,
|
||||||
|
name: 'ckeditor',
|
||||||
|
chunks: 'all',
|
||||||
|
priority: 20,
|
||||||
|
},
|
||||||
|
// Separate ApexCharts chunks
|
||||||
|
apexcharts: {
|
||||||
|
test: /[\\/]node_modules[\\/](apexcharts|react-apexcharts)[\\/]/,
|
||||||
|
name: 'apexcharts',
|
||||||
|
chunks: 'all',
|
||||||
|
priority: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add error handling for chunk loading
|
||||||
|
if (!isServer) {
|
||||||
|
config.output = {
|
||||||
|
...config.output,
|
||||||
|
chunkFilename: '[name].[chunkhash].js',
|
||||||
|
filename: '[name].[chunkhash].js',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
// Add experimental features for better chunk handling
|
||||||
|
experimental: {
|
||||||
|
optimizePackageImports: ['@ckeditor/ckeditor5-react', 'react-apexcharts'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Global chunk loading error handler
|
||||||
|
export function setupChunkErrorHandler() {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Handle chunk loading errors
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
const error = event.error;
|
||||||
|
|
||||||
|
// Check if it's a chunk loading error
|
||||||
|
if (
|
||||||
|
error?.name === 'ChunkLoadError' ||
|
||||||
|
error?.message?.includes('Loading chunk') ||
|
||||||
|
error?.message?.includes('Failed to fetch')
|
||||||
|
) {
|
||||||
|
console.warn('Chunk loading error detected:', error);
|
||||||
|
|
||||||
|
// Prevent the error from being logged to console
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Show a user-friendly message
|
||||||
|
const message = document.createElement('div');
|
||||||
|
message.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 9999;
|
||||||
|
max-width: 300px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
`;
|
||||||
|
message.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span>Application update detected. Please refresh the page.</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="window.location.reload()" style="
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #dc2626;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
">Refresh</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(message);
|
||||||
|
|
||||||
|
// Auto-remove after 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (message.parentNode) {
|
||||||
|
message.parentNode.removeChild(message);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle unhandled promise rejections (which might include chunk loading errors)
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
const error = event.reason;
|
||||||
|
|
||||||
|
if (
|
||||||
|
error?.name === 'ChunkLoadError' ||
|
||||||
|
error?.message?.includes('Loading chunk') ||
|
||||||
|
error?.message?.includes('Failed to fetch')
|
||||||
|
) {
|
||||||
|
console.warn('Unhandled chunk loading rejection:', error);
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Reload the page after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-setup when this module is imported
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
setupChunkErrorHandler();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
|
interface DynamicImportOptions {
|
||||||
|
ssr?: boolean;
|
||||||
|
loading?: () => React.ReactElement;
|
||||||
|
retries?: number;
|
||||||
|
retryDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSafeDynamicImport<T extends ComponentType<any>>(
|
||||||
|
importFn: () => Promise<{ default: T }>,
|
||||||
|
options: DynamicImportOptions = {}
|
||||||
|
) {
|
||||||
|
const { ssr = false, loading, retries = 3, retryDelay = 1000 } = options;
|
||||||
|
|
||||||
|
return dynamic(
|
||||||
|
() => {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
const attemptImport = async () => {
|
||||||
|
try {
|
||||||
|
const module = await importFn();
|
||||||
|
resolve(module.default);
|
||||||
|
} catch (error) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
// Check if it's a chunk loading error
|
||||||
|
if (
|
||||||
|
(error as any)?.name === 'ChunkLoadError' ||
|
||||||
|
(error as any)?.message?.includes('Loading chunk') ||
|
||||||
|
(error as any)?.message?.includes('Failed to fetch')
|
||||||
|
) {
|
||||||
|
if (attempts < retries) {
|
||||||
|
console.warn(`Chunk loading failed, retrying... (${attempts}/${retries})`);
|
||||||
|
setTimeout(attemptImport, retryDelay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
attemptImport();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ssr,
|
||||||
|
loading,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined safe dynamic imports for common components
|
||||||
|
export const SafeCustomEditor = createSafeDynamicImport(
|
||||||
|
() => import('@/components/editor/custom-editor'),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SafeViewEditor = createSafeDynamicImport(
|
||||||
|
() => import('@/components/editor/view-editor'),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SafeReactApexChart = createSafeDynamicImport(
|
||||||
|
() => import('react-apexcharts'),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue