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:
Michael Mainguy 2025-08-26 14:03:36 -05:00
parent c44c820239
commit de3fa100d1
2 changed files with 113 additions and 1 deletions

26
README.md Normal file
View 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.

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { useState, useCallback, useEffect } from 'react' import { useState, useCallback, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { IconCheck, IconX } from '@tabler/icons-react' import { IconCheck, IconX } from '@tabler/icons-react'
import Button from './Button' import Button from './Button'
@ -17,6 +17,10 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
const [isValid, setIsValid] = useState<boolean | null>(null) const [isValid, setIsValid] = useState<boolean | null>(null)
const [validationTimeout, setValidationTimeout] = useState<NodeJS.Timeout | null>(null) const [validationTimeout, setValidationTimeout] = useState<NodeJS.Timeout | null>(null)
const [mounted, setMounted] = useState(false) 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(() => { useEffect(() => {
setMounted(true) setMounted(true)
@ -42,6 +46,10 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
const result = await response.json() const result = await response.json()
setIsValid(result.valid) setIsValid(result.valid)
setSuggestions(result.suggestions || [])
setShowSuggestions(result.suggestions && result.suggestions.length > 0)
setSelectedSuggestionIndex(-1)
suggestionRefs.current = []
} catch (error) { } catch (error) {
setIsValid(false) setIsValid(false)
} finally { } finally {
@ -52,6 +60,8 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value const value = e.target.value
setDirectory(value) setDirectory(value)
setShowSuggestions(false) // Hide suggestions while typing
setSelectedSuggestionIndex(-1)
if (validationTimeout) { if (validationTimeout) {
clearTimeout(validationTimeout) clearTimeout(validationTimeout)
@ -64,6 +74,58 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
setValidationTimeout(timeout) 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 if (!isOpen || !mounted) return null
console.log('Modal is rendering with isOpen:', isOpen) console.log('Modal is rendering with isOpen:', isOpen)
@ -80,6 +142,9 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
const handleClose = () => { const handleClose = () => {
setDirectory('') setDirectory('')
setIsValid(null) setIsValid(null)
setSuggestions([])
setShowSuggestions(false)
setSelectedSuggestionIndex(-1)
if (validationTimeout) { if (validationTimeout) {
clearTimeout(validationTimeout) clearTimeout(validationTimeout)
} }
@ -101,6 +166,8 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
type="text" type="text"
value={directory} value={directory}
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={() => setShowSuggestions(suggestions.length > 0)}
placeholder="/path/to/photos" 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" 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 autoFocus
@ -116,6 +183,25 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod
<IconX className="h-4 w-4 text-red-600" /> <IconX className="h-4 w-4 text-red-600" />
)} )}
</div> </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>
<div className="flex gap-2 mt-4"> <div className="flex gap-2 mt-4">