8000 feat: add group address book and modal location pickers by mwoods79 · Pull Request #108 · huddlz-hq/huddlz · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions lib/huddlz/communities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,15 @@ defmodule Huddlz.Communities do
end

resource Huddlz.Communities.HuddlTemplate

resource Huddlz.Communities.GroupLocation do
define :create_group_location,
action: :create,
args: [:name, :address, :latitude, :longitude, :group_id]

define :list_group_locations, action: :by_group, args: [:group_id]
define :update_group_location, action: :update
define :delete_group_location, action: :destroy
end
end
end
12 changes: 12 additions & 0 deletions lib/huddlz/communities/group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ defmodule Huddlz.Communities.Group do
description "Create a new group"
accept [:name, :description, :location, :is_public, :owner_id, :slug]

argument :provided_latitude, :float, allow_nil?: true, public?: false
argument :provided_longitude, :float, allow_nil?: true, public?: false

change Huddlz.Communities.Group.Changes.AddOwnerAsMember
change Huddlz.Communities.Group.Changes.GenerateSlug
change Huddlz.Geocoding.ApplyProvidedCoordinates
change Huddlz.Communities.Group.Changes.GeocodeLocation
end

Expand Down Expand Up @@ -82,6 +86,10 @@ defmodule Huddlz.Communities.Group do
accept [:name, :description, :location, :is_public, :slug]
require_atomic? false

argument :provided_latitude, :float, allow_nil?: true, public?: false
argument :provided_longitude, :float, allow_nil?: true, public?: false

change Huddlz.Geocoding.ApplyProvidedCoordinates
change Huddlz.Communities.Group.Changes.GeocodeLocation
end
end
Expand Down Expand Up @@ -185,6 +193,10 @@ defmodule Huddlz.Communities.Group do
has_many :group_images, Huddlz.Communities.GroupImage do
destination_attribute :group_id
end

has_many :group_locations, Huddlz.Communities.GroupLocation do
destination_attribute :group_id
end
end

calculations do
Expand Down
109 changes: 109 additions & 0 deletions lib/huddlz/communities/group_location.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
defmodule Huddlz.Communities.GroupLocation do
@moduledoc """
A saved location in a group's address book.
Stores geocoded coordinates so locations can be reused without re-geocoding.
"""

use Ash.Resource,
otp_app: :huddlz,
domain: Huddlz.Communities,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]

postgres do
table "group_locations"
repo Huddlz.Repo

references do
reference :group, on_delete: :delete
end
end

actions do
defaults [:read, :destroy]

create :create do
primary? true
accept [:name, :address, :latitude, :longitude, :group_id]
end

update :update do
primary? true
accept [:name]
require_atomic? false
end

read :by_group do
argument :group_id, :uuid, allow_nil?: false
filter expr(group_id == ^arg(:group_id))
prepare build(sort: [name: :asc])
end
end

policies do
bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end

policy action(:create) do
authorize_if Huddlz.Communities.Huddl.Checks.GroupOwnerOrOrganizer
end

policy action_type(:read) do
authorize_if always()
end

policy action_type([:update, :destroy]) do
authorize_if expr(group.owner_id == ^actor(:id))

authorize_if expr(
exists(
group.group_members,
user_id == ^actor(:id) and role == :organizer
)
)
end
end

attributes do
uuid_primary_key :id

attribute :name, :string do
allow_nil? true
description "Optional friendly name (e.g., 'Community Center')"
constraints max_length: 200
end

attribute :address, :string do
allow_nil? false
description "Full address string from Google Places"
constraints min_length: 1, max_length: 500
end

attribute :latitude, :float do
allow_nil? false
constraints min: -90, max: 90
end

attribute :longitude, :float do
allow_nil? false
constraints min: -180, max: 180
end

create_timestamp :inserted_at
update_timestamp :updated_at
end

relationships do
belongs_to :group, Huddlz.Communities.Group do
attribute_type :uuid
allow_nil? false
end
end

identities do
identity :unique_name_per_group, [:group_id, :name],
nils_distinct?: false,
pre_check?: true
end
end
8 changes: 8 additions & 0 deletions lib/huddlz/communities/huddl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,14 @@ defmodule Huddlz.Communities.Huddl do
argument :repeat_until, :date, allow_nil?: true
argument :frequency, :string, allow_nil?: true

argument :provided_latitude, :float, allow_nil?: true, public?: false
argument :provided_longitude, :float, allow_nil?: true, public?: false

