chore: clear out page/post usage

This commit is contained in:
sloane 2025-03-23 14:25:15 -04:00
parent 4c3c5547fe
commit e0de9ae8d9
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
33 changed files with 3 additions and 902 deletions

View file

@ -1,4 +1,4 @@
defmodule Core do
@moduledoc false
use Boundary, deps: [Schema], exports: [Author, Posts, Statuses]
use Boundary, deps: [Schema], exports: []
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,4 @@
defmodule Schema do
@moduledoc false
use Boundary, deps: [], exports: [Post, Status]
use Boundary, deps: [], exports: []
end

View file

@ -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

View file

@ -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

View file

@ -3,147 +3,4 @@ defmodule Web.CoreComponents do
Provides core UI components.
"""
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

View file

@ -10,10 +10,4 @@ defmodule Web.Layouts do
use Web, :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,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">
<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>
{@inner_content}

View file

@ -10,11 +10,6 @@
<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>
<%= 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>
<body class="bg-white">
{@inner_content}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -1,6 +0,0 @@
defmodule Web.PostHTML do
@moduledoc false
use Web, :html
embed_templates "post_html/*"
end

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

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

View file

@ -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>

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,12 +1,6 @@
defmodule Web.Router do
use Web, :router
import Web.AdminAuth
import Web.Globals
alias Web.AdminAuth
alias Web.Globals
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
@ -14,47 +8,5 @@ defmodule Web.Router do
plug :put_root_layout, html: {Web.Layouts, :root}
plug :protect_from_forgery
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

View file

@ -51,9 +51,6 @@ defmodule SlaonelyButSurely.MixProject do
{:bandit, "~> 1.5"},
# Added dependencies
{:argon2_elixir, "~> 4.1"},
{:timex, "~> 3.7"},
{:typed_struct, "~> 0.3.0"},
{:boundary, "~> 0.10.4"},
# Added dev and/or test dependencies

View file

@ -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

View file

@ -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