← Back to Community

TaskFlow

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.

12 views
Created 11/15/2025

Features

  • Add, edit, and delete tasks with titles and optional descriptions
  • Mark tasks as complete/incomplete with visual feedback
  • Filter tasks by status (all, active, completed)
  • Persist tasks to browser localStorage for data retention
  • Priority levels (low, medium, high) with color-coded indicators

Tech Stack

Next.js 15React 19TypeScriptTailwind CSSshadcn/uiReact Hooks (useState, useEffect)localStorage API

Generated Files (7)

app/page.tsx5.3 KB
'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>
  );
}
components/TaskItem.tsx9.4 KB
'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>
  );
}
lib/storage.ts4.7 KB
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,
  };
}
components/TaskList.tsx0.8 KB
'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>
  );
}
components/AddTaskForm.tsx6.2 KB
'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>
  );
}
lib/types.ts1.1 KB
/**
 * 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;
}
app/globals.css4.7 KB
@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;
  }
}

Build Something Similar

Get inspired and create your own version