refactor: modify layout to use ClientOnly and BackgroundBlobsClient components fix: correct import statement for ActivityFeed in the main page fix: enhance sitemap fetching logic with error handling and mock support refactor: convert BackgroundBlobs to default export for consistency refactor: simplify ErrorBoundary component and improve error handling UI chore: update framer-motion to version 12.24.10 in package.json and package-lock.json test: add minimal Prisma Client mock for testing purposes feat: create BackgroundBlobsClient for dynamic import of BackgroundBlobs feat: implement ClientOnly component to handle client-side rendering feat: add custom error handling components for better user experience
260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
Code2,
|
|
Disc3,
|
|
Gamepad2,
|
|
ExternalLink,
|
|
Cpu,
|
|
Zap,
|
|
Clock,
|
|
Music
|
|
} from "lucide-react";
|
|
|
|
// Types passend zu deinem n8n Output
|
|
interface StatusData {
|
|
status: {
|
|
text: string;
|
|
color: string;
|
|
};
|
|
music: {
|
|
isPlaying: boolean;
|
|
track: string;
|
|
artist: string;
|
|
album: string;
|
|
albumArt: string;
|
|
url: string;
|
|
} | null;
|
|
gaming: {
|
|
isPlaying: boolean;
|
|
name: string;
|
|
image: string | null;
|
|
state?: string;
|
|
details?: string;
|
|
} | null;
|
|
coding: {
|
|
isActive: boolean;
|
|
project?: string;
|
|
file?: string;
|
|
stats?: {
|
|
time: string;
|
|
topLang: string;
|
|
topProject: string;
|
|
};
|
|
} | null;
|
|
}
|
|
|
|
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) return;
|
|
const json = await res.json();
|
|
setData(json);
|
|
} catch (e) {
|
|
console.error("Failed to fetch activity", e);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
const interval = setInterval(fetchData, 10000); // 10s Refresh
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
if (!data) return null;
|
|
|
|
return (
|
|
<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
|
|
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"}`}
|
|
>
|
|
{/* 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="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>
|
|
<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>
|
|
</motion.div>
|
|
)}
|
|
|
|
|
|
{/* --------------------------------------------------------------------------------
|
|
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"
|
|
/>
|
|
<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>
|
|
);
|
|
} |