diff --git a/app/page.tsx b/app/page.tsx index 3f36f7c..6761c5c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,62 @@ -import Image from "next/image"; +// Next.js Server Component (no 'use client' needed here) +// This page runs on the server, reads from SQLite directly, and renders HTML + +import { getTasks } from '@/src/lib/tasks'; +import TaskForm from '@/src/lib/components/TaskForm'; +import TaskCard from '@/src/lib/components/TaskCard'; +import TaskFilterToolbar from '@/src/lib/components/TaskFilterToolbar'; + +// Next.js 15 page props for async searchParams +interface PageProps { + searchParams: Promise<{ sortBy?: 'due_date' | 'topic' | 'status' }>; +} + +export default async function HomePage({ searchParams }: PageProps) { + // Await search parameters from the URL (e.g., ?sortBy=topic) + const params = await searchParams; + const sortBy = params.sortBy || 'due_date'; + + // Direct SQLite query on the server to get active tasks sorted + const tasks = getTasks(sortBy); -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
+
+ {/* Header */} +
+

+ 📝 Todo App +

+

+ Local-first SQLite task manager with dynamic overdue detection. +

+
+ + {/* Task Creation Form */} + + + {/* Task Sorting Toolbar */} + + + {/* List of Tasks */} +
+

+ Tasks ({tasks.length}) +

+ + {/* Empty state if no tasks exist */} + {tasks.length === 0 ? ( +
+

No active tasks found. Create one above!

+
+ ) : ( + /* Render grid of task cards */ +
+ {tasks.map(task => ( + + ))} +
+ )} +
+
); } diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/lib/actions.ts b/src/lib/actions.ts index 239f610..ead9407 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -16,6 +16,7 @@ export async function createTaskAction(formData: FormData) { } createTask({ title, description, due_date, topic, status }); revalidatePath('/tasks'); + revalidatePath('/'); return { success: true }; } export async function updateTaskAction(id: number, formData: FormData) { @@ -29,11 +30,13 @@ export async function updateTaskAction(id: number, formData: FormData) { } updateTask(id, { title, description, due_date, topic, status }); revalidatePath('/tasks'); + revalidatePath('/'); return { success: true }; } export async function archiveTaskAction(id: number) { archiveTask(id); revalidatePath('/tasks'); + revalidatePath('/'); revalidatePath('/archived'); return { success: true }; } \ No newline at end of file diff --git a/src/lib/components/TaskCard.tsx b/src/lib/components/TaskCard.tsx new file mode 100644 index 0000000..4ee32e3 --- /dev/null +++ b/src/lib/components/TaskCard.tsx @@ -0,0 +1,108 @@ +// Tells Next.js that this component runs in the browser (needed for click handlers & useState) +'use client'; + +// Import React hook to keep track of state (like whether the edit modal is open) +import { useState } from 'react'; + +// Import the TypeScript definition of a Task object from our types file +import { Task } from '../types'; + +// Import the Server Action function to archive a task in SQLite +import { archiveTaskAction } from '../actions'; + +// Import the pop-up modal component used to edit tasks +import TaskEditModal from './TaskEditModal'; + +// Define what props (inputs) this component expects (it needs 1 Task object) +interface TaskCardProps { + task: Task; +} + +export default function TaskCard({ task }: TaskCardProps) { + // Local state: tracks if the edit modal pop-up is open (true or false) + const [isEditing, setIsEditing] = useState(false); + + // Local state: tracks if we are currently archiving (used to dim the card) + const [isArchiving, setIsArchiving] = useState(false); + + // Function called when the user clicks the "Archive" button + const handleArchive = async () => { + // Ask the user to confirm before archiving + if (confirm(`Are you sure you want to archive "${task.title}"?`)) { + setIsArchiving(true); // Dim the card visually + await archiveTaskAction(task.id); // Call the server action to update SQLite + } + }; + + return ( + // Outer card container div (dims opacity if isArchiving is true) +
+ + {/* Header section: Title and Status badge side by side */} +
+ {/* Task Title */} +

{task.title}

