add admin media library with image management and block picker integration

- Schema: alt, caption, tags fields on images table with metadata changeset
- Context: list_images with filters, find_usages, used_image_ids, delete_with_cleanup
- Admin UI: /admin/media with grid view, upload, filters, detail panel, metadata editing
- Block editor: :image field type for image_text and content_body blocks
- Page renderer: image_id resolution with legacy URL fallback
- Mobile: bottom sheet detail panel with slide-up animation
- CSS: uses correct --t-* admin theme tokens, admin-badge colour variants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-27 22:20:51 +00:00
parent a039c8d53c
commit 847b5f3e5e
15 changed files with 1828 additions and 17 deletions

View File

@@ -206,6 +206,53 @@ defmodule BerrypodWeb.BlockEditorComponents do
"""
end
def block_field(%{field: %{type: :image}} = assigns) do
image_id = assigns.value
image = if is_binary(image_id) && image_id != "", do: Berrypod.Media.get_image(image_id)
assigns = assign(assigns, :image, image)
~H"""
<div class="admin-fieldset">
<span class="admin-label">{@field.label}</span>
<div class="image-field">
<%= if @image do %>
<div class="image-field-preview">
<%= if @image.is_svg do %>
<div class="image-field-svg">
<.icon name="hero-code-bracket" class="size-6" />
</div>
<% else %>
<img
src={"/image_cache/#{@image.id}-thumb.jpg"}
alt={@image.alt || @image.filename}
class="image-field-thumb"
/>
<% end %>
<div class="image-field-info">
<span class="image-field-filename">{@image.filename}</span>
<span :if={@image.alt} class="image-field-alt">{@image.alt}</span>
</div>
</div>
<% else %>
<div class="image-field-empty">
<.icon name="hero-photo" class="size-6" />
<span>No image selected</span>
</div>
<% end %>
<input
type="text"
name={"block_settings[#{@field.key}]"}
id={"block-#{@block_id}-#{@field.key}"}
value={if(@image, do: @image.id, else: "")}
placeholder="Paste image ID from media library"
class="admin-input admin-input-sm"
phx-debounce="blur"
/>
</div>
</div>
"""
end
def block_field(%{field: %{type: :repeater}} = assigns) do
items = if is_list(assigns.value), do: assigns.value, else: []
item_count = length(items)

View File

@@ -102,6 +102,14 @@
<.icon name="hero-document" class="size-5" /> Pages
</.link>
</li>
<li>
<.link
navigate={~p"/admin/media"}
class={admin_nav_active?(@current_path, "/admin/media")}
>
<.icon name="hero-photo" class="size-5" /> Media
</.link>
</li>
<li>
<.link
href={~p"/admin/theme"}