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 (
-