+ + {/* Status Badge: Green for Complete, Yellow for In-Progress, Blue for Todo */} + + {task.status} + +
+ + {/* Task Description (only renders if a description was provided) */} + {task.description && ( +

{task.description}

+ )} + + {/* Footer section: Topic tag, Due Date, and Dynamic OVERDUE badge */} +
+
+ {/* Topic Badge */} + + #{task.topic} + + {/* Due Date display */} + Due: {task.due_date} +
+ + {/* Dynamic Overdue Badge: Only renders if task.is_overdue is calculated as true */} + {task.is_overdue && ( + + ⚠️ OVERDUE + + )} +
+ + {/* Action buttons: Edit and Archive */} +
+ {/* Click to open the Edit Modal */} + + + {/* Click to archive the task */} + +
+ + {/* Edit Modal Pop-up: Only renders when isEditing === true */} + {isEditing && ( + setIsEditing(false)} /> + )} +
+ ); +} diff --git a/src/lib/components/TaskEditModal.tsx b/src/lib/components/TaskEditModal.tsx new file mode 100644 index 0000000..e5f1f80 --- /dev/null +++ b/src/lib/components/TaskEditModal.tsx @@ -0,0 +1,137 @@ +// Client component directive for browser interactive pop-up modal +'use client'; + +// Import state hook from React +import { useState } from 'react'; + +// Import Task and TaskStatus types +import { Task, TaskStatus } from '../types'; + +// Import the update server action function +import { updateTaskAction } from '../actions'; + +// Props needed for the modal: the task to edit, and an onClose function to close the pop-up +interface TaskEditModalProps { + task: Task; + onClose: () => void; +} + +export default function TaskEditModal({ task, onClose }: TaskEditModalProps) { + // State to track if saving is in progress (shows 'Saving...' on button) + const [isSaving, setIsSaving] = useState(false); + + // Form submit handler to update task details + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); // Stop normal page refresh + setIsSaving(true); + + const formData = new FormData(e.currentTarget); // Get data entered into form inputs + try { + // Call the server action to update SQLite database using formData directly + await updateTaskAction(task.id, formData); + onClose(); // Close modal upon successful update + } catch (err) { + alert('Failed to update task'); + } finally { + setIsSaving(false); + } + }; + + return ( + // Modal Overlay: dark background overlay that covers the entire screen +
+ {/* Modal Dialog Box */} +
+ + {/* Modal Header */} +
+

Edit Task #{task.id}

+ {/* Close button (X) */} + +
+ + {/* Edit Form */} +
+ {/* Title input (pre-populated with task.title) */} +
+ + +
+ +
+ {/* Topic input */} +
+ + +
+ + {/* Due date picker */} +
+ + +
+
+ + {/* Status dropdown */} +
+ + +
+ + {/* Description text area */} +
+ + +
+ + {/* Modal Action buttons */} +
+ + +
+
+
+
+ ); +} diff --git a/src/lib/components/TaskFilterToolbar.tsx b/src/lib/components/TaskFilterToolbar.tsx new file mode 100644 index 0000000..13acc16 --- /dev/null +++ b/src/lib/components/TaskFilterToolbar.tsx @@ -0,0 +1,35 @@ +// Client component directive for drop-down navigation +'use client'; + +// Next.js navigation hooks to change URL search parameters (like ?sortBy=due_date) +import { useRouter, useSearchParams } from 'next/navigation'; + +export default function TaskFilterToolbar() { + const router = useRouter(); // Hook to navigate/push new URL + const searchParams = useSearchParams(); // Hook to read current URL search query (?sortBy=...) + const currentSort = searchParams.get('sortBy') || 'due_date'; // Default sort is 'due_date' + + // Called whenever user changes the dropdown selection + const handleSortChange = (e: React.ChangeEvent) => { + const params = new URLSearchParams(searchParams); + params.set('sortBy', e.target.value); // Set new sortBy query value + router.push(`/?${params.toString()}`); // Update browser URL, triggering server re-query + }; + + return ( +
+ Sort Tasks By: + + {/* Dropdown for sorting options */} + +
+ ); +} diff --git a/src/lib/components/TaskForm.tsx b/src/lib/components/TaskForm.tsx new file mode 100644 index 0000000..1b972e0 --- /dev/null +++ b/src/lib/components/TaskForm.tsx @@ -0,0 +1,111 @@ +// Client component directive for form interactivity +'use client'; + +// useRef gets direct reference to the DOM form element; useState manages submission state +import { useRef, useState } from 'react'; + +// Import server action to create tasks in SQLite +import { createTaskAction } from '../actions'; + +export default function TaskForm() { + const formRef = useRef(null); // Used to reset input fields after submit + const [isSubmitting, setIsSubmitting] = useState(false); // Prevents multiple fast clicks + + // Form submit handler + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); // Prevents normal full-page browser reload + setIsSubmitting(true); + const formData = new FormData(e.currentTarget); // Gathers values from inputs by their 'name' + try { + await createTaskAction(formData); // Call server action to insert task into SQLite + formRef.current?.reset(); // Clear all form inputs + } catch (err) { + alert('Failed to create task'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+

Create New Task

+ +
+ {/* Title Input */} +
+ + +
+ + {/* Topic Input */} +
+ + +
+ + {/* Due Date Input */} +
+ + +
+ + {/* Status Selector */} +
+ + +
+
+ + {/* Optional Description */} +
+ + +
+ + {/* Submit Button */} +
+ +
+
+ ); +}