feat: major UI/UX overhaul, snippets system, and performance fixes
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Failing after 9m26s

This commit is contained in:
2026-02-16 12:31:40 +01:00
parent 6f62b37c3a
commit a5dba298f3
41 changed files with 1610 additions and 499 deletions

View File

@@ -1,51 +1,70 @@
"use client";
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { motion, AnimatePresence } from "framer-motion";
import { Disc3, Gamepad2, Zap, Quote as QuoteIcon } from "lucide-react";
import { useTranslations } from "next-intl";
interface CustomActivity {
[key: string]: unknown;
}
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>;
customActivities?: Record<string, CustomActivity>;
}
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" }
];
const techQuotes = {
de: [
{ content: "Informatik hat nicht mehr mit Computern zu tun als Astronomie mit Teleskopen.", author: "Edsger W. Dijkstra" },
{ content: "Einfachheit ist die Voraussetzung für Verlässlichkeit.", author: "Edsger W. Dijkstra" },
{ content: "Wenn Debugging der Prozess des Entfernens von Fehlern ist, dann muss Programmieren der Prozess des Einbauens sein.", author: "Edsger W. Dijkstra" },
{ content: "Gelöschter Code ist gedebuggter Code.", author: "Jeff Sickel" },
{ content: "Zuerst löse das Problem. Dann schreibe den Code.", author: "John Johnson" }
],
en: [
{ 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: "Deleted code is debugged code.", author: "Jeff Sickel" },
{ 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
idleQuote,
locale = 'en'
}: {
onActivityChange?: (active: boolean) => void;
idleQuote?: string;
locale?: string;
}) {
const [data, setData] = useState<StatusData | null>(null);
const [hasActivity, setHasActivity] = useState(false);
const [randomQuote, setRandomQuote] = useState(techQuotes[0]);
const [quoteIndex, setQuoteIndex] = useState(0);
const [loading, setLoading] = useState(true);
const t = useTranslations("home.about.activity");
const currentQuotes = techQuotes[locale as keyof typeof techQuotes] || techQuotes.en;
// Combine CMS quote with tech quotes if available
const allQuotes = React.useMemo(() => {
if (idleQuote) {
return [{ content: idleQuote, author: "Dennis Konkol" }, ...currentQuotes];
}
return currentQuotes;
}, [idleQuote, currentQuotes]);
useEffect(() => {
setRandomQuote(techQuotes[Math.floor(Math.random() * techQuotes.length)]);
const fetchData = async () => {
try {
const res = await fetch("/api/n8n/status");
@@ -58,62 +77,89 @@ export default function ActivityFeed({
activityData.coding?.isActive ||
activityData.gaming?.isPlaying ||
activityData.music?.isPlaying ||
Object.values(activityData.customActivities || {}).some((act: any) => act?.enabled)
Object.values(activityData.customActivities || {}).some((act) => Boolean((act as { enabled?: boolean })?.enabled))
);
setHasActivity(isActive);
onActivityChange?.(isActive);
} catch {
setHasActivity(false);
onActivityChange?.(false);
} finally {
setLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [onActivityChange]);
const statusInterval = setInterval(fetchData, 30000);
// Cycle quotes every 10 seconds
const quoteInterval = setInterval(() => {
setQuoteIndex((prev) => (prev + 1) % allQuotes.length);
}, 10000);
return () => {
clearInterval(statusInterval);
clearInterval(quoteInterval);
};
}, [onActivityChange, allQuotes.length]);
if (loading) {
return <div className="animate-pulse space-y-4">
<div className="h-24 bg-stone-100 dark:bg-stone-800 rounded-2xl" />
</div>;
}
if (!hasActivity) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="h-full flex flex-col justify-center space-y-6"
>
<div 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" />
<QuoteIcon size={18} className="text-emerald-600 dark: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 className="min-h-[120px] relative">
<AnimatePresence mode="wait">
<motion.div
key={quoteIndex}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.5 }}
className="space-y-4"
>
<p className="text-xl md:text-2xl font-light leading-tight text-stone-700 dark:text-stone-300 italic">
&ldquo;{allQuotes[quoteIndex].content}&rdquo;
</p>
<p className="text-xs font-black text-stone-400 dark:text-stone-500 uppercase tracking-widest">
{allQuotes[quoteIndex].author}
</p>
</motion.div>
</AnimatePresence>
</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 className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-stone-400 dark:text-stone-600 pt-4 border-t border-stone-100 dark:border-stone-800">
<span className="w-1.5 h-1.5 rounded-full bg-stone-200 dark:bg-stone-700 animate-pulse" />
{t("idleStatus")}
</div>
</motion.div>
</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">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="bg-emerald-500/5 dark:bg-emerald-500/10 border border-emerald-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>
<Zap size={14} className="text-emerald-600 dark:text-emerald-400 animate-pulse" />
<span className="text-[10px] font-black uppercase tracking-widest text-emerald-600 dark:text-emerald-400">{t("codingNow")}</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>
<p className="font-bold text-stone-900 dark:text-white text-lg truncate">{data.coding.project}</p>
<p className="text-xs text-stone-500 dark: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">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="bg-indigo-500/5 dark: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>
<Gamepad2 size={14} className="text-indigo-600 dark:text-indigo-400" />
<span className="text-[10px] font-black uppercase tracking-widest text-indigo-600 dark:text-indigo-400">{t("gaming")}</span>
</div>
<div className="flex gap-4">
{data.gaming.image && (
@@ -122,9 +168,9 @@ export default function ActivityFeed({
</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 className="font-bold text-stone-900 dark:text-white text-base truncate">{data.gaming.name}</p>
<p className="text-xs text-stone-500 dark:text-white/50 truncate">
{getSafeGamingText(data.gaming.details, data.gaming.state, t("inGame"))}
</p>
</div>
</div>
@@ -132,20 +178,48 @@ export default function ActivityFeed({
)}
{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>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-[#1DB954]/5 dark:bg-[#1DB954]/10 border border-[#1DB954]/20 rounded-2xl p-5 relative overflow-hidden group"
>
<div className="flex items-center justify-between mb-3 relative z-10">
<div className="flex items-center gap-2">
<Disc3 size={14} className="text-[#1DB954] animate-spin-slow" />
<span className="text-[10px] font-black uppercase tracking-widest text-[#1DB954]">{t("listening")}</span>
</div>
{/* Simple Animated Music Bars */}
<div className="flex items-end gap-[2px] h-3">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{ height: ["20%", "100%", "20%"] }}
transition={{
duration: 0.8,
repeat: Infinity,
delay: i * 0.2,
ease: "easeInOut"
}}
className="w-[2px] bg-[#1DB954] rounded-full"
/>
))}
</div>
</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 className="flex gap-4 relative z-10">
<div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-500">
<img
src={data.music.albumArt}
alt="Album Art"
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
</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>
<p className="font-bold text-[#1DB954] dark:text-[#1DB954] text-base truncate leading-tight mb-1">{data.music.track}</p>
<p className="text-sm text-stone-600 dark:text-white/60 truncate font-medium">{data.music.artist}</p>
</div>
</div>
{/* Subtle Spotify branding gradient */}
<div className="absolute top-0 right-0 w-32 h-32 bg-[#1DB954]/5 blur-[60px] rounded-full -mr-16 -mt-16 pointer-events-none" />
</motion.div>
)}
</div>