add statuses, change navigation
This commit is contained in:
parent
76ccea9619
commit
574d6a42a0
20 changed files with 326 additions and 95 deletions
lib
cms
cms_web
components
controllers
live
router.expriv/repo/migrations
|
@ -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
31
lib/cms/statuses.ex
Normal 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
|
19
lib/cms/statuses/status.ex
Normal file
19
lib/cms/statuses/status.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
21
lib/cms_web/controllers/status_controller.ex
Normal file
21
lib/cms_web/controllers/status_controller.ex
Normal 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
|
6
lib/cms_web/controllers/status_html.ex
Normal file
6
lib/cms_web/controllers/status_html.ex
Normal file
|
@ -0,0 +1,6 @@
|
|||
defmodule CMSWeb.StatusHTML do
|
||||
@moduledoc false
|
||||
use CMSWeb, :html
|
||||
|
||||
embed_templates "status_html/*"
|
||||
end
|
16
lib/cms_web/controllers/status_html/index.html.heex
Normal file
16
lib/cms_web/controllers/status_html/index.html.heex
Normal 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>
|
3
lib/cms_web/controllers/status_html/show.html.heex
Normal file
3
lib/cms_web/controllers/status_html/show.html.heex
Normal file
|
@ -0,0 +1,3 @@
|
|||
<article>
|
||||
<p>{@status.body}</p>
|
||||
</article>
|
|
@ -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)}>
|
||||
|
|
|
@ -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>
|
||||
|
|
67
lib/cms_web/live/status_live.ex
Normal file
67
lib/cms_web/live/status_live.ex
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
14
priv/repo/migrations/20250222201807_add_statuses_table.exs
Normal file
14
priv/repo/migrations/20250222201807_add_statuses_table.exs
Normal 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
|
Loading…
Reference in a new issue