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

@@ -0,0 +1,87 @@
defmodule Mix.Tasks.Berrypod.BackfillAltText do
@shortdoc "Backfill alt text on existing images"
@moduledoc """
One-time task to populate alt text on images that were uploaded before
the alt field existed.
- Product images: copies alt from `product_images.alt`, falls back to product title
- Logo: uses site name from theme settings
- Header: "Header background"
- Icon: "Site icon"
- Skips images that already have alt text set
## Usage
mix berrypod.backfill_alt_text
"""
use Mix.Task
alias Berrypod.Repo
alias Berrypod.Media.Image
alias Berrypod.Products.ProductImage
alias Berrypod.Products.Product
import Ecto.Query
@impl true
def run(_args) do
Mix.Task.run("app.start")
backfill_product_images()
backfill_theme_images()
Mix.shell().info("Alt text backfill complete.")
end
defp backfill_product_images do
# Find product images with linked image records missing alt text
results =
from(pi in ProductImage,
join: i in Image,
on: i.id == pi.image_id,
join: p in Product,
on: p.id == pi.product_id,
where: is_nil(i.alt) and not is_nil(pi.image_id),
select: {i.id, pi.alt, p.title}
)
|> Repo.all()
count =
Enum.reduce(results, 0, fn {image_id, pi_alt, product_title}, acc ->
alt = pi_alt || product_title || "Product image"
from(i in Image, where: i.id == ^image_id)
|> Repo.update_all(set: [alt: alt])
acc + 1
end)
Mix.shell().info(" Updated #{count} product image(s)")
end
defp backfill_theme_images do
theme = Berrypod.Settings.get_theme_settings()
mapping = [
{theme.logo_image_id, theme.site_name || "Shop logo"},
{theme.header_image_id, "Header background"},
{theme.icon_image_id, "Site icon"}
]
count =
Enum.reduce(mapping, 0, fn {image_id, alt}, acc ->
if image_id do
{updated, _} =
from(i in Image, where: i.id == ^image_id and is_nil(i.alt))
|> Repo.update_all(set: [alt: alt])
acc + updated
else
acc
end
end)
Mix.shell().info(" Updated #{count} theme image(s)")
end
end