full upgrade

This commit is contained in:
2026-01-10 00:52:08 +01:00
parent b487f4ba75
commit ae37294b06
14 changed files with 2680 additions and 1340 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,17 @@
"use client";
import dynamic from "next/dynamic";
import React from "react";
// Dynamically import the heavy framer-motion component on the client only
const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs"), { ssr: false });
import React, { useEffect, useState } from "react";
import BackgroundBlobs from "@/components/BackgroundBlobs";
export default function BackgroundBlobsClient() {
// Avoid SSR/webpack bailout issues from `next/dynamic({ ssr:false })`
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return <BackgroundBlobs />;
}

View File

@@ -53,8 +53,8 @@ export default function ChatWidget() {
// Helper function to decode HTML entities
const decodeHtmlEntities = (text: string): string => {
if (!text || typeof text !== 'string') return text;
const textarea = document.createElement('textarea');
if (!text || typeof text !== "string") return text;
const textarea = document.createElement("textarea");
textarea.innerHTML = text;
return textarea.value;
};
@@ -129,25 +129,28 @@ export default function ChatWidget() {
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
const errorText = await response.text().catch(() => "Unknown error");
console.error("Chat API error:", {
status: response.status,
statusText: response.statusText,
error: errorText,
});
throw new Error(`Failed to get response: ${response.status} - ${errorText.substring(0, 100)}`);
throw new Error(
`Failed to get response: ${response.status} - ${errorText.substring(0, 100)}`,
);
}
const data = await response.json();
// Log response for debugging (only in development)
if (process.env.NODE_ENV === 'development') {
if (process.env.NODE_ENV === "development") {
console.log("Chat API response:", data);
}
// Decode HTML entities in the reply
let replyText = data.reply || "Sorry, I couldn't process that. Please try again.";
let replyText =
data.reply || "Sorry, I couldn't process that. Please try again.";
// Decode HTML entities client-side (double safety)
replyText = decodeHtmlEntities(replyText);
@@ -218,11 +221,11 @@ export default function ChatWidget() {
setIsOpen(true);
}
}}
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 bg-gradient-to-br from-blue-500 to-purple-600 text-white p-3 rounded-full shadow-2xl hover:shadow-blue-500/50 hover:scale-110 transition-all duration-300 group cursor-pointer"
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 bg-[#5A4E42]/90 backdrop-blur-md text-white p-3 rounded-full shadow-2xl hover:bg-[#4A3F35]/90 hover:scale-110 transition-all duration-300 group cursor-pointer border border-white/10"
aria-label="Open chat"
>
<MessageCircle size={20} />
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full animate-pulse" />
<span className="absolute -top-1 -right-1 w-3 h-3 bg-[#8B7D6F] rounded-full animate-pulse shadow-lg" />
{/* Tooltip */}
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-black/90 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-lg">
@@ -236,26 +239,29 @@ export default function ChatWidget() {
<AnimatePresence>
{isOpen && (
<motion.div
data-chat-widget
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 w-[300px] sm:w-[340px] md:w-[380px] max-w-[calc(100vw-2rem)] h-[450px] sm:h-[500px] md:h-[550px] max-h-[calc(100vh-10rem)] bg-white dark:bg-gray-900 rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-gray-200 dark:border-gray-800"
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 w-[300px] sm:w-[340px] md:w-[380px] max-w-[calc(100vw-2rem)] h-[450px] sm:h-[500px] md:h-[550px] max-h-[calc(100vh-10rem)] bg-[#f5f1e8]/85 backdrop-blur-xl rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-[#8B7D6F]/30 [&_a]:text-inherit [&_a]:no-underline [&_a]:text-[#2A241F] [&_a:hover]:text-[#2A241F] [&_a:visited]:text-[#2A241F] [&_a:active]:text-[#2A241F] [&_*]:outline-none [&_*:focus]:outline-none [&_*:focus-visible]:outline-none"
>
{/* Header */}
<div className="bg-gradient-to-br from-blue-500 to-purple-600 text-white p-3 md:p-4 flex items-center justify-between">
<div className="bg-gradient-to-r from-[#5A4E42]/90 to-[#4A3F35]/90 backdrop-blur-md text-white p-3 md:p-4 flex items-center justify-between border-b border-[#6B5D4F]/30">
<div className="flex items-center gap-3">
<div className="relative">
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
<Sparkles size={20} />
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center ring-2 ring-white/20 shadow-lg">
<Sparkles size={20} className="text-white" />
</div>
<span className="absolute bottom-0 right-0 w-3 h-3 bg-green-400 rounded-full border-2 border-white" />
<span className="absolute bottom-0 right-0 w-3 h-3 bg-[#8B7D6F] rounded-full border-2 border-[#5A4E42] shadow-lg" />
</div>
<div className="min-w-0 flex-1">
<h3 className="font-bold text-sm truncate">
Dennis{'\''}s AI Assistant
<h3 className="font-bold text-sm truncate text-white">
Dennis{"'"}s AI Assistant
</h3>
<p className="text-xs text-white/80 truncate">Always online</p>
<p className="text-xs text-white/90 truncate">
Always online
</p>
</div>
</div>
@@ -278,7 +284,7 @@ export default function ChatWidget() {
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-3 md:p-4 space-y-3 md:space-y-4 bg-gray-50 dark:bg-gray-950">
<div className="flex-1 overflow-y-auto scrollbar-hide p-3 md:p-4 space-y-3 md:space-y-4 bg-transparent">
{messages.map((message) => (
<motion.div
key={message.id}
@@ -287,20 +293,22 @@ export default function ChatWidget() {
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
className={`max-w-[80%] rounded-2xl px-4 py-2.5 backdrop-blur-sm ${
message.sender === "user"
? "bg-gradient-to-br from-blue-500 to-purple-600 text-white"
: "bg-white dark:bg-gray-800 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700"
? "bg-gradient-to-br from-[#5A4E42] to-[#4A3F35] text-white shadow-lg"
: "bg-white/90 backdrop-blur-sm text-[#2A241F] border border-[#8B7D6F]/30 shadow-md"
}`}
>
<p className="text-sm whitespace-pre-wrap break-words">
<p className={`text-sm whitespace-pre-wrap break-words leading-relaxed [&_a]:text-inherit [&_a]:no-underline [&_a]:text-current [&_a:hover]:text-current [&_a:visited]:text-current [&_a:active]:text-current ${
message.sender === "user" ? "text-white" : "text-[#2A241F]"
}`}>
{message.text}
</p>
<p
className={`text-[10px] mt-1 ${
className={`text-[10px] mt-1.5 ${
message.sender === "user"
? "text-white/60"
: "text-gray-500 dark:text-gray-400"
? "text-white/80"
: "text-[#5A4E42]/70"
}`}
>
{message.timestamp.toLocaleTimeString([], {
@@ -319,10 +327,10 @@ export default function ChatWidget() {
animate={{ opacity: 1, y: 0 }}
className="flex justify-start"
>
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl px-4 py-3">
<div className="flex gap-1">
<div className="bg-white/90 backdrop-blur-sm border border-[#8B7D6F]/30 rounded-2xl px-4 py-3 shadow-md">
<div className="flex gap-1.5">
<motion.div
className="w-2 h-2 bg-gray-400 rounded-full"
className="w-2 h-2 bg-[#5A4E42] rounded-full"
animate={{ y: [0, -8, 0] }}
transition={{
duration: 0.6,
@@ -331,7 +339,7 @@ export default function ChatWidget() {
}}
/>
<motion.div
className="w-2 h-2 bg-gray-400 rounded-full"
className="w-2 h-2 bg-[#5A4E42] rounded-full"
animate={{ y: [0, -8, 0] }}
transition={{
duration: 0.6,
@@ -340,7 +348,7 @@ export default function ChatWidget() {
}}
/>
<motion.div
className="w-2 h-2 bg-gray-400 rounded-full"
className="w-2 h-2 bg-[#5A4E42] rounded-full"
animate={{ y: [0, -8, 0] }}
transition={{
duration: 0.6,
@@ -357,7 +365,7 @@ export default function ChatWidget() {
</div>
{/* Input */}
<div className="p-3 md:p-4 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800">
<div className="p-3 md:p-4 bg-[#f5f1e8]/60 backdrop-blur-md border-t border-[#8B7D6F]/25">
<div className="flex gap-2">
<input
ref={inputRef}
@@ -367,12 +375,12 @@ export default function ChatWidget() {
onKeyPress={handleKeyPress}
placeholder="Ask anything..."
disabled={isLoading}
className="flex-1 px-3 md:px-4 py-2 text-sm bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white rounded-full border border-gray-200 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
className="flex-1 px-4 py-2.5 text-sm bg-white/90 backdrop-blur-sm text-[#2A241F] rounded-full border border-[#8B7D6F]/40 focus:outline-none focus:ring-2 focus:ring-[#5A4E42]/20 focus:border-[#5A4E42]/50 focus:bg-white disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#8B7D6F]/70 transition-all focus-visible:ring-[#5A4E42]/20 focus-visible:border-[#5A4E42]/50"
/>
<button
onClick={handleSend}
disabled={!inputValue.trim() || isLoading}
className="p-2 bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full hover:shadow-lg hover:scale-110 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
className="p-2.5 bg-gradient-to-br from-[#5A4E42] to-[#4A3F35] text-white rounded-full hover:from-[#6B5D4F] hover:to-[#5A4E42] hover:shadow-xl hover:scale-110 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-lg"
aria-label="Send message"
>
{isLoading ? (
@@ -384,7 +392,7 @@ export default function ChatWidget() {
</div>
{/* Quick Actions */}
<div className="flex gap-2 mt-2 overflow-x-auto pb-1 scrollbar-hide">
<div className="flex gap-2 mt-2.5 overflow-x-auto pb-1 scrollbar-hide">
{[
"What are Dennis's skills?",
"Tell me about his projects",
@@ -397,7 +405,7 @@ export default function ChatWidget() {
inputRef.current?.focus();
}}
disabled={isLoading}
className="px-2 md:px-3 py-1 text-[10px] md:text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors whitespace-nowrap disabled:opacity-50 flex-shrink-0"
className="px-3 py-1.5 text-[10px] md:text-xs bg-white/80 backdrop-blur-sm text-[#2A241F] rounded-full hover:bg-white/95 border border-[#8B7D6F]/30 transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
>
{suggestion}
</button>

View File

@@ -1,8 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
export function ClientOnly({ children }: { children: React.ReactNode }) {
export default function ClientOnly({ children }: { children: React.ReactNode }) {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {

View File

@@ -0,0 +1,52 @@
"use client";
import React, { useEffect, useState, Suspense, lazy } from "react";
import { usePathname } from "next/navigation";
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"));
export default function ClientProviders({
children,
}: {
children: React.ReactNode;
}) {
const [mounted, setMounted] = useState(false);
const [is404Page, setIs404Page] = useState(false);
useEffect(() => {
setMounted(true);
// Check if we're on a 404 page by looking for the data attribute
const check404 = () => {
if (typeof window !== "undefined") {
const has404Component = document.querySelector('[data-404-page]');
setIs404Page(!!has404Component);
}
};
// Check immediately and after a short delay
check404();
const timeout = setTimeout(check404, 100);
return () => clearTimeout(timeout);
}, []);
return (
<AnalyticsProvider>
<ToastProvider>
{mounted && (
<Suspense fallback={null}>
<BackgroundBlobs />
</Suspense>
)}
<div className="relative z-10">{children}</div>
{mounted && !is404Page && (
<Suspense fallback={null}>
<ChatWidget />
</Suspense>
)}
</ToastProvider>
</AnalyticsProvider>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
"use client";
import React, { useEffect, useState } from "react";
// Lazy load providers to avoid webpack module resolution issues
const AnalyticsProvider = React.lazy(() =>
import("@/components/AnalyticsProvider").then((mod) => ({
default: mod.AnalyticsProvider,
}))
);
const ToastProvider = React.lazy(() =>
import("@/components/Toast").then((mod) => ({
default: mod.ToastProvider,
}))
);
const BackgroundBlobs = React.lazy(() =>
import("@/components/BackgroundBlobs")
);
const ChatWidget = React.lazy(() => import("./ChatWidget"));
export default function RootProviders({
children,
}: {
children: React.ReactNode;
}) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="relative z-10">{children}</div>;
}
return (
<React.Suspense fallback={<div className="relative z-10">{children}</div>}>
<AnalyticsProvider>
<ToastProvider>
<BackgroundBlobs />
<div className="relative z-10">{children}</div>
<ChatWidget />
</ToastProvider>
</AnalyticsProvider>
</React.Suspense>
);
}