task status implementation

This commit is contained in:
2026-02-17 17:00:00 -06:00
parent 3d6e29d2c0
commit b8f5db030c
5 changed files with 196 additions and 39 deletions
+67
View File
@@ -0,0 +1,67 @@
import { type Component } from "solid-js";
import { CheckCircle2 } from "lucide-solid";
import { cn } from "@/lib/utils";
interface StatusCircleProps {
status: number;
size?: number;
className?: string;
}
export const StatusCircle: Component<StatusCircleProps> = (props) => {
const size = () => props.size || 24;
const radius = () => (size() / 2) - 2; // Subtract stroke width/padding
const circumference = () => 2 * Math.PI * radius();
// Status 0: Empty Circle
// Status 10: CheckCircle2 (Green)
// Status 1-9: Progress Ring
const strokeDashoffset = () => {
// Invert to fill clockwise?
// Progress 1-9.
// 1 = 10%, 9 = 90%
const percentage = Math.max(0, Math.min(10, props.status)) / 10;
return circumference() * (1 - percentage);
};
return (
<div class={cn("relative flex items-center justify-center", props.className)} style={{ width: `${size()}px`, height: `${size()}px` }}>
{props.status >= 10 ? (
<CheckCircle2 size={size()} class="text-green-500" />
) : (
// Progress Circle
<div class="relative group">
{/* Background Circle (Ghost) */}
<svg width={size()} height={size()} class="rotate-[-90deg]">
<circle
cx={size() / 2}
cy={size() / 2}
r={radius()}
fill="transparent"
stroke="currentColor"
stroke-width="2"
class="text-muted-foreground/20"
/>
{/* Progress Arc */}
<circle
cx={size() / 2}
cy={size() / 2}
r={radius()}
fill="transparent"
stroke="currentColor"
stroke-width="2"
stroke-dasharray={circumference().toString()}
stroke-dashoffset={strokeDashoffset()}
stroke-linecap="round"
class="text-primary transition-all duration-300"
/>
</svg>
{/* Hover indicator (maybe show check on hover?)
User didn't ask for generic hover, just visual update.
*/}
</div>
)}
</div>
);
};
+36 -14
View File
@@ -1,11 +1,13 @@
import { type Component, createMemo } from "solid-js";
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store, copyTask } from "@/store";
import { type Component, createMemo, For } from "solid-js";
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask } from "@/store";
import { cn } from "@/lib/utils";
import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
import { Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
import { Badge } from "@/components/ui/badge";
import { For } from "solid-js";
import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { STATUS_OPTIONS } from "@/lib/constants";
import { StatusCircle } from "./StatusCircle";
export const TaskCard: Component<{ task: Task }> = (props) => {
const { resolvedTheme } = useTheme();
@@ -32,16 +34,36 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
)}
onClick={() => setActiveTaskId(props.task.id)}
>
<button
onClick={(e) => { e.stopPropagation(); toggleTask(props.task.id); }}
class="mr-3 sm:mr-4 text-muted-foreground hover:text-primary transition-colors shrink-0"
>
{props.task.completed ? (
<CheckCircle2 class="text-green-500" size={22} />
) : (
<Circle size={22} />
)}
</button>
{/* Status Dropdown */}
<div class="mr-2 sm:mr-3 shrink-0" onClick={(e) => e.stopPropagation()}>
<Select<any>
value={(props.task.status ?? 0).toString()}
onChange={(val: any) => {
const value = typeof val === 'object' ? val?.value : val;
if (value) {
const s = parseInt(value);
if (!isNaN(s)) updateTask(props.task.id, { status: s });
}
}}
options={STATUS_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Status"
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
>
<SelectTrigger
class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
onDoubleClick={(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
updateTask(props.task.id, { status: 10 });
}}
>
<StatusCircle status={props.task.status ?? 0} size={26} />
</SelectTrigger>
<SelectContent />
</Select>
</div>
<div class="flex-1 min-w-0 flex flex-col">
<h3 class={cn(
+58 -21
View File
@@ -4,11 +4,12 @@ import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, sha
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings } from "lucide-solid";
import { StatusCircle } from "./StatusCircle";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
import { TagPicker } from "./TagPicker";
import { Badge } from "./ui/badge";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS, STATUS_OPTIONS } from "@/lib/constants";
import { store } from "@/store";
import { cn } from "@/lib/utils";
import { toast } from "solid-sonner";
@@ -72,6 +73,14 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
}
};
const updateStatus = (val: any) => {
const value = typeof val === 'object' ? val.value : val;
const s = parseInt(value);
if (!isNaN(s)) {
updateTask(props.task.id, { status: s });
}
};
const updateUrgency = (val: any) => {
const value = typeof val === 'object' ? val.value : val;
const u = parseInt(value);
@@ -119,29 +128,57 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
>
{/* Header Area */}
<div class="px-6 pt-6 pb-2 shrink-0">
<textarea
id="task-detail-title"
name="task-title"
value={title()}
onInput={(e) => {
updateTitle(e.currentTarget.value);
// Auto-resize
e.currentTarget.style.height = 'auto';
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
}}
ref={(el) => {
setTimeout(() => {
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}, 0);
}}
rows={1}
class="text-xl sm:text-2xl font-bold bg-transparent border-none focus:outline-none focus:ring-0 w-full placeholder:text-muted-foreground/50 resize-none overflow-hidden leading-tight"
placeholder="Untitled Task"
/>
<div class="flex items-start gap-4">
<div class="shrink-0 mt-0.5">
<Select<any>
value={(props.task.status ?? 0).toString()}
onChange={(val: string | null) => val && updateStatus(val)}
options={STATUS_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Status"
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
>
<SelectTrigger
class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 px-0 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
onDoubleClick={(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
updateStatus("10");
}}
>
<StatusCircle status={props.task.status ?? 0} size={28} />
</SelectTrigger>
<SelectContent />
</Select>
</div>
<textarea
id="task-detail-title"
name="task-title"
value={title()}
onInput={(e) => {
updateTitle(e.currentTarget.value);
// Auto-resize
e.currentTarget.style.height = 'auto';
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
}}
ref={(el) => {
setTimeout(() => {
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}, 0);
}}
rows={1}
class="text-xl sm:text-2xl font-bold bg-transparent border-none focus:outline-none focus:ring-0 w-full placeholder:text-muted-foreground/50 resize-none overflow-hidden leading-tight flex-1 min-w-0"
placeholder="Untitled Task"
/>
</div>
{/* Properties Bar */}
<div class="flex flex-wrap items-center gap-2 sm:gap-4 mt-2 text-sm">
{/* 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>