filter and layout improvements. added created at and modified at date/time stamps
This commit is contained in:
@@ -65,10 +65,11 @@ export const Layout: Component<LayoutProps> = (props) => {
|
|||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<main class="flex-1 min-w-0 overflow-y-auto overflow-x-hidden pb-32 sm:pb-6 relative scroll-smooth">
|
<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">
|
<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 */}
|
<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">
|
<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 />
|
<FilterBar />
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type Component, createMemo } from "solid-js";
|
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 { 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 { Badge } from "@/components/ui/badge";
|
||||||
import { For } from "solid-js";
|
import { For } from "solid-js";
|
||||||
|
|
||||||
@@ -89,9 +89,16 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-2 sm:ml-4 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
<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">
|
<button
|
||||||
<div class="w-1 h-1 bg-muted-foreground rounded-full" />
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+141
-80
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, createEffect, createSignal, For } from "solid-js";
|
import { type Component, createEffect, createSignal, For } from "solid-js";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
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 { 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 { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { TagPicker } from "./TagPicker";
|
import { TagPicker } from "./TagPicker";
|
||||||
@@ -46,7 +46,6 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
|
|
||||||
const updateTitle = (val: string) => {
|
const updateTitle = (val: string) => {
|
||||||
setTitle(val);
|
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 });
|
updateTask(props.task.id, { title: val });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={props.isOpen} onOpenChange={(open) => !open && props.onClose()}>
|
<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 */}
|
{/* Header Area */}
|
||||||
<div class="px-6 pt-6 pb-2 shrink-0">
|
<div class="px-6 pt-6 pb-2 shrink-0">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -107,7 +106,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Properties Bar */}
|
{/* 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 */}
|
{/* Priority */}
|
||||||
<div class="flex items-center gap-1 group shrink-0">
|
<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>
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent />
|
<SelectContent />
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Commands Shortcut */}
|
{/* Commands Shortcut */}
|
||||||
@@ -166,93 +164,46 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
<span class="hidden sm:inline">Commands</span>
|
<span class="hidden sm:inline">Commands</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Date - Editable */}
|
{/* Metadata Row (Dates/Timestamps) */}
|
||||||
<div class="flex items-center gap-1 text-muted-foreground group relative shrink-0 min-w-0 flex-1 sm:flex-initial">
|
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 mt-4 px-1">
|
||||||
<Calendar size={14} class="group-hover:text-primary transition-colors shrink-0" />
|
<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
|
<input
|
||||||
id="task-detail-date"
|
id="task-detail-date"
|
||||||
name="due-date"
|
name="due-date"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={dateString()}
|
value={dateString()}
|
||||||
onInput={onDateInputChange}
|
onInput={onDateInputChange}
|
||||||
onClick={(e) => (e.target as HTMLInputElement).showPicker?.()}
|
class="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</MetadataItem>
|
||||||
|
|
||||||
{/* Copy button */}
|
<MetadataItem
|
||||||
<div class="flex items-center gap-1 shrink-0 ml-auto">
|
label="Created"
|
||||||
<Button
|
date={new Date(props.task.created).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||||
variant="ghost"
|
time={new Date(props.task.created).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Delete Button */}
|
<MetadataItem
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
label="Updated"
|
||||||
<Button
|
date={new Date(props.task.updated).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||||
variant="ghost"
|
time={new Date(props.task.updated).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tags Row */}
|
<div class="h-px w-full bg-border mt-5" />
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Editor Content */}
|
{/* Content Area */}
|
||||||
<div class="flex-1 overflow-y-auto px-6 py-4 pb-20">
|
<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" />}>
|
<Suspense fallback={<div class="h-32 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||||
<TaskEditor
|
<TaskEditor
|
||||||
content={props.task.content}
|
content={props.task.content}
|
||||||
@@ -260,8 +211,118 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
onEditorReady={setEditorInstance}
|
onEditorReady={setEditorInstance}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</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>
|
</div>
|
||||||
</SheetContent>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ const SheetOverlay: Component<ComponentProps<typeof SheetPrimitive.Overlay>> = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SheetContent: Component<ComponentProps<typeof SheetPrimitive.Content>> = (props) => {
|
const SheetContent: Component<ComponentProps<typeof SheetPrimitive.Content> & { hideCloseButton?: boolean }> = (props) => {
|
||||||
const [local, others] = splitProps(props, ["class", "children"]);
|
const [local, others] = splitProps(props, ["class", "children", "hideCloseButton"]);
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Portal>
|
<SheetPrimitive.Portal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
@@ -33,10 +33,12 @@ const SheetContent: Component<ComponentProps<typeof SheetPrimitive.Content>> = (
|
|||||||
{...others}
|
{...others}
|
||||||
>
|
>
|
||||||
{/* Close Button */}
|
{/* 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">
|
{!local.hideCloseButton && (
|
||||||
<X class="h-4 w-4" />
|
<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">
|
||||||
<span class="sr-only">Close</span>
|
<X class="h-4 w-4" />
|
||||||
</SheetPrimitive.CloseButton>
|
<span class="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.CloseButton>
|
||||||
|
)}
|
||||||
{local.children}
|
{local.children}
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPrimitive.Portal>
|
</SheetPrimitive.Portal>
|
||||||
|
|||||||
+15
-4
@@ -23,6 +23,8 @@ export interface Task {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
content?: string; // HTML content from Tiptap
|
content?: string; // HTML content from Tiptap
|
||||||
deletedAt?: number; // Timestamp of soft delete
|
deletedAt?: number; // Timestamp of soft delete
|
||||||
|
created: string; // ISO string from PB
|
||||||
|
updated: string; // ISO string from PB
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
@@ -212,7 +214,9 @@ export const initStore = async () => {
|
|||||||
completed: r.completed,
|
completed: r.completed,
|
||||||
tags: r.tags || [],
|
tags: r.tags || [],
|
||||||
content: r.content,
|
content: r.content,
|
||||||
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined
|
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined,
|
||||||
|
created: r.created,
|
||||||
|
updated: r.updated
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Use reconcile for efficient store update (keeps observers happy)
|
// Use reconcile for efficient store update (keeps observers happy)
|
||||||
@@ -254,7 +258,9 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
|
|||||||
priority,
|
priority,
|
||||||
completed: false,
|
completed: false,
|
||||||
tags,
|
tags,
|
||||||
content
|
content,
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
updated: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optimistic UI update
|
// Optimistic UI update
|
||||||
@@ -272,8 +278,13 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
|
|||||||
content
|
content
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace temp task with real record
|
// Replace temp task with real record (merging server fields like id, created, updated)
|
||||||
setStore("tasks", (t) => t.map(task => task.id === tempId ? { ...task, id: record.id } : task));
|
setStore("tasks", (t) => t.map(task => task.id === tempId ? {
|
||||||
|
...task,
|
||||||
|
id: record.id,
|
||||||
|
created: record.created,
|
||||||
|
updated: record.updated
|
||||||
|
} : task));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("create failed", err);
|
console.error("create failed", err);
|
||||||
toast.error("Failed to save task.");
|
toast.error("Failed to save task.");
|
||||||
|
|||||||
+46
-38
@@ -12,37 +12,48 @@ import { createSignal } from "solid-js";
|
|||||||
|
|
||||||
export const SettingsView: Component = () => {
|
export const SettingsView: Component = () => {
|
||||||
return (
|
return (
|
||||||
<div class="py-10 max-w-2xl mx-auto space-y-10 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
<div class="w-full max-w-2xl mx-auto py-4 sm:py-10 px-4 sm:px-0 space-y-8 sm:space-y-12 animate-in fade-in slide-in-from-bottom-2 duration-500 min-w-0 overflow-hidden text-balance">
|
||||||
<header class="space-y-1 flex items-center justify-between">
|
<header class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div class="min-w-0 flex-1">
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Settings</h1>
|
<h1 class="text-2xl sm:text-4xl font-black tracking-tighter">Settings</h1>
|
||||||
<p class="text-muted-foreground">{pb.authStore.model?.email}</p>
|
<p class="text-[10px] sm:text-xs text-muted-foreground truncate opacity-70 font-mono tracking-tight" title={pb.authStore.model?.email}>
|
||||||
|
{pb.authStore.model?.email}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" onClick={() => { pb.authStore.clear(); location.reload(); }}>Sign Out</Button>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="w-full sm:w-auto font-black uppercase tracking-widest text-[10px] h-10 sm:h-8 rounded-xl shadow-sm border-border/60"
|
||||||
|
onClick={() => { pb.authStore.clear(); location.reload(); }}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
<section class="p-5 rounded-2xl border border-border bg-card shadow-sm">
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<h3 class="text-base font-semibold">Appearance</h3>
|
<h3 class="text-base font-semibold">Appearance</h3>
|
||||||
<p class="text-sm text-muted-foreground">Select your preferred color scheme.</p>
|
<p class="text-sm text-muted-foreground">Select your preferred color scheme.</p>
|
||||||
</div>
|
</div>
|
||||||
<ThemeToggle />
|
<div class="flex justify-start sm:justify-end">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Matrix Configuration */}
|
{/* Matrix Configuration */}
|
||||||
<section class="p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4">
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5 min-w-0">
|
||||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
<ArrowLeftRight size={16} />
|
<ArrowLeftRight size={16} />
|
||||||
Matrix Horizon
|
Matrix Horizon
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-muted-foreground">Adjust the time scale for your strategy grid.</p>
|
<p class="text-sm text-muted-foreground truncate sm:whitespace-normal">Adjust the time scale for your strategy grid.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-[120px]">
|
<div class="w-full sm:w-[120px] shrink-0">
|
||||||
<Select
|
<Select
|
||||||
value={store.matrixScaleDays.toString()}
|
value={store.matrixScaleDays.toString()}
|
||||||
onChange={(val) => val && setMatrixScaleDays(parseInt(val))}
|
onChange={(val) => val && setMatrixScaleDays(parseInt(val))}
|
||||||
@@ -50,7 +61,7 @@ export const SettingsView: Component = () => {
|
|||||||
placeholder="Select days"
|
placeholder="Select days"
|
||||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue} days</SelectItem>}
|
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue} days</SelectItem>}
|
||||||
>
|
>
|
||||||
<SelectTrigger class="h-9">
|
<SelectTrigger class="h-9 w-full">
|
||||||
<span class="text-sm font-medium">{store.matrixScaleDays} days</span>
|
<span class="text-sm font-medium">{store.matrixScaleDays} days</span>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent />
|
<SelectContent />
|
||||||
@@ -59,7 +70,7 @@ export const SettingsView: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4">
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
<Hash size={16} />
|
<Hash size={16} />
|
||||||
@@ -79,20 +90,20 @@ export const SettingsView: Component = () => {
|
|||||||
const [tempName, setTempName] = createSignal(tagName);
|
const [tempName, setTempName] = createSignal(tagName);
|
||||||
|
|
||||||
const handleRename = () => {
|
const handleRename = () => {
|
||||||
if (tempName() !== tagName) {
|
if (tempName() && tempName() !== tagName) {
|
||||||
renameTagDefinition(tagName, tempName());
|
renameTagDefinition(tagName, tempName());
|
||||||
}
|
}
|
||||||
setIsEditingName(false);
|
setIsEditingName(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/30 border border-border/50">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded-xl bg-muted/30 border border-border/50 gap-3 sm:gap-4">
|
||||||
<div class="flex items-center gap-3 flex-1">
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<div class={cn("w-2 h-2 rounded-full shrink-0", val > 5 ? "bg-green-500" : val < 5 ? "bg-red-500" : "bg-gray-400")} />
|
<div class={cn("w-2 h-2 rounded-full shrink-0", val > 5 ? "bg-green-500" : val < 5 ? "bg-red-500" : "bg-gray-400")} />
|
||||||
|
|
||||||
{isEditingName() ? (
|
{isEditingName() ? (
|
||||||
<input
|
<input
|
||||||
class="flex h-7 w-48 rounded-md border border-input bg-background px-2 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
class="flex h-7 min-w-0 w-full rounded-md border border-input bg-background px-2 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
value={tempName()}
|
value={tempName()}
|
||||||
onInput={(e) => setTempName(e.currentTarget.value)}
|
onInput={(e) => setTempName(e.currentTarget.value)}
|
||||||
onBlur={handleRename}
|
onBlur={handleRename}
|
||||||
@@ -101,7 +112,7 @@ export const SettingsView: Component = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
class="font-medium cursor-pointer hover:underline decoration-dashed underline-offset-4"
|
class="font-medium cursor-pointer hover:underline decoration-dashed underline-offset-4 truncate"
|
||||||
onClick={() => setIsEditingName(true)}
|
onClick={() => setIsEditingName(true)}
|
||||||
title="Click to rename"
|
title="Click to rename"
|
||||||
>
|
>
|
||||||
@@ -110,7 +121,7 @@ export const SettingsView: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center grow sm:grow-0 justify-between sm:justify-end gap-3 sm:gap-4 shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 border-border/20">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||||
<select
|
<select
|
||||||
@@ -144,13 +155,13 @@ export const SettingsView: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4">
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 min-w-0 overflow-hidden">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5 min-w-0">
|
||||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
Trash
|
Trash
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-muted-foreground">Items are permanently deleted after 7 days.</p>
|
<p class="text-sm text-muted-foreground truncate">Items are permanently deleted after 7 days.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -162,35 +173,32 @@ export const SettingsView: Component = () => {
|
|||||||
{(task) => {
|
{(task) => {
|
||||||
const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24));
|
const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24));
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/30 border border-border/50 group hover:bg-muted/50 transition-colors">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-3.5 rounded-2xl bg-muted/20 border border-border/40 group hover:bg-muted/40 transition-all gap-3 min-w-0">
|
||||||
<div class="min-w-0 flex-1 mr-4">
|
<div class="min-w-0 flex-1 px-1">
|
||||||
<p class="font-medium truncate">{task.title || "Untitled"}</p>
|
<p class="font-semibold truncate text-sm sm:text-base">{task.title || "Untitled"}</p>
|
||||||
<p class="text-[10px] text-muted-foreground">Expires in {Math.max(0, daysLeft)} days</p>
|
<p class="text-[11px] text-muted-foreground opacity-70">Expires in {Math.max(0, daysLeft)} days</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center justify-end gap-2 shrink-0 border-t sm:border-t-0 pt-3 sm:pt-0 border-border/10">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-8 px-2 text-xs hover:bg-green-500/10 hover:text-green-600"
|
class="h-9 px-4 text-[10px] font-bold uppercase tracking-widest hover:bg-green-500/10 hover:text-green-600 bg-background/50 border border-border/50 transition-all rounded-xl shadow-sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
restoreTask(task.id);
|
restoreTask(task.id);
|
||||||
toast.success("Task restored");
|
toast.success("Task restored");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Undo2 size={14} class="mr-1" />
|
<Undo2 size={14} class="mr-2" />
|
||||||
Recover
|
Recover
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8 hover:bg-red-500/10 hover:text-red-600"
|
class="h-9 w-9 hover:bg-red-500/10 hover:text-red-600 rounded-xl border border-border/50 bg-background/30"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// For permanent deletion, still using confirm for safety but ensuring toast follows
|
|
||||||
if (confirm("Permanently delete this task? This cannot be undone.")) {
|
if (confirm("Permanently delete this task? This cannot be undone.")) {
|
||||||
deleteTaskPermanently(task.id);
|
deleteTaskPermanently(task.id);
|
||||||
toast.error("Task permanently deleted", {
|
toast.error("Task permanently deleted");
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user