veza/apps/web/src/features/chat/components/RoomMembersModal.tsx
senke 7b39efa176 fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- Fix 98 TypeScript errors across 37 files:
  - Service layer double-unwrapping (subscriptionService, distributionService, gearService)
  - Self-referencing variables in SearchPageResults
  - FeedView/ExploreView .posts→.items alignment
  - useQueueSync Zustand subscribe API
  - AdminAuditLogsView missing interface fields
  - Toast proxy type, interceptor type narrowing
  - 22 unused imports/variables removed
  - 5 storybook mock data fixes

- Align frontend API calls with backend endpoints:
  - Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics)
  - Chat: chatService uses /conversations (was mock data), WS URL from backend token
  - Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros)
  - Settings: suppress 2FA toast error when endpoint unavailable

- Fix marketplace products: seed uses 'active' status (was 'published')
- Enrich seed: admin follows all creators (feed has content)

- Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%)
  Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc.

- Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:18:49 +01:00

197 lines
6.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Dialog } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
import { getRoomMembers, kickMember, leaveConversation } from '../services/conversationService';
import type { RoomMemberWithRole } from '../services/conversationService';
import { useChatStore } from '../store/chatStore';
import { useToast } from '@/hooks/useToast';
import { UserMinus, Loader2, Crown, Shield, User, LogOut } from 'lucide-react';
interface RoomMembersModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
conversationId: string;
}
function RoleBadge({ role }: { role: string }) {
const config =
role === 'owner'
? { icon: Crown, label: 'Propriétaire', className: 'text-amber-500' }
: role === 'admin'
? { icon: Shield, label: 'Admin', className: 'text-primary' }
: { icon: User, label: 'Membre', className: 'text-muted-foreground' };
const Icon = config.icon;
return (
<span
className={`inline-flex items-center gap-1 text-xs ${config.className}`}
title={config.label}
>
<Icon className="h-3 w-3" />
{config.label}
</span>
);
}
/**
* RoomMembersModal (v0.9.7): List members with roles, kick for owner/admin
*/
export function RoomMembersModal({
open,
onOpenChange,
conversationId,
}: RoomMembersModalProps) {
const { userId } = useChatStore();
const [members, setMembers] = useState<RoomMemberWithRole[]>([]);
const [myRole, setMyRole] = useState<string>('');
const [loading, setLoading] = useState(true);
const [kicking, setKicking] = useState<string | null>(null);
const [leaving, setLeaving] = useState(false);
const [pendingKick, setPendingKick] = useState<{ userId: string; username: string } | null>(null);
const [pendingLeave, setPendingLeave] = useState(false);
const { success: showSuccess, error: showError } = useToast();
const canKick = myRole === 'owner' || myRole === 'admin';
useEffect(() => {
if (!open || !conversationId) return;
setLoading(true);
getRoomMembers(conversationId)
.then((res) => {
setMembers(res.members);
setMyRole(res.my_role);
})
.catch(() => setMembers([]))
.finally(() => setLoading(false));
}, [open, conversationId]);
const handleKickClick = (m: RoomMemberWithRole) => {
setPendingKick({ userId: m.user_id, username: m.username });
};
const handleKickConfirm = async () => {
if (!pendingKick || !canKick || pendingKick.userId === userId) return;
setKicking(pendingKick.userId);
setPendingKick(null);
try {
await kickMember(conversationId, pendingKick.userId);
setMembers((prev) => prev.filter((m) => m.user_id !== pendingKick.userId));
showSuccess('Membre exclu');
} catch (err) {
showError(err instanceof Error ? err.message : 'Impossible d\'exclure');
} finally {
setKicking(null);
}
};
const handleLeaveClick = () => setPendingLeave(true);
const handleLeaveConfirm = async () => {
if (!pendingLeave) return;
setLeaving(true);
setPendingLeave(false);
try {
await leaveConversation(conversationId);
showSuccess('Vous avez quitté la conversation');
onOpenChange(false);
useChatStore.getState().setCurrentConversation(null);
} catch (err) {
showError(err instanceof Error ? err.message : 'Impossible de quitter');
} finally {
setLeaving(false);
}
};
return (
<Dialog
open={open}
onOpenChange={onOpenChange}
title="Membres"
size="md"
footer={
!loading && (
<Button
variant="outline"
onClick={handleLeaveClick}
disabled={leaving}
className="text-destructive border-destructive/30 hover:bg-destructive/10 hover:border-destructive/50"
aria-label="Quitter la conversation"
>
{leaving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<LogOut className="mr-2 h-4 w-4" />
Quitter la conversation
</>
)}
</Button>
)
}
showCancel={false}
>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<ul className="space-y-2">
{members.map((m) => (
<li
key={m.user_id}
className="flex items-center justify-between gap-4 py-2 px-3 rounded-lg hover:bg-muted/50"
>
<div className="flex flex-col min-w-0">
<span className="font-medium truncate">{m.username}</span>
<RoleBadge role={m.role} />
</div>
{canKick &&
m.user_id !== userId &&
m.role !== 'owner' && (
<Button
variant="ghost"
size="sm"
onClick={() => handleKickClick(m)}
disabled={!!kicking}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
aria-label={`Exclure ${m.username}`}
>
{kicking === m.user_id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<UserMinus className="h-4 w-4" />
)}
</Button>
)}
</li>
))}
</ul>
)}
<ConfirmationDialog
open={!!pendingKick}
onClose={() => setPendingKick(null)}
onConfirm={handleKickConfirm}
title="Exclure un membre"
description={
pendingKick
? `Êtes-vous sûr de vouloir exclure ${pendingKick.username} du canal ?`
: ''
}
confirmLabel="Exclure"
variant="destructive"
isLoading={!!kicking}
/>
<ConfirmationDialog
open={pendingLeave}
onClose={() => setPendingLeave(false)}
onConfirm={handleLeaveConfirm}
title="Quitter la conversation"
description="Vous ne recevrez plus de messages de ce canal. Vous pourrez le rejoindre à nouveau via une invitation."
confirmLabel="Quitter"
variant="destructive"
isLoading={leaving}
/>
</Dialog>
);
}