refactor: enhance error handling and performance tracking across components

- Improve localStorage access in ActivityFeed, ChatWidget, and AdminPage with try-catch blocks to handle potential errors gracefully.
- Update performance tracking in AnalyticsProvider and analytics.ts to ensure robust error handling and prevent failures from affecting user experience.
- Refactor Web Vitals tracking to include error handling for observer initialization and data collection.
- Ensure consistent handling of hydration mismatches in components like BackgroundBlobs and ChatWidget to improve rendering reliability.
This commit is contained in:
2026-01-10 16:53:06 +01:00
parent 20f0ccb85b
commit ca2ed13446
10 changed files with 573 additions and 268 deletions

View File

@@ -126,29 +126,30 @@ export async function POST(request: NextRequest) {
await prisma.project.update({
where: { id: projectIdNum },
data: {
performance: {
...perf,
lighthouse: lighthouseScore,
loadTime: performance.loadTime || perf.loadTime || 0,
firstContentfulPaint: fcp || perf.firstContentfulPaint || 0,
largestContentfulPaint: lcp || perf.largestContentfulPaint || 0,
cumulativeLayoutShift: cls || perf.cumulativeLayoutShift || 0,
totalBlockingTime: performance.tbt || perf.totalBlockingTime || 0,
speedIndex: performance.si || perf.speedIndex || 0,
coreWebVitals: {
lcp: lcp || (perf.coreWebVitals as Record<string, unknown>)?.lcp || 0,
fid: fid || (perf.coreWebVitals as Record<string, unknown>)?.fid || 0,
cls: cls || (perf.coreWebVitals as Record<string, unknown>)?.cls || 0
data: {
performance: {
...perf,
lighthouse: lighthouseScore,
loadTime: performance.loadTime || perf.loadTime || 0,
firstContentfulPaint: fcp || perf.firstContentfulPaint || 0,
largestContentfulPaint: lcp || perf.largestContentfulPaint || 0,
cumulativeLayoutShift: cls || perf.cumulativeLayoutShift || 0,
totalBlockingTime: performance.tbt || perf.totalBlockingTime || 0,
speedIndex: performance.si || perf.speedIndex || 0,
coreWebVitals: {
lcp: lcp || (perf.coreWebVitals as Record<string, unknown>)?.lcp || 0,
fid: fid || (perf.coreWebVitals as Record<string, unknown>)?.fid || 0,
cls: cls || (perf.coreWebVitals as Record<string, unknown>)?.cls || 0
},
lastUpdated: new Date().toISOString()
},
lastUpdated: new Date().toISOString()
},
analytics: {
...analytics,
lastUpdated: new Date().toISOString()
analytics: {
...analytics,
lastUpdated: new Date().toISOString()
}
}
}
});
});
}
}
}
}

View File

