diff --git a/app/api/analytics/track/route.ts b/app/api/analytics/track/route.ts index abb84e8..a06a34f 100644 --- a/app/api/analytics/track/route.ts +++ b/app/api/analytics/track/route.ts @@ -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)?.lcp || 0, - fid: fid || (perf.coreWebVitals as Record)?.fid || 0, - cls: cls || (perf.coreWebVitals as Record)?.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)?.lcp || 0, + fid: fid || (perf.coreWebVitals as Record)?.fid || 0, + cls: cls || (perf.coreWebVitals as Record)?.cls || 0 + }, + lastUpdated: new Date().toISOString() }, - lastUpdated: new Date().toISOString() - }, - analytics: { - ...analytics, - lastUpdated: new Date().toISOString() + analytics: { + ...analytics, + lastUpdated: new Date().toISOString() + } } - } - }); + }); + } } } } diff --git a/app/components/ActivityFeed.tsx b/app/components/ActivityFeed.tsx index d174949..b49694c 100644 --- a/app/components/ActivityFeed.tsx +++ b/app/components/ActivityFeed.tsx @@ -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) { diff --git a/app/components/ChatWidget.tsx b/app/components/ChatWidget.tsx index 09272c8..27722e2 100644 --- a/app/components/ChatWidget.tsx +++ b/app/components/ChatWidget.tsx @@ -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([]); 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("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(null); const inputRef = useRef(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 */} diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index 8f7588d..df361b3 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -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 ( - {mounted && ( - - - - )} + {mounted && }
{children}
- {mounted && !is404Page && ( - - - - )} + {mounted && !is404Page && }
); diff --git a/app/manage/page.tsx b/app/manage/page.tsx index 071afc8..b4a6c3c 100644 --- a/app/manage/page.tsx +++ b/app/manage/page.tsx @@ -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 = () => {

Too many failed attempts. Please try again in 15 minutes.