Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
68b5e83
Add CommunityFridge data model
neal-bpm Aug 25, 2026
be328c4
Fix parameter name in changeset function for CommunityFridge module
neal-bpm Aug 26, 2026
e841246
Community Fridge CRUD operation added
neal-bpm Aug 26, 2026
32365d6
Addressed the review comments
neal-bpm Aug 26, 2026
f38a1bc
Refactor CommunityFridgeTest to use alias for Location and update tes…
neal-bpm Aug 27, 2026
d7c2e06
Merge branch 'main' into vn/community-fridges-data-model-v1
neal-bpm Aug 27, 2026
3c70e80
Defer community fridge location fixture improvement to separate PR
neal-bpm Aug 28, 2026
6226e42
Enforce one community fridge per location via unique_constraint
neal-bpm Aug 31, 2026
702a77b
Merge branch 'vn/community-fridges-data-model-v1' into vn/community-f…
neal-bpm Aug 31, 2026
be26bd2
Add community fridge fixtures and comprehensive test coverage
neal-bpm Aug 31, 2026
0a046ab
Merge branch 'main' into vn/community-fridge-crud-v1
neal-bpm Sep 3, 2026
55476d1
Merge branch 'main' into vn/community-fridge-ui-v1
neal-bpm Sep 10, 2026
a4f815f
Add Community Fridge dispatcher UI to dispatchers only.
neal-bpm Sep 14, 2026
c3c5d45
Merge branch 'main' into vn/community-fridges-data-model-v1
neal-bpm Sep 14, 2026
5e0859f
Add photo upload UI to community fridge form
neal-bpm Sep 14, 2026
ea5e3ce
Add Cancel button to community fridge form actions
neal-bpm Sep 14, 2026
14d55d9
Add cancel button to community fridge form
neal-bpm Sep 14, 2026
d3a2509
Add CommunityFridge data model
neal-bpm Aug 25, 2026
25b84a3
Fix parameter name in changeset function for CommunityFridge module
neal-bpm Aug 26, 2026
727cb30
Community Fridge CRUD operation added
neal-bpm Aug 26, 2026
e3f3813
Addressed the review comments
neal-bpm Aug 26, 2026
58ee025
Refactor CommunityFridgeTest to use alias for Location and update tes…
neal-bpm Aug 27, 2026
36eeb07
Defer community fridge location fixture improvement to separate PR
neal-bpm Aug 28, 2026
34cd1c7
Enforce one community fridge per location via unique_constraint
neal-bpm Aug 31, 2026
8849b2b
Add community fridge fixtures and comprehensive test coverage
neal-bpm Aug 31, 2026
dd8e389
Merge branch 'vn/community-fridges-data-model-v1' into vn/community-f…
mveytsman Sep 28, 2026
281eb5e
Merge branch 'main' into vn/community-fridge-crud-v1
neal-bpm Sep 30, 2026
b9eadc7
Merge remote-tracking branch 'origin/vn/community-fridge-crud-v1' int…
neal-bpm Sep 30, 2026
3fc3a85
Enhance community fridge functionality with location support and map …
neal-bpm Oct 7, 2026
5bb96b4
Implement mode switching between list and map views in community frid…
neal-bpm Oct 7, 2026
b77e307
Fix redundant DB query and register :community_fridges page in layout…
neal-bpm Oct 7, 2026
fd8674a
Merge branch 'vn/community-fridge-ui-v1' into vn/community-fridge-pho…
neal-bpm Oct 7, 2026
fc379dc
Enhance photo upload functionality: set max file size and add delete …
neal-bpm Oct 7, 2026
05b4646
Refactor photo input field for improved readability
neal-bpm Oct 7, 2026
aab451b
Remove unnecessary blank line in Community Fridge photo upload tests
neal-bpm Oct 7, 2026
821fe3f
Merge branch 'vn/community-fridge-photo-upload-v1' into vn/community-…
neal-bpm Oct 7, 2026
3a219c0
addressed merge conflicts
neal-bpm Oct 7, 2026
e31545b
Refactor CommunityFridgeLive components: remove unused Repo alias and…
neal-bpm Oct 7, 2026
6905d40
Merge branch 'vn/community-fridge-location-mapping-v1' into vn/commun…
neal-bpm Oct 7, 2026
277cc3d
Update CommunityFridgeLive: set default location for new fridge and r…
neal-bpm Oct 8, 2026
7763175
Refactor apply_action in CommunityFridgeLive to directly list communi…
neal-bpm Oct 8, 2026
0c8390d
Merge branch 'main' into vn/community-fridge-ui-v1
mveytsman Oct 8, 2026
c4e8ee5
Remove unused alias for Locations in CommunityFridgeLiveTest
neal-bpm Oct 9, 2026
3aa6f8c
Merge branch 'vn/community-fridge-ui-v1' into vn/community-fridge-fea…
neal-bpm Oct 9, 2026
caf254e
Merge branch 'vn/community-fridge-photo-upload-v1' into vn/community-…
neal-bpm Oct 9, 2026
cc754fb
Merge branch 'vn/community-fridge-location-mapping-v1' into vn/commun…
neal-bpm Oct 9, 2026
52f8021
Merge branch 'vn/community-fridge-ui-fixes' into vn/community-fridge-…
neal-bpm Oct 9, 2026
8835201
Update community fridge tests and seed data with location coordinates
neal-bpm Oct 9, 2026
300eca3
Simplify CommunityFridge location handling to match project conventions
neal-bpm Oct 13, 2026
b9dc11c
Add location handling to community fridge creation test
neal-bpm Oct 13, 2026
7d151d1
Remove unique constraint on location_id in community fridge schema an…
neal-bpm Oct 16, 2026
0e96e2a
remove unique_index from location_id in CommunityFridges table
neal-bpm Oct 16, 2026
c5e7b73
Refactor delete_photo event to streamline changeset update for photo …
neal-bpm Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions lib/bike_brigade/locations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,29 @@ 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])
|> preload(:location)
|> Repo.all()
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)
def get_community_fridge!(id) do
CommunityFridge
|> where([cf], cf.id == ^id)
|> preload(:location)
|> Repo.one!()
end

