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
Diff view
5 changes: 5 additions & 0 deletions .dev.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ MAILER_ADAPTER=Swoosh.Adapters.Local
# AWS_ACCESS_KEY_ID=your-access-key
# AWS_SECRET_ACCESS_KEY=your-secret-key

# -----------------------------------------------------------------------------
# Google Maps (geocoding for location search)
# -----------------------------------------------------------------------------
GOOGLE_MAPS_API_KEY=your-google-maps-api-key

# -----------------------------------------------------------------------------
# Optional Services
# -----------------------------------------------------------------------------
Expand Down
5 changes: 4 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
[
import_deps: [
:ash_graphql,
:absinthe,
:ash_json_api,
:ash_oban,
:oban,
:ash_authentication,
Expand All @@ -13,6 +16,6 @@
:phoenix
],
subdirectories: ["priv/*/migrations"],
plugins: [Spark.Formatter, Phoenix.LiveView.HTMLFormatter],
plugins: [Absinthe.Formatter, Spark.Formatter, Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
]
2 changes: 1 addition & 1 deletion .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
# Additional services can be defined here if required.
services:
db:
image: postgres:17
image: postgis/postgis:17-3.5
ports: ['5432:5432']
env:
POSTGRES_PASSWORD: postgres
Expand Down
3 changes: 3 additions & 0 deletions .test.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ TOKEN_SIGNING_SECRET=B/l8TS6gx/jZednXDgVLAka5u5vIqk22

# Mailer
MAILER_ADAPTER=Swoosh.Adapters.Test

# Google Maps (not used in test — mock adapter is configured)
GOOGLE_MAPS_API_KEY=test-dummy-key
108 changes: 106 additions & 2 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,115 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

const Hooks = {}

Hooks.LocationAutocomplete = {
mounted() {
this.highlightIndex = -1
this.setupInput()
},

updated() {
this.syncHighlightFromDOM()
if (!this._input || !this.el.contains(this._input)) {
this.setupInput()
}
},

destroyed() {
if (this._input && this._handler) {
this._input.removeEventListener("keydown", this._handler)
}
},

setupInput() {
this._input = this.el.querySelector("input[role='combobox']")
if (!this._input) return

if (this._handler) {
this._input.removeEventListener("keydown", this._handler)
}

this._handler = (e) => this.handleKeydown(e)
this._input.addEventListener("keydown", this._handler)
},

handleKeydown(e) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault()
e.stopPropagation()

const listbox = this.el.querySelector("[role='listbox']")
if (!listbox) return

const options = listbox.querySelectorAll("[role='option']")
if (options.length === 0) return

this.moveHighlight(e.key === "ArrowDown" ? 1 : -1, options)
this.pushEventTo(this.el, "keydown", {key: e.key})
} else if (e.key === "Enter") {
const listbox = this.el.querySelector("[role='listbox']")
if (listbox && this.highlightIndex >= 0) {
e.preventDefault()
e.stopPropagation()

const options = listbox.querySelectorAll("[role='option']")
const highlighted = options[this.highlightIndex]
if (highlighted) {
this.pushEventTo(this.el, "select", {
"place-id": highlighted.dataset.placeId,
"display-text": highlighted.dataset.displayText
})
}
}
} else if (e.key === "Escape") {
e.stopPropagation()
this.pushEventTo(this.el, "keydown", {key: "Escape"})
}
},

moveHighlight(direction, options) {
const maxIdx = options.length - 1
const newIdx = direction > 0
? Math.min(this.highlightIndex + 1, maxIdx)
: Math.max(this.highlightIndex - 1, -1)

if (this.highlightIndex >= 0 && this.highlightIndex < options.length) {
options[this.highlightIndex].classList.remove("bg-primary/20", "border-l-primary")
options[this.highlightIndex].classList.add("border-l-transparent")
}

if (newIdx >= 0 && newIdx < options.length) {
options[newIdx].classList.add("bg-primary/20", "border-l-primary")
options[newIdx].classList.remove("border-l-transparent")
}

this.highlightIndex = newIdx
},

syncHighlightFromDOM() {
const listbox = this.el.querySelector("[role='listbox']")
if (!listbox) {
this.highlightIndex = -1
return
}

const options = listbox.querySelectorAll("[role='option']")
this.highlightIndex = -1
options.forEach((opt, idx) => {
if (opt.classList.contains("border-l-primary") &&
!opt.matches(":hover")) {
this.highlightIndex = idx
}
})
}
}

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
params: {_csrf_token: csrfToken},
hooks: Hooks
})

// Show progress bar on live navigation and form submits
Expand Down Expand Up @@ -78,4 +183,3 @@ if (process.env.NODE_ENV === "development") {
window.liveReloader = reloader
})
}

30 changes: 29 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
# General application configuration
import Config

config :ash_graphql, authorize_update_destroy_with_error?: true

config :mime,
extensions: %{"json" => "application/vnd.api+json"},
types: %{"application/vnd.api+json" => ["json"]}

config :ash_json_api,
show_public_calculations_when_loaded?: false,
authorize_update_destroy_with_error?: true

config :ash_oban, pro?: false

config :huddlz, Oban,
Expand Down Expand Up @@ -37,6 +47,8 @@ config :spark,
remove_parens?: true,
"Ash.Resource": [
section_order: [
:graphql,
:json_api,
:authentication,
:tokens,
:postgres,
Expand All @@ -56,14 +68,30 @@ config :spark,
:identities
]
],
"Ash.Domain": [section_order: [:resources, :policies, :authorization, :domain, :execution]]
"Ash.Domain": [
section_order: [
:graphql,
:json_api,
:resources,
:policies,
:authorization,
:domain,
:execution
]
]
]

