mission-control/components/activity/activity-feed.tsx

270 lines
8.6 KiB
TypeScript

"use client";
import { useState } from "react";
import { formatDistanceToNow } from "date-fns";
import {
Activity,
CheckCircle2,
PlusCircle,
MessageSquare,
UserPlus,
Edit3,
FolderKanban,
Filter,
RefreshCw,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useActivityFeed, ActivityFilterType } 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 { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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",
},
};
const filterOptions: { value: ActivityFilterType; label: string }[] = [
{ value: "all", label: "All Activity" },
{ value: "task_created", label: "Created" },
{ value: "task_completed", label: "Completed" },
{ value: "task_updated", label: "Updated" },
{ value: "comment_added", label: "Comments" },
{ value: "task_assigned", label: "Assigned" },
];
function ActivityItemCard({ 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-4 pb-6 border-b border-border last:border-0 last:pb-0">
{/* Icon */}
<div
className={cn(
"w-10 h-10 rounded-full flex items-center justify-center shrink-0",
config.color
)}
>
<Icon className="w-5 h-5" />
</div>
{/* Content */}
<div className="flex-1 min-w-0 space-y-1">
{/* Header */}
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium text-sm">{activity.user_name}</span>
<Badge variant="secondary" className="text-xs font-normal">
{config.label}
</Badge>
<span className="text-muted-foreground text-sm truncate">
{activity.task_title}
</span>
</div>
{/* Project badge */}
<div className="flex items-center gap-2">
<FolderKanban className="w-3 h-3 text-muted-foreground" />
<Badge
variant="outline"
className="text-xs"
style={{
borderColor: activity.project_color,
color: activity.project_color,
}}
>
{activity.project_name}
</Badge>
</div>
{/* Details */}
{activity.details && (
<p className="text-sm text-muted-foreground">{activity.details}</p>
)}
{/* Comment text */}
{activity.comment_text && (
<div className="mt-2 p-3 bg-muted rounded-md">
<p className="text-sm text-muted-foreground line-clamp-3">
"{activity.comment_text}"
</p>
</div>
)}
{/* Timestamp */}
<p className="text-xs text-muted-foreground">{timeAgo}</p>
</div>
{/* Avatar */}
{activity.user_avatar_url ? (
<img
src={activity.user_avatar_url}
alt={activity.user_name}
className="w-8 h-8 rounded-full shrink-0"
/>
) : (
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 shrink-0" />
)}
</div>
);
}
function ActivitySkeleton() {
return (
<div className="flex gap-4 pb-6 border-b border-border">
<Skeleton className="w-10 h-10 rounded-full shrink-0" />
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-16" />
</div>
<Skeleton className="h-3 w-32" />
<Skeleton className="h-3 w-20" />
</div>
</div>
);
}
export function ActivityFeed() {
const [filterType, setFilterType] = useState<ActivityFilterType>("all");
const [projectId, setProjectId] = useState<string>("all");
const { activities, projects, loading, error, refresh } = useActivityFeed({
limit: 50,
projectId: projectId === "all" ? undefined : projectId,
filterType: filterType === "all" ? undefined : filterType,
});
return (
<Card>
<CardHeader className="pb-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CardTitle className="flex items-center gap-2 text-lg">
<Activity className="w-5 h-5" />
Recent Activity
</CardTitle>
<div className="flex flex-wrap items-center gap-2">
{/* Filter by type */}
<Select value={filterType} onValueChange={(v) => setFilterType(v as ActivityFilterType)}>
<SelectTrigger className="w-[140px] h-9">
<Filter className="w-4 h-4 mr-2" />
<SelectValue placeholder="Filter type" />
</SelectTrigger>
<SelectContent>
{filterOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Filter by project */}
<Select value={projectId} onValueChange={setProjectId}>
<SelectTrigger className="w-[160px] h-9">
<FolderKanban className="w-4 h-4 mr-2" />
<SelectValue placeholder="All projects" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All projects</SelectItem>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
<span
className="inline-block w-2 h-2 rounded-full mr-2"
style={{ backgroundColor: project.color }}
/>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Refresh button */}
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={refresh}
disabled={loading}
>
<RefreshCw className={cn("w-4 h-4", loading && "animate-spin")} />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-6">
{loading ? (
// Loading skeletons
Array.from({ length: 5 }).map((_, i) => (
<ActivitySkeleton key={i} />
))
) : error ? (
// Error state
<div className="text-center py-12">
<p className="text-muted-foreground">Failed to load activities</p>
<p className="text-sm text-muted-foreground mt-1">{error}</p>
<Button variant="outline" className="mt-4" onClick={refresh}>
Try again
</Button>
</div>
) : activities.length === 0 ? (
// Empty state
<div className="text-center py-12">
<Activity className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground">No activity yet</p>
<p className="text-sm text-muted-foreground mt-1">
Tasks and comments will appear here
</p>
</div>
) : (
// Activity list
activities.map((activity) => (
<ActivityItemCard key={activity.id} activity={activity} />
))
)}
</div>
</ScrollArea>
</CardContent>
</Card>
);
}