import { type Component, createSignal, createEffect, onCleanup, onMount, For, Show } from "solid-js"; import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertMultipleTagDefinitions, type TaskTemplate } from "@/store"; import { Search, Command, Clock, ArrowUpCircle, Plus, Copy, ChevronDown } from "lucide-solid"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { TextField, TextFieldInput } from "@/components/ui/textfield"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { TagPicker } from "@/components/TagPicker"; import { lazy, Suspense } from "solid-js"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { toast } from "solid-sonner"; const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor }))); export const QuickEntry: Component = () => { // Diff tracking for reactive tags let lastParsedTags: string[] = []; const [isOpen, setIsOpen] = createSignal(false); const [input, setInput] = createSignal(""); const [priority, setPriority] = createSignal("5"); const [urgency, setUrgency] = createSignal("5"); const [tags, setTags] = createSignal([]); const [description, setDescription] = createSignal(""); const [editorInstance, setEditorInstance] = createSignal(null); // Reactive shorthand parsing createEffect(() => { const text = input(); const pMatch = text.match(/\/p(\d+)/); if (pMatch) { const val = Math.min(10, Math.max(1, parseInt(pMatch[1]))).toString(); setPriority(val); } const uMatch = text.match(/\/u(\d+)/); if (uMatch) { const val = Math.min(10, Math.max(1, parseInt(uMatch[1]))).toString(); if (val !== urgency()) { onUrgencySimpleChange(val); } } // Tag Shortcut Parsing: /t [tag name]:[optional value] // This is reactive. To avoid "prefix storm", only add if terminated by : or / // Tag Shortcut Parsing: /t [tag name]:[optional value] // Regex: /t, name (no : or /), lookahead for terminator (: or /), optional value group const tagRegex = /\/t\s*([^:/]+)(?=(?:[:/]))(?:[:](\d+))?/g; const newParsedTags: string[] = []; let match; while ((match = tagRegex.exec(text)) !== null) { const tagName = match[1].trim(); if (tagName) { newParsedTags.push(tagName); } } const currentTags = [...tags()]; // Determine changes const tagsToRemove = lastParsedTags.filter(t => !newParsedTags.includes(t)); const tagsToAdd = newParsedTags.filter(t => !currentTags.includes(t)); // Apply changes let nextTags = currentTags.filter(t => !tagsToRemove.includes(t)); nextTags = [...nextTags, ...tagsToAdd]; if (JSON.stringify(nextTags) !== JSON.stringify(currentTags)) { setTags(nextTags); } lastParsedTags = newParsedTags; }); // Explicit date string for the input (YYYY-MM-DDTHH:mm) const [dateString, setDateString] = createSignal(""); let inputRef: HTMLInputElement | undefined; createEffect(() => { if (isOpen()) { // Slight delay to ensure DOM is ready and animations don't interfere setTimeout(() => inputRef?.focus(), 50); } }); const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); if (isOpen()) { inputRef?.focus(); } else { setIsOpen(true); } } if (e.key === "Escape") { setIsOpen(false); } }; onMount(() => window.addEventListener("keydown", handleKeyDown)); onCleanup(() => window.removeEventListener("keydown", handleKeyDown)); // When urgency changes via dropdown, update the date const onUrgencySimpleChange = (val: string) => { setUrgency(val); const level = parseInt(val); if (!isNaN(level)) { const newDateIso = calculateDateFromUrgency(level); // Format for datetime-local input: YYYY-MM-DDTHH:mm const dateObj = new Date(newDateIso); const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); setDateString(localIso); } }; // When date changes via input, update urgency level const onDateInputChange = (e: Event) => { const val = (e.target as HTMLInputElement).value; setDateString(val); if (val) { const level = calculateUrgencyFromDate(new Date(val).toISOString()); setUrgency(level.toString()); } }; const parseAndAdd = async () => { let text = input(); if (!text || !priority() || !urgency()) return; let finalPriority = parseInt(priority()); let finalUrgency = parseInt(urgency()); const finalTags = [...tags()]; // 1. Handle Priority / Urgency and clean them from text const pMatch = text.match(/\/p\s*(\d+)/); if (pMatch) { finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1]))); text = text.replace(pMatch[0], ""); } const uMatch = text.match(/\/u\s*(\d+)/); if (uMatch) { finalUrgency = Math.min(10, Math.max(1, parseInt(uMatch[1]))); text = text.replace(uMatch[0], ""); } // 2. Handle Tags: /t [name]:[value] const segments = text.split(/\/t\s*/); let title = segments[0]; const tagsToUpsert: Record = {}; for (let i = 1; i < segments.length; i++) { const seg = segments[i]; const firstColon = seg.indexOf(':'); let tagName = ""; let tagValue: number | undefined; let remainingText = ""; if (firstColon === -1) { tagName = seg.trim(); } else { tagName = seg.slice(0, firstColon).trim(); const afterColon = seg.slice(firstColon + 1); const valueMatch = afterColon.match(/^\s*(\d+)/); if (valueMatch) { tagValue = Math.min(10, Math.max(0, parseInt(valueMatch[1]))); remainingText = afterColon.slice(valueMatch[0].length); } else { remainingText = afterColon; } } if (tagName) { if (!finalTags.includes(tagName)) { finalTags.push(tagName); } if (tagValue !== undefined) { tagsToUpsert[tagName] = tagValue; } else if (store.tagDefinitions?.[tagName] === undefined) { tagsToUpsert[tagName] = 5; } } title += " " + remainingText; } if (Object.keys(tagsToUpsert).length > 0) { upsertMultipleTagDefinitions(tagsToUpsert); } let finalDueDate: Date | null = null; if (dateString()) { finalDueDate = new Date(dateString()); } else { const u = finalUrgency; const dateStr = calculateDateFromUrgency(u); finalDueDate = new Date(dateStr); } addTask(title.replace(/\s+/g, " ").trim(), { priority: finalPriority, dueDate: finalDueDate || undefined, tags: finalTags, content: description() }); setInput(""); setPriority("5"); setUrgency("5"); setTags([]); setDescription(""); lastParsedTags = []; setDateString(""); setIsOpen(false); // Reset editor content const instance = editorInstance(); if (instance) instance.commands.setContent(""); }; const applyTemplate = (template: TaskTemplate) => { setInput(template.title); setPriority(template.priority.toString()); setUrgency(template.urgency.toString()); setTags([...(template.tags || [])]); setDescription(template.content || ""); // Sync urgency to date const level = template.urgency; const newDateIso = calculateDateFromUrgency(level); const dateObj = new Date(newDateIso); const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); setDateString(localIso); // Update editor if ready const instance = editorInstance(); if (instance) { instance.commands.setContent(template.content || ""); } toast.info(`Applied template: ${template.name}`); }; return ( <> {isOpen() && (
setIsOpen(false)} />
setInput(e.currentTarget.value)} onKeyDown={(e) => e.key === "Enter" && parseAndAdd()} placeholder="Task title... type /p1-10 for priority and /u1-10 for urgency" class="border-none shadow-none focus-visible:ring-0 text-lg" />
(e.target as HTMLInputElement).showPicker?.()} onKeyDown={(e) => { if (e.key !== "Tab" && e.key !== "Escape") { e.preventDefault(); } }} step="900" class="w-full h-10 px-3 bg-background border-none rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring z-[102] relative cursor-pointer" />
{(tag) => ( setTags(tags().filter(t => t !== tag))} >
5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} /> {tag} )}
}>
Enter to Add
0}>
{(template) => ( )}
)} ); };