@doc """
Creates a community_fridge.
"""
def create_community_fridge(attrs \\ %{}) do
%CommunityFridge{}
%CommunityFridge{location: nil}
|> CommunityFridge.changeset(attrs)
|> Repo.insert()
end
Expand All @@ -45,6 +53,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
Expand Down
9 changes: 4 additions & 5 deletions lib/bike_brigade/locations/community_fridge.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@ 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])
|> validate_required([:name, :location_id])
|> unique_constraint(:location_id)
|> assoc_constraint(:location)
|> cast(params, @fields)
|> cast_assoc(:location)
|> validate_required([:name])
end
end
10 changes: 10 additions & 0 deletions lib/bike_brigade_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule BikeBrigadeWeb.Layouts do
:campaigns,
:opportunities,
:riders,
:community_fridges,
:stats,
:users,
:messages,
Expand Down Expand Up @@ -69,6 +70,15 @@ defmodule BikeBrigadeWeb.Layouts do
</:icon>
Riders
</.sidebar_link>
<.sidebar_link
selected={@current_page == :community_fridges}
navigate={~p"/community_fridges"}
>
<:icon>
<Heroicons.archive_box />
</:icon>
Community Fridges
</.sidebar_link>
<.sidebar_link selected={@current_page == :stats} navigate={~p"/stats"}>
<:icon>
<Heroicons.chart_bar />
Expand Down
96 changes: 96 additions & 0 deletions lib/bike_brigade_web/live/community_fridge_live/form_component.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule BikeBrigadeWeb.CommunityFridgeLive.FormComponent do
use BikeBrigadeWeb, :live_component

alias BikeBrigade.{Locations, MediaStorage}
alias BikeBrigadeWeb.Components.LiveLocation

