5 Commits

Author SHA1 Message Date
denshooter
ede591c89e Fix ActivityFeed hydration error: Move localStorage read to useEffect to prevent server/client mismatch
All checks were successful
Production Deployment (Zero Downtime) / deploy-production (push) Successful in 12m10s
2026-01-10 18:28:25 +01:00
denshooter
2defd7a4a9 Fix ActivityFeed: Remove dynamic import that was causing it to disappear in production
Some checks failed
Production Deployment (Zero Downtime) / deploy-production (push) Has been cancelled
2026-01-10 18:16:01 +01:00
denshooter
9cc03bc475 Prevent white screen: wrap ActivityFeed in error boundary and improve ClientProviders error handling
All checks were successful
Dev Deployment (Zero Downtime) / deploy-dev (push) Successful in 13m10s
Production Deployment (Zero Downtime) / deploy-production (push) Successful in 11m4s
2026-01-10 17:08:16 +01:00
denshooter
832b468ea7 Fix white screen: add error boundaries and improve error handling in AnalyticsProvider and useWebVitals 2026-01-10 17:07:00 +01:00
denshooter
2a260abe0a Fix ActivityFeed fetch TypeError: add proper error handling and type safety 2026-01-10 17:03:07 +01:00
6 changed files with 214 additions and 90 deletions

View File

