added templates and fixed UX for virtual keyboard opening when opening a task
This commit is contained in:
+362
-132
@@ -1,16 +1,22 @@
|
||||
import { type Component, For } from "solid-js";
|
||||
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
|
||||
import { ThemeToggle } from "../components/ThemeToggle";
|
||||
import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays } from "@/store";
|
||||
import { Trash2, Undo2, ArrowLeftRight, Hash } from "lucide-solid";
|
||||
import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition } from "@/store";
|
||||
import { Trash2, Undo2, ArrowLeftRight, Hash, Copy, Plus, ChevronDown, ChevronRight, Type } from "lucide-solid";
|
||||
import { TagPicker } from "@/components/TagPicker";
|
||||
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 { pb } from "@/lib/pocketbase";
|
||||
import { upsertTagDefinition, removeTagDefinition, renameTagDefinition } from "@/store";
|
||||
import { createSignal } from "solid-js";
|
||||
|
||||
const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
|
||||
export const SettingsView: Component = () => {
|
||||
const [isTagsOpen, setIsTagsOpen] = createSignal(false);
|
||||
const [isTemplatesOpen, setIsTemplatesOpen] = createSignal(false);
|
||||
const [isTrashOpen, setIsTrashOpen] = createSignal(false);
|
||||
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
||||
|
||||
return (
|
||||
<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="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
@@ -31,6 +37,7 @@ export const SettingsView: Component = () => {
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6">
|
||||
{/* Appearance */}
|
||||
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="space-y-0.5">
|
||||
@@ -70,149 +77,372 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Hash size={16} />
|
||||
Global Tags
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">Manage your global tag definitions and values.</p>
|
||||
{/* Global Tag Manager */}
|
||||
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 overflow-hidden">
|
||||
<div
|
||||
class="flex items-center justify-between cursor-pointer group"
|
||||
onClick={() => setIsTagsOpen(!isTagsOpen())}
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Hash size={16} />
|
||||
Global Tags
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">Manage your global tag definitions and values.</p>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isTagsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<For each={Object.entries(store.tagDefinitions || {}).sort((a, b) => a[0].localeCompare(b[0]))} fallback={
|
||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
No specific tag definitions found.
|
||||
</div>
|
||||
}>
|
||||
{([tagName, val]) => {
|
||||
const [isEditingName, setIsEditingName] = createSignal(false);
|
||||
const [tempName, setTempName] = createSignal(tagName);
|
||||
<Show when={isTagsOpen()}>
|
||||
<div class="space-y-2 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<For each={Object.entries(store.tagDefinitions || {}).sort((a, b) => a[0].localeCompare(b[0]))} fallback={
|
||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
No specific tag definitions found.
|
||||
</div>
|
||||
}>
|
||||
{([tagName, val]) => {
|
||||
const [isEditingName, setIsEditingName] = createSignal(false);
|
||||
const [tempName, setTempName] = createSignal(tagName);
|
||||
|
||||
const handleRename = () => {
|
||||
if (tempName() && tempName() !== tagName) {
|
||||
renameTagDefinition(tagName, tempName());
|
||||
}
|
||||
setIsEditingName(false);
|
||||
};
|
||||
const handleRename = () => {
|
||||
if (tempName() && tempName() !== tagName) {
|
||||
renameTagDefinition(tagName, tempName());
|
||||
}
|
||||
setIsEditingName(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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 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")} />
|
||||
return (
|
||||
<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 group">
|
||||
<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")} />
|
||||
|
||||
{isEditingName() ? (
|
||||
<input
|
||||
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()}
|
||||
onInput={(e) => setTempName(e.currentTarget.value)}
|
||||
onBlur={handleRename}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleRename()}
|
||||
autofocus
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
class="font-medium cursor-pointer hover:underline decoration-dashed underline-offset-4 truncate"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
title="Click to rename"
|
||||
>
|
||||
{tagName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<select
|
||||
value={String(val)}
|
||||
onChange={(e) => upsertTagDefinition(tagName, parseInt(e.currentTarget.value))}
|
||||
class="flex h-8 w-16 items-center justify-between rounded-md border border-input bg-background px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
>
|
||||
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1", "0"]}>
|
||||
{(v) => <option value={v}>{v}</option>}
|
||||
</For>
|
||||
</select>
|
||||
{isEditingName() ? (
|
||||
<input
|
||||
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 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={tempName()}
|
||||
onInput={(e) => setTempName(e.currentTarget.value)}
|
||||
onBlur={handleRename}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleRename()}
|
||||
autofocus
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
class="font-medium cursor-pointer hover:underline decoration-dashed underline-offset-4 truncate"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
title="Click to rename"
|
||||
>
|
||||
{tagName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => {
|
||||
if (confirm(`Delete tag "${tagName}" globally? This will remove it from all tasks.`)) {
|
||||
removeTagDefinition(tagName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
<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">
|
||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<select
|
||||
value={String(val)}
|
||||
onChange={(e) => upsertTagDefinition(tagName, parseInt(e.currentTarget.value))}
|
||||
class="flex h-8 w-16 items-center justify-between rounded-md border border-input bg-background px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
>
|
||||
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1", "0"]}>
|
||||
{(v) => <option value={v}>{v}</option>}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => {
|
||||
if (confirm(`Delete tag "${tagName}" globally? This will remove it from all tasks.`)) {
|
||||
removeTagDefinition(tagName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
|
||||
<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 min-w-0">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Trash2 size={16} />
|
||||
Trash
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground truncate">Items are permanently deleted after 7 days.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<For each={store.tasks.filter(t => t.deletedAt)} fallback={
|
||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
Trash is empty.
|
||||
{/* Task Template Manager */}
|
||||
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="flex flex-1 items-center justify-between cursor-pointer group mr-4"
|
||||
onClick={() => setIsTemplatesOpen(!isTemplatesOpen())}
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Copy size={16} />
|
||||
Task Templates
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">Save frequent task configurations for quick reuse.</p>
|
||||
</div>
|
||||
}>
|
||||
{(task) => {
|
||||
const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24));
|
||||
return (
|
||||
<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 px-1">
|
||||
<p class="font-semibold truncate text-sm sm:text-base">{task.title || "Untitled"}</p>
|
||||
<p class="text-[11px] text-muted-foreground opacity-70">Expires in {Math.max(0, daysLeft)} days</p>
|
||||
</div>
|
||||
<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
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
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={() => {
|
||||
restoreTask(task.id);
|
||||
toast.success("Task restored");
|
||||
}}
|
||||
>
|
||||
<Undo2 size={14} class="mr-2" />
|
||||
Recover
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-9 w-9 hover:bg-red-500/10 hover:text-red-600 rounded-xl border border-border/50 bg-background/30"
|
||||
onClick={() => {
|
||||
if (confirm("Permanently delete this task? This cannot be undone.")) {
|
||||
deleteTaskPermanently(task.id);
|
||||
toast.error("Task permanently deleted");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isTemplatesOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
class="h-9 gap-2 rounded-xl font-bold uppercase tracking-wider text-[10px]"
|
||||
onClick={() => {
|
||||
addTemplate({
|
||||
name: "New Template",
|
||||
title: "",
|
||||
priority: 5,
|
||||
urgency: 5,
|
||||
tags: [],
|
||||
content: ""
|
||||
});
|
||||
toast.success("New template created");
|
||||
}}
|
||||
</For>
|
||||
>
|
||||
<Plus size={14} />
|
||||
New
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={isTemplatesOpen()}>
|
||||
<div class="grid gap-3 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<For each={store.templates || []} fallback={
|
||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
No templates created yet.
|
||||
</div>
|
||||
}>
|
||||
{(template) => {
|
||||
const [isEditingName, setIsEditingName] = createSignal(false);
|
||||
const [editName, setEditName] = createSignal(template.name);
|
||||
const isExpanded = () => expandedTemplateId() === template.id;
|
||||
|
||||
const handleSaveName = () => {
|
||||
if (editName() && editName() !== template.name) {
|
||||
updateTemplate(template.id, { name: editName() });
|
||||
}
|
||||
setIsEditingName(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={cn(
|
||||
"flex flex-col rounded-2xl border transition-all overflow-hidden shadow-sm",
|
||||
isExpanded() ? "bg-card border-primary/50 ring-1 ring-primary/20" : "bg-muted/20 border-border/40 hover:bg-muted/40"
|
||||
)}>
|
||||
<div class="flex items-center justify-between gap-3 p-3.5">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div
|
||||
class={cn(
|
||||
"w-8 h-8 rounded-xl flex items-center justify-center shrink-0 transition-colors cursor-pointer",
|
||||
isExpanded() ? "bg-primary text-primary-foreground" : "bg-primary/10 text-primary"
|
||||
)}
|
||||
onClick={() => setExpandedTemplateId(isExpanded() ? null : template.id)}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</div>
|
||||
{isEditingName() ? (
|
||||
<input
|
||||
class="flex h-8 min-w-0 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={editName()}
|
||||
onInput={(e) => setEditName(e.currentTarget.value)}
|
||||
onBlur={handleSaveName}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSaveName()}
|
||||
autofocus
|
||||
/>
|
||||
) : (
|
||||
<div class="min-w-0 cursor-pointer flex-1" onClick={() => setExpandedTemplateId(isExpanded() ? null : template.id)}>
|
||||
<p class="font-bold text-sm tracking-tight truncate">{template.name}</p>
|
||||
<p class="text-[10px] text-muted-foreground uppercase font-black tracking-widest opacity-60">
|
||||
P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:bg-primary/10 hover:text-primary rounded-xl"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEditingName(true);
|
||||
}}
|
||||
>
|
||||
<Type size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:bg-red-500/10 hover:text-red-600 rounded-xl"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm(`Delete template "${template.name}"?`)) {
|
||||
removeTemplate(template.id);
|
||||
toast.error("Template removed");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={isExpanded()}>
|
||||
<div class="border-t border-border/50 p-4 space-y-4 bg-background/50 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<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 })}
|
||||
placeholder="Default task title..."
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<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
|
||||
class="flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-sm outline-none focus:ring-1 focus:ring-primary appearance-none px-2"
|
||||
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>
|
||||
</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</label>
|
||||
<select
|
||||
class="flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-sm outline-none focus:ring-1 focus:ring-primary appearance-none px-2"
|
||||
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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Tags</label>
|
||||
<div class="flex flex-wrap gap-2 p-2 rounded-xl bg-muted/30 border border-border/40">
|
||||
<TagPicker
|
||||
selectedTags={template.tags || []}
|
||||
onTagsChange={(tags) => updateTemplate(template.id, { tags })}
|
||||
/>
|
||||
<For each={template.tags || []}>
|
||||
{(tag) => (
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-background border border-border/50 text-xs hover:bg-destructive/10 hover:border-destructive/30 group/tag transition-all"
|
||||
onClick={() => updateTemplate(template.id, { tags: (template.tags || []).filter(t => t !== tag) })}
|
||||
>
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
|
||||
{tag}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Description (Content)</label>
|
||||
<div class="min-h-[120px] rounded-xl border border-border/40 bg-background/50 p-2 overflow-hidden prose-sm">
|
||||
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||
<TaskEditor
|
||||
content={template.content}
|
||||
onUpdate={(html) => updateTemplate(template.id, { content: html })}
|
||||
class="min-h-[100px]"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
|
||||
{/* Trash Manager */}
|
||||
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 overflow-hidden">
|
||||
<div
|
||||
class="flex items-center justify-between cursor-pointer group"
|
||||
onClick={() => setIsTrashOpen(!isTrashOpen())}
|
||||
>
|
||||
<div class="space-y-0.5 min-w-0">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Trash2 size={16} />
|
||||
Trash
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground truncate">Items are permanently deleted after 7 days.</p>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isTrashOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={isTrashOpen()}>
|
||||
<div class="space-y-2 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<For each={store.tasks.filter(t => t.deletedAt)} fallback={
|
||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
Trash is empty.
|
||||
</div>
|
||||
}>
|
||||
{(task) => {
|
||||
const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24));
|
||||
return (
|
||||
<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 px-1">
|
||||
<p class="font-semibold truncate text-sm sm:text-base">{task.title || "Untitled"}</p>
|
||||
<p class="text-[11px] text-muted-foreground opacity-70">Expires in {Math.max(0, daysLeft)} days</p>
|
||||
</div>
|
||||
<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
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
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={() => {
|
||||
restoreTask(task.id);
|
||||
toast.success("Task restored");
|
||||
}}
|
||||
>
|
||||
<Undo2 size={14} class="mr-2" />
|
||||
Recover
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-9 w-9 hover:bg-red-500/10 hover:text-red-600 rounded-xl border border-border/50 bg-background/30"
|
||||
onClick={() => {
|
||||
if (confirm("Permanently delete this task? This cannot be undone.")) {
|
||||
deleteTaskPermanently(task.id);
|
||||
toast.error("Task permanently deleted");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user