171 lines
4.9 KiB
JavaScript
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>
|
|
);
|
|
}
|