Compare commits
5 Commits
80f2ac61ac
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ede591c89e | ||
|
|
2defd7a4a9 | ||
|
|
9cc03bc475 | ||
|
|
832b468ea7 | ||
|
|
2a260abe0a |
@@ -50,69 +50,116 @@ interface StatusData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ActivityFeed() {
|
export default function ActivityFeed() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
const [data, setData] = useState<StatusData | null>(null);
|
const [data, setData] = useState<StatusData | null>(null);
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
const [isMinimized, setIsMinimized] = useState(false);
|
const [isMinimized, setIsMinimized] = useState(false);
|
||||||
const [hasActivity, setHasActivity] = useState(false);
|
const [hasActivity, setHasActivity] = useState(false);
|
||||||
const [isTrackingEnabled, setIsTrackingEnabled] = useState(() => {
|
// Initialize with default value to prevent hydration mismatch
|
||||||
// Check localStorage for tracking preference
|
const [isTrackingEnabled, setIsTrackingEnabled] = useState(true);
|
||||||
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;
|
|
||||||
});
|
|
||||||
const [quote, setQuote] = useState<{
|
const [quote, setQuote] = useState<{
|
||||||
content: string;
|
content: string;
|
||||||
author: string;
|
author: string;
|
||||||
} | null>(null);
|
} | 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)
|
// Fetch data every 30 seconds (optimized to match server cache)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't fetch if tracking is disabled
|
// Don't fetch if tracking is disabled or during SSR
|
||||||
if (!isTrackingEnabled) {
|
if (!isTrackingEnabled || typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
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
|
// Add timestamp to prevent aggressive caching but respect server cache
|
||||||
const res = await fetch("/api/n8n/status", {
|
const res = await fetch("/api/n8n/status", {
|
||||||
cache: "default",
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log("ActivityFeed data (raw):", json);
|
console.log("ActivityFeed data (raw):", json);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle array response if API returns it wrapped
|
// Handle array response if API returns it wrapped
|
||||||
if (Array.isArray(json)) {
|
if (Array.isArray(json)) {
|
||||||
json = json[0] || null;
|
json = json[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log("ActivityFeed data (processed):", json);
|
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
|
// Check if there's any active activity
|
||||||
const hasActiveActivity =
|
const coding = activityData.coding;
|
||||||
json.coding?.isActive ||
|
const gaming = activityData.gaming;
|
||||||
json.gaming?.isPlaying ||
|
const music = activityData.music;
|
||||||
json.music?.isPlaying;
|
|
||||||
|
|
||||||
|
const hasActiveActivity = Boolean(
|
||||||
|
coding?.isActive ||
|
||||||
|
gaming?.isPlaying ||
|
||||||
|
music?.isPlaying
|
||||||
|
);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log("Has activity:", hasActiveActivity, {
|
console.log("Has activity:", hasActiveActivity, {
|
||||||
coding: json.coding?.isActive,
|
coding: coding?.isActive,
|
||||||
gaming: json.gaming?.isPlaying,
|
gaming: gaming?.isPlaying,
|
||||||
music: json.music?.isPlaying,
|
music: music?.isPlaying,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setHasActivity(hasActiveActivity);
|
setHasActivity(hasActiveActivity);
|
||||||
|
|
||||||
@@ -120,8 +167,12 @@ export default function ActivityFeed() {
|
|||||||
if (hasActiveActivity && !isMinimized) {
|
if (hasActiveActivity && !isMinimized) {
|
||||||
setIsExpanded(true);
|
setIsExpanded(true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch activity", e);
|
// 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
|
// Don't render if tracking is disabled and no data
|
||||||
if (!isTrackingEnabled && !data) return null;
|
if (!isTrackingEnabled && !data) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { ToastProvider } from "@/components/Toast";
|
import { ToastProvider } from "@/components/Toast";
|
||||||
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
|
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
|
||||||
|
|
||||||
// Dynamic import with SSR disabled to avoid framer-motion issues
|
// Dynamic import with SSR disabled to avoid framer-motion issues
|
||||||
@@ -30,29 +31,55 @@ export default function ClientProviders({
|
|||||||
setMounted(true);
|
setMounted(true);
|
||||||
// Check if we're on a 404 page by looking for the data attribute or pathname
|
// Check if we're on a 404 page by looking for the data attribute or pathname
|
||||||
const check404 = () => {
|
const check404 = () => {
|
||||||
if (typeof window !== "undefined") {
|
try {
|
||||||
|
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
||||||
const has404Component = document.querySelector('[data-404-page]');
|
const has404Component = document.querySelector('[data-404-page]');
|
||||||
const is404Path = pathname === '/404' || window.location.pathname === '/404' || window.location.pathname.includes('404');
|
const is404Path = pathname === '/404' || (window.location && (window.location.pathname === '/404' || window.location.pathname.includes('404')));
|
||||||
setIs404Page(!!has404Component || is404Path);
|
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
|
// Check immediately and after a short delay
|
||||||
|
try {
|
||||||
check404();
|
check404();
|
||||||
const timeout = setTimeout(check404, 100);
|
const timeout = setTimeout(check404, 100);
|
||||||
const interval = setInterval(check404, 500);
|
const interval = setInterval(check404, 500);
|
||||||
return () => {
|
return () => {
|
||||||
|
try {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
clearInterval(interval);
|
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]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
// Wrap in multiple error boundaries to isolate failures
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ErrorBoundary>
|
||||||
<AnalyticsProvider>
|
<AnalyticsProvider>
|
||||||
|
<ErrorBoundary>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{mounted && <BackgroundBlobs />}
|
{mounted && <BackgroundBlobs />}
|
||||||
<div className="relative z-10">{children}</div>
|
<div className="relative z-10">{children}</div>
|
||||||
{mounted && !is404Page && <ChatWidget />}
|
{mounted && !is404Page && <ChatWidget />}
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
</AnalyticsProvider>
|
</AnalyticsProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Projects from "./components/Projects";
|
|||||||
import Contact from "./components/Contact";
|
import Contact from "./components/Contact";
|
||||||
import Footer from "./components/Footer";
|
import Footer from "./components/Footer";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
import ActivityFeed from "./components/ActivityFeed";
|
import ActivityFeed from "./components/ActivityFeed";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
@@ -35,7 +36,9 @@ export default function Home() {
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ErrorBoundary>
|
||||||
<ActivityFeed />
|
<ActivityFeed />
|
||||||
|
</ErrorBoundary>
|
||||||
<Header />
|
<Header />
|
||||||
{/* Spacer to prevent navbar overlap */}
|
{/* Spacer to prevent navbar overlap */}
|
||||||
<div className="h-24 md:h-32" aria-hidden="true"></div>
|
<div className="h-24 md:h-32" aria-hidden="true"></div>
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ interface AnalyticsProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }) => {
|
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();
|
useWebVitals();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Wrap entire effect in try-catch to prevent any errors from breaking the app
|
||||||
|
try {
|
||||||
|
|
||||||
// Track page view
|
// Track page view
|
||||||
const trackPageView = async () => {
|
const trackPageView = async () => {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
@@ -49,8 +53,15 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track page load performance
|
// Track page load performance - wrapped in try-catch
|
||||||
|
try {
|
||||||
trackPageLoad();
|
trackPageLoad();
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn('Error tracking page load:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Track initial page view
|
// Track initial page view
|
||||||
trackPageView();
|
trackPageView();
|
||||||
@@ -257,14 +268,27 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
|
|||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
return () => {
|
return () => {
|
||||||
|
try {
|
||||||
window.removeEventListener('popstate', handleRouteChange);
|
window.removeEventListener('popstate', handleRouteChange);
|
||||||
document.removeEventListener('click', handleClick);
|
document.removeEventListener('click', handleClick);
|
||||||
document.removeEventListener('submit', handleSubmit);
|
document.removeEventListener('submit', handleSubmit);
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
window.removeEventListener('error', handleError);
|
window.removeEventListener('error', handleError);
|
||||||
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
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}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,18 +22,20 @@ export default class ErrorBoundary extends React.Component<
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
|
// Still render children to prevent white screen - just log the error
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 m-4 bg-red-50 border border-red-200 rounded text-red-800">
|
<div>
|
||||||
<h2>Something went wrong!</h2>
|
<div className="p-2 m-2 bg-yellow-50 border border-yellow-200 rounded text-yellow-800 text-xs">
|
||||||
<button
|
⚠️ Error boundary triggered - rendering children anyway
|
||||||
className="mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
</div>
|
||||||
onClick={() => this.setState({ hasError: false })}
|
{this.props.children}
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// In production, just render children silently
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
|
||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,8 @@ export const useWebVitals = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Wrap everything in try-catch to prevent errors from breaking the app
|
||||||
|
try {
|
||||||
// Store web vitals for batch sending
|
// Store web vitals for batch sending
|
||||||
const webVitals: Record<string, number> = {};
|
const webVitals: Record<string, number> = {};
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
@@ -328,7 +330,19 @@ export const useWebVitals = () => {
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
window.removeEventListener('load', handleLoad);
|
window.removeEventListener('load', handleLoad);
|
||||||
|
} catch {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
} 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 () => {};
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user