Files
project-afterlife/apps/web/src/components/activity/ActivityFeed.tsx
consultoria-as 449c02eadc
Some checks failed
Deploy Multi-VM / Deploy VM Web (push) Has been cancelled
Deploy Multi-VM / Deploy VM Auth (push) Has been cancelled
Deploy Multi-VM / Deploy Game Servers (docker-compose.fusionfall.yml, VM_FUSIONFALL_HOST, VM_FUSIONFALL_SSH_KEY, VM_FUSIONFALL_USER, fusionfall) (push) Has been cancelled
Deploy Multi-VM / Deploy Game Servers (docker-compose.maple2.yml, VM_MAPLE2_HOST, VM_MAPLE2_SSH_KEY, VM_MAPLE2_USER, maple2) (push) Has been cancelled
Deploy Multi-VM / Deploy Game Servers (docker-compose.minecraft.yml, VM_MINECRAFT_HOST, VM_MINECRAFT_SSH_KEY, VM_MINECRAFT_USER, minecraft) (push) Has been cancelled
Deploy Multi-VM / Deploy Game Servers (docker-compose.retro.yml, VM_RETRO_HOST, VM_RETRO_SSH_KEY, VM_RETRO_USER, retro) (push) Has been cancelled
feat: phase 3 redesign, game images, auth system, vm guides, service isolation
- Redesign all internal pages to warm/gold aesthetic (catalog, game detail,
  documentary, about, donate, community, guides, contact, server-status,
  login, profile, admin, not-found)
- Add real cover images for all 4 games via Strapi CMS with getImageUrl helper
- Integrate NextAuth v5 with Authentik OIDC authentication
- Add new public pages: community, guides, contact, server-status
- Add new protected pages: login, profile, admin dashboard
- Remove legacy AFC/MercadoPago system entirely
- Add Docker Compose split files for service isolation (main, auth, fusionfall, nier)
- Add OpenFusion VM deployment configs (config.vm.ini, systemd service, README-VM)
- Add NieR Reincarnation server guide and desktop client guide
- Add architecture docs for multi-VM deployment
- Add healthcheck, SSE, contact, newsletter, admin API routes
- Add reusable UI components, skeleton loaders, activity feed, bookmark system
- Update deployment and game server documentation
2026-04-28 05:15:38 +00:00

99 lines
3.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { LiveIndicator } from "@/components/live/LiveIndicator";
interface Activity {
id: number;
type: string;
details: Record<string, unknown>;
created_at: string;
}
const typeConfig: Record<string, { label: string; color: string; icon: string }> = {
newsletter_subscribe: { label: "New subscriber", color: "text-emerald-400 bg-emerald-400/10", icon: "✉️" },
contact_message: { label: "Contact message", color: "text-blue-400 bg-blue-400/10", icon: "💬" },
server_online: { label: "Server online", color: "text-green-400 bg-green-400/10", icon: "🟢" },
server_offline: { label: "Server offline", color: "text-red-400 bg-red-400/10", icon: "🔴" },
};
function formatTimeAgo(iso: string) {
const diff = Date.now() - new Date(iso).getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return "just now";
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
return `${days}d ago`;
}
export function ActivityFeed() {
const [activities, setActivities] = useState<Activity[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/activities?limit=10")
.then((r) => r.json())
.then((data) => {
setActivities(data.activities || []);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
if (loading) {
return (
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-12 bg-[#1a1a24] rounded-lg animate-pulse" />
))}
</div>
</div>
);
}
if (activities.length === 0) return null;
return (
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-[#a0a0a8] uppercase tracking-wider">Recent Activity</h3>
<LiveIndicator />
</div>
<div className="space-y-2">
{activities.map((activity, i) => {
const config = typeConfig[activity.type] || { label: activity.type, color: "text-[#a0a0a8] bg-[rgba(160,160,168,0.1)]", icon: "•" };
const details = activity.details as Record<string, string>;
return (
<motion.div
key={activity.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05, duration: 0.3 }}
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-[rgba(255,255,255,0.02)] border border-[rgba(255,255,255,0.04)] hover:border-[rgba(255,255,255,0.08)] transition-colors"
>
<span className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs ${config.color}`}>
{config.icon}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-[#a0a0a8] truncate">
<span className="font-medium">{config.label}</span>
{details.email && <span className="text-[#6b6b75]"> {details.email}</span>}
{details.name && <span className="text-[#6b6b75]"> {details.name}</span>}
{details.server && <span className="text-[#6b6b75]"> {details.server}</span>}
</p>
</div>
<span className="text-xs text-[#3a3a44] flex-shrink-0">{formatTimeAgo(activity.created_at)}</span>
</motion.div>
);
})}
</div>
</div>
);
}