config :huddlz,
ecto_repos: [Huddlz.Repo],
generators: [timestamp_type: :utc_datetime],
ash_domains: [Huddlz.Accounts, Huddlz.Communities]

# Geocoding adapter (compile-time)
config :huddlz, :geocoding, adapter: Huddlz.Geocoding.Google

# Places autocomplete adapter (compile-time)
config :huddlz, :places, adapter: Huddlz.Places.Google

# Configures the endpoint
config :huddlz, HuddlzWeb.Endpoint,
url: [host: "localhost"],
Expand Down
6 changes: 6 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ config :huddlz, Huddlz.Mailer, mailer_opts
# Storage Configuration (adapter set in compile-time configs)
# =============================================================================

# =============================================================================
# Google Maps Configuration (geocoding)
# =============================================================================

config :huddlz, :google_maps, api_key: required!("GOOGLE_MAPS_API_KEY")

if config_env() == :prod do
required!("AWS_ACCESS_KEY_ID")
required!("AWS_SECRET_ACCESS_KEY")
Expand Down
3 changes: 3 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ config :swoosh, :api_client, false

# Adapters (compile-time)
config :huddlz, :storage, adapter: Huddlz.Storage.Local
config :huddlz, :geocoding, adapter: Huddlz.MockGeocoding
config :huddlz, :places, adapter: Huddlz.MockPlaces
config :huddlz, geocoding_req_plug: {Req.Test, Huddlz.Geocoding.Google}
config :huddlz, Huddlz.Repo, pool: Ecto.Adapters.SQL.Sandbox
4 changes: 4 additions & 0 deletions lib/huddlz/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ defmodule Huddlz.Accounts do
define :update_role, action: :update_role, args: [:role]
define :get_by_email, action: :get_by_email, args: [:email]
define :update_display_name, action: :update_display_name, args: [:display_name]

define :update_home_location,
action: :update_home_location,
args: [:home_location, :home_latitude, :home_longitude]
end

resource Huddlz.Accounts.ProfilePicture do
Expand Down
27 changes: 27 additions & 0 deletions lib/huddlz/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ defmodule Huddlz.Accounts.User do
validate string_length(:display_name, min: 1, max: 70)
end

update :update_home_location do
description "Update a user's home location for search pre-fill"
accept [:home_location, :home_latitude, :home_longitude]
end

update :update_role do
description "Update a user's role"
accept [:role]
Expand Down Expand Up @@ -336,6 +341,11 @@ defmodule Huddlz.Accounts.User do
authorize_if expr(id == ^actor(:id))
end

policy action(:update_home_location) do
description "Users can update their own home location"
authorize_if expr(id == ^actor(:id))
end

policy action(:change_password) do
description "Users can change their own password"
authorize_if expr(id == ^actor(:id))
Expand Down Expand Up @@ -377,6 +387,23 @@ defmodule Huddlz.Accounts.User do
# Allow setting for testing/seeding purposes
public? true
end

attribute :home_location, :string do
description "User's home city/region for pre-filling location search"
allow_nil? true
public? true
constraints max_length: 200
end

attribute :home_latitude, :float do
allow_nil? true
constraints min: -90, max: 90
end

attribute :home_longitude, :float do
allow_nil? true
constraints min: -180, max: 180
end
end

relationships do
Expand Down
4 changes: 3 additions & 1 deletion lib/huddlz/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ defmodule Huddlz.Application do
# {Huddlz.Worker, arg},
# Start to serve requests, typically the last entry
HuddlzWeb.Endpoint,
{AshAuthentication.Supervisor, [otp_app: :huddlz]}
{AshAuthentication.Supervisor, [otp_app: :huddlz]},
{Absinthe.Subscription, HuddlzWeb.Endpoint},
AshGraphql.Subscription.Batcher
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
12 changes: 10 additions & 2 deletions lib/huddlz/communities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ defmodule Huddlz.Communities do
"""

use Ash.Domain,
otp_app: :huddlz
otp_app: :huddlz,
extensions: [AshJsonApi.Domain, AshGraphql.Domain]

resources do
resource Huddlz.Communities.Huddl do
Expand All @@ -13,7 +14,14 @@ defmodule Huddlz.Communities do

define :search_huddlz,
action: :search,
args: [:query, {:optional, :date_filter}, {:optional, :event_type}],
args: [
:query,
{:optional, :date_filter},
{:optional, :event_type},
{:optional, :search_latitude},
{:optional, :search_longitude},
{:optional, :distance_miles}
],
get?: false

define :get_by_status, action: :by_status, args: [:status]
Expand Down
23 changes: 23 additions & 0 deletions lib/huddlz/communities/group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ defmodule Huddlz.Communities.Group do
postgres do
table "groups"
repo Huddlz.Repo

custom_indexes do
index "ST_MakePoint(longitude, latitude)",
name: "groups_location_gist_index",
using: "GIST",
where: "latitude IS NOT NULL AND longitude IS NOT NULL"
end
end

actions do
Expand All @@ -23,6 +30,7 @@ defmodule Huddlz.Communities.Group do

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

read :search do
Expand Down Expand Up @@ -72,6 +80,9 @@ defmodule Huddlz.Communities.Group do
update :update_details do
description "Update group details"
accept [:name, :description, :location, :is_public, :slug]
require_atomic? false

change Huddlz.Communities.Group.Changes.GeocodeLocation
end
end

Expand Down Expand Up @@ -132,6 +143,18 @@ defmodule Huddlz.Communities.Group do
public? true
end

attribute :latitude, :float do
allow_nil? true
description "Geocoded latitude of group location"
constraints min: -90, max: 90
end

attribute :longitude, :float do
allow_nil? true
description "Geocoded longitude of group location"
constraints min: -180, max: 180
end

create_timestamp :created_at
update_timestamp :updated_at
end
Expand Down
Loading