feat: fully integrated grid activity and chat
Removed floating overlays. Integrated ActivityFeed and Chat directly into Bento grid cells. Refined layout for maximum clarity and 'Dennis' feel.
This commit is contained in:
@@ -1,31 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
type ActivityFeedComponent = React.ComponentType<Record<string, never>>;
|
|
||||||
|
|
||||||
export default function ActivityFeedClient() {
|
|
||||||
const [Comp, setComp] = useState<ActivityFeedComponent | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const mod = await import("../components/ActivityFeed");
|
|
||||||
const C = (mod as unknown as { default?: ActivityFeedComponent }).default;
|
|
||||||
if (!cancelled && typeof C === "function") {
|
|
||||||
setComp(() => C);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!Comp) return null;
|
|
||||||
return <Comp />;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -54,7 +54,6 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActivityFeedClient />
|
|
||||||
<Header locale={locale} />
|
<Header locale={locale} />
|
||||||
{/* 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>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, BookOpen, MessageSquare, ArrowRight, Send } from "lucide-react";
|
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, BookOpen, MessageSquare, ArrowRight } from "lucide-react";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import type { JSONContent } from "@tiptap/react";
|
import type { JSONContent } from "@tiptap/react";
|
||||||
import RichTextClient from "./RichTextClient";
|
import RichTextClient from "./RichTextClient";
|
||||||
import CurrentlyReading from "./CurrentlyReading";
|
import CurrentlyReading from "./CurrentlyReading";
|
||||||
import ReadBooks from "./ReadBooks";
|
import ReadBooks from "./ReadBooks";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { TechStackCategory, Hobby } from "@/lib/directus";
|
import { TechStackCategory, Hobby } from "@/lib/directus";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ActivityFeed from "./ActivityFeed";
|
import ActivityFeed from "./ActivityFeed";
|
||||||
|
import BentoChat from "./BentoChat";
|
||||||
|
|
||||||
const iconMap: Record<string, any> = {
|
const iconMap: Record<string, any> = {
|
||||||
Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2
|
Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2
|
||||||
@@ -73,7 +74,7 @@ const About = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pt-8">
|
<div className="pt-8">
|
||||||
<div className="inline-block bg-liquid-mint/5 px-8 py-4 rounded-3xl border border-liquid-mint/20">
|
<div className="inline-block bg-liquid-mint/5 px-8 py-4 rounded-3xl border border-liquid-mint/20">
|
||||||
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-liquid-mint mb-2">{t("funFactTitle")}</p>
|
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-liquid-mint mb-2">{t("funFactTitle")}</p>
|
||||||
<p className="text-base font-bold opacity-90">{t("funFactBody")}</p>
|
<p className="text-base font-bold opacity-90">{t("funFactBody")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,41 +110,23 @@ const About = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Design Element */}
|
|
||||||
<div className="absolute -bottom-10 -right-10 w-40 h-40 bg-liquid-mint/10 blur-[80px] rounded-full" />
|
<div className="absolute -bottom-10 -right-10 w-40 h-40 bg-liquid-mint/10 blur-[80px] rounded-full" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 3. AI Chat Box (Direct Integration) */}
|
{/* 3. AI Chat Box (Integrated) */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
className="md:col-span-12 lg:col-span-4 bg-gradient-to-br from-liquid-purple/10 to-liquid-sky/10 dark:from-stone-900 rounded-[3rem] p-10 border border-liquid-purple/30 dark:border-stone-800/60 flex flex-col justify-between group shadow-sm overflow-hidden relative"
|
className="md:col-span-12 lg:col-span-4 bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 flex flex-col shadow-sm"
|
||||||
>
|
>
|
||||||
<div className="relative z-10">
|
<div className="flex items-center gap-2 mb-8">
|
||||||
<div className="flex justify-between items-start mb-10">
|
<MessageSquare className="text-liquid-purple" size={24} />
|
||||||
<div className="w-14 h-14 rounded-2xl bg-white dark:bg-stone-800 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
|
<h3 className="text-2xl font-black text-stone-900 dark:text-stone-50 uppercase tracking-tighter text-liquid-purple">AI Twin</h3>
|
||||||
<MessageSquare className="text-liquid-purple" size={28} />
|
</div>
|
||||||
</div>
|
<div className="flex-1">
|
||||||
<div className="flex -space-x-2">
|
<BentoChat />
|
||||||
{[1,2,3].map(i => <div key={i} className="w-8 h-8 rounded-full border-2 border-white dark:border-stone-900 bg-stone-200 dark:bg-stone-700 overflow-hidden shadow-sm"><img src={`/images/me.jpg`} className="w-full h-full object-cover" /></div>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="text-3xl font-black text-stone-900 dark:text-stone-50 mb-4 tracking-tighter">Ask my AI Twin</h3>
|
|
||||||
<p className="text-stone-600 dark:text-stone-400 text-lg font-light leading-relaxed mb-8">Get instant answers about my stack, availability or experience.</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const chatBtn = document.querySelector('[aria-label="Open chat"]') as HTMLElement;
|
|
||||||
if (chatBtn) chatBtn.click();
|
|
||||||
}}
|
|
||||||
className="w-full py-4 bg-white dark:bg-stone-800 rounded-2xl border border-stone-200 dark:border-stone-700 flex items-center justify-between px-6 font-bold text-stone-400 hover:text-stone-900 dark:hover:text-white transition-all shadow-sm"
|
|
||||||
>
|
|
||||||
<span>Type a message...</span>
|
|
||||||
<Send size={18} className="text-liquid-purple" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -158,10 +141,10 @@ const About = () => {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||||
{techStack.map((cat) => (
|
{techStack.map((cat) => (
|
||||||
<div key={cat.id} className="space-y-6">
|
<div key={cat.id} className="space-y-6">
|
||||||
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-stone-400">{cat.name}</h4>
|
<h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-stone-400">{cat.name}</h4>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{cat.items?.map((item: any) => (
|
{cat.items?.map((item: any) => (
|
||||||
<span key={item.id} className="px-4 py-2 bg-stone-50 dark:bg-stone-800 rounded-xl text-xs font-bold border border-stone-100 dark:border-stone-700/50 hover:border-liquid-mint transition-colors">
|
<span key={item.id} className="px-4 py-2 bg-stone-50 dark:bg-stone-800/50 rounded-xl text-xs font-bold border border-stone-100 dark:border-stone-700/50 hover:border-liquid-mint transition-colors">
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -171,7 +154,7 @@ const About = () => {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 5. Library Link & Hobbies */}
|
{/* 5. Library & Hobbies */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
@@ -191,7 +174,6 @@ const About = () => {
|
|||||||
</div>
|
</div>
|
||||||
<CurrentlyReading />
|
<CurrentlyReading />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-liquid-purple/5 blur-[60px] rounded-full" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col justify-between">
|
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col justify-between">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
101
app/components/BentoChat.tsx
Normal file
101
app/components/BentoChat.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Send, Loader2, MessageSquare, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
sender: "user" | "bot";
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BentoChat() {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [conversationId, setConversationId] = useState<string>("default");
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const storedId = localStorage.getItem("chatSessionId");
|
||||||
|
if (storedId) setConversationId(storedId);
|
||||||
|
else {
|
||||||
|
const newId = crypto.randomUUID();
|
||||||
|
localStorage.setItem("chatSessionId", newId);
|
||||||
|
setConversationId(newId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedMsgs = localStorage.getItem("chatMessages");
|
||||||
|
if (storedMsgs) {
|
||||||
|
setMessages(JSON.parse(storedMsgs).map((m: any) => ({ ...m, timestamp: new Date(m.timestamp) })));
|
||||||
|
} else {
|
||||||
|
setMessages([{ id: "welcome", text: "Hi! Ask me anything about Dennis! 🚀", sender: "bot", timestamp: new Date() }]);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0) localStorage.setItem("chatMessages", JSON.stringify(messages));
|
||||||
|
scrollRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!inputValue.trim() || isLoading) return;
|
||||||
|
const userMsg: Message = { id: Date.now().toString(), text: inputValue.trim(), sender: "user", timestamp: new Date() };
|
||||||
|
setMessages(prev => [...prev, userMsg]);
|
||||||
|
setInputValue("");
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/n8n/chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ message: userMsg.text, conversationId, history: messages.slice(-5).map(m => ({ role: m.sender === "user" ? "user" : "assistant", content: m.text })) }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), text: data.reply || "Error", sender: "bot", timestamp: new Date() }]);
|
||||||
|
} catch {
|
||||||
|
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), text: "Connection error.", sender: "bot", timestamp: new Date() }]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(true);
|
||||||
|
setTimeout(() => setIsLoading(false), 500); // Small delay for feel
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full min-h-[300px]">
|
||||||
|
<div className="flex-1 overflow-y-auto pr-2 scrollbar-hide space-y-4 mb-4">
|
||||||
|
{messages.map((m) => (
|
||||||
|
<div key={m.id} className={`flex ${m.sender === "user" ? "justify-end" : "justify-start"}`}>
|
||||||
|
<div className={`max-w-[90%] rounded-2xl px-4 py-2 text-sm shadow-sm ${m.sender === "user" ? "bg-liquid-purple text-white" : "bg-white dark:bg-stone-800 text-stone-900 dark:text-stone-100 border border-stone-100 dark:border-stone-700"}`}>
|
||||||
|
{m.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="bg-stone-100 dark:bg-stone-800 rounded-2xl px-4 py-2"><Loader2 size={14} className="animate-spin text-stone-400" /></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={scrollRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||||
|
placeholder="Ask me..."
|
||||||
|
className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-all shadow-inner dark:text-white"
|
||||||
|
/>
|
||||||
|
<button onClick={handleSend} className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-liquid-purple hover:scale-110 transition-transform">
|
||||||
|
<Send size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,11 +15,6 @@ const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").cat
|
|||||||
loading: () => null,
|
loading: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChatWidget = dynamic(() => import("./ChatWidget").catch(() => ({ default: () => null })), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ClientProviders({
|
export default function ClientProviders({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -107,7 +102,6 @@ function GatedProviders({
|
|||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{mounted && <BackgroundBlobs />}
|
{mounted && <BackgroundBlobs />}
|
||||||
<div className="relative z-10">{children}</div>
|
<div className="relative z-10">{children}</div>
|
||||||
{mounted && !is404Page && !isAdminRoute && chatEnabled && <ChatWidget />}
|
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user