From 5e0859f0a63d5ef23f6dedcb4a91f7a9d917b879 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 14 Sep 2026 15:09:14 -0400 Subject: [PATCH 1/4] Add photo upload UI to community fridge form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the plain "Photo URL" text input with a drag-and-drop upload widget backed by LiveView's file upload primitives and MediaStorage. - mount/1 registers a single-file upload slot (:photo, gif/png/jpg/jpeg) - Save handler calls consume_uploaded_entries → MediaStorage.upload_file! and injects the returned URL into params; falls through unchanged when no file is selected so the hidden input preserves the existing photo - cancel_upload event removes a pending entry before save - Template shows existing photo preview (with hidden input to preserve it through form submits), drag-and-drop zone with live_file_input, pending entry preview via live_img_preview, and per-entry error display Tests cover: existing photo renders in edit form, dropzone markup is present, pending upload preview appears after file selection, and cancel removes the preview. --- .../community_fridge_live/form_component.ex | 24 +++++++- .../form_component.html.heex | 44 +++++++++++++- .../live/community_fridge_live_test.exs | 58 ++++++++++++++++++- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.ex b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex index e63130fd..59522fc8 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/form_component.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex @@ -1,7 +1,12 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do use BikeBrigadeWeb, :live_component - alias BikeBrigade.Locations + alias BikeBrigade.{Locations, MediaStorage} + + @impl true + def mount(socket) do + {:ok, allow_upload(socket, :photo, accept: ~w(.gif .png .jpg .jpeg), max_entries: 1)} + end @impl true def update(%{community_fridge: community_fridge} = assigns, socket) do @@ -24,9 +29,26 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do end def handle_event("save", %{"community_fridge" => params}, socket) do + params = + case consume_uploaded_entries(socket, :photo, fn %{path: path}, + %{client_type: content_type} -> + {:ok, MediaStorage.upload_file!(path, content_type)} + end) do + [%{url: url}] -> Map.put(params, "photo", url) + [] -> params + end + save_community_fridge(socket, socket.assigns.action, params) end + def handle_event("cancel_upload", %{"ref" => ref}, socket) do + {:noreply, cancel_upload(socket, :photo, ref)} + end + + defp error_to_string(:too_large), do: "Too large" + defp error_to_string(:too_many_files), do: "You have selected too many files" + defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type" + defp save_community_fridge(socket, :edit, params) do case Locations.update_community_fridge(socket.assigns.community_fridge, params) do {:ok, _community_fridge} -> diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex index fbd9064e..03a4e40d 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex @@ -12,7 +12,49 @@ <.input type="textarea" field={f[:description]} label="Description" /> - <.input type="text" field={f[:photo]} label="Photo URL" /> +
+ +
+ + +
+
+
+ +
+ +

or drag and drop

+
+

PNG, JPG, GIF up to 10MB

+
+
+
+
+ <.live_img_preview entry={entry} class="w-16 rounded" /> +
{entry.client_name}
+ +

+ {error_to_string(err)} +

