add statuses, change navigation

This commit is contained in:
sloane 2025-02-22 15:09:28 -05:00
parent 76ccea9619
commit 574d6a42a0
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
20 changed files with 326 additions and 95 deletions

View file

@ -7,14 +7,14 @@ defmodule CMS.Posts.Post do
@primary_key {:id, :binary_id, autogenerate: true}
schema "posts" do
field :title, :string
field :contents, :string
field :body, :string
timestamps()
end
def changeset(%__MODULE__{} = post, attrs \\ %{}) do
post
|> cast(attrs, [:title, :contents])
|> validate_required([:contents])
|> cast(attrs, [:title, :body])
|> validate_required([:body])
end
end

31
lib/cms/statuses.ex Normal file
View file

@ -0,0 +1,31 @@
defmodule CMS.Statuses do
@moduledoc false
import Ecto.Query
alias CMS.Repo
alias CMS.Statuses.Status
def create_status(attrs) do
%Status{}
|> Status.changeset(attrs)
|> Repo.insert()
end
def update_status(status, attrs) do
status
|> Status.changeset(attrs)
|> Repo.update()
end
def get_status!(id) do
Repo.get!(Status, id)
end
def list_statuses do
query =
from status in Status,
order_by: [desc: status.inserted_at]
Repo.all(query)
end
end

View file

@ -0,0 +1,19 @@
defmodule CMS.Statuses.Status do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "statuses" do
field :body
timestamps()
end
def changeset(%__MODULE__{} = status, attrs \\ %{}) do
status
|> cast(attrs, [:body])
|> validate_required([:body])
end
end

View file

