diff --git a/web/app/(pages)/tasks/today/page.tsx b/web/app/(pages)/tasks/today/page.tsx
new file mode 100644
index 00000000..d2e4f8a0
--- /dev/null
+++ b/web/app/(pages)/tasks/today/page.tsx
@@ -0,0 +1,15 @@
+import { TaskRoute } from "@/components/routes/task/TaskRoute"
+import { currentUser } from "@clerk/nextjs/server"
+import { notFound } from "next/navigation"
+import { get } from "ronin"
+
+export default async function TodayTasksPage() {
+ const user = await currentUser()
+ const flag = await get.featureFlag.with.name("TASK")
+
+ if (!user?.emailAddresses.some(email => flag?.emails.includes(email.emailAddress))) {
+ notFound()
+ }
+
+ return
+}
diff --git a/web/app/(pages)/tasks/upcoming/page.tsx b/web/app/(pages)/tasks/upcoming/page.tsx
new file mode 100644
index 00000000..2ab33656
--- /dev/null
+++ b/web/app/(pages)/tasks/upcoming/page.tsx
@@ -0,0 +1,15 @@
+import { TaskRoute } from "@/components/routes/task/TaskRoute"
+import { currentUser } from "@clerk/nextjs/server"
+import { notFound } from "next/navigation"
+import { get } from "ronin"
+
+export default async function UpcomingTasksPage() {
+ const user = await currentUser()
+ const flag = await get.featureFlag.with.name("TASK")
+
+ if (!user?.emailAddresses.some(email => flag?.emails.includes(email.emailAddress))) {
+ notFound()
+ }
+
+ return
+}
diff --git a/web/components/custom/sidebar/partial/task-section.tsx b/web/components/custom/sidebar/partial/task-section.tsx
index 203100f7..ae90e9a2 100644
--- a/web/components/custom/sidebar/partial/task-section.tsx
+++ b/web/components/custom/sidebar/partial/task-section.tsx
@@ -1,17 +1,22 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
-import { ListOfTasks } from "@/lib/schema/tasks"
+import { ListOfTasks, Task } from "@/lib/schema/tasks"
import { LaIcon } from "../../la-icon"
import { useEffect, useState } from "react"
import { useAuth, useUser } from "@clerk/nextjs"
import { getFeatureFlag } from "@/app/actions"
+import { isToday, isFuture } from "date-fns"
+import { useAccount } from "@/lib/providers/jazz-provider"
export const TaskSection: React.FC<{ pathname: string }> = ({ pathname }) => {
- const me = { root: { tasks: [{ id: "1", title: "Test Task" }] } }
+ const { me } = useAccount({ root: { tasks: [] } })
- const taskCount = me?.root.tasks?.length || 0
- const isActive = pathname === "/tasks"
+ const taskCount = me?.root?.tasks?.length || 0
+ const todayTasks =
+ me?.root?.tasks?.filter(task => task?.status !== "done" && task?.dueDate && isToday(task.dueDate)) || []
+ const upcomingTasks =
+ me?.root?.tasks?.filter(task => task?.status !== "done" && task?.dueDate && isFuture(task.dueDate)) || []
const [isFetching, setIsFetching] = useState(false)
const [isFeatureActive, setIsFeatureActive] = useState(false)
@@ -53,7 +58,19 @@ export const TaskSection: React.FC<{ pathname: string }> = ({ pathname }) => {
return (
-
+
+
+
{isFetching ? (
Fetching tasks...
) : (
@@ -64,11 +81,13 @@ export const TaskSection: React.FC<{ pathname: string }> = ({ pathname }) => {
}
interface TaskSectionHeaderProps {
- taskCount: number
+ title: string
+ href: string
+ count: number
isActive: boolean
}
-const TaskSectionHeader: React.FC
= ({ taskCount, isActive }) => (
+const TaskSectionHeader: React.FC = ({ title, href, count, isActive }) => (
= ({ taskCount, isActi
)}
>
- Tasks
- {taskCount > 0 && {taskCount}}
+ {title}
+ {count > 0 && {count}}
- //
- //
- //
)
interface ListProps {
- tasks: ListOfTasks
+ tasks: (Task | null)[] | undefined
}
const List: React.FC = ({ tasks }) => {
- const pathname = usePathname()
-
return (
-
-
-
+ {tasks?.filter((task): task is Task => task !== null).map(task =>
{task.title}
)}
)
}
-
-interface ListItemProps {
- label: string
- href: string
- count: number
- isActive: boolean
-}
-
-const ListItem: React.FC = ({ label, href, count, isActive }) => (
-
-
-
-
-
- {count > 0 && (
-
{count}
- )}
-
-
-)
diff --git a/web/components/routes/task/TaskForm.tsx b/web/components/routes/task/TaskForm.tsx
index eb7090d5..98bdda9a 100644
--- a/web/components/routes/task/TaskForm.tsx
+++ b/web/components/routes/task/TaskForm.tsx
@@ -1,4 +1,126 @@
-"use client"
+// import { useState, useEffect, useRef } from "react"
+// import { motion, AnimatePresence } from "framer-motion"
+// import { ListOfTasks, Task } from "@/lib/schema/tasks"
+// import { Input } from "@/components/ui/input"
+// import { Button } from "@/components/ui/button"
+// import { useAccount } from "@/lib/providers/jazz-provider"
+// import { LaIcon } from "@/components/custom/la-icon"
+// import { Checkbox } from "@/components/ui/checkbox"
+// import { format } from "date-fns"
+// import { DatePicker } from "@/components/ui/date-picker"
+
+// interface TaskFormProps {
+// filter?: "today" | "upcoming"
+// }
+
+// export const TaskForm: React.FC = ({ filter }) => {
+// const [title, setTitle] = useState("")
+// const [dueDate, setDueDate] = useState(filter === "today" ? new Date() : undefined)
+// const [inputVisible, setInputVisible] = useState(false)
+// const { me } = useAccount({ root: {} })
+// const inputRef = useRef(null)
+
+// const handleSubmit = (e: React.FormEvent) => {
+// e.preventDefault()
+// if (title.trim() && (filter !== "upcoming" || dueDate)) {
+// if (me?.root?.tasks === undefined) {
+// if (!me) return
+// me.root.tasks = ListOfTasks.create([], { owner: me })
+// }
+
+// const newTask = Task.create(
+// {
+// title,
+// description: "",
+// status: "todo",
+// createdAt: new Date(),
+// updatedAt: new Date(),
+// dueDate: dueDate || null
+// },
+// { owner: me._owner }
+// )
+// me.root.tasks?.push(newTask)
+// resetForm()
+// }
+// }
+
+// const resetForm = () => {
+// setTitle("")
+// setDueDate(filter === "today" ? new Date() : undefined)
+// setInputVisible(false)
+// }
+
+// const handleKeyDown = (e: React.KeyboardEvent) => {
+// if (e.key === "Escape") {
+// resetForm()
+// } else if (e.key === "Backspace" && title.trim() === "") {
+// resetForm()
+// }
+// }
+
+// useEffect(() => {
+// if (inputVisible && inputRef.current) {
+// inputRef.current.focus()
+// }
+// }, [inputVisible])
+
+// const formattedDate = dueDate ? format(dueDate, "EEE, MMMM do, yyyy") : "Select a date"
+
+// return (
+//
+//
+// {!inputVisible ? (
+//
+//
+//
+// ) : (
+//
+//
+// {}} />
+// setTitle(e.target.value)}
+// onKeyDown={handleKeyDown}
+// placeholder="Task title"
+// />
+//
+//
+// {filter === "upcoming" && (
+// setDueDate(date)} />
+// )}
+// {formattedDate}
+//
+//
+// )}
+//
+//
+// )
+// }
+
import { useState, useEffect, useRef } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { ListOfTasks, Task } from "@/lib/schema/tasks"
@@ -8,18 +130,22 @@ import { useAccount } from "@/lib/providers/jazz-provider"
import { LaIcon } from "@/components/custom/la-icon"
import { Checkbox } from "@/components/ui/checkbox"
import { format } from "date-fns"
+import { DatePicker } from "@/components/ui/date-picker"
-interface TaskFormProps {}
+interface TaskFormProps {
+ filter?: "today" | "upcoming" | undefined
+}
-export const TaskForm: React.FC = ({}) => {
+export const TaskForm: React.FC = ({ filter }) => {
const [title, setTitle] = useState("")
+ const [dueDate, setDueDate] = useState(filter === "today" ? new Date() : undefined)
const [inputVisible, setInputVisible] = useState(false)
const { me } = useAccount({ root: {} })
const inputRef = useRef(null)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
- if (title.trim()) {
+ if (title.trim() && (filter === "today" || (filter === "upcoming" && dueDate))) {
if (me?.root?.tasks === undefined) {
if (!me) return
me.root.tasks = ListOfTasks.create([], { owner: me })
@@ -31,7 +157,8 @@ export const TaskForm: React.FC = ({}) => {
description: "",
status: "todo",
createdAt: new Date(),
- updatedAt: new Date()
+ updatedAt: new Date(),
+ dueDate: dueDate || (filter === "today" ? new Date() : null)
},
{ owner: me._owner }
)
@@ -42,6 +169,7 @@ export const TaskForm: React.FC = ({}) => {
const resetForm = () => {
setTitle("")
+ setDueDate(filter === "today" ? new Date() : undefined)
setInputVisible(false)
}
@@ -59,55 +187,60 @@ export const TaskForm: React.FC = ({}) => {
}
}, [inputVisible])
- const formattedDate = format(new Date(), "EEE, MMMM do, yyyy")
+ const formattedDate = dueDate ? format(dueDate, "EEE, MMMM do, yyyy") : "Select a date"
return (
)
diff --git a/web/components/routes/task/TaskItem.tsx b/web/components/routes/task/TaskItem.tsx
index e4fd058c..ee20ef2e 100644
--- a/web/components/routes/task/TaskItem.tsx
+++ b/web/components/routes/task/TaskItem.tsx
@@ -1,24 +1,78 @@
import { Task } from "@/lib/schema/tasks"
import { Checkbox } from "@/components/ui/checkbox"
import { format } from "date-fns"
+import { useState, useRef, useEffect } from "react"
+import { Input } from "@/components/ui/input"
interface TaskItemProps {
task: Task
onUpdateTask: (taskId: string, updates: Partial) => void
+ onDeleteTask: (taskId: string) => void
}
-export const TaskItem: React.FC = ({ task, onUpdateTask }) => {
+export const TaskItem: React.FC = ({ task, onUpdateTask, onDeleteTask }) => {
+ const [isEditing, setIsEditing] = useState(false)
+ const [editedTitle, setEditedTitle] = useState(task.title)
+ const inputRef = useRef(null)
+
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus()
+ }
+ }, [isEditing])
+
const statusChange = (checked: boolean) => {
- onUpdateTask(task.id, { status: checked ? "done" : "todo" })
+ if (checked) {
+ onDeleteTask(task.id)
+ } else {
+ onUpdateTask(task.id, { status: "todo" })
+ }
+ }
+
+ const clickTitle = () => {
+ setIsEditing(true)
+ }
+
+ const titleChange = (e: React.ChangeEvent) => {
+ setEditedTitle(e.target.value)
+ }
+
+ const titleBlur = () => {
+ setIsEditing(false)
+ if (editedTitle.trim() !== task.title) {
+ onUpdateTask(task.id, { title: editedTitle.trim() })
+ }
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ titleBlur()
+ }
}
const formattedDate = format(new Date(task.createdAt), "EEE, MMMM do, yyyy")
return (
-
+
-
{task.title}
+ {isEditing ? (
+
+ ) : (
+
+ {task.title}
+
+ )}
{formattedDate}
diff --git a/web/components/routes/task/TaskList.tsx b/web/components/routes/task/TaskList.tsx
index 8478dae3..83d68470 100644
--- a/web/components/routes/task/TaskList.tsx
+++ b/web/components/routes/task/TaskList.tsx
@@ -1,20 +1,21 @@
import React from "react"
-import { ListOfTasks, Task } from "@/lib/schema/tasks"
+import { Task } from "@/lib/schema/tasks"
import { TaskItem } from "./TaskItem"
interface TaskListProps {
- tasks?: ListOfTasks
+ tasks: Task[]
onUpdateTask: (taskId: string, updates: Partial
) => void
+ onDeleteTask: (taskId: string) => void
}
-export const TaskList: React.FC = ({ tasks, onUpdateTask }) => {
+export const TaskList: React.FC = ({ tasks, onUpdateTask, onDeleteTask }) => {
return (
{tasks?.map(
task =>
task?.id && (
-
-
+
)
)}
diff --git a/web/components/routes/task/TaskRoute.tsx b/web/components/routes/task/TaskRoute.tsx
index a2efe5ec..9a3278fd 100644
--- a/web/components/routes/task/TaskRoute.tsx
+++ b/web/components/routes/task/TaskRoute.tsx
@@ -5,11 +5,28 @@ import { Task } from "@/lib/schema/tasks"
import { TaskList } from "./TaskList"
import { TaskForm } from "./TaskForm"
import { LaIcon } from "@/components/custom/la-icon"
+import { isToday, isFuture } from "date-fns"
+import { useTaskActions } from "./new-task-actions"
+import { ID } from "jazz-tools"
-export const TaskRoute: React.FC = () => {
+interface TaskRouteProps {
+ filter?: "today" | "upcoming"
+}
+
+export const TaskRoute: React.FC = ({ filter }) => {
const { me } = useAccount({ root: { tasks: [] } })
const tasks = me?.root.tasks
- console.log(tasks, "tasks here")
+ const { deleteTask } = useTaskActions()
+
+ const filteredTasks = tasks?.filter(task => {
+ if (!task) return false
+ if (filter === "today") {
+ return task.status !== "done" && task.dueDate && isToday(task.dueDate)
+ } else if (filter === "upcoming") {
+ return task.status !== "done" && task.dueDate && isFuture(task.dueDate)
+ }
+ return true
+ })
const updateTask = (taskId: string, updates: Partial) => {
if (me?.root?.tasks) {
@@ -20,14 +37,26 @@ export const TaskRoute: React.FC = () => {
}
}
+ const onDeleteTask = (taskId: string) => {
+ if (me) {
+ deleteTask(me, taskId as ID)
+ }
+ }
+
return (
-
Current Tasks
+
+ {filter === "today" ? "Today's Tasks" : filter === "upcoming" ? "Upcoming Tasks" : "All Tasks"}
+
-
-
+
+
task !== null) || []}
+ onUpdateTask={updateTask}
+ onDeleteTask={onDeleteTask}
+ />
)
}
diff --git a/web/components/routes/task/TodayTaskRoute.tsx b/web/components/routes/task/TodayTaskRoute.tsx
new file mode 100644
index 00000000..d2e4f8a0
--- /dev/null
+++ b/web/components/routes/task/TodayTaskRoute.tsx
@@ -0,0 +1,15 @@
+import { TaskRoute } from "@/components/routes/task/TaskRoute"
+import { currentUser } from "@clerk/nextjs/server"
+import { notFound } from "next/navigation"
+import { get } from "ronin"
+
+export default async function TodayTasksPage() {
+ const user = await currentUser()
+ const flag = await get.featureFlag.with.name("TASK")
+
+ if (!user?.emailAddresses.some(email => flag?.emails.includes(email.emailAddress))) {
+ notFound()
+ }
+
+ return
+}
diff --git a/web/components/routes/task/UpcomingTaskRoute.tsx b/web/components/routes/task/UpcomingTaskRoute.tsx
new file mode 100644
index 00000000..2ab33656
--- /dev/null
+++ b/web/components/routes/task/UpcomingTaskRoute.tsx
@@ -0,0 +1,15 @@
+import { TaskRoute } from "@/components/routes/task/TaskRoute"
+import { currentUser } from "@clerk/nextjs/server"
+import { notFound } from "next/navigation"
+import { get } from "ronin"
+
+export default async function UpcomingTasksPage() {
+ const user = await currentUser()
+ const flag = await get.featureFlag.with.name("TASK")
+
+ if (!user?.emailAddresses.some(email => flag?.emails.includes(email.emailAddress))) {
+ notFound()
+ }
+
+ return
+}
diff --git a/web/components/routes/task/new-task-actions.ts b/web/components/routes/task/new-task-actions.ts
new file mode 100644
index 00000000..484a8dbb
--- /dev/null
+++ b/web/components/routes/task/new-task-actions.ts
@@ -0,0 +1,62 @@
+import { useCallback } from "react"
+import { toast } from "sonner"
+import { LaAccount } from "@/lib/schema"
+import { Task, ListOfTasks } from "@/lib/schema/tasks"
+import { ID } from "jazz-tools"
+
+export const useTaskActions = () => {
+ const newTask = useCallback((me: LaAccount): Task | null => {
+ if (!me.root) {
+ console.error("User root is not initialized")
+ return null
+ }
+
+ if (!me.root.tasks) {
+ me.root.tasks = ListOfTasks.create([], { owner: me })
+ }
+
+ const newTask = Task.create(
+ {
+ title: "",
+ description: "",
+ status: "todo",
+ createdAt: new Date(),
+ updatedAt: new Date()
+ },
+ { owner: me._owner }
+ )
+
+ me.root.tasks.push(newTask)
+ return newTask
+ }, [])
+
+ const deleteTask = useCallback((me: LaAccount, taskId: ID): void => {
+ if (!me.root?.tasks) return
+
+ const index = me.root.tasks.findIndex(item => item?.id === taskId)
+ if (index === -1) {
+ toast.error("Task not found")
+ return
+ }
+
+ const task = me.root.tasks[index]
+ if (!task) {
+ toast.error("Task data is invalid")
+ return
+ }
+
+ try {
+ me.root.tasks.splice(index, 1)
+
+ toast.success("Task deleted", {
+ position: "bottom-right",
+ description: `${task.title} has been deleted.`
+ })
+ } catch (error) {
+ console.error("Failed to delete task", error)
+ toast.error("Failed to delete task")
+ }
+ }, [])
+
+ return { newTask, deleteTask }
+}
diff --git a/web/components/ui/date-picker.tsx b/web/components/ui/date-picker.tsx
new file mode 100644
index 00000000..6885577d
--- /dev/null
+++ b/web/components/ui/date-picker.tsx
@@ -0,0 +1,32 @@
+import * as React from "react"
+import { format } from "date-fns"
+import { Calendar as CalendarIcon } from "lucide-react"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Calendar } from "@/components/ui/calendar"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+
+interface DatePickerProps {
+ date: Date | undefined
+ onDateChange: (date: Date | undefined) => void
+ className?: string
+}
+
+export function DatePicker({ date, onDateChange, className }: DatePickerProps) {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/lib/schema/tasks.ts b/web/lib/schema/tasks.ts
index 71cf643f..e5cb9c2a 100644
--- a/web/lib/schema/tasks.ts
+++ b/web/lib/schema/tasks.ts
@@ -6,6 +6,7 @@ export class Task extends CoMap {
status = co.literal("todo", "in_progress", "done")
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
+ dueDate = co.optional.encoded(Encoders.Date)
}
export class ListOfTasks extends CoList.Of(co.ref(Task)) {}