141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import {
|
|
Activity,
|
|
CheckCircle2,
|
|
PlusCircle,
|
|
MessageSquare,
|
|
UserPlus,
|
|
Edit3,
|
|
ArrowRight,
|
|
} from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { useActivityFeed } from "@/hooks/use-activity-feed";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import type { ActivityItem } from "@/lib/supabase/database.types";
|
|
|
|
const activityTypeConfig: Record<
|
|
ActivityItem["type"],
|
|
{ label: string; icon: React.ElementType; color: string }
|
|
> = {
|
|
task_created: {
|
|
label: "Created",
|
|
icon: PlusCircle,
|
|
color: "bg-blue-500/10 text-blue-500",
|
|
},
|
|
task_completed: {
|
|
label: "Completed",
|
|
icon: CheckCircle2,
|
|
color: "bg-green-500/10 text-green-500",
|
|
},
|
|
task_updated: {
|
|
label: "Updated",
|
|
icon: Edit3,
|
|
color: "bg-yellow-500/10 text-yellow-500",
|
|
},
|
|
comment_added: {
|
|
label: "Comment",
|
|
icon: MessageSquare,
|
|
color: "bg-purple-500/10 text-purple-500",
|
|
},
|
|
task_assigned: {
|
|
label: "Assigned",
|
|
icon: UserPlus,
|
|
color: "bg-pink-500/10 text-pink-500",
|
|
},
|
|
};
|
|
|
|
function RecentActivityItem({ activity }: { activity: ActivityItem }) {
|
|
const config = activityTypeConfig[activity.type];
|
|
const Icon = config.icon;
|
|
const timeAgo = formatDistanceToNow(new Date(activity.timestamp), { addSuffix: true });
|
|
|
|
return (
|
|
<div className="flex gap-3 py-3 border-b border-border last:border-0">
|
|
<div
|
|
className={cn(
|
|
"w-8 h-8 rounded-full flex items-center justify-center shrink-0",
|
|
config.color
|
|
)}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="font-medium text-sm truncate">{activity.user_name}</span>
|
|
<Badge variant="secondary" className="text-xs font-normal">
|
|
{config.label}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground truncate">
|
|
{activity.task_title}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">{timeAgo}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RecentActivitySkeleton() {
|
|
return (
|
|
<div className="flex gap-3 py-3 border-b border-border">
|
|
<Skeleton className="w-8 h-8 rounded-full shrink-0" />
|
|
<div className="flex-1 space-y-2">
|
|
<Skeleton className="h-4 w-24" />
|
|
<Skeleton className="h-3 w-32" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function RecentActivityWidget() {
|
|
const { activities, loading, error } = useActivityFeed({ limit: 5 });
|
|
|
|
return (
|
|
<Card className="h-full">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Activity className="w-4 h-4" />
|
|
Recent Activity
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<div className="space-y-1">
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
<RecentActivitySkeleton key={i} />
|
|
))}
|
|
</div>
|
|
) : error ? (
|
|
<div className="text-center py-6">
|
|
<p className="text-sm text-muted-foreground">Failed to load</p>
|
|
</div>
|
|
) : activities.length === 0 ? (
|
|
<div className="text-center py-6">
|
|
<Activity className="w-8 h-8 text-muted-foreground mx-auto mb-2" />
|
|
<p className="text-sm text-muted-foreground">No recent activity</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{activities.map((activity) => (
|
|
<RecentActivityItem key={activity.id} activity={activity} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<Link href="/activity">
|
|
<Button variant="ghost" className="w-full mt-4 text-sm" size="sm">
|
|
View all activity
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</Button>
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|