No preview available
A clean, intuitive todo list application that helps users organize tasks with priorities, completion tracking, and persistent storage. Features a minimal interface focused on productivity.
'use client';
import { ReactElement, useState, useEffect } from 'react';
import { Task, Filter } from '@/lib/types';
import { loadTasks, saveTasks } from '@/lib/storage';
import TaskList from '@/components/TaskList';
import AddTaskForm from '@/components/AddTaskForm';
/**
* Main application page component that orchestrates the todo list interface
* Manages global task state and renders the task list with filters
*/
export default function Page(): ReactElement {
const [tasks, setTasks] = useState<Task[]>([]);
const [filter, setFilter] = useState<Filter>('all');
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const loadedTasks = loadTasks();
setTasks(loadedTasks);
setIsLoaded(true);
}, []);
useEffect(() => {
if (isLoaded) {
saveTasks(tasks);
}
}, [tasks, isLoaded]);
const addTask = (title: string, description: string, priority: 'low' | 'medium' | 'high') => {
const newTask: Task = {
id: crypto.randomUUID(),
title,
description,
priority,
completed: false,
createdAt: new Date().toISOString(),
};
setTasks((prevTasks) => [newTask, ...prevTasks]);
};
const toggleTask = (id: string) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.id === id ? { ...task, completed: !task.completed } : task
)
);
};
const deleteTask = (id: string) => {
setTasks((prevTasks) => prevTasks.filter((task) => task.id !== id));
};
const updateTask = (id: string, updates: Partial<Task>) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.id === id ? { ...task, ...updates } : task
)
);
};
const filteredTasks = tasks.filter((task) => {
if (filter === 'active') return !task.completed;
if (filter === 'completed') return task.completed;
return true;
});
const activeCount = tasks.filter((task) => !task.completed).length;
const completedCount = tasks.filter((task) => task.completed).length;
return (
<div className="min-h-screen bg-[var(--color-bg)] text-[var(--color-bg-text)]">
<div className="max-w-4xl mx-auto px-4 py-8 md:py-12">
<header className="mb-8 md:mb-12">
<h1 className="font-[var(--font-heading)] text-4xl md:text-5xl font-bold mb-2">
TaskFlow
</h1>
<p className="font-[var(--font-body)] text-lg text-[var(--color-surface-text)] opacity-75">
Organize your tasks with clarity and focus
</p>
</header>
<div className="mb-8">
<AddTaskForm onAdd={addTask} />
</div>
<div className="bg-[var(--color-surface)] text-[var(--color-surface-text)] rounded-[var(--radius-default)] shadow-sm p-6">
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
<div className="flex items-center gap-4">
<button
onClick={() => setFilter('all')}
className={`font-[var(--font-body)] text-sm font-medium px-3 py-1.5 rounded-[var(--radius-default)] transition-colors ${
filter === 'all'
? 'bg-[var(--color-primary)] text-[var(--color-primary-text)]'
: 'bg-[var(--color-secondary)] text-[var(--color-secondary-text)] hover:opacity-80'
}`}
>
All ({tasks.length})
</button>
<button
onClick={() => setFilter('active')}
className={`font-[var(--font-body)] text-sm font-medium px-3 py-1.5 rounded-[var(--radius-default)] transition-colors ${
filter === 'active'
? 'bg-[var(--color-primary)] text-[var(--color-primary-text)]'
: 'bg-[var(--color-secondary)] text-[var(--color-secondary-text)] hover:opacity-80'
}`}
>
Active ({activeCount})
</button>
<button
onClick={() => setFilter('completed')}
className={`font-[var(--font-body)] text-sm font-medium px-3 py-1.5 rounded-[var(--radius-default)] transition-colors ${
filter === 'completed'
? 'bg-[var(--color-primary)] text-[var(--color-primary-text)]'
: 'bg-[var(--color-secondary)] text-[var(--color-secondary-text)] hover:opacity-80'
}`}
>
Completed ({completedCount})
</button>
</div>
</div>
{filteredTasks.length === 0 ? (
<div className="text-center py-12">
<p className="font-[var(--font-body)] text-[var(--color-surface-text)] opacity-50">
{filter === 'all' && 'No tasks yet. Create your first task above.'}
{filter === 'active' && 'No active tasks. Great job!'}
{filter === 'completed' && 'No completed tasks yet.'}
</p>
</div>
) : (
<TaskList
tasks={filteredTasks}
onToggle={toggleTask}
onDelete={deleteTask}
onUpdate={updateTask}
/>
)}
</div>
<footer className="mt-8 text-center">
<p className="font-[var(--font-body)] text-sm text-[var(--color-surface-text)] opacity-50">
Built with focus and clarity
</p>
</footer>
</div>
</div>
);
}'use client';
import { ReactElement, useState } from 'react';
import { Task } from '@/lib/types';
interface TaskItemProps {
task: Task;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onUpdate: (id: string, updates: Partial<Task>) => void;
}
/**
* TaskItem component displays an individual task with checkbox,
* priority indicator, and action buttons (edit, delete)
*/
export default function TaskItem({
task,
onToggle,
onDelete,
onUpdate,
}: TaskItemProps): ReactElement {
const [isEditing, setIsEditing] = useState(false);
const [editTitle, setEditTitle] = useState(task.title);
const [editDescription, setEditDescription] = useState(task.description);
const [editPriority, setEditPriority] = useState<'low' | 'medium' | 'high'>(task.priority);
const handleSaveEdit = () => {
if (editTitle.trim()) {
onUpdate(task.id, {
title: editTitle.trim(),
description: editDescription.trim(),
priority: editPriority,
});
setIsEditing(false);
}
};
const handleCancelEdit = () => {
setEditTitle(task.title);
setEditDescription(task.description);
setEditPriority(task.priority);
setIsEditing(false);
};
const getPriorityColor = (priority: 'low' | 'medium' | 'high'): string => {
switch (priority) {
case 'high':
return 'bg-[var(--color-error)] text-[var(--color-error-text)]';
case 'medium':
return 'bg-[var(--color-warning)] text-[var(--color-warning-text)]';
case 'low':
return 'bg-[var(--color-info)] text-[var(--color-info-text)]';
default:
return 'bg-[var(--color-secondary)] text-[var(--color-secondary-text)]';
}
};
const getPriorityLabel = (priority: 'low' | 'medium' | 'high'): string => {
return priority.charAt(0).toUpperCase() + priority.slice(1);
};
if (isEditing) {
return (
<div className="bg-[var(--color-bg)] text-[var(--color-bg-text)] p-[calc(var(--spacing-unit)*4)] rounded-[var(--radius-default)] shadow-md border border-[var(--color-primary)]">
<div className="space-y-[calc(var(--spacing-unit)*3)]">
<div>
<label
htmlFor={`edit-title-${task.id}`}
className="block font-[var(--font-body)] text-sm font-medium mb-[calc(var(--spacing-unit)*2)]"
>
Task Title
</label>
<input
id={`edit-title-${task.id}`}
type="text"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
className="w-full font-[var(--font-body)] px-[calc(var(--spacing-unit)*3)] py-[calc(var(--spacing-unit)*2)] rounded-[var(--radius-default)] bg-[var(--color-surface)] text-[var(--color-surface-text)] border border-[var(--color-secondary)] focus:outline-none focus:border-[var(--color-primary)]"
placeholder="Enter task title"
/>
</div>
<div>
<label
htmlFor={`edit-description-${task.id}`}
className="block font-[var(--font-body)] text-sm font-medium mb-[calc(var(--spacing-unit)*2)]"
>
Description (optional)
</label>
<textarea
id={`edit-description-${task.id}`}
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
rows={3}
className="w-full font-[var(--font-body)] px-[calc(var(--spacing-unit)*3)] py-[calc(var(--spacing-unit)*2)] rounded-[var(--radius-default)] bg-[var(--color-surface)] text-[var(--color-surface-text)] border border-[var(--color-secondary)] focus:outline-none focus:border-[var(--color-primary)] resize-none"
placeholder="Enter task description"
/>
</div>
<div>
<label
htmlFor={`edit-priority-${task.id}`}
className="block font-[var(--font-body)] text-sm font-medium mb-[calc(var(--spacing-unit)*2)]"
>
Priority
</label>
<select
id={`edit-priority-${task.id}`}
value={editPriority}
onChange={(e) => setEditPriority(e.target.value as 'low' | 'medium' | 'high')}
className="w-full font-[var(--font-body)] px-[calc(var(--spacing-unit)*3)] py-[calc(var(--spacing-unit)*2)] rounded-[var(--radius-default)] bg-[var(--color-surface)] text-[var(--color-surface-text)] border border-[var(--color-secondary)] focus:outline-none focus:border-[var(--color-primary)]"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div className="flex items-center gap-[calc(var(--spacing-unit)*2)] pt-[calc(var(--spacing-unit)*2)]">
<button
onClick={handleSaveEdit}
disabled={!editTitle.trim()}
className="font-[var(--font-body)] text-sm font-medium px-[calc(var(--spacing-unit)*4)] py-[calc(var(--spacing-unit)*2)] rounded-[var(--radius-default)] bg-[var(--color-primary)] text-[var(--color-primary-text)] hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
Save
</button>
<button
onClick={handleCancelEdit}
className="font-[var(--font-body)] text-sm font-medium px-[calc(var(--spacing-unit)*4)] py-[calc(var(--spacing-unit)*2)] rounded-[var(--radius-default)] bg-[var(--color-secondary)] text-[var(--color-secondary-text)] hover:opacity-80 transition-opacity"
>
Cancel
</button>
</div>
</div>
</div>
);
}
return (
<div
className={`bg-[var(--color-bg)] text-[var(--color-bg-text)] p-[calc(var(--spacing-unit)*4)] rounded-[var(--radius-default)] shadow-sm border transition-all ${
task.completed
? 'border-[var(--color-success)] opacity-60'
: 'border-[var(--color-secondary)] hover:shadow-md'
}`}
>
<div className="flex items-start gap-[calc(var(--spacing-unit)*3)]">
<div className="flex-shrink-0 pt-[calc(var(--spacing-unit)*1)]">
<button
onClick={() => onToggle(task.id)}
className="w-5 h-5 rounded-[calc(var(--radius-default)/2)] border-2 flex items-center justify-center transition-all"
style={{
borderColor: task.completed ? 'var(--color-success)' : 'var(--color-secondary)',
backgroundColor: task.completed ? 'var(--color-success)' : 'transparent',
}}
aria-label={task.completed ? 'Mark as incomplete' : 'Mark as complete'}
>
{task.completed && (
<svg
className="w-3 h-3"
style={{ color: 'var(--color-success-text)' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-[calc(var(--spacing-unit)*2)] mb-[calc(var(--spacing-unit)*2)]">
<h3
className={`font-[var(--font-heading)] text-lg font-semibold ${
task.completed ? 'line-through opacity-60' : ''
}`}
>
{task.title}
</h3>
<span
className={`flex-shrink-0 font-[var(--font-body)] text-xs font-medium px-[calc(var(--spacing-unit)*2)] py-[calc(var(--spacing-unit)*1)] rounded-[calc(var(--radius-default)/2)] ${getPriorityColor(
task.priority
)}`}
>
{getPriorityLabel(task.priority)}
</span>
</div>
{task.description && (
<p
className={`font-[var(--font-body)] text-sm mb-[calc(var(--spacing-unit)*3)] ${
task.completed ? 'line-through opacity-50' : 'opacity-75'
}`}
>
{task.description}
</p>
)}
<div className="flex items-center justify-between gap-[calc(var(--spacing-unit)*2)]">
<span className="font-[var(--font-mono)] text-xs opacity-50">
{new Date(task.createdAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</span>
<div className="flex items-center gap-[calc(var(--spacing-unit)*2)]">
{!task.completed && (
<button
onClick={() => setIsEditing(true)}
className="font-[var(--font-body)] text-sm font-medium text-[var(--color-primary)] hover:opacity-80 transition-opacity"
aria-label="Edit task"
>
Edit
</button>
)}
<button
onClick={() => onDelete(task.id)}
className="font-[var(--font-body)] text-sm font-medium text-[var(--color-error)] hover:opacity-80 transition-opacity"
aria-label="Delete task"
>
Delete
</button>
</div>
</div>
</div>
</div>
</div>
);
}import { Task } from './types';
const STORAGE_KEY = 'taskflow_tasks';
/**
* Retrieves all tasks from localStorage
* @returns Array of tasks, or empty array if none exist or error occurs
*/
export function getTasks(): Task[] {
if (typeof window === 'undefined') {
return [];
}
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) {
return [];
}
const parsed = JSON.parse(stored);
if (!Array.isArray(parsed)) {
console.error('Invalid tasks data in localStorage');
return [];
}
return parsed as Task[];
} catch (error) {
console.error('Error retrieving tasks from localStorage:', error);
return [];
}
}
/**
* Saves tasks array to localStorage
* @param tasks - Array of tasks to persist
* @returns boolean indicating success
*/
export function saveTasks(tasks: Task[]): boolean {
if (typeof window === 'undefined') {
return false;
}
try {
const serialized = JSON.stringify(tasks);
localStorage.setItem(STORAGE_KEY, serialized);
return true;
} catch (error) {
console.error('Error saving tasks to localStorage:', error);
return false;
}
}
/**
* Adds a new task to localStorage
* @param task - Task object to add
* @returns boolean indicating success
*/
export function addTask(task: Task): boolean {
try {
const tasks = getTasks();
const updatedTasks = [...tasks, task];
return saveTasks(updatedTasks);
} catch (error) {
console.error('Error adding task:', error);
return false;
}
}
/**
* Updates an existing task in localStorage
* @param id - ID of the task to update
* @param updates - Partial task object with fields to update
* @returns boolean indicating success
*/
export function updateTask(id: string, updates: Partial<Task>): boolean {
try {
const tasks = getTasks();
const taskIndex = tasks.findIndex((t) => t.id === id);
if (taskIndex === -1) {
console.error(`Task with id ${id} not found`);
return false;
}
const updatedTasks = [...tasks];
updatedTasks[taskIndex] = {
...updatedTasks[taskIndex],
...updates,
updatedAt: new Date().toISOString(),
};
return saveTasks(updatedTasks);
} catch (error) {
console.error('Error updating task:', error);
return false;
}
}
/**
* Deletes a task from localStorage
* @param id - ID of the task to delete
* @returns boolean indicating success
*/
export function deleteTask(id: string): boolean {
try {
const tasks = getTasks();
const filteredTasks = tasks.filter((t) => t.id !== id);
if (filteredTasks.length === tasks.length) {
console.error(`Task with id ${id} not found`);
return false;
}
return saveTasks(filteredTasks);
} catch (error) {
console.error('Error deleting task:', error);
return false;
}
}
/**
* Toggles the completed status of a task
* @param id - ID of the task to toggle
* @returns boolean indicating success
*/
export function toggleTaskComplete(id: string): boolean {
try {
const tasks = getTasks();
const task = tasks.find((t) => t.id === id);
if (!task) {
console.error(`Task with id ${id} not found`);
return false;
}
return updateTask(id, { completed: !task.completed });
} catch (error) {
console.error('Error toggling task completion:', error);
return false;
}
}
/**
* Clears all tasks from localStorage
* @returns boolean indicating success
*/
export function clearAllTasks(): boolean {
if (typeof window === 'undefined') {
return false;
}
try {
localStorage.removeItem(STORAGE_KEY);
return true;
} catch (error) {
console.error('Error clearing tasks:', error);
return false;
}
}
/**
* Gets tasks filtered by completion status
* @param completed - Filter by completed (true), active (false), or all (undefined)
* @returns Filtered array of tasks
*/
export function getFilteredTasks(completed?: boolean): Task[] {
const tasks = getTasks();
if (completed === undefined) {
return tasks;
}
return tasks.filter((task) => task.completed === completed);
}
/**
* Gets tasks sorted by priority (high to low)
* @returns Array of tasks sorted by priority
*/
export function getTasksByPriority(): Task[] {
const tasks = getTasks();
const priorityOrder = { high: 3, medium: 2, low: 1 };
return [...tasks].sort((a, b) => {
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
}
/**
* Gets task count statistics
* @returns Object with total, active, and completed task counts
*/
export function getTaskStats(): {
total: number;
active: number;
completed: number;
} {
const tasks = getTasks();
return {
total: tasks.length,
active: tasks.filter((t) => !t.completed).length,
completed: tasks.filter((t) => t.completed).length,
};
}'use client';
import { ReactElement } from 'react';
import { Task } from '@/lib/types';
import TaskItem from '@/components/TaskItem';
interface TaskListProps {
tasks: Task[];
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onUpdate: (id: string, updates: Partial<Task>) => void;
}
/**
* TaskList component displays a list of tasks with filtering capabilities
* and handles task interactions (complete, delete, update)
*/
export default function TaskList({
tasks,
onToggle,
onDelete,
onUpdate,
}: TaskListProps): ReactElement {
return (
<div className="space-y-[calc(var(--spacing-unit)*2)]">
{tasks.map((task) => (
<TaskItem
key={task.id}
task={task}
onToggle={onToggle}
onDelete={onDelete}
onUpdate={onUpdate}
/>
))}
</div>
);
}'use client';
import { ReactElement, useState, FormEvent } from 'react';
interface AddTaskFormProps {
onAdd: (title: string, description: string, priority: 'low' | 'medium' | 'high') => void;
}
/**
* AddTaskForm component provides a form for creating new tasks
* with title, description, and priority selection
*/
export default function AddTaskForm({ onAdd }: AddTaskFormProps): ReactElement {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
const [isExpanded, setIsExpanded] = useState(false);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!title.trim()) {
return;
}
onAdd(title.trim(), description.trim(), priority);
setTitle('');
setDescription('');
setPriority('medium');
setIsExpanded(false);
};
const handleCancel = () => {
setTitle('');
setDescription('');
setPriority('medium');
setIsExpanded(false);
};
if (!isExpanded) {
return (
<button
onClick={() => setIsExpanded(true)}
className="w-full font-[var(--font-body)] text-left px-[calc(var(--spacing-unit)*4)] py-[calc(var(--spacing-unit)*4)] rounded-[var(--radius-default)] bg-[var(--color-surface)] text-[var(--color-surface-text)] border-2 border-dashed border-[var(--color-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--color-bg)] transition-all"
>
<span className="flex items-center gap-[calc(var(--spacing-unit)*2)]">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
<span className="font-medium">Add New Task</span>
</span>
</button>
);
}
return (
<form
onSubmit={handleSubmit}
className="bg-[var(--color-surface)] text-[var(--color-surface-text)] p-[calc(var(--spacing-unit)*6)] rounded-[var(--radius-default)] shadow-md border-2 border-[var(--color-primary)]"
>
<h2 className="font-[var(--font-heading)] text-2xl font-bold mb-[calc(var(--spacing-unit)*4)]">
Create New Task
</h2>
<div className="space-y-[calc(var(--spacing-unit)*4)]">
<div>
<label
htmlFor="task-title"
className="block font-[var(--font-body)] text-sm font-medium mb-[calc(var(--spacing-unit)*2)]"
>
Task Title <span className="text-[var(--color-error)]">*</span>
</label>
<input
id="task-title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full font-[var(--font-body)] px-[calc(var(--spacing-unit)*3)] py-[calc(var(--spacing-unit)*3)] rounded-[var(--radius-default)] bg-[var(--color-bg)] text-[var(--color-bg-text)] border border-[var(--color-secondary)] focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-opacity-20 transition-all"
placeholder="Enter task title"
autoFocus
required
/>
</div>
<div>
<label
htmlFor="task-description"
className="block font-[var(--font-body)] text-sm font-medium mb-[calc(var(--spacing-unit)*2)]"
>
Description <span className="text-[var(--color-surface-text)] opacity-50">(optional)</span>
</label>
<textarea
id="task-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full font-[var(--font-body)] px-[calc(var(--spacing-unit)*3)] py-[calc(var(--spacing-unit)*3)] rounded-[var(--radius-default)] bg-[var(--color-bg)] text-[var(--color-bg-text)] border border-[var(--color-secondary)] focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-opacity-20 transition-all resize-none"
placeholder="Add additional details about this task"
/>
</div>
<div>
<label
htmlFor="task-priority"
className="block font-[var(--font-body)] text-sm font-medium mb-[calc(var(--spacing-unit)*2)]"
>
Priority
</label>
<select
id="task-priority"
value={priority}
onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high')}
className="w-full font-[var(--font-body)] px-[calc(var(--spacing-unit)*3)] py-[calc(var(--spacing-unit)*3)] rounded-[var(--radius-default)] bg-[var(--color-bg)] text-[var(--color-bg-text)] border border-[var(--color-secondary)] focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-opacity-20 transition-all cursor-pointer"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div className="flex items-center gap-[calc(var(--spacing-unit)*3)] pt-[calc(var(--spacing-unit)*2)]">
<button
type="submit"
disabled={!title.trim()}
className="font-[var(--font-body)] text-sm font-medium px-[calc(var(--spacing-unit)*6)] py-[calc(var(--spacing-unit)*3)] rounded-[var(--radius-default)] bg-[var(--color-primary)] text-[var(--color-primary-text)] hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
>
Add Task
</button>
<button
type="button"
onClick={handleCancel}
className="font-[var(--font-body)] text-sm font-medium px-[calc(var(--spacing-unit)*6)] py-[calc(var(--spacing-unit)*3)] rounded-[var(--radius-default)] bg-[var(--color-secondary)] text-[var(--color-secondary-text)] hover:opacity-80 transition-opacity"
>
Cancel
</button>
</div>
</div>
</form>
);
}/**
* Priority levels for tasks
*/
export type Priority = 'low' | 'medium' | 'high';
/**
* Filter options for task display
*/
export type Filter = 'all' | 'active' | 'completed';
/**
* Task interface representing a single todo item
*/
export interface Task {
/**
* Unique identifier for the task
*/
id: string;
/**
* Task title/name
*/
title: string;
/**
* Optional task description with additional details
*/
description: string;
/**
* Task priority level
*/
priority: Priority;
/**
* Completion status of the task
*/
completed: boolean;
/**
* Timestamp when the task was created
*/
createdAt: string;
/**
* Timestamp when the task was last updated (optional)
*/
updatedAt?: string;
}
/**
* Props interface for creating a new task
*/
export interface CreateTaskInput {
title: string;
description?: string;
priority?: Priority;
}
/**
* Props interface for updating an existing task
*/
export interface UpdateTaskInput {
title?: string;
description?: string;
priority?: Priority;
completed?: boolean;
}@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
:root {
--color-accent: #059669;
--color-accent-text: #FFFFFF;
--color-bg: #FFFFFF;
--color-bg-text: #1A1A1A;
--color-error: #DC2626;
--color-error-text: #FFFFFF;
--color-info: #3B82F6;
--color-info-text: #FFFFFF;
--color-primary: #1A1A1A;
--color-primary-text: #FFFFFF;
--color-secondary: #F5F5F5;
--color-secondary-text: #1A1A1A;
--color-success: #059669;
--color-success-text: #FFFFFF;
--color-surface: #FAFAFA;
--color-surface-text: #2D2D2D;
--color-warning: #F59E0B;
--color-warning-text: #000000;
--font-body: 'Source Sans Pro', sans-serif;
--font-heading: 'Playfair Display', serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-default: 4px;
--spacing-unit: 4px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
background-color: var(--color-bg);
color: var(--color-bg-text);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
min-height: 100vh;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-heading);
font-weight: 700;
line-height: 1.3;
letter-spacing: -0.02em;
}
h1 {
font-size: 3rem;
margin-bottom: calc(var(--spacing-unit) * 6);
}
h2 {
font-size: 2.25rem;
margin-bottom: calc(var(--spacing-unit) * 5);
}
h3 {
font-size: 1.75rem;
margin-bottom: calc(var(--spacing-unit) * 4);
}
h4 {
font-size: 1.5rem;
margin-bottom: calc(var(--spacing-unit) * 3);
}
h5 {
font-size: 1.25rem;
margin-bottom: calc(var(--spacing-unit) * 3);
}
h6 {
font-size: 1rem;
margin-bottom: calc(var(--spacing-unit) * 2);
}
p {
margin-bottom: calc(var(--spacing-unit) * 4);
}
a {
color: var(--color-primary);
text-decoration: none;
transition: opacity 0.2s ease;
}
a:hover {
opacity: 0.7;
}
button {
font-family: var(--font-body);
cursor: pointer;
border: none;
outline: none;
background: none;
}
button:disabled {
cursor: not-allowed;
}
input,
textarea,
select {
font-family: var(--font-body);
outline: none;
}
input::placeholder,
textarea::placeholder {
color: var(--color-surface-text);
opacity: 0.5;
}
code,
pre {
font-family: var(--font-mono);
font-size: 0.9em;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.transition-base {
transition: all 0.2s ease;
}
.shadow-elevated {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.shadow-elevated-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
}
@layer components {
.priority-indicator {
width: calc(var(--spacing-unit) * 1);
height: 100%;
position: absolute;
left: 0;
top: 0;
border-radius: var(--radius-default) 0 0 var(--radius-default);
}
.priority-high {
background-color: var(--color-error);
}
.priority-medium {
background-color: var(--color-warning);
}
.priority-low {
background-color: var(--color-info);
}
.checkbox-custom {
appearance: none;
width: calc(var(--spacing-unit) * 5);
height: calc(var(--spacing-unit) * 5);
border: 2px solid var(--color-primary);
border-radius: var(--radius-default);
cursor: pointer;
position: relative;
flex-shrink: 0;
transition: all 0.2s ease;
}
.checkbox-custom:hover {
border-color: var(--color-accent);
}
.checkbox-custom:checked {
background-color: var(--color-success);
border-color: var(--color-success);
}
.checkbox-custom:checked::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) rotate(45deg);
width: calc(var(--spacing-unit) * 1.5);
height: calc(var(--spacing-unit) * 3);
border: solid var(--color-success-text);
border-width: 0 2px 2px 0;
}
.task-completed {
opacity: 0.6;
}
.task-completed .task-title {
text-decoration: line-through;
}
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.75rem;
}
h3 {
font-size: 1.5rem;
}
body {
font-size: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}