chore(services): update components based on recent changes

Changes:
 services/frontend/src/features/dashboard/components/ChannelProfileDetail.tsx | 258 +++++++++++-----------------------------------------------------------------
 services/frontend/src/features/dashboard/components/ChannelSummaryList.tsx   | 171 ++++++++-------------------------------------------
 services/frontend/src/features/dashboard/components/DashboardStats.tsx       |   8 ++-
 services/frontend/src/features/dashboard/components/UserProfileDetail.tsx    | 297 +++++++++++++---------------------------------------------------------------------------
 services/frontend/src/features/dashboard/components/UserSummaryList.tsx      | 204 +++++++++++-------------------------------------------------
 services/frontend/src/features/dashboard/hooks/useDashboard.ts               | 203 +++++++++++++++---------------------------------------------
 services/frontend/src/features/messages/components/MessageCard.tsx           |  10 +--
 services/frontend/src/shared/api/client.ts                                   |  87 +++++++++++++++++++-------
 services/frontend/src/shared/hooks/useItemDetail.ts                          |  58 +++++++++++++++++
 services/frontend/src/shared/hooks/usePaginatedList.ts                       | 105 +++++++++++++++++++++++++++++++
This commit is contained in:
MythEclipse
2026-06-14 15:16:18 +07:00
parent 2b084b326f
commit 41e67423b8
25 changed files with 1090 additions and 1045 deletions
@@ -1,16 +1,7 @@
import { motion } from "framer-motion";
import { AlertCircle, Loader2, RefreshCw, Search, User } from "lucide-react";
import { User } from "lucide-react";
import type { DashboardUser } from "../../../shared/api/client";
import { cardItem, cardStagger } from "../../../shared/hooks/useFramerStagger";
import { cn } from "../../../shared/lib/utils";
import {
Card,
CardContent,
CardHeader,
CardTitle,
Input,
Skeleton,
} from "../../../shared/ui";
import { SummaryList } from "../../../shared/ui";
import type { SummaryItem } from "../../../shared/ui";
interface UserSummaryListProps {
users: DashboardUser[];
@@ -35,163 +26,42 @@ export function UserSummaryList({
onRefetch,
onSelectUser,
}: UserSummaryListProps) {
const items: SummaryItem[] = users.map((u) => ({
id: u.user_id,
label: u.username ?? u.user_id,
subtitle: u.trust_score !== null ? `Trust: ${u.trust_score}` : undefined,
summaryText: u.profile_summary ?? `${u.total_messages} messages`,
onClick: () => onSelectUser(u.user_id),
}));
const avatarMap = new Map(users.map((u) => [u.user_id, u.avatar_url]));
return (
<motion.div
className="grid gap-6"
variants={cardStagger}
initial="initial"
animate="animate"
>
{/* Search bar */}
<motion.div variants={cardItem} className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-primary" />
<Input
className="pl-9 rounded-full focus-visible:ring-primary"
placeholder="Search by username or user ID..."
value={search}
onChange={(e) => onSearchChange(e.target.value)}
<SummaryList
items={items}
loading={loading}
error={error}
searchValue={search}
onSearchChange={onSearchChange}
onRetry={onRefetch}
hasMore={hasMore}
onLoadMore={onLoadMore}
loadingMore={loading}
renderIcon={(item) => {
const avatarUrl = avatarMap.get(item.id);
return avatarUrl ? (
<img
src={avatarUrl}
alt=""
className="h-10 w-10 rounded-full object-cover"
/>
</div>
</motion.div>
{/* Error state */}
{error && (
<motion.div
variants={cardItem}
className="flex flex-col items-center gap-4 py-10 text-muted-foreground"
>
<AlertCircle className="h-10 w-10 text-destructive" />
<p className="text-sm">{error}</p>
<button
onClick={onRefetch}
className="inline-flex items-center gap-1.5 rounded-xl border border-border px-4 py-2 text-sm font-medium hover:bg-accent transition-colors"
>
<RefreshCw className="h-4 w-4" /> Retry
</button>
</motion.div>
)}
{/* Loading state */}
{loading && users.length === 0 && !error && <UserListSkeleton />}
{/* Empty state */}
{!loading && !error && users.length === 0 && (
<motion.div
variants={cardItem}
className="flex flex-col items-center gap-4 py-20 text-muted-foreground"
>
<User className="h-10 w-10" />
<p className="text-sm">No users found.</p>
</motion.div>
)}
{/* User cards */}
{users.length > 0 && (
<motion.div variants={cardItem} className="grid gap-3 sm:grid-cols-2">
{users.map((u) => (
<button
key={u.user_id}
onClick={() => onSelectUser(u.user_id)}
className="group w-full text-left"
>
<Card className="transition-all hover:ring-1 hover:ring-primary/30 cursor-pointer">
<CardContent className="p-4">
<div className="flex items-start gap-3">
{/* Avatar */}
<div className="shrink-0">
{u.avatar_url ? (
<img
src={u.avatar_url}
alt={u.username ?? "User"}
className="h-10 w-10 rounded-full object-cover"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
<User className="h-5 w-5 text-muted-foreground" />
</div>
)}
</div>
{/* Info */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate text-sm font-semibold">
{u.username ?? u.user_id}
</span>
{u.trust_score !== null && (
<span
className={cn(
"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium",
u.trust_score >= 80
? "bg-emerald-100 text-emerald-700"
: u.trust_score >= 50
? "bg-amber-100 text-amber-700"
: "bg-red-100 text-red-700",
)}
>
{u.trust_score}
</span>
)}
</div>
{u.profile_summary && (
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{u.profile_summary}
</p>
)}
<div className="mt-1.5 flex items-center gap-3 text-[11px] text-muted-foreground">
<span>{u.total_messages} messages</span>
{u.flagged_count > 0 && (
<span className="text-destructive">
{u.flagged_count} flagged
</span>
)}
</div>
</div>
</div>
</CardContent>
</Card>
</button>
))}
</motion.div>
)}
{/* Load more */}
{hasMore && (
<motion.div variants={cardItem} className="flex justify-center">
<button
onClick={onLoadMore}
disabled={loading}
className="inline-flex items-center gap-2 rounded-xl border border-border px-6 py-2 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50"
>
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
{loading ? "Loading..." : "Load More"}
</button>
</motion.div>
)}
</motion.div>
);
}
function UserListSkeleton() {
return (
<div className="grid gap-3 sm:grid-cols-2">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-24" />
</div>
</div>
</CardContent>
</Card>
))}
</div>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
<User className="h-5 w-5 text-muted-foreground" />
</div>
);
}}
emptyMessage="No users found."
/>
);
}