Files
panel/src/components/NoteForm.js

171 lines
4.9 KiB
JavaScript

"use client";
import React, { useState, useEffect, useRef } from "react";
import { useTranslation } from "@/lib/i18n";
export default function NoteForm({ projectId, onNoteAdded }) {
const { t } = useTranslation();
const [note, setNote] = useState("");
const [status, setStatus] = useState(null);
const [projects, setProjects] = useState([]);
const [showDropdown, setShowDropdown] = useState(false);
const [filteredProjects, setFilteredProjects] = useState([]);
const [cursorPosition, setCursorPosition] = useState(0);
const [triggerChar, setTriggerChar] = useState(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const textareaRef = useRef(null);
useEffect(() => {
async function fetchProjects() {
try {
const res = await fetch("/api/projects");
if (res.ok) {
const data = await res.json();
setProjects(data);
}
} catch (error) {
console.error("Failed to fetch projects:", error);
}
}
fetchProjects();
}, []);
useEffect(() => {
if (note && cursorPosition > 0) {
const beforeCursor = note.slice(0, cursorPosition);
const match = beforeCursor.match(/([@#])([^@#]*)$/);
if (match) {
const char = match[1];
const query = match[2].toLowerCase();
setTriggerChar(char);
const filtered = projects.filter(project =>
project.project_name.toLowerCase().includes(query)
);
setFilteredProjects(filtered);
setShowDropdown(filtered.length > 0);
setSelectedIndex(0);
} else {
setShowDropdown(false);
}
} else {
setShowDropdown(false);
}
}, [note, cursorPosition, projects]);
async function handleSubmit(e) {
e.preventDefault();
const res = await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ project_id: projectId, note }),
});
if (res.ok) {
const newNote = await res.json();
setNote("");
setStatus(t("common.addNoteSuccess"));
// Call the callback to add the new note to the parent component's state
if (onNoteAdded) {
onNoteAdded(newNote);
}
// Clear status message after 3 seconds
setTimeout(() => setStatus(null), 3000);
} else {
setStatus(t("common.addNoteError"));
}
}
function handleKeyDown(e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
handleSubmit(e);
}
if (showDropdown) {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % filteredProjects.length);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => (prev - 1 + filteredProjects.length) % filteredProjects.length);
} else if (e.key === 'Enter') {
e.preventDefault();
if (filteredProjects.length > 0) {
selectProject(filteredProjects[selectedIndex]);
}
} else if (e.key === 'Escape') {
setShowDropdown(false);
}
}
}
function handleInputChange(e) {
setNote(e.target.value);
setCursorPosition(e.target.selectionStart);
}
function handleInputBlur() {
// Delay hiding dropdown to allow for clicks on dropdown items
setTimeout(() => setShowDropdown(false), 150);
}
function selectProject(project) {
const beforeCursor = note.slice(0, cursorPosition);
const afterCursor = note.slice(cursorPosition);
const match = beforeCursor.match(/([@#])([^@#]*)$/);
if (match) {
const start = match.index;
const end = cursorPosition;
const link = `[${triggerChar}${project.project_name}](/projects/${project.project_id})`;
const newNote = note.slice(0, start) + link + afterCursor;
setNote(newNote);
setShowDropdown(false);
// Set cursor after the inserted link
setTimeout(() => {
textareaRef.current.focus();
textareaRef.current.setSelectionRange(start + link.length, start + link.length);
}, 0);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-2 mb-4 relative">
<div className="relative">
<textarea
ref={textareaRef}
value={note}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onClick={(e) => setCursorPosition(e.target.selectionStart)}
onKeyUp={(e) => setCursorPosition(e.target.selectionStart)}
onBlur={handleInputBlur}
placeholder={t("common.addNotePlaceholder")}
className="border p-2 w-full"
rows={3}
required
/>
{showDropdown && (
<div className="absolute top-full left-0 right-0 bg-white border border-gray-300 rounded shadow-lg z-10 max-h-40 overflow-y-auto">
{filteredProjects.map((project, index) => (
<div
key={project.project_id}
className={`p-2 cursor-pointer ${index === selectedIndex ? 'bg-blue-100' : 'hover:bg-gray-100'}`}
onClick={() => selectProject(project)}
>
{triggerChar}{project.project_name}
</div>
))}
</div>
)}
</div>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
{t("common.addNote")}
</button>
{status && <p className="text-sm text-gray-600">{status}</p>}
</form>
);
}