chore: clear out page/post usage
This commit is contained in:
parent
4c3c5547fe
commit
e0de9ae8d9
33 changed files with 3 additions and 902 deletions
lib
core.ex
mix.exscore
schema.exschema
web
components
controllers
admin_auth.exadmin_session_controller.exglobals.expage_controller.expage_html.ex
page_html
post_controller.expost_html.expost_html
status_controller.exstatus_html.exstatus_html
live
router.expriv/repo/migrations
|
@ -1,4 +1,4 @@
|
||||||
defmodule Core do
|
defmodule Core do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Boundary, deps: [Schema], exports: [Author, Posts, Statuses]
|
use Boundary, deps: [Schema], exports: []
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
defmodule Core.Author do
|
|
||||||
@moduledoc """
|
|
||||||
Properties of the author, Sloane
|
|
||||||
|
|
||||||
Modeled after the [h-card] specification.
|
|
||||||
|
|
||||||
[h-card]: https://microformats.org/wiki/h-card
|
|
||||||
"""
|
|
||||||
use TypedStruct
|
|
||||||
|
|
||||||
@public_properties ~w[name nickname url]a
|
|
||||||
|
|
||||||
typedstruct do
|
|
||||||
field :name, String.t()
|
|
||||||
field :given_name, String.t()
|
|
||||||
field :additional_name, String.t()
|
|
||||||
field :family_name, String.t()
|
|
||||||
field :nickname, String.t()
|
|
||||||
field :email, String.t()
|
|
||||||
field :url, String.t()
|
|
||||||
end
|
|
||||||
|
|
||||||
def sloane do
|
|
||||||
%__MODULE__{
|
|
||||||
name: "Sloane Perrault",
|
|
||||||
given_name: "Sloane",
|
|
||||||
additional_name: "Loretta",
|
|
||||||
family_name: "Perrault",
|
|
||||||
nickname: "sloanely_but_surely",
|
|
||||||
email: "sloane@fastmail.com",
|
|
||||||
url: "https://sloanelybutsurely.com"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def full do
|
|
||||||
sloane()
|
|
||||||
end
|
|
||||||
|
|
||||||
def public do
|
|
||||||
author = full()
|
|
||||||
|
|
||||||
for key <- @public_properties, reduce: %__MODULE__{} do
|
|
||||||
acc -> Map.put(acc, key, Map.get(author, key))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,37 +0,0 @@
|
||||||
defmodule Core.Posts do
|
|
||||||
@moduledoc false
|
|
||||||
import Ecto.Changeset
|
|
||||||
import Ecto.Query
|
|
||||||
|
|
||||||
alias Core.Repo
|
|
||||||
|
|
||||||
def changeset(%Schema.Post{} = post, attrs) do
|
|
||||||
post
|
|
||||||
|> cast(attrs, [:title, :body])
|
|
||||||
|> validate_required([:body])
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_post(attrs) do
|
|
||||||
%Schema.Post{}
|
|
||||||
|> changeset(attrs)
|
|
||||||
|> Repo.insert()
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_post(post, attrs) do
|
|
||||||
post
|
|
||||||
|> changeset(attrs)
|
|
||||||
|> Repo.update()
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_post!(id) do
|
|
||||||
Repo.get!(Schema.Post, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_posts do
|
|
||||||
query =
|
|
||||||
from post in Schema.Post,
|
|
||||||
order_by: [desc: post.inserted_at]
|
|
||||||
|
|
||||||
Repo.all(query)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,37 +0,0 @@
|
||||||
defmodule Core.Statuses do
|
|
||||||
@moduledoc false
|
|
||||||
import Ecto.Changeset
|
|
||||||
import Ecto.Query
|
|
||||||
|
|
||||||
alias Core.Repo
|
|
||||||
|
|
||||||
def changeset(%Schema.Status{} = status, attrs \\ %{}) do
|
|
||||||
status
|
|
||||||
|> cast(attrs, [:body])
|
|
||||||
|> validate_required([:body])
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_status(attrs) do
|
|
||||||
%Schema.Status{}
|
|
||||||
|> changeset(attrs)
|
|
||||||
|> Repo.insert()
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_status(status, attrs) do
|
|
||||||
status
|
|
||||||
|> changeset(attrs)
|
|
||||||
|> Repo.update()
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_status!(id) do
|
|
||||||
Repo.get!(Schema.Status, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_statuses do
|
|
||||||
query =
|
|
||||||
from status in Schema.Status,
|
|
||||||
order_by: [desc: status.inserted_at]
|
|
||||||
|
|
||||||
Repo.all(query)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Schema do
|
defmodule Schema do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Boundary, deps: [], exports: [Post, Status]
|
use Boundary, deps: [], exports: []
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
defmodule Schema.Post do
|
|
||||||
@moduledoc false
|
|
||||||
use Ecto.Schema
|
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
schema "posts" do
|
|
||||||
field :title, :string
|
|
||||||
field :body, :string
|
|
||||||
|
|
||||||
timestamps()
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,11 +0,0 @@
|
||||||
defmodule Schema.Status do
|
|
||||||
@moduledoc false
|
|
||||||
use Ecto.Schema
|
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
|
||||||
schema "statuses" do
|
|
||||||
field :body
|
|
||||||
|
|
||||||
timestamps()
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -3,147 +3,4 @@ defmodule Web.CoreComponents do
|
||||||
Provides core UI components.
|
Provides core UI components.
|
||||||
"""
|
"""
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
attr :global, :global
|
|
||||||
slot :inner_block
|
|
||||||
|
|
||||||
def title(assigns) do
|
|
||||||
~H"""
|
|
||||||
<h1 class={["font-bold text-xl mb-3", @class]} {@global}>{render_slot(@inner_block)}</h1>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
attr :global, :global
|
|
||||||
slot :inner_block
|
|
||||||
|
|
||||||
def subtitle(assigns) do
|
|
||||||
~H"""
|
|
||||||
<h2 class={["font-bold text-lg mb-2", @class]} {@global}>{render_slot(@inner_block)}</h2>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
|
|
||||||
attr :global, :global,
|
|
||||||
include: ~w[navigate patch href replace method csrf_token download hreflang referrerpolicy rel target type]
|
|
||||||
|
|
||||||
slot :inner_block
|
|
||||||
|
|
||||||
def a(assigns) do
|
|
||||||
~H"""
|
|
||||||
<.link class={["hover:underline", @class]} {@global}>{render_slot(@inner_block)}</.link>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
attr :type, :string, default: "button"
|
|
||||||
attr :global, :global
|
|
||||||
slot :inner_block
|
|
||||||
|
|
||||||
def button(assigns) do
|
|
||||||
~H"""
|
|
||||||
<button type={@type} class={["hover:underline", @class]} {@global}>
|
|
||||||
{render_slot(@inner_block)}
|
|
||||||
</button>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
attr :field, Phoenix.HTML.FormField, required: true
|
|
||||||
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-2 py-1 border border-gray-400 rounded", @class]}
|
|
||||||
type={@type}
|
|
||||||
id={@field.id}
|
|
||||||
name={@field.name}
|
|
||||||
value={@field.value}
|
|
||||||
{@global}
|
|
||||||
/>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a [Heroicon](https://heroicons.com).
|
|
||||||
|
|
||||||
Heroicons come in three styles – outline, solid, and mini.
|
|
||||||
By default, the outline style is used, but solid and mini may
|
|
||||||
be applied by using the `-solid` and `-mini` suffix.
|
|
||||||
|
|
||||||
You can customize the size and colors of the icons by setting
|
|
||||||
width, height, and background color classes.
|
|
||||||
|
|
||||||
Icons are extracted from the `deps/heroicons` directory and bundled within
|
|
||||||
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.icon name="hero-x-mark-solid" />
|
|
||||||
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
|
|
||||||
"""
|
|
||||||
attr :name, :string, required: true
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
|
|
||||||
def icon(%{name: "hero-" <> _} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<span class={[@name, @class]} />
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
attr :format, :string, required: true
|
|
||||||
attr :value, :any, default: nil
|
|
||||||
attr :formatter, :atom, default: :default
|
|
||||||
attr :timezone, :string, default: "America/New_York"
|
|
||||||
attr :global, :global
|
|
||||||
|
|
||||||
def timex(%{value: nil} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<time datetime="">--</time>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def timex(%{value: value, timezone: timezone} = assigns) do
|
|
||||||
assigns =
|
|
||||||
assign_new(assigns, :local_value, fn ->
|
|
||||||
case value do
|
|
||||||
%DateTime{} = datetime ->
|
|
||||||
datetime
|
|
||||||
|
|
||||||
%NaiveDateTime{} = naive ->
|
|
||||||
naive
|
|
||||||
|> DateTime.from_naive!("Etc/UTC")
|
|
||||||
|> DateTime.shift_zone!(timezone)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
~H"""
|
|
||||||
<time
|
|
||||||
datetime={Timex.format!(@local_value, "{ISO:Extended}")}
|
|
||||||
title={Timex.format!(@local_value, "{Mshort} {D}, {YYYY}, {h12}:{m} {AM} {Zabbr}")}
|
|
||||||
{@global}
|
|
||||||
>
|
|
||||||
{Timex.format!(@local_value, @format, timex_formatter(@formatter))}
|
|
||||||
</time>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp timex_formatter(formatter) do
|
|
||||||
Module.concat(Timex.Format.DateTime.Formatters, :string.titlecase("#{formatter}"))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,10 +10,4 @@ defmodule Web.Layouts do
|
||||||
use Web, :html
|
use Web, :html
|
||||||
|
|
||||||
embed_templates "layouts/*"
|
embed_templates "layouts/*"
|
||||||
|
|
||||||
defp post?("/writing/" <> _), do: true
|
|
||||||
defp post?(_), do: false
|
|
||||||
|
|
||||||
defp status?("/microblog/" <> _), do: true
|
|
||||||
defp status?(_), do: false
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,53 +1 @@
|
||||||
<div class="sticky top-0 z-50 flex flex-row bg-white justify-between py-1 md:mb-2 border-b border-gray-200">
|
{@inner_content}
|
||||||
<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>
|
|
||||||
|
|
||||||
<.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>
|
|
||||||
|
|
|
@ -10,11 +10,6 @@
|
||||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
<%= if @load_trix? do %>
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css" />
|
|
||||||
<script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js">
|
|
||||||
</script>
|
|
||||||
<% end %>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white">
|
<body class="bg-white">
|
||||||
{@inner_content}
|
{@inner_content}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
defmodule Web.AdminAuth do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :verified_routes
|
|
||||||
|
|
||||||
import Phoenix.Controller
|
|
||||||
import Plug.Conn
|
|
||||||
|
|
||||||
def log_in_admin(conn, params) do
|
|
||||||
conn
|
|
||||||
|> renew_session()
|
|
||||||
|> put_session(:admin?, true)
|
|
||||||
|> redirect(to: params["return_to"] || ~p"/admin")
|
|
||||||
end
|
|
||||||
|
|
||||||
def log_out_admin(conn, params) do
|
|
||||||
if live_socket_id = get_session(conn, :live_socket_id) do
|
|
||||||
Web.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> renew_session()
|
|
||||||
|> redirect(to: params["return_to"] || ~p"/")
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount_admin(%Plug.Conn{} = conn, _opts) do
|
|
||||||
assign(conn, :admin?, admin?(conn))
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_admin(%Plug.Conn{assigns: %{admin?: true}} = conn, _opts) do
|
|
||||||
conn
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_admin(conn, _opts) do
|
|
||||||
redirect(conn, to: ~p"/sign-in?return_to=#{conn.request_path}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def correct_password?(password) do
|
|
||||||
password_hash = Application.fetch_env!(:sloanely_but_surely, :password_hash)
|
|
||||||
|
|
||||||
Argon2.verify_pass(password, password_hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_mount(:default, _params, session, socket) do
|
|
||||||
{:cont, Phoenix.Component.assign(socket, :admin?, admin?(session))}
|
|
||||||
end
|
|
||||||
|
|
||||||
## private
|
|
||||||
|
|
||||||
defp renew_session(conn) do
|
|
||||||
delete_csrf_token()
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> configure_session(renew: true)
|
|
||||||
|> clear_session()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp admin?(%Plug.Conn{} = conn) do
|
|
||||||
Plug.Conn.get_session(conn, :admin?, false) == true
|
|
||||||
end
|
|
||||||
|
|
||||||
defp admin?(%{} = session), do: Map.get(session, "admin?", false) == true
|
|
||||||
end
|
|
|
@ -1,21 +0,0 @@
|
||||||
defmodule Web.AdminSessionController do
|
|
||||||
use Web, :controller
|
|
||||||
|
|
||||||
alias Web.AdminAuth
|
|
||||||
|
|
||||||
def create(conn, %{"password" => password} = params) do
|
|
||||||
if AdminAuth.correct_password?(password) do
|
|
||||||
AdminAuth.log_in_admin(conn, params)
|
|
||||||
else
|
|
||||||
redirect(conn, to: ~p"/sign-in")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create(conn, _params) do
|
|
||||||
redirect(conn, to: ~p"/sign-in")
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy(conn, params) do
|
|
||||||
AdminAuth.log_out_admin(conn, params)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,22 +0,0 @@
|
||||||
defmodule Web.Globals do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :live_view
|
|
||||||
|
|
||||||
def assign_globals(%Plug.Conn{} = conn, _opts) do
|
|
||||||
conn
|
|
||||||
|> Plug.Conn.assign(:current_path, conn.request_path)
|
|
||||||
|> Plug.Conn.assign(:load_trix?, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_mount(:default, _params, _session, socket) do
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> attach_hook(:assign_handle_params_globals, :handle_params, fn _params, uri, socket ->
|
|
||||||
%URI{path: current_path} = URI.parse(uri)
|
|
||||||
{:cont, assign(socket, :current_path, current_path)}
|
|
||||||
end)
|
|
||||||
|> assign(:load_trix?, false)
|
|
||||||
|
|
||||||
{:cont, socket}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,13 +0,0 @@
|
||||||
defmodule Web.PageController do
|
|
||||||
use Web, :controller
|
|
||||||
|
|
||||||
def home(conn, _params) do
|
|
||||||
posts = Enum.take(Core.Posts.list_posts(), 5)
|
|
||||||
statuses = Enum.take(Core.Statuses.list_statuses(), 10)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> assign(:posts, posts)
|
|
||||||
|> assign(:statuses, statuses)
|
|
||||||
|> render(:home)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,10 +0,0 @@
|
||||||
defmodule Web.PageHTML do
|
|
||||||
@moduledoc """
|
|
||||||
This module contains pages rendered by PageController.
|
|
||||||
|
|
||||||
See the `page_html` directory for all templates available.
|
|
||||||
"""
|
|
||||||
use Web, :html
|
|
||||||
|
|
||||||
embed_templates "page_html/*"
|
|
||||||
end
|
|
|
@ -1,46 +0,0 @@
|
||||||
<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 text-nowrap"
|
|
||||||
/>
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</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 text-nowrap"
|
|
||||||
/>
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
|
@ -1,19 +0,0 @@
|
||||||
defmodule Web.PostController do
|
|
||||||
use Web, :controller
|
|
||||||
|
|
||||||
def index(conn, _params) do
|
|
||||||
posts = Core.Posts.list_posts()
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> assign(:posts, posts)
|
|
||||||
|> render(:index)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show(conn, %{"post_id" => post_id}) do
|
|
||||||
post = Core.Posts.get_post!(post_id)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> assign(:post, post)
|
|
||||||
|> render(:show)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +0,0 @@
|
||||||
defmodule Web.PostHTML do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :html
|
|
||||||
|
|
||||||
embed_templates "post_html/*"
|
|
||||||
end
|
|
|
@ -1,19 +0,0 @@
|
||||||
<.title>writing</.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 text-nowrap"
|
|
||||||
/>
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<article>
|
|
||||||
<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.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>
|
|
||||||
</article>
|
|
|
@ -1,19 +0,0 @@
|
||||||
defmodule Web.StatusController do
|
|
||||||
use Web, :controller
|
|
||||||
|
|
||||||
def index(conn, _params) do
|
|
||||||
statuses = Core.Statuses.list_statuses()
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> assign(:statuses, statuses)
|
|
||||||
|> render(:index)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show(conn, %{"status_id" => status_id}) do
|
|
||||||
status = Core.Statuses.get_status!(status_id)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> assign(:status, status)
|
|
||||||
|> render(:show)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +0,0 @@
|
||||||
defmodule Web.StatusHTML do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :html
|
|
||||||
|
|
||||||
embed_templates "status_html/*"
|
|
||||||
end
|
|
|
@ -1,16 +0,0 @@
|
||||||
<.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 text-nowrap"
|
|
||||||
/>
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<article>
|
|
||||||
<p>{@status.body}</p>
|
|
||||||
</article>
|
|
|
@ -1,16 +0,0 @@
|
||||||
defmodule Web.AdminLive do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :live_view
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<h1>AdminLive</h1>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,39 +0,0 @@
|
||||||
defmodule Web.AdminLoginLive do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :live_view
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(params, _session, socket) do
|
|
||||||
socket =
|
|
||||||
assign(
|
|
||||||
socket,
|
|
||||||
form: to_form(%{"password" => "", "return_to" => params["return_to"]}),
|
|
||||||
return_to: params["return_to"]
|
|
||||||
)
|
|
||||||
|
|
||||||
{:ok, socket, layout: false}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) 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" 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)}>
|
|
||||||
cancel
|
|
||||||
</.a>
|
|
||||||
</div>
|
|
||||||
</.form>
|
|
||||||
</main>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp cancel_href("/admin"), do: ~p"/"
|
|
||||||
defp cancel_href("/admin/" <> _), do: ~p"/"
|
|
||||||
defp cancel_href(nil), do: ~p"/"
|
|
||||||
defp cancel_href(return_to), do: return_to
|
|
||||||
end
|
|
|
@ -1,76 +0,0 @@
|
||||||
defmodule Web.PostLive do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :live_view
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
socket = assign(socket, :load_trix?, true)
|
|
||||||
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_params(_params, _uri, %{assigns: %{live_action: :new}} = socket) do
|
|
||||||
post = %Schema.Post{}
|
|
||||||
changeset = Core.Posts.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 = Core.Posts.get_post!(post_id)
|
|
||||||
|
|
||||||
changeset = Core.Posts.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 Core.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 Core.Posts.update_post(post, attrs) do
|
|
||||||
{:ok, post} ->
|
|
||||||
assign(socket,
|
|
||||||
post: post,
|
|
||||||
form:
|
|
||||||
post
|
|
||||||
|> Core.Posts.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 gap-y-2" phx-submit="save_post">
|
|
||||||
<.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[:body].id} class="prose prose-cms max-w-none"></trix-editor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="self-end">save</button>
|
|
||||||
</.form>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,70 +0,0 @@
|
||||||
defmodule Web.StatusLive do
|
|
||||||
@moduledoc false
|
|
||||||
use Web, :live_view
|
|
||||||
|
|
||||||
@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 = %Schema.Status{}
|
|
||||||
|
|
||||||
changeset = Core.Statuses.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 = Core.Statuses.get_status!(status_id)
|
|
||||||
|
|
||||||
changeset = Core.Statuses.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 Core.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 Core.Statuses.update_status(status, attrs) do
|
|
||||||
{:ok, status} ->
|
|
||||||
assign(socket,
|
|
||||||
status: status,
|
|
||||||
form:
|
|
||||||
status
|
|
||||||
|> Core.Statuses.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 gap-y-2" phx-submit="save_status">
|
|
||||||
<.input type="textarea" field={@form[:body]} />
|
|
||||||
<button type="submit" class="self-end">save</button>
|
|
||||||
</.form>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,12 +1,6 @@
|
||||||
defmodule Web.Router do
|
defmodule Web.Router do
|
||||||
use Web, :router
|
use Web, :router
|
||||||
|
|
||||||
import Web.AdminAuth
|
|
||||||
import Web.Globals
|
|
||||||
|
|
||||||
alias Web.AdminAuth
|
|
||||||
alias Web.Globals
|
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html"]
|
plug :accepts, ["html"]
|
||||||
plug :fetch_session
|
plug :fetch_session
|
||||||
|
@ -14,47 +8,5 @@ defmodule Web.Router do
|
||||||
plug :put_root_layout, html: {Web.Layouts, :root}
|
plug :put_root_layout, html: {Web.Layouts, :root}
|
||||||
plug :protect_from_forgery
|
plug :protect_from_forgery
|
||||||
plug :put_secure_browser_headers
|
plug :put_secure_browser_headers
|
||||||
plug :assign_globals
|
|
||||||
end
|
|
||||||
|
|
||||||
pipeline :supports_admin_action do
|
|
||||||
plug :mount_admin
|
|
||||||
end
|
|
||||||
|
|
||||||
pipeline :requires_admin do
|
|
||||||
plug :mount_admin
|
|
||||||
plug :require_admin
|
|
||||||
end
|
|
||||||
|
|
||||||
live_session :default, on_mount: [AdminAuth, Globals] do
|
|
||||||
scope "/", Web do
|
|
||||||
pipe_through :browser
|
|
||||||
pipe_through :supports_admin_action
|
|
||||||
|
|
||||||
get "/", PageController, :home
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
scope "/admin", Web do
|
|
||||||
pipe_through :browser
|
|
||||||
pipe_through :requires_admin
|
|
||||||
|
|
||||||
live "/", AdminLive
|
|
||||||
|
|
||||||
live "/posts/new", PostLive, :new
|
|
||||||
live "/posts/:post_id", PostLive, :edit
|
|
||||||
|
|
||||||
live "/statuses/new", StatusLive, :new
|
|
||||||
live "/statuses/:status_id", StatusLive, :edit
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -51,9 +51,6 @@ defmodule SlaonelyButSurely.MixProject do
|
||||||
{:bandit, "~> 1.5"},
|
{:bandit, "~> 1.5"},
|
||||||
|
|
||||||
# Added dependencies
|
# Added dependencies
|
||||||
{:argon2_elixir, "~> 4.1"},
|
|
||||||
{:timex, "~> 3.7"},
|
|
||||||
{:typed_struct, "~> 0.3.0"},
|
|
||||||
{:boundary, "~> 0.10.4"},
|
{:boundary, "~> 0.10.4"},
|
||||||
|
|
||||||
# Added dev and/or test dependencies
|
# Added dev and/or test dependencies
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
defmodule Core.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 :body, :text, null: false, default: ""
|
|
||||||
|
|
||||||
timestamps()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,14 +0,0 @@
|
||||||
defmodule Core.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