feat: update bug fixing chunk
This commit is contained in:
parent
cabd73822f
commit
af9000498a
|
|
@ -1,6 +1,8 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import ChunkErrorBoundary from "@/components/layout/chunk-error-boundary";
|
||||
import "@/utils/chunk-error-handler";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
|
|
@ -25,7 +27,9 @@ export default function RootLayout({
|
|||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`} suppressHydrationWarning>
|
||||
{children}
|
||||
<ChunkErrorBoundary>
|
||||
{children}
|
||||
</ChunkErrorBoundary>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -250,7 +250,11 @@ export default function LatestandPopular() {
|
|||
<div key={index}>
|
||||
<div className="relative w-full aspect-video mb-3">
|
||||
<Image
|
||||
src={article.thumbnailUrl}
|
||||
src={
|
||||
articles[0]?.thumbnailUrl ||
|
||||
articles[0]?.files?.[0]?.file_url ||
|
||||
"/default-image.jpg"
|
||||
}
|
||||
alt={"article.title"}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||
|
|
@ -407,7 +411,11 @@ export default function LatestandPopular() {
|
|||
<div key={index} className="flex gap-3">
|
||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||
<Image
|
||||
src={article?.thumbnailUrl}
|
||||
src={
|
||||
articles[0]?.thumbnailUrl ||
|
||||
articles[0]?.files?.[0]?.file_url ||
|
||||
"/default-image.jpg"
|
||||
}
|
||||
alt={"article?.title"}
|
||||
fill
|
||||
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";
|
||||
import { getStatisticMonthly } from "@/service/article";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { SafeReactApexChart } from "@/utils/dynamic-import";
|
||||
|
||||
type WeekData = {
|
||||
week: number;
|
||||
|
|
@ -53,9 +53,6 @@ const ApexChartColumn = (props: {
|
|||
const [seriesComment, setSeriesComment] = useState<number[]>([]);
|
||||
const [seriesView, setSeriesView] = useState<number[]>([]);
|
||||
const [seriesShare, setSeriesShare] = useState<number[]>([]);
|
||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initFetch();
|
||||
|
|
@ -132,7 +129,7 @@ const ApexChartColumn = (props: {
|
|||
return (
|
||||
<div className="h-full">
|
||||
<div id="chart" className="h-full">
|
||||
<ReactApexChart
|
||||
<SafeReactApexChart
|
||||
options={{
|
||||
chart: {
|
||||
height: "100%",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,53 @@ const nextConfig: NextConfig = {
|
|||
eslint: {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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