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

@@ -249,12 +249,13 @@ defmodule BerrypodWeb.PageRenderer do
defp render_block(%{block: %{"type" => "image_text"}} = assigns) do
settings = assigns.block["settings"] || %{}
image_url = resolve_block_image_url(settings["image_id"], settings["image_url"])
assigns =
assigns
|> assign(:section_title, settings["title"] || "")
|> assign(:section_description, settings["description"] || "")
|> assign(:image_url, settings["image_url"] || "")
|> assign(:image_url, image_url)
|> assign(:link_text, settings["link_text"])
|> assign(:link_href, settings["link_href"])
@@ -560,11 +561,12 @@ defmodule BerrypodWeb.PageRenderer do
defp render_block(%{block: %{"type" => "content_body"}} = assigns) do
settings = assigns.block["settings"] || %{}
content = settings["content"] || ""
{image_src, image_alt} = resolve_content_image(settings)
assigns =
assigns
|> assign(:image_src, settings["image_src"])
|> assign(:image_alt, settings["image_alt"] || "")
|> assign(:image_src, image_src)
|> assign(:image_alt, image_alt)
|> assign(:content, content)
~H"""
@@ -998,6 +1000,42 @@ defmodule BerrypodWeb.PageRenderer do
defp collection_path(slug, "featured"), do: ~p"/collections/#{slug}"
defp collection_path(slug, sort), do: ~p"/collections/#{slug}?sort=#{sort}"
# Resolves an image_id to a URL, falling back to a legacy URL string
defp resolve_block_image_url(image_id, fallback_url) do
case resolve_image(image_id) do
{url, _alt} -> url
nil -> fallback_url || ""
end
end
# Resolves image_id for content_body blocks, returning {src, alt}
defp resolve_content_image(settings) do
case resolve_image(settings["image_id"]) do
{src, alt} -> {src, alt}
nil -> {settings["image_src"], settings["image_alt"] || ""}
end
end
defp resolve_image(nil), do: nil
defp resolve_image(""), do: nil
defp resolve_image(image_id) do
case Berrypod.Media.get_image(image_id) do
nil ->
nil
image ->
url =
if image.is_svg do
"/image_cache/#{image.id}.webp"
else
"/image_cache/#{image.id}-800.webp"
end
{url, image.alt || image.filename}
end
end
def format_order_status("unfulfilled"), do: "Being prepared"
def format_order_status("submitted"), do: "Sent to printer"
def format_order_status("processing"), do: "In production"