favoriting
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { type Component, createSignal, For, Show } from "solid-js";
|
||||
import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark } from "lucide-solid";
|
||||
import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory, setNoteFilter, clearNoteFilter, saveNoteFilterTemplate, applyNoteFilterTemplate, removeNoteFilterTemplate, syncPreferences, type Filter } from "@/store";
|
||||
import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark, User, Star } from "lucide-solid";
|
||||
import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory, setNoteFilter, clearNoteFilter, saveNoteFilterTemplate, applyNoteFilterTemplate, removeNoteFilterTemplate, type Filter } from "@/store";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "./ui/badge";
|
||||
@@ -26,7 +26,7 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
|
||||
|
||||
const hasActiveFilters = () => {
|
||||
const f = currentFilter();
|
||||
const baseActive = (f.query || "") !== "" || (f.tags || []).length > 0 || f.editedToday;
|
||||
const baseActive = (f.query || "") !== "" || (f.tags || []).length > 0 || f.editedToday || f.ownedByMe || f.starred;
|
||||
if (mode() === 'tasks') {
|
||||
return baseActive || f.priorityMin !== 1 || f.priorityMax !== 10 || f.urgencyMin !== 1 || f.urgencyMax !== 10;
|
||||
}
|
||||
@@ -78,6 +78,30 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full", currentFilter().editedToday ? "bg-primary" : "bg-muted-foreground/30")} />
|
||||
Edited Today
|
||||
</button>
|
||||
<button
|
||||
onClick={() => currentSetFilter({ ownedByMe: !currentFilter().ownedByMe })}
|
||||
class={cn(
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[0.5625rem] font-black uppercase tracking-tight transition-all border",
|
||||
currentFilter().ownedByMe
|
||||
? "bg-primary/10 text-primary border-primary/20"
|
||||
: "bg-muted/50 text-muted-foreground border-transparent hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<User size={10} class={cn(currentFilter().ownedByMe ? "text-primary" : "text-muted-foreground/50")} />
|
||||
Mine
|
||||
</button>
|
||||
<button
|
||||
onClick={() => currentSetFilter({ starred: !currentFilter().starred })}
|
||||
class={cn(
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[0.5625rem] font-black uppercase tracking-tight transition-all border",
|
||||
currentFilter().starred
|
||||
? "bg-amber-500/10 text-amber-500 border-amber-500/20"
|
||||
: "bg-muted/50 text-muted-foreground border-transparent hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<Star size={10} class={cn(currentFilter().starred ? "fill-amber-500 text-amber-500" : "text-muted-foreground/50")} />
|
||||
Starred
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={mode() === 'tasks'}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Component, createMemo, For, Show } from "solid-js";
|
||||
import { store } from "@/store";
|
||||
import { store, getFavoriteTag } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Plus, Lock } from "lucide-solid";
|
||||
import { Plus, Lock, Star } from "lucide-solid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NOTES_COLLECTION } from "@/lib/constants";
|
||||
@@ -54,6 +54,16 @@ export const NotesSidebar: Component<{
|
||||
});
|
||||
}
|
||||
|
||||
// Owned By Me Filter
|
||||
if (filter.ownedByMe) {
|
||||
n = n.filter(note => note.user === currentUserId);
|
||||
}
|
||||
|
||||
// Starred Filter
|
||||
if (filter.starred) {
|
||||
n = n.filter(note => note.tags?.includes(getFavoriteTag()));
|
||||
}
|
||||
|
||||
return n;
|
||||
});
|
||||
|
||||
@@ -105,23 +115,28 @@ export const NotesSidebar: Component<{
|
||||
)}
|
||||
>
|
||||
<div class="font-semibold text-sm truncate flex items-center justify-between">
|
||||
<span>{note.title || "Untitled"}</span>
|
||||
<div class="flex items-center gap-1.5 truncate">
|
||||
<Show when={note.tags?.includes(getFavoriteTag())}>
|
||||
<Star size={12} class="fill-amber-500 text-amber-500 shrink-0" />
|
||||
</Show>
|
||||
<span class="truncate">{note.title || "Untitled"}</span>
|
||||
</div>
|
||||
<Show when={note.isPrivate}>
|
||||
<Lock size={12} class="text-orange-500 shrink-0 ml-2" />
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={note.tags && note.tags.length > 0}>
|
||||
<Show when={note.tags && note.tags.some(t => !t.endsWith("_favorite__"))}>
|
||||
<div class="flex gap-1 mt-2 flex-wrap">
|
||||
<For each={note.tags.slice(0, 3)}>
|
||||
<For each={note.tags.filter(t => !t.endsWith("_favorite__")).slice(0, 3)}>
|
||||
{(t) => (
|
||||
<span class="text-[0.6rem] px-1.5 py-0.5 bg-muted rounded-md text-muted-foreground whitespace-nowrap border border-border/50">
|
||||
{t}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
<Show when={note.tags.length > 3}>
|
||||
<Show when={note.tags.filter(t => !t.endsWith("_favorite__")).length > 3}>
|
||||
<span class="text-[0.6rem] px-1.5 py-0.5 text-muted-foreground whitespace-nowrap shrink-0">
|
||||
+{note.tags.length - 3}
|
||||
+{note.tags.filter(t => !t.endsWith("_favorite__")).length - 3}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -345,7 +345,7 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
<div class="flex flex-col md:flex-row items-stretch md:items-center px-4 py-3 gap-4 bg-muted/10 border-b border-border">
|
||||
<div class="flex-1 flex flex-wrap gap-2 items-center">
|
||||
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
|
||||
<For each={tags()}>
|
||||
<For each={tags().filter(t => !t.endsWith("_favorite__"))}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
|
||||
@@ -23,7 +23,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
};
|
||||
|
||||
const allTagNames = () => {
|
||||
const definedTags = store.tagDefinitions.map(d => d.name);
|
||||
const definedTags = store.tagDefinitions.filter(d => !d.name.endsWith("_favorite__")).map(d => d.name);
|
||||
const bucketTags = store.buckets.map(b => `@${b.name}`);
|
||||
const noteTags = store.notes.map(n => `#${n.title}`);
|
||||
return [...new Set([...definedTags, ...bucketTags, ...noteTags])].sort();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Component, createMemo, For, Show } from "solid-js";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now } from "@/store";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
|
||||
import { Clock, ArrowUpCircle, Copy, Users2, Star } from "lucide-solid";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
@@ -35,10 +35,10 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
// We are in a bucket view. Hide the tag that matches this bucket's name.
|
||||
// Buckets are now tagged with '@' + name.
|
||||
const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName);
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__"));
|
||||
}
|
||||
|
||||
return tags;
|
||||
return tags.filter(t => !t.endsWith("_favorite__"));
|
||||
});
|
||||
|
||||
const isDueWithin12Hours = createMemo(() => {
|
||||
@@ -218,9 +218,9 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 sm:ml-4 opacity-100 sm:opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||
<div class="ml-2 sm:ml-4 shrink-0 flex items-center gap-1">
|
||||
<button
|
||||
class="p-1.5 hover:bg-muted rounded-full transition-colors text-muted-foreground hover:text-primary"
|
||||
class="p-1.5 hover:bg-muted rounded-full transition-all text-muted-foreground hover:text-primary opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyTask(props.task.id);
|
||||
@@ -229,6 +229,25 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
<button
|
||||
class={cn(
|
||||
"p-1.5 rounded-full transition-all",
|
||||
props.task.tags?.includes(getFavoriteTag())
|
||||
? "text-amber-500 bg-amber-500/10 hover:bg-amber-500/20 opacity-100"
|
||||
: "text-muted-foreground hover:text-amber-500 hover:bg-muted opacity-0 group-hover:opacity-100"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const favTag = getFavoriteTag();
|
||||
const currentTags = props.task.tags || [];
|
||||
const isFav = currentTags.includes(favTag);
|
||||
const newTags = isFav ? currentTags.filter(t => t !== favTag) : [...currentTags, favTag];
|
||||
updateTask(props.task.id, { tags: newTags });
|
||||
}}
|
||||
title="Toggle Favorite"
|
||||
>
|
||||
<Star size={16} class={cn(props.task.tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup } from "solid-js";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments } from "@/store";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag } from "@/store";
|
||||
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 { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings, Star } from "lucide-solid";
|
||||
import { StatusCircle } from "./StatusCircle";
|
||||
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
||||
import { Button } from "./ui/button";
|
||||
@@ -201,10 +201,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
// We are in a bucket view. Hide the tag that matches this bucket's name.
|
||||
// Buckets are now tagged with '@' + name.
|
||||
const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName);
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__"));
|
||||
}
|
||||
|
||||
return tags;
|
||||
return tags.filter(t => !t.endsWith("_favorite__"));
|
||||
});
|
||||
|
||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||
@@ -361,6 +361,26 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Favorite */}
|
||||
<div class="flex items-center gap-1 group shrink-0">
|
||||
<span class="text-[0.625rem] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Favorite</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn("h-8 px-1 sm:px-2 text-[0.625rem] font-bold uppercase tracking-wider transition-colors shrink-0", props.task.tags?.includes(getFavoriteTag()) ? "text-amber-500 hover:bg-amber-500/10" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground")}
|
||||
onClick={() => {
|
||||
const favTag = getFavoriteTag();
|
||||
const currentTags = props.task.tags || [];
|
||||
const isFav = currentTags.includes(favTag);
|
||||
const newTags = isFav ? currentTags.filter(t => t !== favTag) : [...currentTags, favTag];
|
||||
updateTags(newTags);
|
||||
}}
|
||||
>
|
||||
<Star size={14} class={cn("opacity-70 group-hover:opacity-100 transition-opacity shrink-0", props.task.tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
|
||||
<span class="hidden sm:inline sm:ml-1.5">{props.task.tags?.includes(getFavoriteTag()) ? "Starred" : "Star"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Commands Shortcut */}
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
|
||||
+34
-5
@@ -4,6 +4,12 @@ import { pb } from "@/lib/pocketbase";
|
||||
import { toast } from "solid-sonner";
|
||||
import { TASGRID_COLLECTION, TAGS_COLLECTION, SHARE_RULES_COLLECTION, BUCKETS_COLLECTION, NOTES_COLLECTION, URGENCY_HOURS, SIZE_HOURS } from "@/lib/constants";
|
||||
|
||||
// Helper to get the current user's personalized favorite tag
|
||||
export const getFavoriteTag = () => {
|
||||
const userId = pb.authStore.model?.id;
|
||||
return userId ? `__${userId}_favorite__` : "__favorite__";
|
||||
};
|
||||
|
||||
const LIST_FIELDS = 'id,collectionId,collectionName,created,updated,title,startDate,dueDate,priority,status,completed,tags,user,size,recurrence,sharedWith,deletedAt';
|
||||
|
||||
const getStorageKey = () => {
|
||||
@@ -163,6 +169,8 @@ export interface Filter {
|
||||
urgencyMin: number;
|
||||
urgencyMax: number;
|
||||
editedToday: boolean;
|
||||
ownedByMe: boolean;
|
||||
starred: boolean;
|
||||
}
|
||||
|
||||
// Background sync for Context Switcher (polling fallback for realtime)
|
||||
@@ -288,7 +296,9 @@ export const [store, setStore] = createStore<TaskStore>({
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
editedToday: false,
|
||||
ownedByMe: false,
|
||||
starred: false
|
||||
},
|
||||
shareRules: [],
|
||||
filterTemplates: [],
|
||||
@@ -304,7 +314,9 @@ export const [store, setStore] = createStore<TaskStore>({
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
editedToday: false,
|
||||
ownedByMe: false,
|
||||
starred: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -403,6 +415,16 @@ export const matchesFilter = (task: Task) => {
|
||||
if (today !== updated) return false;
|
||||
}
|
||||
|
||||
// Owned By Me
|
||||
if (f.ownedByMe && task.ownerId !== currentUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Starred
|
||||
if (f.starred && !task.tags?.includes(getFavoriteTag())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1153,7 +1175,9 @@ export const initStore = async () => {
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
editedToday: false,
|
||||
ownedByMe: false,
|
||||
starred: false
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2704,12 +2728,15 @@ export const clearFilter = () => {
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
editedToday: false,
|
||||
ownedByMe: false,
|
||||
starred: false
|
||||
});
|
||||
};
|
||||
|
||||
export const setNoteFilter = (update: Partial<Filter>) => {
|
||||
setStore("noteFilter", (f) => ({ ...f, ...update }));
|
||||
syncPreferences();
|
||||
};
|
||||
|
||||
const NOTE_FILTER_TEMPLATES_COLLECTION = 'TasGrid_NoteFilterTemplates';
|
||||
@@ -2783,7 +2810,9 @@ export const clearNoteFilter = () => {
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
editedToday: false,
|
||||
ownedByMe: false,
|
||||
starred: false
|
||||
});
|
||||
syncPreferences();
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
||||
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store";
|
||||
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { type Note } from "@/store";
|
||||
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, Plus } from "lucide-solid";
|
||||
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, Plus, Star } from "lucide-solid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DragDropProvider, DragDropSensors, SortableProvider, createSortable, closestCenter } from "@thisbeyond/solid-dnd";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -505,6 +505,25 @@ export const NotepadView: Component<{
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn(
|
||||
"h-8 w-8 p-0 transition-all rounded-xl border border-border/40 shadow-sm",
|
||||
note().tags?.includes(getFavoriteTag()) ? "bg-amber-500/10 text-amber-500 hover:bg-amber-500/20" : "text-muted-foreground hover:bg-muted"
|
||||
)}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const favTag = getFavoriteTag();
|
||||
const currentTags = note().tags || [];
|
||||
const isFav = currentTags.includes(favTag);
|
||||
const newTags = isFav ? currentTags.filter(t => t !== favTag) : [...currentTags, favTag];
|
||||
await handleUpdateNote(note().id, { tags: newTags });
|
||||
}}
|
||||
title="Toggle Favorite"
|
||||
>
|
||||
<Star size={14} class={cn(note().tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
|
||||
</Button>
|
||||
<Show when={isOwner}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
Reference in New Issue
Block a user