import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js"; import { ThemeToggle } from "../components/ThemeToggle"; import { store, restoreTask, deleteTaskPermanently, restoreNote, deleteNotePermanently, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store"; import { useTheme } from "@/components/ThemeProvider"; import { Trash2, Undo2, ArrowLeftRight, Tag, ChevronDown, ChevronRight, Share2, Users, HelpCircle, Copy, Plus, Type, Upload, Box, Edit2, Archive } 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, SelectValue } from "@/components/ui/select"; import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants"; import { pb } from "@/lib/pocketbase"; import { createEffect } from "solid-js"; const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor }))); const ImportTool = lazy(() => import("../components/ImportTool").then(m => ({ default: m.ImportTool }))); export const SettingsView: Component<{ setView?: (v: string) => void }> = (props) => { const { resolvedTheme } = useTheme(); const [isTagsOpen, setIsTagsOpen] = createSignal(false); const [isTemplatesOpen, setIsTemplatesOpen] = createSignal(false); const [isTrashOpen, setIsTrashOpen] = createSignal(false); const [isSharingOpen, setIsSharingOpen] = createSignal(false); const [isImportOpen, setIsImportOpen] = createSignal(false); const [isBucketsOpen, setIsBucketsOpen] = createSignal(false); const [expandedTemplateId, setExpandedTemplateId] = createSignal(null); const [shareUserId, setShareUserId] = createSignal(""); const [shareType, setShareType] = createSignal<'all' | 'tag'>('all'); const [shareMode, setShareMode] = createSignal<'ADD_USER' | 'SEND_TASK'>('ADD_USER'); const [shareTag, setShareTag] = createSignal(""); const [allUsers, setAllUsers] = createSignal>([]); const [usersLoading, setUsersLoading] = createSignal(false); // Fetch users when sharing section opens createEffect(() => { if (isSharingOpen() && allUsers().length === 0 && !usersLoading()) { setUsersLoading(true); pb.collection('users').getFullList({ fields: 'id,name,email' }) .then(records => { setAllUsers(records.map((r: any) => ({ id: r.id, name: r.name || r.email || r.id }))); }) .catch(err => console.error("Failed to load users:", err)) .finally(() => setUsersLoading(false)); } }); const getUserName = (userId: string) => { return allUsers().find(u => u.id === userId)?.name || userId; }; const availableUsers = () => { const currentUserId = pb.authStore.model?.id; return allUsers().filter(u => u.id !== currentUserId); }; return (

Settings

{pb.authStore.model?.email}

{/* Resources Category (Mostly for Mobile) */}

Resources

{/* Sharing Category */}

Sharing

{/* Sharing */}
setIsSharingOpen(!isSharingOpen())} >

Task Sharing

Share all tasks or tasks with specific tags with others.

{isSharingOpen() ? : }
{/* Add new share rule */}
{/* Share Mode Selection */}
value={allUsers().find(u => u.id === shareUserId()) || null} onChange={(user) => setShareUserId(user?.id || "")} options={availableUsers()} optionValue="id" optionTextValue="name" placeholder={usersLoading() ? "Loading..." : "Select user..."} itemComponent={(itemProps: any) => ( {itemProps.item.rawValue.name} )} > > {(state) => state.selectedOption()?.name || "Select user..."}

{shareMode() === 'ADD_USER' ? "User will be added to the task's shared list." : "Task owner will change to the recipient immediately when tagged."}

{/* Active share rules */} {/* Active share rules - Outgoing */}

Sharing With Others

{ const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null; const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName); const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag; const isOwner = r.ownerId === pb.authStore.model?.id; return !isSystem && isOwner; })} fallback={
You aren't sharing with anyone yet.
}> {(rule) => (
{rule.type === 'all' ? : }

To: {getUserName(rule.targetUserId)}

{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `}

)}
{/* Active share rules - Incoming */}

Shared With You

{ const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null; const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName); const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag; const isTarget = r.targetUserId === pb.authStore.model?.id; return !isSystem && isTarget; })} fallback={
No one has shared with you yet.
}> {(rule) => (
{rule.type === 'all' ? : }

From: {getUserName(rule.ownerId)}

{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `} {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}

{/* No delete button for incoming rules unless we add leave logic later */}
)}
{/* Active share rules - System */}
System Rules (Auto-Generated)
{ const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null; const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName); return tagDef?.isUser || tagDef?.isBucket || isBucketTag; })}> {(rule) => { const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName); return (

{/* If it's a user tag, show the person's name. If it's a bucket, show the bucket name (tagName). */} {tagDef?.isUser ? getUserName(rule.targetUserId) : rule.tagName} {tagDef?.isUser ? : }

Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag

); }}
{/* Buckets Manager */}
setIsBucketsOpen(!isBucketsOpen())} >

