From d02833e47270c7801baf693a1ac6d21498695c0f Mon Sep 17 00:00:00 2001
From: sloane <git@sloanelybutsurely.com>
Date: Sat, 22 Feb 2025 12:35:38 -0500
Subject: [PATCH] create simple posts

---
 lib/cms/posts.ex                              | 25 ++++++++
 lib/cms/posts/post.ex                         | 20 ++++++
 lib/cms_web/components/layouts/app.html.heex  | 12 ++--
 lib/cms_web/controllers/post_controller.ex    | 21 +++++++
 lib/cms_web/controllers/post_html.ex          |  6 ++
 .../controllers/post_html/index.html.heex     |  6 ++
 .../controllers/post_html/show.html.heex      |  5 ++
 lib/cms_web/live/post_live.ex                 | 62 +++++++++++++++++++
 lib/cms_web/router.ex                         |  8 ++-
 .../20250222164951_add_posts_table.exs        | 13 ++++
 10 files changed, 171 insertions(+), 7 deletions(-)
 create mode 100644 lib/cms/posts.ex
 create mode 100644 lib/cms/posts/post.ex
 create mode 100644 lib/cms_web/controllers/post_controller.ex
 create mode 100644 lib/cms_web/controllers/post_html.ex
 create mode 100644 lib/cms_web/controllers/post_html/index.html.heex
 create mode 100644 lib/cms_web/controllers/post_html/show.html.heex
 create mode 100644 lib/cms_web/live/post_live.ex
 create mode 100644 priv/repo/migrations/20250222164951_add_posts_table.exs

diff --git a/lib/cms/posts.ex b/lib/cms/posts.ex
new file mode 100644
index 0000000..8bcf248
--- /dev/null
+++ b/lib/cms/posts.ex
@@ -0,0 +1,25 @@
+defmodule CMS.Posts do
+  @moduledoc false
+  alias CMS.Posts.Post
+  alias CMS.Repo
+
+  def create_post(attrs) do
+    %Post{}
+    |> Post.changeset(attrs)
+    |> Repo.insert()
+  end
+
+  def update_post(post, attrs) do
+    post
+    |> Post.changeset(attrs)
+    |> Repo.update()
+  end
+
+  def get_post!(id) do
+    Repo.get!(Post, id)
+  end
+
+  def list_posts do
+    Repo.all(Post)
+  end
+end
diff --git a/lib/cms/posts/post.ex b/lib/cms/posts/post.ex
new file mode 100644
index 0000000..3918e97
--- /dev/null
+++ b/lib/cms/posts/post.ex
@@ -0,0 +1,20 @@
+defmodule CMS.Posts.Post do
+  @moduledoc false
+  use Ecto.Schema
+
+  import Ecto.Changeset, warn: false
+
+  @primary_key {:id, :binary_id, autogenerate: true}
+  schema "posts" do
+    field :title, :string
+    field :contents, :string
+
+    timestamps()
+  end
+
+  def changeset(%__MODULE__{} = post, attrs \\ %{}) do
+    post
+    |> cast(attrs, [:title, :contents])
+    |> validate_required([:contents])
+  end
+end
diff --git a/lib/cms_web/components/layouts/app.html.heex b/lib/cms_web/components/layouts/app.html.heex
index fbe0799..223cdd9 100644
--- a/lib/cms_web/components/layouts/app.html.heex
+++ b/lib/cms_web/components/layouts/app.html.heex
@@ -1,11 +1,14 @@
-<div :if={@admin?} class="flex flex-row justify-between py-1 px-3 mb-2 border-b border-slate-100">
+<div
+  :if={@admin?}
+  class="flex flex-row justify-between py-1 px-3 md:mb-2 border-b border-slate-100"
+>
   <section class="flex flex-row gap-x-2">
     <div class="pr-2 border-r border-slate-100">
       <.link navigate={~p"/admin"} class="font-bold">admin mode</.link>
     </div>
     <nav>
       <ul class="flex flex-row">
-        <.link href="#" class="hover:underline">new post</.link>
+        <.link href={~p"/admin/posts/new"} class="hover:underline">new post</.link>
       </ul>
     </nav>
   </section>
@@ -21,12 +24,11 @@
     <.link href={~p"/"} class="font-bold hover:underline">sloanelybutsurely.com</.link>
     <nav>
       <ul>
-        <li><.link href={~p"/writing"} class="hover:underline">writing</.link></li>
-        <li><.link href={~p"/microblog"} class="hover:underline">microblog</.link></li>
+        <li><.link href={~p"/posts"} class="hover:underline">writing</.link></li>
       </ul>
     </nav>
   </section>
-  <main class="p-2">
+  <main class="p-2 w-full">
     {@inner_content}
   </main>
 </div>
