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
|
||||
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 <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";
|
||||
|
||||
: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;
|
||||
}
|
||||
@@ -1,21 +1,17 @@
|
||||
/* Container & Page Layout */
|
||||
/* Light Canvas Page Layout */
|
||||
.mainContainer {
|
||||
max-width: 56rem;
|
||||
/* 896px */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 1rem;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
background-color: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Header Section */
|
||||
/* Header & Royal Blue Titles */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -23,31 +19,20 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
font-weight: 700;
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
color: #0000df;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.title {
|
||||
color: #0808ee;
|
||||
}
|
||||
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;
|
||||
@@ -55,46 +40,29 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 3.5rem 1rem;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.emptyState {
|
||||
border-color: #27272a;
|
||||
}
|
||||
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;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
@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 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 (
|
||||
<main className={styles.mainContainer}>
|
||||
@@ -29,10 +31,13 @@ export default async function HomePage({ searchParams }: PageProps) {
|
||||
{/* Task Sorting Toolbar */}
|
||||
<TaskFilterToolbar />
|
||||
|
||||
{/* List of Tasks */}
|
||||
{/* Archived Tasks Trigger Button */}
|
||||
<ArchivedSection archivedTasks={archivedTasks} />
|
||||
|
||||
{/* List of Active Tasks */}
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
Tasks ({tasks.length})
|
||||
Active Tasks ({tasks.length})
|
||||
</h2>
|
||||
|
||||
{/* 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';
|
||||
import { createTask, updateTask, archiveTask } from './tasks';
|
||||
import { createTask, updateTask, archiveTask, unarchiveTask } from './tasks';
|
||||
import { TaskStatus } from './types';
|
||||
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 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 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');
|
||||
@@ -42,3 +42,11 @@ export async function archiveTaskAction(id: number) {
|
||||
revalidatePath('/archived');
|
||||
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 {
|
||||
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;
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user