From a83dcb55673ace39d25b47fc724f365c812659b4 Mon Sep 17 00:00:00 2001 From: mahlatseclayton Date: Thu, 30 Jul 2026 00:03:20 +0200 Subject: [PATCH] basic ui --- src/lib/components/TaskCard.module.css | 221 ++++++++++++++++++ src/lib/components/TaskCard.tsx | 155 ++++++------ src/lib/components/TaskEditModal.module.css | 180 ++++++++++++++ src/lib/components/TaskEditModal.tsx | 93 +++----- .../components/TaskFilterToolbar.module.css | 47 ++++ src/lib/components/TaskFilterToolbar.tsx | 26 +-- src/lib/components/TaskForm.module.css | 115 +++++++++ src/lib/components/TaskForm.tsx | 81 +++---- 8 files changed, 703 insertions(+), 215 deletions(-) create mode 100644 src/lib/components/TaskCard.module.css create mode 100644 src/lib/components/TaskEditModal.module.css create mode 100644 src/lib/components/TaskFilterToolbar.module.css create mode 100644 src/lib/components/TaskForm.module.css diff --git a/src/lib/components/TaskCard.module.css b/src/lib/components/TaskCard.module.css new file mode 100644 index 0000000..1981cd7 --- /dev/null +++ b/src/lib/components/TaskCard.module.css @@ -0,0 +1,221 @@ +/* Card Container */ +.cardContainer { + padding: 1.25rem; + border-radius: 0.75rem; + border: 1px solid #e4e4e7; + background-color: #ffffff; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + transition: all 0.2s ease-in-out; +} + +.archiving { + opacity: 0.5; +} + +@media (prefers-color-scheme: dark) { + .cardContainer { + background-color: #18181b; + border-color: #27272a; + } +} + +/* Header Section */ +.header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.title { + font-weight: 600; + font-size: 1.125rem; + color: #18181b; + margin: 0; +} + +@media (prefers-color-scheme: dark) { + .title { + color: #f4f4f5; + } +} + +/* Status Badges */ +.badge { + font-size: 0.75rem; + padding: 0.25rem 0.625rem; + border-radius: 9999px; + font-weight: 500; +} + +.statusComplete { + background-color: #d1fae5; + color: #065f46; +} + +.statusInProgress { + background-color: #fef3c7; + color: #92400e; +} + +.statusTodo { + background-color: #dbeafe; + color: #1e40af; +} + +@media (prefers-color-scheme: dark) { + .statusComplete { + background-color: #022c22; + color: #6ee7b7; + } + .statusInProgress { + background-color: #451a03; + color: #fcd34d; + } + .statusTodo { + background-color: #172554; + color: #93c5fd; + } +} + +/* Description */ +.description { + margin-top: 0.5rem; + font-size: 0.875rem; + color: #52525b; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +@media (prefers-color-scheme: dark) { + .description { + color: #a1a1aa; + } +} + +/* 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; + padding-top: 0.75rem; +} + +@media (prefers-color-scheme: dark) { + .footer { + border-color: #27272a; + color: #a1a1aa; + } +} + +.metaGroup { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.topicTag { + background-color: #f4f4f5; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + font-family: monospace; + color: #3f3f46; +} + +@media (prefers-color-scheme: dark) { + .topicTag { + background-color: #27272a; + color: #d4d4d8; + } +} + +/* Overdue Badge */ +.overdueBadge { + background-color: #fee2e2; + color: #b91c1c; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + font-weight: 600; + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@media (prefers-color-scheme: dark) { + .overdueBadge { + background-color: #450a0a; + color: #fca5a5; + } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: .5; } +} + +/* Actions Section */ +.actions { + margin-top: 1rem; + display: flex; + justify-content: flex-end; + gap: 0.5rem; + font-size: 0.75rem; +} + +.btnEdit { + padding: 0.375rem 0.75rem; + border-radius: 0.25rem; + background-color: #f4f4f5; + color: #27272a; + font-weight: 500; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.btnEdit:hover { + background-color: #e4e4e7; +} + +.btnArchive { + padding: 0.375rem 0.75rem; + border-radius: 0.25rem; + background-color: #fef2f2; + color: #dc2626; + font-weight: 500; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.btnArchive:hover { + background-color: #fee2e2; +} + +.btnArchive:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +@media (prefers-color-scheme: dark) { + .btnEdit { + background-color: #27272a; + color: #e4e4e7; + } + .btnEdit:hover { + background-color: #3f3f46; + } + .btnArchive { + background-color: rgba(69, 10, 10, 0.5); + color: #f87171; + } + .btnArchive:hover { + background-color: rgba(127, 29, 29, 0.5); + } +} diff --git a/src/lib/components/TaskCard.tsx b/src/lib/components/TaskCard.tsx index 4ee32e3..ef9df13 100644 --- a/src/lib/components/TaskCard.tsx +++ b/src/lib/components/TaskCard.tsx @@ -13,96 +13,89 @@ import { archiveTaskAction } from '../actions'; // Import the pop-up modal component used to edit tasks import TaskEditModal from './TaskEditModal'; +// Import CSS Modules styling +import styles from './TaskCard.module.css'; + // Define what props (inputs) this component expects (it needs 1 Task object) interface TaskCardProps { - task: Task; + task: Task; } export default function TaskCard({ task }: TaskCardProps) { - // Local state: tracks if the edit modal pop-up is open (true or false) - const [isEditing, setIsEditing] = useState(false); + // Local state: tracks if the edit modal pop-up is open (true or false) + const [isEditing, setIsEditing] = useState(false); - // Local state: tracks if we are currently archiving (used to dim the card) - const [isArchiving, setIsArchiving] = useState(false); + // Local state: tracks if we are currently archiving (used to dim the card) + const [isArchiving, setIsArchiving] = useState(false); - // Function called when the user clicks the "Archive" button - const handleArchive = async () => { - // Ask the user to confirm before archiving - if (confirm(`Are you sure you want to archive "${task.title}"?`)) { - setIsArchiving(true); // Dim the card visually - await archiveTaskAction(task.id); // Call the server action to update SQLite - } - }; + // Function called when the user clicks the "Archive" button + const handleArchive = async () => { + // Ask the user to confirm before archiving + if (confirm(`Are you sure you want to archive "${task.title}"?`)) { + setIsArchiving(true); // Dim the card visually + await archiveTaskAction(task.id); // Call the server action to update SQLite + } + }; - return ( - // Outer card container div (dims opacity if isArchiving is true) -
- - {/* Header section: Title and Status badge side by side */} -
- {/* Task Title */} -

{task.title}

- - {/* Status Badge: Green for Complete, Yellow for In-Progress, Blue for Todo */} - - {task.status} - -
+ // Helper function to return status badge CSS class based on task.status + const getStatusClass = () => { + if (task.status === 'Complete') return styles.statusComplete; + if (task.status === 'In-Progress') return styles.statusInProgress; + return styles.statusTodo; + }; - {/* Task Description (only renders if a description was provided) */} - {task.description && ( -

{task.description}

- )} + return ( + // Outer card container div (dims opacity if isArchiving is true) +
- {/* Footer section: Topic tag, Due Date, and Dynamic OVERDUE badge */} -
-
- {/* Topic Badge */} - - #{task.topic} - - {/* Due Date display */} - Due: {task.due_date} + {/* Header section: Title and Status badge side by side */} +
+ {/* Task Title */} +

{task.title}

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

{task.description}

+ )} + + {/* Footer section: Topic tag, Due Date, and Dynamic OVERDUE badge */} +
+
+ {/* Topic Badge */} + #{task.topic} + {/* Due Date display */} + Due: {task.due_date} +
+ + {/* Dynamic Overdue Badge: Only renders if task.is_overdue is calculated as true */} + {task.is_overdue && ( + OVERDUE + )} +
+ + {/* Action buttons: Edit and Archive */} +
+ {/* Click to open the Edit Modal */} + + + {/* Click to archive the task */} + +
+ + {/* Edit Modal Pop-up: Only renders when isEditing === true */} + {isEditing && ( + setIsEditing(false)} /> + )}
- - {/* Dynamic Overdue Badge: Only renders if task.is_overdue is calculated as true */} - {task.is_overdue && ( - - ⚠️ OVERDUE - - )} -
- - {/* Action buttons: Edit and Archive */} -
- {/* Click to open the Edit Modal */} - - - {/* Click to archive the task */} - -
- - {/* Edit Modal Pop-up: Only renders when isEditing === true */} - {isEditing && ( - setIsEditing(false)} /> - )} -
- ); + ); } diff --git a/src/lib/components/TaskEditModal.module.css b/src/lib/components/TaskEditModal.module.css new file mode 100644 index 0000000..bcf10a9 --- /dev/null +++ b/src/lib/components/TaskEditModal.module.css @@ -0,0 +1,180 @@ +/* Modal Backdrop Overlay */ +.overlay { + position: fixed; + inset: 0; + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.5); + padding: 1rem; + backdrop-filter: blur(4px); +} + +/* Modal Dialog Box */ +.dialog { + width: 100%; + max-width: 32rem; + border-radius: 0.75rem; + background-color: #ffffff; + border: 1px solid #e4e4e7; + 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; + } +} + +/* Modal Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.title { + font-size: 1.125rem; + font-weight: 600; + color: #18181b; + margin: 0; +} + +.btnClose { + background: none; + border: none; + font-size: 1.25rem; + color: #71717a; + cursor: pointer; +} + +.btnClose:hover { + color: #27272a; +} + +@media (prefers-color-scheme: dark) { + .title { + color: #f4f4f5; + } + .btnClose { + color: #a1a1aa; + } + .btnClose:hover { + color: #f4f4f5; + } +} + +/* Form Layout */ +.form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.gridTwo { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.field { + display: flex; + flex-direction: column; +} + +.label { + display: block; + font-size: 0.75rem; + font-weight: 500; + color: #52525b; + margin-bottom: 0.25rem; +} + +@media (prefers-color-scheme: dark) { + .label { + color: #a1a1aa; + } +} + +.input, .select, .textarea { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + border-radius: 0.5rem; + border: 1px solid #d4d4d8; + background-color: transparent; + color: #18181b; + outline: none; + box-sizing: border-box; +} + +.select { + background-color: #fafafa; +} + +.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; + } +} + +/* Footer Buttons */ +.footerActions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + padding-top: 0.5rem; +} + +.btnCancel { + padding: 0.5rem 1rem; + font-size: 0.875rem; + border-radius: 0.5rem; + border: 1px solid #d4d4d8; + background: transparent; + color: #27272a; + cursor: pointer; +} + +.btnCancel:hover { + background-color: #f4f4f5; +} + +.btnSave { + padding: 0.5rem 1rem; + font-size: 0.875rem; + border-radius: 0.5rem; + border: none; + background-color: #2563eb; + color: #ffffff; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.btnSave:hover { + background-color: #1d4ed8; +} + +@media (prefers-color-scheme: dark) { + .btnCancel { + border-color: #3f3f46; + color: #e4e4e7; + } + .btnCancel:hover { + background-color: #27272a; + } +} diff --git a/src/lib/components/TaskEditModal.tsx b/src/lib/components/TaskEditModal.tsx index e5f1f80..9b65647 100644 --- a/src/lib/components/TaskEditModal.tsx +++ b/src/lib/components/TaskEditModal.tsx @@ -1,35 +1,26 @@ -// Client component directive for browser interactive pop-up modal 'use client'; -// Import state hook from React import { useState } from 'react'; - -// Import Task and TaskStatus types -import { Task, TaskStatus } from '../types'; - -// Import the update server action function +import { Task } from '../types'; import { updateTaskAction } from '../actions'; +import styles from './TaskEditModal.module.css'; -// Props needed for the modal: the task to edit, and an onClose function to close the pop-up interface TaskEditModalProps { task: Task; onClose: () => void; } export default function TaskEditModal({ task, onClose }: TaskEditModalProps) { - // State to track if saving is in progress (shows 'Saving...' on button) const [isSaving, setIsSaving] = useState(false); - // Form submit handler to update task details const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); // Stop normal page refresh + e.preventDefault(); setIsSaving(true); - const formData = new FormData(e.currentTarget); // Get data entered into form inputs + const formData = new FormData(e.currentTarget); try { - // Call the server action to update SQLite database using formData directly await updateTaskAction(task.id, formData); - onClose(); // Close modal upon successful update + onClose(); } catch (err) { alert('Failed to update task'); } finally { @@ -38,95 +29,71 @@ export default function TaskEditModal({ task, onClose }: TaskEditModalProps) { }; return ( - // Modal Overlay: dark background overlay that covers the entire screen -
- {/* Modal Dialog Box */} -
- - {/* Modal Header */} -
-

Edit Task #{task.id}

- {/* Close button (X) */} - +
+
+
+

Edit Task #{task.id}

+
- {/* Edit Form */} -
- {/* Title input (pre-populated with task.title) */} -
- + +
+
-
- {/* Topic input */} -
- +
+
+
- {/* Due date picker */} -
- +
+
- {/* Status dropdown */} -
- -
- {/* Description text area */} -
- +
+
- {/* Modal Action buttons */} -
- -
diff --git a/src/lib/components/TaskFilterToolbar.module.css b/src/lib/components/TaskFilterToolbar.module.css new file mode 100644 index 0000000..3ec94ff --- /dev/null +++ b/src/lib/components/TaskFilterToolbar.module.css @@ -0,0 +1,47 @@ +.toolbar { + 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; + } +} + +.label { + font-size: 0.875rem; + font-weight: 500; + color: #3f3f46; +} + +@media (prefers-color-scheme: dark) { + .label { + color: #d4d4d8; + } +} + +.select { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + border-radius: 0.5rem; + border: 1px solid #d4d4d8; + background-color: #fafafa; + color: #18181b; + outline: none; +} + +@media (prefers-color-scheme: dark) { + .select { + border-color: #3f3f46; + background-color: #27272a; + color: #f4f4f5; + } +} diff --git a/src/lib/components/TaskFilterToolbar.tsx b/src/lib/components/TaskFilterToolbar.tsx index 13acc16..791d69c 100644 --- a/src/lib/components/TaskFilterToolbar.tsx +++ b/src/lib/components/TaskFilterToolbar.tsx @@ -1,31 +1,23 @@ -// Client component directive for drop-down navigation 'use client'; -// Next.js navigation hooks to change URL search parameters (like ?sortBy=due_date) import { useRouter, useSearchParams } from 'next/navigation'; +import styles from './TaskFilterToolbar.module.css'; export default function TaskFilterToolbar() { - const router = useRouter(); // Hook to navigate/push new URL - const searchParams = useSearchParams(); // Hook to read current URL search query (?sortBy=...) - const currentSort = searchParams.get('sortBy') || 'due_date'; // Default sort is 'due_date' + const router = useRouter(); + const searchParams = useSearchParams(); + const currentSort = searchParams.get('sortBy') || 'due_date'; - // Called whenever user changes the dropdown selection const handleSortChange = (e: React.ChangeEvent) => { const params = new URLSearchParams(searchParams); - params.set('sortBy', e.target.value); // Set new sortBy query value - router.push(`/?${params.toString()}`); // Update browser URL, triggering server re-query + params.set('sortBy', e.target.value); + router.push(`/?${params.toString()}`); }; return ( -
- Sort Tasks By: - - {/* Dropdown for sorting options */} - diff --git a/src/lib/components/TaskForm.module.css b/src/lib/components/TaskForm.module.css new file mode 100644 index 0000000..3cb330b --- /dev/null +++ b/src/lib/components/TaskForm.module.css @@ -0,0 +1,115 @@ +/* Creation Form Container */ +.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); + display: flex; + flex-direction: column; + gap: 1rem; +} + +@media (prefers-color-scheme: dark) { + .formCard { + background-color: #18181b; + border-color: #27272a; + } +} + +.heading { + font-size: 1.125rem; + font-weight: 600; + color: #18181b; + margin: 0; +} + +@media (prefers-color-scheme: dark) { + .heading { + color: #f4f4f5; + } +} + +.gridTwo { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} + +@media (min-width: 768px) { + .gridTwo { + grid-template-columns: 1fr 1fr; + } +} + +.field { + display: flex; + flex-direction: column; +} + +.label { + display: block; + font-size: 0.75rem; + font-weight: 500; + color: #52525b; + margin-bottom: 0.25rem; +} + +@media (prefers-color-scheme: dark) { + .label { + color: #a1a1aa; + } +} + +.input, .select, .textarea { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + border-radius: 0.5rem; + border: 1px solid #d4d4d8; + background-color: transparent; + color: #18181b; + outline: none; + box-sizing: border-box; +} + +.select { + background-color: #fafafa; +} + +.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; + } +} + +.actions { + display: flex; + justify-content: flex-end; +} + +.btnSubmit { + padding: 0.5rem 1rem; + background-color: #2563eb; + color: #ffffff; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + border: none; + cursor: pointer; + transition: background-color 0.2s; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.btnSubmit:hover { + background-color: #1d4ed8; +} diff --git a/src/lib/components/TaskForm.tsx b/src/lib/components/TaskForm.tsx index 1b972e0..63eb270 100644 --- a/src/lib/components/TaskForm.tsx +++ b/src/lib/components/TaskForm.tsx @@ -1,24 +1,20 @@ -// Client component directive for form interactivity 'use client'; -// useRef gets direct reference to the DOM form element; useState manages submission state import { useRef, useState } from 'react'; - -// Import server action to create tasks in SQLite import { createTaskAction } from '../actions'; +import styles from './TaskForm.module.css'; export default function TaskForm() { - const formRef = useRef(null); // Used to reset input fields after submit - const [isSubmitting, setIsSubmitting] = useState(false); // Prevents multiple fast clicks + const formRef = useRef(null); + const [isSubmitting, setIsSubmitting] = useState(false); - // Form submit handler const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); // Prevents normal full-page browser reload + e.preventDefault(); setIsSubmitting(true); - const formData = new FormData(e.currentTarget); // Gathers values from inputs by their 'name' + const formData = new FormData(e.currentTarget); try { - await createTaskAction(formData); // Call server action to insert task into SQLite - formRef.current?.reset(); // Clear all form inputs + await createTaskAction(formData); + formRef.current?.reset(); } catch (err) { alert('Failed to create task'); } finally { @@ -27,57 +23,40 @@ export default function TaskForm() { }; return ( - -

Create New Task

+ +

Create New Task

-
- {/* Title Input */} -
- +
+
+
- {/* Topic Input */} -
- +
+
- {/* Due Date Input */} -
- - +
+ +
- {/* Status Selector */} -
- - @@ -85,24 +64,18 @@ export default function TaskForm() {
- {/* Optional Description */} -
- +
+
- {/* Submit Button */} -
-