diff --git a/client/src/components/atoms/AddTaskButton.tsx b/client/src/components/atoms/AddTaskButton.tsx new file mode 100644 index 0000000..07d02d7 --- /dev/null +++ b/client/src/components/atoms/AddTaskButton.tsx @@ -0,0 +1,21 @@ +import { Button } from "@/components/atoms/Button"; +import { Plus } from "lucide-react"; + +interface AddTaskButtonProps { + onClick: () => void; + className?: string; +} + +export function AddTaskButton({ onClick, className }: AddTaskButtonProps) { + return ( + + ); +} diff --git a/client/src/components/atoms/TaskCard.tsx b/client/src/components/atoms/TaskCard.tsx new file mode 100644 index 0000000..48a9222 --- /dev/null +++ b/client/src/components/atoms/TaskCard.tsx @@ -0,0 +1,99 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Trash2 } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { Task } from "@/lib/store"; +import styles from "@/styles/TaskCard.module.scss"; + +interface TaskCardProps { + task: Task; + onClick: () => void; + onDelete: () => void; + onToggleComplete: () => void; + className?: string; +} + +export function TaskCard({ task, onClick, onDelete, onToggleComplete, className }: TaskCardProps) { + const formatDuration = (minutes: number) => { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0) { + return `${hours}h ${mins}m`; + } + return `${mins}m`; + }; + + const handleDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onDelete(); + }; + + const handleCheckboxClick = (checked: boolean) => { + onToggleComplete(); + }; + + const handleCardClick = () => { + // Card is always clickable, regardless of completion status + onClick(); + }; + + return ( + + +
+ e.stopPropagation()} + /> +
+

+ {task.title} +

+ {task.description && ( +

+ {task.description} +

+ )} +
+ {task.eta > 0 && ( + + ETA: {formatDuration(task.eta)} + + )} + {task.duration > 0 && ( + + Spent: {formatDuration(task.duration)} + + )} +
+
+
+ {!task.completed && ( + + )} +
+
+
+
+ ); +} diff --git a/client/src/components/molecules/TaskForm.tsx b/client/src/components/molecules/TaskForm.tsx new file mode 100644 index 0000000..c6630f8 --- /dev/null +++ b/client/src/components/molecules/TaskForm.tsx @@ -0,0 +1,123 @@ +import { useState, useEffect } from "react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/atoms/Button"; +import type { Task } from "@/lib/store"; +import styles from "@/styles/TaskForm.module.scss"; + +interface TaskFormProps { + task?: Task; + onSubmit: (task: Omit) => void; + onCancel: () => void; + className?: string; +} + +export function TaskForm({ task, onSubmit, onCancel, className }: TaskFormProps) { + const [title, setTitle] = useState(task?.title || ""); + const [description, setDescription] = useState(task?.description || ""); + const [notes, setNotes] = useState(task?.notes || ""); + const [eta, setEta] = useState(task?.eta || 0); + const [duration, setDuration] = useState(task?.duration || 0); + + useEffect(() => { + if (task) { + setTitle(task.title); + setDescription(task.description); + setNotes(task.notes); + setEta(task.eta); + setDuration(task.duration); + } + }, [task]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!title.trim()) return; + + onSubmit({ + title: title.trim(), + description: description.trim(), + notes: notes.trim(), + eta, + duration, + completed: task?.completed || false, + }); + }; + + return ( +
+
+
+ + setTitle(e.target.value)} + placeholder="Enter task title" + required + className={styles.input} + /> +
+ +
+ + setDescription(e.target.value)} + placeholder="Brief description of the task" + className={styles.input} + /> +
+ +
+ +