@ -52,12 +52,25 @@ defmodule CMSWeb.CoreComponents do
attr :class, :string, default: nil
attr :field, Phoenix.HTML.FormField, required: true
attr :global, :global, include: ~w[required placeholder type]
attr :type, :string, default: "text"
attr :global, :global, include: ~w[required placeholder]
def input(%{type: "textarea"} = assigns) do
~H"""
<textarea
class={["px-2 py-1 border border-gray-400 rounded", @class]}
id={@field.id}
name={@field.name}
{@global}
>{@field.value}</textarea>
"""
end
def input(assigns) do
~H"""
<input
class={["px-1 py-0.5 border border-gray-400 rounded", @class]}
class={["px-2 py-1 border border-gray-400 rounded", @class]}
type={@type}
id={@field.id}
name={@field.name}
value={@field.value}
@ -95,6 +108,7 @@ defmodule CMSWeb.CoreComponents do
attr :format, :string, required: true
attr :value, :any, default: nil
attr :formatter, :atom, default: :default
attr :global, :global
def timex(%{value: nil} = assigns) do
@ -106,8 +120,12 @@ defmodule CMSWeb.CoreComponents do
def timex(assigns) do
~H"""
<time datetime={Timex.format!(@value, "{ISO:Extended:Z}")} {@global}>
{Timex.format!(@value, @format)}
{Timex.format!(@value, @format, timex_formatter(@formatter))}
</time>
"""
end
defp timex_formatter(formatter) do
Module.concat(Timex.Format.DateTime.Formatters, :string.titlecase("#{formatter}"))
end
end

View file

@ -11,4 +11,10 @@ defmodule CMSWeb.Layouts do
use CMSWeb, :html
embed_templates "layouts/*"
defp post?("/writing/" <> _), do: true
defp post?(_), do: false
defp status?("/microblog/" <> _), do: true
defp status?(_), do: false
end

View file

@ -1,50 +1,53 @@
<div
:if={@admin?}
class="sticky top-0 bg-white z-50 flex flex-row justify-between py-1 px-3 md:mb-2 border-b border-gray-200"
>
<section class="flex flex-row gap-x-2">
<div class="pr-2 border-r border-gray-200">
<.a navigate={~p"/admin"} class="font-bold">admin</.a>
</div>
<nav>
<ul class="flex flex-row gap-x-2">
<li>
<.a href={~p"/admin/microblog/new"}>new status</.a>
</li>
<li>
<.a href={~p"/admin/writing/new"}>new draft</.a>
</li>
</ul>
</nav>
</section>
<div class="sticky top-0 z-50 flex flex-row bg-white justify-between py-1 md:mb-2 border-b border-gray-200">
<div class="flex flex-col md:flex-row">
<section class="flex flex-row gap-x-2 md:border-r border-gray-200 px-2">
<.a href={~p"/"} class="font-bold">sloanelybutsurely.com</.a>
<nav>
<ul class="flex flex-row gap-x-2">
<li>
<.a href={~p"/writing"}>writing</.a>
</li>
<li>
<.a href={~p"/microblog"}>microblog</.a>
</li>
</ul>
</nav>
</section>
<section :if={@admin?} class="flex flex-row gap-x-2 px-2">
<nav>
<ul class="flex flex-row gap-x-2">
<li>
<.a navigate={~p"/admin"}>admin</.a>
</li>
<li>
<.a href={~p"/admin/statuses/new"}>new status</.a>
</li>
<li>
<.a href={~p"/admin/posts/new"}>new post</.a>
</li>
<li :if={post?(@current_path)}>
<.a href={~p"/admin/posts/#{@post}"}>edit post</.a>
</li>
<li :if={status?(@current_path)}>
<.a href={~p"/admin/statuses/#{@status}"}>edit status</.a>
</li>
</ul>
</nav>
</section>
</div>
<section class="flex flex-row">
<.a href={~p"/admin/session/destroy?return_to=#{@current_path}"}>
sign out
</.a>
</section>
</div>
<div class="flex flex-col md:flex-row mx-auto max-w-4xl">
<section class="flex flex-col p-2 gap-y-1 border-gray-200 border-b md:border-b-0">
<.a href={~p"/"} class="font-bold">sloanelybutsurely.com</.a>
<nav>
<ul>
<li>
<.a href={~p"/writing"}>writing</.a>
</li>
<li>
<.a href={~p"/microblog"}>microblog</.a>
</li>
</ul>
</nav>
</section>
<main class="p-2 w-full">
{@inner_content}
</main>
</div>
<div
:if={not @admin?}
class="fixed right-0 bottom-0 p-2 text-transparent underline hover:text-current"
>
<.a href={~p"/sign-in?return_to=#{@current_path}"}>sign in</.a>
<.a :if={@admin?} class="px-2" href={~p"/admin/session/destroy?return_to=#{@current_path}"}>
sign out
</.a>
<.a
:if={!@admin?}
class="px-2 text-transparent hover:text-current"
href={~p"/sign-in?return_to=#{@current_path}"}
>
sign in
</.a>
</div>
<main class="p-2 max-w-2xl mx-auto">
{@inner_content}
</main>

View file

@ -2,12 +2,15 @@ defmodule CMSWeb.PageController do
use CMSWeb, :controller
alias CMS.Posts
alias CMS.Statuses
def home(conn, _params) do
posts = Enum.take(Posts.list_posts(), 5)
statuses = Enum.take(Statuses.list_statuses(), 10)
conn
|> assign(:posts, posts)
|> assign(:statuses, statuses)
|> render(:home)
end
end

View file

@ -1,11 +1,42 @@
<section>
<.subtitle>
<.a href={~p"/writing"}>writing</.a>
</.subtitle>
</section>
<div class="flex flex-col gap-y-4">
<section>
<.title>
<.a href={~p"/writing"}>writing</.a>
</.title>
<ul class="flex flex-col">
<li :for={post <- @posts}>
<.link href={~p"/writing/#{post}"} class="flex flex-row justify-between group">
<span class="group-hover:underline">
<%= if post.title do %>
{post.title}
<% else %>
(no title)
<% end %>
</span>
<.timex value={post.inserted_at} format="{YYYY}-{0M}-{0D}" class="text-gray-500" />
</.link>
</li>
</ul>
</section>
<section>
<.subtitle>
<.a href={~p"/microblog"}>microblog</.a>
</.subtitle>
</section>
<section>
<.title>
<.a href={~p"/microblog"}>microblog</.a>
</.title>
<ul class="flex flex-col">
<li :for={status <- @statuses}>
<.link href={~p"/microblog/#{status}"} class="flex flex-row justify-between group">
<span class="group-hover:underline overflow-hidden text-ellipsis text-nowrap">
{status.body}
</span>
<.timex
value={status.inserted_at}
format="{relative}"
formatter={:relative}
class="text-gray-500"
/>
</.link>
</li>
</ul>
</section>
</div>

View file

@ -1,13 +1,15 @@
<.title>writing</.title>
<ul class="flex flex-col">
<li :for={post <- @posts} class="flex flex-row justify-between">
<.a href={~p"/writing/#{post}"}>
<%= if post.title do %>
{post.title}
<% else %>
(no title)
<% end %>
</.a>
<.timex value={post.inserted_at} format="{YYYY}-{0M}-{0D}" class="text-gray-500" />
<li :for={post <- @posts}>
<.link href={~p"/writing/#{post}"} class="flex flex-row justify-between group">
<span class="group-hover:underline">
<%= if post.title do %>
{post.title}
<% else %>
(no title)
<% end %>
</span>
<.timex value={post.inserted_at} format="{YYYY}-{0M}-{0D}" class="text-gray-500" />
</.link>
</li>
</ul>

View file

@ -2,7 +2,7 @@
<header class="flex flex-row">
<h1 :if={@post.title} class="font-bold text-xl mb-3">{@post.title}</h1>
</header>
<section class="prose prose-cms max-w-none">{raw(@post.contents)}</section>
<section class="prose prose-cms max-w-none">{raw(@post.body)}</section>
<footer class="mt-5 py-1 border-t border-gray-200 relative">
<.link :if={@admin?} class="absolute right-0" href={~p"/admin/posts/#{@post}"}>edit</.link>
</footer>

View file

@ -0,0 +1,21 @@
defmodule CMSWeb.StatusController do
use CMSWeb, :controller
alias CMS.Statuses
def index(conn, _params) do
statuses = Statuses.list_statuses()
conn
|> assign(:statuses, statuses)
|> render(:index)
end
def show(conn, %{"status_id" => status_id}) do
status = Statuses.get_status!(status_id)
conn
|> assign(:status, status)
|> render(:show)
end
end

View file

@ -0,0 +1,6 @@
defmodule CMSWeb.StatusHTML do
@moduledoc false
use CMSWeb, :html
embed_templates "status_html/*"
end

View file

@ -0,0 +1,16 @@
<.title>microblog</.title>
<ul class="flex flex-col">
<li :for={status <- @statuses}>
<.link href={~p"/microblog/#{status}"} class="flex flex-row justify-between group">
<span class="group-hover:underline overflow-hidden text-ellipsis text-nowrap">
{status.body}
</span>
<.timex
value={status.inserted_at}
format="{relative}"
formatter={:relative}
class="text-gray-500"
/>
</.link>
</li>
</ul>

View file

@ -0,0 +1,3 @@
<article>
<p>{@status.body}</p>
</article>

View file

@ -19,20 +19,8 @@ defmodule CMSWeb.AdminLoginLive do
~H"""
<main class="flex flex-col w-screen h-screen fixed justify-center items-center">
<.form for={@form} action={~p"/admin/session/create"} class="flex flex-col gap-y-2">
<input
type="hidden"
id={@form[:return_to].id}
name={@form[:return_to].name}
value={@form[:return_to].value}
/>
<input
type="password"
placeholder="password"
id={@form[:password].id}
name={@form[:password].name}
value={@form[:password].value}
required
/>
<.input type="hidden" field={@form[:return_to]} />
<.input type="password" placeholder="password" field={@form[:password]} required />
<div class="flex flex-col items-end">
<button type="submit" class="font-bold hover:underline">sign in</button>
<.a href={cancel_href(@return_to)}>