validate Huddlz.Communities.Huddl.Validations.FutureDateValidation
change Huddlz.Communities.Huddl.Changes.CalculateDateTimeFromInputs
change Huddlz.Communities.Huddl.Changes.ForcePrivateForPrivateGroups
change Huddlz.Communities.Huddl.Changes.AddHuddlTemplate
change Huddlz.Geocoding.ApplyProvidedCoordinates
change Huddlz.Communities.Huddl.Changes.GeocodeLocation
change Huddlz.Communities.Huddl.Changes.DefaultLocationFromGroup
end
Expand Down Expand Up @@ -130,10 +134,14 @@ defmodule Huddlz.Communities.Huddl do
default "instance"
end

argument :provided_latitude, :float, allow_nil?: true, public?: false
argument :provided_longitude, :float, allow_nil?: true, public?: false

require_atomic? false

change Huddlz.Communities.Huddl.Changes.CalculateDateTimeFromInputs
change Huddlz.Communities.Huddl.Changes.EditRecurringHuddlz
change Huddlz.Geocoding.ApplyProvidedCoordinates
change Huddlz.Communities.Huddl.Changes.GeocodeLocation
change Huddlz.Communities.Huddl.Changes.DefaultLocationFromGroup
end
Expand Down
20 changes: 20 additions & 0 deletions lib/huddlz/geocoding/apply_provided_coordinates.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Huddlz.Geocoding.ApplyProvidedCoordinates do
@moduledoc """
Applies pre-existing coordinates from the provided_latitude/provided_longitude arguments.
Runs before GeocodeLocation so that geocoding is skipped when coordinates are already set.
"""
use Ash.Resource.Change

def change(changeset, _opts, _context) do
lat = Ash.Changeset.get_argument(changeset, :provided_latitude)
lng = Ash.Changeset.get_argument(changeset, :provided_longitude)

if is_number(lat) and is_number(lng) do
changeset
|> Ash.Changeset.force_change_attribute(:latitude, lat)
|> Ash.Changeset.force_change_attribute(:longitude, lng)
else
changeset
end
end
end
19 changes: 15 additions & 4 deletions lib/huddlz/geocoding/change.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ defmodule Huddlz.Geocoding.Change do
require Logger

def geocode_if_changed(changeset, location_attribute) do
if Ash.Changeset.changing_attribute?(changeset, location_attribute) do
if provided_coordinates?(changeset) do
changeset
|> Ash.Changeset.get_attribute(location_attribute)
|> geocode_and_apply(changeset)
else
changeset
if Ash.Changeset.changing_attribute?(changeset, location_attribute) do
changeset
|> Ash.Changeset.get_attribute(location_attribute)
|> geocode_and_apply(changeset)
else
changeset
end
end
end

defp provided_coordinates?(changeset) do
lat = Ash.Changeset.get_argument(changeset, :provided_latitude)
lng = Ash.Changeset.get_argument(changeset, :provided_longitude)

is_number(lat) and is_number(lng)
end

defp geocode_and_apply(nil, changeset), do: set_coordinates(changeset, nil, nil)

defp geocode_and_apply(location, changeset) do
Expand Down
90 changes: 89 additions & 1 deletion lib/huddlz_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ defmodule HuddlzWeb.CoreComponents do
<.button phx-click="go" variant="primary">Send!</.button>
<.button navigate={~p"/"}>Home</.button>
"""
attr :rest, :global, include: ~w(href navigate patch method)
attr :rest, :global, include: ~w(href navigate patch method disabled)
attr :variant, :string, values: ~w(primary)
slot :inner_block, required: true

Expand Down Expand Up @@ -587,6 +587,94 @@ defmodule HuddlzWeb.CoreComponents do
)
end

@doc """
Renders a modal dialog as an overlay.

## Examples

<.modal id="add-location" show on_cancel={JS.navigate(~p"/back")}>
<h2>Add Location</h2>
...
</.modal>
"""
attr :id, :string, required: true
attr :show, :boolean, default: false
attr :on_cancel, JS, default: %JS{}
slot :inner_block, required: true

def modal(assigns) do
~H"""
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden"
>
<div
id={"#{@id}-bg"}
class="bg-base-100/80 fixed inset-0 backdrop-blur-sm transition-opacity"
aria-hidden="true"
/>
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center p-4">
<div class="w-full max-w-xl">
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
class="relative border border-base-300 bg-base-200 shadow-[0_0_30px_oklch(75%_0.18_195/0.15)] p-6"
>
<button
phx-click={JS.exec("data-cancel", to: "##{@id}")}
type="button"
class="absolute top-4 right-4 text-base-content/40 hover:text-base-content transition-colors"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark" class="h-5 w-5" />
</button>
{render_slot(@inner_block)}
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end

def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
time: 300,
transition: {"transition-all ease-out duration-300", "opacity-0", "opacity-100"}
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-container")
end

def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
time: 200,
transition: {"transition-all ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
end

@doc """
Translates an error message using gettext.
"""
Expand Down
Loading
Loading
0