Files
palace/src/components/NavigationConsole.tsx

170 lines
4.9 KiB
TypeScript

import { useState, useEffect, useRef } from 'react'
import { navigateToSpace } from '../store'
import { db } from '../db'
interface NavigationConsoleProps {
isOpen: boolean
onClose: () => void
}
export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
const [input, setInput] = useState('')
const [existingSpaces, setExistingSpaces] = useState<string[]>([])
const [filteredSpaces, setFilteredSpaces] = useState<string[]>([])
const [selectedIndex, setSelectedIndex] = useState(0)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
const loadSpaces = async () => {
const spaces = await db.getAllSpaceIds()
setExistingSpaces(spaces)
}
if (isOpen) {
loadSpaces()
}
}, [isOpen])
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus()
}
}, [isOpen])
useEffect(() => {
if (input.trim() === '') {
setFilteredSpaces(existingSpaces.slice(0, 8))
} else {
const filtered = existingSpaces.filter(space =>
space.toLowerCase().includes(input.toLowerCase())
).slice(0, 8)
if (filtered.length === 0 || !filtered.includes(input)) {
filtered.unshift(input)
}
setFilteredSpaces(filtered)
}
setSelectedIndex(0)
}, [input, existingSpaces])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
} else if (e.key === 'Enter') {
e.preventDefault()
const targetSpace = filteredSpaces[selectedIndex] || input
if (targetSpace.trim()) {
navigateToSpace(sanitizeSpaceId(targetSpace.trim()))
onClose()
}
} else if (e.key === 'ArrowDown') {
e.preventDefault()
setSelectedIndex(prev => (prev + 1) % filteredSpaces.length)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setSelectedIndex(prev => (prev - 1 + filteredSpaces.length) % filteredSpaces.length)
}
}
const sanitizeSpaceId = (input: string): string => {
if (input.startsWith('http://') || input.startsWith('https://')) {
try {
const url = new URL(input)
const path = url.pathname.slice(1) || url.hostname
return path.toLowerCase().replace(/[^a-z0-9]/g, '-')
} catch {
return input.toLowerCase().replace(/[^a-z0-9]/g, '-')
}
}
return input.toLowerCase().replace(/[^a-z0-9]/g, '-')
}
const handleItemClick = (space: string) => {
navigateToSpace(sanitizeSpaceId(space))
onClose()
}
if (!isOpen) return null
return (
<>
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
zIndex: 2000
}}
onClick={onClose}
/>
<div
style={{
position: 'fixed',
top: '20%',
left: '50%',
transform: 'translateX(-50%)',
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: '4px',
width: '400px',
zIndex: 2001,
fontFamily: 'monospace',
fontSize: '14px'
}}
>
<div style={{ padding: '8px' }}>
<input
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Name a space"
style={{
width: '100%',
background: 'transparent',
border: 'none',
outline: 'none',
color: 'white',
fontSize: '14px',
fontFamily: 'monospace'
}}
/>
</div>
{filteredSpaces.length > 0 && (
<div style={{ borderTop: '1px solid #333', maxHeight: '200px', overflowY: 'auto' }}>
{filteredSpaces.map((space, index) => {
const isExisting = existingSpaces.includes(space)
const isSelected = index === selectedIndex
const isUrl = space.startsWith('http://') || space.startsWith('https://')
return (
<div
key={space}
onClick={() => handleItemClick(space)}
style={{
padding: '8px 12px',
cursor: 'pointer',
background: isSelected ? '#333' : 'transparent',
color: isExisting ? '#4CAF50' : (isUrl ? '#2196F3' : '#FFA726'),
borderLeft: isSelected ? '3px solid white' : '3px solid transparent'
}}
>
{space}
{!isExisting && (
<span style={{ color: '#666', marginLeft: '8px', fontSize: '12px' }}>
(nouveau)
</span>
)}
</div>
)
})}
</div>
)}
</div>
</>
)
}