Improve directory modal with keyboard navigation and README
- Add arrow key navigation for autosuggestion dropdown - Add scroll-into-view for long suggestion lists - Add enter key selection for highlighted suggestions - Add README with macOS CIFS share access instructions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c44c820239
commit
de3fa100d1
26
README.md
Normal file
26
README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Photos Gallery
|
||||
|
||||
A Next.js application for displaying and organizing photos.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### macOS CIFS Share Access
|
||||
|
||||
If you're running this application on macOS and accessing photos from a mounted CIFS share, you'll need to grant your terminal application full disk access:
|
||||
|
||||
1. Open **System Preferences** → **Security & Privacy** → **Privacy**
|
||||
2. Select **Full Disk Access** from the left sidebar
|
||||
3. Click the lock icon and enter your password to make changes
|
||||
4. Click the **+** button and add your terminal application (e.g., Terminal.app, iTerm2, etc.)
|
||||
5. Restart your terminal application
|
||||
|
||||
This is required because macOS restricts access to network-mounted drives without explicit permission.
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { IconCheck, IconX } from '@tabler/icons-react'
|
||||
import Button from './Button'
|
||||
@ -17,6 +17,10 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
|
||||
const [isValid, setIsValid] = useState<boolean | null>(null)
|
||||
const [validationTimeout, setValidationTimeout] = useState<NodeJS.Timeout | null>(null)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1)
|
||||
const suggestionRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
@ -42,6 +46,10 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
|
||||
|
||||
const result = await response.json()
|
||||
setIsValid(result.valid)
|
||||
setSuggestions(result.suggestions || [])
|
||||
setShowSuggestions(result.suggestions && result.suggestions.length > 0)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
suggestionRefs.current = []
|
||||
} catch (error) {
|
||||
setIsValid(false)
|
||||
} finally {
|
||||
@ -52,6 +60,8 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
setDirectory(value)
|
||||
setShowSuggestions(false) // Hide suggestions while typing
|
||||
setSelectedSuggestionIndex(-1)
|
||||
|
||||
if (validationTimeout) {
|
||||
clearTimeout(validationTimeout)
|
||||
@ -64,6 +74,58 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
|
||||
setValidationTimeout(timeout)
|
||||
}
|
||||
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
setDirectory(suggestion)
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
validateDirectory(suggestion)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (!showSuggestions || suggestions.length === 0) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedSuggestionIndex(prev => {
|
||||
const newIndex = prev === -1 ? 0 : (prev < suggestions.length - 1 ? prev + 1 : 0)
|
||||
setTimeout(() => {
|
||||
suggestionRefs.current[newIndex]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest'
|
||||
})
|
||||
}, 0)
|
||||
return newIndex
|
||||
})
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedSuggestionIndex(prev => {
|
||||
const newIndex = prev > 0 ? prev - 1 : suggestions.length - 1
|
||||
setTimeout(() => {
|
||||
suggestionRefs.current[newIndex]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest'
|
||||
})
|
||||
}, 0)
|
||||
return newIndex
|
||||
})
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < suggestions.length) {
|
||||
handleSuggestionClick(suggestions[selectedSuggestionIndex])
|
||||
} else if (directory.trim() && isValid) {
|
||||
handleSave()
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen || !mounted) return null
|
||||
|
||||
console.log('Modal is rendering with isOpen:', isOpen)
|
||||
@ -80,6 +142,9 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
|
||||
const handleClose = () => {
|
||||
setDirectory('')
|
||||
setIsValid(null)
|
||||
setSuggestions([])
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
if (validationTimeout) {
|
||||
clearTimeout(validationTimeout)
|
||||
}
|
||||
@ -101,6 +166,8 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
|
||||
type="text"
|
||||
value={directory}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setShowSuggestions(suggestions.length > 0)}
|
||||
placeholder="/path/to/photos"
|
||||
className="w-full p-2 pr-10 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
autoFocus
|
||||
@ -116,6 +183,25 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
|
||||
<IconX className="h-4 w-4 text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded shadow-lg max-h-48 overflow-y-auto z-10">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={(el) => (suggestionRefs.current[index] = el)}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
className={`px-3 py-2 cursor-pointer text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-600 last:border-b-0 ${
|
||||
index === selectedSuggestionIndex
|
||||
? 'bg-blue-100 dark:bg-blue-900'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{suggestion}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
|
Loading…
Reference in New Issue
Block a user