diff --git a/src/automerge/AutomergeToTLStore.ts b/src/automerge/AutomergeToTLStore.ts index 6c90a35..6ffd93d 100644 --- a/src/automerge/AutomergeToTLStore.ts +++ b/src/automerge/AutomergeToTLStore.ts @@ -765,6 +765,79 @@ export function sanitizeRecord(record: any): TLRecord { console.log(`🔧 Sanitized Multmux shape ${sanitized.id} props:`, JSON.stringify(sanitized.props)) } + // CRITICAL: Sanitize Map shapes - ensure all required props have defaults + // Old shapes may be missing pinnedToView, isMinimized, or other newer properties + if (sanitized.type === 'Map') { + // Ensure boolean props have proper defaults (old data may have undefined) + if (typeof sanitized.props.pinnedToView !== 'boolean') { + sanitized.props.pinnedToView = false + } + if (typeof sanitized.props.isMinimized !== 'boolean') { + sanitized.props.isMinimized = false + } + if (typeof sanitized.props.showSidebar !== 'boolean') { + sanitized.props.showSidebar = true + } + if (typeof sanitized.props.interactive !== 'boolean') { + sanitized.props.interactive = true + } + if (typeof sanitized.props.showGPS !== 'boolean') { + sanitized.props.showGPS = false + } + if (typeof sanitized.props.showSearch !== 'boolean') { + sanitized.props.showSearch = false + } + if (typeof sanitized.props.showDirections !== 'boolean') { + sanitized.props.showDirections = false + } + if (typeof sanitized.props.sharingLocation !== 'boolean') { + sanitized.props.sharingLocation = false + } + // Ensure array props exist + if (!Array.isArray(sanitized.props.annotations)) { + sanitized.props.annotations = [] + } + if (!Array.isArray(sanitized.props.waypoints)) { + sanitized.props.waypoints = [] + } + if (!Array.isArray(sanitized.props.collaborators)) { + sanitized.props.collaborators = [] + } + if (!Array.isArray(sanitized.props.gpsUsers)) { + sanitized.props.gpsUsers = [] + } + if (!Array.isArray(sanitized.props.tags)) { + sanitized.props.tags = ['map'] + } + // Ensure string props exist + if (typeof sanitized.props.styleKey !== 'string') { + sanitized.props.styleKey = 'voyager' + } + if (typeof sanitized.props.title !== 'string') { + sanitized.props.title = 'Collaborative Map' + } + if (typeof sanitized.props.description !== 'string') { + sanitized.props.description = '' + } + // Ensure viewport exists with defaults + if (!sanitized.props.viewport || typeof sanitized.props.viewport !== 'object') { + sanitized.props.viewport = { + center: { lat: 40.7128, lng: -74.006 }, + zoom: 12, + bearing: 0, + pitch: 0, + } + } + // Ensure numeric props + if (typeof sanitized.props.w !== 'number' || isNaN(sanitized.props.w)) { + sanitized.props.w = 800 + } + if (typeof sanitized.props.h !== 'number' || isNaN(sanitized.props.h)) { + sanitized.props.h = 550 + } + console.log(`🔧 Sanitized Map shape ${sanitized.id}`) + } + // CRITICAL: Infer type from properties BEFORE defaulting to 'geo' // This ensures arrows and other shapes are properly recognized if (!sanitized.type || typeof sanitized.type !== 'string') { diff --git a/src/components/auth/CryptIDDropdown.tsx b/src/components/auth/CryptIDDropdown.tsx new file mode 100644 index 0000000..58390ac --- /dev/null +++ b/src/components/auth/CryptIDDropdown.tsx @@ -0,0 +1,417 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useAuth } from '../../context/AuthContext'; +import CryptID from './CryptID'; +import { GoogleDataService, type GoogleService } from '../../lib/google'; +import { GoogleExportBrowser } from '../GoogleExportBrowser'; + +interface CryptIDDropdownProps { + isDarkMode?: boolean; +} + +/** + * CryptID dropdown component for the top-right corner. + * Shows logged-in user with dropdown containing account info and integrations. + */ +const CryptIDDropdown: React.FC = ({ isDarkMode = false }) => { + const { session, logout } = useAuth(); + const [showDropdown, setShowDropdown] = useState(false); + const [showCryptIDModal, setShowCryptIDModal] = useState(false); + const [showGoogleBrowser, setShowGoogleBrowser] = useState(false); + const [googleConnected, setGoogleConnected] = useState(false); + const [googleLoading, setGoogleLoading] = useState(false); + const [googleCounts, setGoogleCounts] = useState>({ + gmail: 0, + drive: 0, + photos: 0, + calendar: 0, + }); + const dropdownRef = useRef(null); + + // Check Google connection on mount + useEffect(() => { + const checkGoogleStatus = async () => { + try { + const service = GoogleDataService.getInstance(); + const isAuthed = await service.isAuthenticated(); + setGoogleConnected(isAuthed); + if (isAuthed) { + const counts = await service.getStoredCounts(); + setGoogleCounts(counts); + } + } catch (error) { + console.warn('Failed to check Google status:', error); + } + }; + checkGoogleStatus(); + }, []); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setShowDropdown(false); + } + }; + if (showDropdown) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showDropdown]); + + const handleGoogleConnect = async () => { + setGoogleLoading(true); + try { + const service = GoogleDataService.getInstance(); + await service.authenticate(['gmail', 'drive', 'photos', 'calendar']); + setGoogleConnected(true); + const counts = await service.getStoredCounts(); + setGoogleCounts(counts); + } catch (error) { + console.error('Google auth failed:', error); + } finally { + setGoogleLoading(false); + } + }; + + const handleGoogleDisconnect = async () => { + try { + const service = GoogleDataService.getInstance(); + await service.signOut(); + setGoogleConnected(false); + setGoogleCounts({ gmail: 0, drive: 0, photos: 0, calendar: 0 }); + } catch (error) { + console.error('Google disconnect failed:', error); + } + }; + + const handleAddToCanvas = async (items: any[], position: { x: number; y: number }) => { + // Emit event for canvas to handle + window.dispatchEvent(new CustomEvent('add-google-items-to-canvas', { + detail: { items, position } + })); + setShowGoogleBrowser(false); + setShowDropdown(false); + }; + + const totalGoogleItems = Object.values(googleCounts).reduce((a, b) => a + b, 0); + + // Get initials for avatar + const getInitials = (name: string) => { + return name.charAt(0).toUpperCase(); + }; + + // If showing CryptID modal + if (showCryptIDModal) { + return ( +
+
+ setShowCryptIDModal(false)} + onCancel={() => setShowCryptIDModal(false)} + /> +
+
+ ); + } + + return ( +
+ {/* Trigger button */} + + + {/* Dropdown menu */} + {showDropdown && ( +
+ {session.authed ? ( + <> + {/* Account section */} +
+
+
+ {getInitials(session.username)} +
+
+
+ {session.username} +
+
+ 🔒 CryptID secured +
+
+
+
+ + {/* Integrations section */} +
+
+ Integrations +
+ + {/* Google Workspace */} +
+
+
+ G +
+
+
+ Google Workspace +
+
+ {googleConnected ? `${totalGoogleItems} items imported` : 'Not connected'} +
+
+ {googleConnected && ( + + )} +
+ +
+ {googleConnected ? ( + <> + + + + ) : ( + + )} +
+
+
+ + {/* Sign out */} +
+ +
+ + ) : ( +
+
+
+ Sign in with CryptID +
+
+ Create a username to edit boards and sync your data across devices. +
+
+ +
+ )} +
+ )} + + {/* Google Export Browser Modal */} + {showGoogleBrowser && ( + setShowGoogleBrowser(false)} + onAddToCanvas={handleAddToCanvas} + isDarkMode={isDarkMode} + /> + )} +
+ ); +}; + +export default CryptIDDropdown; diff --git a/src/components/networking/NetworkGraphMinimap.tsx b/src/components/networking/NetworkGraphMinimap.tsx index ed2d923..a86191e 100644 --- a/src/components/networking/NetworkGraphMinimap.tsx +++ b/src/components/networking/NetworkGraphMinimap.tsx @@ -379,7 +379,7 @@ export function NetworkGraphMinimap({
-

Network

+

Social Network

- - {/* Dropdown with user names */} - {showDropdown && ( -
-
- Participants ({totalUsers}) -
- - {/* Current user */} -
-
- {myUserName.charAt(0).toUpperCase()} -
- - {myUserName} (you) - -
- - {/* Other users */} - {collaborators.map((presence) => ( -
-
- {(presence.userName || 'A').charAt(0).toUpperCase()} -
- - {presence.userName || 'Anonymous'} - -
- ))} - - {/* Separator */} -
- - {/* Google Workspace Section */} -
- Integrations -
- -
-
- G -
-
-
- Google Workspace -
-
- {googleConnected ? 'Connected' : 'Not connected'} -
-
- {googleConnected ? ( - - ) : null} -
- - {/* Google action buttons */} -
- {!googleConnected ? ( - - ) : ( - - )} -
-
- )} - - {/* Google Export Browser Modal */} - {showGoogleBrowser && ( - setShowGoogleBrowser(false)} - onAddToCanvas={handleAddToCanvas} - isDarkMode={isDarkMode} - /> - )} - - {/* Click outside to close */} - {showDropdown && ( -
setShowDropdown(false)} - /> - )} -
- ) -} - -// Custom SharePanel that shows people menu and help button +// Custom SharePanel with layout: CryptID -> Star -> Gear -> Question mark function CustomSharePanel() { const tools = useTools() const actions = useActions() const [showShortcuts, setShowShortcuts] = React.useState(false) + const [showSettings, setShowSettings] = React.useState(false) + const [showSettingsDropdown, setShowSettingsDropdown] = React.useState(false) + + // Detect dark mode - use state to trigger re-render on change + const [isDarkMode, setIsDarkMode] = React.useState( + typeof document !== 'undefined' && document.documentElement.classList.contains('dark') + ) + + const handleToggleDarkMode = () => { + const newIsDark = !document.documentElement.classList.contains('dark') + document.documentElement.classList.toggle('dark') + localStorage.setItem('theme', newIsDark ? 'dark' : 'light') + setIsDarkMode(newIsDark) + } // Helper to extract label string from tldraw label (can be string or {default, menu} object) const getLabelString = (label: any, fallback: string): string => { @@ -487,9 +119,159 @@ function CustomSharePanel() { return (
- {/* Help/Keyboard shortcuts button */} + {/* CryptID dropdown - leftmost */} + + + {/* Star board button */} + + + {/* Settings gear button with dropdown */} +
+ + + {/* Settings dropdown */} + {showSettingsDropdown && ( + <> +
setShowSettingsDropdown(false)} + /> +
+ {/* Dark mode toggle */} + + +
+ + {/* All settings */} + +
+ + )} +
+ + {/* Help/Keyboard shortcuts button - rightmost */}
) }