This commit is contained in:
mahlatseclayton
2026-07-30 00:03:20 +02:00
parent ceed219c87
commit a83dcb5567
8 changed files with 703 additions and 215 deletions

View File

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

View File

@@ -13,6 +13,9 @@ import { archiveTaskAction } from '../actions';
// Import the pop-up modal component used to edit tasks // Import the pop-up modal component used to edit tasks
import TaskEditModal from './TaskEditModal'; 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) // Define what props (inputs) this component expects (it needs 1 Task object)
interface TaskCardProps { interface TaskCardProps {
task: Task; task: Task;
@@ -34,67 +37,57 @@ export default function TaskCard({ task }: TaskCardProps) {
} }
}; };
// 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;
};
return ( return (
// Outer card container div (dims opacity if isArchiving is true) // Outer card container div (dims opacity if isArchiving is true)
<div className={`p-5 rounded-xl border shadow-sm transition-all bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 ${isArchiving ? 'opacity-50' : ''}`}> <div className={`${styles.cardContainer} ${isArchiving ? styles.archiving : ''}`}>
{/* Header section: Title and Status badge side by side */} {/* Header section: Title and Status badge side by side */}
<div className="flex items-start justify-between gap-4"> <div className={styles.header}>
{/* Task Title */} {/* Task Title */}
<h3 className="font-semibold text-lg text-zinc-900 dark:text-zinc-100">{task.title}</h3> <h3 className={styles.title}>{task.title}</h3>
{/* Status Badge: Green for Complete, Yellow for In-Progress, Blue for Todo */} {/* Status Badge: Green for Complete, Yellow for In-Progress, Blue for Todo */}
<span className={`text-xs px-2.5 py-1 rounded-full font-medium ${ <span className={`${styles.badge} ${getStatusClass()}`}>
task.status === 'Complete'
? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300'
: task.status === 'In-Progress'
? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300'
: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300'
}`}>
{task.status} {task.status}
</span> </span>
</div> </div>
{/* Task Description (only renders if a description was provided) */} {/* Task Description (only renders if a description was provided) */}
{task.description && ( {task.description && (
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400 line-clamp-2">{task.description}</p> <p className={styles.description}>{task.description}</p>
)} )}
{/* Footer section: Topic tag, Due Date, and Dynamic OVERDUE badge */} {/* Footer section: Topic tag, Due Date, and Dynamic OVERDUE badge */}
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 text-xs text-zinc-500 dark:text-zinc-400 border-t border-zinc-100 dark:border-zinc-800 pt-3"> <div className={styles.footer}>
<div className="flex items-center gap-2"> <div className={styles.metaGroup}>
{/* Topic Badge */} {/* Topic Badge */}
<span className="bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 rounded font-mono text-zinc-700 dark:text-zinc-300"> <span className={styles.topicTag}>#{task.topic}</span>
#{task.topic}
</span>
{/* Due Date display */} {/* Due Date display */}
<span>Due: {task.due_date}</span> <span>Due: {task.due_date}</span>
</div> </div>
{/* Dynamic Overdue Badge: Only renders if task.is_overdue is calculated as true */} {/* Dynamic Overdue Badge: Only renders if task.is_overdue is calculated as true */}
{task.is_overdue && ( {task.is_overdue && (
<span className="bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300 px-2 py-0.5 rounded font-semibold animate-pulse"> <span className={styles.overdueBadge}>OVERDUE</span>
OVERDUE
</span>
)} )}
</div> </div>
{/* Action buttons: Edit and Archive */} {/* Action buttons: Edit and Archive */}
<div className="mt-4 flex justify-end gap-2 text-xs"> <div className={styles.actions}>
{/* Click to open the Edit Modal */} {/* Click to open the Edit Modal */}
<button <button onClick={() => setIsEditing(true)} className={styles.btnEdit}>
onClick={() => setIsEditing(true)}
className="px-3 py-1.5 rounded bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-zinc-800 dark:text-zinc-200 font-medium transition"
>
Edit Edit
</button> </button>
{/* Click to archive the task */} {/* Click to archive the task */}
<button <button onClick={handleArchive} disabled={isArchiving} className={styles.btnArchive}>
onClick={handleArchive}
disabled={isArchiving}
className="px-3 py-1.5 rounded bg-red-50 hover:bg-red-100 dark:bg-red-950/50 dark:hover:bg-red-900/50 text-red-600 dark:text-red-400 font-medium transition"
>
Archive Archive
</button> </button>
</div> </div>

View File

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

View File

@@ -1,35 +1,26 @@
// Client component directive for browser interactive pop-up modal
'use client'; 'use client';
// Import state hook from React
import { useState } from 'react'; import { useState } from 'react';
import { Task } from '../types';
// Import Task and TaskStatus types
import { Task, TaskStatus } from '../types';
// Import the update server action function
import { updateTaskAction } from '../actions'; 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 { interface TaskEditModalProps {
task: Task; task: Task;
onClose: () => void; onClose: () => void;
} }
export default function TaskEditModal({ task, onClose }: TaskEditModalProps) { export default function TaskEditModal({ task, onClose }: TaskEditModalProps) {
// State to track if saving is in progress (shows 'Saving...' on button)
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
// Form submit handler to update task details
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // Stop normal page refresh e.preventDefault();
setIsSaving(true); setIsSaving(true);
const formData = new FormData(e.currentTarget); // Get data entered into form inputs const formData = new FormData(e.currentTarget);
try { try {
// Call the server action to update SQLite database using formData directly
await updateTaskAction(task.id, formData); await updateTaskAction(task.id, formData);
onClose(); // Close modal upon successful update onClose();
} catch (err) { } catch (err) {
alert('Failed to update task'); alert('Failed to update task');
} finally { } finally {
@@ -38,95 +29,71 @@ export default function TaskEditModal({ task, onClose }: TaskEditModalProps) {
}; };
return ( return (
// Modal Overlay: dark background overlay that covers the entire screen <div className={styles.overlay}>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"> <div className={styles.dialog}>
{/* Modal Dialog Box */} <div className={styles.header}>
<div className="w-full max-w-lg rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-6 shadow-xl"> <h2 className={styles.title}>Edit Task #{task.id}</h2>
<button onClick={onClose} className={styles.btnClose}></button>
{/* Modal Header */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Edit Task #{task.id}</h2>
{/* Close button (X) */}
<button onClick={onClose} className="text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 text-lg"></button>
</div> </div>
{/* Edit Form */} <form onSubmit={handleSubmit} className={styles.form}>
<form onSubmit={handleSubmit} className="space-y-4"> <div className={styles.field}>
{/* Title input (pre-populated with task.title) */} <label className={styles.label}>Title *</label>
<div>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Title *</label>
<input <input
name="title" name="title"
defaultValue={task.title} defaultValue={task.title}
required required
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className={styles.input}
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className={styles.gridTwo}>
{/* Topic input */} <div className={styles.field}>
<div> <label className={styles.label}>Topic *</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Topic *</label>
<input <input
name="topic" name="topic"
defaultValue={task.topic} defaultValue={task.topic}
required required
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className={styles.input}
/> />
</div> </div>
{/* Due date picker */} <div className={styles.field}>
<div> <label className={styles.label}>Due Date *</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Due Date *</label>
<input <input
name="due_date" name="due_date"
type="date" type="date"
defaultValue={task.due_date} defaultValue={task.due_date}
required required
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className={styles.input}
/> />
</div> </div>
</div> </div>
{/* Status dropdown */} <div className={styles.field}>
<div> <label className={styles.label}>Status</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Status</label> <select name="status" defaultValue={task.status} className={styles.select}>
<select
name="status"
defaultValue={task.status}
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="Todo">Todo</option> <option value="Todo">Todo</option>
<option value="In-Progress">In-Progress</option> <option value="In-Progress">In-Progress</option>
<option value="Complete">Complete</option> <option value="Complete">Complete</option>
</select> </select>
</div> </div>
{/* Description text area */} <div className={styles.field}>
<div> <label className={styles.label}>Description</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Description</label>
<textarea <textarea
name="description" name="description"
defaultValue={task.description} defaultValue={task.description}
rows={3} rows={3}
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className={styles.textarea}
></textarea> ></textarea>
</div> </div>
{/* Modal Action buttons */} <div className={styles.footerActions}>
<div className="flex justify-end gap-2 pt-2"> <button type="button" onClick={onClose} className={styles.btnCancel}>
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 text-zinc-800 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition"
>
Cancel Cancel
</button> </button>
<button <button type="submit" disabled={isSaving} className={styles.btnSave}>
type="submit"
disabled={isSaving}
className="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 transition"
>
{isSaving ? 'Saving...' : 'Save Changes'} {isSaving ? 'Saving...' : 'Save Changes'}
</button> </button>
</div> </div>

View File

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

View File

@@ -1,31 +1,23 @@
// Client component directive for drop-down navigation
'use client'; 'use client';
// Next.js navigation hooks to change URL search parameters (like ?sortBy=due_date)
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import styles from './TaskFilterToolbar.module.css';
export default function TaskFilterToolbar() { export default function TaskFilterToolbar() {
const router = useRouter(); // Hook to navigate/push new URL const router = useRouter();
const searchParams = useSearchParams(); // Hook to read current URL search query (?sortBy=...) const searchParams = useSearchParams();
const currentSort = searchParams.get('sortBy') || 'due_date'; // Default sort is 'due_date' const currentSort = searchParams.get('sortBy') || 'due_date';
// Called whenever user changes the dropdown selection
const handleSortChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleSortChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
params.set('sortBy', e.target.value); // Set new sortBy query value params.set('sortBy', e.target.value);
router.push(`/?${params.toString()}`); // Update browser URL, triggering server re-query router.push(`/?${params.toString()}`);
}; };
return ( return (
<div className="flex items-center justify-between bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-4 rounded-xl shadow-sm"> <div className={styles.toolbar}>
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Sort Tasks By:</span> <span className={styles.label}>Sort Tasks By:</span>
<select value={currentSort} onChange={handleSortChange} className={styles.select}>
{/* Dropdown for sorting options */}
<select
value={currentSort}
onChange={handleSortChange}
className="px-3 py-1.5 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100"
>
<option value="due_date">Due Date</option> <option value="due_date">Due Date</option>
<option value="topic">Topic</option> <option value="topic">Topic</option>
<option value="status">Status</option> <option value="status">Status</option>

View File

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

View File

@@ -1,24 +1,20 @@
// Client component directive for form interactivity
'use client'; 'use client';
// useRef gets direct reference to the DOM form element; useState manages submission state
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
// Import server action to create tasks in SQLite
import { createTaskAction } from '../actions'; import { createTaskAction } from '../actions';
import styles from './TaskForm.module.css';
export default function TaskForm() { export default function TaskForm() {
const formRef = useRef<HTMLFormElement>(null); // Used to reset input fields after submit const formRef = useRef<HTMLFormElement>(null);
const [isSubmitting, setIsSubmitting] = useState(false); // Prevents multiple fast clicks const [isSubmitting, setIsSubmitting] = useState(false);
// Form submit handler
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // Prevents normal full-page browser reload e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
const formData = new FormData(e.currentTarget); // Gathers values from inputs by their 'name' const formData = new FormData(e.currentTarget);
try { try {
await createTaskAction(formData); // Call server action to insert task into SQLite await createTaskAction(formData);
formRef.current?.reset(); // Clear all form inputs formRef.current?.reset();
} catch (err) { } catch (err) {
alert('Failed to create task'); alert('Failed to create task');
} finally { } finally {
@@ -27,57 +23,40 @@ export default function TaskForm() {
}; };
return ( return (
<form <form ref={formRef} onSubmit={handleSubmit} className={styles.formCard}>
ref={formRef} <h2 className={styles.heading}>Create New Task</h2>
onSubmit={handleSubmit}
className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl p-5 shadow-sm space-y-4"
>
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Create New Task</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className={styles.gridTwo}>
{/* Title Input */} <div className={styles.field}>
<div> <label className={styles.label}>Title *</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Title *</label>
<input <input
name="title" name="title"
type="text" type="text"
required required
placeholder="e.g. Finish Lab 1 Report" placeholder="e.g. Finish Lab 1 Report"
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className={styles.input}
/> />
</div> </div>
{/* Topic Input */} <div className={styles.field}>
<div> <label className={styles.label}>Topic *</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Topic *</label>
<input <input
name="topic" name="topic"
type="text" type="text"
required required
placeholder="e.g. University / SDP" placeholder="e.g. University / SDP"
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className={styles.input}
/> />
</div> </div>
{/* Due Date Input */} <div className={styles.field}>
<div> <label className={styles.label}>Due Date *</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Due Date *</label> <input name="due_date" type="date" required className={styles.input} />
<input
name="due_date"
type="date"
required
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div> </div>
{/* Status Selector */} <div className={styles.field}>
<div> <label className={styles.label}>Status</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Status</label> <select name="status" defaultValue="Todo" className={styles.select}>
<select
name="status"
defaultValue="Todo"
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="Todo">Todo</option> <option value="Todo">Todo</option>
<option value="In-Progress">In-Progress</option> <option value="In-Progress">In-Progress</option>
<option value="Complete">Complete</option> <option value="Complete">Complete</option>
@@ -85,24 +64,18 @@ export default function TaskForm() {
</div> </div>
</div> </div>
{/* Optional Description */} <div className={styles.field}>
<div> <label className={styles.label}>Description</label>
<label className="block text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">Description</label>
<textarea <textarea
name="description" name="description"
rows={2} rows={2}
placeholder="Additional notes or details..." placeholder="Additional notes or details..."
className="w-full px-3 py-2 text-sm rounded-lg border border-zinc-300 dark:border-zinc-700 bg-transparent text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className={styles.textarea}
></textarea> ></textarea>
</div> </div>
{/* Submit Button */} <div className={styles.actions}>
<div className="flex justify-end"> <button type="submit" disabled={isSubmitting} className={styles.btnSubmit}>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition shadow"
>
{isSubmitting ? 'Adding Task...' : '+ Add Task'} {isSubmitting ? 'Adding Task...' : '+ Add Task'}
</button> </button>
</div> </div>