@@ -57,8 +57,16 @@ export default function ActivityFeed() {
const [isTrackingEnabled, setIsTrackingEnabled] = useState(() => {
// Check localStorage for tracking preference
if (typeof window !== "undefined") {
const stored = localStorage.getItem("activityTrackingEnabled");
return stored !== "false"; // Default to true if not set
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;
});
@@ -1385,7 +1393,14 @@ export default function ActivityFeed() {
const newValue = !isTrackingEnabled;
setIsTrackingEnabled(newValue);
if (typeof window !== "undefined") {
localStorage.setItem("activityTrackingEnabled", String(newValue));
try {
localStorage.setItem("activityTrackingEnabled", String(newValue));
} catch (error) {
// localStorage might be full or disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to save tracking preference:', error);
}
}
}
// Clear data when disabling
if (!newValue) {

View File

@@ -20,21 +20,47 @@ interface Message {
}
export default function ChatWidget() {
// Prevent hydration mismatch by only rendering after mount
const [mounted, setMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [conversationId, setConversationId] = useState(() => {
// Generate or retrieve conversation ID
if (typeof window !== "undefined") {
const [conversationId, setConversationId] = useState<string>("default");
useEffect(() => {
setMounted(true);
// Generate or retrieve conversation ID only on client
try {
const stored = localStorage.getItem("chatSessionId");
if (stored) return stored;
const newId = crypto.randomUUID();
if (stored) {
setConversationId(stored);
return;
}
// Generate UUID with fallback for browsers without crypto.randomUUID
let newId: string;
if (typeof crypto !== "undefined" && crypto.randomUUID) {
newId = crypto.randomUUID();
} else {
// Fallback UUID generation
newId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
localStorage.setItem("chatSessionId", newId);
return newId;
setConversationId(newId);
} catch (error) {
// localStorage might be disabled or full
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to access localStorage for chat session:', error);
}
setConversationId(`session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
}
return "default";
});
}, []);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
@@ -62,22 +88,55 @@ export default function ChatWidget() {
// Load messages from localStorage
useEffect(() => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem("chatMessages");
if (stored) {
try {
const parsed = JSON.parse(stored);
setMessages(
parsed.map((m: Message) => ({
...m,
text: decodeHtmlEntities(m.text), // Decode HTML entities when loading
timestamp: new Date(m.timestamp),
})),
);
} catch (e) {
console.error("Failed to load chat history", e);
try {
const stored = localStorage.getItem("chatMessages");
if (stored) {
try {
const parsed = JSON.parse(stored);
setMessages(
parsed.map((m: Message) => ({
...m,
text: decodeHtmlEntities(m.text), // Decode HTML entities when loading
timestamp: new Date(m.timestamp),
})),
);
} catch (e) {
if (process.env.NODE_ENV === 'development') {
console.error("Failed to parse chat history", e);
}
// Clear corrupted data
try {
localStorage.removeItem("chatMessages");
} catch {
// Ignore cleanup errors
}
// Add welcome message
setMessages([
{
id: "welcome",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀",
sender: "bot",
timestamp: new Date(),
},
]);
}
} else {
// Add welcome message
setMessages([
{
id: "welcome",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀",
sender: "bot",
timestamp: new Date(),
},
]);
}
} else {
// Add welcome message
} catch (error) {
// localStorage might be disabled
if (process.env.NODE_ENV === 'development') {
console.warn("Failed to load chat history from localStorage:", error);
}
// Add welcome message anyway
setMessages([
{
id: "welcome",
@@ -93,7 +152,14 @@ export default function ChatWidget() {
// Save messages to localStorage
useEffect(() => {
if (typeof window !== "undefined" && messages.length > 0) {
localStorage.setItem("chatMessages", JSON.stringify(messages));
try {
localStorage.setItem("chatMessages", JSON.stringify(messages));
} catch (error) {
// localStorage might be full or disabled
if (process.env.NODE_ENV === 'development') {
console.warn("Failed to save chat messages to localStorage:", error);
}
}
}
}, [messages]);
@@ -204,6 +270,11 @@ export default function ChatWidget() {
]);
};
// Don't render until mounted to prevent hydration mismatch
if (!mounted) {
return null;
}
return (
<>
{/* Chat Button */}

View File

@@ -1,13 +1,21 @@
"use client";
import React, { useEffect, useState, Suspense, lazy } from "react";
import React, { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import dynamic from "next/dynamic";
import { ToastProvider } from "@/components/Toast";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
// Lazy load heavy components to avoid webpack issues
const BackgroundBlobs = lazy(() => import("@/components/BackgroundBlobs"));
const ChatWidget = lazy(() => import("./ChatWidget"));
// Dynamic import with SSR disabled to avoid framer-motion issues
const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), {
ssr: false,
loading: () => null,
});
const ChatWidget = dynamic(() => import("./ChatWidget").catch(() => ({ default: () => null })), {
ssr: false,
loading: () => null,
});
export default function ClientProviders({
children,
@@ -41,17 +49,9 @@ export default function ClientProviders({
return (
<AnalyticsProvider>
<ToastProvider>
{mounted && (
<Suspense fallback={null}>
<BackgroundBlobs />
</Suspense>
)}
{mounted && <BackgroundBlobs />}
<div className="relative z-10">{children}</div>
{mounted && !is404Page && (
<Suspense fallback={null}>
<ChatWidget />
</Suspense>
)}
{mounted && !is404Page && <ChatWidget />}
</ToastProvider>
</AnalyticsProvider>
);

View File

@@ -57,25 +57,42 @@ const AdminPage = () => {
// Check if user is locked out
const checkLockout = useCallback(() => {
const lockoutData = localStorage.getItem('admin_lockout');
if (lockoutData) {
try {
const { timestamp, attempts } = JSON.parse(lockoutData);
const now = Date.now();
if (typeof window === 'undefined') return false;
try {
const lockoutData = localStorage.getItem('admin_lockout');
if (lockoutData) {
try {
const { timestamp, attempts } = JSON.parse(lockoutData);
const now = Date.now();
if (now - timestamp < LOCKOUT_DURATION) {
setAuthState(prev => ({
...prev,
isLocked: true,
attempts,
isLoading: false
}));
return true;
} else {
localStorage.removeItem('admin_lockout');
if (now - timestamp < LOCKOUT_DURATION) {
setAuthState(prev => ({
...prev,
isLocked: true,
attempts,
isLoading: false
}));
return true;
} else {
try {
localStorage.removeItem('admin_lockout');
} catch {
// Ignore errors
}
}
} catch {
try {
localStorage.removeItem('admin_lockout');
} catch {
// Ignore errors
}
}
} catch {
localStorage.removeItem('admin_lockout');
}
} catch (error) {
// localStorage might be disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to check lockout status:', error);
}
}
return false;
@@ -197,7 +214,11 @@ const AdminPage = () => {
attempts: 0,
isLoading: false
}));
localStorage.removeItem('admin_lockout');
try {
localStorage.removeItem('admin_lockout');
} catch {
// Ignore errors
}
} else {
const newAttempts = authState.attempts + 1;
setAuthState(prev => ({
@@ -208,10 +229,17 @@ const AdminPage = () => {
}));
if (newAttempts >= 5) {
localStorage.setItem('admin_lockout', JSON.stringify({
timestamp: Date.now(),
attempts: newAttempts
}));
try {
localStorage.setItem('admin_lockout', JSON.stringify({
timestamp: Date.now(),
attempts: newAttempts
}));
} catch (error) {
// localStorage might be full or disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to save lockout data:', error);
}
}
setAuthState(prev => ({
...prev,
isLocked: true,
@@ -252,7 +280,11 @@ const AdminPage = () => {
<p className="text-stone-500">Too many failed attempts. Please try again in 15 minutes.</p>
<button
onClick={() => {
localStorage.removeItem('admin_lockout');
try {
localStorage.removeItem('admin_lockout');
} catch (error) {
// Ignore errors
}
window.location.reload();
}}
className="mt-4 px-6 py-2 bg-stone-900 text-stone-50 rounded-xl hover:bg-stone-800 transition-colors"