@impl true
def mount(socket) do
{:ok,
allow_upload(socket, :photo,
accept: ~w(.gif .png .jpg .jpeg),
max_entries: 1,
max_file_size: 10_000_000
)}
end

@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
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)
[] -> Map.update(params, "photo", nil, &if(&1 == "", do: nil, else: &1))
end

save_community_fridge(socket, socket.assigns.action, params)
end
Comment thread
neal-bpm marked this conversation as resolved.

def handle_event("delete_photo", _params, socket) do
params = socket.assigns.changeset.params || %{}

{:noreply,
socket
|> assign(
:changeset,
socket.assigns.community_fridge
|> Locations.change_community_fridge(Map.put(params, "photo", nil))
|> Map.put(:action, :validate)
)}
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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} ->
{:noreply,
socket
|> put_flash(:info, "Community fridge updated successfully")
|> push_patch(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_patch(to: socket.assigns.navigate)}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<div>
<.header>{@title}</.header>
<.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" />

<.live_component
module={LiveLocation}
id="location-form"
field={f[:location]}
label="Location"
/>

<div>
<label class="block text-sm font-semibold leading-6 text-zinc-800">Photo</label>
<input
type="hidden"
name={Phoenix.HTML.Form.input_name(f, :photo)}
value={Phoenix.HTML.Form.input_value(f, :photo) || ""}
/>
<figure :if={Phoenix.HTML.Form.input_value(f, :photo) not in [nil, ""]} class="mt-2">
<img class="w-32 rounded" src={Phoenix.HTML.Form.input_value(f, :photo)} />
<figcaption class="flex mt-1">
<.button
color={:lightred}
size={:xsmall}
phx-click={JS.push("delete_photo", target: @myself)}
type="button"
>
Delete
</.button>
</figcaption>
</figure>
<div
class="flex justify-center px-6 py-10 mt-2 border border-dashed rounded-lg border-gray-900/25"
phx-drop-target={@uploads.photo.ref}
>
<div class="text-center">
<Heroicons.photo solid class="w-12 h-12 mx-auto text-gray-300" />
<div class="flex mt-4 text-sm leading-6 text-gray-600">
<label
for={@uploads.photo.ref}
class="relative font-semibold text-indigo-600 bg-white rounded-md cursor-pointer focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
>
<span>Upload a file</span>
<.live_file_input upload={@uploads.photo} class="sr-only" />
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs leading-5 text-gray-600">PNG, JPG, GIF up to 10MB</p>
</div>
</div>
<div class="mt-2">
<figure :for={entry <- @uploads.photo.entries} class="flex items-center gap-3 mt-2">
<.live_img_preview entry={entry} class="w-16 rounded" />
<div class="text-sm text-gray-600">{entry.client_name}</div>
<button
type="button"
phx-click={JS.push("cancel_upload", value: %{ref: entry.ref}, target: @myself)}
aria-label="cancel"
class="text-gray-400 hover:text-gray-600"
>
&times;
</button>
<p :for={err <- upload_errors(@uploads.photo, entry)} class="text-sm text-red-600">
{error_to_string(err)}
</p>
</figure>
</div>
</div>

<.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 navigate={~p"/community_fridges"} color={:white}>
Cancel
</.button>
<.button type="submit" phx-disable-with="Saving...">Save</.button>
</:actions>
</.simple_form>
</div>
93 changes: 93 additions & 0 deletions lib/bike_brigade_web/live/community_fridge_live/index.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
defmodule BikeBrigadeWeb.CommunityFridgeLive.Index do
use BikeBrigadeWeb, :live_view

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
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())
|> assign(:map_layers, [])
|> assign(:map_coords, @default_coords)
|> assign(:mode, :list)}
end

@impl Phoenix.LiveView
def handle_params(params, _url, socket) 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()

socket
|> assign(:page_title, "Community Fridges")
|> assign(:community_fridge, nil)
|> 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
socket
|> assign(:page_title, "New Fridge")
|> assign(:community_fridge, %CommunityFridge{location: nil})
end

defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> 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

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
Loading
Loading