full upgrade to dev

This commit is contained in:
2026-01-07 14:30:00 +01:00
parent 4dc727fcd6
commit 26a8610aa7
34 changed files with 6890 additions and 920 deletions

View File

@@ -1,48 +1,270 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Music, Code, Monitor, MessageSquare, Send, X } from 'lucide-react';
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Music,
Code,
Monitor,
MessageSquare,
Send,
X,
Loader2,
Github,
Tv,
Gamepad2,
Coffee,
Headphones,
Terminal,
Sparkles,
ExternalLink,
Activity,
Waves,
Zap,
} from "lucide-react";
interface ActivityData {
activity: {
type: 'coding' | 'listening' | 'watching';
type:
| "coding"
| "listening"
| "watching"
| "gaming"
| "reading"
| "running";
details: string;
timestamp: string;
project?: string;
language?: string;
repo?: string;
link?: string;
} | null;
music: {
isPlaying: boolean;
track: string;
artist: string;
platform: 'spotify' | 'apple';
album?: string;
platform: "spotify" | "apple";
progress?: number;
albumArt?: string;
spotifyUrl?: string;
} | null;
watching: {
title: string;
platform: 'youtube' | 'netflix';
platform: "youtube" | "netflix" | "twitch";
type: "video" | "stream" | "movie" | "series";
} | null;
gaming: {
game: string;
platform: "steam" | "playstation" | "xbox";
status: "playing" | "idle";
} | null;
status: {
mood: string;
customMessage?: string;
} | null;
}
// Matrix rain effect for coding
const MatrixRain = () => {
const chars = "01";
return (
<div className="absolute inset-0 overflow-hidden opacity-20 pointer-events-none">
{[...Array(15)].map((_, i) => (
<motion.div
key={i}
className="absolute text-liquid-mint font-mono text-xs"
style={{ left: `${(i / 15) * 100}%` }}
animate={{
y: ["-100%", "200%"],
opacity: [0, 1, 0],
}}
transition={{
duration: Math.random() * 3 + 2,
repeat: Infinity,
delay: Math.random() * 2,
ease: "linear",
}}
>
{[...Array(20)].map((_, j) => (
<div key={j}>{chars[Math.floor(Math.random() * chars.length)]}</div>
))}
</motion.div>
))}
</div>
);
};
// Sound waves for music
const SoundWaves = () => {
return (
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
{[...Array(5)].map((_, i) => (
<motion.div
key={i}
className="absolute w-1 bg-gradient-to-t from-liquid-rose to-liquid-coral rounded-full"
style={{ left: `${20 + i * 15}%` }}
animate={{
height: ["20%", "80%", "20%"],
}}
transition={{
duration: 0.8,
repeat: Infinity,
delay: i * 0.1,
ease: "easeInOut",
}}
/>
))}
</div>
);
};
// Running animation
const RunningAnimation = () => {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute bottom-2 text-4xl"
animate={{
x: ["-10%", "110%"],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear",
}}
>
🏃
</motion.div>
<div className="absolute bottom-2 left-0 right-0 h-0.5 bg-liquid-lime/30" />
</div>
);
};
// Gaming particles
const GamingParticles = () => {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{[...Array(10)].map((_, i) => (
<motion.div
key={i}
className="absolute w-2 h-2 bg-liquid-peach/60 rounded-full"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
}}
animate={{
scale: [0, 1, 0],
opacity: [0, 1, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
delay: Math.random() * 2,
ease: "easeInOut",
}}
/>
))}
</div>
);
};
// TV scan lines
const TVScanLines = () => {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute inset-0 bg-gradient-to-b from-transparent via-white/10 to-transparent h-8"
animate={{
y: ["-100%", "200%"],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear",
}}
/>
</div>
);
};
const activityIcons = {
coding: Terminal,
listening: Headphones,
watching: Tv,
gaming: Gamepad2,
reading: Coffee,
running: Activity,
};
const activityColors = {
coding: {
bg: "from-liquid-mint/20 to-liquid-sky/20",
border: "border-liquid-mint/40",
text: "text-liquid-mint",
pulse: "bg-green-500",
},
listening: {
bg: "from-liquid-rose/20 to-liquid-coral/20",
border: "border-liquid-rose/40",
text: "text-liquid-rose",
pulse: "bg-red-500",
},
watching: {
bg: "from-liquid-lavender/20 to-liquid-pink/20",
border: "border-liquid-lavender/40",
text: "text-liquid-lavender",
pulse: "bg-purple-500",
},
gaming: {
bg: "from-liquid-peach/20 to-liquid-yellow/20",
border: "border-liquid-peach/40",
text: "text-liquid-peach",
pulse: "bg-orange-500",
},
reading: {
bg: "from-liquid-teal/20 to-liquid-lime/20",
border: "border-liquid-teal/40",
text: "text-liquid-teal",
pulse: "bg-teal-500",
},
running: {
bg: "from-liquid-lime/20 to-liquid-mint/20",
border: "border-liquid-lime/40",
text: "text-liquid-lime",
pulse: "bg-lime-500",
},
};
export const ActivityFeed = () => {
const [data, setData] = useState<ActivityData | null>(null);
const [showChat, setShowChat] = useState(false);
const [chatMessage, setChatMessage] = useState('');
const [chatHistory, setChatHistory] = useState<{
role: 'user' | 'ai';
text: string;
}[]>([
{ role: 'ai', text: 'Hi! I am Dennis\'s AI assistant. Ask me anything about him!' }
const [chatMessage, setChatMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [chatHistory, setChatHistory] = useState<
{
role: "user" | "ai";
text: string;
timestamp: number;
}[]
>([
{
role: "ai",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his work, skills, or projects! 🚀",
timestamp: Date.now(),
},
]);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('/api/n8n/status');
const res = await fetch("/api/n8n/status");
if (res.ok) {
const json = await res.json();
setData(json);
}
} catch (e) {
console.error('Failed to fetch activity', e);
console.error("Failed to fetch activity", e);
}
};
fetchData();
@@ -50,67 +272,306 @@ export const ActivityFeed = () => {
return () => clearInterval(interval);
}, []);
const handleSendMessage = (e: React.FormEvent) => {
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!chatMessage.trim()) return;
if (!chatMessage.trim() || isLoading) return;
const userMsg = chatMessage;
setChatHistory(prev => [...prev, { role: 'user', text: userMsg }]);
setChatMessage('');
setChatHistory((prev) => [
...prev,
{ role: "user", text: userMsg, timestamp: Date.now() },
]);
setChatMessage("");
setIsLoading(true);
// Mock AI response - would connect to n8n webhook
setTimeout(() => {
setChatHistory(prev => [...prev, { role: 'ai', text: `That's a great question about "${userMsg}"! I'll ask Dennis to add more info about that.` }]);
}, 1000);
try {
const response = await fetch("/api/n8n/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMsg }),
});
if (response.ok) {
const data = await response.json();
setChatHistory((prev) => [
...prev,
{ role: "ai", text: data.reply, timestamp: Date.now() },
]);
} else {
throw new Error("Chat API failed");
}
} catch (error) {
console.error("Chat error:", error);
setChatHistory((prev) => [
...prev,
{
role: "ai",
text: "Sorry, I encountered an error. Please try again later.",
timestamp: Date.now(),
},
]);
} finally {
setIsLoading(false);
}
};
if (!data) return null;
const renderActivityBubble = () => {
if (!data?.activity) return null;
const { type, details, project, language, link } = data.activity;
const Icon = activityIcons[type];
const colors = activityColors[type];
return (
<motion.div
initial={{ x: 50, opacity: 0, scale: 0.8 }}
animate={{ x: 0, opacity: 1, scale: 1 }}
exit={{ x: 50, opacity: 0, scale: 0.8 }}
transition={{ duration: 0.8, ease: [0.25, 0.1, 0.25, 1] }}
className={`relative bg-gradient-to-r ${colors.bg} backdrop-blur-md border-2 ${colors.border} shadow-lg rounded-2xl px-5 py-3 flex items-start gap-3 text-sm text-stone-800 max-w-xs overflow-hidden`}
>
{/* Background Animation based on activity type */}
{type === "coding" && <MatrixRain />}
{type === "running" && <RunningAnimation />}
{type === "gaming" && <GamingParticles />}
{type === "watching" && <TVScanLines />}
<div className="relative z-10 flex-shrink-0 mt-1">
<span className="relative flex h-3 w-3">
<span
className={`animate-ping absolute inline-flex h-full w-full rounded-full ${colors.pulse} opacity-75`}
></span>
<span
className={`relative inline-flex rounded-full h-3 w-3 ${colors.pulse}`}
></span>
</span>
</div>
<div className="relative z-10 flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<motion.div
animate={
type === "coding"
? { rotate: [0, 360] }
: type === "running"
? { scale: [1, 1.2, 1] }
: {}
}
transition={{
duration: type === "coding" ? 2 : 1,
repeat: Infinity,
ease: "linear",
}}
>
<Icon size={16} className={colors.text} />
</motion.div>
<span className="font-semibold capitalize">{type}</span>
</div>
<p className="text-stone-900 font-medium truncate">{details}</p>
{project && (
<p className="text-stone-600 text-xs mt-1 flex items-center gap-1">
<Github size={12} />
{project}
</p>
)}
{language && (
<span className="inline-block mt-2 px-2 py-0.5 bg-white/60 rounded text-xs text-stone-700 border border-stone-200 font-mono">
{language}
</span>
)}
{link && (
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 mt-2 text-xs text-stone-700 hover:text-stone-900 underline"
>
View <ExternalLink size={10} />
</a>
)}
</div>
</motion.div>
);
};
const renderMusicBubble = () => {
if (!data?.music?.isPlaying) return null;
const { track, artist, album, progress, albumArt, spotifyUrl } = data.music;
return (
<motion.div
initial={{ x: 50, opacity: 0, scale: 0.8 }}
animate={{ x: 0, opacity: 1, scale: 1 }}
exit={{ x: 50, opacity: 0, scale: 0.8 }}
transition={{ duration: 0.8, delay: 0.15, ease: [0.25, 0.1, 0.25, 1] }}
className="relative bg-gradient-to-r from-liquid-rose/20 to-liquid-coral/20 backdrop-blur-md border-2 border-liquid-rose/40 shadow-lg rounded-2xl px-5 py-3 flex items-center gap-3 text-sm text-stone-800 max-w-xs overflow-hidden"
>
{/* Animated sound waves background */}
<SoundWaves />
{albumArt && (
<motion.div
animate={{ rotate: [0, 360] }}
transition={{ duration: 10, repeat: Infinity, ease: "linear" }}
className="relative z-10 w-14 h-14 rounded-full overflow-hidden flex-shrink-0 border-2 border-white shadow-md"
>
<img
src={albumArt}
alt={album || track}
className="w-full h-full object-cover"
/>
</motion.div>
)}
<div className="relative z-10 flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 1, repeat: Infinity, ease: "easeInOut" }}
>
<Headphones size={16} className="text-liquid-rose" />
</motion.div>
<span className="font-semibold">Now Playing</span>
</div>
<p className="text-stone-900 font-bold text-sm truncate">{track}</p>
<p className="text-stone-600 text-xs truncate">{artist}</p>
{progress !== undefined && (
<div className="mt-2 w-full bg-white/50 rounded-full h-1.5 overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-liquid-rose to-liquid-coral"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5 }}
/>
</div>
)}
{spotifyUrl && (
<a
href={spotifyUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 mt-2 text-xs text-stone-700 hover:text-stone-900 underline"
>
<Waves size={10} />
Listen with me
</a>
)}
</div>
</motion.div>
);
};
const renderStatusBubble = () => {
if (!data?.status) return null;
const { mood, customMessage } = data.status;
return (
<motion.div
initial={{ x: 50, opacity: 0, scale: 0.8 }}
animate={{ x: 0, opacity: 1, scale: 1 }}
exit={{ x: 50, opacity: 0, scale: 0.8 }}
transition={{ duration: 0.8, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="relative bg-gradient-to-r from-liquid-lavender/20 to-liquid-pink/20 backdrop-blur-md border-2 border-liquid-lavender/40 shadow-lg rounded-2xl px-5 py-3 flex items-center gap-3 text-sm text-stone-800 max-w-xs overflow-hidden"
>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
className="text-3xl flex-shrink-0"
>
{mood}
</motion.div>
<div className="flex-1 min-w-0">
{customMessage && (
<p className="text-stone-900 font-medium text-sm">
{customMessage}
</p>
)}
</div>
</motion.div>
);
};
return (
<div className="fixed bottom-6 right-6 z-40 flex flex-col items-end gap-4 pointer-events-none">
<div className="fixed bottom-6 right-6 z-[9999] flex flex-col items-end gap-4 pointer-events-none">
{/* Chat Window */}
<AnimatePresence>
{showChat && (
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="pointer-events-auto bg-white/80 backdrop-blur-xl border border-white/60 shadow-2xl rounded-2xl w-80 overflow-hidden"
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}
className="pointer-events-auto bg-white/95 backdrop-blur-xl border-2 border-stone-200 shadow-2xl rounded-2xl w-96 max-w-[calc(100vw-3rem)] overflow-hidden"
>
<div className="p-4 border-b border-white/50 flex justify-between items-center bg-white/40">
<span className="font-semibold text-stone-800 flex items-center gap-2">
<MessageSquare size={16} />
Ask me anything
<div className="p-4 border-b-2 border-stone-200 flex justify-between items-center bg-gradient-to-r from-liquid-mint/10 to-liquid-sky/10">
<span className="font-bold text-stone-900 flex items-center gap-2">
<Sparkles size={18} className="text-liquid-mint" />
AI Assistant
</span>
<button onClick={() => setShowChat(false)} className="text-stone-500 hover:text-stone-800">
<X size={16} />
<button
onClick={() => setShowChat(false)}
className="text-stone-500 hover:text-stone-900 transition-colors duration-300 p-1 hover:bg-stone-100 rounded-lg"
>
<X size={18} />
</button>
</div>
<div className="h-64 overflow-y-auto p-4 space-y-3 bg-white/20">
<div className="h-96 overflow-y-auto p-4 space-y-3 bg-gradient-to-b from-stone-50/50 to-white/50">
{chatHistory.map((msg, i) => (
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] p-3 rounded-xl text-sm ${
msg.role === 'user'
? 'bg-stone-800 text-white rounded-tr-none'
: 'bg-white text-stone-800 shadow-sm rounded-tl-none'
}`}>
<motion.div
key={`chat-${msg.timestamp}-${i}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: i * 0.05 }}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[85%] p-3 rounded-2xl text-sm ${
msg.role === "user"
? "bg-gradient-to-br from-stone-800 to-stone-900 text-white rounded-tr-none shadow-md"
: "bg-gradient-to-br from-white to-stone-50 text-stone-900 shadow-md rounded-tl-none border-2 border-stone-100"
}`}
>
{msg.text}
</div>
</div>
</motion.div>
))}
{isLoading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex justify-start"
>
<div className="max-w-[85%] p-3 rounded-2xl text-sm bg-gradient-to-br from-white to-stone-50 text-stone-900 shadow-md rounded-tl-none border-2 border-stone-100 flex items-center gap-2">
<Loader2
size={14}
className="animate-spin text-liquid-mint"
/>
<span>Thinking...</span>
</div>
</motion.div>
)}
</div>
<form onSubmit={handleSendMessage} className="p-3 border-t border-white/50 bg-white/40 flex gap-2">
<input
type="text"
<form
onSubmit={handleSendMessage}
className="p-4 border-t-2 border-stone-200 bg-gradient-to-r from-liquid-mint/5 to-liquid-sky/5 flex gap-2"
>
<input
type="text"
value={chatMessage}
onChange={(e) => setChatMessage(e.target.value)}
placeholder="Type a message..."
className="flex-1 bg-white/60 border border-white/60 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
placeholder="Ask me anything..."
disabled={isLoading}
className="flex-1 bg-white border-2 border-stone-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-mint focus:border-transparent disabled:opacity-50 transition-all duration-300"
/>
<button type="submit" className="p-2 bg-stone-800 text-white rounded-lg hover:bg-black transition-colors">
<Send size={16} />
</button>
<motion.button
type="submit"
disabled={isLoading || !chatMessage.trim()}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="p-3 bg-gradient-to-br from-stone-900 to-stone-800 text-white rounded-xl hover:from-black hover:to-stone-900 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
>
<Send size={18} />
</motion.button>
</form>
</motion.div>
)}
@@ -118,41 +579,40 @@ export const ActivityFeed = () => {
{/* Activity Bubbles */}
<div className="flex flex-col items-end gap-2 pointer-events-auto">
{data.activity?.type === 'coding' && (
<motion.div
initial={{ x: 50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className="bg-white/80 backdrop-blur-md border border-white/60 shadow-lg rounded-full px-4 py-2 flex items-center gap-2 text-sm text-stone-700"
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<Code size={14} />
<span>Working on <strong>{data.activity.details}</strong></span>
</motion.div>
)}
<AnimatePresence mode="wait">
{renderActivityBubble()}
{renderMusicBubble()}
{renderStatusBubble()}
</AnimatePresence>
{data.music?.isPlaying && (
<motion.div
initial={{ x: 50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="bg-white/80 backdrop-blur-md border border-white/60 shadow-lg rounded-full px-4 py-2 flex items-center gap-2 text-sm text-stone-700"
>
<Music size={14} className="animate-pulse text-liquid-rose" />
<span>Listening to <strong>{data.music.track}</strong></span>
</motion.div>
)}
{/* Chat Toggle Button */}
{/* Chat Toggle Button with Notification Indicator */}
<motion.button
whileHover={{ scale: 1.05 }}
whileHover={{ scale: 1.08, rotate: 5 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.4, ease: "easeOut" }}
onClick={() => setShowChat(!showChat)}
className="bg-stone-900 text-white rounded-full p-4 shadow-xl hover:bg-black transition-all"
className="relative bg-stone-900 text-white rounded-full p-4 shadow-xl hover:bg-stone-950 transition-all duration-500 ease-out"
title="Ask me anything about Dennis"
>
<MessageSquare size={20} />
{!showChat && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5, ease: "easeOut", delay: 0.2 }}
className="absolute -top-1 -right-1 w-3 h-3 bg-liquid-mint rounded-full border-2 border-white"
>
<motion.span
animate={{ scale: [1, 1.3, 1] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
className="absolute inset-0 bg-liquid-mint rounded-full"
/>
</motion.span>
)}
</motion.button>
</div>
</div>