admin improvements pt. 2
This commit is contained in:
parent
1675efb514
commit
0bcb8f0768
13 changed files with 124 additions and 55 deletions
lib
|
@ -1,4 +1,4 @@
|
||||||
defmodule Core do
|
defmodule Core do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Boundary, deps: [Schema], exports: [Accounts, Posts, Author]
|
use Boundary, deps: [Schema], exports: [Accounts, Posts, Author, DateTime]
|
||||||
end
|
end
|
||||||
|
|
9
lib/core/date_time.ex
Normal file
9
lib/core/date_time.ex
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Core.DateTime do
|
||||||
|
@local_time_zone "America/New_York"
|
||||||
|
|
||||||
|
def to_local_time(nil), do: nil
|
||||||
|
|
||||||
|
def to_local_time(date_time) do
|
||||||
|
DateTime.shift_zone!(date_time, @local_time_zone)
|
||||||
|
end
|
||||||
|
end
|
|
@ -50,11 +50,15 @@ defmodule Core.Posts do
|
||||||
|> Flop.validate_and_run(params, for: Schema.Post)
|
|> Flop.validate_and_run(params, for: Schema.Post)
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish_date(%Schema.Post{published_at: nil}), do: nil
|
def publish_date_time(%Schema.Post{published_at: nil}), do: nil
|
||||||
|
|
||||||
def publish_date(%Schema.Post{published_at: published_at}) do
|
def publish_date_time(%Schema.Post{published_at: published_at}) do
|
||||||
published_at
|
Core.DateTime.to_local_time(published_at)
|
||||||
|> DateTime.shift_zone!("America/New_York")
|
end
|
||||||
|
|
||||||
|
def publish_date(%Schema.Post{} = post) do
|
||||||
|
post
|
||||||
|
|> publish_date_time()
|
||||||
|> DateTime.to_date()
|
|> DateTime.to_date()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ defmodule Core.Posts.Post do
|
||||||
def content_changeset(%Schema.Post{} = post, attrs) do
|
def content_changeset(%Schema.Post{} = post, attrs) do
|
||||||
changeset =
|
changeset =
|
||||||
post
|
post
|
||||||
|> cast(attrs, [:tid, :kind, :slug, :title, :body])
|
|> cast(attrs, [:tid, :kind, :slug, :title, :body, :deleted_at, :published_at])
|
||||||
|> validate_required([:kind], message: "must have a kind")
|
|> validate_required([:kind], message: "must have a kind")
|
||||||
|> validate_required([:body], message: "must have a body")
|
|> validate_required([:body], message: "must have a body")
|
||||||
|
|
||||||
|
@ -40,7 +40,9 @@ defmodule Core.Posts.Post do
|
||||||
changeset = change(post)
|
changeset = change(post)
|
||||||
|
|
||||||
if is_nil(get_field(changeset, :published_at)) do
|
if is_nil(get_field(changeset, :published_at)) do
|
||||||
put_change(changeset, :published_at, published_at)
|
changeset
|
||||||
|
|> put_change(:deleted_at, nil)
|
||||||
|
|> put_change(:published_at, published_at)
|
||||||
else
|
else
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
|
@ -50,7 +52,9 @@ defmodule Core.Posts.Post do
|
||||||
changeset = change(post)
|
changeset = change(post)
|
||||||
|
|
||||||
if is_nil(get_field(changeset, :deleted_at)) do
|
if is_nil(get_field(changeset, :deleted_at)) do
|
||||||
put_change(changeset, :deleted_at, deleted_at)
|
changeset
|
||||||
|
|> put_change(:published_at, nil)
|
||||||
|
|> put_change(:deleted_at, deleted_at)
|
||||||
else
|
else
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
|
@ -81,7 +85,12 @@ defmodule Core.Posts.Post do
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_order(query \\ base()) do
|
def default_order(query \\ base()) do
|
||||||
order_by(query, [posts: p], desc: :inserted_at, desc: :updated_at)
|
order_by(query, [posts: p],
|
||||||
|
desc: :published_at,
|
||||||
|
desc: :inserted_at,
|
||||||
|
desc: :updated_at,
|
||||||
|
desc: :id
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def where_kind(query \\ base(), kind) do
|
def where_kind(query \\ base(), kind) do
|
||||||
|
|
|
@ -5,11 +5,11 @@ defmodule Schema.Post do
|
||||||
@derive {
|
@derive {
|
||||||
Flop.Schema,
|
Flop.Schema,
|
||||||
filterable: [],
|
filterable: [],
|
||||||
sortable: [:published_at, :id],
|
sortable: [:published_at, :inserted_at, :updated_at, :id],
|
||||||
pagination_types: [:first, :last],
|
pagination_types: [:first, :last],
|
||||||
default_order: %{
|
default_order: %{
|
||||||
order_by: [:published_at, :id],
|
order_by: [:published_at, :inserted_at, :updated_at, :id],
|
||||||
order_directions: [:desc, :desc]
|
order_directions: [:desc, :desc, :desc, :desc]
|
||||||
},
|
},
|
||||||
default_pagination_type: :first,
|
default_pagination_type: :first,
|
||||||
default_limit: 20,
|
default_limit: 20,
|
||||||
|
|
|
@ -10,7 +10,8 @@ defmodule Web.CoreComponents do
|
||||||
attr :name, :any
|
attr :name, :any
|
||||||
attr :label, :string, default: nil
|
attr :label, :string, default: nil
|
||||||
attr :value, :any
|
attr :value, :any
|
||||||
attr :type, :string, default: "text", values: ~w[text password textarea]
|
attr :class, :string, default: nil
|
||||||
|
attr :type, :string, default: "text", values: ~w[text password textarea datetime-local]
|
||||||
attr :field, FormField
|
attr :field, FormField
|
||||||
attr :errors, :list, default: []
|
attr :errors, :list, default: []
|
||||||
attr :rest, :global, include: ~w[disabled form pattern placeholder readonly required]
|
attr :rest, :global, include: ~w[disabled form pattern placeholder readonly required]
|
||||||
|
@ -36,9 +37,9 @@ defmodule Web.CoreComponents do
|
||||||
|
|
||||||
def input(%{type: "textarea"} = assigns) do
|
def input(%{type: "textarea"} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div class={["flex flex-col", @class]}>
|
||||||
<.label for={@id}>{@label}</.label>
|
<.label for={@id}>{@label}</.label>
|
||||||
<textarea id={@id} name={@name}>{Phoenix.HTML.Form.normalize_value(@type, @value)}</textarea>
|
<textarea id={@id} name={@name} class="h-80">{Phoenix.HTML.Form.normalize_value(@type, @value)}</textarea>
|
||||||
<.error :for={error <- @errors}>{error}</.error>
|
<.error :for={error <- @errors}>{error}</.error>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
@ -46,7 +47,7 @@ defmodule Web.CoreComponents do
|
||||||
|
|
||||||
def input(assigns) do
|
def input(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div class="flex flex-col">
|
||||||
<.label for={@id}>{@label}</.label>
|
<.label for={@id}>{@label}</.label>
|
||||||
<input
|
<input
|
||||||
id={@id}
|
id={@id}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
>
|
>
|
||||||
<h3 class="p-name u-url">{blog.title}</h3>
|
<h3 class="p-name u-url">{blog.title}</h3>
|
||||||
<div class="text-gray-500 dt-published">
|
<div class="text-gray-500 dt-published">
|
||||||
<.timex value={blog.published_at} format="{Mfull} {D}" />
|
<.timex value={Core.Posts.publish_date_time(blog)} format="{Mfull} {D}" />
|
||||||
</div>
|
</div>
|
||||||
</.link>
|
</.link>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -3,10 +3,18 @@
|
||||||
<header class="mb-4">
|
<header class="mb-4">
|
||||||
<h1 class="p-name text-2xl font-bold mb-2">{@blog.title}</h1>
|
<h1 class="p-name text-2xl font-bold mb-2">{@blog.title}</h1>
|
||||||
<%= if @blog.published_at do %>
|
<%= if @blog.published_at do %>
|
||||||
<div class="text-sm text-gray-500 flex items-center">
|
<div class="text-sm text-gray-500 flex items-center gap-x-2">
|
||||||
<span class="p-author h-card">{Core.Author.get(:name)}</span>
|
<span class="p-author h-card">{Core.Author.get(:name)}</span>
|
||||||
<span class="mx-2">·</span>
|
<span>·</span>
|
||||||
<.timex value={@blog.published_at} format="{Mfull} {D}, {YYYY}" class="dt-published" />
|
<.timex
|
||||||
|
value={Core.Posts.publish_date_time(@blog)}
|
||||||
|
format="{Mfull} {D}, {YYYY}"
|
||||||
|
class="dt-published"
|
||||||
|
/>
|
||||||
|
<%= if @current_user do %>
|
||||||
|
<span>·</span>
|
||||||
|
<.link navigate={~p"/admin/posts/#{@blog}"}>edit</.link>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
>
|
>
|
||||||
<h3 class="p-name u-url">{blog.title}</h3>
|
<h3 class="p-name u-url">{blog.title}</h3>
|
||||||
<div class="text-gray-500 dt-published">
|
<div class="text-gray-500 dt-published">
|
||||||
<.timex value={blog.published_at} format="{Mfull} {D}" />
|
<.timex value={Core.Posts.publish_date_time(blog)} format="{Mfull} {D}" />
|
||||||
</div>
|
</div>
|
||||||
</.link>
|
</.link>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -49,7 +49,7 @@ defmodule Web.StatusHTML do
|
||||||
<span class="mx-2 text-sm text-gray-500">·</span>
|
<span class="mx-2 text-sm text-gray-500">·</span>
|
||||||
<div class="text-sm text-gray-500">
|
<div class="text-sm text-gray-500">
|
||||||
<.timex
|
<.timex
|
||||||
value={@status.published_at}
|
value={Core.Posts.publish_date_time(@status)}
|
||||||
format="{relative}"
|
format="{relative}"
|
||||||
formatter={:relative}
|
formatter={:relative}
|
||||||
class="dt-published"
|
class="dt-published"
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="h-entry p-4 border border-gray-200">
|
<article class="h-entry p-4 border border-gray-200">
|
||||||
<header class="flex items-center mb-3">
|
<header class="flex mb-3 justify-between">
|
||||||
<div class="h-card flex items-center gap-3 p-author">
|
<div class="h-card flex items-center gap-3 p-author">
|
||||||
<%= if Core.Author.get(:avatar_url) do %>
|
<%= if Core.Author.get(:avatar_url) do %>
|
||||||
<img
|
<img
|
||||||
|
@ -34,6 +34,10 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%= if @current_user do %>
|
||||||
|
<.link class="text-sm text-gray-500" navigate={~p"/admin/posts/#{@status}"}>edit</.link>
|
||||||
|
<% end %>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="e-content my-4">
|
<div class="e-content my-4">
|
||||||
|
@ -43,7 +47,7 @@
|
||||||
<footer class="mt-4 border-t border-gray-200 pt-2 text-sm text-gray-500">
|
<footer class="mt-4 border-t border-gray-200 pt-2 text-sm text-gray-500">
|
||||||
<%= if @status.published_at do %>
|
<%= if @status.published_at do %>
|
||||||
<.timex
|
<.timex
|
||||||
value={@status.published_at}
|
value={Core.Posts.publish_date_time(@status)}
|
||||||
format="{Mfull} {D}, {YYYY} at {h12}:{m} {AM}"
|
format="{Mfull} {D}, {YYYY} at {h12}:{m} {AM}"
|
||||||
class="dt-published"
|
class="dt-published"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -53,7 +53,13 @@ defmodule Web.AdminPostLive do
|
||||||
socket =
|
socket =
|
||||||
case Core.Posts.create_or_update_post(post, attrs) do
|
case Core.Posts.create_or_update_post(post, attrs) do
|
||||||
{:ok, post} ->
|
{:ok, post} ->
|
||||||
|
form =
|
||||||
|
post
|
||||||
|
|> Core.Posts.change_post(%{})
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|
|> assign(post: post, form: form)
|
||||||
|> put_flash(:info, "post saved")
|
|> put_flash(:info, "post saved")
|
||||||
|> push_patch(to: ~p"/admin/posts/#{post}", replace: true)
|
|> push_patch(to: ~p"/admin/posts/#{post}", replace: true)
|
||||||
|
|
||||||
|
@ -68,7 +74,12 @@ defmodule Web.AdminPostLive do
|
||||||
socket =
|
socket =
|
||||||
case Core.Posts.publish_post(post) do
|
case Core.Posts.publish_post(post) do
|
||||||
{:ok, post} ->
|
{:ok, post} ->
|
||||||
assign(socket, post: post)
|
form =
|
||||||
|
post
|
||||||
|
|> Core.Posts.change_post(%{})
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
assign(socket, post: post, form: form)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
socket
|
socket
|
||||||
|
@ -81,7 +92,12 @@ defmodule Web.AdminPostLive do
|
||||||
socket =
|
socket =
|
||||||
case Core.Posts.unpublish_post(post) do
|
case Core.Posts.unpublish_post(post) do
|
||||||
{:ok, post} ->
|
{:ok, post} ->
|
||||||
assign(socket, post: post)
|
form =
|
||||||
|
post
|
||||||
|
|> Core.Posts.change_post(%{})
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
assign(socket, post: post, form: form)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
socket
|
socket
|
||||||
|
@ -94,7 +110,12 @@ defmodule Web.AdminPostLive do
|
||||||
socket =
|
socket =
|
||||||
case Core.Posts.delete_post(post) do
|
case Core.Posts.delete_post(post) do
|
||||||
{:ok, post} ->
|
{:ok, post} ->
|
||||||
assign(socket, post: post)
|
form =
|
||||||
|
post
|
||||||
|
|> Core.Posts.change_post(%{})
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
assign(socket, post: post, form: form)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
socket
|
socket
|
||||||
|
@ -107,7 +128,12 @@ defmodule Web.AdminPostLive do
|
||||||
socket =
|
socket =
|
||||||
case Core.Posts.undelete_post(post) do
|
case Core.Posts.undelete_post(post) do
|
||||||
{:ok, post} ->
|
{:ok, post} ->
|
||||||
assign(socket, post: post)
|
form =
|
||||||
|
post
|
||||||
|
|> Core.Posts.change_post(%{})
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
assign(socket, post: post, form: form)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
socket
|
socket
|
||||||
|
|
|
@ -1,33 +1,41 @@
|
||||||
<main>
|
<.form
|
||||||
<header>
|
for={@form}
|
||||||
<h1>{page_title(@post, @live_action)}</h1>
|
phx-change="validate"
|
||||||
</header>
|
phx-submit="save"
|
||||||
|
class="flex flex-col md:flex-row p-4 gap-4"
|
||||||
<.form for={@form} phx-change="validate" phx-submit="save">
|
>
|
||||||
<.input :if={@post.kind == :blog} type="text" field={@form[:title]} />
|
<div class="flex-grow flex flex-col gap-y-4">
|
||||||
|
<.input :if={@post.kind == :blog} type="text" label="title" field={@form[:title]} />
|
||||||
|
<.input type="textarea" field={@form[:body]} label="body" class="md:flex-grow" />
|
||||||
|
</div>
|
||||||
|
<div class="md:w-80 flex flex-col gap-y-2">
|
||||||
|
<.button type="submit" class="self-end">save</.button>
|
||||||
|
<.link
|
||||||
|
:if={@post.published_at}
|
||||||
|
class="self-end"
|
||||||
|
navigate={Web.Paths.public_post_path(@post)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
view
|
||||||
|
</.link>
|
||||||
|
<.input type="datetime-local" label="published (utc)" field={@form[:published_at]} />
|
||||||
|
<%= if @post.published_at do %>
|
||||||
|
<.button phx-click="unpublish" class="self-end">unpublish</.button>
|
||||||
|
<% else %>
|
||||||
|
<.button phx-click="publish" class="self-end">publish</.button>
|
||||||
|
<% end %>
|
||||||
|
<.input type="datetime-local" label="deleted (utc)" field={@form[:deleted_at]} />
|
||||||
|
<%= if @post.deleted_at do %>
|
||||||
|
<.button phx-click="undelete" class="self-end">undelete</.button>
|
||||||
|
<% else %>
|
||||||
|
<.button phx-click="delete" class="self-end">delete</.button>
|
||||||
|
<% end %>
|
||||||
<.input
|
<.input
|
||||||
:if={@post.kind == :blog}
|
:if={@post.kind == :blog}
|
||||||
|
label="slug"
|
||||||
type="text"
|
type="text"
|
||||||
field={@form[:slug]}
|
field={@form[:slug]}
|
||||||
disabled={not update_slug?(@post)}
|
disabled={not update_slug?(@post)}
|
||||||
/>
|
/>
|
||||||
<.input type="textarea" field={@form[:body]} />
|
</div>
|
||||||
|
</.form>
|
||||||
<.button type="submit">save</.button>
|
|
||||||
</.form>
|
|
||||||
|
|
||||||
<%= if @live_action == :edit do %>
|
|
||||||
<div>
|
|
||||||
<%= if @post.published_at do %>
|
|
||||||
<.button phx-click="unpublish">unpublish</.button>
|
|
||||||
<% else %>
|
|
||||||
<.button phx-click="publish">publish</.button>
|
|
||||||
<% end %>
|
|
||||||
<%= if @post.deleted_at do %>
|
|
||||||
<.button phx-click="undelete">undelete</.button>
|
|
||||||
<% else %>
|
|
||||||
<.button phx-click="delete">delete</.button>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</main>
|
|
||||||
|
|
Loading…
Reference in a new issue