diff --git a/lib/maker_passport/maker.ex b/lib/maker_passport/maker.ex index 0dc4542..367f559 100644 --- a/lib/maker_passport/maker.ex +++ b/lib/maker_passport/maker.ex @@ -18,30 +18,56 @@ defmodule MakerPassport.Maker do [%Profile{}, ...] """ - def list_profiles(skills \\ []) do - Repo.all(Profile) |> Repo.preload([:skills, :user]) - + def list_profiles(filter_params \\ %{}) do query = - Profile - |> join(:left, [p], s in assoc(p, :skills)) - |> maybe_filter_by_skills(skills) - |> preload([:skills, :user]) - |> distinct([p], p.id) + Profile + |> join(:left, [p], s in assoc(p, :skills)) + |> join(:left, [p], l in assoc(p, :location)) + |> join(:left, [p], u in assoc(p, :user)) + |> where([p, ..., u], not is_nil(u.confirmed_at)) + |> maybe_filter_by_country(filter_params) + |> maybe_filter_by_city(filter_params) + |> maybe_filter_by_skills(filter_params) + |> preload([:skills, :user]) + |> distinct([p], p.id) Repo.all(query) end - defp maybe_filter_by_skills(query, []), do: query - - defp maybe_filter_by_skills(query, skills) do + defp maybe_filter_by_skills(query, %{search_skills: skills}) when skills != [], do: query |> where([p, s], s.name in ^skills) |> group_by([p], p.id) |> having([p, s], count(s.id) == ^length(skills)) + + defp maybe_filter_by_skills(query, _), do: query + + def maybe_filter_by_city(query, %{city_search: city}) when city != "" do + query + |> where([p, s, l], ilike(l.city, ^"%#{city}%")) + end + + def maybe_filter_by_city(query, _), do: query + + def maybe_filter_by_country(query, %{country_search: country}) when country != "" do + query + |> where([p, s, l], ilike(l.country, ^"%#{country}%")) end - def list_profiles_by_criteria(criteria) when is_list(criteria) do - query = from(p in Profile, where: not is_nil(p.name)) + def maybe_filter_by_country(query, _), do: query + + + def list_profiles_by_criteria(criteria, filter_params \\ %{}) when is_list(criteria) do + query = Profile + |> where([p], not is_nil(p.name)) + |> join(:left, [p], s in assoc(p, :skills)) + |> join(:left, [p], l in assoc(p, :location)) + |> join(:left, [p], u in assoc(p, :user)) + |> where([p, ..., u], not is_nil(u.confirmed_at)) + |> maybe_filter_by_country(filter_params) + |> maybe_filter_by_city(filter_params) + |> maybe_filter_by_skills(filter_params) + |> distinct([p], p.id) Enum.reduce(criteria, query, fn {:sort, %{sort_by: sort_by, sort_order: sort_order}}, query -> @@ -52,6 +78,9 @@ defmodule MakerPassport.Maker do {:preload, preload}, query -> from q in query, preload: ^preload + + {:current_id, current_id}, query -> + from q in query, where: q.id != ^current_id end) |> Repo.all() |> Repo.preload([:user, :skills]) @@ -346,7 +375,15 @@ defmodule MakerPassport.Maker do Repo.all(from l in Location, where: ilike(l.city, ^"%#{search_text}%")) end - def to_city_list(cities, nil) do + def search_cities("", _), do: [] + + def search_cities(search_text, country) do + Repo.all(from l in Location, where: ilike(l.city, ^"%#{search_text}%") and l.country == ^country) + end + + def to_city_list(cities, selected_city) when is_binary(selected_city) do + cities = Enum.filter(cities, &(&1.city != selected_city)) + cities |> Enum.map(&to_city_tuple/1) |> Enum.sort_by(&elem(&1, 0)) @@ -364,6 +401,31 @@ defmodule MakerPassport.Maker do {location.city, location.id} end + def search_countries(""), do: [] + + def search_countries(search_text) do + Repo.all(from c in Location, where: ilike(c.country, ^"%#{search_text}%"), distinct: c.country) + end + + def to_country_list(countries, selected_country) do + countries = Enum.filter(countries, &(&1.country != selected_country)) + + countries + |> Enum.map(&to_country_tuple/1) + |> Enum.sort_by(&elem(&1, 0)) + end + + defp to_country_tuple(location) do + {get_country_name(location.country), location.country} + end + + def get_country_name(country_code) do + case Countries.get(country_code) do + nil -> "Unknown" + country -> country.name + end + end + @doc """ Gets a single website. diff --git a/lib/maker_passport_web/live/home_live/index.ex b/lib/maker_passport_web/live/home_live/index.ex index abe58d9..bffb4cd 100644 --- a/lib/maker_passport_web/live/home_live/index.ex +++ b/lib/maker_passport_web/live/home_live/index.ex @@ -9,12 +9,7 @@ defmodule MakerPassportWeb.HomeLive.Index do @impl true def mount(_params, _session, socket) do - latest_profiles = - Maker.list_profiles_by_criteria( - limit: 4, - sort: %{sort_by: :updated_at, sort_order: :desc}, - preload: [:skills] - ) + latest_profiles = fetch_latest_profiles(socket.assigns.current_user) socket = socket @@ -22,4 +17,21 @@ defmodule MakerPassportWeb.HomeLive.Index do {:ok, socket} end + + defp fetch_latest_profiles(nil) do + Maker.list_profiles_by_criteria( + limit: 4, + sort: %{sort_by: :updated_at, sort_order: :desc}, + preload: [:skills] + ) + end + + defp fetch_latest_profiles(user) do + Maker.list_profiles_by_criteria( + limit: 4, + sort: %{sort_by: :updated_at, sort_order: :desc}, + preload: [:skills], + current_id: user.id + ) + end end diff --git a/lib/maker_passport_web/live/profile_live/index.ex b/lib/maker_passport_web/live/profile_live/index.ex index 1620455..2475370 100644 --- a/lib/maker_passport_web/live/profile_live/index.ex +++ b/lib/maker_passport_web/live/profile_live/index.ex @@ -22,9 +22,10 @@ defmodule MakerPassportWeb.ProfileLive.Index do socket = socket |> assign(:page_title, "Maker Profiles") - |> assign(:search_skills, []) + |> assign(:filter_params, %{search_skills: [], country_search: "", city_search: ""}) |> assign(:no_skills_results, false) |> assign(:profile, nil) + |> assign(:form, to_form(%{}, as: "filter_params")) |> stream(:profiles, profiles) {:ok, socket} @@ -41,20 +42,33 @@ defmodule MakerPassportWeb.ProfileLive.Index do end @impl true - def handle_info({:typeahead, {name, _}, _}, socket) do + def handle_info( + {:typeahead, {country_name, country_code}, "country-search-picker" = id}, + socket + ) do socket = socket - |> update(:search_skills, fn skills -> [name | skills] end) + |> push_event(%{id: id, label: country_name}) + |> update(:filter_params, fn params -> + Map.put(params, :country_search, country_code) + |> Map.put(:city_search, "") + end) - profiles = - case socket.assigns.current_user do - nil -> - Maker.list_profiles(socket.assigns.search_skills) + profiles = filter_profiles(socket.assigns.current_user, socket.assigns.filter_params) - user -> - Maker.list_profiles(socket.assigns.search_skills) - |> Enum.reject(fn profile -> profile.user && profile.user.id == user.id end) - end + {:noreply, stream(socket, :profiles, profiles)} + end + + @impl true + def handle_info({:typeahead, {value, _}, id}, socket) do + socket = + socket + |> push_event(%{id: id, label: value}) + |> update(:filter_params, fn params -> + add_filter_params(id, params, value) + end) + + profiles = filter_profiles(socket.assigns.current_user, socket.assigns.filter_params) socket = socket @@ -65,36 +79,98 @@ defmodule MakerPassportWeb.ProfileLive.Index do end @impl true - def handle_event("delete", %{"id" => id}, socket) do - profile = Maker.get_profile!(id) - {:ok, _} = Maker.delete_profile(profile) + def handle_event( + "filter-profiles", + %{"filter_params" => %{"country_search" => ""}}, + %{assigns: %{filter_params: %{country_search: country_search}}} = socket + ) + when country_search != "" do + handle_remove_search(socket, [:city_search, :country_search]) + end - {:noreply, stream_delete(socket, :profiles, profile)} + @impl true + def handle_event( + "filter-profiles", + %{"filter_params" => %{"city_search" => ""}}, + %{assigns: %{filter_params: %{city_search: city_search}}} = socket + ) when city_search != "" do + handle_remove_search(socket, [:city_search]) end + @impl true def handle_event("remove-skill", %{"skill_name" => skill_name}, socket) do - updated_skills = - Enum.filter(socket.assigns.search_skills, fn skill -> skill != skill_name end) - - profiles = - case socket.assigns.current_user do - nil -> - Maker.list_profiles(updated_skills) - - user -> - Maker.list_profiles(updated_skills) - |> Enum.reject(fn profile -> profile.user && profile.user.id == user.id end) - end + filter_params = socket.assigns.filter_params + updated_skills = Enum.filter(filter_params.search_skills, fn skill -> skill != skill_name end) + filter_params = %{filter_params | search_skills: updated_skills} + profiles = filter_profiles(socket.assigns.current_user, filter_params) socket = socket |> assign(:no_skills_results, profiles == []) - |> assign(:search_skills, updated_skills) + |> assign(:filter_params, filter_params) |> stream(:profiles, profiles) {:noreply, socket} end + @impl true + def handle_event("remove-country", _params, socket) do + handle_remove_search(socket, [:city_search, :country_search]) + end + + @impl true + def handle_event("remove-city", _params, socket) do + handle_remove_search(socket, [:city_search]) + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + profile = Maker.get_profile!(id) + {:ok, _} = Maker.delete_profile(profile) + + {:noreply, stream_delete(socket, :profiles, profile)} + end + + @impl true + def handle_event(_event, _params, socket) do + {:noreply, socket} + end + + defp add_filter_params("city-search-picker", filter_params, value) do + Map.put(filter_params, :city_search, value) + end + + defp add_filter_params("skills-search-picker", filter_params, value) do + Map.update(filter_params, :search_skills, [value], fn skills -> [value | skills] end) + end + + def filter_profiles(current_user, filter_params) do + case current_user do + nil -> + Maker.list_profiles_by_criteria([{:sort, %{sort_by: :updated_at, sort_order: :desc}}], filter_params) + + user -> + Maker.list_profiles(filter_params) + |> Enum.reject(fn profile -> profile.user && profile.user.id == user.id end) + end + end + + defp push_event(socket, %{id: id} = params) when id != "skills-search-picker" do + push_event(socket, "set-input-value", params) + end + + defp push_event(socket, _), do: socket + + defp handle_remove_search(socket, fields) do + filter_params = + Enum.reduce(fields, socket.assigns.filter_params, fn field, acc -> + Map.put(acc, field, "") + end) + + profiles = filter_profiles(socket.assigns.current_user, filter_params) + {:noreply, stream(socket, :profiles, profiles) |> assign(:filter_params, filter_params)} + end + defp remove_selected_skill(skills, selected_skills) do Enum.filter(skills, fn {skill, _} -> skill not in selected_skills end) end diff --git a/lib/maker_passport_web/live/profile_live/index.html.heex b/lib/maker_passport_web/live/profile_live/index.html.heex index 00d6ba9..71ae1e6 100644 --- a/lib/maker_passport_web/live/profile_live/index.html.heex +++ b/lib/maker_passport_web/live/profile_live/index.html.heex @@ -2,30 +2,93 @@
Makers
+ <.simple_form for={@form} phx-change="filter-profiles"> +
+
+ <.typeahead + id="country-search-picker" + class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-gray-500 pr-10" + placeholder="Select country by search..." + name="country_search" + value={ + @filter_params.country_search != "" && + Maker.get_country_name(@filter_params.country_search) + } + on_search={ + fn search_text -> + Maker.search_countries(search_text) + |> Maker.to_country_list(@filter_params.country_search) + end + } + on_select={ + fn country -> send(self(), {:typeahead, country, "country-search-picker"}) end + } + /> + +
-
-
Filter by skill
- <.typeahead - id="skills-search-picker" - class="w-full rounded-md" - placeholder="Skill..." - on_search={ - fn search_text -> - Maker.search_skills(search_text) - |> Maker.to_skill_list() - |> remove_selected_skill(@search_skills) - end - } - on_select={fn skill -> send(self(), {:typeahead, skill, "skills-search-picker"}) end} - /> -
+
+ <.typeahead + id="city-search-picker" + class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-gray-500 pr-10" + placeholder="Select city by search..." + value={@filter_params.city_search} + name="city_search" + on_search={ + fn search_text -> + Maker.search_cities(search_text, @filter_params.country_search) + |> Maker.to_city_list(@filter_params.city_search) + end + } + on_select={fn city -> send(self(), {:typeahead, city, "city-search-picker"}) end} + disabled={@filter_params.country_search == ""} + /> + +
+ +
+ <.typeahead + id="skills-search-picker" + class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-gray-500" + placeholder="Select skill by search..." + name="search_skills" + on_search={ + fn search_text -> + Maker.search_skills(search_text) + |> Maker.to_skill_list() + |> remove_selected_skill(@filter_params.search_skills) + end + } + on_select={fn skill -> send(self(), {:typeahead, skill, "skills-search-picker"}) end} + /> +
+
+ -
+
Showing makers with these skills:
<%= skill %> diff --git a/lib/maker_passport_web/live/profile_live/profile_form_component.ex b/lib/maker_passport_web/live/profile_live/profile_form_component.ex index 17bcad7..4881e1c 100644 --- a/lib/maker_passport_web/live/profile_live/profile_form_component.ex +++ b/lib/maker_passport_web/live/profile_live/profile_form_component.ex @@ -104,15 +104,6 @@ defmodule MakerPassportWeb.ProfileLive.ProfileFormComponent do |> Enum.sort_by(fn {name, _code} -> name end) end - def get_country_name(%{country: country_code}) do - case Countries.get(country_code) do - nil -> "Unknown" - country -> {country.name, country_code} - end - end - - def get_country_name(_), do: "" - defp presign_upload(entry, socket) do config = %{ region: @do_region, diff --git a/lib/maker_passport_web/live/profile_live/profile_form_component.html.heex b/lib/maker_passport_web/live/profile_live/profile_form_component.html.heex index e85a055..5c11c40 100644 --- a/lib/maker_passport_web/live/profile_live/profile_form_component.html.heex +++ b/lib/maker_passport_web/live/profile_live/profile_form_component.html.heex @@ -20,7 +20,7 @@ phx-target={@myself} allow_clear={true} id="country_search" - value={@form[:country].value || get_country_name(@profile.location)} + value={@form[:country].value || (@profile.location && Maker.get_country_name(@profile.location.country))} />
diff --git a/lib/maker_passport_web/live/profile_live/show.ex b/lib/maker_passport_web/live/profile_live/show.ex index 75c82d2..f733915 100644 --- a/lib/maker_passport_web/live/profile_live/show.ex +++ b/lib/maker_passport_web/live/profile_live/show.ex @@ -106,13 +106,6 @@ defmodule MakerPassportWeb.ProfileLive.Show do {:noreply, push_navigate(socket, to: ~p"/profiles/#{socket.assigns.profile.id}/edit-profile")} end - def get_country_name(country_code) do - case Countries.get(country_code) do - nil -> "Unknown" - country -> country.name - end - end - defp save_skill(socket, skill_name, profile) do skill = skill_name |> String.trim() |> check_or_create_skill() add_or_update_skill(socket, skill, profile) diff --git a/lib/maker_passport_web/live/profile_live/show.html.heex b/lib/maker_passport_web/live/profile_live/show.html.heex index 3057cb9..9f079a2 100644 --- a/lib/maker_passport_web/live/profile_live/show.html.heex +++ b/lib/maker_passport_web/live/profile_live/show.html.heex @@ -31,7 +31,7 @@

<.icon :if={@profile.location} name="hero-map-pin" class="w-5 h-5" /> <%= @profile.location && - get_country_name(@profile.location.country) <> " . " <> @profile.location.city %> + Maker.get_country_name(@profile.location.country) <> " . " <> @profile.location.city %>

<%= @profile.bio %>

diff --git a/lib/maker_passport_web/live/profile_live/typeahead_component.ex b/lib/maker_passport_web/live/profile_live/typeahead_component.ex index 879fe9f..8c6be70 100644 --- a/lib/maker_passport_web/live/profile_live/typeahead_component.ex +++ b/lib/maker_passport_web/live/profile_live/typeahead_component.ex @@ -45,6 +45,9 @@ defmodule MakerPassportWeb.ProfileLive.TypeaheadComponent do data-focused-option={@focused_option} class="max-h-60 py-2 overflow-y-auto text-gray-700 dark:text-gray-200" role="listbox"> +
  • + No options found +
  • Enum.with_index(0)} id={"#{@id}-#{idx}"} @@ -70,9 +73,7 @@ defmodule MakerPassportWeb.ProfileLive.TypeaheadComponent do ~H"""
    <.label :if={@label} for={@id}><%= @label %> -
    +
    1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, - "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "live_select": {:hex, :live_select, "1.4.3", "ec9706952f589d8e2e6f98a0e1633c5b51ab5b807d503bd0d9622a26c999fb9a", [:mix], [{:ecto, "~> 3.8", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.6.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "58f7d702b0f786c73d31e60a342c0a49afaf56ca5a6a078b51babf3490465220"},