From 68b5e8302ec8f0544ae0f0f73b40401a0719803f Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Tue, 25 Aug 2026 16:08:04 -0400 Subject: [PATCH 01/37] Add CommunityFridge data model Introduces the CommunityFridge schema, migration, and test fixtures as the first step of the community fridges feature. --- .../locations/community_fridge.ex | 33 ++++++++ lib/bike_brigade/locations/location.ex | 2 + ...0825183237_add_community_fridges_table.exs | 16 ++++ .../locations/community_fridge_test.exs | 80 +++++++++++++++++++ test/support/fixtures.ex | 32 ++++++++ 5 files changed, 163 insertions(+) create mode 100644 lib/bike_brigade/locations/community_fridge.ex create mode 100644 priv/repo/migrations/20260825183237_add_community_fridges_table.exs create mode 100644 test/bike_brigade/locations/community_fridge_test.exs diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex new file mode 100644 index 00000000..7bb8cc62 --- /dev/null +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -0,0 +1,33 @@ +defmodule BikeBrigade.Locations.CommunityFridge do + use BikeBrigade.Schema + import Ecto.Changeset + + alias BikeBrigade.Locations.Location + + @fields [ + :name, + :description, + :photo, + :active, + :pair_preferred + ] + + schema "community_fridges" do + field :name, :string + field :description, :string + field :photo, :string + field :active, :boolean, default: true + field :pair_preferred, :boolean, default: false + + belongs_to :location, Location + + timestamps() + end + + def changeset(community_fridge, attrs) do + community_fridge + |> cast(attrs, @fields ++ [:location_id]) + |> validate_required([:name]) + |> assoc_constraint(:location) + end +end diff --git a/lib/bike_brigade/locations/location.ex b/lib/bike_brigade/locations/location.ex index e9eb6cb2..7ae0f2c3 100644 --- a/lib/bike_brigade/locations/location.ex +++ b/lib/bike_brigade/locations/location.ex @@ -4,6 +4,7 @@ defmodule BikeBrigade.Locations.Location do alias BikeBrigade.Geocoder alias BikeBrigade.Locations.LocationNeighborhood + alias BikeBrigade.Locations.CommunityFridge alias BikeBrigade.Riders.Rider alias BikeBrigade.Delivery.{Campaign, Opportunity, Task} @@ -29,6 +30,7 @@ defmodule BikeBrigade.Locations.Location do has_one :task_dropoff, Task, foreign_key: :dropoff_location_id, on_delete: :nilify_all has_one :task_pickup, Task, foreign_key: :pickup_location_id, on_delete: :nilify_all has_one :opportunity, Opportunity, on_delete: :nilify_all + has_one :community_fridge, CommunityFridge, on_delete: :nilify_all timestamps() end diff --git a/priv/repo/migrations/20260825183237_add_community_fridges_table.exs b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs new file mode 100644 index 00000000..ea421f9f --- /dev/null +++ b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs @@ -0,0 +1,16 @@ +defmodule BikeBrigade.Repo.Migrations.AddCommunityFridgesTable do + use Ecto.Migration + + def change do + create table(:community_fridges) do + add :name, :string + add :description, :text + add :photo, :string + add :active, :boolean, default: true, null: false + add :pair_preferred, :boolean, default: false, null: false + add :location_id, references(:locations, on_delete: :nilify_all) + + timestamps() + end + end +end diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs new file mode 100644 index 00000000..e4c86ca8 --- /dev/null +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -0,0 +1,80 @@ +defmodule BikeBrigade.Locations.CommunityFridgeTest do + use BikeBrigade.DataCase + + alias BikeBrigade.Locations.CommunityFridge + + setup do + location_data = fixture(:location) + + {:ok, location} = + %BikeBrigade.Locations.Location{} + |> BikeBrigade.Locations.Location.changeset(location_data) + |> Repo.insert() + + %{location: location} + end + + describe "CommunityFridge changeset" do + test "changeset with valid attributes", %{location: location} do + attrs = %{ + name: "Downtown Community Fridge", + description: "A fridge for the community", + photo: "https://example.com/photo.jpg", + active: true, + pair_preferred: false, + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + assert changeset.valid? + end + + test "changeset with minimal required attributes", %{location: location} do + attrs = %{ + name: "Community Fridge", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + assert changeset.valid? + end + + test "changeset requires name", %{location: location} do + attrs = %{ + description: "A fridge without a name", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + refute changeset.valid? + assert "can't be blank" in errors_on(changeset).name + end + + test "changeset rejects empty name", %{location: location} do + attrs = %{ + name: "", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + refute changeset.valid? + assert "can't be blank" in errors_on(changeset).name + end + + test "changeset sets default values for active and pair_preferred", %{location: location} do + attrs = %{ + name: "Fridge with Defaults", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + assert Ecto.Changeset.get_field(changeset, :active) == true + assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false + end + end +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 4581f4b2..05dcab70 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -186,6 +186,38 @@ defmodule BikeBrigade.Fixtures do Toronto.random_location() end + def fixture(:community_fridge, attrs) do + # Ensure we have a location_id + attrs = + attrs + |> Map.put_new_lazy(:location_id, fn -> + location_data = fixture(:location) + + {:ok, location} = + %BikeBrigade.Locations.Location{} + |> BikeBrigade.Locations.Location.changeset(location_data) + |> Repo.insert() + + location.id + end) + + {:ok, fridge} = + %BikeBrigade.Locations.CommunityFridge{} + |> BikeBrigade.Locations.CommunityFridge.changeset( + %{ + name: "Community Fridge #{System.unique_integer([:positive])}", + description: "A community fridge for the neighborhood", + active: true, + pair_preferred: false + } + |> Map.merge(attrs) + ) + |> Repo.insert() + + fridge + |> Repo.preload(:location) + end + def fixture(:sms_message_from_rider, rider), do: fixture(:sms_message_from_rider, rider, %{}) From be328c45fd558925e0f704c670f6884fc5c1554c Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 26 Aug 2026 07:32:37 -0400 Subject: [PATCH 02/37] Fix parameter name in changeset function for CommunityFridge module --- lib/bike_brigade/locations/community_fridge.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index 7bb8cc62..da532257 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -24,9 +24,9 @@ defmodule BikeBrigade.Locations.CommunityFridge do timestamps() end - def changeset(community_fridge, attrs) do + def changeset(community_fridge, params) do community_fridge - |> cast(attrs, @fields ++ [:location_id]) + |> cast(params, @fields ++ [:location_id]) |> validate_required([:name]) |> assoc_constraint(:location) end From e841246cbe47ad71e769acf9075337a1c67c1437 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 26 Aug 2026 11:57:25 -0400 Subject: [PATCH 03/37] Community Fridge CRUD operation added --- lib/bike_brigade/locations.ex | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lib/bike_brigade/locations.ex b/lib/bike_brigade/locations.ex index 214dc3f5..e4568a08 100644 --- a/lib/bike_brigade/locations.ex +++ b/lib/bike_brigade/locations.ex @@ -1,6 +1,9 @@ defmodule BikeBrigade.Locations do + import Ecto.Query, warn: false + alias BikeBrigade.Repo alias BikeBrigade.Locations.Location + alias BikeBrigade.Locations.CommunityFridge def neighborhood(%Location{} = location) do if neighborhood = Repo.preload(location, :neighborhood).neighborhood do @@ -13,4 +16,50 @@ defmodule BikeBrigade.Locations do def neighborhood(nil) do "Unknown" end + + @doc """ + Returns the list of community_fridges. + """ + def list_community_fridges do + Repo.all(from cf in CommunityFridge, order_by: [desc: cf.active, asc: cf.name]) + end + + @doc """ + Gets a single community_fridge. + + Raises `Ecto.NoResultsError` if the Community fridge does not exist. + """ + def get_community_fridge!(id), do: Repo.get!(CommunityFridge, id) + + @doc """ + Creates a community_fridge. + """ + def create_community_fridge(attrs \\ %{}) do + %CommunityFridge{} + |> CommunityFridge.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a community_fridge. + """ + def update_community_fridge(%CommunityFridge{} = community_fridge, attrs) do + community_fridge + |> CommunityFridge.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a community_fridge. + """ + def delete_community_fridge(%CommunityFridge{} = community_fridge) do + Repo.delete(community_fridge) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking community_fridge changes. + """ + def change_community_fridge(%CommunityFridge{} = community_fridge, attrs \\ %{}) do + CommunityFridge.changeset(community_fridge, attrs) + end end From 32365d65f4b6833b329c47b47c705448c2096e53 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 26 Aug 2026 12:14:08 -0400 Subject: [PATCH 04/37] Addressed the review comments --- lib/bike_brigade/locations/community_fridge.ex | 2 +- .../migrations/20260825183237_add_community_fridges_table.exs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index da532257..208092ab 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -27,7 +27,7 @@ defmodule BikeBrigade.Locations.CommunityFridge do def changeset(community_fridge, params) do community_fridge |> cast(params, @fields ++ [:location_id]) - |> validate_required([:name]) + |> validate_required([:name, :location_id]) |> assoc_constraint(:location) end end diff --git a/priv/repo/migrations/20260825183237_add_community_fridges_table.exs b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs index ea421f9f..14a97a67 100644 --- a/priv/repo/migrations/20260825183237_add_community_fridges_table.exs +++ b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs @@ -3,7 +3,7 @@ defmodule BikeBrigade.Repo.Migrations.AddCommunityFridgesTable do def change do create table(:community_fridges) do - add :name, :string + add :name, :string, null: false add :description, :text add :photo, :string add :active, :boolean, default: true, null: false @@ -12,5 +12,7 @@ defmodule BikeBrigade.Repo.Migrations.AddCommunityFridgesTable do timestamps() end + + create unique_index(:community_fridges, [:location_id]) end end From f38a1bc1dcce55dc35afb40e54c6413e1a31d97c Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Thu, 27 Aug 2026 14:42:13 -0400 Subject: [PATCH 05/37] Refactor CommunityFridgeTest to use alias for Location and update test descriptions --- .../locations/community_fridge_test.exs | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs index e4c86ca8..44395d11 100644 --- a/test/bike_brigade/locations/community_fridge_test.exs +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -1,14 +1,15 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do use BikeBrigade.DataCase + alias BikeBrigade.Locations.Location alias BikeBrigade.Locations.CommunityFridge setup do location_data = fixture(:location) {:ok, location} = - %BikeBrigade.Locations.Location{} - |> BikeBrigade.Locations.Location.changeset(location_data) + %Location{} + |> Location.changeset(location_data) |> Repo.insert() %{location: location} @@ -30,7 +31,7 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do assert changeset.valid? end - test "changeset with minimal required attributes", %{location: location} do + test "changeset with minimal required attributes and default values", %{location: location} do attrs = %{ name: "Community Fridge", location_id: location.id @@ -39,6 +40,8 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) assert changeset.valid? + assert Ecto.Changeset.get_field(changeset, :active) == true + assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false end test "changeset requires name", %{location: location} do @@ -64,17 +67,5 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do refute changeset.valid? assert "can't be blank" in errors_on(changeset).name end - - test "changeset sets default values for active and pair_preferred", %{location: location} do - attrs = %{ - name: "Fridge with Defaults", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) - - assert Ecto.Changeset.get_field(changeset, :active) == true - assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false - end end end From 3c70e8065119df471b44a1e0c56a8d13a5613a7c Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Fri, 28 Aug 2026 07:56:31 -0400 Subject: [PATCH 06/37] Defer community fridge location fixture improvement to separate PR The cubic-dev review suggested supporting a `location:` key in fixture(:community_fridge, ...) so callers can pass a location struct directly, matching the pattern used by rider and campaign fixtures. Implementing this requires fixture(:location) to persist a Location record and return a struct with an id, rather than returning a plain map as it does today. That change affects every fixture and test that currently calls fixture(:location), so the blast radius is wider than this PR's scope. The improvement will be handled in a follow-up PR dedicated to normalising the location fixture across the test suite. --- test/support/fixtures.ex | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 05dcab70..4581f4b2 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -186,38 +186,6 @@ defmodule BikeBrigade.Fixtures do Toronto.random_location() end - def fixture(:community_fridge, attrs) do - # Ensure we have a location_id - attrs = - attrs - |> Map.put_new_lazy(:location_id, fn -> - location_data = fixture(:location) - - {:ok, location} = - %BikeBrigade.Locations.Location{} - |> BikeBrigade.Locations.Location.changeset(location_data) - |> Repo.insert() - - location.id - end) - - {:ok, fridge} = - %BikeBrigade.Locations.CommunityFridge{} - |> BikeBrigade.Locations.CommunityFridge.changeset( - %{ - name: "Community Fridge #{System.unique_integer([:positive])}", - description: "A community fridge for the neighborhood", - active: true, - pair_preferred: false - } - |> Map.merge(attrs) - ) - |> Repo.insert() - - fridge - |> Repo.preload(:location) - end - def fixture(:sms_message_from_rider, rider), do: fixture(:sms_message_from_rider, rider, %{}) From 6226e4273e548f6be4a2a6997715a33402a8be50 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 31 Aug 2026 13:50:40 -0400 Subject: [PATCH 07/37] Enforce one community fridge per location via unique_constraint Add unique_constraint(:location_id) to CommunityFridge.changeset/2 so a location cannot be associated with more than one community fridge. The has_one relationship does not enforce cardinality at the database level, so this constraint surfaces duplicate-location errors as changeset errors. Also trim the minimal-attributes changeset test to focus on required fields rather than default values. --- lib/bike_brigade/locations/community_fridge.ex | 1 + test/bike_brigade/locations/community_fridge_test.exs | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index 208092ab..fb00feba 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -28,6 +28,7 @@ defmodule BikeBrigade.Locations.CommunityFridge do community_fridge |> cast(params, @fields ++ [:location_id]) |> validate_required([:name, :location_id]) + |> unique_constraint(:location_id) |> assoc_constraint(:location) end end diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs index 44395d11..c8b3dc46 100644 --- a/test/bike_brigade/locations/community_fridge_test.exs +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -31,7 +31,7 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do assert changeset.valid? end - test "changeset with minimal required attributes and default values", %{location: location} do + test "changeset with minimal required attributes", %{location: location} do attrs = %{ name: "Community Fridge", location_id: location.id @@ -40,8 +40,6 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) assert changeset.valid? - assert Ecto.Changeset.get_field(changeset, :active) == true - assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false end test "changeset requires name", %{location: location} do From be26bd26b22f85e6a63cd7d128ca8cbeabf8853b Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 31 Aug 2026 15:25:34 -0400 Subject: [PATCH 08/37] Add community fridge fixtures and comprehensive test coverage - Add `persisted_location` fixture that inserts a Location into the database (complements existing in-memory `location` fixture) - Add `community_fridge` fixture that lazily creates a persisted location and handles required location_id association - Add LocationsTest with full coverage of community_fridge operations - Use setup block to eliminate fixture repetition across tests - Use pin operator (^) in pattern matches to validate update attrs without assertion repetition - Enable async: true for parallel test execution --- test/bike_brigade/locations_test.exs | 118 +++++++++++++++++++++++++++ test/support/fixtures.ex | 24 +++++- 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 test/bike_brigade/locations_test.exs diff --git a/test/bike_brigade/locations_test.exs b/test/bike_brigade/locations_test.exs new file mode 100644 index 00000000..3e647d18 --- /dev/null +++ b/test/bike_brigade/locations_test.exs @@ -0,0 +1,118 @@ +defmodule BikeBrigade.LocationsTest do + use BikeBrigade.DataCase, async: true + + alias BikeBrigade.Locations + alias BikeBrigade.Locations.CommunityFridge + + describe "community_fridges" do + @invalid_attrs %{active: nil, name: nil, description: nil, photo: nil, pair_preferred: nil} + + setup do + %{community_fridge: fixture(:community_fridge)} + end + + test "list_community_fridges/0 returns all community_fridges", %{ + community_fridge: community_fridge + } do + assert community_fridge.id in Enum.map(Locations.list_community_fridges(), & &1.id) + end + + test "get_community_fridge!/1 returns the community_fridge with given id", %{ + community_fridge: community_fridge + } do + assert Locations.get_community_fridge!(community_fridge.id) == community_fridge + end + + test "create_community_fridge/1 with valid data creates a community_fridge" do + location = fixture(:persisted_location, %{}) + + valid_attrs = %{ + active: true, + name: "some name", + description: "some description", + photo: "some photo", + pair_preferred: true, + location_id: location.id + } + + assert {:ok, %CommunityFridge{} = community_fridge} = + Locations.create_community_fridge(valid_attrs) + + assert community_fridge.active == true + assert community_fridge.name == "some name" + assert community_fridge.description == "some description" + assert community_fridge.photo == "some photo" + assert community_fridge.pair_preferred == true + end + + test "create_community_fridge/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Locations.create_community_fridge(@invalid_attrs) + end + + test "update_community_fridge/2 with valid data updates the community_fridge", %{ + community_fridge: community_fridge + } do + active = false + name = "some updated name" + description = "some updated description" + photo = "some updated photo" + pair_preferred = false + + update_attrs = %{ + active: active, + name: name, + description: description, + photo: photo, + pair_preferred: pair_preferred + } + + assert {:ok, + %CommunityFridge{ + active: ^active, + name: ^name, + description: ^description, + photo: ^photo, + pair_preferred: ^pair_preferred + }} = Locations.update_community_fridge(community_fridge, update_attrs) + end + + test "update_community_fridge/2 with invalid data returns error changeset", %{ + community_fridge: community_fridge + } do + assert {:error, %Ecto.Changeset{}} = + Locations.update_community_fridge(community_fridge, @invalid_attrs) + + assert community_fridge == Locations.get_community_fridge!(community_fridge.id) + end + + test "delete_community_fridge/1 deletes the community_fridge", %{ + community_fridge: community_fridge + } do + assert {:ok, %CommunityFridge{}} = Locations.delete_community_fridge(community_fridge) + + assert_raise Ecto.NoResultsError, fn -> + Locations.get_community_fridge!(community_fridge.id) + end + end + + test "change_community_fridge/1 returns a community_fridge changeset", %{ + community_fridge: community_fridge + } do + assert %Ecto.Changeset{} = Locations.change_community_fridge(community_fridge) + end + end + + describe "community_fridges ordering" do + test "list_community_fridges/0 returns active fridges first then alphabetically by name" do + inactive = fixture(:community_fridge, %{name: "Alpha", active: false}) + active_b = fixture(:community_fridge, %{name: "Beta", active: true}) + active_a = fixture(:community_fridge, %{name: "Alpha", active: true}) + + assert Enum.map(Locations.list_community_fridges(), & &1.id) == [ + active_a.id, + active_b.id, + inactive.id + ] + end + end +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 4581f4b2..e392f86e 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -1,5 +1,5 @@ defmodule BikeBrigade.Fixtures do - alias BikeBrigade.{Accounts, Delivery, Riders, Messaging, Notifications, Repo} + alias BikeBrigade.{Accounts, Delivery, Locations, Riders, Messaging, Notifications, Repo} alias BikeBrigade.Repo.Seeds.Toronto alias BikeBrigade.Repo.Seeds.Toronto @@ -186,6 +186,28 @@ defmodule BikeBrigade.Fixtures do Toronto.random_location() end + def fixture(:persisted_location, attrs) do + %Locations.Location{} + |> Locations.Location.changeset(Map.merge(Toronto.random_location(), attrs)) + |> Repo.insert!() + end + + def fixture(:community_fridge, attrs) do + attrs = + attrs + |> Map.put_new_lazy(:location_id, fn -> fixture(:persisted_location, %{}).id end) + + {:ok, community_fridge} = + %{ + name: Faker.Company.name(), + active: true + } + |> Map.merge(attrs) + |> Locations.create_community_fridge() + + community_fridge + end + def fixture(:sms_message_from_rider, rider), do: fixture(:sms_message_from_rider, rider, %{}) From a4f815f9a24ebdcf111a2a91a0cc09794848d7ac Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 14 Sep 2026 14:03:00 -0400 Subject: [PATCH 09/37] Add Community Fridge dispatcher UI to dispatchers only. Changes: - Add CommunityFridgeLive.Index and FormComponent with routes for /community_fridges, /community_fridges/new, and /community_fridges/:id/edit - Add "Community Fridges" entry to the sidebar navigation (archive_box icon) - Relax CommunityFridge changeset to not require location_id at the UI level (name is the only required field; location association remains constrained at the DB level via assoc_constraint) - Add LiveView tests covering list, create, validate, edit, direct navigation, and access control (non-dispatchers redirected to /login) --- .../locations/community_fridge.ex | 2 +- lib/bike_brigade_web/components/layouts.ex | 9 ++ .../community_fridge_live/form_component.ex | 55 +++++++++++ .../form_component.html.heex | 34 +++++++ .../live/community_fridge_live/index.ex | 40 ++++++++ .../community_fridge_live/index.html.heex | 47 ++++++++++ lib/bike_brigade_web/router.ex | 4 + .../live/community_fridge_live_test.exs | 93 +++++++++++++++++++ 8 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 lib/bike_brigade_web/live/community_fridge_live/form_component.ex create mode 100644 lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex create mode 100644 lib/bike_brigade_web/live/community_fridge_live/index.ex create mode 100644 lib/bike_brigade_web/live/community_fridge_live/index.html.heex create mode 100644 test/bike_brigade_web/live/community_fridge_live_test.exs diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index fb00feba..c801e49e 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -27,7 +27,7 @@ defmodule BikeBrigade.Locations.CommunityFridge do def changeset(community_fridge, params) do community_fridge |> cast(params, @fields ++ [:location_id]) - |> validate_required([:name, :location_id]) + |> validate_required([:name]) |> unique_constraint(:location_id) |> assoc_constraint(:location) end diff --git a/lib/bike_brigade_web/components/layouts.ex b/lib/bike_brigade_web/components/layouts.ex index 7771b87d..17f6db13 100644 --- a/lib/bike_brigade_web/components/layouts.ex +++ b/lib/bike_brigade_web/components/layouts.ex @@ -69,6 +69,15 @@ defmodule BikeBrigadeWeb.Layouts do Riders + <.sidebar_link + selected={@current_page == :community_fridges} + navigate={~p"/community_fridges"} + > + <:icon> + + + Community Fridges + <.sidebar_link selected={@current_page == :stats} navigate={~p"/stats"}> <:icon> 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 new file mode 100644 index 00000000..e63130fd --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.ex @@ -0,0 +1,55 @@ +defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do + use BikeBrigadeWeb, :live_component + + alias BikeBrigade.Locations + + @impl true + def update(%{community_fridge: community_fridge} = assigns, socket) do + changeset = Locations.change_community_fridge(community_fridge) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"community_fridge" => params}, socket) do + changeset = + socket.assigns.community_fridge + |> Locations.change_community_fridge(params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"community_fridge" => params}, socket) do + save_community_fridge(socket, socket.assigns.action, params) + end + + defp save_community_fridge(socket, :edit, params) do + case Locations.update_community_fridge(socket.assigns.community_fridge, params) do + {:ok, _community_fridge} -> + {:noreply, + socket + |> put_flash(:info, "Community fridge updated successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_community_fridge(socket, :new, params) do + case Locations.create_community_fridge(params) do + {:ok, _community_fridge} -> + {:noreply, + socket + |> put_flash(:info, "Community fridge created successfully") + |> push_navigate(to: socket.assigns.navigate)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +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 new file mode 100644 index 00000000..fbd9064e --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/form_component.html.heex @@ -0,0 +1,34 @@ +
+ <.header>{@title} + <.simple_form + :let={f} + for={@changeset} + id="community-fridge-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input type="text" field={f[:name]} label="Name" /> + + <.input type="textarea" field={f[:description]} label="Description" /> + + <.input type="text" field={f[:photo]} label="Photo URL" /> + + <.input + type="checkbox" + field={f[:active]} + label="Active" + /> + + <.input + type="checkbox" + field={f[:pair_preferred]} + label="Pair Preferred" + help_text="Deliveries to this fridge prefer a rider pair" + /> + + <:actions> + <.button type="submit" phx-disable-with="Saving...">Save + + +
diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.ex b/lib/bike_brigade_web/live/community_fridge_live/index.ex new file mode 100644 index 00000000..2e3433b3 --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/index.ex @@ -0,0 +1,40 @@ +defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do + use BikeBrigadeWeb, :live_view + + alias BikeBrigade.Locations + alias BikeBrigade.Locations.CommunityFridge + + @impl Phoenix.LiveView + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page, :community_fridges) + |> assign(:page_title, "Community Fridges") + |> assign(:community_fridge, nil) + |> assign(:community_fridges, Locations.list_community_fridges())} + end + + @impl Phoenix.LiveView + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Community Fridges") + |> assign(:community_fridge, nil) + |> assign(:community_fridges, Locations.list_community_fridges()) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Fridge") + |> assign(:community_fridge, %CommunityFridge{}) + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Fridge") + |> assign(:community_fridge, Locations.get_community_fridge!(id)) + end +end diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex new file mode 100644 index 00000000..dd8e8ee9 --- /dev/null +++ b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex @@ -0,0 +1,47 @@ +<.header> + Community Fridges + <:subtitle> + Community fridges are neighbourhood food-sharing locations. Dispatchers can manage fridge details here. + + +
+ <.button patch={~p"/community_fridges/new"}> + New Community Fridge + +
+<.table id="community_fridges" rows={@community_fridges}> + <:col :let={community_fridge} label="Name"> + {community_fridge.name} + + <:col :let={community_fridge} label="Description"> + {community_fridge.description} + + <:col :let={community_fridge} label="Pair Preferred"> + {if community_fridge.pair_preferred, do: "Yes", else: "No"} + + <:col :let={community_fridge} label="Active"> + {if community_fridge.active, do: "Yes", else: "No"} + + + <:action :let={community_fridge}> + <.link patch={~p"/community_fridges/#{community_fridge.id}/edit"} class="link"> + Edit + + + + +<.modal + :if={@live_action in [:new, :edit]} + id="community-fridge-modal" + show + on_cancel={JS.navigate(~p"/community_fridges")} +> + <.live_component + module={BikeBrigadeWeb.CommunityFridgeLive.FormComponent} + id={@community_fridge.id || :new} + action={@live_action} + title={@page_title} + community_fridge={@community_fridge} + navigate={~p"/community_fridges"} + /> + diff --git a/lib/bike_brigade_web/router.ex b/lib/bike_brigade_web/router.ex index aa288e6a..3080f591 100644 --- a/lib/bike_brigade_web/router.ex +++ b/lib/bike_brigade_web/router.ex @@ -180,6 +180,10 @@ defmodule BikeBrigadeWeb.Router do live "/tags/:id/edit", TagLive.Index, :edit live "/delivery_notes", DeliveryNoteLive.Index, :index + + live "/community_fridges", CommunityFridgeLive.Index, :index + live "/community_fridges/new", CommunityFridgeLive.Index, :new + live "/community_fridges/:id/edit", CommunityFridgeLive.Index, :edit end get "/stats/leaderboard/download", ExportStatsController, :leaderboard diff --git a/test/bike_brigade_web/live/community_fridge_live_test.exs b/test/bike_brigade_web/live/community_fridge_live_test.exs new file mode 100644 index 00000000..3a172463 --- /dev/null +++ b/test/bike_brigade_web/live/community_fridge_live_test.exs @@ -0,0 +1,93 @@ +defmodule BikeBrigadeWeb.CommunityFridgeLiveTest do + use BikeBrigadeWeb.ConnCase + import Phoenix.LiveViewTest + + alias BikeBrigade.Locations + + describe "Community Fridges index (dispatcher)" do + setup [:login] + + test "lists all community fridges", ctx do + fridge = fixture(:community_fridge, %{name: "Test Fridge"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ "Community Fridges" + assert html =~ fridge.name + end + + test "can create a new community fridge", ctx do + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges") + + view |> element("a", "New Community Fridge") |> render_click() + assert_patched(view, ~p"/community_fridges/new") + + {:ok, _view, html} = + view + |> form("#community-fridge-form", community_fridge: %{name: "Brand New Fridge"}) + |> render_submit() + |> follow_redirect(ctx.conn) + + assert html =~ "Community fridge created successfully" + assert html =~ "Brand New Fridge" + end + + test "validates name is required", ctx do + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges/new") + + html = + view + |> form("#community-fridge-form", community_fridge: %{name: ""}) + |> render_submit() + + assert html =~ "can't be blank" + end + + test "can edit a community fridge", ctx do + fridge = fixture(:community_fridge, %{name: "Original Fridge"}) + {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges") + + view |> element("a", "Edit") |> render_click() + assert_patched(view, ~p"/community_fridges/#{fridge.id}/edit") + + {:ok, _view, html} = + view + |> form("#community-fridge-form", community_fridge: %{name: "Updated Fridge"}) + |> render_submit() + |> follow_redirect(ctx.conn) + + assert html =~ "Community fridge updated successfully" + assert html =~ "Updated Fridge" + refute html =~ "Original Fridge" + end + + test "can navigate directly to edit page", ctx do + fridge = fixture(:community_fridge, %{name: "Edit Me"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges/#{fridge.id}/edit") + + assert html =~ "Edit Fridge" + end + + test "edit preserves existing values", ctx do + fridge = + fixture(:community_fridge, %{ + name: "Preserve Me", + description: "Some description", + active: true, + pair_preferred: true + }) + + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges/#{fridge.id}/edit") + + assert html =~ "Preserve Me" + assert html =~ "Some description" + end + end + + describe "Community Fridges access control" do + setup [:login_as_rider] + + test "non-dispatchers cannot access community fridges page", ctx do + assert {:error, {:redirect, %{to: "/login"}}} = live(ctx.conn, ~p"/community_fridges") + end + end +end From 5e0859f0a63d5ef23f6dedcb4a91f7a9d917b879 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 14 Sep 2026 15:09:14 -0400 Subject: [PATCH 10/37] 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 ea5e3ceff3b645121e5bfdea8ce6286dd3bd0343 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 14 Sep 2026 15:12:01 -0400 Subject: [PATCH 11/37] Add Cancel button to community fridge form actions --- .../live/community_fridge_live/form_component.html.heex | 3 +++ 1 file changed, 3 insertions(+) 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..714bdf14 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 @@ -28,6 +28,9 @@ /> <:actions> + <.button type="button" navigate={~p"/community_fridges"} color={:white}> + Cancel + <.button type="submit" phx-disable-with="Saving...">Save From 14d55d967f7655fe2952c36984e5fa27c7e35637 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 14 Sep 2026 19:54:01 -0400 Subject: [PATCH 12/37] Add cancel button to community fridge form --- .../live/community_fridge_live/form_component.html.heex | 2 +- 1 file changed, 1 insertion(+), 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 714bdf14..7741fd21 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 @@ -28,7 +28,7 @@ /> <:actions> - <.button type="button" navigate={~p"/community_fridges"} color={:white}> + <.button navigate={~p"/community_fridges"} color={:white}> Cancel <.button type="submit" phx-disable-with="Saving...">Save From d3a250953dd1674405213a65a91d8094cd5acb53 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Tue, 25 Aug 2026 16:08:04 -0400 Subject: [PATCH 13/37] Add CommunityFridge data model Introduces the CommunityFridge schema, migration, and test fixtures as the first step of the community fridges feature. --- .../locations/community_fridge.ex | 33 ++++++++ lib/bike_brigade/locations/location.ex | 2 + ...0825183237_add_community_fridges_table.exs | 16 ++++ .../locations/community_fridge_test.exs | 80 +++++++++++++++++++ test/support/fixtures.ex | 32 ++++++++ 5 files changed, 163 insertions(+) create mode 100644 lib/bike_brigade/locations/community_fridge.ex create mode 100644 priv/repo/migrations/20260825183237_add_community_fridges_table.exs create mode 100644 test/bike_brigade/locations/community_fridge_test.exs diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex new file mode 100644 index 00000000..7bb8cc62 --- /dev/null +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -0,0 +1,33 @@ +defmodule BikeBrigade.Locations.CommunityFridge do + use BikeBrigade.Schema + import Ecto.Changeset + + alias BikeBrigade.Locations.Location + + @fields [ + :name, + :description, + :photo, + :active, + :pair_preferred + ] + + schema "community_fridges" do + field :name, :string + field :description, :string + field :photo, :string + field :active, :boolean, default: true + field :pair_preferred, :boolean, default: false + + belongs_to :location, Location + + timestamps() + end + + def changeset(community_fridge, attrs) do + community_fridge + |> cast(attrs, @fields ++ [:location_id]) + |> validate_required([:name]) + |> assoc_constraint(:location) + end +end diff --git a/lib/bike_brigade/locations/location.ex b/lib/bike_brigade/locations/location.ex index e9eb6cb2..7ae0f2c3 100644 --- a/lib/bike_brigade/locations/location.ex +++ b/lib/bike_brigade/locations/location.ex @@ -4,6 +4,7 @@ defmodule BikeBrigade.Locations.Location do alias BikeBrigade.Geocoder alias BikeBrigade.Locations.LocationNeighborhood + alias BikeBrigade.Locations.CommunityFridge alias BikeBrigade.Riders.Rider alias BikeBrigade.Delivery.{Campaign, Opportunity, Task} @@ -29,6 +30,7 @@ defmodule BikeBrigade.Locations.Location do has_one :task_dropoff, Task, foreign_key: :dropoff_location_id, on_delete: :nilify_all has_one :task_pickup, Task, foreign_key: :pickup_location_id, on_delete: :nilify_all has_one :opportunity, Opportunity, on_delete: :nilify_all + has_one :community_fridge, CommunityFridge, on_delete: :nilify_all timestamps() end diff --git a/priv/repo/migrations/20260825183237_add_community_fridges_table.exs b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs new file mode 100644 index 00000000..ea421f9f --- /dev/null +++ b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs @@ -0,0 +1,16 @@ +defmodule BikeBrigade.Repo.Migrations.AddCommunityFridgesTable do + use Ecto.Migration + + def change do + create table(:community_fridges) do + add :name, :string + add :description, :text + add :photo, :string + add :active, :boolean, default: true, null: false + add :pair_preferred, :boolean, default: false, null: false + add :location_id, references(:locations, on_delete: :nilify_all) + + timestamps() + end + end +end diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs new file mode 100644 index 00000000..e4c86ca8 --- /dev/null +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -0,0 +1,80 @@ +defmodule BikeBrigade.Locations.CommunityFridgeTest do + use BikeBrigade.DataCase + + alias BikeBrigade.Locations.CommunityFridge + + setup do + location_data = fixture(:location) + + {:ok, location} = + %BikeBrigade.Locations.Location{} + |> BikeBrigade.Locations.Location.changeset(location_data) + |> Repo.insert() + + %{location: location} + end + + describe "CommunityFridge changeset" do + test "changeset with valid attributes", %{location: location} do + attrs = %{ + name: "Downtown Community Fridge", + description: "A fridge for the community", + photo: "https://example.com/photo.jpg", + active: true, + pair_preferred: false, + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + assert changeset.valid? + end + + test "changeset with minimal required attributes", %{location: location} do + attrs = %{ + name: "Community Fridge", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + assert changeset.valid? + end + + test "changeset requires name", %{location: location} do + attrs = %{ + description: "A fridge without a name", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + refute changeset.valid? + assert "can't be blank" in errors_on(changeset).name + end + + test "changeset rejects empty name", %{location: location} do + attrs = %{ + name: "", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + refute changeset.valid? + assert "can't be blank" in errors_on(changeset).name + end + + test "changeset sets default values for active and pair_preferred", %{location: location} do + attrs = %{ + name: "Fridge with Defaults", + location_id: location.id + } + + changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + + assert Ecto.Changeset.get_field(changeset, :active) == true + assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false + end + end +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 4581f4b2..05dcab70 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -186,6 +186,38 @@ defmodule BikeBrigade.Fixtures do Toronto.random_location() end + def fixture(:community_fridge, attrs) do + # Ensure we have a location_id + attrs = + attrs + |> Map.put_new_lazy(:location_id, fn -> + location_data = fixture(:location) + + {:ok, location} = + %BikeBrigade.Locations.Location{} + |> BikeBrigade.Locations.Location.changeset(location_data) + |> Repo.insert() + + location.id + end) + + {:ok, fridge} = + %BikeBrigade.Locations.CommunityFridge{} + |> BikeBrigade.Locations.CommunityFridge.changeset( + %{ + name: "Community Fridge #{System.unique_integer([:positive])}", + description: "A community fridge for the neighborhood", + active: true, + pair_preferred: false + } + |> Map.merge(attrs) + ) + |> Repo.insert() + + fridge + |> Repo.preload(:location) + end + def fixture(:sms_message_from_rider, rider), do: fixture(:sms_message_from_rider, rider, %{}) From 25b84a3838defaef54663b393cf3604b8350b22b Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 26 Aug 2026 07:32:37 -0400 Subject: [PATCH 14/37] Fix parameter name in changeset function for CommunityFridge module --- lib/bike_brigade/locations/community_fridge.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index 7bb8cc62..da532257 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -24,9 +24,9 @@ defmodule BikeBrigade.Locations.CommunityFridge do timestamps() end - def changeset(community_fridge, attrs) do + def changeset(community_fridge, params) do community_fridge - |> cast(attrs, @fields ++ [:location_id]) + |> cast(params, @fields ++ [:location_id]) |> validate_required([:name]) |> assoc_constraint(:location) end From 727cb30b891d400598f55c75787e63c6446d40fe Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 26 Aug 2026 11:57:25 -0400 Subject: [PATCH 15/37] Community Fridge CRUD operation added --- lib/bike_brigade/locations.ex | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lib/bike_brigade/locations.ex b/lib/bike_brigade/locations.ex index 214dc3f5..e4568a08 100644 --- a/lib/bike_brigade/locations.ex +++ b/lib/bike_brigade/locations.ex @@ -1,6 +1,9 @@ defmodule BikeBrigade.Locations do + import Ecto.Query, warn: false + alias BikeBrigade.Repo alias BikeBrigade.Locations.Location + alias BikeBrigade.Locations.CommunityFridge def neighborhood(%Location{} = location) do if neighborhood = Repo.preload(location, :neighborhood).neighborhood do @@ -13,4 +16,50 @@ defmodule BikeBrigade.Locations do def neighborhood(nil) do "Unknown" end + + @doc """ + Returns the list of community_fridges. + """ + def list_community_fridges do + Repo.all(from cf in CommunityFridge, order_by: [desc: cf.active, asc: cf.name]) + end + + @doc """ + Gets a single community_fridge. + + Raises `Ecto.NoResultsError` if the Community fridge does not exist. + """ + def get_community_fridge!(id), do: Repo.get!(CommunityFridge, id) + + @doc """ + Creates a community_fridge. + """ + def create_community_fridge(attrs \\ %{}) do + %CommunityFridge{} + |> CommunityFridge.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a community_fridge. + """ + def update_community_fridge(%CommunityFridge{} = community_fridge, attrs) do + community_fridge + |> CommunityFridge.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a community_fridge. + """ + def delete_community_fridge(%CommunityFridge{} = community_fridge) do + Repo.delete(community_fridge) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking community_fridge changes. + """ + def change_community_fridge(%CommunityFridge{} = community_fridge, attrs \\ %{}) do + CommunityFridge.changeset(community_fridge, attrs) + end end From e3f38132fd885d49ad32ee95249c30660fbca0d2 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 26 Aug 2026 12:14:08 -0400 Subject: [PATCH 16/37] Addressed the review comments --- lib/bike_brigade/locations/community_fridge.ex | 2 +- .../migrations/20260825183237_add_community_fridges_table.exs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index da532257..208092ab 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -27,7 +27,7 @@ defmodule BikeBrigade.Locations.CommunityFridge do def changeset(community_fridge, params) do community_fridge |> cast(params, @fields ++ [:location_id]) - |> validate_required([:name]) + |> validate_required([:name, :location_id]) |> assoc_constraint(:location) end end diff --git a/priv/repo/migrations/20260825183237_add_community_fridges_table.exs b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs index ea421f9f..14a97a67 100644 --- a/priv/repo/migrations/20260825183237_add_community_fridges_table.exs +++ b/priv/repo/migrations/20260825183237_add_community_fridges_table.exs @@ -3,7 +3,7 @@ defmodule BikeBrigade.Repo.Migrations.AddCommunityFridgesTable do def change do create table(:community_fridges) do - add :name, :string + add :name, :string, null: false add :description, :text add :photo, :string add :active, :boolean, default: true, null: false @@ -12,5 +12,7 @@ defmodule BikeBrigade.Repo.Migrations.AddCommunityFridgesTable do timestamps() end + + create unique_index(:community_fridges, [:location_id]) end end From 58ee025cdb7b3783c3a5a16b8af722057581d2d4 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Thu, 27 Aug 2026 14:42:13 -0400 Subject: [PATCH 17/37] Refactor CommunityFridgeTest to use alias for Location and update test descriptions --- .../locations/community_fridge_test.exs | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs index e4c86ca8..44395d11 100644 --- a/test/bike_brigade/locations/community_fridge_test.exs +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -1,14 +1,15 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do use BikeBrigade.DataCase + alias BikeBrigade.Locations.Location alias BikeBrigade.Locations.CommunityFridge setup do location_data = fixture(:location) {:ok, location} = - %BikeBrigade.Locations.Location{} - |> BikeBrigade.Locations.Location.changeset(location_data) + %Location{} + |> Location.changeset(location_data) |> Repo.insert() %{location: location} @@ -30,7 +31,7 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do assert changeset.valid? end - test "changeset with minimal required attributes", %{location: location} do + test "changeset with minimal required attributes and default values", %{location: location} do attrs = %{ name: "Community Fridge", location_id: location.id @@ -39,6 +40,8 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) assert changeset.valid? + assert Ecto.Changeset.get_field(changeset, :active) == true + assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false end test "changeset requires name", %{location: location} do @@ -64,17 +67,5 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do refute changeset.valid? assert "can't be blank" in errors_on(changeset).name end - - test "changeset sets default values for active and pair_preferred", %{location: location} do - attrs = %{ - name: "Fridge with Defaults", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) - - assert Ecto.Changeset.get_field(changeset, :active) == true - assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false - end end end From 36eeb0776f400ee137d0cd36954bded576513e2b Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Fri, 28 Aug 2026 07:56:31 -0400 Subject: [PATCH 18/37] Defer community fridge location fixture improvement to separate PR The cubic-dev review suggested supporting a `location:` key in fixture(:community_fridge, ...) so callers can pass a location struct directly, matching the pattern used by rider and campaign fixtures. Implementing this requires fixture(:location) to persist a Location record and return a struct with an id, rather than returning a plain map as it does today. That change affects every fixture and test that currently calls fixture(:location), so the blast radius is wider than this PR's scope. The improvement will be handled in a follow-up PR dedicated to normalising the location fixture across the test suite. --- test/support/fixtures.ex | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 05dcab70..4581f4b2 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -186,38 +186,6 @@ defmodule BikeBrigade.Fixtures do Toronto.random_location() end - def fixture(:community_fridge, attrs) do - # Ensure we have a location_id - attrs = - attrs - |> Map.put_new_lazy(:location_id, fn -> - location_data = fixture(:location) - - {:ok, location} = - %BikeBrigade.Locations.Location{} - |> BikeBrigade.Locations.Location.changeset(location_data) - |> Repo.insert() - - location.id - end) - - {:ok, fridge} = - %BikeBrigade.Locations.CommunityFridge{} - |> BikeBrigade.Locations.CommunityFridge.changeset( - %{ - name: "Community Fridge #{System.unique_integer([:positive])}", - description: "A community fridge for the neighborhood", - active: true, - pair_preferred: false - } - |> Map.merge(attrs) - ) - |> Repo.insert() - - fridge - |> Repo.preload(:location) - end - def fixture(:sms_message_from_rider, rider), do: fixture(:sms_message_from_rider, rider, %{}) From 34cd1c7f062836e13555472ea2db9c6ec5d8456a Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 31 Aug 2026 13:50:40 -0400 Subject: [PATCH 19/37] Enforce one community fridge per location via unique_constraint Add unique_constraint(:location_id) to CommunityFridge.changeset/2 so a location cannot be associated with more than one community fridge. The has_one relationship does not enforce cardinality at the database level, so this constraint surfaces duplicate-location errors as changeset errors. Also trim the minimal-attributes changeset test to focus on required fields rather than default values. --- lib/bike_brigade/locations/community_fridge.ex | 1 + test/bike_brigade/locations/community_fridge_test.exs | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index 208092ab..fb00feba 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -28,6 +28,7 @@ defmodule BikeBrigade.Locations.CommunityFridge do community_fridge |> cast(params, @fields ++ [:location_id]) |> validate_required([:name, :location_id]) + |> unique_constraint(:location_id) |> assoc_constraint(:location) end end diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs index 44395d11..c8b3dc46 100644 --- a/test/bike_brigade/locations/community_fridge_test.exs +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -31,7 +31,7 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do assert changeset.valid? end - test "changeset with minimal required attributes and default values", %{location: location} do + test "changeset with minimal required attributes", %{location: location} do attrs = %{ name: "Community Fridge", location_id: location.id @@ -40,8 +40,6 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) assert changeset.valid? - assert Ecto.Changeset.get_field(changeset, :active) == true - assert Ecto.Changeset.get_field(changeset, :pair_preferred) == false end test "changeset requires name", %{location: location} do From 8849b2b3c1f75cc99a7ecc8d3f343a49915b56cd Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 31 Aug 2026 15:25:34 -0400 Subject: [PATCH 20/37] Add community fridge fixtures and comprehensive test coverage - Add `persisted_location` fixture that inserts a Location into the database (complements existing in-memory `location` fixture) - Add `community_fridge` fixture that lazily creates a persisted location and handles required location_id association - Add LocationsTest with full coverage of community_fridge operations - Use setup block to eliminate fixture repetition across tests - Use pin operator (^) in pattern matches to validate update attrs without assertion repetition - Enable async: true for parallel test execution --- test/bike_brigade/locations_test.exs | 118 +++++++++++++++++++++++++++ test/support/fixtures.ex | 24 +++++- 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 test/bike_brigade/locations_test.exs diff --git a/test/bike_brigade/locations_test.exs b/test/bike_brigade/locations_test.exs new file mode 100644 index 00000000..3e647d18 --- /dev/null +++ b/test/bike_brigade/locations_test.exs @@ -0,0 +1,118 @@ +defmodule BikeBrigade.LocationsTest do + use BikeBrigade.DataCase, async: true + + alias BikeBrigade.Locations + alias BikeBrigade.Locations.CommunityFridge + + describe "community_fridges" do + @invalid_attrs %{active: nil, name: nil, description: nil, photo: nil, pair_preferred: nil} + + setup do + %{community_fridge: fixture(:community_fridge)} + end + + test "list_community_fridges/0 returns all community_fridges", %{ + community_fridge: community_fridge + } do + assert community_fridge.id in Enum.map(Locations.list_community_fridges(), & &1.id) + end + + test "get_community_fridge!/1 returns the community_fridge with given id", %{ + community_fridge: community_fridge + } do + assert Locations.get_community_fridge!(community_fridge.id) == community_fridge + end + + test "create_community_fridge/1 with valid data creates a community_fridge" do + location = fixture(:persisted_location, %{}) + + valid_attrs = %{ + active: true, + name: "some name", + description: "some description", + photo: "some photo", + pair_preferred: true, + location_id: location.id + } + + assert {:ok, %CommunityFridge{} = community_fridge} = + Locations.create_community_fridge(valid_attrs) + + assert community_fridge.active == true + assert community_fridge.name == "some name" + assert community_fridge.description == "some description" + assert community_fridge.photo == "some photo" + assert community_fridge.pair_preferred == true + end + + test "create_community_fridge/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Locations.create_community_fridge(@invalid_attrs) + end + + test "update_community_fridge/2 with valid data updates the community_fridge", %{ + community_fridge: community_fridge + } do + active = false + name = "some updated name" + description = "some updated description" + photo = "some updated photo" + pair_preferred = false + + update_attrs = %{ + active: active, + name: name, + description: description, + photo: photo, + pair_preferred: pair_preferred + } + + assert {:ok, + %CommunityFridge{ + active: ^active, + name: ^name, + description: ^description, + photo: ^photo, + pair_preferred: ^pair_preferred + }} = Locations.update_community_fridge(community_fridge, update_attrs) + end + + test "update_community_fridge/2 with invalid data returns error changeset", %{ + community_fridge: community_fridge + } do + assert {:error, %Ecto.Changeset{}} = + Locations.update_community_fridge(community_fridge, @invalid_attrs) + + assert community_fridge == Locations.get_community_fridge!(community_fridge.id) + end + + test "delete_community_fridge/1 deletes the community_fridge", %{ + community_fridge: community_fridge + } do + assert {:ok, %CommunityFridge{}} = Locations.delete_community_fridge(community_fridge) + + assert_raise Ecto.NoResultsError, fn -> + Locations.get_community_fridge!(community_fridge.id) + end + end + + test "change_community_fridge/1 returns a community_fridge changeset", %{ + community_fridge: community_fridge + } do + assert %Ecto.Changeset{} = Locations.change_community_fridge(community_fridge) + end + end + + describe "community_fridges ordering" do + test "list_community_fridges/0 returns active fridges first then alphabetically by name" do + inactive = fixture(:community_fridge, %{name: "Alpha", active: false}) + active_b = fixture(:community_fridge, %{name: "Beta", active: true}) + active_a = fixture(:community_fridge, %{name: "Alpha", active: true}) + + assert Enum.map(Locations.list_community_fridges(), & &1.id) == [ + active_a.id, + active_b.id, + inactive.id + ] + end + end +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 4581f4b2..e392f86e 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -1,5 +1,5 @@ defmodule BikeBrigade.Fixtures do - alias BikeBrigade.{Accounts, Delivery, Riders, Messaging, Notifications, Repo} + alias BikeBrigade.{Accounts, Delivery, Locations, Riders, Messaging, Notifications, Repo} alias BikeBrigade.Repo.Seeds.Toronto alias BikeBrigade.Repo.Seeds.Toronto @@ -186,6 +186,28 @@ defmodule BikeBrigade.Fixtures do Toronto.random_location() end + def fixture(:persisted_location, attrs) do + %Locations.Location{} + |> Locations.Location.changeset(Map.merge(Toronto.random_location(), attrs)) + |> Repo.insert!() + end + + def fixture(:community_fridge, attrs) do + attrs = + attrs + |> Map.put_new_lazy(:location_id, fn -> fixture(:persisted_location, %{}).id end) + + {:ok, community_fridge} = + %{ + name: Faker.Company.name(), + active: true + } + |> Map.merge(attrs) + |> Locations.create_community_fridge() + + community_fridge + end + def fixture(:sms_message_from_rider, rider), do: fixture(:sms_message_from_rider, rider, %{}) From 3fc3a85ad76b8099e8c54d48382dab06eeabf3c6 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 13:25:25 -0400 Subject: [PATCH 21/37] Enhance community fridge functionality with location support and map integration - Preload location data in community fridge queries and forms. - Update changeset to handle nested location parameters. - Add map display and address column in community fridge index. - Improve tests to validate location handling and map rendering. --- lib/bike_brigade/locations.ex | 14 +++- .../locations/community_fridge.ex | 20 +++++- .../community_fridge_live/form_component.ex | 5 +- .../form_component.html.heex | 7 ++ .../live/community_fridge_live/index.ex | 44 +++++++++++- .../community_fridge_live/index.html.heex | 5 ++ .../locations/community_fridge_test.exs | 67 +++++++++---------- .../live/community_fridge_live_test.exs | 21 +++++- test/support/fixtures.ex | 9 +-- 9 files changed, 138 insertions(+), 54 deletions(-) diff --git a/lib/bike_brigade/locations.ex b/lib/bike_brigade/locations.ex index e4568a08..64985f4c 100644 --- a/lib/bike_brigade/locations.ex +++ b/lib/bike_brigade/locations.ex @@ -21,7 +21,10 @@ defmodule BikeBrigade.Locations do Returns the list of community_fridges. """ def list_community_fridges do - Repo.all(from cf in CommunityFridge, order_by: [desc: cf.active, asc: cf.name]) + CommunityFridge + |> from(order_by: [desc: :active, asc: :name]) + |> Repo.all() + |> Repo.preload(:location) end @doc """ @@ -29,13 +32,17 @@ defmodule BikeBrigade.Locations do Raises `Ecto.NoResultsError` if the Community fridge does not exist. """ - def get_community_fridge!(id), do: Repo.get!(CommunityFridge, id) + def get_community_fridge!(id) do + CommunityFridge + |> Repo.get!(id) + |> Repo.preload(:location) + end @doc """ Creates a community_fridge. """ def create_community_fridge(attrs \\ %{}) do - %CommunityFridge{} + %CommunityFridge{location: nil} |> CommunityFridge.changeset(attrs) |> Repo.insert() end @@ -45,6 +52,7 @@ defmodule BikeBrigade.Locations do """ def update_community_fridge(%CommunityFridge{} = community_fridge, attrs) do community_fridge + |> Repo.preload(:location) |> CommunityFridge.changeset(attrs) |> Repo.update() end diff --git a/lib/bike_brigade/locations/community_fridge.ex b/lib/bike_brigade/locations/community_fridge.ex index c801e49e..aa536b6a 100644 --- a/lib/bike_brigade/locations/community_fridge.ex +++ b/lib/bike_brigade/locations/community_fridge.ex @@ -19,16 +19,30 @@ defmodule BikeBrigade.Locations.CommunityFridge do field :active, :boolean, default: true field :pair_preferred, :boolean, default: false - belongs_to :location, Location + belongs_to :location, Location, on_replace: :update timestamps() end def changeset(community_fridge, params) do community_fridge - |> cast(params, @fields ++ [:location_id]) + |> cast(params, @fields) + |> maybe_cast_location(params) |> validate_required([:name]) |> unique_constraint(:location_id) - |> assoc_constraint(:location) + end + + # Only associate a location when an address is present in the params (new location), + # or the fridge already has a location that may need updating (edit scenario). + defp maybe_cast_location(changeset, params) do + location_params = Map.get(params, "location") || Map.get(params, :location, %{}) + address = Map.get(location_params, "address") || Map.get(location_params, :address) + has_existing = not is_nil(changeset.data.location_id) + + if address not in [nil, ""] || has_existing do + cast_assoc(changeset, :location) + else + changeset + end end end 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..2f0a7697 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,8 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do use BikeBrigadeWeb, :live_component - alias BikeBrigade.{Locations, MediaStorage} + alias BikeBrigade.{Locations, MediaStorage, Repo} + alias BikeBrigadeWeb.Components.LiveLocation @impl true def mount(socket) do @@ -10,11 +11,13 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do @impl true def update(%{community_fridge: community_fridge} = assigns, socket) do + community_fridge = Repo.preload(community_fridge, :location) changeset = Locations.change_community_fridge(community_fridge) {:ok, socket |> assign(assigns) + |> assign(:community_fridge, community_fridge) |> assign(:changeset, changeset)} 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 03a4e40d..858267e5 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,6 +12,13 @@ <.input type="textarea" field={f[:description]} label="Description" /> + <.live_component + module={LiveLocation} + id="location-form" + field={f[:location]} + label="Location" + /> +
diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.ex b/lib/bike_brigade_web/live/community_fridge_live/index.ex index 2e3433b3..2776253a 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.ex @@ -4,6 +4,8 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do alias BikeBrigade.Locations alias BikeBrigade.Locations.CommunityFridge + @default_coords %Geo.Point{coordinates: {-79.3832, 43.6532}} + @impl Phoenix.LiveView def mount(_params, _session, socket) do {:ok, @@ -11,7 +13,9 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do |> assign(:page, :community_fridges) |> assign(:page_title, "Community Fridges") |> assign(:community_fridge, nil) - |> assign(:community_fridges, Locations.list_community_fridges())} + |> assign(:community_fridges, []) + |> assign(:map_layers, []) + |> assign(:map_coords, @default_coords)} end @impl Phoenix.LiveView @@ -20,10 +24,14 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do end defp apply_action(socket, :index, _params) do + community_fridges = Locations.list_community_fridges() + socket |> assign(:page_title, "Community Fridges") |> assign(:community_fridge, nil) - |> assign(:community_fridges, Locations.list_community_fridges()) + |> assign(:community_fridges, community_fridges) + |> assign(:map_layers, fridge_markers(community_fridges)) + |> assign(:map_coords, map_center(community_fridges)) end defp apply_action(socket, :new, _params) do @@ -37,4 +45,36 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do |> assign(:page_title, "Edit Fridge") |> assign(:community_fridge, Locations.get_community_fridge!(id)) end + + defp fridge_markers(community_fridges) do + for %{id: id, name: name, location: location} <- community_fridges, + not is_nil(location), + not is_nil(location.coords) do + %{ + id: "fridge-#{id}", + type: :marker, + data: %{ + lat: lat(location), + lng: lng(location), + icon: "warehouse", + color: "#1c64f2", + tooltip: name + } + } + end + end + + # Toronto city hall as fallback centre + @default_coords %Geo.Point{coordinates: {-79.3832, 43.6532}} + + defp map_center(community_fridges) do + community_fridges + |> Enum.find_value(fn %{location: location} -> + location && location.coords + end) + |> case do + nil -> @default_coords + coords -> coords + end + end end diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex index dd8e8ee9..6ff4cd11 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex @@ -4,6 +4,8 @@ Community fridges are neighbourhood food-sharing locations. Dispatchers can manage fridge details here. +<.map id="community-fridges-map" coords={@map_coords} initial_layers={@map_layers} class="w-full h-64 mb-4" /> +
<.button patch={~p"/community_fridges/new"}> New Community Fridge @@ -13,6 +15,9 @@ <:col :let={community_fridge} label="Name"> {community_fridge.name} + <:col :let={community_fridge} label="Address"> + {community_fridge.location} + <:col :let={community_fridge} label="Description"> {community_fridge.description} diff --git a/test/bike_brigade/locations/community_fridge_test.exs b/test/bike_brigade/locations/community_fridge_test.exs index c8b3dc46..e2e4004c 100644 --- a/test/bike_brigade/locations/community_fridge_test.exs +++ b/test/bike_brigade/locations/community_fridge_test.exs @@ -1,69 +1,62 @@ defmodule BikeBrigade.Locations.CommunityFridgeTest do use BikeBrigade.DataCase - alias BikeBrigade.Locations.Location + alias BikeBrigade.Locations alias BikeBrigade.Locations.CommunityFridge - - setup do - location_data = fixture(:location) - - {:ok, location} = - %Location{} - |> Location.changeset(location_data) - |> Repo.insert() - - %{location: location} - end + alias BikeBrigade.Repo.Seeds.Toronto describe "CommunityFridge changeset" do - test "changeset with valid attributes", %{location: location} do + test "valid with nested location params" do attrs = %{ name: "Downtown Community Fridge", description: "A fridge for the community", - photo: "https://example.com/photo.jpg", active: true, pair_preferred: false, - location_id: location.id + location: Toronto.random_location() } - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + changeset = CommunityFridge.changeset(%CommunityFridge{location: nil}, attrs) assert changeset.valid? end - test "changeset with minimal required attributes", %{location: location} do - attrs = %{ - name: "Community Fridge", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + test "valid without a location" do + changeset = + CommunityFridge.changeset(%CommunityFridge{location: nil}, %{name: "Bare Fridge"}) assert changeset.valid? end - test "changeset requires name", %{location: location} do - attrs = %{ - description: "A fridge without a name", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + test "requires name" do + changeset = + CommunityFridge.changeset(%CommunityFridge{location: nil}, %{ + location: Toronto.random_location() + }) refute changeset.valid? assert "can't be blank" in errors_on(changeset).name end - test "changeset rejects empty name", %{location: location} do - attrs = %{ - name: "", - location_id: location.id - } - - changeset = CommunityFridge.changeset(%CommunityFridge{}, attrs) + test "rejects empty name" do + changeset = + CommunityFridge.changeset(%CommunityFridge{location: nil}, %{ + name: "", + location: Toronto.random_location() + }) refute changeset.valid? assert "can't be blank" in errors_on(changeset).name end + + test "updating location updates the associated record in place" do + fridge = fixture(:community_fridge) + original_location_id = fridge.location.id + + new_location = Toronto.random_location() + {:ok, updated} = Locations.update_community_fridge(fridge, %{location: new_location}) + + assert updated.location.id == original_location_id + assert updated.location.address == new_location.address + end end end 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..0cb5f01e 100644 --- a/test/bike_brigade_web/live/community_fridge_live_test.exs +++ b/test/bike_brigade_web/live/community_fridge_live_test.exs @@ -15,6 +15,26 @@ defmodule BikeBrigadeWeb.CommunityFridgeLiveTest do assert html =~ fridge.name end + test "shows address column for fridge with a location", ctx do + fridge = fixture(:community_fridge, %{name: "Located Fridge"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ fridge.location.address + end + + test "renders map element on index page", ctx do + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ "community-fridges-map" + end + + test "map includes pin for fridge with coordinates", ctx do + fridge = fixture(:community_fridge, %{name: "Mapped Fridge"}) + {:ok, _view, html} = live(ctx.conn, ~p"/community_fridges") + + assert html =~ "fridge-#{fridge.id}" + end + test "can create a new community fridge", ctx do {:ok, view, _html} = live(ctx.conn, ~p"/community_fridges") @@ -136,7 +156,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 diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index e392f86e..d96147d3 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -193,15 +193,10 @@ defmodule BikeBrigade.Fixtures do end def fixture(:community_fridge, attrs) do - attrs = - attrs - |> Map.put_new_lazy(:location_id, fn -> fixture(:persisted_location, %{}).id end) + attrs = Map.put_new_lazy(attrs, :location, fn -> Toronto.random_location() end) {:ok, community_fridge} = - %{ - name: Faker.Company.name(), - active: true - } + %{name: Faker.Company.name(), active: true} |> Map.merge(attrs) |> Locations.create_community_fridge() From 5bb96b433cfcf0f73226894defb3f8f2f69c9457 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 14:14:50 -0400 Subject: [PATCH 22/37] Implement mode switching between list and map views in community fridge index --- .../community_fridge_live/form_component.ex | 4 +- .../live/community_fridge_live/index.ex | 20 ++++- .../community_fridge_live/index.html.heex | 77 +++++++++++++------ 3 files changed, 71 insertions(+), 30 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 2f0a7697..1f8e917b 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 @@ -58,7 +58,7 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do {:noreply, socket |> put_flash(:info, "Community fridge updated successfully") - |> push_navigate(to: socket.assigns.navigate)} + |> push_patch(to: socket.assigns.navigate)} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, :changeset, changeset)} @@ -71,7 +71,7 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do {:noreply, socket |> put_flash(:info, "Community fridge created successfully") - |> push_navigate(to: socket.assigns.navigate)} + |> push_patch(to: socket.assigns.navigate)} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, changeset: changeset)} diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.ex b/lib/bike_brigade_web/live/community_fridge_live/index.ex index 2776253a..c03bc87e 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.ex @@ -15,7 +15,8 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do |> assign(:community_fridge, nil) |> assign(:community_fridges, []) |> assign(:map_layers, []) - |> assign(:map_coords, @default_coords)} + |> assign(:map_coords, @default_coords) + |> assign(:mode, :list)} end @impl Phoenix.LiveView @@ -23,6 +24,20 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end + @impl Phoenix.LiveView + def handle_event("set_mode", %{"mode" => mode}, socket) do + socket = assign(socket, :mode, String.to_existing_atom(mode)) + + socket = + if socket.assigns.mode == :map do + push_event(socket, "leaflet:redraw_map", %{recenter: false}) + else + socket + end + + {:noreply, socket} + end + defp apply_action(socket, :index, _params) do community_fridges = Locations.list_community_fridges() @@ -64,9 +79,6 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do end end - # Toronto city hall as fallback centre - @default_coords %Geo.Point{coordinates: {-79.3832, 43.6532}} - defp map_center(community_fridges) do community_fridges |> Enum.find_value(fn %{location: location} -> diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex index 6ff4cd11..6d027b5e 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex @@ -4,36 +4,65 @@ Community fridges are neighbourhood food-sharing locations. Dispatchers can manage fridge details here. -<.map id="community-fridges-map" coords={@map_coords} initial_layers={@map_layers} class="w-full h-64 mb-4" /> -
+
+
+ + +
<.button patch={~p"/community_fridges/new"}> New Community Fridge
-<.table id="community_fridges" rows={@community_fridges}> - <:col :let={community_fridge} label="Name"> - {community_fridge.name} - - <:col :let={community_fridge} label="Address"> - {community_fridge.location} - - <:col :let={community_fridge} label="Description"> - {community_fridge.description} - - <:col :let={community_fridge} label="Pair Preferred"> - {if community_fridge.pair_preferred, do: "Yes", else: "No"} - - <:col :let={community_fridge} label="Active"> - {if community_fridge.active, do: "Yes", else: "No"} - - <:action :let={community_fridge}> - <.link patch={~p"/community_fridges/#{community_fridge.id}/edit"} class="link"> - Edit - - - +<%= if @mode == :map do %> +
+
+ <.map + id="community-fridges-map" + coords={@map_coords} + initial_layers={@map_layers} + class="h-full" + /> +
+
+<% else %> + <.table id="community_fridges" rows={@community_fridges}> + <:col :let={community_fridge} label="Name"> + {community_fridge.name} + + <:col :let={community_fridge} label="Address"> + {community_fridge.location} + + <:col :let={community_fridge} label="Description"> + {community_fridge.description} + + <:col :let={community_fridge} label="Pair Preferred"> + {if community_fridge.pair_preferred, do: "Yes", else: "No"} + + <:col :let={community_fridge} label="Active"> + {if community_fridge.active, do: "Yes", else: "No"} + + + <:action :let={community_fridge}> + <.link patch={~p"/community_fridges/#{community_fridge.id}/edit"} class="link"> + Edit + + + +<% end %> <.modal :if={@live_action in [:new, :edit]} From b77e3072ac4d2ba9bd52d743b1758d4f64d3b56f Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 15:41:12 -0400 Subject: [PATCH 23/37] Fix redundant DB query and register :community_fridges page in layout attr - Remove duplicate list_community_fridges/0 call from apply_action(:index) since mount/3 already loads the list on every mount - Add :community_fridges to the current_page attr values list in layouts.ex --- lib/bike_brigade_web/components/layouts.ex | 1 + lib/bike_brigade_web/live/community_fridge_live/index.ex | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bike_brigade_web/components/layouts.ex b/lib/bike_brigade_web/components/layouts.ex index 17f6db13..9685aa26 100644 --- a/lib/bike_brigade_web/components/layouts.ex +++ b/lib/bike_brigade_web/components/layouts.ex @@ -32,6 +32,7 @@ defmodule BikeBrigadeWeb.Layouts do :campaigns, :opportunities, :riders, + :community_fridges, :stats, :users, :messages, diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.ex b/lib/bike_brigade_web/live/community_fridge_live/index.ex index 2e3433b3..ac341c8a 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.ex @@ -23,7 +23,6 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do socket |> assign(:page_title, "Community Fridges") |> assign(:community_fridge, nil) - |> assign(:community_fridges, Locations.list_community_fridges()) end defp apply_action(socket, :new, _params) do From fc379dce11ca4b5545e1fb985f48588d8bc03004 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 16:36:11 -0400 Subject: [PATCH 24/37] 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 25/37] 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 26/37] 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 From 3a219c0a1476a6bbd559bcbe59c1bab92db0b18b Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 17:16:11 -0400 Subject: [PATCH 27/37] addressed merge conflicts --- .../live/community_fridge_live/index.html.heex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex index 6ff4cd11..5986223a 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex @@ -4,7 +4,12 @@ Community fridges are neighbourhood food-sharing locations. Dispatchers can manage fridge details here. -<.map id="community-fridges-map" coords={@map_coords} initial_layers={@map_layers} class="w-full h-64 mb-4" /> +<.map + id="community-fridges-map" + coords={@map_coords} + initial_layers={@map_layers} + class="w-full h-64 mb-4" +/>
<.button patch={~p"/community_fridges/new"}> From e31545bcae5a4872b8213168885eb486ae8270c9 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Wed, 7 Oct 2026 17:34:32 -0400 Subject: [PATCH 28/37] Refactor CommunityFridgeLive components: remove unused Repo alias and duplicate default_coords definition --- .../live/community_fridge_live/form_component.ex | 4 +--- lib/bike_brigade_web/live/community_fridge_live/index.ex | 4 +--- 2 files changed, 2 insertions(+), 6 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 21f54090..4d41ff3a 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,7 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do use BikeBrigadeWeb, :live_component - alias BikeBrigade.{Locations, MediaStorage, Repo} + alias BikeBrigade.{Locations, MediaStorage} alias BikeBrigadeWeb.Components.LiveLocation @impl true @@ -16,13 +16,11 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do @impl true def update(%{community_fridge: community_fridge} = assigns, socket) do - community_fridge = Repo.preload(community_fridge, :location) changeset = Locations.change_community_fridge(community_fridge) {:ok, socket |> assign(assigns) - |> assign(:community_fridge, community_fridge) |> assign(:changeset, changeset)} end diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.ex b/lib/bike_brigade_web/live/community_fridge_live/index.ex index 8e559fba..caf2aa85 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.ex @@ -4,6 +4,7 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do alias BikeBrigade.Locations alias BikeBrigade.Locations.CommunityFridge + # Toronto city hall as fallback centre @default_coords %Geo.Point{coordinates: {-79.3832, 43.6532}} @impl Phoenix.LiveView @@ -61,9 +62,6 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do end end - # Toronto city hall as fallback centre - @default_coords %Geo.Point{coordinates: {-79.3832, 43.6532}} - defp map_center(community_fridges) do community_fridges |> Enum.find_value(fn %{location: location} -> From 277cc3db7818af4bb1aaa3aff5d04e04d01754d5 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Thu, 8 Oct 2026 10:28:17 -0400 Subject: [PATCH 29/37] Update CommunityFridgeLive: set default location for new fridge and remove map component from index --- lib/bike_brigade_web/live/community_fridge_live/index.ex | 2 +- .../live/community_fridge_live/index.html.heex | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.ex b/lib/bike_brigade_web/live/community_fridge_live/index.ex index bf44f7e6..ac39f608 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.ex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.ex @@ -50,7 +50,7 @@ defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do defp apply_action(socket, :new, _params) do socket |> assign(:page_title, "New Fridge") - |> assign(:community_fridge, %CommunityFridge{}) + |> assign(:community_fridge, %CommunityFridge{location: nil}) end defp apply_action(socket, :edit, %{"id" => id}) do diff --git a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex index fabf43a8..92cd6840 100644 --- a/lib/bike_brigade_web/live/community_fridge_live/index.html.heex +++ b/lib/bike_brigade_web/live/community_fridge_live/index.html.heex @@ -4,13 +4,6 @@ Community fridges are neighbourhood food-sharing locations. Dispatchers can manage fridge details here. -<.map - id="community-fridges-map" - coords={@map_coords} - initial_layers={@map_layers} - class="w-full h-64 mb-4" -/> -