@@ -50,69 +50,116 @@ interface StatusData {
}
export default function ActivityFeed() {
const [mounted, setMounted] = useState(false);
const [data, setData] = useState<StatusData | null>(null);
const [isExpanded, setIsExpanded] = useState(true);
const [isMinimized, setIsMinimized] = useState(false);
const [hasActivity, setHasActivity] = useState(false);
const [isTrackingEnabled, setIsTrackingEnabled] = useState(() => {
// Check localStorage for tracking preference
if (typeof window !== "undefined") {
try {
const stored = localStorage.getItem("activityTrackingEnabled");
return stored !== "false"; // Default to true if not set
} catch (error) {
// localStorage might be disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to read tracking preference:', error);
}
return true; // Default to enabled
}
}
return true;
});
// Initialize with default value to prevent hydration mismatch
const [isTrackingEnabled, setIsTrackingEnabled] = useState(true);
const [quote, setQuote] = useState<{
content: string;
author: string;
} | null>(null);
// Load tracking preference from localStorage after mount to prevent hydration mismatch
useEffect(() => {
setMounted(true);
if (typeof window !== "undefined") {
try {
const stored = localStorage.getItem("activityTrackingEnabled");
setIsTrackingEnabled(stored !== "false"); // Default to true if not set
} catch (error) {
// localStorage might be disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to read tracking preference:', error);
}
setIsTrackingEnabled(true); // Default to enabled
}
}
}, []);
// Fetch data every 30 seconds (optimized to match server cache)
useEffect(() => {
// Don't fetch if tracking is disabled
if (!isTrackingEnabled) {
// Don't fetch if tracking is disabled or during SSR
if (!isTrackingEnabled || typeof window === 'undefined') {
return;
}
const fetchData = async () => {
try {
// Check if fetch is available (should be, but safety check)
if (typeof fetch === 'undefined') {
return;
}
// Add timestamp to prevent aggressive caching but respect server cache
const res = await fetch("/api/n8n/status", {
cache: "default",
}).catch((fetchError) => {
// Handle network errors gracefully
if (process.env.NODE_ENV === 'development') {
console.warn('ActivityFeed: Fetch failed:', fetchError);
}
return null;
});
if (!res.ok) return;
let json = await res.json();
if (!res || !res.ok) {
if (process.env.NODE_ENV === 'development' && res) {
console.warn('ActivityFeed: API returned non-OK status:', res.status);
}
return;
}
let json: unknown;
try {
json = await res.json();
} catch (parseError) {
if (process.env.NODE_ENV === 'development') {
console.warn('ActivityFeed: Failed to parse JSON response:', parseError);
}
return;
}
console.log("ActivityFeed data (raw):", json);
if (process.env.NODE_ENV === 'development') {
console.log("ActivityFeed data (raw):", json);
}
// Handle array response if API returns it wrapped
if (Array.isArray(json)) {
json = json[0] || null;
}
console.log("ActivityFeed data (processed):", json);
if (process.env.NODE_ENV === 'development') {
console.log("ActivityFeed data (processed):", json);
}
setData(json);
if (!json || typeof json !== 'object') {
return;
}
// Type assertion - API should return StatusData format
const activityData = json as StatusData;
setData(activityData);
// Check if there's any active activity
const hasActiveActivity =
json.coding?.isActive ||
json.gaming?.isPlaying ||
json.music?.isPlaying;
const coding = activityData.coding;
const gaming = activityData.gaming;
const music = activityData.music;
const hasActiveActivity = Boolean(
coding?.isActive ||
gaming?.isPlaying ||
music?.isPlaying
);
console.log("Has activity:", hasActiveActivity, {
coding: json.coding?.isActive,
gaming: json.gaming?.isPlaying,
music: json.music?.isPlaying,
});
if (process.env.NODE_ENV === 'development') {
console.log("Has activity:", hasActiveActivity, {
coding: coding?.isActive,
gaming: gaming?.isPlaying,
music: music?.isPlaying,
});
}
setHasActivity(hasActiveActivity);
@@ -120,8 +167,12 @@ export default function ActivityFeed() {
if (hasActiveActivity && !isMinimized) {
setIsExpanded(true);
}
} catch (e) {
console.error("Failed to fetch activity", e);
} catch (error) {
// Silently fail - activity feed is not critical
if (process.env.NODE_ENV === 'development') {
console.error("Failed to fetch activity:", error);
}
// Don't set error state - just fail silently
}
};
@@ -1409,6 +1460,9 @@ export default function ActivityFeed() {
}
};
// Don't render until mounted to prevent hydration mismatch
if (!mounted) return null;
// Don't render if tracking is disabled and no data
if (!isTrackingEnabled && !data) return null;

View File

@@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import dynamic from "next/dynamic";
import { ToastProvider } from "@/components/Toast";
import ErrorBoundary from "@/components/ErrorBoundary";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
// Dynamic import with SSR disabled to avoid framer-motion issues
@@ -30,29 +31,55 @@ export default function ClientProviders({
setMounted(true);
// Check if we're on a 404 page by looking for the data attribute or pathname
const check404 = () => {
if (typeof window !== "undefined") {
const has404Component = document.querySelector('[data-404-page]');
const is404Path = pathname === '/404' || window.location.pathname === '/404' || window.location.pathname.includes('404');
setIs404Page(!!has404Component || is404Path);
try {
if (typeof window !== "undefined" && typeof document !== "undefined") {
const has404Component = document.querySelector('[data-404-page]');
const is404Path = pathname === '/404' || (window.location && (window.location.pathname === '/404' || window.location.pathname.includes('404')));
setIs404Page(!!has404Component || is404Path);
}
} catch (error) {
// Silently fail - 404 detection is not critical
if (process.env.NODE_ENV === 'development') {
console.warn('Error checking 404 status:', error);
}
}
};
// Check immediately and after a short delay
check404();
const timeout = setTimeout(check404, 100);
const interval = setInterval(check404, 500);
return () => {
clearTimeout(timeout);
clearInterval(interval);
};
try {
check404();
const timeout = setTimeout(check404, 100);
const interval = setInterval(check404, 500);
return () => {
try {
clearTimeout(timeout);
clearInterval(interval);
} catch {
// Silently fail during cleanup
}
};
} catch (error) {
// If setup fails, just return empty cleanup
if (process.env.NODE_ENV === 'development') {
console.warn('Error setting up 404 check:', error);
}
return () => {};
}
}, [pathname]);
// Wrap in multiple error boundaries to isolate failures
return (
<AnalyticsProvider>
<ToastProvider>
{mounted && <BackgroundBlobs />}
<div className="relative z-10">{children}</div>
{mounted && !is404Page && <ChatWidget />}
</ToastProvider>
</AnalyticsProvider>
<ErrorBoundary>
<ErrorBoundary>
<AnalyticsProvider>
<ErrorBoundary>
<ToastProvider>
{mounted && <BackgroundBlobs />}
<div className="relative z-10">{children}</div>
{mounted && !is404Page && <ChatWidget />}
</ToastProvider>
</ErrorBoundary>
</AnalyticsProvider>
</ErrorBoundary>
</ErrorBoundary>
);
}

View File

@@ -7,7 +7,8 @@ import Projects from "./components/Projects";
import Contact from "./components/Contact";
import Footer from "./components/Footer";
import Script from "next/script";
import ActivityFeed from "./components/ActivityFeed";
import ErrorBoundary from "@/components/ErrorBoundary";
import ActivityFeed from "./components/ActivityFeed";
import { motion } from "framer-motion";
export default function Home() {
@@ -35,7 +36,9 @@ export default function Home() {
}),
}}
/>
<ActivityFeed />
<ErrorBoundary>
<ActivityFeed />
</ErrorBoundary>
<Header />
{/* Spacer to prevent navbar overlap */}
<div className="h-24 md:h-32" aria-hidden="true"></div>

View File

@@ -9,12 +9,16 @@ interface AnalyticsProviderProps {
}
export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }) => {
// Initialize Web Vitals tracking
// Initialize Web Vitals tracking - wrapped to prevent crashes
// Hooks must be called unconditionally, but the hook itself handles errors
useWebVitals();
useEffect(() => {
if (typeof window === 'undefined') return;
// Wrap entire effect in try-catch to prevent any errors from breaking the app
try {
// Track page view
const trackPageView = async () => {
const path = window.location.pathname;
@@ -49,8 +53,15 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
}
};
// Track page load performance
trackPageLoad();
// Track page load performance - wrapped in try-catch
try {
trackPageLoad();
} catch (error) {
// Silently fail
if (process.env.NODE_ENV === 'development') {
console.warn('Error tracking page load:', error);
}
}
// Track initial page view
trackPageView();
@@ -255,16 +266,29 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleUnhandledRejection);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
document.removeEventListener('click', handleClick);
document.removeEventListener('submit', handleSubmit);
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
};
// Cleanup
return () => {
try {
window.removeEventListener('popstate', handleRouteChange);
document.removeEventListener('click', handleClick);
document.removeEventListener('submit', handleSubmit);
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
} catch {
// Silently fail during cleanup
}
};
} catch (error) {
// If anything fails, log but don't break the app
if (process.env.NODE_ENV === 'development') {
console.error('AnalyticsProvider initialization error:', error);
}
// Return empty cleanup function
return () => {};
}
}, []);
// Always render children, even if analytics fails
return <>{children}</>;
};

