gitdataai/src/app/me/components/ActivityTimeline.tsx
ZhenYi 9981664731 feat(me): add ActivityTimeline and NotificationList components
Add ActivityTimeline component with user activity display and
NotificationList for user notifications.
2026-05-14 21:50:18 +08:00

97 lines
3.3 KiB
TypeScript

import { memo } from "react";
import { formatDistanceToNow } from "date-fns";
import {
History,
LogIn,
LogOut,
UserPlus,
ShieldCheck,
Key,
FolderPlus,
GitCommit,
GitPullRequest,
AlertCircle,
Settings,
Image as ImageIcon,
} from "lucide-react";
import type { UserActivityItem } from "@/client/model";
import { Skeleton } from "@/components/ui/skeleton";
interface ActivityTimelineProps {
items: UserActivityItem[];
isLoading?: boolean;
}
const ICON_MAP: Record<string, React.ComponentType<{ className?: string; "aria-hidden"?: string }>> = {
login: LogIn,
logout: LogOut,
register: UserPlus,
password_change: ShieldCheck,
ssh_key_add: Key,
project_create: FolderPlus,
commit: GitCommit,
pull_request_create: GitPullRequest,
issue_create: AlertCircle,
profile_update: Settings,
avatar_upload: ImageIcon,
};
export const ActivityTimeline = memo(function ActivityTimeline({ items, isLoading }: ActivityTimelineProps) {
if (isLoading) {
return (
<div className="flow-root">
<ul className="space-y-0">
{[...Array(6)].map((_, i) => (
<li key={i} className="flex items-start gap-3 py-3 border-b border-[0.5px] last:border-0" style={{ borderColor: "var(--border-subtle)" }}>
<Skeleton className="w-7 h-7 rounded-full shrink-0 mt-0.5" />
<div className="flex flex-col flex-1 gap-2">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-2 w-24" />
</div>
</li>
))}
</ul>
</div>
);
}
if (items.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-12" style={{ color: "var(--text-tertiary)" }}>
<History className="w-12 h-12 mb-4 opacity-20" />
<p className="text-[13px]">No recent activity</p>
</div>
);
}
return (
<div className="flow-root">
<ul className="space-y-0">
{items.map((item) => {
const Icon = ICON_MAP[item.action] || History;
return (
<li key={item.id} className="flex items-start gap-3 py-3 border-b border-[0.5px] last:border-0" style={{ borderColor: "var(--border-subtle)" }}>
<div
className="w-7 h-7 rounded-full flex items-center justify-center shrink-0 mt-0.5"
style={{ backgroundColor: "var(--accent-bg)", color: "var(--accent)" }}
>
<Icon className="h-[13px] w-[13px]" aria-hidden="true" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<p className="text-[12px] leading-relaxed" style={{ color: "var(--text-secondary)" }}>
<span style={{ color: "var(--text-primary)", fontWeight: 500 }}>{item.title}</span>
{item.resource_name && (
<> in <span className="font-semibold" style={{ color: "var(--text-primary)" }}>{item.resource_name}</span></>
)}
</p>
<time className="text-[11px] mt-1" style={{ color: "var(--text-tertiary)" }} dateTime={item.created_at}>
{formatDistanceToNow(new Date(item.created_at), { addSuffix: true })}
</time>
</div>
</li>
);
})}
</ul>
</div>
);
});