first commit
This commit is contained in:
169
src/components/NavigationConsole.tsx
Normal file
169
src/components/NavigationConsole.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
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="Nom de l'espace ou URL..."
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user