170 lines
4.9 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|