From ccec7969d3888f98373c453a76d1adc9cb0fb6a4 Mon Sep 17 00:00:00 2001 From: Hiro Date: Mon, 22 Dec 2025 12:28:48 -0600 Subject: [PATCH 01/11] Task Deletion Fix --- packages/app/src/hooks/tasks.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app/src/hooks/tasks.ts b/packages/app/src/hooks/tasks.ts index 5ebdea0..f44274e 100644 --- a/packages/app/src/hooks/tasks.ts +++ b/packages/app/src/hooks/tasks.ts @@ -253,15 +253,18 @@ export function useAddTasksBulk(): UseMutationResult[ export function useUpdateTask(): UseMutationResult< void, Error, - { id: string; data: Object }, + { id: string; data: Partial }, unknown > { const queryClient = useQueryClient(); - const mutationFn = async ({ id, data }: { id: string; data: Object }) => { + const mutationFn = async ({ id, data }: { id: string; data: Partial }) => { + // Users are managed via dedicated invite/remove endpoints; omit them to avoid clobbering membership. + const { users: _omitUsers, ...rest } = data ?? {}; + await fetchData("/task", { method: "PATCH", - body: serializeTask(data as Partial) + body: serializeTask(rest) }); }; From 67ae8de004ed76fdb0c3bdd87320b3386dbb5a67 Mon Sep 17 00:00:00 2001 From: Hiro Date: Mon, 22 Dec 2025 23:16:23 -0600 Subject: [PATCH 02/11] Add repeating tasks --- packages/api/src/metrics/metrics.service.ts | 36 ++--- packages/api/src/task/task.service.ts | 134 +++++++++++++++--- packages/app/src/components/task/TaskItem.tsx | 19 ++- .../(Layout)/(TaskInfoMenu)/MenuFields.tsx | 53 +++---- packages/app/src/utils/data.ts | 87 +++++------- 5 files changed, 185 insertions(+), 144 deletions(-) diff --git a/packages/api/src/metrics/metrics.service.ts b/packages/api/src/metrics/metrics.service.ts index d22a78f..9863e98 100644 --- a/packages/api/src/metrics/metrics.service.ts +++ b/packages/api/src/metrics/metrics.service.ts @@ -13,42 +13,22 @@ export class MetricsService { } async getTaskTodayCount(userId: string): Promise<{ count: number }> { - const todayFormat = this.taskService.getTaskDateFormat(new Date()); - const count = await Task.countDocuments({ users: userId, date: { $regex: todayFormat }, done: false }).exec(); - - return { count }; + const tasks = await this.taskService.getTasksToday(userId); + return { count: tasks.length }; } async getTaskTomorrowCount(userId: string): Promise<{ count: number }> { - const today = new Date(); - today.setDate(today.getDate() + 1); - - const tomorrowFormat = this.taskService.getTaskDateFormat(today); - const count = await Task.countDocuments({ users: userId, date: { $regex: tomorrowFormat }, done: false }).exec(); - - return { count }; + const tasks = await this.taskService.getTasksTomorrow(userId); + return { count: tasks.length }; } async getTaskWeekCount(userId: string): Promise<{ count: number }> { - const format = this.taskService.getTaskDateWeekFormat(new Date()); - const count = await Task.countDocuments({ users: userId, date: { $regex: format }, done: false }).exec(); - - return { count }; + const tasks = await this.taskService.getTasksWeek(userId); + return { count: tasks.length }; } async getTaskOverdueCount(userId: string): Promise<{ count: number }> { - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const tasks = await Task.find({ users: userId, done: false }).lean().exec(); - const count = tasks.filter((task) => { - if (!task.date) return false; - const taskDate = new Date(task.date); - const time = taskDate.getTime(); - if (Number.isNaN(time) || time <= 0) return false; - return taskDate < today; - }).length; - - return { count }; + const tasks = await this.taskService.getTasksOverdue(userId); + return { count: tasks.length }; } } diff --git a/packages/api/src/task/task.service.ts b/packages/api/src/task/task.service.ts index 8afc897..5e5bbe9 100644 --- a/packages/api/src/task/task.service.ts +++ b/packages/api/src/task/task.service.ts @@ -7,6 +7,93 @@ import mongoose from "mongoose"; @Injectable() export class TaskService { + private readonly ONE_DAY_MS = 1000 * 60 * 60 * 24; + + private normalizeDay(date: Date): Date { + const normalized = new Date(date); + normalized.setHours(0, 0, 0, 0); + return normalized; + } + + private matchDay(a: string | Date | undefined, b: Date): boolean { + if (!a) return false; + const dayA = this.normalizeDay(new Date(a)); + const dayB = this.normalizeDay(b); + if (Number.isNaN(dayA.getTime()) || Number.isNaN(dayB.getTime())) return false; + return dayA.getTime() === dayB.getTime(); + } + + private occursOnDate(task: Task, target: Date): boolean { + if (!task?.date) return false; + const startDay = this.normalizeDay(new Date(task.date)); + const targetDay = this.normalizeDay(target); + + if (Number.isNaN(startDay.getTime()) || Number.isNaN(targetDay.getTime())) return false; + if (targetDay < startDay) return false; + + if (!task.repeater) return startDay.getTime() === targetDay.getTime(); + + switch (task.repeater) { + case "daily": + return true; + case "weekly": + return startDay.getDay() === targetDay.getDay(); + case "bi-weekly": { + const diffDays = Math.floor(Math.abs(targetDay.getTime() - startDay.getTime()) / this.ONE_DAY_MS); + return diffDays % 14 === 0; + } + case "monthly": + return startDay.getDate() === targetDay.getDate(); + default: + return false; + } + } + + private isCompletedOnDate(task: Task, target: Date): boolean { + // For repeating tasks, ignore legacy boolean `done` and rely on per-day markers. + if (task.repeater && !Array.isArray(task.done)) return false; + + if (Array.isArray(task.done)) { + return task.done.some((entry) => this.matchDay(entry as any, target)); + } + + return Boolean(task.done); + } + + private isPendingOnDate(task: Task, target: Date): boolean { + return this.occursOnDate(task, target) && !this.isCompletedOnDate(task, target); + } + + private hasPendingWithinDays(task: Task, start: Date, days: number): boolean { + const startDay = this.normalizeDay(start); + for (let i = 0; i < days; i++) { + const checkDay = new Date(startDay); + checkDay.setDate(startDay.getDate() + i); + if (this.isPendingOnDate(task, checkDay)) return true; + } + return false; + } + + private hasPendingBefore(task: Task, target: Date): boolean { + if (!task?.date) return false; + + const targetDay = this.normalizeDay(target); + const dayBefore = new Date(targetDay); + dayBefore.setDate(targetDay.getDate() - 1); + + const startDay = this.normalizeDay(new Date(task.date)); + if (Number.isNaN(startDay.getTime()) || startDay > dayBefore) return false; + + const totalDays = Math.floor((dayBefore.getTime() - startDay.getTime()) / this.ONE_DAY_MS) + 1; + for (let i = 0; i < totalDays; i++) { + const checkDay = new Date(startDay); + checkDay.setDate(startDay.getDate() + i); + if (this.isPendingOnDate(task, checkDay)) return true; + } + + return false; + } + normalizeTags(tags?: string[] | Array<{ title?: string }>): string[] | undefined { if (!Array.isArray(tags)) return undefined; @@ -94,60 +181,65 @@ export class TaskService { } async getTasksToday(userId: string): Promise { - const todayFormat = this.getTaskDateFormat(new Date()); + const today = this.normalizeDay(new Date()); - return Task.find({ users: userId, date: { $regex: todayFormat }, done: false }) + const tasks = await Task.find({ users: userId }) .populate({ path: "users", select: "first last email id" }) .lean() .exec(); + + return tasks.filter((task) => this.isPendingOnDate(task, today)); } async getTasksTomorrow(userId: string): Promise { - const today = new Date(); - today.setDate(today.getDate() + 1); + const tomorrow = this.normalizeDay(new Date()); + tomorrow.setDate(tomorrow.getDate() + 1); - const tomorrowFormat = this.getTaskDateFormat(today); - - return Task.find({ users: userId, date: { $regex: tomorrowFormat }, done: false }) + const tasks = await Task.find({ users: userId }) .populate({ path: "users", select: "first last email id" }) .lean() .exec(); + + return tasks.filter((task) => this.isPendingOnDate(task, tomorrow)); } async getTasksWeek(userId: string): Promise { - const today = new Date(); - const format = this.getTaskDateWeekFormat(today); + const startDay = this.normalizeDay(new Date()); - return Task.find({ users: userId, date: { $regex: format }, done: false }) + const tasks = await Task.find({ users: userId }) .populate({ path: "users", select: "first last email id" }) .lean() .exec(); + + return tasks.filter((task) => this.hasPendingWithinDays(task, startDay, 7)); } async getTasksOverdue(userId: string): Promise { - const today = new Date(); - today.setHours(0, 0, 0, 0); + const today = this.normalizeDay(new Date()); - const tasks = await Task.find({ users: userId, done: false }) + const tasks = await Task.find({ users: userId }) .populate({ path: "users", select: "first last email id" }) .lean() .exec(); - return tasks.filter((task) => { - if (!task.date) return false; - const taskDate = new Date(task.date); - const time = taskDate.getTime(); - if (Number.isNaN(time) || time <= 0) return false; - return taskDate < today; - }); + return tasks.filter((task) => this.hasPendingBefore(task, today)); } async getTasksIncomplete(userId: string): Promise { - return Task.find({ users: userId, done: false }) + const today = this.normalizeDay(new Date()); + + const tasks = await Task.find({ users: userId }) .populate({ path: "users", select: "first last email id" }) .sort({ priority: -1 }) .lean() .exec(); + + // Consider tasks that are pending today, within the next 90 days, or overdue. + return tasks.filter((task) => + this.isPendingOnDate(task, today) || + this.hasPendingWithinDays(task, today, 90) || + this.hasPendingBefore(task, today) + ); } async deleteTask(id: string): Promise { diff --git a/packages/app/src/components/task/TaskItem.tsx b/packages/app/src/components/task/TaskItem.tsx index 657166b..b0191b9 100644 --- a/packages/app/src/components/task/TaskItem.tsx +++ b/packages/app/src/components/task/TaskItem.tsx @@ -83,28 +83,25 @@ export function TaskItem({ skeleton, item, setIsInspecting, taskFilter, selectio let newData = {}; if (item.repeater && item.repeater.length != 0) { - let newDone = item.done || []; - let activeDate = appData.activeDate; - - if (!Array.isArray(item.done)) item.done = newDone; + const activeDate = appData.activeDate; + const newDone = Array.isArray(item.done) ? [...item.done] : []; let rawDate = new Date(activeDate); rawDate.setHours(0, 0, 0, 0); - let foundDate = [...item.done].find((ite) => - matchDate(new Date(ite), rawDate) - ); + const foundIdx = newDone.findIndex((entry) => matchDate(new Date(entry), rawDate)); - if (!foundDate) newDone.push(rawDate); - else newDone.splice(newDone.indexOf(rawDate), 1); + if (foundIdx === -1) { + newDone.push(rawDate); + } else { + newDone.splice(foundIdx, 1); + } updateTask({ id: item.id, data: { ...item, done: newDone } }); } else { updateTask({ id: item.id, data: { ...item, done: !item.done } }); } - setIsManaging(false); - // allow fade-out before hiding when filtering incomplete setTimeout(() => setIsCompleting(false), 600); }; diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx index 67e6af1..b3ed357 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/MenuFields.tsx @@ -2,6 +2,7 @@ import { formatDateTime } from "@/utils/date"; import TaskInfoMenuItem from "./Shared/TaskInfoMenuItem"; import TaskInfoMenuUser from "./Shared/TaskInfoUser/TaskInfoMenuUser"; import TaskInfoMenuTags from "./Shared/TaskInfoMenuTags"; +import TaskInfoMenuSelect from "./Shared/TaskInfoMenuSelect"; interface MenuFieldsProps { type: string | undefined; @@ -117,6 +118,24 @@ export default function MenuFields({ onChange={(tags) => setTempData({ ...tempData, tags })} /> + ) => { + setTempData({ repeater: e.target.value }); + }} + options={[ + { name: "Do Not Repeat", value: "" }, + { name: "Every Day", value: "daily" }, + { name: "Every Week", value: "weekly" }, + { name: "Every 2 Weeks", value: "bi-weekly" }, + { name: "Every Month", value: "monthly" }, + ]} + /> + + Repeating tasks can be completed once per due day; completion is tracked per occurrence. + + {tempData.date.getTime() != 0 && ( } - {/* ) => { - setTempData({ reminder: e.target.value }); - }} - options={[ - { name: "Do not remind", value: "" }, - { name: "0min before", value: "0" }, - { name: "15min before", value: "15" }, - { name: "30min before", value: "30" }, - { name: "45min before", value: "45" }, - { name: "1hr Before", value: "60" }, - { name: "2hr Before", value: "120" }, - { name: "12hr before", value: "720" }, - { name: "1 day before", value: "1440" }, - ]} - /> - - ) => { - setTempData({ repeater: e.target.value }); - }} - options={[ - { name: "Do Not Repeat", value: "" }, - { name: "Every Day", value: "daily" }, - { name: "Every Week", value: "weekly" }, - { name: "Every 2 Weeks", value: "bi-weekly" }, - { name: "Every Month", value: "monthly" }, - ]} - /> */} -
Priority
diff --git a/packages/app/src/utils/data.ts b/packages/app/src/utils/data.ts index 4bb9537..6e1331d 100644 --- a/packages/app/src/utils/data.ts +++ b/packages/app/src/utils/data.ts @@ -2,6 +2,14 @@ import { Task } from "@/hooks/tasks"; import { matchDate } from "./date"; import { SERVER_IP } from "@/hooks/app"; +const DAY_MS = 1000 * 60 * 60 * 24; + +const normalizeDay = (value: Date | string | number | undefined) => { + const date = new Date(value as any); + date.setHours(0, 0, 0, 0); + return date; +}; + export async function fetchData(url: string, options: any) { const payload = { headers: { @@ -47,67 +55,46 @@ export function isDateGreater(a, b) { * @returns true/false if within proximity */ export function isDateWithinProximity(mode, a, b) { - let tempDate = new Date(a.date); + return occursOnDate({ ...a, repeater: mode }, b); +} - switch (mode) { - case "daily": - tempDate = new Date(a.date); - if (isDateGreater(tempDate, b)) return true; - break; +export function occursOnDate(task: Task, target: Date): boolean { + if (!task?.date) return false; - case "weekly": - tempDate = new Date(a.date); - return new Date(a.date).getDay() == new Date(b).getDay(); + const start = normalizeDay(task.date); + const day = normalizeDay(target); - case "bi-weekly": - tempDate = new Date(a.date); - for (let i = 0; i < 100; i++) { - tempDate.setDate(new Date(a.date).getDate() + i * 14); - if (matchDate(tempDate, b)) { - return true; - } - } - - return false; + if (Number.isNaN(start.getTime()) || Number.isNaN(day.getTime())) return false; + if (day < start) return false; + switch (task.repeater) { + case "daily": + return true; + case "weekly": + return start.getDay() === day.getDay(); + case "bi-weekly": { + const diffDays = Math.floor(Math.abs(day.getTime() - start.getTime()) / DAY_MS); + return diffDays % 14 === 0; + } case "monthly": - tempDate = new Date(a.date); - for (let i = 0; i < 12; i++) { - tempDate.setMonth(tempDate.getMonth() + i); - if ( - tempDate.getFullYear() == b.getFullYear() && - tempDate.getMonth() == b.getMonth() && - tempDate.getDate() == b.getDate() - ) { - return true; - } - } - - return false; + return start.getDate() === day.getDate(); + default: + return start.getTime() === day.getTime(); } } -/** - * - * @param {Object} task task - * @param {Object} activeDate context date - * @returns - */ export function isTaskDone(task, activeDate) { - if (Array.isArray(task.done)) { - if (task.done.length == 0) return true; - else if (task.done.length > 0) { - return !task.done.find((task) => - matchDate(new Date(task), new Date(activeDate)) - ); - } - } else { - return task.done == false; - } + const day = normalizeDay(activeDate ?? new Date()); + if (!occursOnDate(task, day)) return false; - return false; + const isCompletedForDay = Array.isArray(task.done) + ? task.done.some((entry) => matchDate(new Date(entry), day)) + : (!task.repeater && Boolean(task.done)); + + // Return true when the task is still pending for the selected day. + return !isCompletedForDay; } export function sortByPriority(tasks: Task[]) { return tasks.sort((a: Task, b: Task) => { return (b.priority || 0) > (a.priority || 0) ? 1 : -1; }); -} \ No newline at end of file +} From bbf82fcff824e77c4667dd3f8ff6350f7bcadec1 Mon Sep 17 00:00:00 2001 From: Hiro Date: Mon, 22 Dec 2025 23:19:29 -0600 Subject: [PATCH 03/11] Change password accordian --- packages/app/src/pages/Settings.tsx | 93 ++++++++++++++++------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/packages/app/src/pages/Settings.tsx b/packages/app/src/pages/Settings.tsx index fbe3429..a1627f0 100644 --- a/packages/app/src/pages/Settings.tsx +++ b/packages/app/src/pages/Settings.tsx @@ -42,6 +42,7 @@ export default function SettingsPage() { const [dataMessage, setDataMessage] = useState(""); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleteInput, setDeleteInput] = useState(""); + const [showChangePassword, setShowChangePassword] = useState(false); useEffect(() => { getSettings().then(async (tempSettings) => { @@ -318,46 +319,58 @@ export default function SettingsPage() {
-

Change password

-
- - - -
- - {passwordMessage && {passwordMessage}} -
-
+ + + {showChangePassword && ( +
+ + + +
+ + {passwordMessage && {passwordMessage}} +
+
+ )}

Privacy

From 59d158ded897135652a51d14bb7b0639d126cf7d Mon Sep 17 00:00:00 2001 From: Hiro Date: Mon, 22 Dec 2025 23:30:45 -0600 Subject: [PATCH 04/11] Status logging --- packages/api/src/logging/webhook.ts | 2 ++ packages/api/src/user/user.controller.ts | 27 ++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/api/src/logging/webhook.ts b/packages/api/src/logging/webhook.ts index 8efd9e2..6ede6f9 100644 --- a/packages/api/src/logging/webhook.ts +++ b/packages/api/src/logging/webhook.ts @@ -1,6 +1,8 @@ type WebhookMessage = string | Record; export default async function sendToWebhook(message: WebhookMessage): Promise { + // Avoid spamming Discord during local development. + // if (process.env.NODE_ENV === "development") return; if (!process.env.UPDATES_WEBHOOK_URL) return; const payload = diff --git a/packages/api/src/user/user.controller.ts b/packages/api/src/user/user.controller.ts index 34da686..b2907dd 100644 --- a/packages/api/src/user/user.controller.ts +++ b/packages/api/src/user/user.controller.ts @@ -4,6 +4,7 @@ import { session } from "@/_middleware/session"; import { Request } from "express"; import { User } from "./user.entity"; import { BadRequest } from "@outwalk/firefly/errors"; +import sendToWebhook from "@/logging/webhook"; @Controller() @Middleware(session) @@ -45,12 +46,34 @@ export class UserController { @Get("/export") async exportData({ session }: Request): Promise<{ user: User | null; tasks: any[] }> { - return this.userService.exportUserData(session.user.id); + const user = await this.userService.getUserById(session.user.id); + const data = await this.userService.exportUserData(session.user.id); + await sendToWebhook({ + embeds: [ + { + title: "User Data Exported", + description: `User **${session.user.id}** (${user?.first ?? "Unknown"} ${user?.last ?? ""} | ${user?.email ?? "No email"}) exported their data.`, + timestamp: new Date() + } + ] + }); + return data; } @Post("/delete") async deleteData({ session }: Request): Promise<{ deletedUser: boolean; removedFromTasks: number; deletedTasks: number }> { - return this.userService.deleteUserData(session.user.id); + const user = await this.userService.getUserById(session.user.id); + const result = await this.userService.deleteUserData(session.user.id); + await sendToWebhook({ + embeds: [ + { + title: "User Requested Deletion", + description: `User **${session.user.id}** (${user?.first ?? "Unknown"} ${user?.last ?? ""} | ${user?.email ?? "No email"}) requested account deletion.\nRemoved from ${result.removedFromTasks} tasks; deleted ${result.deletedTasks} tasks.`, + timestamp: new Date() + } + ] + }); + return result; } @Get("/synced") From 7525cff4acc7f272be98b19114c1ba628cdc4184 Mon Sep 17 00:00:00 2001 From: Hiro Date: Mon, 22 Dec 2025 23:53:37 -0600 Subject: [PATCH 05/11] Account notif + task fix --- packages/api/src/auth/auth.controller.ts | 2 -- packages/app/src/utils/data.ts | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/api/src/auth/auth.controller.ts b/packages/api/src/auth/auth.controller.ts index 09f4254..5dccdf0 100644 --- a/packages/api/src/auth/auth.controller.ts +++ b/packages/api/src/auth/auth.controller.ts @@ -3,7 +3,6 @@ import { BadRequest, Unauthorized } from "@outwalk/firefly/errors"; import { UserService } from "@/user/user.service"; import { User } from "@/user/user.entity"; import { Request } from "express"; -import sendToWebhook from "@/logging/webhook"; @Controller() export class AuthController { @@ -14,7 +13,6 @@ export class AuthController { @Get("/") async getAuth({ session }: Request): Promise<{ message: string }> { if (!session.user) throw new Unauthorized("Not Logged In"); - await sendToWebhook(`${session.user.id} (${session.user.first}) has logged in.`); return { message: "Logged In" }; } diff --git a/packages/app/src/utils/data.ts b/packages/app/src/utils/data.ts index 6e1331d..0d1d410 100644 --- a/packages/app/src/utils/data.ts +++ b/packages/app/src/utils/data.ts @@ -85,6 +85,13 @@ export function occursOnDate(task: Task, target: Date): boolean { export function isTaskDone(task, activeDate) { const day = normalizeDay(activeDate ?? new Date()); + + // Non-repeating tasks: show as pending until explicitly marked done, regardless of date. + if (!task?.repeater) { + return task?.done === false || typeof task?.done === "undefined"; + } + + // Repeating tasks only count on their active occurrence day(s). if (!occursOnDate(task, day)) return false; const isCompletedForDay = Array.isArray(task.done) From 26008a3fd7e08a7a7f7689963782cd0b000fffee Mon Sep 17 00:00:00 2001 From: Hiro Date: Tue, 23 Dec 2025 00:03:19 -0600 Subject: [PATCH 06/11] Task deletion menu not hiding --- .../task/TaskItemMenu/TaskItemMenuDeletion.tsx | 2 ++ .../(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx | 9 ++++----- packages/app/src/pages/Task.tsx | 13 +++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/task/TaskItemMenu/TaskItemMenuDeletion.tsx b/packages/app/src/components/task/TaskItemMenu/TaskItemMenuDeletion.tsx index 95f2fe6..6518293 100644 --- a/packages/app/src/components/task/TaskItemMenu/TaskItemMenuDeletion.tsx +++ b/packages/app/src/components/task/TaskItemMenu/TaskItemMenuDeletion.tsx @@ -12,6 +12,8 @@ export default function TaskItemMenuDeletion({ const handleDelete = () => { deleteTask(item); + // Close both deletion dialog and the parent task action menu. + setIsManaging?.(false); setIsDeleting(false); }; diff --git a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx index 5d59d4b..cb6229f 100644 --- a/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx +++ b/packages/app/src/pages/(Layout)/(TaskInfoMenu)/Shared/TaskInfoMenuDelete.tsx @@ -9,12 +9,11 @@ export function TaskInfoMenuDelete({ }) { const { mutate: deleteTask } = useDeleteTask(); - const setDeleteTask = () => { - deleteTask(task); - + const setDeleteTask = async () => { + // Close the menu immediately to avoid lingering after deletion. + closeMenu?.(); setIsDeleting(false); - - closeMenu(); + await deleteTask(task); }; return ( diff --git a/packages/app/src/pages/Task.tsx b/packages/app/src/pages/Task.tsx index a674590..81712d7 100644 --- a/packages/app/src/pages/Task.tsx +++ b/packages/app/src/pages/Task.tsx @@ -35,6 +35,19 @@ export default function Task() { } }, [appData, isInspecting, setAppData]); + // Close the TaskInfoMenu if the active task no longer exists (e.g., after deletion). + useEffect(() => { + if (!isInspecting) return; + if (!appData.activeTask?.id) return; + if (!tasks.isSuccess) return; + + const exists = tasks.data?.some((task) => task?.id === appData.activeTask?.id); + if (!exists) { + setIsInspecting(false); + setAppData({ activeTask: undefined }); + } + }, [isInspecting, appData.activeTask?.id, tasks.isSuccess, tasks.data, setAppData]); + if (tasks.isLoading) { return (
From 601c38ffd194be134ab82b0f5681a885bfcbaf49 Mon Sep 17 00:00:00 2001 From: Hiro Date: Tue, 23 Dec 2025 00:07:30 -0600 Subject: [PATCH 07/11] New Task / Update Task Hidden --- packages/app/src/pages/(Layout)/TaskInfoMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx index 0c5e8c1..e27a702 100644 --- a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx +++ b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx @@ -346,7 +346,7 @@ export default function TaskInfoMenu({ className="relative z-50" >
Date: Tue, 23 Dec 2025 00:08:30 -0600 Subject: [PATCH 08/11] Force tasks to have a title on client side --- packages/app/src/pages/(Layout)/TaskInfoMenu.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx index e27a702..61cdd67 100644 --- a/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx +++ b/packages/app/src/pages/(Layout)/TaskInfoMenu.tsx @@ -258,6 +258,11 @@ export default function TaskInfoMenu({ }; const saveAll = () => { + if (!tempData.title || tempData.title.trim().length === 0) { + alert("Please add a task title."); + return; + } + if (!validateBeforeSubmit(false)) return; const cleanedTask = { @@ -308,6 +313,10 @@ export default function TaskInfoMenu({ } if (!validateBeforeSubmit(false)) return; + if (!tempData.title || tempData.title.trim().length === 0) { + alert("Please add a task title."); + return; + } if (!tempData.id) tempData.id = createID(20); From 196400be0d65963e5530aaa3ed25296c1e31ce5f Mon Sep 17 00:00:00 2001 From: Hiro Date: Tue, 23 Dec 2025 00:10:19 -0600 Subject: [PATCH 09/11] Add search feature --- packages/app/src/pages/Task.tsx | 56 +++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/app/src/pages/Task.tsx b/packages/app/src/pages/Task.tsx index 81712d7..bae7191 100644 --- a/packages/app/src/pages/Task.tsx +++ b/packages/app/src/pages/Task.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTasks, filterBroken } from "@/hooks/tasks"; import { sortByDate } from "@/utils/data"; @@ -15,6 +15,7 @@ export default function Task() { const [appData, setAppData] = useApp(); const [activeDate, setActiveDate] = useState(appData.activeDate); const [isInspecting, setIsInspecting] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); const tasks = useTasks(); @@ -48,6 +49,38 @@ export default function Task() { } }, [isInspecting, appData.activeTask?.id, tasks.isSuccess, tasks.data, setAppData]); + const filteredTasks = useMemo(() => { + if (!tasks.isSuccess) return []; + const term = searchTerm.trim().toLowerCase(); + if (!term) return tasks.data; + + return tasks.data.filter((task) => { + const title = (task.title ?? "").toLowerCase(); + const description = (task.description ?? "").toLowerCase(); + const group = (task.group ?? "").toLowerCase(); + const tags = Array.isArray(task.tags) + ? task.tags + .map((tag) => + typeof tag === "string" + ? tag.toLowerCase() + : tag && typeof (tag as any).title === "string" + ? (tag as any).title.toLowerCase() + : "" + ) + .join(" ") + : ""; + + return ( + title.includes(term) || + description.includes(term) || + group.includes(term) || + tags.includes(term) + ); + }); + }, [tasks.isSuccess, tasks.data, searchTerm]); + + const tasksForDay = tasks.isSuccess ? { ...tasks, data: filteredTasks } : tasks; + if (tasks.isLoading) { return (
@@ -71,18 +104,29 @@ export default function Task() {
- {tasks.isSuccess && ( - - )} +
+
+ setSearchTerm(e.target.value)} + placeholder="Search tasks by title, description, or tag..." + className="w-full bg-transparent text-sm text-primary outline-none placeholder:text-slate-400" + /> +
+ {tasks.isSuccess && ( + + )} +
From 7e198ab7ea1758fd069df7122e779f2343368edb Mon Sep 17 00:00:00 2001 From: Hiro Date: Tue, 23 Dec 2025 00:13:05 -0600 Subject: [PATCH 10/11] Add search icon --- packages/app/src/pages/Task.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/Task.tsx b/packages/app/src/pages/Task.tsx index bae7191..d37e037 100644 --- a/packages/app/src/pages/Task.tsx +++ b/packages/app/src/pages/Task.tsx @@ -105,7 +105,21 @@ export default function Task() {
-
+
+ Date: Tue, 23 Dec 2025 00:15:53 -0600 Subject: [PATCH 11/11] Improve navbar design on desktop --- .../app/src/pages/(Layout)/DataContainer.tsx | 2 +- .../app/src/pages/(Layout)/Nav/NavBar.tsx | 62 ++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/packages/app/src/pages/(Layout)/DataContainer.tsx b/packages/app/src/pages/(Layout)/DataContainer.tsx index 0c88a19..2e96e8c 100644 --- a/packages/app/src/pages/(Layout)/DataContainer.tsx +++ b/packages/app/src/pages/(Layout)/DataContainer.tsx @@ -7,7 +7,7 @@ export default function DataContainer() { return (
-
+
diff --git a/packages/app/src/pages/(Layout)/Nav/NavBar.tsx b/packages/app/src/pages/(Layout)/Nav/NavBar.tsx index 9a3a2ab..b7c1a7b 100644 --- a/packages/app/src/pages/(Layout)/Nav/NavBar.tsx +++ b/packages/app/src/pages/(Layout)/Nav/NavBar.tsx @@ -14,14 +14,14 @@ export function NavBar() { const [isAdding, setIsAdding] = useState(false); - const renderBar = (isInteractive: boolean) => ( -
+ const renderMobileBar = (isInteractive: boolean) => ( +
-
-
+
+
@@ -42,7 +42,7 @@ export function NavBar() {
-
+
@@ -55,6 +55,53 @@ export function NavBar() {
); + const renderDesktopBar = (isInteractive: boolean) => ( +
+
+
+ + S + +
+ Sequenced + Plan • Track • Repeat +
+
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+
+ ); + const isAuthed = auth.isSuccess && (auth.data?.message === "Logged In" || !auth.data?.statusCode); const showBar = auth.isLoading || auth.isFetching || isAuthed; @@ -63,7 +110,8 @@ export function NavBar() { <> {showBar && ( <> - {renderBar(isAuthed)} + {renderMobileBar(isAuthed)} + {renderDesktopBar(isAuthed)} )}