181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { ArrowTrendingUpIcon } from "@heroicons/react/24/outline";
|
|
import { cn } from "@/lib/utils";
|
|
import { DashboardActivityItem } from "./DashboardActivityItem";
|
|
import {
|
|
filterActivities,
|
|
ACTIVITY_FILTERS,
|
|
getActivityNavigationPath,
|
|
isActivityClickable,
|
|
} from "../utils/dashboard.utils";
|
|
import type { Activity } from "@customer-portal/domain/billing";
|
|
import type { ActivityFilter } from "@customer-portal/domain/billing";
|
|
|
|
export interface ActivityFeedProps {
|
|
activities: Activity[];
|
|
onItemClick?: (activity: Activity) => void;
|
|
className?: string;
|
|
maxItems?: number;
|
|
showFilter?: boolean;
|
|
loading?: boolean;
|
|
error?: string | null;
|
|
}
|
|
|
|
export function ActivityFeed({
|
|
activities,
|
|
onItemClick,
|
|
className,
|
|
maxItems = 10,
|
|
showFilter = true,
|
|
loading = false,
|
|
error = null,
|
|
}: ActivityFeedProps) {
|
|
const [filter, setFilter] = useState<ActivityFilter>("all");
|
|
|
|
const filteredActivities = filterActivities(activities, filter);
|
|
const displayActivities = filteredActivities.slice(0, maxItems);
|
|
|
|
const handleActivityClick = (activity: Activity) => {
|
|
if (onItemClick) {
|
|
onItemClick(activity);
|
|
} else if (isActivityClickable(activity)) {
|
|
const path = getActivityNavigationPath(activity);
|
|
if (path) {
|
|
window.location.href = path;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden",
|
|
className
|
|
)}
|
|
>
|
|
<div className="px-6 py-4 border-b border-gray-100">
|
|
<div className="flex items-center justify-between">
|
|
<div className="h-6 bg-gray-200 rounded animate-pulse w-32" />
|
|
{showFilter && (
|
|
<div className="flex items-center space-x-1 bg-gray-100 rounded-lg p-1">
|
|
{ACTIVITY_FILTERS.map((_, index) => (
|
|
<div key={index} className="h-6 w-12 bg-gray-200 rounded animate-pulse" />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="p-6 space-y-4">
|
|
{Array.from({ length: 3 }).map((_, index) => (
|
|
<div key={index} className="flex items-start space-x-4">
|
|
<div className="w-10 h-10 rounded-full bg-gray-200 animate-pulse" />
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-4 bg-gray-200 rounded animate-pulse w-3/4" />
|
|
<div className="h-3 bg-gray-200 rounded animate-pulse w-1/2" />
|
|
<div className="h-3 bg-gray-200 rounded animate-pulse w-1/4" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"bg-white rounded-2xl shadow-lg border border-red-200 overflow-hidden",
|
|
className
|
|
)}
|
|
>
|
|
<div className="px-6 py-4 border-b border-red-100">
|
|
<h3 className="text-lg font-semibold text-gray-900">Recent Activity</h3>
|
|
</div>
|
|
<div className="p-6">
|
|
<div className="text-center py-8">
|
|
<div className="text-red-600 text-sm">{error}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden",
|
|
className
|
|
)}
|
|
>
|
|
<div className="px-6 py-4 border-b border-gray-100">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-gray-900">Recent Activity</h3>
|
|
{showFilter && (
|
|
<div className="flex items-center space-x-1 bg-gray-100 rounded-lg p-1">
|
|
{ACTIVITY_FILTERS.map(filterOption => (
|
|
<button
|
|
key={filterOption.key}
|
|
onClick={() => setFilter(filterOption.key)}
|
|
className={cn(
|
|
"px-2.5 py-1 text-xs rounded-md font-medium transition-all duration-200",
|
|
filter === filterOption.key
|
|
? "bg-white text-gray-900 shadow"
|
|
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
|
)}
|
|
>
|
|
{filterOption.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 max-h-[360px] overflow-y-auto">
|
|
{displayActivities.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{displayActivities.map(activity => {
|
|
const clickable = isActivityClickable(activity);
|
|
return (
|
|
<DashboardActivityItem
|
|
key={activity.id}
|
|
id={activity.id}
|
|
type={activity.type}
|
|
title={activity.title ?? ""}
|
|
description={activity.description ?? ""}
|
|
date={activity.date}
|
|
onClick={clickable ? () => handleActivityClick(activity) : undefined}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<ArrowTrendingUpIcon className="mx-auto h-12 w-12 text-gray-400" />
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">
|
|
{filter === "all" ? "No recent activity" : `No ${filter} activity`}
|
|
</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
{filter === "all"
|
|
? "Your account activity will appear here."
|
|
: `Your ${filter} activity will appear here.`}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{filteredActivities.length > maxItems && (
|
|
<div className="px-6 py-3 border-t border-gray-100 bg-gray-50">
|
|
<p className="text-xs text-gray-500 text-center">
|
|
Showing {maxItems} of {filteredActivities.length} activities
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|