+
+
+
<.input type="checkbox" diff --git a/test/bike_brigade_web/live/community_fridge_live_test.exs b/test/bike_brigade_web/live/community_fridge_live_test.exs index 3a172463..b1f3d537 100644 --- a/test/bike_brigade_web/live/community_fridge_live_test.exs +++ b/test/bike_brigade_web/live/community_fridge_live_test.exs @@ -2,7 +2,7 @@ defmodule BikeBrigadeWeb.CommunityFridgeLiveTest do use BikeBrigadeWeb.ConnCase import Phoenix.LiveViewTest - alias BikeBrigade.Locations + alias BikeBrigade.MediaStorage.FakeMediaStorage describe "Community Fridges index (dispatcher)" do setup [:login] @@ -83,6 +83,62 @@ defmodule BikeBrigadeWeb.CommunityFridgeLiveTest do end end + describe "Community Fridge photo upload" do + setup [:login] + + test "shows existing photo in edit form", ctx do + fridge = + fixture(:community_fridge, %{ + name: "Photo Fridge", + photo: "https://example.com/fridge.jpg" + }) + + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges/#{fridge.id}/edit") + + assert html =~ "https://example.com/fridge.jpg" + end + + test "renders upload dropzone", ctx do + fixture(:community_fridge, %{name: "Upload Fridge"}) + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges/new") + + html = render(view) + assert html =~ "Upload a file" + assert html =~ "or drag and drop" + assert html =~ "PNG, JPG, GIF" + end + + test "shows pending upload preview after file is selected", ctx do + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges/new") + + upload = + file_input(view, "#community-fridge-form", :photo, [ + %{name: "fridge.jpg", content: <<0, 1, 2>>, type: "image/jpeg"} + ]) + + render_upload(upload, "fridge.jpg", 100) + + html = render(view) + assert html =~ "fridge.jpg" + end + + test "can cancel a pending upload", ctx do + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges/new") + + upload = + file_input(view, "#community-fridge-form", :photo, [ + %{name: "fridge.jpg", content: <<0, 1, 2>>, type: "image/jpeg"} + ]) + + render_upload(upload, "fridge.jpg", 100) + assert render(view) =~ "fridge.jpg" + + view |> element("button[aria-label='cancel']") |> render_click() + refute render(view) =~ "fridge.jpg" + end + + end + describe "Community Fridges access control" do setup [:login_as_rider] From fc379dce11ca4b5545e1fb985f48588d8bc03004 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 16:36:11 -0400 Subject: [PATCH 2/4] Enhance photo upload functionality: set max file size and add delete option --- .../community_fridge_live/form_component.ex | 18 ++++++++++++++++-- .../form_component.html.heex | 16 +++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.ex b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex index 59522fc8..3f821db7 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/form_component.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex @@ -5,7 +5,12 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do @impl true def mount(socket) do - {:ok, allow_upload(socket, :photo, accept: ~w(.gif .png .jpg .jpeg), max_entries: 1)} + {:ok, + allow_upload(socket, :photo, + accept: ~w(.gif .png .jpg .jpeg), + max_entries: 1, + max_file_size: 10_000_000 + )} end @impl true @@ -35,12 +40,21 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do {:ok, MediaStorage.upload_file!(path, content_type)} end) do [%{url: url}] -> Map.put(params, "photo", url) - [] -> params + [] -> Map.update(params, "photo", nil, &if(&1 == "", do: nil, else: &1)) end save_community_fridge(socket, socket.assigns.action, params) end + def handle_event("delete_photo", _params, socket) do + changeset = + socket.assigns.community_fridge + |> Locations.change_community_fridge(%{photo: nil}) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + def handle_event("cancel_upload", %{"ref" => ref}, socket) do {:noreply, cancel_upload(socket, :photo, ref)} end diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex index fecffd30..506850ac 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex @@ -14,9 +14,19 @@
-
- - + +
+ +
+ <.button + color={:lightred} + size={:xsmall} + phx-click={JS.push("delete_photo", target: @myself)} + type="button" + > + Delete + +
Date: Wed, 7 Oct 2026 16:46:37 -0400 Subject: [PATCH 3/4] Refactor photo input field for improved readability --- .../live/community_fridge_live/form_component.html.heex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex index 506850ac..2c21fcfd 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex @@ -14,7 +14,11 @@
- +
From aab451bfaaa3d2f018df79021fe9f889899d343e Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 16:49:37 -0400 Subject: [PATCH 4/4] Remove unnecessary blank line in Community Fridge photo upload tests --- test/bike_brigade_web/live/community_fridge_live_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/bike_brigade_web/live/community_fridge_live_test.exs b/test/bike_brigade_web/live/community_fridge_live_test.exs index b1f3d537..b9cc4e38 100644 --- a/test/bike_brigade_web/live/community_fridge_live_test.exs +++ b/test/bike_brigade_web/live/community_fridge_live_test.exs @@ -136,7 +136,6 @@ defmodule BikeBrigadeWeb.CommunityFridgeLiveTest do view |> element("button[aria-label='cancel']") |> render_click() refute render(view) =~ "fridge.jpg" end - end describe "Community Fridges access control" do