added UPDATED indicators and task collapsing
This commit is contained in:
+102
-33
@@ -1,7 +1,7 @@
|
||||
import { type Component, createMemo, For, Show } from "solid-js";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, isCurrentUserContextTag } from "@/store";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, isCurrentUserContextTag, dismissTaskUpdateIndicator, isTaskUpdatedByAnotherUser, collapseTaskUntilUpdated, expandCollapsedTask, isTaskCollapsedUntilUpdated, isTaskCollaborativelyShared } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Clock, ArrowUpCircle, Copy, Users2, Star } from "lucide-solid";
|
||||
import { Clock, ArrowUpCircle, Copy, Users2, Star, ChevronRight } from "lucide-solid";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
@@ -54,17 +54,30 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
}
|
||||
});
|
||||
|
||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||
const canCollapseUntilUpdated = createMemo(() => isTaskCollaborativelyShared(props.task));
|
||||
const statusOptions = createMemo(() => {
|
||||
const baseOptions = getStatusOptionsForTask(props.task.recurrence);
|
||||
if (!canCollapseUntilUpdated()) return baseOptions;
|
||||
return [
|
||||
...baseOptions,
|
||||
{ value: "__collapse_until_updated__", label: "Collapse until updated" }
|
||||
];
|
||||
});
|
||||
const recurrenceProgress = createMemo(() => {
|
||||
if (props.task.status !== 10) return null;
|
||||
return getRecurrenceProgress(props.task.recurrence, props.task.updated, now());
|
||||
});
|
||||
const shouldQuickloadFade = createMemo(() => store.quickloadFadeTaskIds.includes(props.task.id));
|
||||
const showUpdateIndicator = createMemo(() => isTaskUpdatedByAnotherUser(props.task));
|
||||
const isCollapsed = createMemo(() => isTaskCollapsedUntilUpdated(props.task));
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"group relative flex items-center p-3 sm:p-4 bg-card border border-border rounded-2xl transition-[background-color,border-color,box-shadow,opacity] duration-500 hover:duration-100 hover:shadow-xl hover:shadow-primary/5 hover:-translate-y-0.5 cursor-pointer w-full min-w-0 overflow-hidden",
|
||||
"group relative flex items-center bg-card border border-border rounded-2xl transition-[background-color,border-color,box-shadow,opacity] duration-500 hover:duration-100 hover:shadow-xl hover:shadow-primary/5 hover:-translate-y-0.5 cursor-pointer w-full min-w-0 overflow-hidden",
|
||||
isCollapsed()
|
||||
? "py-1.5 pl-5 pr-5 sm:py-2 sm:pl-6 sm:pr-6"
|
||||
: "py-3 pl-6 pr-6 sm:py-4 sm:pl-7 sm:pr-7",
|
||||
props.task.completed && "opacity-60",
|
||||
props.isShaking && "animate-task-shake bg-accent/20",
|
||||
shouldQuickloadFade() && "animate-task-quickload-fade"
|
||||
@@ -73,40 +86,73 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
>
|
||||
{/* Today indicator */}
|
||||
<Show when={isDueWithin12Hours() && !props.task.completed}>
|
||||
<div class="absolute left-0 top-3 bottom-3 w-1 bg-primary rounded-r-md" />
|
||||
<div class="absolute left-0 top-3 bottom-3 w-3 sm:w-[0.875rem] rounded-r-md bg-primary/65 text-primary-foreground/90">
|
||||
<span
|
||||
class="absolute inset-0 flex items-center justify-center text-[0.4375rem] sm:text-[0.5rem] font-semibold uppercase tracking-[0.08em] [writing-mode:vertical-rl] [text-orientation:mixed]"
|
||||
>
|
||||
Today
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={showUpdateIndicator()}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-0 top-3 bottom-3 w-3 sm:w-[0.875rem] rounded-l-md bg-primary/65 text-primary-foreground/90 transition-colors hover:bg-primary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
title="Hide updates until the next change from another user"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dismissTaskUpdateIndicator(props.task.id, props.task.updated);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
class="absolute inset-0 flex items-center justify-center text-[0.4375rem] sm:text-[0.5rem] font-semibold uppercase tracking-[0.08em] [writing-mode:vertical-rl] [text-orientation:mixed]"
|
||||
>
|
||||
Updates
|
||||
</span>
|
||||
</button>
|
||||
</Show>
|
||||
{/* Status Dropdown */}
|
||||
<div class="mr-2 sm:mr-3 shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
<Select<any>
|
||||
value={(props.task.status ?? 0).toString()}
|
||||
onChange={(val: any) => {
|
||||
const value = typeof val === 'object' ? val?.value : val;
|
||||
if (value) {
|
||||
<Show when={!isCollapsed()}>
|
||||
<div class="mr-2 sm:mr-3 shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
<Select<any>
|
||||
value={(props.task.status ?? 0).toString()}
|
||||
onChange={(val: any) => {
|
||||
const value = typeof val === 'object' ? val?.value : val;
|
||||
if (!value) return;
|
||||
if (value === "__collapse_until_updated__") {
|
||||
collapseTaskUntilUpdated(props.task.id, props.task.updated);
|
||||
return;
|
||||
}
|
||||
|
||||
const s = parseInt(value);
|
||||
if (!isNaN(s)) updateTask(props.task.id, { status: s });
|
||||
}
|
||||
}}
|
||||
options={statusOptions()}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
placeholder="Status"
|
||||
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger
|
||||
class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
|
||||
onDoubleClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateTask(props.task.id, { status: 10 });
|
||||
}}
|
||||
options={statusOptions()}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
placeholder="Status"
|
||||
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||
>
|
||||
<StatusCircle status={props.task.status ?? 0} size={26} recurrenceProgress={recurrenceProgress()} />
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
<SelectTrigger
|
||||
class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
|
||||
onDoubleClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateTask(props.task.id, { status: 10 });
|
||||
}}
|
||||
>
|
||||
<StatusCircle status={props.task.status ?? 0} size={26} recurrenceProgress={recurrenceProgress()} />
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
<div class={cn("flex-1 min-w-0", isCollapsed() ? "flex items-center justify-between gap-2" : "flex flex-col")}>
|
||||
<Show
|
||||
when={isCollapsed()}
|
||||
fallback={
|
||||
<>
|
||||
<h3 class={cn(
|
||||
"text-sm sm:text-base font-semibold truncate transition-all",
|
||||
props.task.completed && "line-through"
|
||||
@@ -213,9 +259,31 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<h3 class={cn(
|
||||
"text-[0.6875rem] sm:text-xs font-semibold truncate text-muted-foreground/90",
|
||||
props.task.completed && "line-through"
|
||||
)}>
|
||||
{props.task.title}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-full p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
|
||||
title="Expand task"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
expandCollapsedTask(props.task.id);
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 sm:ml-4 shrink-0 flex items-center gap-1">
|
||||
<Show when={!isCollapsed()}>
|
||||
<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-all text-muted-foreground hover:text-primary opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
@@ -245,7 +313,8 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
>
|
||||
<Star size={16} class={cn(props.task.tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user