fixed urgency-date connection and added urgency-date guidance. fixed password extension break. fixed template saving.

This commit is contained in:
2026-01-31 19:19:33 -06:00
parent 8f7a067b75
commit b2c197454d
7 changed files with 154 additions and 142 deletions
-2
View File
@@ -39,7 +39,6 @@ export const AuthCallback = (props: { onSuccess: () => void }) => {
type="email"
autocomplete="email"
placeholder="Email"
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required
@@ -52,7 +51,6 @@ export const AuthCallback = (props: { onSuccess: () => void }) => {
type="password"
autocomplete="current-password"
placeholder="Password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required
+34 -20
View File
@@ -10,6 +10,7 @@ import { TagPicker } from "@/components/TagPicker";
import { lazy, Suspense } from "solid-js";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { toast } from "solid-sonner";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
@@ -19,6 +20,7 @@ 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");
const [tags, setTags] = createSignal<string[]>([]);
@@ -104,9 +106,12 @@ export const QuickEntry: Component = () => {
onCleanup(() => window.removeEventListener("keydown", handleKeyDown));
// When urgency changes via dropdown, update the date
const onUrgencySimpleChange = (val: string) => {
setUrgency(val);
const level = parseInt(val);
const onUrgencySimpleChange = (val: any) => {
const value = typeof val === 'object' ? val.value : val;
if (!value) return;
setUrgency(value);
const level = parseInt(value);
if (!isNaN(level)) {
const newDateIso = calculateDateFromUrgency(level);
// Format for datetime-local input: YYYY-MM-DDTHH:mm
@@ -118,7 +123,7 @@ export const QuickEntry: Component = () => {
// When date changes via input, update urgency level
const onDateInputChange = (e: Event) => {
const val = (e.target as HTMLInputElement).value;
const val = (e.currentTarget as HTMLInputElement).value;
setDateString(val);
if (val) {
const level = calculateUrgencyFromDate(new Date(val).toISOString());
@@ -278,21 +283,28 @@ export const QuickEntry: Component = () => {
<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
<Select<any>
value={priority()}
onChange={(val) => val && setPriority(val)}
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
onChange={(val) => {
const v = typeof val === 'object' ? val.value : val;
if (v) setPriority(v);
}}
options={PRIORITY_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Priority"
itemComponent={(props) => (
<SelectItem item={props.item}>
{props.item.rawValue}
{props.item.rawValue.label}
</SelectItem>
)}
>
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
<div class="flex items-center gap-2">
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3 overflow-hidden">
<div class="flex items-center gap-2 truncate">
<ArrowUpCircle size={14} class="text-primary shrink-0" />
<span class="text-sm font-medium">{priority()}</span>
<span class="text-sm font-medium truncate">
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || priority()}
</span>
</div>
</SelectTrigger>
<SelectContent />
@@ -301,23 +313,25 @@ export const QuickEntry: Component = () => {
<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
<Select<any>
value={urgency()}
onChange={(val) => {
if (val) onUrgencySimpleChange(val);
}}
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
onChange={(val) => onUrgencySimpleChange(val)}
options={URGENCY_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Urgency"
itemComponent={(props) => (
<SelectItem item={props.item}>
{props.item.rawValue}
{props.item.rawValue.label}
</SelectItem>
)}
>
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
<div class="flex items-center gap-2">
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3 overflow-hidden">
<div class="flex items-center gap-2 truncate">
<Clock size={14} class="text-primary shrink-0" />
<span class="text-sm font-medium">{urgency()}</span>
<span class="text-sm font-medium truncate">
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || urgency()}
</span>
</div>
</SelectTrigger>
<SelectContent />
+1 -1
View File
@@ -73,7 +73,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Value (Influence)</label>
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Value (Sorting Influence)</label>
<select
value={valueScore().toString()}
onInput={(e) => setValueScore(parseInt(e.currentTarget.value))}
+29 -18
View File
@@ -7,6 +7,7 @@ import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
import { TagPicker } from "./TagPicker";
import { Badge } from "./ui/badge";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
import { store } from "@/store";
import { cn } from "@/lib/utils";
import { toast } from "solid-sonner";
@@ -49,15 +50,17 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
updateTask(props.task.id, { title: val });
};
const updatePriority = (val: string) => {
const p = parseInt(val);
const updatePriority = (val: any) => {
const value = typeof val === 'object' ? val.value : val;
const p = parseInt(value);
if (!isNaN(p)) {
updateTask(props.task.id, { priority: p });
}
};
const updateUrgency = (val: string) => {
const u = parseInt(val);
const updateUrgency = (val: any) => {
const value = typeof val === 'object' ? val.value : val;
const u = parseInt(value);
if (!isNaN(u)) {
const newDate = calculateDateFromUrgency(u);
updateTask(props.task.id, { dueDate: newDate });
@@ -119,17 +122,21 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
{/* Priority */}
<div class="flex items-center gap-1 group shrink-0">
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Priority</span>
<Select
<Select<any>
value={props.task.priority.toString()}
onChange={(val: string | null) => val && updatePriority(val)}
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
options={PRIORITY_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Priority"
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
>
<SelectTrigger class="h-8 min-w-[40px] bg-transparent border-transparent hover:bg-muted/50 px-1 sm:px-2 shadow-none focus:ring-0 shrink-0">
<div class="flex items-center gap-1 sm:gap-1.5 text-foreground font-medium">
<ArrowUpCircle size={14} class="text-primary opacity-70 group-hover:opacity-100 transition-opacity" />
<span class="text-xs sm:text-sm">{props.task.priority}</span>
<SelectTrigger class="h-8 min-w-[40px] max-w-[180px] bg-transparent border-transparent hover:bg-muted/50 px-1 sm:px-2 shadow-none focus:ring-0 shrink-0 overflow-hidden">
<div class="flex items-center gap-1 sm:gap-1.5 text-foreground font-medium truncate">
<ArrowUpCircle size={14} class="text-primary opacity-70 group-hover:opacity-100 transition-opacity shrink-0" />
<span class="text-xs sm:text-sm truncate">
{PRIORITY_OPTIONS.find(o => o.value === props.task.priority.toString())?.label || props.task.priority}
</span>
</div>
</SelectTrigger>
<SelectContent />
@@ -139,17 +146,21 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
{/* Urgency */}
<div class="flex items-center gap-1 group shrink-0">
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Time</span>
<Select
<Select<any>
value={currentUrgency()}
onChange={(val: string | null) => val && updateUrgency(val)}
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
options={URGENCY_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Urgency"
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
>
<SelectTrigger class="h-8 min-w-[40px] bg-transparent border-transparent hover:bg-muted/50 px-1 sm:px-2 shadow-none focus:ring-0 shrink-0">
<div class="flex items-center gap-1 sm:gap-1.5 text-foreground font-medium">
<Clock size={14} class="text-primary opacity-70 group-hover:opacity-100 transition-opacity" />
<span class="text-xs sm:text-sm">{currentUrgency()}</span>
<SelectTrigger class="h-8 min-w-[40px] max-w-[180px] bg-transparent border-transparent hover:bg-muted/50 px-1 sm:px-2 shadow-none focus:ring-0 shrink-0 overflow-hidden">
<div class="flex items-center gap-1 sm:gap-1.5 text-foreground font-medium truncate">
<Clock size={14} class="text-primary opacity-70 group-hover:opacity-100 transition-opacity shrink-0" />
<span class="text-xs sm:text-sm truncate">
{URGENCY_OPTIONS.find(o => o.value === currentUrgency())?.label || currentUrgency()}
</span>
</div>
</SelectTrigger>
<SelectContent />
+37
View File
@@ -0,0 +1,37 @@
export const PRIORITY_OPTIONS = [
{ value: "10", label: "10 - Necessary" },
{ value: "9", label: "9" },
{ value: "8", label: "8" },
{ value: "7", label: "7" },
{ value: "6", label: "6" },
{ value: "5", label: "5" },
{ value: "4", label: "4" },
{ value: "3", label: "3" },
{ value: "2", label: "2" },
{ value: "1", label: "1 - Trivial" }
];
export const URGENCY_OPTIONS = [
{ value: "10", label: "10 - Today" },
{ value: "9", label: "9 - Tomorrow Morning" },
{ value: "8", label: "8 - EOD Tomorrow" },
{ value: "7", label: "7 - Half-Week" },
{ value: "6", label: "6 - This Week/5 Days" },
{ value: "5", label: "5 - Next Week/10 Days" },
{ value: "4", label: "4 - 3 Weeks" },
{ value: "3", label: "3 - 6 Weeks" },
{ value: "2", label: "2 - 3 Months" },
{ value: "1", label: "1 - 6 Months" }
];
export const URGENCY_HOURS: Record<number, number> = {
10: 12,
9: 24,
8: 36,
7: 72,
6: 120,
5: 240,
4: 504,
3: 1080,
2: 2160,
1: 4320
};
+28 -92
View File
@@ -2,6 +2,7 @@ import { createStore, reconcile, produce } from "solid-js/store";
import { createSignal, createEffect, createRoot } from "solid-js";
import { pb, TASGRID_COLLECTION } from "@/lib/pocketbase";
import { toast } from "solid-sonner";
import { URGENCY_HOURS } from "@/lib/constants";
const getStorageKey = () => {
const userId = pb.authStore.model?.id;
@@ -117,25 +118,31 @@ export const calculateUrgencyScore = (dueDateStr: string): number => {
const due = new Date(dueDateStr).getTime();
const diffHours = (due - now()) / (1000 * 60 * 60);
// Geometric formulation
// Base is derived so that urgency 1 ~ matrixScaleDays
const base = Math.pow(Math.max(1, store.matrixScaleDays), 1 / 9);
if (diffHours <= URGENCY_HOURS[10]) return 10;
if (diffHours >= URGENCY_HOURS[1]) return 1;
// Reverse formula: Level = 10 - (ln(hours/24) / ln(base))
// If hours <= 24, log(<=1) is negative or 0, so Level >= 10.
if (diffHours <= 24) return 10;
// Linear interpolation between breakpoints
const levels = Object.keys(URGENCY_HOURS).map(Number).sort((a, b) => b - a); // 10, 9, 8...
for (let i = 0; i < levels.length - 1; i++) {
const highLevel = levels[i];
const lowLevel = levels[i + 1];
const highHours = URGENCY_HOURS[highLevel];
const lowHours = URGENCY_HOURS[lowLevel];
const level = 10 - (Math.log(diffHours / 24) / Math.log(base));
return Math.max(1, Math.min(10, level));
if (diffHours >= highHours && diffHours <= lowHours) {
// Level decreases as hours increase
const range = lowHours - highHours;
const progress = (diffHours - highHours) / range;
return highLevel - progress * (highLevel - lowLevel);
}
}
return 1;
};
export const calculateDateFromUrgency = (level: number): string => {
// Inverse of above
if (level >= 10) {
return new Date(now() + 24 * 60 * 60 * 1000).toISOString();
}
const base = Math.pow(Math.max(1, store.matrixScaleDays), 1 / 9);
const hours = 24 * Math.pow(base, 10 - level);
const roundedLevel = Math.max(1, Math.min(10, Math.round(level)));
const hours = URGENCY_HOURS[roundedLevel] || 24;
return new Date(now() + hours * 60 * 60 * 1000).toISOString();
};
@@ -400,24 +407,7 @@ export const updateTaskField = (id: string, field: keyof Task, value: any) => {
export const setMatrixScaleDays = async (days: number) => {
setStore("matrixScaleDays", days);
// Sync to users collection
if (store.prefId) {
try {
const prefs = {
pWeight: store.pWeight,
uWeight: store.uWeight,
matrixScaleDays: days
};
await pb.collection('users').update(store.prefId, {
Taskgrid_pref: prefs
});
} catch (err) {
console.error("Failed to update preferences", err);
}
}
await syncPreferences();
};
export const toggleTask = (id: string) => {
@@ -491,27 +481,7 @@ export const upsertMultipleTagDefinitions = async (updates: Record<string, numbe
setStore("tagDefinitions", (defs) => ({ ...(defs || {}), ...updates }));
// Persist
const map = { ...store.tagDefinitions };
const userId = pb.authStore.model?.id;
if (userId) {
try {
const prefs = {
pWeight: store.pWeight,
uWeight: store.uWeight,
matrixScaleDays: store.matrixScaleDays,
tagDefinitions: map
};
await pb.collection('users').update(userId, {
Taskgrid_pref: prefs
}, { requestKey: null });
} catch (e: any) {
// Ignore abort errors
if (e.isAbort) return;
console.error("Failed to persist tag definitions", e);
toast.error("Failed to save tag definitions");
}
}
await syncPreferences();
};
export const removeTagDefinition = async (name: string) => {
@@ -528,25 +498,8 @@ export const removeTagDefinition = async (name: string) => {
}));
// 3. Persist
const map = { ...store.tagDefinitions };
const userId = pb.authStore.model?.id;
if (userId) {
try {
const prefs = {
pWeight: store.pWeight,
uWeight: store.uWeight,
matrixScaleDays: store.matrixScaleDays,
tagDefinitions: map
};
await pb.collection('users').update(userId, {
Taskgrid_pref: prefs
});
toast.success(`Tag "${name}" deleted`);
} catch (e) {
console.error("Failed to delete tag definition", e);
toast.error("Failed to delete tag");
}
}
await syncPreferences();
toast.success(`Tag "${name}" deleted`);
};
export const renameTagDefinition = async (oldName: string, newName: string) => {
@@ -574,31 +527,13 @@ export const renameTagDefinition = async (oldName: string, newName: string) => {
}));
// 4. Persist
const map = { ...store.tagDefinitions };
const userId = pb.authStore.model?.id;
if (userId) {
try {
const prefs = {
pWeight: store.pWeight,
uWeight: store.uWeight,
matrixScaleDays: store.matrixScaleDays,
tagDefinitions: map
};
await pb.collection('users').update(userId, {
Taskgrid_pref: prefs
});
toast.success(`Tag renamed to "${finalName}"`);
} catch (e) {
console.error("Failed to rename tag", e);
toast.error("Failed to rename tag");
}
}
await syncPreferences();
};
// -- Templates --
const syncPreferences = async () => {
export const syncPreferences = async () => {
const userId = pb.authStore.model?.id;
if (!userId) return;
@@ -617,6 +552,7 @@ const syncPreferences = async () => {
} catch (e: any) {
if (e.isAbort) return;
console.error("Failed to sync preferences", e);
// We could toast here, but many of these are background syncs
}
};
+25 -9
View File
@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { toast } from "solid-sonner";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
import { pb } from "@/lib/pocketbase";
const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor })));
@@ -221,6 +222,8 @@ export const SettingsView: Component = () => {
{(template) => {
const [isEditingName, setIsEditingName] = createSignal(false);
const [editName, setEditName] = createSignal(template.name);
const [editTitle, setEditTitle] = createSignal(template.title);
const isExpanded = () => expandedTemplateId() === template.id;
const handleSaveName = () => {
@@ -230,6 +233,12 @@ export const SettingsView: Component = () => {
setIsEditingName(false);
};
const handleSaveTitle = () => {
if (editTitle() !== template.title) {
updateTemplate(template.id, { title: editTitle() });
}
};
return (
<div class={cn(
"flex flex-col rounded-2xl border transition-all overflow-hidden shadow-sm",
@@ -269,7 +278,7 @@ export const SettingsView: Component = () => {
variant="ghost"
size="icon"
class="h-8 w-8 hover:bg-primary/10 hover:text-primary rounded-xl"
onClick={(e) => {
onClick={(e: MouseEvent) => {
e.stopPropagation();
setIsEditingName(true);
}}
@@ -280,7 +289,7 @@ export const SettingsView: Component = () => {
variant="ghost"
size="icon"
class="h-8 w-8 hover:bg-red-500/10 hover:text-red-600 rounded-xl"
onClick={(e) => {
onClick={(e: MouseEvent) => {
e.stopPropagation();
if (confirm(`Delete template "${template.name}"?`)) {
removeTemplate(template.id);
@@ -300,8 +309,9 @@ export const SettingsView: Component = () => {
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Title Placeholder</label>
<input
class="flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
value={template.title}
onInput={(e) => updateTemplate(template.id, { title: e.currentTarget.value })}
value={editTitle()}
onInput={(e) => setEditTitle(e.currentTarget.value)}
onBlur={handleSaveTitle}
placeholder="Default task title..."
/>
</div>
@@ -313,8 +323,8 @@ export const SettingsView: Component = () => {
value={template.priority}
onChange={(e) => updateTemplate(template.id, { priority: parseInt(e.currentTarget.value) })}
>
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}>
{(v) => <option value={v}>{v}</option>}
<For each={PRIORITY_OPTIONS}>
{(opt) => <option value={opt.value}>{opt.label}</option>}
</For>
</select>
</div>
@@ -325,8 +335,8 @@ export const SettingsView: Component = () => {
value={template.urgency}
onChange={(e) => updateTemplate(template.id, { urgency: parseInt(e.currentTarget.value) })}
>
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}>
{(v) => <option value={v}>{v}</option>}
<For each={URGENCY_OPTIONS}>
{(opt) => <option value={opt.value}>{opt.label}</option>}
</For>
</select>
</div>
@@ -360,7 +370,13 @@ export const SettingsView: Component = () => {
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
<TaskEditor
content={template.content}
onUpdate={(html) => updateTemplate(template.id, { content: html })}
onUpdate={(html) => {
// For the rich editor, we can use a slightly debounced store update
// to avoid losing focus if the parent re-renders,
// although Tiptap usually handles its own state well.
// But to be safe, let's just update the store.
updateTemplate(template.id, { content: html });
}}
class="min-h-[100px]"
/>
</Suspense>