- 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>
197 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|