import { useState, useCallback, useRef, useEffect } from "react";
import type { DaemonDirectory } from "../../lib/api";
interface DirectoryInputProps {
value: string;
onChange: (value: string) => void;
suggestions: DaemonDirectory[];
placeholder?: string;
/** Repository URL to extract repo name for home directory suggestions */
repoUrl?: string | null;
className?: string;
disabled?: boolean;
}
/** Extract repository name from URL */
function extractRepoName(url: string | null | undefined): string | null {
if (!url) return null;
// Handle various URL formats:
// https://github.com/user/repo.git -> repo
// https://github.com/user/repo -> repo
// git@github.com:user/repo.git -> repo
// /path/to/local/repo -> repo
let name = url;
// Remove trailing .git
if (name.endsWith(".git")) {
name = name.slice(0, -4);
}
// Remove trailing slash
if (name.endsWith("/")) {
name = name.slice(0, -1);
}
// Get the last path segment
const lastSlash = name.lastIndexOf("/");
if (lastSlash !== -1) {
name = name.slice(lastSlash + 1);
}
// Handle git@host:user/repo format
const colonIndex = name.lastIndexOf(":");
if (colonIndex !== -1) {
const afterColon = name.slice(colonIndex + 1);
const slashIndex = afterColon.lastIndexOf("/");
if (slashIndex !== -1) {
name = afterColon.slice(slashIndex + 1);
} else {
name = afterColon;
}
}
return name || null;
}
export function DirectoryInput({
value,
onChange,
suggestions,
placeholder = "/path/to/directory",
repoUrl,
className = "",
disabled = false,
}: DirectoryInputProps) {
const [showSuggestions, setShowSuggestions] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
// Extract repo name for home directory suggestions
const repoName = extractRepoName(repoUrl);
// Process suggestions to add repo name to home directory
const processedSuggestions = suggestions.map((dir) => {
if (dir.directoryType === "home" && repoName) {
return {
...dir,
path: `${dir.path}/${repoName}`,
label: `${dir.label} (${repoName})`,
};
}
return dir;
});
// Filter suggestions based on current input
const filteredSuggestions = processedSuggestions.filter((dir) => {
if (!value) return true;
const lowerValue = value.toLowerCase();
return (
dir.path.toLowerCase().includes(lowerValue) ||
dir.label.toLowerCase().includes(lowerValue)
);
});
// Handle clicking outside to close dropdown
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node) &&
inputRef.current &&
!inputRef.current.contains(e.target as Node)
) {
setShowSuggestions(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleFocus = useCallback(() => {
setShowSuggestions(true);
setHighlightedIndex(-1);
}, []);
const handleBlur = useCallback(() => {
// Delay hiding to allow click on suggestion
setTimeout(() => {
setShowSuggestions(false);
}, 150);
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (!showSuggestions || filteredSuggestions.length === 0) return;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setHighlightedIndex((prev) =>
prev < filteredSuggestions.length - 1 ? prev + 1 : 0
);
break;
case "ArrowUp":
e.preventDefault();
setHighlightedIndex((prev) =>
prev > 0 ? prev - 1 : filteredSuggestions.length - 1
);
break;
case "Enter":
e.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < filteredSuggestions.length) {
onChange(filteredSuggestions[highlightedIndex].path);
setShowSuggestions(false);
}
break;
case "Escape":
setShowSuggestions(false);
break;
}
},
[showSuggestions, filteredSuggestions, highlightedIndex, onChange]
);
const handleSelectSuggestion = useCallback(
(path: string) => {
onChange(path);
setShowSuggestions(false);
inputRef.current?.focus();
},
[onChange]
);
return (
<div className={`relative ${className}`}>
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
className="w-full bg-[#0a1525] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-3 py-2 outline-none focus:border-[#3f6fb3] disabled:opacity-50"
/>
{/* Suggestions dropdown */}
{showSuggestions && filteredSuggestions.length > 0 && (
<div
ref={dropdownRef}
className="absolute z-50 left-0 right-0 top-full mt-1 bg-[#0a1525] border border-[rgba(117,170,252,0.35)] shadow-lg max-h-48 overflow-auto"
>
{filteredSuggestions.map((dir, index) => (
<button
key={`${dir.directoryType}-${index}`}
type="button"
onClick={() => handleSelectSuggestion(dir.path)}
onMouseEnter={() => setHighlightedIndex(index)}
className={`w-full text-left px-3 py-2 font-mono text-xs transition-colors ${
index === highlightedIndex
? "bg-[rgba(117,170,252,0.2)] text-[#dbe7ff]"
: "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)]"
}`}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-[#75aafc]">{dir.label}</span>
{dir.exists === true && (
<span className="text-[#f0ad4e] text-[10px]" title="Directory already exists">
(exists)
</span>
)}
</div>
{dir.hostname && (
<span className="text-[#555] text-[10px]">({dir.hostname})</span>
)}
</div>
<div className="text-[10px] text-[#555] truncate">{dir.path}</div>
</button>
))}
</div>
)}
</div>
);
}