Files
portfolio/app/components/ActivityFeed.tsx
denshooter 7603cb6298 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.
2026-02-16 01:21:49 +01:00

115 lines
4.9 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import {
Code2,
Disc3,
Gamepad2,
Zap,
Clock,
Activity,
} from "lucide-react";
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 | number; details?: string | number; } | null;
coding: { isActive: boolean; project?: string; file?: string; language?: string; stats?: { time: string; topLang: string; topProject: string; }; } | null;
customActivities?: Record<string, any>;
}
function getSafeGamingText(details: string | number | undefined, state: string | number | undefined, fallback: string): string {
if (typeof details === 'string' && details.trim().length > 0) return details;
if (typeof state === 'string' && state.trim().length > 0) return state;
if (typeof details === 'number' && !isNaN(details)) return String(details);
if (typeof state === 'number' && !isNaN(state)) return String(state);
return fallback;
}
export default function ActivityFeed({ onActivityChange }: { onActivityChange?: (active: boolean) => void }) {
const [data, setData] = useState<StatusData | null>(null);
const [hasActivity, setHasActivity] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch("/api/n8n/status");
if (!res.ok) throw new Error();
const json = await res.json();
const activityData = Array.isArray(json) ? json[0] : json;
setData(activityData);
const isActive = Boolean(
activityData.coding?.isActive ||
activityData.gaming?.isPlaying ||
activityData.music?.isPlaying ||
Object.values(activityData.customActivities || {}).some((act: any) => act?.enabled)
);
setHasActivity(isActive);
onActivityChange?.(isActive);
} catch (error) {
setHasActivity(false);
onActivityChange?.(false);
}
};
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [onActivityChange]);
if (!data || !hasActivity) return null;
return (
<div className="space-y-4">
{/* CODING */}
{data.coding?.isActive && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="bg-green-500/10 border border-green-500/20 rounded-2xl p-4">
<div className="flex items-center gap-3 mb-2">
<Zap size={14} className="text-green-400 animate-pulse" />
<span className="text-[10px] font-black uppercase tracking-widest text-green-400">Coding Now</span>
</div>
<p className="font-bold text-white text-sm truncate">{data.coding.project}</p>
<p className="text-xs text-white/50 truncate">{data.coding.file}</p>
</motion.div>
)}
{/* GAMING */}
{data.gaming?.isPlaying && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="bg-indigo-500/10 border border-indigo-500/20 rounded-2xl p-4">
<div className="flex items-center gap-3 mb-3">
<Gamepad2 size={14} className="text-indigo-400" />
<span className="text-[10px] font-black uppercase tracking-widest text-indigo-400">Gaming</span>
</div>
<div className="flex gap-3">
{data.gaming.image && <div className="w-10 h-10 rounded-lg overflow-hidden shrink-0"><img src={data.gaming.image} className="w-full h-full object-cover" /></div>}
<div className="min-w-0">
<p className="font-bold text-white text-sm truncate">{data.gaming.name}</p>
<p className="text-xs text-white/50 truncate">{getSafeGamingText(data.gaming.details, data.gaming.state, "In Game")}</p>
</div>
</div>
</motion.div>
)}
{/* MUSIC */}
{data.music?.isPlaying && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="bg-white/5 border border-white/10 rounded-2xl p-4">
<div className="flex items-center gap-2 mb-3">
<Disc3 size={14} className="text-green-400 animate-spin-slow" />
<span className="text-[10px] font-black uppercase tracking-widest text-white/40">Spotify</span>
</div>
<div className="flex gap-3">
<div className="w-10 h-10 rounded-lg overflow-hidden shrink-0 shadow-lg"><img src={data.music.albumArt} className="w-full h-full object-cover" /></div>
<div className="min-w-0">
<p className="font-bold text-white text-sm truncate">{data.music.track}</p>
<p className="text-xs text-white/50 truncate">{data.music.artist}</p>
</div>
</div>
</motion.div>
)}
</div>
);
}