initial commit 3??
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
import { type Component, createSignal, createEffect, onCleanup, onMount } from "solid-js";
|
||||
import { Search, Command, Clock, ArrowUpCircle, Plus } from "lucide-solid";
|
||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TextField, TextFieldInput } from "@/components/ui/textfield";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
|
||||
export const QuickEntry: Component = () => {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [input, setInput] = createSignal("");
|
||||
const [priority, setPriority] = createSignal<string>("5");
|
||||
const [urgency, setUrgency] = createSignal<string>("5");
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Explicit date string for the input (YYYY-MM-DDTHH:mm)
|
||||
const [dateString, setDateString] = createSignal<string>("");
|
||||
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 = () => {
|
||||
const text = input();
|
||||
if (!text || !priority() || !urgency()) return;
|
||||
|
||||
let finalPriority = parseInt(priority());
|
||||
let finalUrgency = parseInt(urgency());
|
||||
|
||||
// Parse shorthand overrides if present
|
||||
const pMatch = text.match(/\/p(\d+)/);
|
||||
if (pMatch) finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1])));
|
||||
|
||||
// Check for urgency override
|
||||
const uMatchNumeric = text.match(/\/u(\d+)/);
|
||||
if (uMatchNumeric) {
|
||||
finalUrgency = Math.min(10, Math.max(1, parseInt(uMatchNumeric[1])));
|
||||
}
|
||||
|
||||
let finalDueDate: Date | null = null;
|
||||
if (dateString()) {
|
||||
finalDueDate = new Date(dateString());
|
||||
} else {
|
||||
// If urgency is selected, calculate date
|
||||
const u = parseInt(urgency());
|
||||
if (!isNaN(u)) {
|
||||
// Check if override happened
|
||||
if (uMatchNumeric) {
|
||||
const dateStr = calculateDateFromUrgency(finalUrgency);
|
||||
finalDueDate = new Date(dateStr);
|
||||
} else {
|
||||
const dateStr = calculateDateFromUrgency(u);
|
||||
finalDueDate = new Date(dateStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const title = text
|
||||
.replace(/\/p\d+/, "")
|
||||
.replace(/\/u\d+/, "")
|
||||
.trim();
|
||||
|
||||
// Use the new signature: addTask(title, options)
|
||||
addTask(title, {
|
||||
priority: finalPriority,
|
||||
dueDate: finalDueDate || undefined, // undefined will let store pick default if that was the intent, but here we probably want strictly calculated date
|
||||
tags: []
|
||||
});
|
||||
|
||||
setInput("");
|
||||
setPriority("5");
|
||||
setUrgency("5");
|
||||
setDateString("");
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={() => setIsOpen(true)}
|
||||
class="fixed bottom-20 left-[80%] -translate-x-1/2 md:bottom-10 md:right-10 md:left-auto md:translate-x-0 w-14 h-14 rounded-full shadow-2xl z-50 group hover:scale-110 transition-all duration-300"
|
||||
>
|
||||
<Plus size={28} class="group-hover:rotate-90 transition-transform duration-300" />
|
||||
</Button>
|
||||
|
||||
{isOpen() && (
|
||||
<div class="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh] px-4">
|
||||
<div class="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={() => setIsOpen(false)} />
|
||||
|
||||
<div class="relative w-full max-w-xl bg-card border border-border rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<div class="flex items-center px-4 py-3 border-b border-border">
|
||||
<Search class="text-muted-foreground mr-3" size={20} />
|
||||
<TextField class="flex-1">
|
||||
<TextFieldInput
|
||||
ref={inputRef}
|
||||
value={input()}
|
||||
onInput={(e) => 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"
|
||||
/>
|
||||
</TextField>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row items-stretch md:items-center p-4 gap-4 border-b border-border bg-muted/10">
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Priority</label>
|
||||
<Select
|
||||
value={priority()}
|
||||
onChange={(val) => val && setPriority(val)}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
placeholder="Priority"
|
||||
itemComponent={(props) => (
|
||||
<SelectItem item={props.item}>
|
||||
{props.item.rawValue}
|
||||
</SelectItem>
|
||||
)}
|
||||
>
|
||||
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<ArrowUpCircle size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium">{priority()}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Urgency (Time)</label>
|
||||
<Select
|
||||
value={urgency()}
|
||||
onChange={(val) => {
|
||||
if (val) onUrgencySimpleChange(val);
|
||||
}}
|
||||
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
|
||||
placeholder="Urgency"
|
||||
itemComponent={(props) => (
|
||||
<SelectItem item={props.item}>
|
||||
{props.item.rawValue}
|
||||
</SelectItem>
|
||||
)}
|
||||
>
|
||||
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Clock size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium">{urgency()}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={dateString()}
|
||||
onInput={onDateInputChange}
|
||||
onClick={(e) => (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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-muted/30 flex justify-between items-center">
|
||||
<div class="flex items-center space-x-2 text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
<Command size={10} />
|
||||
<span class="hidden sm:inline">K to focus • Enter to Add</span>
|
||||
<span class="sm:hidden">Enter to Add</span>
|
||||
</div>
|
||||
<Button size="sm" onClick={parseAndAdd} disabled={!input() || !priority() || !urgency()}>
|
||||
Create Task
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user