/** * Miro Integration Modal * * Allows users to import Miro boards into their canvas. * Supports two methods: * 1. Paste JSON from miro-export CLI tool (recommended for casual use) * 2. Connect Miro API for direct imports (power users) */ import React, { useState, useCallback, useRef } from 'react'; import { useEditor } from 'tldraw'; import { importMiroJson } from '@/lib/miroImport'; import { getMiroApiKey, saveMiroApiKey, removeMiroApiKey, isMiroApiKeyConfigured, extractMiroBoardId, isValidMiroBoardUrl, } from '@/lib/miroApiKey'; interface MiroIntegrationModalProps { isOpen: boolean; onClose: () => void; username: string; isDarkMode?: boolean; } type Tab = 'import' | 'api-setup' | 'help'; export function MiroIntegrationModal({ isOpen, onClose, username, isDarkMode: _isDarkMode = false, }: MiroIntegrationModalProps) { const editor = useEditor(); const fileInputRef = useRef(null); const [activeTab, setActiveTab] = useState('import'); const [jsonText, setJsonText] = useState(''); const [boardUrl, setBoardUrl] = useState(''); const [apiKeyInput, setApiKeyInput] = useState(''); const [isImporting, setIsImporting] = useState(false); const [progress, setProgress] = useState({ stage: '', percent: 0 }); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const hasApiKey = isMiroApiKeyConfigured(username); const resetState = useCallback(() => { setJsonText(''); setBoardUrl(''); setIsImporting(false); setProgress({ stage: '', percent: 0 }); setError(null); setSuccess(null); }, []); const handleClose = useCallback(() => { resetState(); onClose(); }, [onClose, resetState]); // Import from JSON string const handleJsonImport = useCallback(async (json: string) => { setIsImporting(true); setError(null); setSuccess(null); try { const viewportBounds = editor.getViewportPageBounds(); const offset = { x: viewportBounds.x + viewportBounds.w / 2, y: viewportBounds.y + viewportBounds.h / 2, }; const result = await importMiroJson( json, { migrateAssets: true, offset }, { onProgress: (stage, percent) => { setProgress({ stage, percent: Math.round(percent * 100) }); }, } ); if (result.success && result.shapes.length > 0) { // Create assets first for (const asset of result.assets) { try { editor.createAssets([asset]); } catch (e) { console.warn('Failed to create asset:', e); } } // Create shapes editor.createShapes(result.shapes); // Select and zoom to imported shapes const shapeIds = result.shapes.map((s: any) => s.id); editor.setSelectedShapes(shapeIds); editor.zoomToSelection(); setSuccess(`Imported ${result.shapesCreated} shapes${result.assetsUploaded > 0 ? ` and ${result.assetsUploaded} images` : ''}!`); // Auto-close after success setTimeout(() => handleClose(), 2000); } else { setError(result.errors.join(', ') || 'No shapes found in the import'); } } catch (e) { console.error('Import error:', e); setError(e instanceof Error ? e.message : 'Failed to import Miro board'); } finally { setIsImporting(false); } }, [editor, handleClose]); // Handle file upload const handleFileSelect = useCallback(async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; try { const text = await file.text(); await handleJsonImport(text); } catch (e) { setError('Failed to read file'); } if (fileInputRef.current) { fileInputRef.current.value = ''; } }, [handleJsonImport]); // Handle paste import const handlePasteImport = useCallback(() => { if (!jsonText.trim()) { setError('Please paste Miro JSON data'); return; } handleJsonImport(jsonText); }, [jsonText, handleJsonImport]); // Save API key const handleSaveApiKey = useCallback(() => { if (!apiKeyInput.trim()) { setError('Please enter your Miro API token'); return; } saveMiroApiKey(apiKeyInput.trim(), username); setApiKeyInput(''); setSuccess('Miro API token saved!'); setTimeout(() => setSuccess(null), 2000); }, [apiKeyInput, username]); // Disconnect API const handleDisconnectApi = useCallback(() => { removeMiroApiKey(username); setSuccess('Miro API disconnected'); setTimeout(() => setSuccess(null), 2000); }, [username]); if (!isOpen) return null; return (
{ if (e.target === e.currentTarget) handleClose(); }} >
e.stopPropagation()} > {/* Header */}
📋

Import from Miro

Bring your Miro boards into the canvas

{/* Tabs */}
{[ { id: 'import', label: 'Import Board' }, { id: 'api-setup', label: 'API Setup' }, { id: 'help', label: 'How It Works' }, ].map((tab) => ( ))}
{/* Content */}
{/* Import Tab */} {activeTab === 'import' && (
{/* Method 1: JSON Upload */}

1 Upload JSON File

Export your board using the miro-export CLI, then upload the JSON file here.

{/* Divider */}
or
{/* Method 2: Paste JSON */}

2 Paste JSON