Update:added documentation folder and added all documentation required

This commit is contained in:
mahlatseclayton
2026-07-30 18:51:18 +02:00
parent cb5acb20a0
commit 6641ea66cb
17 changed files with 742 additions and 419 deletions

116
README.md
View File

@@ -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 ## Project Description
npm run dev
# or 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.
yarn dev
# or ---
pnpm dev
# or ## Architectural Choices & Key Decisions
bun dev
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. ### Requirements
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - 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 <PORT_NUMBER>`.
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.

View File

@@ -1,8 +1,8 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: #ffffff; --background: #f8fafc;
--foreground: #171717; --foreground: #0f172a;
} }
@theme inline { @theme inline {
@@ -12,38 +12,10 @@
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
} }
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} margin: 0;
padding: 0;
.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;
}
} }

View File

@@ -1,21 +1,17 @@
/* Container & Page Layout */ /* Light Canvas Page Layout */
.mainContainer { .mainContainer {
max-width: 56rem; max-width: 56rem;
/* 896px */ margin: 0 auto;
margin-left: auto;
margin-right: auto;
padding: 2.5rem 1rem; padding: 2.5rem 1rem;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 2rem;
background-color: #ffffff; background-color: #f8fafc;
color: #000000; color: #0f172a;
} }
/* Header & Royal Blue Titles */
/* Header Section */
.header { .header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -23,31 +19,20 @@
} }
.title { .title {
font-size: 1.875rem; font-size: 2.25rem;
line-height: 2.25rem; line-height: 2.5rem;
font-weight: 700; font-weight: 800;
letter-spacing: -0.025em; letter-spacing: -0.025em;
color: #0000df; color: #1d4ed8;
}
@media (prefers-color-scheme: dark) {
.title {
color: #0808ee;
}
} }
.subtitle { .subtitle {
font-size: 0.875rem; font-size: 0.95rem;
color: #04045f; color: #475569;
font-weight: 500;
} }
@media (prefers-color-scheme: dark) { /* Section Title */
.subtitle {
color: #15154b;
}
}
/* Task List Section */
.section { .section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -55,46 +40,29 @@
} }
.sectionTitle { .sectionTitle {
font-size: 1.125rem; font-size: 1.25rem;
font-weight: 600; font-weight: 700;
color: #5a5ab4; color: #1e40af;
}
@media (prefers-color-scheme: dark) {
.sectionTitle {
color: #9494d6;
}
} }
/* Empty State Card */ /* Empty State Card */
.emptyState { .emptyState {
text-align: center; text-align: center;
padding: 3rem 1rem; padding: 3.5rem 1rem;
border: 1px dashed #4141aa; border: 2px dashed #cbd5e1;
border-radius: 0.75rem; border-radius: 0.75rem;
} background-color: #ffffff;
@media (prefers-color-scheme: dark) {
.emptyState {
border-color: #27272a;
}
} }
.emptyText { .emptyText {
color: #71717a; color: #64748b;
font-weight: 500;
} }
@media (prefers-color-scheme: dark) {
.emptyText {
color: #a1a1aa;
}
}
/* Grid Layout for Task Cards */
.taskGrid { .taskGrid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1rem; gap: 1.25rem;
} }
@media (min-width: 768px) { @media (min-width: 768px) {

View File

@@ -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 TaskForm from '@/src/lib/components/TaskForm';
import TaskCard from '@/src/lib/components/TaskCard'; import TaskCard from '@/src/lib/components/TaskCard';
import TaskFilterToolbar from '@/src/lib/components/TaskFilterToolbar'; import TaskFilterToolbar from '@/src/lib/components/TaskFilterToolbar';
import ArchivedSection from '@/src/lib/components/ArchivedSection';
import styles from './page.module.css'; import styles from './page.module.css';
interface PageProps { interface PageProps {
@@ -12,6 +13,7 @@ export default async function HomePage({ searchParams }: PageProps) {
const params = await searchParams; const params = await searchParams;
const sortBy = params.sortBy || 'due_date'; const sortBy = params.sortBy || 'due_date';
const tasks = getTasks(sortBy); const tasks = getTasks(sortBy);
const archivedTasks = getArchivedTasks();
return ( return (
<main className={styles.mainContainer}> <main className={styles.mainContainer}>
@@ -29,10 +31,13 @@ export default async function HomePage({ searchParams }: PageProps) {
{/* Task Sorting Toolbar */} {/* Task Sorting Toolbar */}
<TaskFilterToolbar /> <TaskFilterToolbar />
{/* List of Tasks */} {/* Archived Tasks Trigger Button */}
<ArchivedSection archivedTasks={archivedTasks} />
{/* List of Active Tasks */}
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}> <h2 className={styles.sectionTitle}>
Tasks ({tasks.length}) Active Tasks ({tasks.length})
</h2> </h2>
{/* Empty state if no tasks exist */} {/* Empty state if no tasks exist */}

26
docs/AI_TRANSPARENCY.md Normal file
View File

@@ -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.

37
docs/DATABASE_DESIGN.md Normal file
View File

@@ -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.

56
docs/RELATIONSHIPS.md Normal file
View File

@@ -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 {
<<enumeration>>
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.

View File

@@ -1,5 +1,5 @@
'use server'; 'use server';
import { createTask, updateTask, archiveTask } from './tasks'; import { createTask, updateTask, archiveTask, unarchiveTask } from './tasks';
import { TaskStatus } from './types'; import { TaskStatus } from './types';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
@@ -25,7 +25,7 @@ export async function updateTaskAction(id: number, formData: FormData) {
const title = formData.get('title') as string; const title = formData.get('title') as string;
const description = formData.get('description') as string; const description = formData.get('description') as string;
const due_date = formData.get('due_date') as string; const due_date = formData.get('due_date') as string;
const status = (formData.get('status') as TaskStatus) || 'To-Do'; const status = (formData.get('status') as TaskStatus) || 'Todo';
const topic = formData.get('topic') as string; const topic = formData.get('topic') as string;
if (!title || !description || !topic || !due_date) { if (!title || !description || !topic || !due_date) {
throw new Error('All fields are required'); throw new Error('All fields are required');
@@ -42,3 +42,11 @@ export async function archiveTaskAction(id: number) {
revalidatePath('/archived'); revalidatePath('/archived');
return { success: true }; return { success: true };
} }
export async function unarchiveTaskAction(id: number) {
unarchiveTask(id);
revalidatePath('/tasks');
revalidatePath('/');
revalidatePath('/archived');
return { success: true };
}

View File

@@ -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;
}

View File

@@ -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 (
<div className={styles.container}>
<button onClick={() => setIsOpen(true)} className={styles.btnArchived}>
📦 View Archived Tasks ({archivedTasks.length})
</button>
{isOpen && (
<ArchivedTasksModal
archivedTasks={archivedTasks}
onClose={() => setIsOpen(false)}
/>
)}
</div>
);
}

View File

@@ -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;
}

View File

@@ -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<number | null>(null);
const handleUnarchive = async (id: number) => {
setRestoringId(id);
try {
await unarchiveTaskAction(id);
} catch (err) {
alert('Failed to restore task');
} finally {
setRestoringId(null);
}
};
return (
<div className={styles.overlay}>
<div className={styles.dialog}>
<div className={styles.header}>
<h2 className={styles.title}>📦 Archived Tasks ({archivedTasks.length})</h2>
<button onClick={onClose} className={styles.btnClose}></button>
</div>
<div className={styles.content}>
{archivedTasks.length === 0 ? (
<p className={styles.emptyText}>No archived tasks currently stored.</p>
) : (
<div className={styles.taskList}>
{archivedTasks.map(task => (
<div key={task.id} className={styles.taskItem}>
<div className={styles.taskInfo}>
<span className={styles.taskTitle}>{task.title}</span>
<span className={styles.taskTopic}>#{task.topic} Due: {task.due_date}</span>
</div>
<button
onClick={() => handleUnarchive(task.id)}
disabled={restoringId === task.id}
className={styles.btnUnarchive}
>
{restoringId === task.id ? 'Restoring...' : '↩ Unarchive'}
</button>
</div>
))}
</div>
)}
</div>
<div className={styles.footer}>
<button onClick={onClose} className={styles.btnCloseBtn}>Close</button>
</div>
</div>
</div>
);
}

View File

@@ -1,164 +1,162 @@
/* Card Container */ /* Task Card Container - Matching Form UI */
.cardContainer { .cardContainer {
padding: 1.25rem; padding: 1.25rem;
border-radius: 0.75rem; border-radius: 0.875rem;
border: 1px solid #c6c6cc; border: 1px solid #334155;
background-color: #e6e1e1; background-color: #1e293b;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease-in-out; 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 { .archiving {
opacity: 0.5; opacity: 0.4;
} }
/* Header Section */ /* Header Section */
.header { .header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 0.75rem;
} }
.title { .title {
font-weight: 600; font-weight: 700;
font-size: 1.125rem; font-size: 1.125rem;
color: #0303e0; color: #f8fafc;
margin: 0; margin: 0;
line-height: 1.4;
} }
/* Unified Status Badges with Subtle Tints */
/* Status Badges */
.badge { .badge {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600;
padding: 0.25rem 0.625rem; padding: 0.25rem 0.625rem;
border-radius: 9999px; border-radius: 9999px;
font-weight: 500; text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
} }
.statusComplete { .statusComplete {
background-color: #d1fae5; background-color: rgba(16, 185, 129, 0.12);
color: #02c28c; color: #34d399;
border: 1px solid rgba(52, 211, 153, 0.25);
} }
.statusInProgress { .statusInProgress {
background-color: #fef3c7; background-color: rgba(245, 158, 11, 0.12);
color: #884216; color: #fbbf24;
border: 1px solid rgba(251, 191, 36, 0.25);
} }
.statusTodo { .statusTodo {
background-color: #fcfdff; background-color: rgba(59, 130, 246, 0.12);
color: #4570ff; color: #60a5fa;
border: 1px solid rgba(96, 165, 250, 0.25);
} }
/* Description Text */
/* Description */
.description { .description {
margin-top: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
color: #52525b; color: #94a3b8;
display: -webkit-box; margin: 0;
-webkit-line-clamp: 2; line-height: 1.5;
-webkit-box-orient: vertical;
overflow: hidden;
} }
/* Footer Section */ /* Footer Section */
.footer { .footer {
margin-top: 1rem;
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; border-top: 1px solid #334155;
font-size: 0.75rem;
color: #71717a;
border-top: 1px solid #f4f4f5;
padding-top: 0.75rem; padding-top: 0.75rem;
color: #94a3b8;
font-size: 0.8125rem;
} }
.metaGroup { .metaGroup {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.75rem;
} }
.topicTag { .topicTag {
background-color: #f4f4f5; background-color: #0f172a;
color: #93c5fd;
font-weight: 600;
padding: 0.125rem 0.5rem; padding: 0.125rem 0.5rem;
border-radius: 0.25rem; border-radius: 0.375rem;
font-family: monospace; border: 1px solid #334155;
color: #3f3f46;
} }
/* Overdue Badge */
.overdueBadge { .overdueBadge {
background-color: #fee2e2; background-color: rgba(239, 68, 68, 0.15);
color: #b91c1c; color: #f87171;
padding: 0.125rem 0.5rem; border: 1px solid rgba(248, 113, 113, 0.3);
border-radius: 15px; padding: 0.15rem 0.5rem;
font-weight: 600; border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; }
0%, 50% { opacity: 0.5; }
100% {
opacity: 1;
}
50% {
opacity: .5;
}
} }
/* Actions Section */ /* Action Buttons */
.actions { .actions {
margin-top: 1rem;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 0.5rem; gap: 0.5rem;
font-size: 0.75rem; margin-top: 0.25rem;
} }
.btnEdit { .btnEdit {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.875rem;
border-radius: 0.25rem; border-radius: 0.5rem;
background-color: #f4f4f5; background-color: #334155;
color: #27272a; color: #f8fafc;
font-weight: 500; font-size: 0.8125rem;
border: none; font-weight: 600;
border: 1px solid #475569;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: all 0.2s ease;
} }
.btnEdit:hover { .btnEdit:hover {
background-color: #e4e4e7; background-color: #2563eb;
border-color: #2563eb;
color: #ffffff;
} }
.btnArchive { .btnArchive {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.875rem;
border-radius: 0.25rem; border-radius: 0.5rem;
background-color: #fef2f2; background-color: #334155;
color: #96a2e4; color: #f87171;
font-weight: 500; font-size: 0.8125rem;
border: none; font-weight: 600;
border: 1px solid #475569;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: all 0.2s ease;
} }
.btnArchive:hover { .btnArchive:hover {
background-color: #f18484; background-color: #ef4444;
border-color: #ef4444;
color: #ffffff;
} }
.btnArchive:disabled { .btnArchive:disabled {

View File

@@ -6,27 +6,20 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(15, 23, 42, 0.7);
padding: 1rem; padding: 1rem;
backdrop-filter: blur(4px); backdrop-filter: blur(6px);
} }
/* Modal Dialog Box */ /* Modal Dialog Box */
.dialog { .dialog {
width: 100%; width: 100%;
max-width: 32rem; max-width: 32rem;
border-radius: 0.75rem; border-radius: 0.875rem;
background-color: #ffffff; background-color: #1e293b;
border: 1px solid #e4e4e7; border: 1px solid #334155;
padding: 1.5rem; padding: 1.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
@media (prefers-color-scheme: dark) {
.dialog {
background-color: #18181b;
border-color: #27272a;
}
} }
/* Modal Header */ /* Modal Header */
@@ -34,13 +27,13 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1.25rem;
} }
.title { .title {
font-size: 1.125rem; font-size: 1.25rem;
font-weight: 600; font-weight: 700;
color: #18181b; color: #f8fafc;
margin: 0; margin: 0;
} }
@@ -48,24 +41,13 @@
background: none; background: none;
border: none; border: none;
font-size: 1.25rem; font-size: 1.25rem;
color: #71717a; color: #94a3b8;
cursor: pointer; cursor: pointer;
transition: color 0.2s;
} }
.btnClose:hover { .btnClose:hover {
color: #27272a; color: #f8fafc;
}
@media (prefers-color-scheme: dark) {
.title {
color: #f4f4f5;
}
.btnClose {
color: #a1a1aa;
}
.btnClose:hover {
color: #f4f4f5;
}
} }
/* Form Layout */ /* Form Layout */
@@ -88,54 +70,35 @@
.label { .label {
display: block; display: block;
font-size: 0.75rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 600;
color: #52525b; color: #cbd5e1;
margin-bottom: 0.25rem; margin-bottom: 0.375rem;
}
@media (prefers-color-scheme: dark) {
.label {
color: #a1a1aa;
}
} }
.input, .select, .textarea { .input, .select, .textarea {
width: 100%; width: 100%;
padding: 0.5rem 0.75rem; padding: 0.625rem 0.875rem;
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid #d4d4d8; border: 1px solid #334155;
background-color: transparent; background-color: #0f172a;
color: #18181b; color: #f8fafc;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
} transition: border-color 0.2s, box-shadow 0.2s;
.select {
background-color: #fafafa;
} }
.input:focus, .select:focus, .textarea:focus { .input:focus, .select:focus, .textarea:focus {
border-color: #3b82f6; border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
@media (prefers-color-scheme: dark) {
.input, .select, .textarea {
border-color: #3f3f46;
color: #f4f4f5;
}
.select {
background-color: #27272a;
}
} }
/* Footer Buttons */ /* Footer Buttons */
.footerActions { .footerActions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 0.5rem; gap: 0.75rem;
padding-top: 0.5rem; padding-top: 0.5rem;
} }
@@ -143,38 +106,32 @@
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid #d4d4d8; border: 1px solid #334155;
background: transparent; background: #0f172a;
color: #27272a; color: #cbd5e1;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s;
} }
.btnCancel:hover { .btnCancel:hover {
background-color: #f4f4f5; background-color: #334155;
color: #f8fafc;
} }
.btnSave { .btnSave {
padding: 0.5rem 1rem; padding: 0.5rem 1.25rem;
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
background-color: #2563eb; background-color: #2563eb;
color: #ffffff; color: #ffffff;
font-weight: 500; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3);
} }
.btnSave:hover { .btnSave:hover {
background-color: #1d4ed8; background-color: #1d4ed8;
} }
@media (prefers-color-scheme: dark) {
.btnCancel {
border-color: #3f3f46;
color: #e4e4e7;
}
.btnCancel:hover {
background-color: #27272a;
}
}

View File

@@ -2,46 +2,32 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: #ffffff; background-color: #1e293b;
border: 1px solid #e4e4e7; border: 1px solid #334155;
padding: 1rem; padding: 1rem 1.25rem;
border-radius: 0.75rem; border-radius: 0.875rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
@media (prefers-color-scheme: dark) {
.toolbar {
background-color: #18181b;
border-color: #27272a;
}
} }
.label { .label {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 600;
color: #3f3f46; color: #94a3b8;
}
@media (prefers-color-scheme: dark) {
.label {
color: #d4d4d8;
}
} }
.select { .select {
padding: 0.375rem 0.75rem; padding: 0.5rem 0.875rem;
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid #d4d4d8; border: 1px solid #334155;
background-color: #fafafa; background-color: #0f172a;
color: #18181b; color: #60a5fa;
font-weight: 600;
outline: none; outline: none;
cursor: pointer;
transition: border-color 0.2s;
} }
@media (prefers-color-scheme: dark) { .select:focus {
.select { border-color: #3b82f6;
border-color: #3f3f46;
background-color: #27272a;
color: #f4f4f5;
}
} }

View File

@@ -1,35 +1,22 @@
/* Creation Form Container */ /* Dark Sleek Form Card */
.formCard { .formCard {
background-color: #ffffff; background-color: #1e293b;
border: 1px solid #e4e4e7; border: 1px solid #334155;
border-radius: 0.75rem; border-radius: 0.875rem;
padding: 1.25rem; padding: 1.5rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1.25rem;
}
@media (prefers-color-scheme: dark) {
.formCard {
background-color: #18181b;
border-color: #27272a;
}
} }
.heading { .heading {
font-size: 1.125rem; font-size: 1.25rem;
font-weight: 600; font-weight: 700;
color: #18181b; color: #f8fafc;
margin: 0; margin: 0;
} }
@media (prefers-color-scheme: dark) {
.heading {
color: #f4f4f5;
}
}
.gridTwo { .gridTwo {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -49,47 +36,28 @@
.label { .label {
display: block; display: block;
font-size: 0.75rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 600;
color: #52525b; color: #cbd5e1;
margin-bottom: 0.25rem; margin-bottom: 0.375rem;
}
@media (prefers-color-scheme: dark) {
.label {
color: #a1a1aa;
}
} }
.input, .select, .textarea { .input, .select, .textarea {
width: 100%; width: 100%;
padding: 0.5rem 0.75rem; padding: 0.625rem 0.875rem;
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid #d4d4d8; border: 1px solid #334155;
background-color: transparent; background-color: #0f172a;
color: #18181b; color: #f8fafc;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
} transition: border-color 0.2s, box-shadow 0.2s;
.select {
background-color: #fafafa;
} }
.input:focus, .select:focus, .textarea:focus { .input:focus, .select:focus, .textarea:focus {
border-color: #3b82f6; border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
@media (prefers-color-scheme: dark) {
.input, .select, .textarea {
border-color: #3f3f46;
color: #f4f4f5;
}
.select {
background-color: #27272a;
}
} }
.actions { .actions {
@@ -98,18 +66,19 @@
} }
.btnSubmit { .btnSubmit {
padding: 0.5rem 1rem; padding: 0.625rem 1.25rem;
background-color: #2563eb; background-color: #2563eb;
color: #ffffff; color: #ffffff;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 600;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s, transform 0.1s;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3);
} }
.btnSubmit:hover { .btnSubmit:hover {
background-color: #1d4ed8; background-color: #1d4ed8;
transform: translateY(-1px);
} }

View File

@@ -66,3 +66,18 @@ export function updateTask(
export function archiveTask(id: number) { export function archiveTask(id: number) {
return db.prepare(`UPDATE tasks SET is_archived = 1 WHERE id = ?`).run(id); 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);
}