import React, { useState, useRef } from 'react'; import { Modal } from '../ui/Modal'; import { ImageCropModal } from '../ui/ImageCropModal'; import { useSpaceStore } from '../../stores/spaceStore'; import { useUIStore } from '../../stores/uiStore'; import { useTransferStore } from '../../stores/transferStore'; import { useNavigate } from 'react-router-dom'; import { waitForTransferAttachment } from '../../utils/waitForTransfer'; import { AVATAR_COLORS } from '@backspace/shared'; import type { SpaceVisibility, AvatarColor } from '@backspace/shared'; import { SPACE_GRADIENT_MAP, getSpaceGradient } from '../../utils/gradients'; const visibilityOptions: { value: SpaceVisibility; label: string; desc: string }[] = [ { value: 'private', label: 'Private', desc: 'Only people with an invite link can join' }, { value: 'request', label: 'Request to Join', desc: 'Visible in Explore — people can request to join' }, { value: 'public', label: 'Public', desc: 'Visible in Explore — anyone can join instantly' }, ]; export function CreateSpaceModal() { const [name, setName] = useState(''); const [visibility, setVisibility] = useState('private'); const [description, setDescription] = useState(''); const [iconFilename, setIconFilename] = useState(null); const [iconPreview, setIconPreview] = useState(null); const [uploadingIcon, setUploadingIcon] = useState(false); const [cropSrc, setCropSrc] = useState(null); const [avatarColor, setAvatarColor] = useState( AVATAR_COLORS[Math.floor(Math.random() * AVATAR_COLORS.length)] ?? 'mint' ); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const fileInputRef = useRef(null); const createSpace = useSpaceStore((s) => s.createSpace); const activeModal = useUIStore((s) => s.activeModal); const closeModal = useUIStore((s) => s.closeModal); const navigate = useNavigate(); const isOpen = activeModal === 'createSpace'; const handleIconSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => setCropSrc(reader.result as string); reader.readAsDataURL(file); // Reset the input so re-selecting the same file triggers onChange if (fileInputRef.current) fileInputRef.current.value = ''; }; const handleCropComplete = async (blob: Blob) => { // Show cropped preview if (iconPreview) URL.revokeObjectURL(iconPreview); const previewUrl = URL.createObjectURL(blob); setIconPreview(previewUrl); setCropSrc(null); // Upload the cropped image const file = new File([blob], 'icon.png', { type: 'image/png' }); setUploadingIcon(true); try { const tid = await useTransferStore.getState().startUpload(file, { tray: false }); const { filename } = await waitForTransferAttachment(tid); setIconFilename(filename); } catch { setError('Failed to upload icon'); setIconPreview(null); URL.revokeObjectURL(previewUrl); } finally { setUploadingIcon(false); } }; const handleRemoveIcon = () => { if (iconPreview) URL.revokeObjectURL(iconPreview); setIconFilename(null); setIconPreview(null); }; const handleClose = () => { closeModal(); setName(''); setVisibility('private'); setDescription(''); setIconFilename(null); if (iconPreview) URL.revokeObjectURL(iconPreview); setIconPreview(null); setCropSrc(null); setAvatarColor(AVATAR_COLORS[Math.floor(Math.random() * AVATAR_COLORS.length)] ?? 'mint'); setError(''); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); if (!name.trim()) { setError('Space name is required'); return; } setIsLoading(true); try { const space = await createSpace({ name: name.trim(), icon: iconFilename ?? undefined, avatarColor, visibility, description: description.trim() || undefined, }); handleClose(); navigate(`/channels/${space.id}`); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create space'); } finally { setIsLoading(false); } }; return ( <>
{error && (
{error}
)} {/* Icon Picker */}
{iconPreview && ( )}
{/* Icon Color */}
{AVATAR_COLORS.map((key) => { const entry = SPACE_GRADIENT_MAP[key]; return (
{/* Space Name */}
setName(e.target.value)} className="input-standard w-full" placeholder="My Awesome Space" autoFocus />
{/* Visibility */}
Visibility
{visibilityOptions.map((opt) => ( ))}
{/* Description */}
Description