This commit is contained in:
Daniel Flanagan 2024-01-21 23:48:10 -06:00
parent 0f48fd8e2e
commit 702e36c88f
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
29 changed files with 1707 additions and 0 deletions

296
lib/homeman/accounts.ex Normal file
View file

@ -0,0 +1,296 @@
defmodule Homeman.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.Query, warn: false
alias Homeman.Repo
alias Homeman.Accounts.User
@doc """
Returns the list of users.
## Examples
iex> list_users()
[%User{}, ...]
"""
def list_users do
Repo.all(User)
end
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.get!(User, id)
@doc """
Creates a user.
## Examples
iex> create_user(%{field: value})
{:ok, %User{}}
iex> create_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a user.
## Examples
iex> update_user(user, %{field: new_value})
{:ok, %User{}}
iex> update_user(user, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a user.
## Examples
iex> delete_user(user)
{:ok, %User{}}
iex> delete_user(user)
{:error, %Ecto.Changeset{}}
"""
def delete_user(%User{} = user) do
Repo.delete(user)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user(%User{} = user, attrs \\ %{}) do
User.changeset(user, attrs)
end
alias Homeman.Accounts.Todo
@doc """
Returns the list of todos.
## Examples
iex> list_todos()
[%Todo{}, ...]
"""
def list_todos do
Repo.all(Todo)
end
@doc """
Gets a single todo.
Raises `Ecto.NoResultsError` if the Todo does not exist.
## Examples
iex> get_todo!(123)
%Todo{}
iex> get_todo!(456)
** (Ecto.NoResultsError)
"""
def get_todo!(id), do: Repo.get!(Todo, id)
@doc """
Creates a todo.
## Examples
iex> create_todo(%{field: value})
{:ok, %Todo{}}
iex> create_todo(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_todo(attrs \\ %{}) do
%Todo{}
|> Todo.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a todo.
## Examples
iex> update_todo(todo, %{field: new_value})
{:ok, %Todo{}}
iex> update_todo(todo, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_todo(%Todo{} = todo, attrs) do
todo
|> Todo.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a todo.
## Examples
iex> delete_todo(todo)
{:ok, %Todo{}}
iex> delete_todo(todo)
{:error, %Ecto.Changeset{}}
"""
def delete_todo(%Todo{} = todo) do
Repo.delete(todo)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking todo changes.
## Examples
iex> change_todo(todo)
%Ecto.Changeset{data: %Todo{}}
"""
def change_todo(%Todo{} = todo, attrs \\ %{}) do
Todo.changeset(todo, attrs)
end
alias Homeman.Accounts.Task
@doc """
Returns the list of tasks.
## Examples
iex> list_tasks()
[%Task{}, ...]
"""
def list_tasks do
Repo.all(Task)
end
@doc """
Gets a single task.
Raises `Ecto.NoResultsError` if the Task does not exist.
## Examples
iex> get_task!(123)
%Task{}
iex> get_task!(456)
** (Ecto.NoResultsError)
"""
def get_task!(id), do: Repo.get!(Task, id)
@doc """
Creates a task.
## Examples
iex> create_task(%{field: value})
{:ok, %Task{}}
iex> create_task(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_task(attrs \\ %{}) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a task.
## Examples
iex> update_task(task, %{field: new_value})
{:ok, %Task{}}
iex> update_task(task, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_task(%Task{} = task, attrs) do
task
|> Task.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a task.
## Examples
iex> delete_task(task)
{:ok, %Task{}}
iex> delete_task(task)
{:error, %Ecto.Changeset{}}
"""
def delete_task(%Task{} = task) do
Repo.delete(task)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking task changes.
## Examples
iex> change_task(task)
%Ecto.Changeset{data: %Task{}}
"""
def change_task(%Task{} = task, attrs \\ %{}) do
Task.changeset(task, attrs)
end
end

View file

@ -0,0 +1,22 @@
defmodule Homeman.Accounts.Task do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "tasks" do
field :description, :string
field :phase, :string
field :emoji, :string
field :completed_at, :naive_datetime
timestamps(type: :utc_datetime)
end
@doc false
def changeset(task, attrs) do
task
|> cast(attrs, [:description, :emoji, :completed_at, :phase])
|> validate_required([:description, :emoji, :phase])
end
end

View file

@ -0,0 +1,22 @@
defmodule Homeman.Accounts.Todo do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "todos" do
field :description, :string
field :emoji, :string
field :completed_at, :naive_datetime
field :assignee_user_id, :binary_id
timestamps(type: :utc_datetime)
end
@doc false
def changeset(todo, attrs) do
todo
|> cast(attrs, [:description, :emoji, :completed_at])
|> validate_required([:description, :emoji, :completed_at])
end
end

View file

@ -0,0 +1,21 @@
defmodule Homeman.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "users" do
field :name, :string
field :color, :string
field :avatar_url, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :avatar_url, :color])
|> validate_required([:name, :avatar_url, :color])
end
end

