diff --git a/src/app/projects/[id]/page.js b/src/app/projects/[id]/page.js index c186628..99967d8 100644 --- a/src/app/projects/[id]/page.js +++ b/src/app/projects/[id]/page.js @@ -28,6 +28,41 @@ export default function ProjectViewPage() { const [loading, setLoading] = useState(true); const [uploadedFiles, setUploadedFiles] = useState([]); + // Helper function to parse note text with links + const parseNoteText = (text) => { + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + const parts = []; + let lastIndex = 0; + let match; + + while ((match = linkRegex.exec(text)) !== null) { + // Add text before the link + if (match.index > lastIndex) { + parts.push(text.slice(lastIndex, match.index)); + } + // Add the link + parts.push( + + {match[1]} + + ); + lastIndex = match.index + match[0].length; + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + + return parts.length > 0 ? parts : text; + }; + // Helper function to add a new note to the list const addNote = (newNote) => { setNotes(prevNotes => [newNote, ...prevNotes]); @@ -758,7 +793,9 @@ export default function ProjectViewPage() { )} -

{n.note}

+
+ {parseNoteText(n.note)} +
))} diff --git a/src/components/NoteForm.js b/src/components/NoteForm.js index b3db5ae..6f649d3 100644 --- a/src/components/NoteForm.js +++ b/src/components/NoteForm.js @@ -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 ( -
-