|
|
|
@@ -1,642 +1,260 @@
|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
|
import {
|
|
|
|
|
Music,
|
|
|
|
|
Code,
|
|
|
|
|
Monitor,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
Send,
|
|
|
|
|
X,
|
|
|
|
|
Loader2,
|
|
|
|
|
Github,
|
|
|
|
|
Tv,
|
|
|
|
|
Code2,
|
|
|
|
|
Disc3,
|
|
|
|
|
Gamepad2,
|
|
|
|
|
Coffee,
|
|
|
|
|
Headphones,
|
|
|
|
|
Terminal,
|
|
|
|
|
Sparkles,
|
|
|
|
|
ExternalLink,
|
|
|
|
|
Activity,
|
|
|
|
|
Waves,
|
|
|
|
|
Cpu,
|
|
|
|
|
Zap,
|
|
|
|
|
Clock,
|
|
|
|
|
Music
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
|
|
|
|
interface ActivityData {
|
|
|
|
|
activity: {
|
|
|
|
|
type:
|
|
|
|
|
| "coding"
|
|
|
|
|
| "listening"
|
|
|
|
|
| "watching"
|
|
|
|
|
| "gaming"
|
|
|
|
|
| "reading"
|
|
|
|
|
| "running";
|
|
|
|
|
details: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
project?: string;
|
|
|
|
|
language?: string;
|
|
|
|
|
repo?: string;
|
|
|
|
|
link?: string;
|
|
|
|
|
} | null;
|
|
|
|
|
// Types passend zu deinem n8n Output
|
|
|
|
|
interface StatusData {
|
|
|
|
|
status: {
|
|
|
|
|
text: string;
|
|
|
|
|
color: string;
|
|
|
|
|
};
|
|
|
|
|
music: {
|
|
|
|
|
isPlaying: boolean;
|
|
|
|
|
track: string;
|
|
|
|
|
artist: string;
|
|
|
|
|
album?: string;
|
|
|
|
|
platform: "spotify" | "apple";
|
|
|
|
|
progress?: number;
|
|
|
|
|
albumArt?: string;
|
|
|
|
|
spotifyUrl?: string;
|
|
|
|
|
} | null;
|
|
|
|
|
watching: {
|
|
|
|
|
title: string;
|
|
|
|
|
platform: "youtube" | "netflix" | "twitch";
|
|
|
|
|
type: "video" | "stream" | "movie" | "series";
|
|
|
|
|
album: string;
|
|
|
|
|
albumArt: string;
|
|
|
|
|
url: string;
|
|
|
|
|
} | null;
|
|
|
|
|
gaming: {
|
|
|
|
|
game: string;
|
|
|
|
|
platform: "steam" | "playstation" | "xbox";
|
|
|
|
|
status: "playing" | "idle";
|
|
|
|
|
isPlaying: boolean;
|
|
|
|
|
name: string;
|
|
|
|
|
image: string | null;
|
|
|
|
|
state?: string;
|
|
|
|
|
details?: string;
|
|
|
|
|
} | null;
|
|
|
|
|
status: {
|
|
|
|
|
mood: string;
|
|
|
|
|
customMessage?: string;
|
|
|
|
|
coding: {
|
|
|
|
|
isActive: boolean;
|
|
|
|
|
project?: string;
|
|
|
|
|
file?: string;
|
|
|
|
|
stats?: {
|
|
|
|
|
time: string;
|
|
|
|
|
topLang: string;
|
|
|
|
|
topProject: 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 with smooth wavy motion
|
|
|
|
|
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%"],
|
|
|
|
|
y: [0, -10, -5, -12, -3, -10, 0, -8, -2, -10, 0],
|
|
|
|
|
}}
|
|
|
|
|
transition={{
|
|
|
|
|
x: {
|
|
|
|
|
duration: 1.2,
|
|
|
|
|
repeat: Infinity,
|
|
|
|
|
ease: "linear",
|
|
|
|
|
},
|
|
|
|
|
y: {
|
|
|
|
|
duration: 0.4,
|
|
|
|
|
repeat: Infinity,
|
|
|
|
|
ease: [0.25, 0.1, 0.25, 1], // Smooth cubic bezier for wavy effect
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
🏃
|
|
|
|
|
</motion.div>
|
|
|
|
|
<motion.div
|
|
|
|
|
className="absolute bottom-2 left-0 right-0 h-0.5 bg-liquid-lime/30"
|
|
|
|
|
animate={{
|
|
|
|
|
opacity: [0.3, 0.6, 0.3],
|
|
|
|
|
}}
|
|
|
|
|
transition={{
|
|
|
|
|
duration: 0.4,
|
|
|
|
|
repeat: Infinity,
|
|
|
|
|
ease: "easeInOut",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</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 [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(),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
export default function ActivityFeed() {
|
|
|
|
|
const [data, setData] = useState<StatusData | null>(null);
|
|
|
|
|
|
|
|
|
|
// Daten abrufen (alle 10 Sekunden für schnelleres Feedback)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/n8n/status");
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
setData(json);
|
|
|
|
|
}
|
|
|
|
|
if (!res.ok) return;
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
setData(json);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
|
|
|
console.error("Failed to fetch activity", e);
|
|
|
|
|
}
|
|
|
|
|
console.error("Failed to fetch activity", e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchData();
|
|
|
|
|
const interval = setInterval(fetchData, 30000); // Poll every 30s
|
|
|
|
|
const interval = setInterval(fetchData, 10000); // 10s Refresh
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleSendMessage = async (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!chatMessage.trim() || isLoading) return;
|
|
|
|
|
|
|
|
|
|
const userMsg = chatMessage;
|
|
|
|
|
setChatHistory((prev) => [
|
|
|
|
|
...prev,
|
|
|
|
|
{ role: "user", text: userMsg, timestamp: Date.now() },
|
|
|
|
|
]);
|
|
|
|
|
setChatMessage("");
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
if (!data) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed bottom-6 right-6 z-[9999] flex flex-col items-end gap-4 pointer-events-none">
|
|
|
|
|
{/* Chat Window */}
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
{showChat && (
|
|
|
|
|
<div className="fixed bottom-6 right-6 flex flex-col items-end gap-3 z-50 font-sans pointer-events-none">
|
|
|
|
|
<AnimatePresence mode="popLayout">
|
|
|
|
|
|
|
|
|
|
{/* --------------------------------------------------------------------------------
|
|
|
|
|
1. CODING CARD
|
|
|
|
|
Zeigt entweder "Live Coding" (Grün) oder "Tagesstatistik" (Grau/Blau)
|
|
|
|
|
-------------------------------------------------------------------------------- */}
|
|
|
|
|
{data.coding && (
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
|
|
|
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"
|
|
|
|
|
key="coding"
|
|
|
|
|
initial={{ opacity: 0, x: 20, scale: 0.95 }}
|
|
|
|
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
|
|
|
|
exit={{ opacity: 0, x: 20, scale: 0.95 }}
|
|
|
|
|
layout
|
|
|
|
|
className={`pointer-events-auto backdrop-blur-xl border p-3 rounded-2xl flex items-center gap-3 w-72 shadow-2xl transition-colors
|
|
|
|
|
${data.coding.isActive
|
|
|
|
|
? "bg-black/80 border-green-500/20 shadow-green-900/10"
|
|
|
|
|
: "bg-black/60 border-white/10"}`}
|
|
|
|
|
>
|
|
|
|
|
<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-900 transition-colors duration-300 p-1 hover:bg-stone-100 rounded-lg"
|
|
|
|
|
>
|
|
|
|
|
<X size={18} />
|
|
|
|
|
</button>
|
|
|
|
|
{/* Icon Box */}
|
|
|
|
|
<div className={`shrink-0 p-2.5 rounded-xl border flex items-center justify-center
|
|
|
|
|
${data.coding.isActive
|
|
|
|
|
? "bg-green-500/10 border-green-500/20 text-green-400"
|
|
|
|
|
: "bg-white/5 border-white/10 text-gray-400"}`}
|
|
|
|
|
>
|
|
|
|
|
{data.coding.isActive ? <Zap size={18} fill="currentColor" /> : <Code2 size={18} />}
|
|
|
|
|
</div>
|
|
|
|
|
<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) => (
|
|
|
|
|
<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-700 to-stone-600 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 className="flex flex-col min-w-0">
|
|
|
|
|
{data.coding.isActive ? (
|
|
|
|
|
// --- LIVE STATUS ---
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex items-center gap-2 mb-0.5">
|
|
|
|
|
<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>
|
|
|
|
|
<span className="text-[10px] font-bold text-green-400 uppercase tracking-widest">
|
|
|
|
|
Coding Now
|
|
|
|
|
</span>
|
|
|
|
|
</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>
|
|
|
|
|
<span className="font-bold text-sm text-white truncate">
|
|
|
|
|
{data.coding.project || "Unknown Project"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-xs text-white/50 truncate">
|
|
|
|
|
{data.coding.file || "Writing code..."}
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
// --- STATS STATUS ---
|
|
|
|
|
<>
|
|
|
|
|
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-0.5 flex items-center gap-1">
|
|
|
|
|
<Clock size={10} /> Today's Stats
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-bold text-sm text-white">
|
|
|
|
|
{data.coding.stats?.time || "0m"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-xs text-white/50 truncate">
|
|
|
|
|
Focus: {data.coding.stats?.topLang}
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<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="Ask me anything..."
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
className="flex-1 bg-white border-2 border-stone-200 rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-liquid-mint focus:border-transparent disabled:opacity-50 transition-all duration-300"
|
|
|
|
|
/>
|
|
|
|
|
<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-700 to-stone-600 text-white rounded-xl hover:from-stone-600 hover:to-stone-500 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
|
|
|
|
|
>
|
|
|
|
|
<Send size={18} />
|
|
|
|
|
</motion.button>
|
|
|
|
|
</form>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
|
|
|
|
{/* Activity Bubbles */}
|
|
|
|
|
<div className="flex flex-col items-end gap-2 pointer-events-auto">
|
|
|
|
|
<AnimatePresence mode="wait">
|
|
|
|
|
{renderActivityBubble()}
|
|
|
|
|
{renderMusicBubble()}
|
|
|
|
|
{renderStatusBubble()}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
|
|
|
|
{/* Chat Toggle Button with Notification Indicator */}
|
|
|
|
|
<motion.button
|
|
|
|
|
whileHover={{ scale: 1.08, rotate: 5 }}
|
|
|
|
|
whileTap={{ scale: 0.95 }}
|
|
|
|
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
|
|
|
onClick={() => setShowChat(!showChat)}
|
|
|
|
|
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"
|
|
|
|
|
{/* --------------------------------------------------------------------------------
|
|
|
|
|
2. GAMING CARD
|
|
|
|
|
Erscheint nur, wenn du spielst
|
|
|
|
|
-------------------------------------------------------------------------------- */}
|
|
|
|
|
{data.gaming?.isPlaying && (
|
|
|
|
|
<motion.div
|
|
|
|
|
key="gaming"
|
|
|
|
|
initial={{ opacity: 0, x: 20, scale: 0.95 }}
|
|
|
|
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
|
|
|
|
exit={{ opacity: 0, x: 20, scale: 0.95 }}
|
|
|
|
|
layout
|
|
|
|
|
className="pointer-events-auto bg-indigo-950/80 backdrop-blur-xl border border-indigo-500/20 p-3 rounded-2xl flex items-center gap-3 w-72 shadow-2xl relative overflow-hidden"
|
|
|
|
|
>
|
|
|
|
|
{/* Background Glow */}
|
|
|
|
|
<div className="absolute -right-4 -top-4 w-24 h-24 bg-indigo-500/20 blur-2xl rounded-full pointer-events-none" />
|
|
|
|
|
|
|
|
|
|
<div className="relative shrink-0">
|
|
|
|
|
{data.gaming.image ? (
|
|
|
|
|
<img
|
|
|
|
|
src={data.gaming.image}
|
|
|
|
|
alt="Game Art"
|
|
|
|
|
className="w-12 h-12 rounded-lg shadow-sm object-cover bg-indigo-900"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="w-12 h-12 rounded-lg bg-indigo-500/20 flex items-center justify-center">
|
|
|
|
|
<Gamepad2 className="text-indigo-400" size={24} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col min-w-0 z-10">
|
|
|
|
|
<span className="text-[10px] font-bold text-indigo-300 uppercase tracking-widest mb-0.5">
|
|
|
|
|
In Game
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-bold text-sm text-white truncate">
|
|
|
|
|
{data.gaming.name}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-xs text-indigo-200/60 truncate">
|
|
|
|
|
{data.gaming.details || data.gaming.state || "Playing..."}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* --------------------------------------------------------------------------------
|
|
|
|
|
3. MUSIC CARD (Spotify)
|
|
|
|
|
Erscheint nur, wenn Musik läuft
|
|
|
|
|
-------------------------------------------------------------------------------- */}
|
|
|
|
|
{data.music?.isPlaying && (
|
|
|
|
|
<motion.div
|
|
|
|
|
key="music"
|
|
|
|
|
initial={{ opacity: 0, x: 20, scale: 0.95 }}
|
|
|
|
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
|
|
|
|
exit={{ opacity: 0, x: 20, scale: 0.95 }}
|
|
|
|
|
layout
|
|
|
|
|
className="pointer-events-auto group bg-black/80 backdrop-blur-md border border-white/10 p-3 rounded-2xl flex items-center gap-3 w-72 shadow-2xl hover:bg-black/90 transition-all"
|
|
|
|
|
>
|
|
|
|
|
<div className="relative shrink-0">
|
|
|
|
|
<img
|
|
|
|
|
src={data.music.albumArt}
|
|
|
|
|
alt="Album"
|
|
|
|
|
className="w-12 h-12 rounded-lg shadow-sm group-hover:scale-105 transition-transform duration-500"
|
|
|
|
|
/>
|
|
|
|
|
</motion.span>
|
|
|
|
|
)}
|
|
|
|
|
</motion.button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="absolute -bottom-1 -right-1 bg-black rounded-full p-1 border border-white/10 shadow-sm z-10">
|
|
|
|
|
<Disc3 size={10} className="text-green-400 animate-spin-slow" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col min-w-0 flex-1">
|
|
|
|
|
<div className="flex items-center justify-between mb-0.5">
|
|
|
|
|
<span className="text-[10px] font-bold text-green-400 uppercase tracking-widest flex items-center gap-1">
|
|
|
|
|
Spotify
|
|
|
|
|
</span>
|
|
|
|
|
{/* Equalizer Animation */}
|
|
|
|
|
<div className="flex gap-[2px] h-2 items-end">
|
|
|
|
|
{[1,2,3].map(i => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={i}
|
|
|
|
|
className="w-0.5 bg-green-500 rounded-full"
|
|
|
|
|
animate={{ height: ["20%", "100%", "40%"] }}
|
|
|
|
|
transition={{ duration: 0.5, repeat: Infinity, repeatType: "reverse", delay: i * 0.1 }}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a
|
|
|
|
|
href={data.music.url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
className="font-bold text-sm text-white truncate hover:underline decoration-white/30 underline-offset-2"
|
|
|
|
|
>
|
|
|
|
|
{data.music.track}
|
|
|
|
|
</a>
|
|
|
|
|
<span className="text-xs text-white/50 truncate">
|
|
|
|
|
{data.music.artist}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* --------------------------------------------------------------------------------
|
|
|
|
|
4. STATUS BADGE (Optional)
|
|
|
|
|
Kleiner Indikator ganz unten, falls nichts anderes da ist oder als Abschluss
|
|
|
|
|
-------------------------------------------------------------------------------- */}
|
|
|
|
|
<motion.div layout className="pointer-events-auto bg-black/40 backdrop-blur-sm border border-white/5 px-3 py-1.5 rounded-full flex items-center gap-2">
|
|
|
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
|
|
|
data.status.color === 'green' ? 'bg-green-500' :
|
|
|
|
|
data.status.color === 'red' ? 'bg-red-500' :
|
|
|
|
|
data.status.color === 'yellow' ? 'bg-yellow-500' : 'bg-gray-500'
|
|
|
|
|
}`} />
|
|
|
|
|
<span className="text-xs font-medium text-white/60 capitalize">
|
|
|
|
|
{data.status.text === 'dnd' ? 'Do not disturb' : data.status.text}
|
|
|
|
|
</span>
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
}
|