View File

@@ -22,17 +22,19 @@ export default class ErrorBoundary extends React.Component<
render() {
if (this.state.hasError) {
return (
<div className="p-4 m-4 bg-red-50 border border-red-200 rounded text-red-800">
<h2>Something went wrong!</h2>
<button
className="mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={() => this.setState({ hasError: false })}
>
Try again
</button>
</div>
);
// Still render children to prevent white screen - just log the error
if (process.env.NODE_ENV === 'development') {
return (
<div>
<div className="p-2 m-2 bg-yellow-50 border border-yellow-200 rounded text-yellow-800 text-xs">
Error boundary triggered - rendering children anyway
</div>
{this.props.children}
</div>
);
}
// In production, just render children silently
return this.props.children;
}
return this.props.children;

View File

@@ -206,12 +206,14 @@ export const useWebVitals = () => {
useEffect(() => {
if (typeof window === 'undefined') return;
// Store web vitals for batch sending
const webVitals: Record<string, number> = {};
const path = window.location.pathname;
const projectMatch = path.match(/\/projects\/([^\/]+)/);
const projectId = projectMatch ? projectMatch[1] : null;
const observers: PerformanceObserver[] = [];
// Wrap everything in try-catch to prevent errors from breaking the app
try {
// Store web vitals for batch sending
const webVitals: Record<string, number> = {};
const path = window.location.pathname;
const projectMatch = path.match(/\/projects\/([^\/]+)/);
const projectId = projectMatch ? projectMatch[1] : null;
const observers: PerformanceObserver[] = [];
const sendWebVitals = async () => {
if (Object.keys(webVitals).length >= 3) { // Wait for at least FCP, LCP, CLS
@@ -319,16 +321,28 @@ export const useWebVitals = () => {
window.addEventListener('load', handleLoad);
}
return () => {
// Cleanup all observers
observers.forEach(observer => {
return () => {
// Cleanup all observers
observers.forEach(observer => {
try {
observer.disconnect();
} catch {
// Silently fail
}
});
try {
observer.disconnect();
window.removeEventListener('load', handleLoad);
} catch {
// Silently fail
}
});
window.removeEventListener('load', handleLoad);
};
};
} catch (error) {
// If Web Vitals initialization fails, don't break the app
if (process.env.NODE_ENV === 'development') {
console.warn('Web Vitals initialization failed:', error);
}
// Return empty cleanup function
return () => {};
}
}, []);
};