View file

@ -60,10 +60,10 @@ defmodule CMSWeb.PostLive do
def render(assigns) do
~H"""
<.form for={@form} class="flex flex-col gap-y-2" phx-submit="save_post">
<.input type="hidden" field={@form[:contents]} />
<.input type="hidden" field={@form[:body]} />
<.input class="text-lg" field={@form[:title]} placeholder="Title" />
<div id="editor" phx-update="ignore">
<trix-editor input={@form[:contents].id} class="prose prose-cms max-w-none"></trix-editor>
<trix-editor input={@form[:body].id} class="prose prose-cms max-w-none"></trix-editor>
</div>
<button type="submit" class="self-end">save</button>

View file

@ -0,0 +1,67 @@
defmodule CMSWeb.StatusLive do
@moduledoc false
use CMSWeb, :live_view
alias CMS.Statuses
alias CMS.Statuses.Status
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(_params, uri, %{assigns: %{live_action: :new}} = socket) do
status = %Status{}
changeset = Status.changeset(status)
socket = assign(socket, status: status, form: to_form(changeset))
{:noreply, socket}
end
def handle_params(%{"status_id" => status_id}, _uri, %{assigns: %{live_action: :edit}} = socket) do
status = Statuses.get_status!(status_id)
changeset = Status.changeset(status)
socket = assign(socket, status: status, form: to_form(changeset))
{:noreply, socket}
end
@impl true
def handle_event("save_status", %{"status" => attrs}, %{assigns: %{live_action: :new}} = socket) do
socket =
case Statuses.create_status(attrs) do
{:ok, status} -> push_navigate(socket, to: ~p"/admin/statuses/#{status}")
{:error, changeset} -> assign(socket, form: to_form(changeset))
end
{:noreply, socket}
end
def handle_event("save_status", %{"status" => attrs}, %{assigns: %{status: status, live_action: :edit}} = socket) do
socket =
case Statuses.update_status(status, attrs) do
{:ok, status} ->
assign(socket, status: status, form: to_form(Status.changeset(status)))
{: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 gap-y-2" phx-submit="save_status">
<.input type="textarea" field={@form[:body]} />
<button type="submit" class="self-end">save</button>
</.form>
"""
end
end

