Files
portfolio/app/components/ActivityFeed.tsx
denshooter 6f62b37c3a
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Failing after 9m19s
fix: build and test stability for design overhaul
Fixed missing types, import errors, and updated test suites to match the new editorial design. Verified Docker container build.
2026-02-16 02:54:02 +01:00

154 lines
7.2 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { Disc3, Gamepad2, Zap, Quote as QuoteIcon } from "lucide-react";
interface StatusData {
music: { isPlaying: boolean; track: string; artist: 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; } | null;
customActivities?: Record<string, any>;
}
const techQuotes = [
{ content: "Computer Science is no more about computers than astronomy is about telescopes.", author: "Edsger W. Dijkstra" },
{ content: "Simplicity is prerequisite for reliability.", author: "Edsger W. Dijkstra" },
{ content: "If debugging is the process of removing software bugs, then programming must be the process of putting them in.", author: "Edsger W. Dijkstra" },
{ content: "There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.", author: "Tony Hoare" },
{ content: "Deleted code is debugged code.", author: "Jeff Sickel" },
{ content: "Walking on water and developing software from a specification are easy if both are frozen.", author: "Edward V. Berard" },
{ content: "Code never lies, comments sometimes do.", author: "Ron Jeffries" },
{ content: "I have no special talent. I am only passionately curious.", author: "Albert Einstein" },
{ content: "No code is faster than no code.", author: "Kevlin Henney" },
{ content: "First, solve the problem. Then, write the code.", author: "John Johnson" }
];
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,
idleQuote
}: {
onActivityChange?: (active: boolean) => void;
idleQuote?: string;
}) {
const [data, setData] = useState<StatusData | null>(null);
const [hasActivity, setHasActivity] = useState(false);
const [randomQuote, setRandomQuote] = useState(techQuotes[0]);
useEffect(() => {
setRandomQuote(techQuotes[Math.floor(Math.random() * techQuotes.length)]);
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 {
setHasActivity(false);
onActivityChange?.(false);
}
};
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [onActivityChange]);
if (!hasActivity) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="h-full flex flex-col justify-center space-y-6"
>
<div className="w-10 h-10 rounded-full bg-liquid-mint/10 flex items-center justify-center">
<QuoteIcon size={18} className="text-liquid-mint" />
</div>
<div className="space-y-4">
<p className="text-xl md:text-2xl font-light leading-tight text-stone-300 italic">
&ldquo;{idleQuote || randomQuote.content}&rdquo;
</p>
{!idleQuote && <p className="text-xs font-bold text-stone-500 uppercase tracking-widest"> {randomQuote.author}</p>}
</div>
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-stone-600">
<span className="w-1.5 h-1.5 rounded-full bg-stone-700" /> Currently Thinking
</div>
</motion.div>
);
}
return (
<div className="space-y-4">
{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-5">
<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-lg truncate">{data.coding.project}</p>
<p className="text-xs text-white/50 truncate">{data.coding.file}</p>
</motion.div>
)}
{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-5">
<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-4">
{data.gaming.image && (
<div className="w-12 h-12 rounded-xl overflow-hidden shrink-0 shadow-lg relative">
<img src={data.gaming.image} alt={data.gaming.name} className="w-full h-full object-cover" />
</div>
)}
<div className="min-w-0 flex flex-col justify-center">
<p className="font-bold text-white text-base 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>
)}
{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-5">
<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">Listening</span>
</div>
<div className="flex gap-4">
<div className="w-12 h-12 rounded-xl overflow-hidden shrink-0 shadow-2xl relative">
<img src={data.music.albumArt} alt="Album Art" className="w-full h-full object-cover" />
</div>
<div className="min-w-0 flex flex-col justify-center">
<p className="font-bold text-white text-base truncate">{data.music.track}</p>
<p className="text-xs text-white/50 truncate">{data.music.artist}</p>
</div>
</div>
</motion.div>
)}
</div>
);
}