feat: Add link parsing functionality to notes in ProjectViewPage and TaskCommentsModal

This commit is contained in:
2025-10-02 11:36:36 +02:00
parent 31736ccc78
commit 79238dd643
3 changed files with 194 additions and 15 deletions

View File

@@ -1,12 +1,56 @@
"use client";
import React, { useState } from "react";
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();
@@ -37,19 +81,83 @@ export default function NoteForm({ projectId, onNoteAdded }) {
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">
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t("common.addNotePlaceholder")}
className="border p-2 w-full"
rows={3}
required
/>
<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"