repeating tasks UI improvements
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { type Component } from "solid-js";
|
||||
import { type Component, For } from "solid-js";
|
||||
import { CheckCircle2 } from "lucide-solid";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -6,6 +6,7 @@ interface StatusCircleProps {
|
||||
status: number;
|
||||
size?: number;
|
||||
className?: string;
|
||||
recurrenceProgress?: number | null;
|
||||
}
|
||||
|
||||
export const StatusCircle: Component<StatusCircleProps> = (props) => {
|
||||
@@ -25,10 +26,39 @@ export const StatusCircle: Component<StatusCircleProps> = (props) => {
|
||||
return circumference() * (1 - percentage);
|
||||
};
|
||||
|
||||
const dotCount = () => 12;
|
||||
const dotRadius = () => Math.max(1.4, size() * 0.06);
|
||||
const dotRingRadius = () => radius() - 1;
|
||||
const filledDots = () => {
|
||||
const progress = props.recurrenceProgress ?? 0;
|
||||
return Math.floor(Math.max(0, Math.min(1, progress)) * dotCount());
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={cn("relative flex items-center justify-center", props.className)} style={{ width: `${size()}px`, height: `${size()}px` }}>
|
||||
{props.status >= 10 ? (
|
||||
props.recurrenceProgress != null ? (
|
||||
<svg width={size()} height={size()}>
|
||||
<For each={Array.from({ length: dotCount() })}>
|
||||
{(_, index) => {
|
||||
const angle = (index() / dotCount()) * Math.PI * 2 - Math.PI / 2;
|
||||
const cx = size() / 2 + Math.cos(angle) * dotRingRadius();
|
||||
const cy = size() / 2 + Math.sin(angle) * dotRingRadius();
|
||||
const isFilled = index() < filledDots();
|
||||
return (
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={dotRadius()}
|
||||
class={isFilled ? "fill-green-500" : "fill-muted-foreground/30"}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</svg>
|
||||
) : (
|
||||
<CheckCircle2 size={size()} class="text-green-500" />
|
||||
)
|
||||
) : (
|
||||
// Progress Circle
|
||||
<div class="relative group">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { getStatusOptionsForTask } from "@/lib/constants";
|
||||
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||
import { StatusCircle } from "./StatusCircle";
|
||||
|
||||
export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props) => {
|
||||
@@ -54,6 +55,10 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
});
|
||||
|
||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||
const recurrenceProgress = createMemo(() => {
|
||||
if (props.task.status !== 10) return null;
|
||||
return getRecurrenceProgress(props.task.recurrence, props.task.updated, now());
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -93,7 +98,7 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
updateTask(props.task.id, { status: 10 });
|
||||
}}
|
||||
>
|
||||
<StatusCircle status={props.task.status ?? 0} size={26} />
|
||||
<StatusCircle status={props.task.status ?? 0} size={26} recurrenceProgress={recurrenceProgress()} />
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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, getFavoriteTag } from "@/store";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now } 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, Star, Image as ImageIcon, Film, FileText } from "lucide-solid";
|
||||
@@ -17,6 +17,7 @@ import { lazy, Suspense } from "solid-js";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||
|
||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
|
||||
@@ -208,6 +209,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
});
|
||||
|
||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||
const recurrenceProgress = createMemo(() => {
|
||||
if (props.task.status !== 10) return null;
|
||||
return getRecurrenceProgress(props.task.recurrence, props.task.updated, now());
|
||||
});
|
||||
|
||||
return (
|
||||
<Sheet open={props.isOpen} onOpenChange={(open) => {
|
||||
@@ -251,7 +256,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
updateStatus("10");
|
||||
}}
|
||||
>
|
||||
<StatusCircle status={props.task.status ?? 0} size={28} />
|
||||
<StatusCircle status={props.task.status ?? 0} size={28} recurrenceProgress={recurrenceProgress()} />
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
export type Recurrence = {
|
||||
type: 'daily' | 'weekly' | 'monthly';
|
||||
days?: number[];
|
||||
dayOfMonth?: number;
|
||||
};
|
||||
|
||||
const startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
|
||||
const addDays = (date: Date, days: number) => {
|
||||
const d = new Date(date);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d;
|
||||
};
|
||||
|
||||
const getNextDaily = (completedAt: Date) => addDays(startOfDay(completedAt), 1);
|
||||
|
||||
const getNextWeekly = (completedAt: Date, days: number[]) => {
|
||||
if (!days || days.length === 0) return null;
|
||||
const base = startOfDay(completedAt);
|
||||
for (let offset = 1; offset <= 7; offset++) {
|
||||
const candidate = addDays(base, offset);
|
||||
if (days.includes(candidate.getDay())) return candidate;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getNextMonthly = (completedAt: Date, dayOfMonth?: number) => {
|
||||
if (!dayOfMonth || dayOfMonth < 1 || dayOfMonth > 31) return null;
|
||||
const base = startOfDay(completedAt);
|
||||
const baseDay = base.getDate();
|
||||
const startOffset = dayOfMonth > baseDay ? 0 : 1;
|
||||
|
||||
for (let offset = startOffset; offset <= 12; offset++) {
|
||||
const year = base.getFullYear();
|
||||
const monthIndex = base.getMonth() + offset;
|
||||
const candidate = new Date(year, monthIndex, dayOfMonth);
|
||||
if (candidate.getMonth() === (monthIndex % 12 + 12) % 12) {
|
||||
if (candidate.getTime() > completedAt.getTime()) {
|
||||
return startOfDay(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getNextRecurrenceDate = (recurrence: Recurrence, completedAt: Date) => {
|
||||
if (recurrence.type === 'daily') return getNextDaily(completedAt);
|
||||
if (recurrence.type === 'weekly') return getNextWeekly(completedAt, recurrence.days || []);
|
||||
if (recurrence.type === 'monthly') return getNextMonthly(completedAt, recurrence.dayOfMonth);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getRecurrenceProgress = (
|
||||
recurrence: Recurrence | null | undefined,
|
||||
completedAtIso: string | null | undefined,
|
||||
nowMs: number
|
||||
) => {
|
||||
if (!recurrence || !completedAtIso) return null;
|
||||
const completedAt = new Date(completedAtIso);
|
||||
if (isNaN(completedAt.getTime())) return null;
|
||||
const next = getNextRecurrenceDate(recurrence, completedAt);
|
||||
if (!next) return null;
|
||||
const total = next.getTime() - completedAt.getTime();
|
||||
if (total <= 0) return null;
|
||||
const progress = (nowMs - completedAt.getTime()) / total;
|
||||
return Math.max(0, Math.min(1, progress));
|
||||
};
|
||||
Reference in New Issue
Block a user