feat: more stat components
This commit is contained in:
98
web/src/components/recent-activity.tsx
Normal file
98
web/src/components/recent-activity.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from "react";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "./ui/card";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { type RecentEvent } from "@shared/modules/dashboard/dashboard.types";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
|
||||
function timeAgo(dateInput: Date | string) {
|
||||
const date = new Date(dateInput);
|
||||
const now = new Date();
|
||||
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||
|
||||
if (seconds < 60) return "just now";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
interface RecentActivityProps {
|
||||
events: RecentEvent[];
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RecentActivity({ events, isLoading, className }: RecentActivityProps) {
|
||||
return (
|
||||
<Card className={cn("glass-card border-none bg-card/40 h-full", className)}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center justify-between text-lg font-medium">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
Live Activity
|
||||
</span>
|
||||
{!isLoading && events.length > 0 && (
|
||||
<Badge variant="glass" className="text-[10px] font-mono">
|
||||
{events.length} EVENTS
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="space-y-4 pt-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||
<div className="space-y-2 flex-1">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : events.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground space-y-2">
|
||||
<div className="text-4xl">😴</div>
|
||||
<p>No recent activity</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto pr-2 -mr-2 custom-scrollbar">
|
||||
{events.map((event, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="group flex items-start gap-3 p-3 rounded-xl bg-background/30 hover:bg-background/50 border border-transparent hover:border-border/50 transition-all duration-300"
|
||||
>
|
||||
<div className="text-2xl p-2 rounded-lg bg-background/50 group-hover:scale-110 transition-transform">
|
||||
{event.icon || "📝"}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 py-1">
|
||||
<p className="text-sm font-medium leading-none truncate mb-1.5 text-foreground/90">
|
||||
{event.message}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
event.type === 'error' ? 'destructive' :
|
||||
event.type === 'warn' ? 'destructive' :
|
||||
event.type === 'success' ? 'aurora' : 'secondary'
|
||||
}
|
||||
className="text-[10px] h-4 px-1.5"
|
||||
>
|
||||
{event.type}
|
||||
</Badge>
|
||||
<span className="text-[10px] text-muted-foreground font-mono">
|
||||
{timeAgo(event.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user