filter and layout improvements. added created at and modified at date/time stamps

This commit is contained in:
2026-01-31 16:30:57 -06:00
parent 2c83b91e66
commit 0554b6bb6b
6 changed files with 227 additions and 137 deletions
+5 -4
View File
@@ -65,10 +65,11 @@ export const Layout: Component<LayoutProps> = (props) => {
{/* Main Content Area */}
<main class="flex-1 min-w-0 overflow-y-auto overflow-x-hidden pb-32 sm:pb-6 relative scroll-smooth">
<div class="w-full max-w-screen-2xl mx-auto p-3 sm:p-6 lg:p-10 space-y-6 sm:space-y-8">
{/* Global Top Header for Filter/Search */}
<div class="sticky top-0 z-40 flex items-center justify-end py-2 -mx-3 px-3 md:relative md:top-auto md:z-auto md:py-0 md:mx-0 md:px-0 bg-background/80 backdrop-blur-md md:bg-transparent md:backdrop-blur-none">
<FilterBar />
</div>
<Show when={props.currentView.toLowerCase() !== "settings"}>
<div class="sticky top-0 z-40 flex items-center justify-end py-2 -mx-3 px-3 md:relative md:top-auto md:z-auto md:py-0 md:mx-0 md:px-0 bg-background/80 backdrop-blur-md md:bg-transparent md:backdrop-blur-none transition-all duration-300">
<FilterBar />
</div>
</Show>
{props.children}
</div>
+12 -5
View File
@@ -1,7 +1,7 @@
import { type Component, createMemo } from "solid-js";
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store } from "@/store";
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store, copyTask } from "@/store";
import { cn } from "@/lib/utils";
import { CheckCircle2, Circle, Clock, ArrowUpCircle } from "lucide-solid";
import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy } from "lucide-solid";
import { Badge } from "@/components/ui/badge";
import { For } from "solid-js";
@@ -89,9 +89,16 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
</div>
</div>
<div class="ml-2 sm:ml-4 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button class="p-1.5 hover:bg-muted rounded-full transition-colors">
<div class="w-1 h-1 bg-muted-foreground rounded-full" />
<div class="ml-2 sm:ml-4 opacity-100 sm:opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button
class="p-1.5 hover:bg-muted rounded-full transition-colors text-muted-foreground hover:text-primary"
onClick={(e) => {
e.stopPropagation();
copyTask(props.task.id);
}}
title="Duplicate Task"
>
<Copy size={16} />
</button>
</div>
</div>
+141 -80
View File
@@ -1,8 +1,8 @@
import { type Component, createEffect, createSignal, For } from "solid-js";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { type Task, removeTask, restoreTask, updateTask, copyTask } from "@/store";
import { type Task, removeTask, restoreTask, updateTask } from "@/store";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, Copy } from "lucide-solid";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X } from "lucide-solid";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
import { TagPicker } from "./TagPicker";
@@ -46,7 +46,6 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
const updateTitle = (val: string) => {
setTitle(val);
// Debouncing could be added here, but direct update is fine for now if PB handles it well
updateTask(props.task.id, { title: val });
};
@@ -82,7 +81,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
return (
<Sheet open={props.isOpen} onOpenChange={(open) => !open && props.onClose()}>
<SheetContent class="w-full sm:max-w-2xl px-0 sm:px-0 flex flex-col h-full bg-background border-l border-border shadow-2xl">
<SheetContent hideCloseButton class="w-full sm:max-w-2xl px-0 sm:px-0 flex flex-col h-full bg-background border-l border-border shadow-2xl">
{/* Header Area */}
<div class="px-6 pt-6 pb-2 shrink-0">
<textarea
@@ -107,7 +106,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
/>
{/* Properties Bar */}
<div class="flex flex-wrap items-center gap-1.5 sm:gap-4 mt-2 text-sm">
<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>
@@ -146,7 +145,6 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
</SelectTrigger>
<SelectContent />
</Select>
</div>
{/* Commands Shortcut */}
@@ -166,93 +164,46 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
<span class="hidden sm:inline">Commands</span>
</Button>
</div>
</div>
{/* Date - Editable */}
<div class="flex items-center gap-1 text-muted-foreground group relative shrink-0 min-w-0 flex-1 sm:flex-initial">
<Calendar size={14} class="group-hover:text-primary transition-colors shrink-0" />
{/* Metadata Row (Dates/Timestamps) */}
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 mt-4 px-1">
<MetadataItem
label="Due"
date={new Date(props.task.dueDate).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
time={new Date(props.task.dueDate).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}
icon={<Calendar size={12} />}
editable={true}
onClick={() => (document.getElementById('task-detail-date') as HTMLInputElement)?.showPicker?.()}
>
<input
id="task-detail-date"
name="due-date"
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="bg-transparent border-none p-0 text-[11px] sm:text-xs font-medium focus:ring-0 focus:outline-none cursor-pointer hover:text-foreground appearance-none min-w-[125px] w-full"
title="Change Due Date"
class="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
/>
</div>
</MetadataItem>
{/* Copy button */}
<div class="flex items-center gap-1 shrink-0 ml-auto">
<Button
variant="ghost"
size="sm"
class="h-8 w-8 px-0 hover:bg-muted/50 text-muted-foreground transition-colors"
onClick={() => copyTask(props.task.id)}
>
<Copy size={16} />
</Button>
</div>
<MetadataItem
label="Created"
date={new Date(props.task.created).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
time={new Date(props.task.created).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}
/>
{/* Delete Button */}
<div class="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="sm"
class="h-8 w-8 px-0 hover:bg-red-500/10 hover:text-red-500 text-muted-foreground transition-colors"
onClick={() => {
const taskId = props.task.id;
removeTask(taskId);
props.onClose();
toast.info("Task moved to trash. Find inside Settings.", {
action: {
label: "Undo",
onClick: () => restoreTask(taskId)
}
});
}}
>
<Trash2 size={16} />
</Button>
</div>
<MetadataItem
label="Updated"
date={new Date(props.task.updated).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
time={new Date(props.task.updated).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}
/>
</div>
{/* Tags Row */}
<div class="flex flex-wrap items-center gap-2 mt-4 pb-1">
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 shrink-0">Tags</span>
<div class="flex flex-wrap items-center gap-1.5 min-w-0">
<TagPicker
selectedTags={props.task.tags || []}
onTagsChange={updateTags}
/>
<div class="flex flex-wrap items-center gap-1.5">
<For each={props.task.tags || []}>
{(tag) => (
<Badge
variant="secondary"
class="h-7 px-2 text-xs gap-1 bg-muted/40 border-border/50 hover:bg-destructive/10 hover:border-destructive/30 cursor-pointer group/tag transition-all whitespace-nowrap shrink-0"
onClick={() => updateTags((props.task.tags || []).filter(t => t !== tag))}
>
<div class={cn("w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
{tag}
</Badge>
)}
</For>
</div>
</div>
</div>
<div class="h-px w-full bg-border mt-4" />
<div class="h-px w-full bg-border mt-5" />
</div>
{/* Editor Content */}
<div class="flex-1 overflow-y-auto px-6 py-4 pb-20">
{/* Content Area */}
<div class="flex-1 overflow-y-auto px-6 py-4 pb-20 flex flex-col gap-6">
<Suspense fallback={<div class="h-32 animate-pulse bg-muted/20 rounded-lg" />}>
<TaskEditor
content={props.task.content}
@@ -260,8 +211,118 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
onEditorReady={setEditorInstance}
/>
</Suspense>
{/* Tags at the bottom */}
<div class="mt-auto pt-6 border-t border-border/50">
<div class="flex flex-wrap items-center gap-3">
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/60 shrink-0">Tags</span>
<div class="flex flex-wrap items-center gap-1.5 min-w-0 flex-1">
<TagPicker
selectedTags={props.task.tags || []}
onTagsChange={updateTags}
/>
<div class="flex flex-wrap items-center gap-1.5">
<For each={props.task.tags || []}>
{(tag) => (
<Badge
variant="secondary"
class="h-7 px-2 text-xs gap-1.5 bg-muted/30 border-border/50 hover:bg-destructive/10 hover:border-destructive/30 cursor-pointer group/tag transition-all whitespace-nowrap shrink-0"
onClick={() => updateTags((props.task.tags || []).filter(t => t !== tag))}
>
<div class={cn("w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
{tag}
</Badge>
)}
</For>
</div>
</div>
</div>
</div>
</div>
{/* Sticky Footer Actions */}
<div class="px-6 py-4 border-t border-border/50 flex items-center justify-between shrink-0 bg-background/80 backdrop-blur-sm">
{/* Delete button (persistent) */}
<Button
variant="ghost"
size="sm"
class="h-9 px-3 gap-2 hover:bg-destructive/10 hover:text-destructive text-muted-foreground transition-all rounded-xl border border-border/40"
onClick={() => {
const taskId = props.task.id;
removeTask(taskId);
props.onClose();
toast.info("Task moved to trash", {
action: {
label: "Undo",
onClick: () => restoreTask(taskId)
}
});
}}
>
<Trash2 size={16} />
<span class="text-xs font-bold uppercase tracking-wider">Delete</span>
</Button>
{/* Close button (Mobile only) */}
<Button
variant="ghost"
size="sm"
class="md:hidden h-9 px-3 gap-2 hover:bg-muted text-muted-foreground transition-all rounded-xl border border-border/40"
onClick={() => props.onClose()}
>
<X size={16} />
<span class="text-xs font-bold uppercase tracking-wider">Close</span>
</Button>
</div>
</SheetContent>
</Sheet >
</Sheet>
);
};
// Reusable Metadata Item with slide transition
const MetadataItem: Component<{
label: string,
date: string,
time: string,
icon?: any,
editable?: boolean,
urgency?: string,
children?: any,
onClick?: () => void
}> = (props) => {
const [isToggled, setIsToggled] = createSignal(false);
return (
<div
class="group relative flex flex-col gap-0.5 cursor-pointer select-none"
onMouseEnter={() => setIsToggled(true)}
onMouseLeave={() => setIsToggled(false)}
onClick={() => {
setIsToggled(!isToggled());
props.onClick?.();
}}
>
<span class="text-[9px] font-bold uppercase tracking-wider text-muted-foreground/60 transition-colors group-hover:text-primary/70">
{props.label}
</span>
<div class="h-4 overflow-hidden relative min-w-[100px]">
<div class={cn(
"flex flex-col transition-transform duration-500 cubic-bezier(0.4, 0, 0.2, 1)",
isToggled() ? "-translate-y-4" : "translate-y-0"
)}>
{/* Layer 1: Date */}
<div class="h-4 flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 whitespace-nowrap">
{props.icon}
{props.date}
</div>
{/* Layer 2: Time */}
<div class="h-4 flex items-center gap-2 text-[11px] font-bold text-foreground whitespace-nowrap">
<Clock size={11} class="text-primary/60 shrink-0" />
<span>{props.time}</span>
</div>
</div>
</div>
{props.children}
</div>
);
};
+8 -6
View File
@@ -20,8 +20,8 @@ const SheetOverlay: Component<ComponentProps<typeof SheetPrimitive.Overlay>> = (
);
};
const SheetContent: Component<ComponentProps<typeof SheetPrimitive.Content>> = (props) => {
const [local, others] = splitProps(props, ["class", "children"]);
const SheetContent: Component<ComponentProps<typeof SheetPrimitive.Content> & { hideCloseButton?: boolean }> = (props) => {
const [local, others] = splitProps(props, ["class", "children", "hideCloseButton"]);
return (
<SheetPrimitive.Portal>
<SheetOverlay />
@@ -33,10 +33,12 @@ const SheetContent: Component<ComponentProps<typeof SheetPrimitive.Content>> = (
{...others}
>
{/* Close Button */}
<SheetPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</SheetPrimitive.CloseButton>
{!local.hideCloseButton && (
<SheetPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</SheetPrimitive.CloseButton>
)}
{local.children}
</SheetPrimitive.Content>
</SheetPrimitive.Portal>