Created
June 11, 2025 11:57
-
-
Save nileshtrivedi/02de3a7f697b14765a3fa04a4f74875d to your computer and use it in GitHub Desktop.
single-file todo with ash without auth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Mix.install([ | |
{:ash, "~> 3.0"}, | |
{:phoenix_playground, "== 0.1.6"}, | |
], consolidate_protocols: false) | |
defmodule Helpdesk do | |
end | |
defmodule Helpdesk.Support do | |
use Ash.Domain, validate_config_inclusion?: false | |
resources do | |
resource Helpdesk.Support.Todo | |
end | |
end | |
defmodule Helpdesk.Support.Todo do | |
# This turns this module into a resource | |
use Ash.Resource, domain: Helpdesk.Support, data_layer: Ash.DataLayer.Ets | |
actions do | |
defaults [:read, :destroy] | |
create :add do | |
accept [:task, :user_id] | |
change set_attribute(:status, "pending") | |
end | |
update :toggle do | |
accept [] | |
change atomic_update(:status, expr(if status == :pending, do: :done, else: :pending)) | |
end | |
end | |
attributes do | |
uuid_primary_key :id | |
attribute :user_id, :string do | |
allow_nil? false | |
public? true | |
end | |
attribute :task, :string do | |
allow_nil? false | |
public? true | |
end | |
attribute :status, :string do | |
default "pending" | |
allow_nil? false | |
public? true | |
end | |
validations do | |
validate attribute_in(:status, ["pending", "done"]) | |
end | |
end | |
end | |
defmodule TodoLive do | |
use Phoenix.LiveView | |
require Ash.Query | |
def get_user_todos(user_id) do | |
Helpdesk.Support.Todo | |
|> Ash.Query.filter(user_id == ^user_id) | |
|> Ash.read!() | |
end | |
def mount(_params, _session, socket) do | |
user_id = "foobar" | |
todos = get_user_todos(user_id) | |
{:ok, assign(socket, todos: todos, user_id: user_id, new_todo_name: "drink water")} | |
end | |
def handle_event("create_todo", %{"name" => name}, socket) do | |
if name == "" do | |
{:noreply, socket} | |
else | |
# Create new todo with current user's ID | |
Helpdesk.Support.Todo | |
|> Ash.Changeset.for_create(:add, %{task: String.trim(name), user_id: "foobar"}) | |
|> Ash.create!() | |
# Update socket with new list of todos | |
socket = | |
socket | |
|> assign(:todos, get_user_todos(socket.assigns.user_id)) | |
|> assign(:new_todo_name, "") | |
{:noreply, socket} | |
end | |
end | |
def handle_event("toggle_todo", %{"id" => id}, socket) do | |
# Find the todo and ensure it belongs to current user | |
todo = Helpdesk.Support.Todo | |
|> Ash.Query.filter(user_id == ^socket.assigns.user_id) | |
|> Ash.Query.filter(id == ^id) | |
|> Ash.read_one!() | |
# Update todo | |
{:ok, _todo} = | |
todo | |
|> Ash.Changeset.for_update(:toggle) | |
|> Ash.update!() | |
# Refresh todos | |
{:noreply, assign(socket, :todos, get_user_todos(socket.assigns.user_id))} | |
end | |
def handle_event("delete_todo", %{"id" => id}, socket) do | |
# Delete todo, ensuring it belongs to current user | |
todo = Helpdesk.Support.Todo | |
|> Ash.Query.filter(user_id == ^socket.assigns.user_id) | |
|> Ash.Query.filter(id == ^id) | |
|> Ash.read_one!() | |
todo | |
|> Ash.Changeset.for_destroy(:destroy) | |
|> Ash.destroy!() | |
# Refresh todos | |
{:noreply, assign(socket, :todos, get_user_todos(socket.assigns.user_id))} | |
end | |
def render(assigns) do | |
~H""" | |
<script src="https://cdn.tailwindcss.com?plugins=forms"></script> | |
<div class="max-w-md mx-auto mt-10 p-6 bg-gray-200 rounded-lg shadow-md"> | |
<h1 class="text-2xl font-bold mb-4">Todo List for <%= @user_id %></h1> | |
<form phx-submit="create_todo" class="mb-4 flex"> | |
<input | |
type="text" | |
name="name" | |
value={@new_todo_name} | |
placeholder="Enter a new todo" | |
class="flex-grow mr-2 px-2 py-1 border rounded" | |
/> | |
<button | |
type="submit" | |
class="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-600" | |
> | |
Add Todo | |
</button> | |
</form> | |
<ul> | |
<%= for todo <- @todos do %> | |
<li class="flex items-center justify-between py-2 border-b"> | |
<label class="flex items-center gap-2"> | |
<input | |
type="checkbox" | |
phx-click="toggle_todo" | |
phx-value-id={todo.id} | |
checked={todo.status == "done"} | |
class="mr-2" | |
/> | |
<span class={if todo.status == "done", do: "line-through text-gray-500", else: ""}> | |
<%= todo.task %> | |
</span> | |
<span>by <%= todo.user_id %></span> | |
</label> | |
<button | |
phx-click="delete_todo" | |
phx-value-id={todo.id} | |
class="text-red-500 hover:text-red-700" | |
> | |
✕ | |
</button> | |
</li> | |
<% end %> | |
</ul> | |
</div> | |
""" | |
end | |
end | |
defmodule Demo.Router do | |
use Phoenix.Router | |
import Phoenix.LiveView.Router | |
pipeline :browser do | |
plug :accepts, ["html"] | |
plug :put_secret_key_base | |
plug :fetch_session | |
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root} | |
plug :put_secure_browser_headers | |
end | |
scope "/" do | |
pipe_through :browser | |
live "/", TodoLive | |
end | |
def put_secret_key_base(conn, _) do | |
put_in conn.secret_key_base, System.get_env("SECRET_KEY_BASE") | |
end | |
end | |
defmodule Demo.Endpoint do | |
use Phoenix.Endpoint, otp_app: :phoenix_playground | |
plug Plug.Logger | |
socket "/live", Phoenix.LiveView.Socket | |
plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix" | |
plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view" | |
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket | |
plug Phoenix.LiveReloader | |
plug Phoenix.CodeReloader, reloader: &PhoenixPlayground.CodeReloader.reload/2 | |
plug Demo.Router | |
end | |
PhoenixPlayground.start( | |
open_browser: false, | |
live_reload: true, | |
child_specs: [], | |
# endpoint: Demo.Endpoint, | |
# endpoint_options: [debug_errors: true] | |
plug: Demo.Router | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment