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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user