Merge pull request 'devel' (#131) from devel into main

Reviewed-on: #131
This commit is contained in:
inhji 2023-06-19 22:24:25 +02:00
commit 0483872058
12 changed files with 259 additions and 201 deletions

View file

@ -1,2 +1,3 @@
erlang 25.2.3
elixir 1.14.3-otp-25
nodejs 18.16.0

View file

@ -53,6 +53,8 @@ defmodule Chiya.Channels do
"""
def get_channel!(id), do: Repo.get!(Channel, id)
def get_channel(id), do: Repo.get(Channel, id)
@doc """
Gets a single channel with all associated entities preloaded.
"""

View file

@ -218,6 +218,15 @@ defmodule Chiya.Notes do
Repo.delete(note)
end
def publish_note(%Note{} = note, published_at) do
{1, nil} =
Note
|> where([n], n.id == ^note.id)
|> Repo.update_all(set: [published_at: published_at])
{:ok, note}
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking note changes.

View file

@ -31,7 +31,8 @@ defmodule Chiya.Site.Setting do
:custom_css,
:custom_html,
:home_channel_id,
:default_channel_id
:default_channel_id,
:micropub_channel_id
])
|> validate_required([:title, :subtitle, :theme, :user_agent])
end

View file

@ -58,7 +58,7 @@ defmodule ChiyaWeb do
layout: {ChiyaWeb.Layouts, :app}
# Import admin components
import ChiyaWeb.AdminComponents
import ChiyaWeb.CoreComponents
unquote(html_helpers())
end
@ -69,7 +69,7 @@ defmodule ChiyaWeb do
use Phoenix.LiveComponent
# Import admin components
import ChiyaWeb.AdminComponents
import ChiyaWeb.CoreComponents
unquote(html_helpers())
end
@ -84,7 +84,7 @@ defmodule ChiyaWeb do
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
# Import admin components
import ChiyaWeb.AdminComponents
import ChiyaWeb.CoreComponents
# Include general helpers for rendering HTML
unquote(html_helpers())
@ -112,7 +112,6 @@ defmodule ChiyaWeb do
# HTML escaping functionality
import Phoenix.HTML
# Core UI components and translation
import ChiyaWeb.CoreComponents
import ChiyaWeb.Gettext
# Shortcut for generating JS commands

View file

@ -1,126 +0,0 @@
defmodule ChiyaWeb.AdminComponents do
use Phoenix.Component
use Phoenix.VerifiedRoutes,
endpoint: ChiyaWeb.Endpoint,
router: ChiyaWeb.Router,
statics: ChiyaWeb.static_paths()
import ChiyaWeb.CoreComponents
@doc """
Renders a horizontal line
"""
def line(assigns) do
~H"""
<hr class="my-6 dark:border-gray-700" />
"""
end
@doc """
Renders a UI for uploading files
"""
attr :upload, :map, required: true
attr :cancel_upload, :string, default: "cancel-upload"
def live_upload(assigns) do
~H"""
<div>
<.live_file_input upload={@upload} class="dark:text-gray-300" />
<section phx-drop-target={@upload.ref}>
<%= for entry <- @upload.entries do %>
<article class="upload-entry">
<figure>
<.live_img_preview entry={entry} />
<figcaption class="dark:text-gray-100"><%= entry.client_name %></figcaption>
</figure>
<div class="flex">
<%!-- entry.progress will update automatically for in-flight entries --%>
<progress value={entry.progress} max="100" class="w-full">
<%= entry.progress %>%
</progress>
<%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/3 --%>
<button
class="px-2 dark:text-white"
type="button"
phx-click="cancel-upload"
phx-value-ref={entry.ref}
aria-label="cancel"
>
&times;
</button>
</div>
<%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%>
<%= for err <- upload_errors(@upload, entry) do %>
<p class="alert alert-danger"><%= upload_error_to_string(err) %></p>
<% end %>
</article>
<% end %>
</section>
</div>
"""
end
@doc """
Renders the admin menu bar
"""
attr :current_user, :map, required: true
attr :settings, :map, required: true
def admin_bar(assigns) do
~H"""
<ul class="sticky top-0 backdrop-blur-sm z-10 flex items-center gap-4 py-3 px-4 sm:px-6 lg:px-8 bg-white/30 dark:bg-black/30">
<li>
<.link
href={~p"/"}
class="flex gap-3 text-sm leading-6 font-semibold text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
<%= @settings.title %>
</.link>
</li>
<li class="flex-1"></li>
<%= if @current_user do %>
<li>
<.link
href={~p"/admin"}
class="text-sm leading-6 text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
Admin
</.link>
</li>
<li>
<.link
href={~p"/user/log_out"}
method="delete"
class="text-sm leading-6 text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
Log out
</.link>
</li>
<% else %>
<li>
<.link
href={~p"/user/log_in"}
class="text-xs leading-6 text-gray-900 dark:text-gray-100 font-semibold dark:hover:text-gray-300 hover:text-gray-700"
>
Log in
</.link>
</li>
<% end %>
<li>
<.darkmode_toggle />
</li>
</ul>
"""
end
defp upload_error_to_string(:too_large), do: "Too large"
defp upload_error_to_string(:too_many_files), do: "You have selected too many files"
defp upload_error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
end

View file

@ -653,6 +653,118 @@ defmodule ChiyaWeb.CoreComponents do
"""
end
@doc """
Renders a horizontal line
"""
def line(assigns) do
~H"""
<hr class="my-6 dark:border-gray-700" />
"""
end
@doc """
Renders a UI for uploading files
"""
attr :upload, :map, required: true
attr :cancel_upload, :string, default: "cancel-upload"
def live_upload(assigns) do
~H"""
<div>
<.live_file_input upload={@upload} class="dark:text-gray-300" />
<section phx-drop-target={@upload.ref}>
<%= for entry <- @upload.entries do %>
<article class="upload-entry">
<figure>
<.live_img_preview entry={entry} />
<figcaption class="dark:text-gray-100"><%= entry.client_name %></figcaption>
</figure>
<div class="flex">
<%!-- entry.progress will update automatically for in-flight entries --%>
<progress value={entry.progress} max="100" class="w-full">
<%= entry.progress %>%
</progress>
<%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/3 --%>
<button
class="px-2 dark:text-white"
type="button"
phx-click="cancel-upload"
phx-value-ref={entry.ref}
aria-label="cancel"
>
&times;
</button>
</div>
<%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%>
<%= for err <- upload_errors(@upload, entry) do %>
<p class="alert alert-danger"><%= upload_error_to_string(err) %></p>
<% end %>
</article>
<% end %>
</section>
</div>
"""
end
@doc """
Renders the admin menu bar
"""
attr :current_user, :map, required: true
attr :settings, :map, required: true
def admin_bar(assigns) do
~H"""
<ul class="sticky top-0 backdrop-blur-sm z-10 flex items-center gap-4 py-3 px-4 sm:px-6 lg:px-8 bg-white/30 dark:bg-black/30">
<li>
<.link
href={~p"/"}
class="flex gap-3 text-sm leading-6 font-semibold text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
<%= @settings.title %>
</.link>
</li>
<li class="flex-1"></li>
<%= if @current_user do %>
<li>
<.link
href={~p"/admin"}
class="text-sm leading-6 text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
Admin
</.link>
</li>
<li>
<.link
href={~p"/user/log_out"}
method="delete"
class="text-sm leading-6 text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
Log out
</.link>
</li>
<% else %>
<li>
<.link
href={~p"/user/log_in"}
class="text-xs leading-6 text-gray-900 dark:text-gray-100 font-semibold dark:hover:text-gray-300 hover:text-gray-700"
>
Log in
</.link>
</li>
<% end %>
<li>
<.darkmode_toggle />
</li>
</ul>
"""
end
## JS Commands
def show(js \\ %JS{}, selector) do
@ -734,4 +846,9 @@ defmodule ChiyaWeb.CoreComponents do
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
defp upload_error_to_string(:too_large), do: "Too large"
defp upload_error_to_string(:too_many_files), do: "You have selected too many files"
defp upload_error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
end

View file

@ -1,7 +1,7 @@
defmodule ChiyaWeb.Layouts do
use ChiyaWeb, :html
import ChiyaWeb.PublicComponents
import ChiyaWeb.PublicComponents, only: [divider: 1]
embed_templates "layouts/*"
end

View file

@ -9,7 +9,35 @@ defmodule ChiyaWeb.PublicComponents do
import ChiyaWeb.Format
import ChiyaWeb.Markdown, only: [render: 1]
import Phoenix.HTML, only: [raw: 1]
import ChiyaWeb.CoreComponents
@doc """
Renders a [Hero Icon](https://heroicons.com).
Hero icons come in three styles outline, solid, and mini.
By default, the outline style is used, but solid an 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 your `priv/hero_icons` directory and bundled
within your compiled app.css by the plugin in your `assets/tailwind.config.js`.
## Examples
<.icon name="hero-cake" />
<.icon name="hero-cake-solid" />
<.icon name="hero-cake-mini" />
<.icon name="hero-bolt" class="bg-blue-500 w-10 h-10" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
def icon(%{name: "hero-" <> _} = assigns) do
~H"""
<span class={[@name, @class]} />
"""
end
@doc """
Renders a middot as divider
@ -164,58 +192,58 @@ defmodule ChiyaWeb.PublicComponents do
end
end
def comment_form(assigns) do
~H"""
<.simple_form :let={f} for={@changeset} action="" class="bg-theme-background -m-3">
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input
field={f[:author_name]}
type="text"
placeholder="Name"
class="bg-theme-background dark:bg-theme-background border-theme-base/20 dark:border-theme-base/20 text-theme-base dark:text-theme-base placeholder-theme-base/40 dark:placeholder-theme-base/60 dark:focus:border-theme-base/60 dark:focus:border-theme-base/60"
/>
<.input
field={f[:content]}
type="textarea"
placeholder="Content"
rows="3"
class="bg-theme-background dark:bg-theme-background border-theme-base/20 dark:border-theme-base/20 text-theme-base dark:text-theme-base placeholder-theme-base/60 dark:placeholder-theme-base/60 focus:border-theme-base/60 dark:focus:border-theme-base/60"
/>
<.input field={f[:note_id]} type="hidden" />
<:actions>
<.button>Submit Comment</.button>
</:actions>
</.simple_form>
"""
end
# def comment_form(assigns) do
# ~H"""
# <.simple_form :let={f} for={@changeset} action="" class="bg-theme-background -m-3">
# <.error :if={@changeset.action}>
# Oops, something went wrong! Please check the errors below.
# </.error>
# <.input
# field={f[:author_name]}
# type="text"
# placeholder="Name"
# class="bg-theme-background dark:bg-theme-background border-theme-base/20 dark:border-theme-base/20 text-theme-base dark:text-theme-base placeholder-theme-base/40 dark:placeholder-theme-base/60 dark:focus:border-theme-base/60 dark:focus:border-theme-base/60"
# />
# <.input
# field={f[:content]}
# type="textarea"
# placeholder="Content"
# rows="3"
# class="bg-theme-background dark:bg-theme-background border-theme-base/20 dark:border-theme-base/20 text-theme-base dark:text-theme-base placeholder-theme-base/60 dark:placeholder-theme-base/60 focus:border-theme-base/60 dark:focus:border-theme-base/60"
# />
# <.input field={f[:note_id]} type="hidden" />
# <:actions>
# <.button>Submit Comment</.button>
# </:actions>
# </.simple_form>
# """
# end
def comment_list(assigns) do
~H"""
<%= if not Enum.empty?(assigns.note.comments) do %>
<.line />
# def comment_list(assigns) do
# ~H"""
# <%= if not Enum.empty?(assigns.note.comments) do %>
# <.line />
<h2 class="mb-6 text-theme-base"><%= Enum.count(assigns.note.comments) %> Comments</h2>
# <h2 class="mb-6 text-theme-base"><%= Enum.count(assigns.note.comments) %> Comments</h2>
<aside id="comments" class="flex flex-col gap-6">
<%= for comment <- assigns.note.comments do %>
<article class="text-theme-base bg-theme-base/10 p-1">
<header class="flex flex-row justify-between">
<strong class="text-theme-primary"><%= comment.author_name %></strong>
<span class="text-theme-dim"><%= from_now(comment.inserted_at) %></span>
</header>
<p><%= comment.content %></p>
</article>
<% end %>
</aside>
<% else %>
<.line />
# <aside id="comments" class="flex flex-col gap-6">
# <%= for comment <- assigns.note.comments do %>
# <article class="text-theme-base bg-theme-base/10 p-1">
# <header class="flex flex-row justify-between">
# <strong class="text-theme-primary"><%= comment.author_name %></strong>
# <span class="text-theme-dim"><%= from_now(comment.inserted_at) %></span>
# </header>
# <p><%= comment.content %></p>
# </article>
# <% end %>
# </aside>
# <% else %>
# <.line />
<h2 class="mb-6 text-theme-base">No comments yet.</h2>
<% end %>
"""
end
# <h2 class="mb-6 text-theme-base">No comments yet.</h2>
# <% end %>
# """
# end
defp gallery_name(note), do: "gallery-#{note.id}"

View file

@ -105,10 +105,9 @@ defmodule ChiyaWeb.NoteController do
end
def publish(conn, %{"id" => id}) do
note_params = %{published_at: NaiveDateTime.local_now()}
note = Notes.get_note_preloaded!(id)
case Notes.update_note(note, note_params) do
case Notes.publish_note(note, NaiveDateTime.local_now()) do
{:ok, note} ->
conn
|> put_flash(:info, "Note published successfully.")
@ -125,10 +124,9 @@ defmodule ChiyaWeb.NoteController do
end
def unpublish(conn, %{"id" => id}) do
note_params = %{published_at: nil}
note = Notes.get_note_preloaded!(id)
case Notes.update_note(note, note_params) do
case Notes.publish_note(note, nil) do
{:ok, note} ->
conn
|> put_flash(:info, "Note un-published successfully.")

View file

@ -20,9 +20,12 @@ defmodule ChiyaWeb.Indie.MicropubHandler do
Logger.info("Properties: #{inspect(properties)}")
Logger.info("Type: #{type}")
settings = Chiya.Site.get_settings()
micropub_channel_id = settings.micropub_channel_id
with :ok <- verify_token(access_token),
{:ok, post_type} <- Props.get_post_type(properties),
{:ok, note_attrs} <- get_attrs(type, post_type, properties),
{:ok, note_attrs} <- get_attrs(type, post_type, properties, micropub_channel_id),
{:ok, note} <- Chiya.Notes.create_note(note_attrs) do
Logger.info("Note created!")
{:ok, :created, Chiya.Notes.Note.note_url(note)}
@ -157,16 +160,18 @@ defmodule ChiyaWeb.Indie.MicropubHandler do
end
end
defp get_attrs(type, post_type, properties) do
defp get_attrs(type, post_type, properties, default_channel_id) do
Logger.info("Creating a #{type}/#{post_type}..")
channel = Chiya.Channels.get_channel(default_channel_id)
case post_type do
:note -> get_note_attrs(properties)
:note -> get_note_attrs(properties, channel)
_ -> {:error, :insufficient_scope}
end
end
defp get_note_attrs(p) do
defp get_note_attrs(p, default_channel) do
content = Props.get_content(p)
name = Props.get_title(p) || String.slice(content, 0..15)
tags = Props.get_tags(p) |> Enum.join(",")
@ -176,13 +181,19 @@ defmodule ChiyaWeb.Indie.MicropubHandler do
do: NaiveDateTime.local_now(),
else: nil
{:ok,
%{
content: content,
name: name,
tags_string: tags,
published_at: published_at
}}
attrs = %{
content: content,
name: name,
tags_string: tags,
published_at: published_at
}
attrs =
if default_channel,
do: Map.put(attrs, :channel, default_channel),
else: attrs
{:ok, attrs}
end
defp get_hostname(),

View file

@ -60,13 +60,21 @@ defmodule ChiyaWeb.NoteShowLive do
<article>
<a href={"/admin/notes/#{@note.id}/image/#{image.id}"}>
<img
class="rounded-lg border border-theme-dim w-28 mb-3"
class={[
"rounded-lg border border-theme-dim w-28 mb-3",
image.featured && "border-theme-primary"
]}
src={ChiyaWeb.Uploaders.NoteImage.url({image.path, image}, :thumb_dithered)}
/>
<.button phx-click="delete_image" phx-value-id={image.id} data-confirm="Are you sure?">
Delete Image
</.button>
/>
</a>
<div class="flex justify-between">
<.button phx-click="delete_image" phx-value-id={image.id} data-confirm="Are you sure?">
<.icon name="hero-trash" />
</.button>
<.button phx-click="toggle_favorite" phx-value-id={image.id}>
<.icon name="hero-star-solid" />
</.button>
</div>
</article>
<% end %>
</div>
@ -138,6 +146,7 @@ defmodule ChiyaWeb.NoteShowLive do
|> assign(:note, Notes.get_note_preloaded!(socket.assigns.note.id))}
end
@impl Phoenix.LiveView
def handle_event("validate_edit_image", assigns, socket) do
{:noreply,
socket
@ -147,6 +156,7 @@ defmodule ChiyaWeb.NoteShowLive do
)}
end
@impl Phoenix.LiveView
def handle_event("update_edit_image", %{"id" => id} = assigns, socket) do
id
|> Notes.get_note_image!()
@ -164,6 +174,14 @@ defmodule ChiyaWeb.NoteShowLive do
{:noreply, assign(socket, :note, Notes.get_note_preloaded!(socket.assigns.note.id))}
end
@impl Phoenix.LiveView
def handle_event("toggle_favorite", %{"id" => id}, socket) do
image = Notes.get_note_image!(id)
Notes.update_note_image(image, %{featured: !image.featured})
{:noreply, assign(socket, :note, Notes.get_note_preloaded!(socket.assigns.note.id))}
end
defp note_links(notes), do: Enum.map_join(notes, ", ", fn n -> n.name end)
defp note_tags(tags), do: Enum.map_join(tags, ", ", fn t -> t.name end)
end