Update:added documentation folder and added all documentation required
This commit is contained in:
116
README.md
116
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
|
## 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.
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
11
app/page.tsx
11
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 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
26
docs/AI_TRANSPARENCY.md
Normal 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
37
docs/DATABASE_DESIGN.md
Normal 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
56
docs/RELATIONSHIPS.md
Normal 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.
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
22
src/lib/components/ArchivedSection.module.css
Normal file
22
src/lib/components/ArchivedSection.module.css
Normal 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;
|
||||||
|
}
|
||||||
29
src/lib/components/ArchivedSection.tsx
Normal file
29
src/lib/components/ArchivedSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
138
src/lib/components/ArchivedTasksModal.module.css
Normal file
138
src/lib/components/ArchivedTasksModal.module.css
Normal 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;
|
||||||
|
}
|
||||||
65
src/lib/components/ArchivedTasksModal.tsx
Normal file
65
src/lib/components/ArchivedTasksModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user