defmodule Raxol.Examples.TodoApp do @moduledoc """ A fully-featured todo list application demonstrating Raxol's capabilities. Features: - Add, edit, and delete todos - Mark todos as complete - Filter by status (all, active, completed) - Persist todos to disk - Keyboard shortcuts - Search functionality """ use Raxol.Application @storage_file Path.expand("~/.raxol_todos.json") @impl true def mount(_params, socket) do todos = load_todos() {:ok, socket |> assign(todos: todos) |> assign(filter: :all) |> assign(input: "") |> assign(search: "") |> assign(editing_id: nil) |> assign(edit_text: "") |> register_shortcuts([ {"ctrl+n", "new_todo"}, {"ctrl+d", "clear_completed"}, {"ctrl+s", "save_todos"}, {"ctrl+f", "focus_search"}, {"/", "focus_search"}, {"escape", "cancel_edit"} ])} end @impl true def render(assigns) do filtered_todos = filter_todos(assigns.todos, assigns.filter, assigns.search) stats = calculate_stats(assigns.todos) ~H""" 📝 Todo List <%= stats.active %> active, <%= stats.completed %> completed <%= if length(filtered_todos) == 0 do %> <%= empty_message(@filter, @search) %> <% else %> <%= for todo <- filtered_todos do %> <% end %> <% end %> Ctrl+S to save | Ctrl+D to clear completed """ end # Event Handlers @impl true def handle_event("update_input", %{"value" => value}, socket) do {:noreply, assign(socket, input: value)} end @impl true def handle_event("add_todo", _, socket) do text = String.trim(socket.assigns.input) if text != "" do todo = %{ id: generate_id(), text: text, completed: false, created_at: DateTime.utc_now(), updated_at: DateTime.utc_now() } socket = socket |> update(:todos, &[todo | &1]) |> assign(input: "") |> save_todos_to_disk() {:noreply, socket} else {:noreply, socket} end end @impl true def handle_event("toggle_todo", %{"id" => id}, socket) do todos = Enum.map(socket.assigns.todos, fn todo -> if todo.id == id do %{todo | completed: !todo.completed, updated_at: DateTime.utc_now() } else todo end end) socket = socket |> assign(todos: todos) |> save_todos_to_disk() {:noreply, socket} end @impl true def handle_event("start_edit", %{"id" => id}, socket) do todo = Enum.find(socket.assigns.todos, &(&1.id == id)) socket = socket |> assign(editing_id: id) |> assign(edit_text: todo.text) {:noreply, socket} end @impl true def handle_event("update_edit", %{"value" => value}, socket) do {:noreply, assign(socket, edit_text: value)} end @impl true def handle_event("save_edit", _, socket) do text = String.trim(socket.assigns.edit_text) if text != "" do todos = Enum.map(socket.assigns.todos, fn todo -> if todo.id == socket.assigns.editing_id do %{todo | text: text, updated_at: DateTime.utc_now() } else todo end end) socket = socket |> assign(todos: todos) |> assign(editing_id: nil) |> assign(edit_text: "") |> save_todos_to_disk() {:noreply, socket} else {:noreply, socket} end end @impl true def handle_event("cancel_edit", _, socket) do socket = socket |> assign(editing_id: nil) |> assign(edit_text: "") {:noreply, socket} end @impl true def handle_event("delete_todo", %{"id" => id}, socket) do todos = Enum.reject(socket.assigns.todos, &(&1.id == id)) socket = socket |> assign(todos: todos) |> save_todos_to_disk() {:noreply, socket} end @impl true def handle_event("change_filter", %{"value" => filter}, socket) do {:noreply, assign(socket, filter: String.to_atom(filter))} end @impl true def handle_event("update_search", %{"value" => value}, socket) do {:noreply, assign(socket, search: value)} end @impl true def handle_event("mark_all_complete", _, socket) do todos = Enum.map(socket.assigns.todos, fn todo -> %{todo | completed: true, updated_at: DateTime.utc_now() } end) socket = socket |> assign(todos: todos) |> save_todos_to_disk() {:noreply, socket} end @impl true def handle_event("clear_completed", _, socket) do todos = Enum.reject(socket.assigns.todos, & &1.completed) socket = socket |> assign(todos: todos) |> save_todos_to_disk() {:noreply, socket} end @impl true def handle_event("new_todo", _, socket) do # Focus the input field (implementation depends on focus management) {:noreply, socket} end @impl true def handle_event("save_todos", _, socket) do socket = save_todos_to_disk(socket) Raxol.Toast.show("Todos saved!", type: :success) {:noreply, socket} end @impl true def handle_event("focus_search", _, socket) do # Focus the search field (implementation depends on focus management) {:noreply, socket} end # Helper Functions defp filter_todos(todos, filter, search) do todos |> filter_by_status(filter) |> filter_by_search(search) |> Enum.sort_by(& &1.created_at, {:desc, DateTime}) end defp filter_by_status(todos, :all), do: todos defp filter_by_status(todos, :active), do: Enum.reject(todos, & &1.completed) defp filter_by_status(todos, :completed), do: Enum.filter(todos, & &1.completed) defp filter_by_search(todos, ""), do: todos defp filter_by_search(todos, search) do search_term = String.downcase(search) Enum.filter(todos, fn todo -> String.contains?(String.downcase(todo.text), search_term) end) end defp calculate_stats(todos) do completed = Enum.count(todos, & &1.completed) %{ total: length(todos), active: length(todos) - completed, completed: completed } end defp empty_message(:all, ""), do: "No todos yet. Add one above!" defp empty_message(:all, _search), do: "No todos match your search." defp empty_message(:active, _), do: "No active todos. Nice work!" defp empty_message(:completed, _), do: "No completed todos yet." defp generate_id do :crypto.strong_rand_bytes(16) |> Base.encode16() end defp save_todos_to_disk(socket) do todos_json = Jason.encode!(socket.assigns.todos, pretty: true) File.write!(@storage_file, todos_json) socket end defp load_todos do if File.exists?(@storage_file) do @storage_file |> File.read!() |> Jason.decode!() |> Enum.map(fn todo -> %{ "id" => id, "text" => text, "completed" => completed, "created_at" => created_at, "updated_at" => updated_at } = todo %{ id: id, text: text, completed: completed, created_at: parse_datetime(created_at), updated_at: parse_datetime(updated_at) } end) else [] end rescue _ -> [] end defp parse_datetime(nil), do: DateTime.utc_now() defp parse_datetime(string) when is_binary(string) do case DateTime.from_iso8601(string) do {:ok, datetime, _} -> datetime _ -> DateTime.utc_now() end end end # TodoItem Component defmodule Raxol.Examples.TodoApp.TodoItem do use Raxol.Component prop :todo, :map, required: true prop :editing, :boolean, default: false prop :editText, :string, default: "" prop :onToggle, :string prop :onEdit, :string prop :onUpdateEdit, :string prop :onSaveEdit, :string prop :onCancelEdit, :string prop :onDelete, :string @impl true def render(assigns) do ~H""" <%= if @editing do %> <% else %> <%= @todo.text %> <% end %> <%= unless @editing do %> <% end %> """ end end