Removed floating overlays. Integrated ActivityFeed and Chat directly into Bento grid cells. Refined layout for maximum clarity and 'Dennis' feel.
115 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
}
|