Shared Global Buckets

Manage public task buckets and your subscriptions.

{isBucketsOpen() ? : }
{/* Create Bucket */}

Create New Bucket

{ e.preventDefault(); const form = e.currentTarget; const nameInput = form.elements.namedItem('name') as HTMLInputElement; const colorInput = form.elements.namedItem('color') as HTMLInputElement; const descInput = form.elements.namedItem('desc') as HTMLInputElement; if (nameInput.value) { createBucket(nameInput.value, colorInput.value || "#6366f1", descInput.value); nameInput.value = ""; descInput.value = ""; form.reset(); } }} >
{/* List Buckets */}
No buckets found.
}> {(bucket) => { const isSubscribed = () => store.subscribedBuckets.includes(bucket.id); const [isEditing, setIsEditing] = createSignal(false); const [editName, setEditName] = createSignal(bucket.name); return (

{bucket.name}

}> setEditName(e.currentTarget.value)} onBlur={() => { if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() }); setIsEditing(false); }} onKeyDown={(e) => { if (e.key === "Enter") { if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() }); setIsEditing(false); } }} class="h-6 w-full max-w-[150px] rounded border border-input bg-transparent px-1 text-sm" autofocus />

{bucket.description || "No description"}

); }}
{/* Tasks Category */}

Tasks

{/* Task Template Manager */}
setIsTemplatesOpen(!isTemplatesOpen())} >

Task Templates

Save frequent task configurations for quick reuse.

{isTemplatesOpen() ? : }
No templates created yet.
}> {(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 = () => { if (editName() && editName() !== template.name) { updateTemplate(template.id, { name: editName() }); } setIsEditingName(false); }; const handleSaveTitle = () => { if (editTitle() !== template.title) { updateTemplate(template.id, { title: editTitle() }); } }; return (
setExpandedTemplateId(isExpanded() ? null : template.id)} >
{isEditingName() ? ( setEditName(e.currentTarget.value)} onBlur={handleSaveName} onKeyDown={(e) => e.key === "Enter" && handleSaveName()} autofocus /> ) : (
setExpandedTemplateId(isExpanded() ? null : template.id)}>

{template.name}

P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags

)}
setEditTitle(e.currentTarget.value)} onBlur={handleSaveTitle} placeholder="Default task title..." />
updateTemplate(template.id, { tags })} /> {(tag) => ( )}
}> { // 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]" />
); }}
{/* Import Tasks */}
setIsImportOpen(!isImportOpen())} >

Import Tasks

Paste a list of tasks from another app to import them.

{isImportOpen() ? : }
}>
{/* Trash Manager */}
setIsTrashOpen(!isTrashOpen())} >

Trash

Items are permanently deleted after 7 days.

{isTrashOpen() ? : }
{/* Tasks Trash */}

Tasks

t.deletedAt)} fallback={
No trashed tasks.
}> {(task) => { const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24)); return (

{task.title || "Untitled"}

Expires in {Math.max(0, daysLeft)} days

); }}
{/* Notes Trash */}

Notes

n.deletedAt)} fallback={
No trashed notes.
}> {(note) => { const daysLeft = Math.ceil(((note.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24)); return (

{note.title || "Untitled"}

Expires in {Math.max(0, daysLeft)} days

); }}
{/* Organization Category */}

Organization

{/* Global Tag Manager */}
setIsTagsOpen(!isTagsOpen())} >

Tag List

Manage tag definitions and values.

{isTagsOpen() ? : }
!t.isUser && !t.isBucket && !t.name.startsWith('#') && !t.name.startsWith('@')).sort((a, b) => a.name.localeCompare(b.name))} fallback={
No custom tags found.
}> {(tag) => { const [isEditingName, setIsEditingName] = createSignal(false); const [tempName, setTempName] = createSignal(tag.name); const handleRename = () => { if (tempName() && tempName() !== tag.name) { renameTagDefinition(tag.name, tempName()); } setIsEditingName(false); }; return (
upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())} class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden" title="Tag accent color" /> {isEditingName() ? ( setTempName(e.currentTarget.value)} onBlur={handleRename} onKeyDown={(e) => e.key === "Enter" && handleRename()} autofocus /> ) : ( setIsEditingName(true)} title="Click to rename" > {tag.name} )}
Val
); }}
{/* User/System Tags Expansion */}
System Tags (Users, Buckets & Notes)
t.isUser || t.isBucket || t.name.startsWith('#') || t.name.startsWith('@')).sort((a, b) => a.name.localeCompare(b.name))}> {(tag) => (
upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())} class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden" title="Tag accent color" /> {tag.name}
Val
)}
); };