diff --git a/lib/chirp/timeline.ex b/lib/chirp/timeline.ex
new file mode 100644
index 0000000..a9c2d76
--- /dev/null
+++ b/lib/chirp/timeline.ex
@@ -0,0 +1,116 @@
+defmodule Chirp.Timeline do
+ @moduledoc """
+ The Timeline context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Chirp.Repo
+
+ alias Chirp.Timeline.Post
+
+ @doc """
+ Returns the list of posts.
+
+ ## Examples
+
+ iex> list_posts()
+ [%Post{}, ...]
+
+ """
+ def list_posts do
+ Repo.all(from p in Post, order_by: [desc: p.id])
+ end
+
+ @doc """
+ Gets a single post.
+
+ Raises `Ecto.NoResultsError` if the Post does not exist.
+
+ ## Examples
+
+ iex> get_post!(123)
+ %Post{}
+
+ iex> get_post!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_post!(id), do: Repo.get!(Post, id)
+
+ @doc """
+ Creates a post.
+
+ ## Examples
+
+ iex> create_post(%{field: value})
+ {:ok, %Post{}}
+
+ iex> create_post(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_post(attrs \\ %{}) do
+ %Post{}
+ |> Post.changeset(attrs)
+ |> Repo.insert()
+ |> broadcast(:post_created)
+ end
+
+ @doc """
+ Updates a post.
+
+ ## Examples
+
+ iex> update_post(post, %{field: new_value})
+ {:ok, %Post{}}
+
+ iex> update_post(post, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_post(%Post{} = post, attrs) do
+ post
+ |> Post.changeset(attrs)
+ |> Repo.update()
+ |> broadcast(:post_updated)
+ end
+
+ @doc """
+ Deletes a post.
+
+ ## Examples
+
+ iex> delete_post(post)
+ {:ok, %Post{}}
+
+ iex> delete_post(post)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_post(%Post{} = post) do
+ Repo.delete(post)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking post changes.
+
+ ## Examples
+
+ iex> change_post(post)
+ %Ecto.Changeset{data: %Post{}}
+
+ """
+ def change_post(%Post{} = post, attrs \\ %{}) do
+ Post.changeset(post, attrs)
+ end
+
+ def subscribe do
+ Phoenix.PubSub.subscribe(Chirp.PubSub,"posts")
+ end
+
+ defp broadcast({:error, _reason} = error, _event), do: error
+ defp broadcast({:ok,post}, event) do
+ Phoenix.PubSub.broadcast(Chirp.PubSub, "posts", {event,post})
+ {:ok, post}
+ end
+end
diff --git a/lib/chirp/timeline/post.ex b/lib/chirp/timeline/post.ex
new file mode 100644
index 0000000..bd5fd39
--- /dev/null
+++ b/lib/chirp/timeline/post.ex
@@ -0,0 +1,22 @@
+defmodule Chirp.Timeline.Post do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "posts" do
+ field :body, :string
+ field :likes_count, :integer, default: 0
+ field :reposts_count, :integer, default: 0
+ field :username, :string, default: "julz"
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(post, attrs) do
+ post
+ |> cast(attrs, [:body])
+ |> validate_required([:body])
+ |> validate_length(:body, min: 2, max: 250)
+
+ end
+end
diff --git a/lib/chirp_web/live/post_live/form_component.ex b/lib/chirp_web/live/post_live/form_component.ex
new file mode 100644
index 0000000..f3d6f44
--- /dev/null
+++ b/lib/chirp_web/live/post_live/form_component.ex
@@ -0,0 +1,81 @@
+defmodule ChirpWeb.PostLive.FormComponent do
+ use ChirpWeb, :live_component
+
+ alias Chirp.Timeline
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ <%= @title %>
+ <:subtitle>Use this form to manage post records in your database.
+
+
+ <.simple_form
+ :let={f}
+ for={@changeset}
+ id="post-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input field={{f, :body}} type="textarea" label="body" />
+ <:actions>
+ <.button phx-disable-with="Saving...">Save Post
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{post: post} = assigns, socket) do
+ changeset = Timeline.change_post(post)
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:changeset, changeset)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"post" => post_params}, socket) do
+ changeset =
+ socket.assigns.post
+ |> Timeline.change_post(post_params)
+ |> Map.put(:action, :validate)
+
+ {:noreply, assign(socket, :changeset, changeset)}
+ end
+
+ def handle_event("save", %{"post" => post_params}, socket) do
+ save_post(socket, socket.assigns.action, post_params)
+ end
+
+ defp save_post(socket, :edit, post_params) do
+ case Timeline.update_post(socket.assigns.post, post_params) do
+ {:ok, _post} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Post updated successfully")
+ |> push_navigate(to: socket.assigns.navigate)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, :changeset, changeset)}
+ end
+ end
+
+ defp save_post(socket, :new, post_params) do
+ case Timeline.create_post(post_params) do
+ {:ok, _post} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Post created successfully")
+ |> push_navigate(to: socket.assigns.navigate)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, changeset: changeset)}
+ end
+ end
+end
diff --git a/lib/chirp_web/live/post_live/index.ex b/lib/chirp_web/live/post_live/index.ex
new file mode 100644
index 0000000..91fe702
--- /dev/null
+++ b/lib/chirp_web/live/post_live/index.ex
@@ -0,0 +1,61 @@
+defmodule ChirpWeb.PostLive.Index do
+ use ChirpWeb, :live_view
+
+ alias Chirp.Timeline
+ alias Chirp.Timeline.Post
+
+ @impl true
+ def mount(_params, _session, socket) do
+ if connected?(socket), do: Timeline.subscribe()
+ {:ok, assign(socket, :posts, list_posts()), temporary_assigns: [posts: []]}
+ 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 Post")
+ |> assign(:post, Timeline.get_post!(id))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Post")
+ |> assign(:post, %Post{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Posts")
+ |> assign(:post, nil)
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ post = Timeline.get_post!(id)
+ {:ok, _} = Timeline.delete_post(post)
+
+ {:noreply, assign(socket, :posts, list_posts())}
+ end
+ def handle_event("like", %{"id" => _id}, socket) do
+ {:noreply,socket}
+ end
+ def handle_event("repost", %{"id" => _id}, socket) do
+ {:noreply,socket}
+ end
+
+ def handle_info({:post_created,post}, socket) do
+ {:noreply, update(socket, :posts, fn posts-> [post|posts] end)}
+ end
+
+ def handle_info({:post_updated,post}, socket) do
+ {:noreply, update(socket, :posts, fn posts-> [post|posts] end)}
+ end
+
+ defp list_posts do
+ Timeline.list_posts()
+ end
+end
diff --git a/lib/chirp_web/live/post_live/index.html.heex b/lib/chirp_web/live/post_live/index.html.heex
new file mode 100644
index 0000000..4f604fd
--- /dev/null
+++ b/lib/chirp_web/live/post_live/index.html.heex
@@ -0,0 +1,34 @@
+<.header>
+ Timeline
+ <:actions>
+ <.link patch={~p"/posts/new"}>
+ <.button>New Post
+
+
+
+
+
+
+ <%= for post <- @posts do %>
+ <.live_component
+ module={ChirpWeb.PostLive.PostComponent}
+ id={post.id}
+ post={post}
+ />
+ <% end %>
+
+
+
+
+<%= if @live_action in [:new, :edit] do %>
+ <.modal id="post-modal" show on_cancel={JS.navigate(~p"/posts")}>
+ <.live_component
+ module={ChirpWeb.PostLive.FormComponent}
+ id={@post.id || :new}
+ title={@page_title}
+ action={@live_action}
+ post={@post}
+ navigate={~p"/posts"}
+ />
+
+<% end %>
diff --git a/lib/chirp_web/live/post_live/post_component.ex b/lib/chirp_web/live/post_live/post_component.ex
new file mode 100644
index 0000000..a2afa13
--- /dev/null
+++ b/lib/chirp_web/live/post_live/post_component.ex
@@ -0,0 +1,78 @@
+defmodule ChirpWeb.PostLive.PostComponent do
+ use ChirpWeb, :live_component
+
+ @spec render(any) :: Phoenix.LiveView.Rendered.t()
+ def render(assigns) do
+ ~H"""
+
+
+
+
+
+
<%= @post.username %>
+
+
+
+
+
+
<%= @post.body %>
+
+
+ <.link navigate={~p"/posts/#{@post}"} class="inline-flex items-center text-blue-600 hover:underline">
+ Show
+
+
+
+
+
+ <.link phx-click="like" phx-value-id={@post.id}>
+
+
+
+ <%= @post.likes_count %>
+
+
+
+
+ <.link phx-click="repost" phx-value-id={@post.id}>
+
+
+
+ <%= @post.reposts_count %>
+
+
+
+ <.link patch={~p"/posts/#{@post}/edit"}>Edit
+
+
+ <.link phx-click={JS.push("delete", value: %{id: @post.id})} data-confirm="Are you sure?">
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ end
+
+end
diff --git a/lib/chirp_web/live/post_live/show.ex b/lib/chirp_web/live/post_live/show.ex
new file mode 100644
index 0000000..f69695b
--- /dev/null
+++ b/lib/chirp_web/live/post_live/show.ex
@@ -0,0 +1,21 @@
+defmodule ChirpWeb.PostLive.Show do
+ use ChirpWeb, :live_view
+
+ alias Chirp.Timeline
+
+ @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(:post, Timeline.get_post!(id))}
+ end
+
+ defp page_title(:show), do: "Show Post"
+ defp page_title(:edit), do: "Edit Post"
+end
diff --git a/lib/chirp_web/live/post_live/show.html.heex b/lib/chirp_web/live/post_live/show.html.heex
new file mode 100644
index 0000000..eeb8a9e
--- /dev/null
+++ b/lib/chirp_web/live/post_live/show.html.heex
@@ -0,0 +1,32 @@
+<.header>
+ Post <%= @post.id %>
+ <:subtitle>This is a post record from your database.
+ <:actions>
+ <.link patch={~p"/posts/#{@post}/show/edit"} phx-click={JS.push_focus()}>
+ <.button>Edit post
+
+
+
+
+<.list>
+ <:item title="Username"><%= @post.username %>
+ <:item title="Body"><%= @post.body %>
+ <:item title="Likes count"><%= @post.likes_count %>
+ <:item title="Reposts count"><%= @post.reposts_count %>
+
+
+<.back navigate={~p"/posts"}>Back to posts
+
+<%= if @live_action in [:edit] do %>
+ <.modal id="post-modal" show on_cancel={JS.patch(~p"/posts/#{@post}")}>
+ <.live_component
+ module={ChirpWeb.PostLive.FormComponent}
+ id={@post.id}
+ title={@page_title}
+ action={@live_action}
+ post={@post}
+ navigate={~p"/posts/#{@post}"}
+ />
+
+<% end %>
+
diff --git a/lib/chirp_web/router.ex b/lib/chirp_web/router.ex
index f18aa43..80ff7b0 100644
--- a/lib/chirp_web/router.ex
+++ b/lib/chirp_web/router.ex
@@ -18,6 +18,15 @@ defmodule ChirpWeb.Router do
pipe_through :browser
get "/", PageController, :index
+
+ live "/posts", PostLive.Index, :index
+ live "/posts/new", PostLive.Index, :new
+ live "/posts/:id/edit", PostLive.Index, :edit
+
+ live "/posts/:id", PostLive.Show, :show
+ live "/posts/:id/show/edit", PostLive.Show, :edit
+
+
end
# Other scopes may use custom stacks.
diff --git a/test/chirp/timeline_test.exs b/test/chirp/timeline_test.exs
new file mode 100644
index 0000000..df96977
--- /dev/null
+++ b/test/chirp/timeline_test.exs
@@ -0,0 +1,65 @@
+defmodule Chirp.TimelineTest do
+ use Chirp.DataCase
+
+ alias Chirp.Timeline
+
+ describe "posts" do
+ alias Chirp.Timeline.Post
+
+ import Chirp.TimelineFixtures
+
+ @invalid_attrs %{body: nil, likes_count: nil, reposts_count: nil, username: nil}
+
+ test "list_posts/0 returns all posts" do
+ post = post_fixture()
+ assert Timeline.list_posts() == [post]
+ end
+
+ test "get_post!/1 returns the post with given id" do
+ post = post_fixture()
+ assert Timeline.get_post!(post.id) == post
+ end
+
+ test "create_post/1 with valid data creates a post" do
+ valid_attrs = %{body: "some body", likes_count: 42, reposts_count: 42, username: "some username"}
+
+ assert {:ok, %Post{} = post} = Timeline.create_post(valid_attrs)
+ assert post.body == "some body"
+ assert post.likes_count == 42
+ assert post.reposts_count == 42
+ assert post.username == "some username"
+ end
+
+ test "create_post/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Timeline.create_post(@invalid_attrs)
+ end
+
+ test "update_post/2 with valid data updates the post" do
+ post = post_fixture()
+ update_attrs = %{body: "some updated body", likes_count: 43, reposts_count: 43, username: "some updated username"}
+
+ assert {:ok, %Post{} = post} = Timeline.update_post(post, update_attrs)
+ assert post.body == "some updated body"
+ assert post.likes_count == 43
+ assert post.reposts_count == 43
+ assert post.username == "some updated username"
+ end
+
+ test "update_post/2 with invalid data returns error changeset" do
+ post = post_fixture()
+ assert {:error, %Ecto.Changeset{}} = Timeline.update_post(post, @invalid_attrs)
+ assert post == Timeline.get_post!(post.id)
+ end
+
+ test "delete_post/1 deletes the post" do
+ post = post_fixture()
+ assert {:ok, %Post{}} = Timeline.delete_post(post)
+ assert_raise Ecto.NoResultsError, fn -> Timeline.get_post!(post.id) end
+ end
+
+ test "change_post/1 returns a post changeset" do
+ post = post_fixture()
+ assert %Ecto.Changeset{} = Timeline.change_post(post)
+ end
+ end
+end
diff --git a/test/chirp_web/live/post_live_test.exs b/test/chirp_web/live/post_live_test.exs
new file mode 100644
index 0000000..f956156
--- /dev/null
+++ b/test/chirp_web/live/post_live_test.exs
@@ -0,0 +1,110 @@
+defmodule ChirpWeb.PostLiveTest do
+ use ChirpWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import Chirp.TimelineFixtures
+
+ @create_attrs %{body: "some body", likes_count: 42, reposts_count: 42, username: "some username"}
+ @update_attrs %{body: "some updated body", likes_count: 43, reposts_count: 43, username: "some updated username"}
+ @invalid_attrs %{body: nil, likes_count: nil, reposts_count: nil, username: nil}
+
+ defp create_post(_) do
+ post = post_fixture()
+ %{post: post}
+ end
+
+ describe "Index" do
+ setup [:create_post]
+
+ test "lists all posts", %{conn: conn, post: post} do
+ {:ok, _index_live, html} = live(conn, ~p"/posts")
+
+ assert html =~ "Listing Posts"
+ assert html =~ post.body
+ end
+
+ test "saves new post", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, ~p"/posts")
+
+ assert index_live |> element("a", "New Post") |> render_click() =~
+ "New Post"
+
+ assert_patch(index_live, ~p"/posts/new")
+
+ assert index_live
+ |> form("#post-form", post: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ {:ok, _, html} =
+ index_live
+ |> form("#post-form", post: @create_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/posts")
+
+ assert html =~ "Post created successfully"
+ assert html =~ "some body"
+ end
+
+ test "updates post in listing", %{conn: conn, post: post} do
+ {:ok, index_live, _html} = live(conn, ~p"/posts")
+
+ assert index_live |> element("#posts-#{post.id} a", "Edit") |> render_click() =~
+ "Edit Post"
+
+ assert_patch(index_live, ~p"/posts/#{post}/edit")
+
+ assert index_live
+ |> form("#post-form", post: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ {:ok, _, html} =
+ index_live
+ |> form("#post-form", post: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/posts")
+
+ assert html =~ "Post updated successfully"
+ assert html =~ "some updated body"
+ end
+
+ test "deletes post in listing", %{conn: conn, post: post} do
+ {:ok, index_live, _html} = live(conn, ~p"/posts")
+
+ assert index_live |> element("#posts-#{post.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#post-#{post.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_post]
+
+ test "displays post", %{conn: conn, post: post} do
+ {:ok, _show_live, html} = live(conn, ~p"/posts/#{post}")
+
+ assert html =~ "Show Post"
+ assert html =~ post.body
+ end
+
+ test "updates post within modal", %{conn: conn, post: post} do
+ {:ok, show_live, _html} = live(conn, ~p"/posts/#{post}")
+
+ assert show_live |> element("a", "Edit") |> render_click() =~
+ "Edit Post"
+
+ assert_patch(show_live, ~p"/posts/#{post}/show/edit")
+
+ assert show_live
+ |> form("#post-form", post: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ {:ok, _, html} =
+ show_live
+ |> form("#post-form", post: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/posts/#{post}")
+
+ assert html =~ "Post updated successfully"
+ assert html =~ "some updated body"
+ end
+ end
+end
diff --git a/test/support/fixtures/timeline_fixtures.ex b/test/support/fixtures/timeline_fixtures.ex
new file mode 100644
index 0000000..38ecaf5
--- /dev/null
+++ b/test/support/fixtures/timeline_fixtures.ex
@@ -0,0 +1,23 @@
+defmodule Chirp.TimelineFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Chirp.Timeline` context.
+ """
+
+ @doc """
+ Generate a post.
+ """
+ def post_fixture(attrs \\ %{}) do
+ {:ok, post} =
+ attrs
+ |> Enum.into(%{
+ body: "some body",
+ likes_count: 42,
+ reposts_count: 42,
+ username: "some username"
+ })
+ |> Chirp.Timeline.create_post()
+
+ post
+ end
+end