diff --git a/lib/cms_web/controllers/post_controller.ex b/lib/cms_web/controllers/post_controller.ex
new file mode 100644
index 0000000..22b3b62
--- /dev/null
+++ b/lib/cms_web/controllers/post_controller.ex
@@ -0,0 +1,21 @@
+defmodule CMSWeb.PostController do
+  use CMSWeb, :controller
+
+  alias CMS.Posts
+
+  def index(conn, _params) do
+    posts = Posts.list_posts()
+
+    conn
+    |> assign(:posts, posts)
+    |> render(:index)
+  end
+
+  def show(conn, %{"post_id" => post_id}) do
+    post = Posts.get_post!(post_id)
+
+    conn
+    |> assign(:post, post)
+    |> render(:show)
+  end
+end
diff --git a/lib/cms_web/controllers/post_html.ex b/lib/cms_web/controllers/post_html.ex
new file mode 100644
index 0000000..f3f2082
--- /dev/null
+++ b/lib/cms_web/controllers/post_html.ex
@@ -0,0 +1,6 @@
+defmodule CMSWeb.PostHTML do
+  @moduledoc false
+  use CMSWeb, :html
+
+  embed_templates "post_html/*"
+end
diff --git a/lib/cms_web/controllers/post_html/index.html.heex b/lib/cms_web/controllers/post_html/index.html.heex
new file mode 100644
index 0000000..30c87c7
--- /dev/null
+++ b/lib/cms_web/controllers/post_html/index.html.heex
@@ -0,0 +1,6 @@
+<%= for post <- @posts do %>
+  <article id={"post-#{post.id}"}>
+    <h2><.link href={~p"/posts/#{post}"}>{post.title}</.link></h2>
+    <p>{post.contents}</p>
+  </article>
+<% end %>
diff --git a/lib/cms_web/controllers/post_html/show.html.heex b/lib/cms_web/controllers/post_html/show.html.heex
new file mode 100644
index 0000000..d037a53
--- /dev/null
+++ b/lib/cms_web/controllers/post_html/show.html.heex
@@ -0,0 +1,5 @@
+<header class="flex flex-row justify-between">
+  <h1>{@post.title}</h1>
+  <.link :if={@admin?} href={~p"/admin/posts/#{@post}"}>edit</.link>
+</header>
+<p>{@post.contents}</p>
diff --git a/lib/cms_web/live/post_live.ex b/lib/cms_web/live/post_live.ex
new file mode 100644
index 0000000..10a5514
--- /dev/null
+++ b/lib/cms_web/live/post_live.ex
@@ -0,0 +1,62 @@
+defmodule CMSWeb.PostLive do
+  @moduledoc false
+  use CMSWeb, :live_view
+
+  alias CMS.Posts
+  alias CMS.Posts.Post
+
+  @impl true
+  def handle_params(_params, _uri, %{assigns: %{live_action: :new}} = socket) do
+    post = %Post{}
+    changeset = Post.changeset(post)
+
+    socket = assign(socket, post: post, form: to_form(changeset))
+
+    {:noreply, socket}
+  end
+
+  def handle_params(%{"post_id" => post_id}, _uri, %{assigns: %{live_action: :edit}} = socket) do
+    post = Posts.get_post!(post_id)
+
+    changeset = Post.changeset(post)
+
+    socket = assign(socket, post: post, form: to_form(changeset))
+
+    {:noreply, socket}
+  end
+
+  @impl true
+  def handle_event("save_post", %{"post" => attrs}, %{assigns: %{live_action: :new}} = socket) do
+    socket =
+      case Posts.create_post(attrs) do
+        {:ok, post} -> push_navigate(socket, to: ~p"/admin/posts/#{post}")
+        {:error, changeset} -> assign(socket, form: to_form(changeset))
+      end
+
+    {:noreply, socket}
+  end
+
+  def handle_event("save_post", %{"post" => attrs}, %{assigns: %{post: post, live_action: :edit}} = socket) do
+    socket =
+      case Posts.update_post(post, attrs) do
+        {:ok, post} ->
+          assign(socket, post: post, form: post |> Post.changeset() |> to_form())
+
+        {:error, changeset} ->
+          assign(socket, form: to_form(changeset))
+      end
+
+    {:noreply, socket}
+  end
+
+  @impl true
+  def render(assigns) do
+    ~H"""
+    <.form for={@form} class="flex flex-col" phx-submit="save_post">
+      <input type="text" id={@form[:title].id} name={@form[:title].name} value={@form[:title].value} />
+      <textarea id={@form[:contents].id} name={@form[:contents].name}>{@form[:contents].value} </textarea>
+      <button type="submit" class="self-end">save</button>
+    </.form>
+    """
+  end
+end
diff --git a/lib/cms_web/router.ex b/lib/cms_web/router.ex
index 6a5885b..b394f2d 100644
--- a/lib/cms_web/router.ex
+++ b/lib/cms_web/router.ex
@@ -32,8 +32,9 @@ defmodule CMSWeb.Router do
       pipe_through :supports_admin_action
 
       get "/", PageController, :home
-      get "/writing", PageController, :writing
-      get "/microblog", PageController, :microblog
+
+      get "/posts", PostController, :index
+      get "/posts/:post_id", PostController, :show
 
       live "/sign-in", AdminLoginLive
       post "/admin/session/create", AdminSessionController, :create
@@ -45,6 +46,9 @@ defmodule CMSWeb.Router do
       pipe_through :requires_admin
 
       live "/", AdminLive
+
+      live "/posts/new", PostLive, :new
+      live "/posts/:post_id", PostLive, :edit
     end
   end
 
diff --git a/priv/repo/migrations/20250222164951_add_posts_table.exs b/priv/repo/migrations/20250222164951_add_posts_table.exs
new file mode 100644
index 0000000..065b85c
--- /dev/null
+++ b/priv/repo/migrations/20250222164951_add_posts_table.exs
@@ -0,0 +1,13 @@
+defmodule CMS.Repo.Migrations.AddPostsTable do
+  use Ecto.Migration
+
+  def change do
+    create table(:posts, primary_key: false) do
+      add :id, :uuid, primary_key: true
+      add :title, :text
+      add :contents, :text, null: false, default: ""
+
+      timestamps()
+    end
+  end
+end