From 6641ea66cb187e080bdcdb20887c9d0f97bde81e Mon Sep 17 00:00:00 2001 From: mahlatseclayton Date: Thu, 30 Jul 2026 18:51:18 +0200 Subject: [PATCH] Update:added documentation folder and added all documentation required --- README.md | 116 ++++++++++--- app/globals.css | 36 +--- app/page.module.css | 116 +++++-------- app/page.tsx | 11 +- docs/AI_TRANSPARENCY.md | 26 +++ docs/DATABASE_DESIGN.md | 37 ++++ docs/RELATIONSHIPS.md | 56 ++++++ src/lib/actions.ts | 96 ++++++----- src/lib/components/ArchivedSection.module.css | 22 +++ src/lib/components/ArchivedSection.tsx | 29 ++++ .../components/ArchivedTasksModal.module.css | 138 +++++++++++++++ src/lib/components/ArchivedTasksModal.tsx | 65 +++++++ src/lib/components/TaskCard.module.css | 160 +++++++++--------- src/lib/components/TaskEditModal.module.css | 111 ++++-------- .../components/TaskFilterToolbar.module.css | 46 ++--- src/lib/components/TaskForm.module.css | 81 +++------ src/lib/tasks.ts | 15 ++ 17 files changed, 742 insertions(+), 419 deletions(-) create mode 100644 docs/AI_TRANSPARENCY.md create mode 100644 docs/DATABASE_DESIGN.md create mode 100644 docs/RELATIONSHIPS.md create mode 100644 src/lib/components/ArchivedSection.module.css create mode 100644 src/lib/components/ArchivedSection.tsx create mode 100644 src/lib/components/ArchivedTasksModal.module.css create mode 100644 src/lib/components/ArchivedTasksModal.tsx diff --git a/README.md b/README.md index e215bc4..eea7876 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,108 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# COMS3011A Lab 1: Todo App -## Getting Started +A local-first, single-user task management application built with Next.js App Router and SQLite (`better-sqlite3`). -First, run the development server: +--- -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +## Project Description + +This project provides a task management interface designed for desktop usage. It features task creation, real-time sorting by topic, status, and due date, dynamic overdue detection, task editing, and soft-deletion (archiving) with unarchiving capabilities. + +--- + +## Architectural Choices & Key Decisions + +1. Next.js App Router (Server Components & Server Actions): + - Server Components execute on Node.js, allowing direct SQLite queries without separate REST API setup. + - Server Actions handle form submissions securely on the server, eliminating client-side API boilerplate. + - Server cache revalidation (`revalidatePath('/')`) keeps UI data in sync instantly after mutations. + +2. SQLite via `better-sqlite3` (Local-First): + - Synchronous, file-based database stored directly in `todo.db`. + - Offers low latency and zero network dependencies. + +3. Dynamic Overdue State Derivation: + - Overdue status is computed dynamically at read-time by comparing `due_date` against the current date for non-completed tasks. + - Overdue state is intentionally excluded as a database column to prevent stale data. + +4. Soft-Deletion (Archiving): + - Tasks are never hard-deleted from SQLite. Archiving sets `is_archived = 1`, preserving historical records while enabling restoration. + +--- + +## System Architecture UML Diagram + +```mermaid +sequenceDiagram + autonumber + actor User + participant ClientComp as React Client Component + participant ServerAct as Server Action (actions.ts) + participant DataLayer as Data Access Layer (tasks.ts) + participant SQLite as SQLite DB (todo.db) + participant Page as Server Page (page.tsx) + + User->>ClientComp: Submit Task / Action Trigger + ClientComp->>ServerAct: Invoke Server Action (formData) + ServerAct->>ServerAct: Validate Inputs & Guard Fields + ServerAct->>DataLayer: Call CRUD Function + DataLayer->>SQLite: Prepared Statement Execution (db.prepare) + SQLite-->>DataLayer: Operation Result + ServerAct->>ServerAct: revalidatePath('/') + ServerAct-->>ClientComp: Action Complete + ServerAct->>Page: Re-render Server Component + Page->>DataLayer: getTasks() + DataLayer->>SQLite: SELECT * FROM tasks WHERE is_archived = 0 + SQLite-->>Page: Active Task Records + Page-->>User: Stream Updated UI HTML ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +--- -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Third-Party Packages & Justifications -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +| Package Name | Type | Justification | +| :--- | :--- | :--- | +| `better-sqlite3` | Production | High-performance synchronous C-based SQLite driver for Node.js. | +| `@types/better-sqlite3` | Development | TypeScript type definitions for SQLite query compilation. | +| `next` | Production | Full-stack React framework providing App Router and Server Actions. | +| `react` / `react-dom` | Production | UI rendering engine for component tree management. | +| `tailwindcss` | Development | Utility styling engine for global baseline styles. | +| `typescript` | Development | Static type checking across server and client boundaries. | -## Learn More +--- -To learn more about Next.js, take a look at the following resources: +## Environment Requirements & Running Instructions -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### Requirements +- Node.js version 18.x or higher +- npm package manager -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +### Installation +```bash +npm install +``` -## Deploy on Vercel +### Running the Application +```bash +npm run dev +``` +Note: Next.js defaults to port 3000. If port 3000 is occupied, Next.js automatically selects the next available port (e.g. 3001), or you can specify a custom port using `npm run dev -- -p `. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +--- -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +## AI Usage Declaration + +AI assistance was utilized during this project for architectural explanations, troubleshooting SQLite schema initialization, and reviewing TSX component patterns. All code additions were executed under guided pair-programming workflows. + +Full session records and transcripts are declared in [docs/AI_TRANSPARENCY.md](docs/AI_TRANSPARENCY.md). + +--- + +## Documentation Directory Index + +All technical documentation modules are located inside the `docs/` folder: + +- [Database Design Documentation](docs/DATABASE_DESIGN.md) - SQLite schema, column specifications, and design rationale. +- [Entity Relationships Documentation](docs/RELATIONSHIPS.md) - Class diagram, state transitions, and dynamic overdue rules. +- [AI Transparency Declaration](docs/AI_TRANSPARENCY.md) - AI usage breakdown, session transcripts, and prompt logs. diff --git a/app/globals.css b/app/globals.css index c797661..3ca6111 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,8 +1,8 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #f8fafc; + --foreground: #0f172a; } @theme inline { @@ -12,38 +12,10 @@ --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - body { background: var(--background); color: var(--foreground); font-family: Arial, Helvetica, sans-serif; -} - -.mainContainer { - max-width: 56rem; - margin-left: auto; - margin-right: auto; - padding: 2.5rem 1rem; - min-height: 100vh; - display: flex; - flex-direction: column; - gap: 2rem; - background-color: #ffffff; - /* White background */ - color: #18181b; - /* Dark text for clear contrast */ -} - -@media (prefers-color-scheme: dark) { - .mainContainer { - background-color: #ffffff; - /* Or keep #ffffff if you want light mode background even in OS dark mode */ - color: #18181b; - } + margin: 0; + padding: 0; } \ No newline at end of file diff --git a/app/page.module.css b/app/page.module.css index 546badc..b71b16e 100644 --- a/app/page.module.css +++ b/app/page.module.css @@ -1,104 +1,72 @@ -/* Container & Page Layout */ +/* Light Canvas Page Layout */ .mainContainer { - max-width: 56rem; - /* 896px */ - margin-left: auto; - margin-right: auto; - padding: 2.5rem 1rem; - min-height: 100vh; - display: flex; - flex-direction: column; - gap: 2rem; - background-color: #ffffff; - color: #000000; + max-width: 56rem; + margin: 0 auto; + padding: 2.5rem 1rem; + min-height: 100vh; + display: flex; + flex-direction: column; + gap: 2rem; + background-color: #f8fafc; + color: #0f172a; } - - -/* Header Section */ +/* Header & Royal Blue Titles */ .header { - display: flex; - flex-direction: column; - gap: 0.25rem; + display: flex; + flex-direction: column; + gap: 0.25rem; } .title { - font-size: 1.875rem; - line-height: 2.25rem; - font-weight: 700; - letter-spacing: -0.025em; - color: #0000df; -} - -@media (prefers-color-scheme: dark) { - .title { - color: #0808ee; - } + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: 800; + letter-spacing: -0.025em; + color: #1d4ed8; } .subtitle { - font-size: 0.875rem; - color: #04045f; + font-size: 0.95rem; + color: #475569; + font-weight: 500; } -@media (prefers-color-scheme: dark) { - .subtitle { - color: #15154b; - } -} - -/* Task List Section */ +/* Section Title */ .section { - display: flex; - flex-direction: column; - gap: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; } .sectionTitle { - font-size: 1.125rem; - font-weight: 600; - color: #5a5ab4; -} - -@media (prefers-color-scheme: dark) { - .sectionTitle { - color: #9494d6; - } + font-size: 1.25rem; + font-weight: 700; + color: #1e40af; } /* Empty State Card */ .emptyState { - text-align: center; - padding: 3rem 1rem; - border: 1px dashed #4141aa; - border-radius: 0.75rem; -} - -@media (prefers-color-scheme: dark) { - .emptyState { - border-color: #27272a; - } + text-align: center; + padding: 3.5rem 1rem; + border: 2px dashed #cbd5e1; + border-radius: 0.75rem; + background-color: #ffffff; } .emptyText { - color: #71717a; + color: #64748b; + font-weight: 500; } -@media (prefers-color-scheme: dark) { - .emptyText { - color: #a1a1aa; - } -} - -/* Grid Layout for Task Cards */ .taskGrid { - display: grid; - grid-template-columns: 1fr; - gap: 1rem; + display: grid; + grid-template-columns: 1fr; + gap: 1.25rem; } @media (min-width: 768px) { - .taskGrid { - grid-template-columns: repeat(2, 1fr); - } + .taskGrid { + grid-template-columns: repeat(2, 1fr); + } } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 8b4e76b..66117f4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,8 @@ -import { getTasks } from '@/src/lib/tasks'; +import { getTasks, getArchivedTasks } 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'; +import ArchivedSection from '@/src/lib/components/ArchivedSection'; import styles from './page.module.css'; interface PageProps { @@ -12,6 +13,7 @@ export default async function HomePage({ searchParams }: PageProps) { const params = await searchParams; const sortBy = params.sortBy || 'due_date'; const tasks = getTasks(sortBy); + const archivedTasks = getArchivedTasks(); return (
@@ -29,10 +31,13 @@ export default async function HomePage({ searchParams }: PageProps) { {/* Task Sorting Toolbar */} - {/* List of Tasks */} + {/* Archived Tasks Trigger Button */} + + + {/* List of Active Tasks */}

- Tasks ({tasks.length}) + Active Tasks ({tasks.length})

{/* Empty state if no tasks exist */} diff --git a/docs/AI_TRANSPARENCY.md b/docs/AI_TRANSPARENCY.md new file mode 100644 index 0000000..c2d63ab --- /dev/null +++ b/docs/AI_TRANSPARENCY.md @@ -0,0 +1,26 @@ +# AI Usage and Transparency Declaration + +## Transparency Statement +In compliance with COMS3011A lab evaluation guidelines, AI assistance was utilized during the development of this project under pair-programming and pedagogical learning workflows. + +## AI Roles & Responsibilities + +1. Concept Explanation: Explaining Next.js Server Components, Server Actions, SQLite prepared statements, and CSS Module scoping. +2. Architecture Design: Structuring local-first database operations, dynamic overdue calculation rules, and component hierarchies. +3. Troubleshooting & Debugging: Identifying schema mismatch errors (`table tasks has no column named description`) and providing database migration reset steps. +4. User-Authored Code: All source code modifications were driven interactively and reviewed by the author for complete hands-on learning. + +## Conversation Log Transcripts + +All raw conversation step logs and prompt histories are preserved locally in the system environment log directory: +- Path: `C:\Users\mahla\.gemini\antigravity-ide\brain\1c0b1ab9-bf0e-425f-a67e-38d23d353de7\.system_generated\logs\transcript.jsonl` +- Full Transcript: `C:\Users\mahla\.gemini\antigravity-ide\brain\1c0b1ab9-bf0e-425f-a67e-38d23d353de7\.system_generated\logs\transcript_full.jsonl` + +## Summary of AI Interactions +- Query 1: Diagnosed `SqliteError: table tasks has no column named description` and provided schema update steps. +- Query 2: Explained Turbopack workspace root detection warnings. +- Query 3: Explained Next.js Server Actions, SQL injection prevention via prepared statements (`?`), and server cache revalidation (`revalidatePath`). +- Query 4: Detailed line-by-line breakdown of TSX components, React hooks (`useState`, `useRef`), and prop passing. +- Query 5: Refactored page inline styles into scoped CSS Modules (`page.module.css`). +- Query 6: Harmonized UI design system with light page background, royal blue typography, and dark component cards. +- Query 7: Built Archived Tasks popup dialog with unarchiving / restoring capabilities. diff --git a/docs/DATABASE_DESIGN.md b/docs/DATABASE_DESIGN.md new file mode 100644 index 0000000..3ca6bc8 --- /dev/null +++ b/docs/DATABASE_DESIGN.md @@ -0,0 +1,37 @@ +# Database Design Documentation + +## Overview +This project uses SQLite as a local-first, single-user relational database engine via the `better-sqlite3` driver for Node.js. + +## Table Schema: `tasks` + +```sql +CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + due_date TEXT NOT NULL, + topic TEXT NOT NULL, + status TEXT CHECK(status IN ('Todo','In-Progress','Complete')) NOT NULL DEFAULT 'Todo', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_archived BOOLEAN NOT NULL DEFAULT 0 +); +``` + +## Field Specifications + +| Column Name | Data Type | Constraints | Description | +| :--- | :--- | :--- | :--- | +| `id` | `INTEGER` | `PRIMARY KEY AUTOINCREMENT` | Unique identifier for each task record. | +| `title` | `TEXT` | `NOT NULL` | Short summary title of the task. | +| `description` | `TEXT` | Optional | Extended notes or detailed instructions for the task. | +| `due_date` | `TEXT` | `NOT NULL` | Due date formatted as ISO string (`YYYY-MM-DD`). | +| `topic` | `TEXT` | `NOT NULL` | Categorical topic tag (e.g. `SDP`, `Personal`). | +| `status` | `TEXT` | `CHECK(status IN ('Todo','In-Progress','Complete'))` | Current lifecycle state of the task. Default is `'Todo'`. | +| `created_at` | `TIMESTAMP` | `DEFAULT CURRENT_TIMESTAMP` | System timestamp recorded on task creation. | +| `is_archived` | `BOOLEAN` | `DEFAULT 0` | Soft-deletion flag. `0` indicates active, `1` indicates archived. | + +## Schema Design Decisions + +1. Soft-Deletion (Archiving): Tasks are never permanently deleted from the database. Setting `is_archived = 1` retains all historical data while hiding archived tasks from active views. +2. Dynamic Overdue State: The database schema explicitly excludes an `is_overdue` column. Storing `is_overdue` in a database column creates stale data when dates pass. Overdue status is dynamically computed at read-time by comparing `due_date` against the current date for non-completed tasks. diff --git a/docs/RELATIONSHIPS.md b/docs/RELATIONSHIPS.md new file mode 100644 index 0000000..d360806 --- /dev/null +++ b/docs/RELATIONSHIPS.md @@ -0,0 +1,56 @@ +# Entity Relationships and Data Lifecycle + +## System Architecture Model + +The application operates as a single-table relational model optimized for local-first desktop usage. + +```mermaid +classDiagram + class Task { + +int id + +string title + +string description + +string due_date + +string topic + +TaskStatus status + +boolean is_archived + +string created_at + +boolean computeIsOverdue() + } + + class TaskStatus { + <> + Todo + In-Progress + Complete + } + + Task "1" -- "1" TaskStatus : holds +``` + +## State Transitions & Task Lifecycle + +A task record moves through defined state transitions: + +```mermaid +stateDiagram-v2 + [*] --> Todo : Task Created + Todo --> InProgress : User updates status + InProgress --> Complete : Task completed + Complete --> InProgress : Reopened + + Todo --> Archived : Soft Deleted + InProgress --> Archived : Soft Deleted + Complete --> Archived : Soft Deleted + + Archived --> Todo : Restored (Unarchived) +``` + +## Overdue Derivation Logic + +Overdue state is derived dynamically at query runtime using the following logic: + +- Condition 1: `status !== 'Complete'` +- Condition 2: `due_date < current_date` (ISO format `YYYY-MM-DD`) + +If both conditions are met, `is_overdue` evaluates to `true`. Completed or archived tasks are never marked as overdue. diff --git a/src/lib/actions.ts b/src/lib/actions.ts index b6dd962..8c07813 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -1,44 +1,52 @@ -'use server'; -import { createTask, updateTask, archiveTask } from './tasks'; -import { TaskStatus } from './types'; -import { revalidatePath } from 'next/cache'; - -// creating tasks -export async function createTaskAction(formData: FormData) { - // get form data - const title = formData.get('title') as string; - const description = formData.get('description') as string; - const due_date = formData.get('due_date') as string; - - const status = (formData.get('status') as TaskStatus) || 'Todo'; - - const topic = formData.get('topic') as string; - if (!title || !description || !topic || !due_date) { - throw new Error('All fields are required'); - } - createTask({ title, description, due_date, topic, status }); - revalidatePath('/tasks'); - revalidatePath('/'); - return { success: true }; -} -export async function updateTaskAction(id: number, formData: FormData) { - const title = formData.get('title') as string; - const description = formData.get('description') as string; - const due_date = formData.get('due_date') as string; - const status = (formData.get('status') as TaskStatus) || 'To-Do'; - const topic = formData.get('topic') as string; - if (!title || !description || !topic || !due_date) { - throw new Error('All fields are required'); - } - 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 +'use server'; +import { createTask, updateTask, archiveTask, unarchiveTask } from './tasks'; +import { TaskStatus } from './types'; +import { revalidatePath } from 'next/cache'; + +// creating tasks +export async function createTaskAction(formData: FormData) { + // get form data + const title = formData.get('title') as string; + const description = formData.get('description') as string; + const due_date = formData.get('due_date') as string; + + const status = (formData.get('status') as TaskStatus) || 'Todo'; + + const topic = formData.get('topic') as string; + if (!title || !description || !topic || !due_date) { + throw new Error('All fields are required'); + } + createTask({ title, description, due_date, topic, status }); + revalidatePath('/tasks'); + revalidatePath('/'); + return { success: true }; +} +export async function updateTaskAction(id: number, formData: FormData) { + const title = formData.get('title') as string; + const description = formData.get('description') as string; + const due_date = formData.get('due_date') as string; + const status = (formData.get('status') as TaskStatus) || 'Todo'; + const topic = formData.get('topic') as string; + if (!title || !description || !topic || !due_date) { + throw new Error('All fields are required'); + } + 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 }; +} + +export async function unarchiveTaskAction(id: number) { + unarchiveTask(id); + revalidatePath('/tasks'); + revalidatePath('/'); + revalidatePath('/archived'); + return { success: true }; +} \ No newline at end of file diff --git a/src/lib/components/ArchivedSection.module.css b/src/lib/components/ArchivedSection.module.css new file mode 100644 index 0000000..68d0d85 --- /dev/null +++ b/src/lib/components/ArchivedSection.module.css @@ -0,0 +1,22 @@ +.container { + display: flex; + justify-content: flex-end; +} + +.btnArchived { + padding: 0.5rem 1rem; + background-color: #1e293b; + color: #94a3b8; + border: 1px solid #334155; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.btnArchived:hover { + color: #f8fafc; + border-color: #2563eb; + background-color: #0f172a; +} diff --git a/src/lib/components/ArchivedSection.tsx b/src/lib/components/ArchivedSection.tsx new file mode 100644 index 0000000..431e908 --- /dev/null +++ b/src/lib/components/ArchivedSection.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { useState } from 'react'; +import { Task } from '../types'; +import ArchivedTasksModal from './ArchivedTasksModal'; +import styles from './ArchivedSection.module.css'; + +interface ArchivedSectionProps { + archivedTasks: Task[]; +} + +export default function ArchivedSection({ archivedTasks }: ArchivedSectionProps) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + + {isOpen && ( + setIsOpen(false)} + /> + )} +
+ ); +} diff --git a/src/lib/components/ArchivedTasksModal.module.css b/src/lib/components/ArchivedTasksModal.module.css new file mode 100644 index 0000000..a5912c4 --- /dev/null +++ b/src/lib/components/ArchivedTasksModal.module.css @@ -0,0 +1,138 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(15, 23, 42, 0.75); + padding: 1rem; + backdrop-filter: blur(6px); +} + +.dialog { + width: 100%; + max-width: 32rem; + border-radius: 0.875rem; + background-color: #1e293b; + border: 1px solid #334155; + padding: 1.5rem; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4); + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 80vh; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #334155; + padding-bottom: 0.75rem; +} + +.title { + font-size: 1.25rem; + font-weight: 700; + color: #f8fafc; + margin: 0; +} + +.btnClose { + background: none; + border: none; + font-size: 1.25rem; + color: #94a3b8; + cursor: pointer; + transition: color 0.2s; +} + +.btnClose:hover { + color: #f8fafc; +} + +.content { + overflow-y: auto; + padding-right: 0.25rem; + max-height: 50vh; +} + +.emptyText { + color: #94a3b8; + text-align: center; + padding: 2rem 0; + font-size: 0.875rem; +} + +.taskList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.taskItem { + display: flex; + align-items: center; + justify-content: space-between; + background-color: #0f172a; + border: 1px solid #334155; + padding: 0.75rem 1rem; + border-radius: 0.5rem; +} + +.taskInfo { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.taskTitle { + font-weight: 600; + color: #f8fafc; + font-size: 0.9375rem; +} + +.taskTopic { + font-size: 0.75rem; + color: #93c5fd; +} + +.btnUnarchive { + padding: 0.375rem 0.75rem; + background-color: #2563eb; + color: #ffffff; + border-radius: 0.375rem; + font-size: 0.8125rem; + font-weight: 600; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.btnUnarchive:hover { + background-color: #1d4ed8; +} + +.footer { + display: flex; + justify-content: flex-end; + border-top: 1px solid #334155; + padding-top: 0.75rem; +} + +.btnCloseBtn { + padding: 0.5rem 1rem; + background-color: #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.btnCloseBtn:hover { + background-color: #475569; +} diff --git a/src/lib/components/ArchivedTasksModal.tsx b/src/lib/components/ArchivedTasksModal.tsx new file mode 100644 index 0000000..950461c --- /dev/null +++ b/src/lib/components/ArchivedTasksModal.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useState } from 'react'; +import { Task } from '../types'; +import { unarchiveTaskAction } from '../actions'; +import styles from './ArchivedTasksModal.module.css'; + +interface ArchivedTasksModalProps { + archivedTasks: Task[]; + onClose: () => void; +} + +export default function ArchivedTasksModal({ archivedTasks, onClose }: ArchivedTasksModalProps) { + const [restoringId, setRestoringId] = useState(null); + + const handleUnarchive = async (id: number) => { + setRestoringId(id); + try { + await unarchiveTaskAction(id); + } catch (err) { + alert('Failed to restore task'); + } finally { + setRestoringId(null); + } + }; + + return ( +
+
+
+

📦 Archived Tasks ({archivedTasks.length})

+ +
+ +
+ {archivedTasks.length === 0 ? ( +

No archived tasks currently stored.

+ ) : ( +
+ {archivedTasks.map(task => ( +
+
+ {task.title} + #{task.topic} • Due: {task.due_date} +
+ +
+ ))} +
+ )} +
+ +
+ +
+
+
+ ); +} diff --git a/src/lib/components/TaskCard.module.css b/src/lib/components/TaskCard.module.css index 8f10c46..072c881 100644 --- a/src/lib/components/TaskCard.module.css +++ b/src/lib/components/TaskCard.module.css @@ -1,164 +1,162 @@ -/* Card Container */ +/* Task Card Container - Matching Form UI */ .cardContainer { padding: 1.25rem; - border-radius: 0.75rem; - border: 1px solid #c6c6cc; - background-color: #e6e1e1; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - transition: all 0.2s ease-in-out; + border-radius: 0.875rem; + border: 1px solid #334155; + background-color: #1e293b; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + gap: 0.875rem; + transition: transform 0.2s ease, border-color 0.2s ease; +} + +.cardContainer:hover { + transform: translateY(-2px); + border-color: #475569; } .archiving { - opacity: 0.5; + opacity: 0.4; } - - /* Header Section */ .header { display: flex; align-items: flex-start; justify-content: space-between; - gap: 1rem; + gap: 0.75rem; } .title { - font-weight: 600; + font-weight: 700; font-size: 1.125rem; - color: #0303e0; + color: #f8fafc; margin: 0; + line-height: 1.4; } - - -/* Status Badges */ +/* Unified Status Badges with Subtle Tints */ .badge { font-size: 0.75rem; + font-weight: 600; padding: 0.25rem 0.625rem; border-radius: 9999px; - font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; } .statusComplete { - background-color: #d1fae5; - color: #02c28c; + background-color: rgba(16, 185, 129, 0.12); + color: #34d399; + border: 1px solid rgba(52, 211, 153, 0.25); } .statusInProgress { - background-color: #fef3c7; - color: #884216; + background-color: rgba(245, 158, 11, 0.12); + color: #fbbf24; + border: 1px solid rgba(251, 191, 36, 0.25); } .statusTodo { - background-color: #fcfdff; - color: #4570ff; + background-color: rgba(59, 130, 246, 0.12); + color: #60a5fa; + border: 1px solid rgba(96, 165, 250, 0.25); } - - -/* Description */ +/* Description Text */ .description { - margin-top: 0.5rem; font-size: 0.875rem; - color: #52525b; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + color: #94a3b8; + margin: 0; + line-height: 1.5; } - /* Footer Section */ .footer { - margin-top: 1rem; display: flex; - flex-wrap: wrap; align-items: center; justify-content: space-between; - gap: 0.5rem; - font-size: 0.75rem; - color: #71717a; - border-top: 1px solid #f4f4f5; + border-top: 1px solid #334155; padding-top: 0.75rem; + color: #94a3b8; + font-size: 0.8125rem; } - - .metaGroup { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.75rem; } .topicTag { - background-color: #f4f4f5; + background-color: #0f172a; + color: #93c5fd; + font-weight: 600; padding: 0.125rem 0.5rem; - border-radius: 0.25rem; - font-family: monospace; - color: #3f3f46; + border-radius: 0.375rem; + border: 1px solid #334155; } -/* Overdue Badge */ .overdueBadge { - background-color: #fee2e2; - color: #b91c1c; - padding: 0.125rem 0.5rem; - border-radius: 15px; - font-weight: 600; + background-color: rgba(239, 68, 68, 0.15); + color: #f87171; + border: 1px solid rgba(248, 113, 113, 0.3); + padding: 0.15rem 0.5rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 700; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } - - @keyframes pulse { - - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: .5; - } + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } } -/* Actions Section */ +/* Action Buttons */ .actions { - margin-top: 1rem; display: flex; justify-content: flex-end; gap: 0.5rem; - font-size: 0.75rem; + margin-top: 0.25rem; } .btnEdit { - padding: 0.375rem 0.75rem; - border-radius: 0.25rem; - background-color: #f4f4f5; - color: #27272a; - font-weight: 500; - border: none; + padding: 0.375rem 0.875rem; + border-radius: 0.5rem; + background-color: #334155; + color: #f8fafc; + font-size: 0.8125rem; + font-weight: 600; + border: 1px solid #475569; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s ease; } .btnEdit:hover { - background-color: #e4e4e7; + background-color: #2563eb; + border-color: #2563eb; + color: #ffffff; } .btnArchive { - padding: 0.375rem 0.75rem; - border-radius: 0.25rem; - background-color: #fef2f2; - color: #96a2e4; - font-weight: 500; - border: none; + padding: 0.375rem 0.875rem; + border-radius: 0.5rem; + background-color: #334155; + color: #f87171; + font-size: 0.8125rem; + font-weight: 600; + border: 1px solid #475569; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s ease; } .btnArchive:hover { - background-color: #f18484; + background-color: #ef4444; + border-color: #ef4444; + color: #ffffff; } .btnArchive:disabled { diff --git a/src/lib/components/TaskEditModal.module.css b/src/lib/components/TaskEditModal.module.css index bcf10a9..814483c 100644 --- a/src/lib/components/TaskEditModal.module.css +++ b/src/lib/components/TaskEditModal.module.css @@ -6,27 +6,20 @@ display: flex; align-items: center; justify-content: center; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(15, 23, 42, 0.7); padding: 1rem; - backdrop-filter: blur(4px); + backdrop-filter: blur(6px); } /* Modal Dialog Box */ .dialog { width: 100%; max-width: 32rem; - border-radius: 0.75rem; - background-color: #ffffff; - border: 1px solid #e4e4e7; + border-radius: 0.875rem; + background-color: #1e293b; + border: 1px solid #334155; padding: 1.5rem; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); -} - -@media (prefers-color-scheme: dark) { - .dialog { - background-color: #18181b; - border-color: #27272a; - } + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3); } /* Modal Header */ @@ -34,13 +27,13 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .title { - font-size: 1.125rem; - font-weight: 600; - color: #18181b; + font-size: 1.25rem; + font-weight: 700; + color: #f8fafc; margin: 0; } @@ -48,24 +41,13 @@ background: none; border: none; font-size: 1.25rem; - color: #71717a; + color: #94a3b8; cursor: pointer; + transition: color 0.2s; } .btnClose:hover { - color: #27272a; -} - -@media (prefers-color-scheme: dark) { - .title { - color: #f4f4f5; - } - .btnClose { - color: #a1a1aa; - } - .btnClose:hover { - color: #f4f4f5; - } + color: #f8fafc; } /* Form Layout */ @@ -88,54 +70,35 @@ .label { display: block; - font-size: 0.75rem; - font-weight: 500; - color: #52525b; - margin-bottom: 0.25rem; -} - -@media (prefers-color-scheme: dark) { - .label { - color: #a1a1aa; - } + font-size: 0.8125rem; + font-weight: 600; + color: #cbd5e1; + margin-bottom: 0.375rem; } .input, .select, .textarea { width: 100%; - padding: 0.5rem 0.75rem; + padding: 0.625rem 0.875rem; font-size: 0.875rem; border-radius: 0.5rem; - border: 1px solid #d4d4d8; - background-color: transparent; - color: #18181b; + border: 1px solid #334155; + background-color: #0f172a; + color: #f8fafc; outline: none; box-sizing: border-box; -} - -.select { - background-color: #fafafa; + transition: border-color 0.2s, box-shadow 0.2s; } .input:focus, .select:focus, .textarea:focus { border-color: #3b82f6; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); -} - -@media (prefers-color-scheme: dark) { - .input, .select, .textarea { - border-color: #3f3f46; - color: #f4f4f5; - } - .select { - background-color: #27272a; - } + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); } /* Footer Buttons */ .footerActions { display: flex; justify-content: flex-end; - gap: 0.5rem; + gap: 0.75rem; padding-top: 0.5rem; } @@ -143,38 +106,32 @@ padding: 0.5rem 1rem; font-size: 0.875rem; border-radius: 0.5rem; - border: 1px solid #d4d4d8; - background: transparent; - color: #27272a; + border: 1px solid #334155; + background: #0f172a; + color: #cbd5e1; + font-weight: 600; cursor: pointer; + transition: background-color 0.2s; } .btnCancel:hover { - background-color: #f4f4f5; + background-color: #334155; + color: #f8fafc; } .btnSave { - padding: 0.5rem 1rem; + padding: 0.5rem 1.25rem; font-size: 0.875rem; border-radius: 0.5rem; border: none; background-color: #2563eb; color: #ffffff; - font-weight: 500; + font-weight: 600; cursor: pointer; transition: background-color 0.2s; + box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3); } .btnSave:hover { background-color: #1d4ed8; } - -@media (prefers-color-scheme: dark) { - .btnCancel { - border-color: #3f3f46; - color: #e4e4e7; - } - .btnCancel:hover { - background-color: #27272a; - } -} diff --git a/src/lib/components/TaskFilterToolbar.module.css b/src/lib/components/TaskFilterToolbar.module.css index 3ec94ff..b2f227f 100644 --- a/src/lib/components/TaskFilterToolbar.module.css +++ b/src/lib/components/TaskFilterToolbar.module.css @@ -2,46 +2,32 @@ display: flex; align-items: center; justify-content: space-between; - background-color: #ffffff; - border: 1px solid #e4e4e7; - padding: 1rem; - border-radius: 0.75rem; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -@media (prefers-color-scheme: dark) { - .toolbar { - background-color: #18181b; - border-color: #27272a; - } + background-color: #1e293b; + border: 1px solid #334155; + padding: 1rem 1.25rem; + border-radius: 0.875rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .label { font-size: 0.875rem; - font-weight: 500; - color: #3f3f46; -} - -@media (prefers-color-scheme: dark) { - .label { - color: #d4d4d8; - } + font-weight: 600; + color: #94a3b8; } .select { - padding: 0.375rem 0.75rem; + padding: 0.5rem 0.875rem; font-size: 0.875rem; border-radius: 0.5rem; - border: 1px solid #d4d4d8; - background-color: #fafafa; - color: #18181b; + border: 1px solid #334155; + background-color: #0f172a; + color: #60a5fa; + font-weight: 600; outline: none; + cursor: pointer; + transition: border-color 0.2s; } -@media (prefers-color-scheme: dark) { - .select { - border-color: #3f3f46; - background-color: #27272a; - color: #f4f4f5; - } +.select:focus { + border-color: #3b82f6; } diff --git a/src/lib/components/TaskForm.module.css b/src/lib/components/TaskForm.module.css index 3cb330b..d5586c1 100644 --- a/src/lib/components/TaskForm.module.css +++ b/src/lib/components/TaskForm.module.css @@ -1,35 +1,22 @@ -/* Creation Form Container */ +/* Dark Sleek Form Card */ .formCard { - background-color: #ffffff; - border: 1px solid #e4e4e7; - border-radius: 0.75rem; - padding: 1.25rem; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + background-color: #1e293b; + border: 1px solid #334155; + border-radius: 0.875rem; + padding: 1.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; - gap: 1rem; -} - -@media (prefers-color-scheme: dark) { - .formCard { - background-color: #18181b; - border-color: #27272a; - } + gap: 1.25rem; } .heading { - font-size: 1.125rem; - font-weight: 600; - color: #18181b; + font-size: 1.25rem; + font-weight: 700; + color: #f8fafc; margin: 0; } -@media (prefers-color-scheme: dark) { - .heading { - color: #f4f4f5; - } -} - .gridTwo { display: grid; grid-template-columns: 1fr; @@ -49,47 +36,28 @@ .label { display: block; - font-size: 0.75rem; - font-weight: 500; - color: #52525b; - margin-bottom: 0.25rem; -} - -@media (prefers-color-scheme: dark) { - .label { - color: #a1a1aa; - } + font-size: 0.8125rem; + font-weight: 600; + color: #cbd5e1; + margin-bottom: 0.375rem; } .input, .select, .textarea { width: 100%; - padding: 0.5rem 0.75rem; + padding: 0.625rem 0.875rem; font-size: 0.875rem; border-radius: 0.5rem; - border: 1px solid #d4d4d8; - background-color: transparent; - color: #18181b; + border: 1px solid #334155; + background-color: #0f172a; + color: #f8fafc; outline: none; box-sizing: border-box; -} - -.select { - background-color: #fafafa; + transition: border-color 0.2s, box-shadow 0.2s; } .input:focus, .select:focus, .textarea:focus { border-color: #3b82f6; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); -} - -@media (prefers-color-scheme: dark) { - .input, .select, .textarea { - border-color: #3f3f46; - color: #f4f4f5; - } - .select { - background-color: #27272a; - } + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); } .actions { @@ -98,18 +66,19 @@ } .btnSubmit { - padding: 0.5rem 1rem; + padding: 0.625rem 1.25rem; background-color: #2563eb; color: #ffffff; border-radius: 0.5rem; font-size: 0.875rem; - font-weight: 500; + font-weight: 600; border: none; cursor: pointer; - transition: background-color 0.2s; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + transition: background-color 0.2s, transform 0.1s; + box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3); } .btnSubmit:hover { background-color: #1d4ed8; + transform: translateY(-1px); } diff --git a/src/lib/tasks.ts b/src/lib/tasks.ts index bee4daf..5ffc871 100644 --- a/src/lib/tasks.ts +++ b/src/lib/tasks.ts @@ -66,3 +66,18 @@ export function updateTask( export function archiveTask(id: number) { return db.prepare(`UPDATE tasks SET is_archived = 1 WHERE id = ?`).run(id); } + +export function getArchivedTasks(): Task[] { + const sql = `SELECT * FROM tasks WHERE is_archived = 1 ORDER BY created_at DESC`; + const tasks = db.prepare(sql).all() as Task[]; + + return tasks.map(task => ({ + ...task, + is_overdue: computeIsOverdue(task.due_date, task.status) + })); +} + +export function unarchiveTask(id: number) { + return db.prepare(`UPDATE tasks SET is_archived = 0 WHERE id = ?`).run(id); +} +