- F481: Co-listening sessions (WebSocket sync, ListenTogether page) - F482: Stem sharing (upload/list/download wav,aiff,flac) - F483: Collaborative rooms (type collaborative, max 10, invite-only) - Roadmap: v0.10.7 → DONE
176 lines
5.3 KiB
TypeScript
176 lines
5.3 KiB
TypeScript
import { useState, useRef } from 'react';
|
|
import { Dialog } from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Select } from '@/components/ui/select';
|
|
import { apiClient } from '@/services/api/client';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useChatStore } from '../store/chatStore';
|
|
import { parseApiError } from '@/utils/apiErrorHandler';
|
|
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
|
|
|
|
// FE-PAGE-005: Complete Chat page implementation - Room Management
|
|
|
|
interface CreateRoomDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function CreateRoomDialog({ open, onClose }: CreateRoomDialogProps) {
|
|
const [name, setName] = useState('');
|
|
const [type, setType] = useState<'public' | 'private' | 'collaborative'>('public');
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
const [mutationError, setMutationError] = useState<Error | null>(null);
|
|
const [retryCount, setRetryCount] = useState(0);
|
|
const lastMutationRef = useRef<(() => Promise<void>) | null>(null);
|
|
const toast = useToast();
|
|
const { addConversation, setCurrentConversation } = useChatStore();
|
|
|
|
const handleCreate = async () => {
|
|
setValidationError(null);
|
|
setMutationError(null);
|
|
|
|
if (!name.trim()) {
|
|
setValidationError('Room name is required');
|
|
return;
|
|
}
|
|
|
|
// Action 3.4.1.3: Store mutation for retry
|
|
const roomName = name.trim();
|
|
const roomType = type;
|
|
const performMutation = async () => {
|
|
const response = await apiClient.post('/conversations', {
|
|
name: roomName,
|
|
type: roomType,
|
|
});
|
|
|
|
const newRoom = {
|
|
id: response.data.id || response.data.conversation?.id,
|
|
name: response.data.name || response.data.conversation?.name,
|
|
type:
|
|
response.data.type || response.data.conversation?.type || roomType,
|
|
participants: response.data.participants || [],
|
|
unread_count: 0,
|
|
};
|
|
|
|
addConversation(newRoom);
|
|
setCurrentConversation(newRoom.id);
|
|
toast.success('Room created successfully');
|
|
setName('');
|
|
setType('public');
|
|
setMutationError(null);
|
|
setRetryCount(0);
|
|
lastMutationRef.current = null;
|
|
onClose();
|
|
};
|
|
|
|
lastMutationRef.current = performMutation;
|
|
setIsCreating(true);
|
|
|
|
try {
|
|
await performMutation();
|
|
} catch (error: unknown) {
|
|
const apiError = parseApiError(error);
|
|
setMutationError(new Error(apiError.message));
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
// Action 3.4.1.3: Retry handler for failed mutations
|
|
const handleRetry = async () => {
|
|
if (!lastMutationRef.current || retryCount >= 3) return;
|
|
|
|
setRetryCount((prev) => prev + 1);
|
|
setIsCreating(true);
|
|
try {
|
|
await lastMutationRef.current();
|
|
} catch (error) {
|
|
// Error will be handled by the mutation function
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onClose={onClose}
|
|
title="Create New Room"
|
|
variant="default"
|
|
size="md"
|
|
>
|
|
<div className="space-y-4">
|
|
{mutationError && (
|
|
<ErrorDisplay
|
|
error={mutationError}
|
|
variant="banner"
|
|
severity="error"
|
|
context={{
|
|
action: 'creating room',
|
|
resource: 'conversation',
|
|
}}
|
|
onRetry={retryCount < 3 ? handleRetry : undefined}
|
|
onDismiss={() => {
|
|
setMutationError(null);
|
|
setRetryCount(0);
|
|
lastMutationRef.current = null;
|
|
}}
|
|
/>
|
|
)}
|
|
{validationError && (
|
|
<ErrorDisplay
|
|
error={validationError}
|
|
variant="inline"
|
|
severity="error"
|
|
size="sm"
|
|
dismissible={false}
|
|
/>
|
|
)}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="room-name">Room Name</Label>
|
|
<Input
|
|
id="room-name"
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
setValidationError(null);
|
|
}}
|
|
placeholder="Enter room name"
|
|
maxLength={100}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="room-type">Room Type</Label>
|
|
<Select
|
|
options={[
|
|
{ value: 'public', label: 'Public' },
|
|
{ value: 'private', label: 'Private' },
|
|
{ value: 'collaborative', label: 'Collaborative (invite only, max 10)' },
|
|
]}
|
|
value={type}
|
|
onChange={(value) =>
|
|
setType(
|
|
(Array.isArray(value) ? value[0] : value) as
|
|
| 'public'
|
|
| 'private'
|
|
| 'collaborative',
|
|
)
|
|
}
|
|
name="room-type"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<Button variant="outline" onClick={onClose} disabled={isCreating}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleCreate} disabled={isCreating || !name.trim()}>
|
|
{isCreating ? 'Creating...' : 'Create Room'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
);
|
|
}
|