View file

@ -0,0 +1,93 @@
defmodule HomemanWeb.TaskLive.FormComponent do
use HomemanWeb, :live_component
alias Homeman.Accounts
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage task records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="task-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:description]} type="text" label="Description" />
<.input field={@form[:emoji]} type="text" label="Emoji" />
<.input field={@form[:completed_at]} type="datetime-local" label="Completed at" />
<.input field={@form[:phase]} type="text" label="Phase" />
<:actions>
<.button phx-disable-with="Saving...">Save Task</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{task: task} = assigns, socket) do
changeset = Accounts.change_task(task)
{:ok,
socket
|> assign(assigns)
|> assign_form(changeset)}
end
@impl true
def handle_event("validate", %{"task" => task_params}, socket) do
changeset =
socket.assigns.task
|> Accounts.change_task(task_params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
def handle_event("save", %{"task" => task_params}, socket) do
save_task(socket, socket.assigns.action, task_params)
end
defp save_task(socket, :edit, task_params) do
case Accounts.update_task(socket.assigns.task, task_params) do
{:ok, task} ->
notify_parent({:saved, task})
{:noreply,
socket
|> put_flash(:info, "Task updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp save_task(socket, :new, task_params) do
case Accounts.create_task(task_params) do
{:ok, task} ->
notify_parent({:saved, task})
{:noreply,
socket
|> put_flash(:info, "Task created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, :form, to_form(changeset))
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View file

@ -0,0 +1,47 @@
defmodule HomemanWeb.TaskLive.Index do
use HomemanWeb, :live_view
alias Homeman.Accounts
alias Homeman.Accounts.Task
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :tasks, Accounts.list_tasks())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Task")
|> assign(:task, Accounts.get_task!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Task")
|> assign(:task, %Task{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Tasks")
|> assign(:task, nil)
end
@impl true
def handle_info({HomemanWeb.TaskLive.FormComponent, {:saved, task}}, socket) do
{:noreply, stream_insert(socket, :tasks, task)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
task = Accounts.get_task!(id)
{:ok, _} = Accounts.delete_task(task)
{:noreply, stream_delete(socket, :tasks, task)}
end
end

View file

@ -0,0 +1,44 @@
<.header>
Listing Tasks
<:actions>
<.link patch={~p"/tasks/new"}>
<.button>New Task</.button>
</.link>
</:actions>
</.header>
<.table
id="tasks"
rows={@streams.tasks}
row_click={fn {_id, task} -> JS.navigate(~p"/tasks/#{task}") end}
>
<:col :let={{_id, task}} label="Description"><%= task.description %></:col>
<:col :let={{_id, task}} label="Emoji"><%= task.emoji %></:col>
<:col :let={{_id, task}} label="Completed at"><%= task.completed_at %></:col>
<:col :let={{_id, task}} label="Phase"><%= task.phase %></:col>
<:action :let={{_id, task}}>
<div class="sr-only">
<.link navigate={~p"/tasks/#{task}"}>Show</.link>
</div>
<.link patch={~p"/tasks/#{task}/edit"}>Edit</.link>
</:action>
<:action :let={{id, task}}>
<.link
phx-click={JS.push("delete", value: %{id: task.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
<.modal :if={@live_action in [:new, :edit]} id="task-modal" show on_cancel={JS.patch(~p"/tasks")}>
<.live_component
module={HomemanWeb.TaskLive.FormComponent}
id={@task.id || :new}
title={@page_title}
action={@live_action}
task={@task}
patch={~p"/tasks"}
/>
</.modal>

View file

@ -0,0 +1,21 @@
defmodule HomemanWeb.TaskLive.Show do
use HomemanWeb, :live_view
alias Homeman.Accounts
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:task, Accounts.get_task!(id))}
end
defp page_title(:show), do: "Show Task"
defp page_title(:edit), do: "Edit Task"
end

View file

@ -0,0 +1,29 @@
<.header>
Task <%= @task.id %>
<:subtitle>This is a task record from your database.</:subtitle>
<:actions>
<.link patch={~p"/tasks/#{@task}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit task</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Description"><%= @task.description %></:item>
<:item title="Emoji"><%= @task.emoji %></:item>
<:item title="Completed at"><%= @task.completed_at %></:item>
<:item title="Phase"><%= @task.phase %></:item>
</.list>
<.back navigate={~p"/tasks"}>Back to tasks</.back>
<.modal :if={@live_action == :edit} id="task-modal" show on_cancel={JS.patch(~p"/tasks/#{@task}")}>
<.live_component
module={HomemanWeb.TaskLive.FormComponent}
id={@task.id}
title={@page_title}
action={@live_action}
task={@task}
patch={~p"/tasks/#{@task}"}
/>
</.modal>

View file

@ -0,0 +1,92 @@
defmodule HomemanWeb.TodoLive.FormComponent do
use HomemanWeb, :live_component
alias Homeman.Accounts
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage todo records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="todo-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:description]} type="text" label="Description" />
<.input field={@form[:emoji]} type="text" label="Emoji" />
<.input field={@form[:completed_at]} type="datetime-local" label="Completed at" />
<:actions>
<.button phx-disable-with="Saving...">Save Todo</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{todo: todo} = assigns, socket) do
changeset = Accounts.change_todo(todo)
{:ok,
socket
|> assign(assigns)
|> assign_form(changeset)}
end
@impl true
def handle_event("validate", %{"todo" => todo_params}, socket) do
changeset =
socket.assigns.todo
|> Accounts.change_todo(todo_params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
def handle_event("save", %{"todo" => todo_params}, socket) do
save_todo(socket, socket.assigns.action, todo_params)
end
defp save_todo(socket, :edit, todo_params) do
case Accounts.update_todo(socket.assigns.todo, todo_params) do
{:ok, todo} ->
notify_parent({:saved, todo})
{:noreply,
socket
|> put_flash(:info, "Todo updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp save_todo(socket, :new, todo_params) do
case Accounts.create_todo(todo_params) do
{:ok, todo} ->
notify_parent({:saved, todo})
{:noreply,
socket
|> put_flash(:info, "Todo created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, :form, to_form(changeset))
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View file

@ -0,0 +1,47 @@
defmodule HomemanWeb.TodoLive.Index do
use HomemanWeb, :live_view
alias Homeman.Accounts
alias Homeman.Accounts.Todo
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :todos, Accounts.list_todos())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Todo")
|> assign(:todo, Accounts.get_todo!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Todo")
|> assign(:todo, %Todo{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Todos")
|> assign(:todo, nil)
end
@impl true
def handle_info({HomemanWeb.TodoLive.FormComponent, {:saved, todo}}, socket) do
{:noreply, stream_insert(socket, :todos, todo)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
todo = Accounts.get_todo!(id)
{:ok, _} = Accounts.delete_todo(todo)
{:noreply, stream_delete(socket, :todos, todo)}
end
end

View file

@ -0,0 +1,43 @@
<.header>
Listing Todos
<:actions>
<.link patch={~p"/todos/new"}>
<.button>New Todo</.button>
</.link>
</:actions>
</.header>
<.table
id="todos"
rows={@streams.todos}
row_click={fn {_id, todo} -> JS.navigate(~p"/todos/#{todo}") end}
>
<:col :let={{_id, todo}} label="Description"><%= todo.description %></:col>
<:col :let={{_id, todo}} label="Emoji"><%= todo.emoji %></:col>
<:col :let={{_id, todo}} label="Completed at"><%= todo.completed_at %></:col>
<:action :let={{_id, todo}}>
<div class="sr-only">
<.link navigate={~p"/todos/#{todo}"}>Show</.link>
</div>
<.link patch={~p"/todos/#{todo}/edit"}>Edit</.link>
</:action>
<:action :let={{id, todo}}>
<.link
phx-click={JS.push("delete", value: %{id: todo.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
<.modal :if={@live_action in [:new, :edit]} id="todo-modal" show on_cancel={JS.patch(~p"/todos")}>
<.live_component
module={HomemanWeb.TodoLive.FormComponent}
id={@todo.id || :new}
title={@page_title}
action={@live_action}
todo={@todo}
patch={~p"/todos"}
/>
</.modal>

View file

@ -0,0 +1,21 @@
defmodule HomemanWeb.TodoLive.Show do
use HomemanWeb, :live_view
alias Homeman.Accounts
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:todo, Accounts.get_todo!(id))}
end
defp page_title(:show), do: "Show Todo"
defp page_title(:edit), do: "Edit Todo"
end

View file

@ -0,0 +1,28 @@
<.header>
Todo <%= @todo.id %>
<:subtitle>This is a todo record from your database.</:subtitle>
<:actions>
<.link patch={~p"/todos/#{@todo}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit todo</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Description"><%= @todo.description %></:item>
<:item title="Emoji"><%= @todo.emoji %></:item>
<:item title="Completed at"><%= @todo.completed_at %></:item>
</.list>
<.back navigate={~p"/todos"}>Back to todos</.back>
<.modal :if={@live_action == :edit} id="todo-modal" show on_cancel={JS.patch(~p"/todos/#{@todo}")}>
<.live_component
module={HomemanWeb.TodoLive.FormComponent}
id={@todo.id}
title={@page_title}
action={@live_action}
todo={@todo}
patch={~p"/todos/#{@todo}"}
/>
</.modal>

View file

@ -0,0 +1,92 @@
defmodule HomemanWeb.UserLive.FormComponent do
use HomemanWeb, :live_component
alias Homeman.Accounts
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage user records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="user-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:name]} type="text" label="Name" />
<.input field={@form[:avatar_url]} type="text" label="Avatar url" />
<.input field={@form[:color]} type="text" label="Color" />
<:actions>
<.button phx-disable-with="Saving...">Save User</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{user: user} = assigns, socket) do
changeset = Accounts.change_user(user)
{:ok,
socket
|> assign(assigns)
|> assign_form(changeset)}
end
@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
socket.assigns.user
|> Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
def handle_event("save", %{"user" => user_params}, socket) do
save_user(socket, socket.assigns.action, user_params)
end
defp save_user(socket, :edit, user_params) do
case Accounts.update_user(socket.assigns.user, user_params) do
{:ok, user} ->
notify_parent({:saved, user})
{:noreply,
socket
|> put_flash(:info, "User updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp save_user(socket, :new, user_params) do
case Accounts.create_user(user_params) do
{:ok, user} ->
notify_parent({:saved, user})
{:noreply,
socket
|> put_flash(:info, "User created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, :form, to_form(changeset))
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View file

@ -0,0 +1,47 @@
defmodule HomemanWeb.UserLive.Index do
use HomemanWeb, :live_view
alias Homeman.Accounts
alias Homeman.Accounts.User
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :users, Accounts.list_users())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit User")
|> assign(:user, Accounts.get_user!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New User")
|> assign(:user, %User{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Users")
|> assign(:user, nil)
end
@impl true
def handle_info({HomemanWeb.UserLive.FormComponent, {:saved, user}}, socket) do
{:noreply, stream_insert(socket, :users, user)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
user = Accounts.get_user!(id)
{:ok, _} = Accounts.delete_user(user)
{:noreply, stream_delete(socket, :users, user)}
end
end

View file

@ -0,0 +1,43 @@
<.header>
Listing Users
<:actions>
<.link patch={~p"/users/new"}>
<.button>New User</.button>
</.link>
</:actions>
</.header>
<.table
id="users"
rows={@streams.users}
row_click={fn {_id, user} -> JS.navigate(~p"/users/#{user}") end}
>
<:col :let={{_id, user}} label="Name"><%= user.name %></:col>
<:col :let={{_id, user}} label="Avatar url"><%= user.avatar_url %></:col>
<:col :let={{_id, user}} label="Color"><%= user.color %></:col>
<:action :let={{_id, user}}>
<div class="sr-only">
<.link navigate={~p"/users/#{user}"}>Show</.link>
</div>
<.link patch={~p"/users/#{user}/edit"}>Edit</.link>
</:action>
<:action :let={{id, user}}>
<.link
phx-click={JS.push("delete", value: %{id: user.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
<.modal :if={@live_action in [:new, :edit]} id="user-modal" show on_cancel={JS.patch(~p"/users")}>
<.live_component
module={HomemanWeb.UserLive.FormComponent}
id={@user.id || :new}
title={@page_title}
action={@live_action}
user={@user}
patch={~p"/users"}
/>
</.modal>

View file

@ -0,0 +1,21 @@
defmodule HomemanWeb.UserLive.Show do
use HomemanWeb, :live_view
alias Homeman.Accounts
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:user, Accounts.get_user!(id))}
end
defp page_title(:show), do: "Show User"
defp page_title(:edit), do: "Edit User"
end

View file

@ -0,0 +1,28 @@
<.header>
User <%= @user.id %>
<:subtitle>This is a user record from your database.</:subtitle>
<:actions>
<.link patch={~p"/users/#{@user}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit user</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Name"><%= @user.name %></:item>
<:item title="Avatar url"><%= @user.avatar_url %></:item>
<:item title="Color"><%= @user.color %></:item>
</.list>
<.back navigate={~p"/users"}>Back to users</.back>
<.modal :if={@live_action == :edit} id="user-modal" show on_cancel={JS.patch(~p"/users/#{@user}")}>
<.live_component
module={HomemanWeb.UserLive.FormComponent}
id={@user.id}
title={@page_title}
action={@live_action}
user={@user}
patch={~p"/users/#{@user}"}
/>
</.modal>

View file

@ -18,6 +18,29 @@ defmodule HomemanWeb.Router do
pipe_through :browser pipe_through :browser
get "/", PageController, :home get "/", PageController, :home
live "/tasks", TaskLive.Index, :index
live "/tasks/new", TaskLive.Index, :new
live "/tasks/:id/edit", TaskLive.Index, :edit
live "/tasks/:id", TaskLive.Show, :show
live "/tasks/:id/show/edit", TaskLive.Show, :edit
live "/todos", TodoLive.Index, :index
live "/todos/new", TodoLive.Index, :new
live "/todos/:id/edit", TodoLive.Index, :edit
live "/todos/:id", TodoLive.Show, :show
live "/todos/:id/show/edit", TodoLive.Show, :edit
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id/edit", UserLive.Index, :edit
live "/users/:id", UserLive.Show, :show
live "/users/:id/show/edit", UserLive.Show, :edit
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.

View file

@ -0,0 +1,14 @@
defmodule Homeman.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users, primary_key: false) do
add :id, :binary_id, primary_key: true
add :name, :string
add :avatar_url, :string, null: true
add :color, :string
timestamps(type: :utc_datetime)
end
end
end

View file

@ -0,0 +1,17 @@
defmodule Homeman.Repo.Migrations.CreateTodos do
use Ecto.Migration
def change do
create table(:todos, primary_key: false) do
add :id, :binary_id, primary_key: true
add :description, :string
add :emoji, :string, null: true
add :completed_at, :naive_datetime, null: true
add :assignee_user_id, references(:users, on_delete: :nothing, type: :binary_id), null: true
timestamps(type: :utc_datetime)
end
create index(:todos, [:assignee_user_id])
end
end

View file

@ -0,0 +1,15 @@
defmodule Homeman.Repo.Migrations.CreateTasks do
use Ecto.Migration
def change do
create table(:tasks, primary_key: false) do
add :id, :binary_id, primary_key: true
add :description, :string
add :emoji, :string, null: true
add :completed_at, :naive_datetime, null: true
add :phase, :string
timestamps(type: :utc_datetime)
end
end
end

View file

@ -9,3 +9,9 @@
# #
# We recommend using the bang functions (`insert!`, `update!` # We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong. # and so on) as they will fail if something goes wrong.
%{
name: "Daddy",
color: "#00aaff",
avatar_url: "/uploads/space.png",
}

View file

@ -0,0 +1,181 @@
defmodule Homeman.AccountsTest do
use Homeman.DataCase
alias Homeman.Accounts
describe "users" do
alias Homeman.Accounts.User
import Homeman.AccountsFixtures
@invalid_attrs %{name: nil, color: nil, avatar_url: nil}
test "list_users/0 returns all users" do
user = user_fixture()
assert Accounts.list_users() == [user]
end
test "get_user!/1 returns the user with given id" do
user = user_fixture()
assert Accounts.get_user!(user.id) == user
end
test "create_user/1 with valid data creates a user" do
valid_attrs = %{name: "some name", color: "some color", avatar_url: "some avatar_url"}
assert {:ok, %User{} = user} = Accounts.create_user(valid_attrs)
assert user.name == "some name"
assert user.color == "some color"
assert user.avatar_url == "some avatar_url"
end
test "create_user/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Accounts.create_user(@invalid_attrs)
end
test "update_user/2 with valid data updates the user" do
user = user_fixture()
update_attrs = %{name: "some updated name", color: "some updated color", avatar_url: "some updated avatar_url"}
assert {:ok, %User{} = user} = Accounts.update_user(user, update_attrs)
assert user.name == "some updated name"
assert user.color == "some updated color"
assert user.avatar_url == "some updated avatar_url"
end
test "update_user/2 with invalid data returns error changeset" do
user = user_fixture()
assert {:error, %Ecto.Changeset{}} = Accounts.update_user(user, @invalid_attrs)
assert user == Accounts.get_user!(user.id)
end
test "delete_user/1 deletes the user" do
user = user_fixture()
assert {:ok, %User{}} = Accounts.delete_user(user)
assert_raise Ecto.NoResultsError, fn -> Accounts.get_user!(user.id) end
end
test "change_user/1 returns a user changeset" do
user = user_fixture()
assert %Ecto.Changeset{} = Accounts.change_user(user)
end
end
describe "todos" do
alias Homeman.Accounts.Todo
import Homeman.AccountsFixtures
@invalid_attrs %{description: nil, emoji: nil, completed_at: nil}
test "list_todos/0 returns all todos" do
todo = todo_fixture()
assert Accounts.list_todos() == [todo]
end
test "get_todo!/1 returns the todo with given id" do
todo = todo_fixture()
assert Accounts.get_todo!(todo.id) == todo
end
test "create_todo/1 with valid data creates a todo" do
valid_attrs = %{description: "some description", emoji: "some emoji", completed_at: ~N[2024-01-21 05:40:00]}
assert {:ok, %Todo{} = todo} = Accounts.create_todo(valid_attrs)
assert todo.description == "some description"
assert todo.emoji == "some emoji"
assert todo.completed_at == ~N[2024-01-21 05:40:00]
end
test "create_todo/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Accounts.create_todo(@invalid_attrs)
end
test "update_todo/2 with valid data updates the todo" do
todo = todo_fixture()
update_attrs = %{description: "some updated description", emoji: "some updated emoji", completed_at: ~N[2024-01-22 05:40:00]}
assert {:ok, %Todo{} = todo} = Accounts.update_todo(todo, update_attrs)
assert todo.description == "some updated description"
assert todo.emoji == "some updated emoji"
assert todo.completed_at == ~N[2024-01-22 05:40:00]
end
test "update_todo/2 with invalid data returns error changeset" do
todo = todo_fixture()
assert {:error, %Ecto.Changeset{}} = Accounts.update_todo(todo, @invalid_attrs)
assert todo == Accounts.get_todo!(todo.id)
end
test "delete_todo/1 deletes the todo" do
todo = todo_fixture()
assert {:ok, %Todo{}} = Accounts.delete_todo(todo)
assert_raise Ecto.NoResultsError, fn -> Accounts.get_todo!(todo.id) end
end
test "change_todo/1 returns a todo changeset" do
todo = todo_fixture()
assert %Ecto.Changeset{} = Accounts.change_todo(todo)
end
end
describe "tasks" do
alias Homeman.Accounts.Task
import Homeman.AccountsFixtures
@invalid_attrs %{description: nil, phase: nil, emoji: nil, completed_at: nil}
test "list_tasks/0 returns all tasks" do
task = task_fixture()
assert Accounts.list_tasks() == [task]
end
test "get_task!/1 returns the task with given id" do
task = task_fixture()
assert Accounts.get_task!(task.id) == task
end
test "create_task/1 with valid data creates a task" do
valid_attrs = %{description: "some description", phase: "some phase", emoji: "some emoji", completed_at: ~N[2024-01-21 05:40:00]}
assert {:ok, %Task{} = task} = Accounts.create_task(valid_attrs)
assert task.description == "some description"
assert task.phase == "some phase"
assert task.emoji == "some emoji"
assert task.completed_at == ~N[2024-01-21 05:40:00]
end
test "create_task/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Accounts.create_task(@invalid_attrs)
end
test "update_task/2 with valid data updates the task" do
task = task_fixture()
update_attrs = %{description: "some updated description", phase: "some updated phase", emoji: "some updated emoji", completed_at: ~N[2024-01-22 05:40:00]}
assert {:ok, %Task{} = task} = Accounts.update_task(task, update_attrs)
assert task.description == "some updated description"
assert task.phase == "some updated phase"
assert task.emoji == "some updated emoji"
assert task.completed_at == ~N[2024-01-22 05:40:00]
end
test "update_task/2 with invalid data returns error changeset" do
task = task_fixture()
assert {:error, %Ecto.Changeset{}} = Accounts.update_task(task, @invalid_attrs)
assert task == Accounts.get_task!(task.id)
end
test "delete_task/1 deletes the task" do
task = task_fixture()
assert {:ok, %Task{}} = Accounts.delete_task(task)
assert_raise Ecto.NoResultsError, fn -> Accounts.get_task!(task.id) end
end
test "change_task/1 returns a task changeset" do
task = task_fixture()
assert %Ecto.Changeset{} = Accounts.change_task(task)
end
end
end

View file

@ -0,0 +1,113 @@
defmodule HomemanWeb.TaskLiveTest do
use HomemanWeb.ConnCase
import Phoenix.LiveViewTest
import Homeman.AccountsFixtures
@create_attrs %{description: "some description", phase: "some phase", emoji: "some emoji", completed_at: "2024-01-21T05:40:00"}
@update_attrs %{description: "some updated description", phase: "some updated phase", emoji: "some updated emoji", completed_at: "2024-01-22T05:40:00"}
@invalid_attrs %{description: nil, phase: nil, emoji: nil, completed_at: nil}
defp create_task(_) do
task = task_fixture()
%{task: task}
end
describe "Index" do
setup [:create_task]
test "lists all tasks", %{conn: conn, task: task} do
{:ok, _index_live, html} = live(conn, ~p"/tasks")
assert html =~ "Listing Tasks"
assert html =~ task.description
end
test "saves new task", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/tasks")
assert index_live |> element("a", "New Task") |> render_click() =~
"New Task"
assert_patch(index_live, ~p"/tasks/new")
assert index_live
|> form("#task-form", task: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#task-form", task: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/tasks")
html = render(index_live)
assert html =~ "Task created successfully"
assert html =~ "some description"
end
test "updates task in listing", %{conn: conn, task: task} do
{:ok, index_live, _html} = live(conn, ~p"/tasks")
assert index_live |> element("#tasks-#{task.id} a", "Edit") |> render_click() =~
"Edit Task"
assert_patch(index_live, ~p"/tasks/#{task}/edit")
assert index_live
|> form("#task-form", task: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#task-form", task: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/tasks")
html = render(index_live)
assert html =~ "Task updated successfully"
assert html =~ "some updated description"
end
test "deletes task in listing", %{conn: conn, task: task} do
{:ok, index_live, _html} = live(conn, ~p"/tasks")
assert index_live |> element("#tasks-#{task.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#tasks-#{task.id}")
end
end
describe "Show" do
setup [:create_task]
test "displays task", %{conn: conn, task: task} do
{:ok, _show_live, html} = live(conn, ~p"/tasks/#{task}")
assert html =~ "Show Task"
assert html =~ task.description
end
test "updates task within modal", %{conn: conn, task: task} do
{:ok, show_live, _html} = live(conn, ~p"/tasks/#{task}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Task"
assert_patch(show_live, ~p"/tasks/#{task}/show/edit")
assert show_live
|> form("#task-form", task: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#task-form", task: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/tasks/#{task}")
html = render(show_live)
assert html =~ "Task updated successfully"
assert html =~ "some updated description"
end
end
end

View file

@ -0,0 +1,113 @@
defmodule HomemanWeb.TodoLiveTest do
use HomemanWeb.ConnCase
import Phoenix.LiveViewTest
import Homeman.AccountsFixtures
@create_attrs %{description: "some description", emoji: "some emoji", completed_at: "2024-01-21T05:40:00"}
@update_attrs %{description: "some updated description", emoji: "some updated emoji", completed_at: "2024-01-22T05:40:00"}
@invalid_attrs %{description: nil, emoji: nil, completed_at: nil}
defp create_todo(_) do
todo = todo_fixture()
%{todo: todo}
end
describe "Index" do
setup [:create_todo]
test "lists all todos", %{conn: conn, todo: todo} do
{:ok, _index_live, html} = live(conn, ~p"/todos")
assert html =~ "Listing Todos"
assert html =~ todo.description
end
test "saves new todo", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/todos")
assert index_live |> element("a", "New Todo") |> render_click() =~
"New Todo"
assert_patch(index_live, ~p"/todos/new")
assert index_live
|> form("#todo-form", todo: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#todo-form", todo: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/todos")
html = render(index_live)
assert html =~ "Todo created successfully"
assert html =~ "some description"
end
test "updates todo in listing", %{conn: conn, todo: todo} do
{:ok, index_live, _html} = live(conn, ~p"/todos")
assert index_live |> element("#todos-#{todo.id} a", "Edit") |> render_click() =~
"Edit Todo"
assert_patch(index_live, ~p"/todos/#{todo}/edit")
assert index_live
|> form("#todo-form", todo: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#todo-form", todo: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/todos")
html = render(index_live)
assert html =~ "Todo updated successfully"
assert html =~ "some updated description"
end
test "deletes todo in listing", %{conn: conn, todo: todo} do
{:ok, index_live, _html} = live(conn, ~p"/todos")
assert index_live |> element("#todos-#{todo.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#todos-#{todo.id}")
end
end
describe "Show" do
setup [:create_todo]
test "displays todo", %{conn: conn, todo: todo} do
{:ok, _show_live, html} = live(conn, ~p"/todos/#{todo}")
assert html =~ "Show Todo"
assert html =~ todo.description
end
test "updates todo within modal", %{conn: conn, todo: todo} do
{:ok, show_live, _html} = live(conn, ~p"/todos/#{todo}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Todo"
assert_patch(show_live, ~p"/todos/#{todo}/show/edit")
assert show_live
|> form("#todo-form", todo: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#todo-form", todo: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/todos/#{todo}")
html = render(show_live)
assert html =~ "Todo updated successfully"
assert html =~ "some updated description"
end
end
end

View file

@ -0,0 +1,113 @@
defmodule HomemanWeb.UserLiveTest do
use HomemanWeb.ConnCase
import Phoenix.LiveViewTest
import Homeman.AccountsFixtures
@create_attrs %{name: "some name", color: "some color", avatar_url: "some avatar_url"}
@update_attrs %{name: "some updated name", color: "some updated color", avatar_url: "some updated avatar_url"}
@invalid_attrs %{name: nil, color: nil, avatar_url: nil}
defp create_user(_) do
user = user_fixture()
%{user: user}
end
describe "Index" do
setup [:create_user]
test "lists all users", %{conn: conn, user: user} do
{:ok, _index_live, html} = live(conn, ~p"/users")
assert html =~ "Listing Users"
assert html =~ user.name
end
test "saves new user", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/users")
assert index_live |> element("a", "New User") |> render_click() =~
"New User"
assert_patch(index_live, ~p"/users/new")
assert index_live
|> form("#user-form", user: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#user-form", user: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/users")
html = render(index_live)
assert html =~ "User created successfully"
assert html =~ "some name"
end
test "updates user in listing", %{conn: conn, user: user} do
{:ok, index_live, _html} = live(conn, ~p"/users")
assert index_live |> element("#users-#{user.id} a", "Edit") |> render_click() =~
"Edit User"
assert_patch(index_live, ~p"/users/#{user}/edit")
assert index_live
|> form("#user-form", user: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#user-form", user: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/users")
html = render(index_live)
assert html =~ "User updated successfully"
assert html =~ "some updated name"
end
test "deletes user in listing", %{conn: conn, user: user} do
{:ok, index_live, _html} = live(conn, ~p"/users")
assert index_live |> element("#users-#{user.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#users-#{user.id}")
end
end
describe "Show" do
setup [:create_user]
test "displays user", %{conn: conn, user: user} do
{:ok, _show_live, html} = live(conn, ~p"/users/#{user}")
assert html =~ "Show User"
assert html =~ user.name
end
test "updates user within modal", %{conn: conn, user: user} do
{:ok, show_live, _html} = live(conn, ~p"/users/#{user}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit User"
assert_patch(show_live, ~p"/users/#{user}/show/edit")
assert show_live
|> form("#user-form", user: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#user-form", user: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/users/#{user}")
html = render(show_live)
assert html =~ "User updated successfully"
assert html =~ "some updated name"
end
end
end

View file

@ -0,0 +1,55 @@
defmodule Homeman.AccountsFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Homeman.Accounts` context.
"""
@doc """
Generate a user.
"""
def user_fixture(attrs \\ %{}) do
{:ok, user} =
attrs
|> Enum.into(%{
avatar_url: "some avatar_url",
color: "some color",
name: "some name"
})
|> Homeman.Accounts.create_user()
user
end
@doc """
Generate a todo.
"""
def todo_fixture(attrs \\ %{}) do
{:ok, todo} =
attrs
|> Enum.into(%{
completed_at: ~N[2024-01-21 05:40:00],
description: "some description",
emoji: "some emoji"
})
|> Homeman.Accounts.create_todo()
todo
end
@doc """
Generate a task.
"""
def task_fixture(attrs \\ %{}) do
{:ok, task} =
attrs
|> Enum.into(%{
completed_at: ~N[2024-01-21 05:40:00],
description: "some description",
emoji: "some emoji",
phase: "some phase"
})
|> Homeman.Accounts.create_task()
task
end
end