View file

@ -36,6 +36,9 @@ defmodule CMSWeb.Router do
get "/writing", PostController, :index
get "/writing/:post_id", PostController, :show
get "/microblog", StatusController, :index
get "/microblog/:status_id", StatusController, :show
live "/sign-in", AdminLoginLive
post "/admin/session/create", AdminSessionController, :create
get "/admin/session/destroy", AdminSessionController, :destroy
@ -47,11 +50,11 @@ defmodule CMSWeb.Router do
live "/", AdminLive
live "/writing/new", PostLive, :new
live "/writing/:post_id", PostLive, :edit
live "/posts/new", PostLive, :new
live "/posts/:post_id", PostLive, :edit
live "/microblog/new", StatusLive, :new
live "/microblog/:status_id", StatusLive, :edit
live "/statuses/new", StatusLive, :new
live "/statuses/:status_id", StatusLive, :edit
end
end

View file

@ -5,7 +5,7 @@ defmodule CMS.Repo.Migrations.AddPostsTable do
create table(:posts, primary_key: false) do
add :id, :uuid, primary_key: true
add :title, :text
add :contents, :text, null: false, default: ""
add :body, :text, null: false, default: ""
timestamps()
end

View file

@ -0,0 +1,14 @@
defmodule CMS.Repo.Migrations.AddStatusesTable do
use Ecto.Migration
def change do
create table(:statuses, primary_key: false) do
add :id, :uuid, primary_key: true
add :body, :text, null: false
timestamps()
end
create index(:statuses, [:inserted_at])
end
end