Add ActivityTimeline component with user activity display and NotificationList for user notifications.
97 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}); |