diff --git a/assets/js/app.js b/assets/js/app.js index b1a365f..c79241c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -23,10 +23,12 @@ import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" import Uploaders from "./uploaders" import {Combobox, ComboboxOption} from "./combobox" +import LiveSelect from "./live_select" let Hooks = {} Hooks.Combobox = Combobox Hooks.ComboboxOption = ComboboxOption +Hooks.LiveSelect = LiveSelect let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { diff --git a/assets/js/live_select.js b/assets/js/live_select.js new file mode 100644 index 0000000..058476d --- /dev/null +++ b/assets/js/live_select.js @@ -0,0 +1,151 @@ +function debounce(func, msec) { + let timer; + return (...args) => { + clearTimeout(timer) + timer = setTimeout(() => { + func.apply(this, args) + }, msec) + } +} + +const LiveSelect = { + textInput() { + return this.el.querySelector("input[type=text]") + }, + + debounceMsec() { + return parseInt(this.el.dataset["debounce"]) + }, + + updateMinLen() { + return parseInt(this.el.dataset["updateMinLen"]) + }, + + maybeStyleClearButton() { + const clear_button = this.el.querySelector('button[phx-click=clear]') + if (clear_button) { + this.textInput().parentElement.style.position = 'relative' + clear_button.style.position = 'absolute' + clear_button.style.top = '0px' + clear_button.style.bottom = '0px' + clear_button.style.right = '5px' + clear_button.style.display = 'block' + } + }, + + pushEventToParent(event, payload) { + const target = this.el.dataset['phxTarget']; + if (target) { + this.pushEventTo(target, event, payload) + } else { + this.pushEvent(event, payload) + } + }, + + attachDomEventHandlers() { + const textInput = this.textInput() + textInput.onkeydown = (event) => { + if (event.code === "Enter") { + event.preventDefault() + } + this.pushEventTo(this.el, 'keydown', {key: event.code}) + } + + const changeEvents = debounce((id, field, text) => { + this.pushEventTo(this.el, "change", {text}) + this.pushEventToParent("live_select_change", {id: this.el.id, field, text}) + }, this.debounceMsec()) + + textInput.oninput = (event) => { + const text = event.target.value.trim() + const field = this.el.dataset['field'] + if (text.length >= this.updateMinLen()) { + changeEvents(this.el.id, field, text) + } else { + this.pushEventTo(this.el, "options_clear", {}) + } + } + + const dropdown = this.el.querySelector("ul") + if (dropdown) { + dropdown.onmousedown = (event) => { + const option = event.target.closest('div[data-idx]') + if (option) { + this.pushEventTo(this.el, 'option_click', {idx: option.dataset.idx}) + event.preventDefault() + } + } + } + + this.el.querySelectorAll("button[data-idx]").forEach(button => { + button.onclick = (event) => { + this.pushEventTo(this.el, 'option_remove', {idx: button.dataset.idx}) + } + }) + }, + + setInputValue(value) { + this.textInput().value = value + }, + + inputEvent(selection, mode) { + const selector = mode === "single" + ? "input.single-mode" + : (selection.length === 0 + ? "input[data-live-select-empty]" + : "input[type=hidden]") + this.el.querySelector(selector).dispatchEvent(new Event('input', {bubbles: true})) + }, + + mounted() { + this.maybeStyleClearButton() + + this.handleEvent("parent_event", ({id, event, payload}) => { + if (this.el.id === id) { + this.pushEventToParent(event, payload) + } + }) + + this.handleEvent("select", ({id, selection, mode, input_event, parent_event}) => { + if (this.el.id === id) { + this.selection = selection + if (mode === "single") { + const label = selection.length > 0 ? selection[0].label : null + this.setInputValue(label) + } else { + this.setInputValue(null) + } + if (input_event) { + this.inputEvent(selection, mode) + } + if (parent_event) { + this.pushEventToParent(parent_event, {id}) + } + } + }) + + this.handleEvent("active", ({id, idx}) => { + if (this.el.id === id) { + const option = this.el.querySelector(`div[data-idx="${idx}"]`) + if (option) { + option.scrollIntoView({block: "nearest"}) + } + } + }) + + this.attachDomEventHandlers() + }, + + updated() { + this.maybeStyleClearButton() + this.attachDomEventHandlers() + }, + + reconnected() { + if (this.selection && this.selection.length > 0) { + this.pushEventTo(this.el, "selection_recovery", this.selection) + } + } +} + +export default LiveSelect \ No newline at end of file diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 01cde2a..37c54e0 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -9,7 +9,9 @@ module.exports = { content: [ "./js/**/*.js", "../lib/maker_passport_web.ex", - "../lib/maker_passport_web/**/*.*ex" + "../lib/maker_passport_web/**/*.*ex", + '../deps/live_select/lib/live_select/component.*ex', + ], theme: { extend: { diff --git a/lib/maker_passport/maker.ex b/lib/maker_passport/maker.ex index caf8fb7..54c943c 100644 --- a/lib/maker_passport/maker.ex +++ b/lib/maker_passport/maker.ex @@ -7,7 +7,7 @@ defmodule MakerPassport.Maker do alias MakerPassport.Accounts alias MakerPassport.Repo - alias MakerPassport.Maker.{Profile, Skill, ProfileSkill} + alias MakerPassport.Maker.{Certification, Profile, ProfileSkill, Skill, Website, Location} @doc """ Returns the list of profiles. @@ -18,11 +18,27 @@ defmodule MakerPassport.Maker do [%Profile{}, ...] """ - def list_profiles do - Repo.all(Profile) |> Repo.preload(:skills) + def list_profiles(skills \\ []) do + Repo.all(Profile) |> Repo.preload([:skills, :user]) + query = + Profile + |> join(:left, [p], s in assoc(p, :skills)) + |> maybe_filter_by_skills(skills) + |> 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 + query + |> where([p, s], s.name in ^skills) + |> group_by([p], p.id) + |> having([p, s], count(s.id) == ^length(skills)) end - def list_profiles(criteria) when is_list(criteria) do + def list_profiles_by_criteria(criteria) when is_list(criteria) do query = from(p in Profile, where: not is_nil(p.name)) Enum.reduce(criteria, query, fn @@ -36,6 +52,7 @@ defmodule MakerPassport.Maker do from q in query, preload: ^preload end) |> Repo.all() + |> Repo.preload([:user]) end @doc """ @@ -52,7 +69,13 @@ defmodule MakerPassport.Maker do ** (Ecto.NoResultsError) """ - def get_profile!(id), do: Repo.get!(Profile, id) |> Repo.preload(:user) |> Repo.preload([:skills]) + def get_profile!(id), + do: + Repo.get!(Profile, id) + |> Repo.preload(:user) + |> Repo.preload([:skills]) + |> Repo.preload(:certifications) + |> Repo.preload(:websites) def get_profile_by_user_id!(user_id) do user = @@ -74,8 +97,6 @@ defmodule MakerPassport.Maker do |> Repo.preload([:skills]) end - - @doc """ Creates a profile. @@ -141,7 +162,28 @@ defmodule MakerPassport.Maker do Profile.changeset(profile, attrs) end - @doc """ + @doc """ + Create %Location{} or get %Location{}. + + ## Examples + + iex> get_or_create_location("Uk", "London") + %Location{} + + """ + def get_or_create_location(country, city) do + case Repo.one(from l in Location, where: ilike(l.country, ^country) and ilike(l.city, ^city)) do + nil -> + %Location{country: country, city: city} + |> Location.changeset() + |> Repo.insert!() + + location -> + location + end + end + + @doc """ Returns the list of skills. ## Examples @@ -278,6 +320,7 @@ defmodule MakerPassport.Maker do profile_skills = Repo.all(from ps in ProfileSkill, where: ps.profile_id == ^profile_id) skill_ids_to_exclude = Enum.map(profile_skills, & &1.skill_id) skills = Enum.reject(skills, &(&1.id in skill_ids_to_exclude)) + skills |> Enum.map(&to_skill_tuple/1) |> Enum.sort_by(&elem(&1, 0)) @@ -289,8 +332,199 @@ defmodule MakerPassport.Maker do def has_skill?(profile, skill_id) do # Check if the profile has the skill by querying the database - Repo.exists?(from s in ProfileSkill, - where: s.profile_id == ^profile.id and s.skill_id == ^skill_id + Repo.exists?( + from s in ProfileSkill, + where: s.profile_id == ^profile.id and s.skill_id == ^skill_id ) end + + def search_cities(""), do: [] + + def search_cities(search_text) do + Repo.all(from l in Location, where: ilike(l.city, ^"%#{search_text}%")) + end + + def to_city_list(cities, nil) do + cities + |> Enum.map(&to_city_tuple/1) + |> Enum.sort_by(&elem(&1, 0)) + end + + def to_city_list(cities, location_id) do + cities = Enum.filter(cities, &(&1.id != location_id)) + + cities + |> Enum.map(&to_city_tuple/1) + |> Enum.sort_by(&elem(&1, 0)) + end + + defp to_city_tuple(location) do + {location.city, location.id} + end + + @doc """ + Gets a single website. + + Raises `Ecto.NoResultsError` if the Website does not exist. + + ## Examples + + iex> get_website!(123) + %Website{} + + iex> get_website!(456) + ** (Ecto.NoResultsError) + + """ + def get_website!(id), do: Repo.get!(Website, id) + + @doc """ + Creates a website, + + ## Examples + + iex> create_website(%{field: value}) + {:ok, %Website{}} + + iex> create_website(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_website(attrs \\ %{}) do + %Website{} + |> Website.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a website. + + ## Examples + + iex> update_website(website, %{field: new_value}) + {:ok, %Website{}} + + iex> update_website(website, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_website(%Website{} = website, attrs) do + website + |> Website.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a website. + + ## Examples + + iex> delete_website(website) + {:ok, %Website{}} + + iex> delete_website(website) + {:error, %Ecto.Changeset{}} + + """ + def delete_website(%Website{} = website) do + Repo.delete(website) + end + + def remove_website(website_id) do + website = get_website!(website_id) + Repo.delete(website) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking website changes. + + ## Examples + + iex> change_website(website) + %Ecto.Changeset{data: %Website{}} + + """ + def change_website(%Website{} = website, attrs \\ %{}) do + Website.changeset(website, attrs) + end + + def add_website(profile_id, website_params) do + website_params = Map.put(website_params, "profile_id", profile_id) + create_website(website_params) + end + + @doc """ + Gets a single certification. + + Raises `Ecto.NoResultsError` if the Certification does not exist. + + ## Examples + + iex> get_certification!(123) + %Certification{} + + iex> get_certification!(456) + ** (Ecto.NoResultsError) + + """ + def get_certification!(id), do: Repo.get!(Certification, id) + + @doc """ + Creates a certification, + + ## Examples + + iex> create_certification(%{field: value}) + {:ok, %Certification{}} + + iex> create_certification(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_certification(attrs \\ %{}) do + IO.inspect(attrs) + + %Certification{} + |> Certification.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Deletes a certification. + + ## Examples + + iex> delete_certification(certification) + {:ok, %Certification{}} + + iex> delete_certification(certification) + {:error, %Ecto.Changeset{}} + + """ + def delete_certification(%Certification{} = certification) do + Repo.delete(certification) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking certification changes. + + ## Examples + + iex> change_certification(certification) + %Ecto.Changeset{data: %Certification{}} + + """ + def change_certification(%Certification{} = certification, attrs \\ %{}) do + Certification.changeset(certification, attrs) + end + + def add_certification(profile_id, certification_params) do + certification_params = Map.put(certification_params, "profile_id", profile_id) + create_certification(certification_params) + end + + def remove_certification(certification_id) do + certification = get_certification!(certification_id) + Repo.delete(certification) + end end diff --git a/lib/maker_passport/maker/certification.ex b/lib/maker_passport/maker/certification.ex new file mode 100644 index 0000000..454efa8 --- /dev/null +++ b/lib/maker_passport/maker/certification.ex @@ -0,0 +1,25 @@ +defmodule MakerPassport.Maker.Certification do + @moduledoc false + use Ecto.Schema + import Ecto.Changeset + + alias MakerPassport.Maker.Profile + + schema "maker_certifications" do + field :issue_date, :date + field :issuer_name, :string + field :title, :string + field :url, :string + + belongs_to :profile, Profile + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(certification, attrs \\ %{}) do + certification + |> cast(attrs, [:title, :issuer_name, :issue_date, :url, :profile_id]) + |> validate_required([:title, :issuer_name, :issue_date, :profile_id]) + end +end diff --git a/lib/maker_passport/maker/location.ex b/lib/maker_passport/maker/location.ex new file mode 100644 index 0000000..c2ee272 --- /dev/null +++ b/lib/maker_passport/maker/location.ex @@ -0,0 +1,23 @@ +defmodule MakerPassport.Maker.Location do + @moduledoc """ + A Maker's profile location. + """ + + use Ecto.Schema + import Ecto.Changeset + + schema "locations" do + field :country, :string + field :city, :string + field :latitude, :float + field :longitude, :float + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(location, attrs \\ %{}) do + location + |> cast(attrs, [:country, :city, :latitude, :longitude]) + end +end diff --git a/lib/maker_passport/maker/profile.ex b/lib/maker_passport/maker/profile.ex index 3585daf..afd1ab1 100644 --- a/lib/maker_passport/maker/profile.ex +++ b/lib/maker_passport/maker/profile.ex @@ -7,7 +7,7 @@ defmodule MakerPassport.Maker.Profile do import Ecto.Changeset alias MakerPassport.Accounts.User - alias MakerPassport.Maker.Skill + alias MakerPassport.Maker.{Certification, Skill, Location, Website} schema "profiles" do field :bio, :string @@ -15,10 +15,13 @@ defmodule MakerPassport.Maker.Profile do field :profile_image_location, :string, default: "" belongs_to :user, User + belongs_to :location, Location + has_many :certifications, Certification + has_many :websites, Website many_to_many :skills, Skill, - join_through: "profile_skills", - on_replace: :delete + join_through: "profile_skills", + on_replace: :delete timestamps(type: :utc_datetime) end @@ -26,19 +29,16 @@ defmodule MakerPassport.Maker.Profile do @doc false def changeset(profile, attrs) do profile - |> cast(attrs, [:name, :bio, :profile_image_location]) + |> cast(attrs, [:name, :bio, :profile_image_location, :location_id]) |> validate_required([:name]) # |> foreign_key_constraint(:user_id) # |> unique_constraint(:user_id) end def profile_complete?(%User{profile: profile}) when not is_nil(profile) do - # Add all required profile fields here - not is_nil(profile.name) and - not is_nil(profile.bio) and - not is_nil(profile.profile_image_location) - # Add other required fields + required_fields = [:name, :bio, :profile_image_location, :location_id] + Enum.all?(required_fields, &(not is_nil(Map.get(profile, &1)))) end - def profile_complete?(_user), do: false + def profile_complete?(_user), do: false end diff --git a/lib/maker_passport/maker/website.ex b/lib/maker_passport/maker/website.ex new file mode 100644 index 0000000..b2d517d --- /dev/null +++ b/lib/maker_passport/maker/website.ex @@ -0,0 +1,36 @@ +defmodule MakerPassport.Maker.Website do + @moduledoc false + use Ecto.Schema + import Ecto.Changeset + + alias MakerPassport.Maker.Profile + + schema "maker_websites" do + field :title, :string + field :url, :string + + belongs_to :profile, Profile + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(website, attrs \\ %{}) do + website + |> cast(attrs, [:title, :url, :profile_id]) + |> validate_required([:title, :url, :profile_id]) + |> validate_url(:url) + end + + defp validate_url(changeset, field) do + validate_change(changeset, field, fn _, value -> + case URI.parse(value) do + %URI{scheme: scheme, host: host} when not is_nil(scheme) and not is_nil(host) -> + [] + + _ -> + [{field, "must be a valid URL"}] + end + end) + end +end diff --git a/lib/maker_passport_web/components/core_components.ex b/lib/maker_passport_web/components/core_components.ex index 75568bc..6c0fcc8 100644 --- a/lib/maker_passport_web/components/core_components.ex +++ b/lib/maker_passport_web/components/core_components.ex @@ -202,7 +202,7 @@ defmodule MakerPassportWeb.CoreComponents do def simple_form(assigns) do ~H""" <.form :let={f} for={@for} as={@as} {@rest}> -
+
<%= render_slot(@inner_block, f) %>
<%= render_slot(action, f) %> @@ -318,7 +318,7 @@ defmodule MakerPassportWeb.CoreComponents do name={@name} value="true" checked={@checked} - class="checkbox" <> @class + class={["checkbox", @class]} {@rest} /> <%= @label %> @@ -332,13 +332,11 @@ defmodule MakerPassportWeb.CoreComponents do ~H"""
<.label for={@id}><%= @label %> - + @class multiple={@multiple} {@rest} - > + > <%= Phoenix.HTML.Form.options_for_select(@options, @value) %> diff --git a/lib/maker_passport_web/components/custom_components.ex b/lib/maker_passport_web/components/custom_components.ex index 80455e1..9d5ed8b 100644 --- a/lib/maker_passport_web/components/custom_components.ex +++ b/lib/maker_passport_web/components/custom_components.ex @@ -3,7 +3,7 @@ defmodule MakerPassportWeb.CustomComponents do Custom components for the application. """ -use Phoenix.Component + use Phoenix.Component alias MakerPassport.Maker.Profile @@ -14,21 +14,30 @@ use Phoenix.Component # svg_size = round(assigns.size * 0.9) ~H""" <%= if @profile.profile_image_location && @profile.profile_image_location != "" do %> -
+
#{@profile.name}'s profile image
<% else %>
- - user-profile-filled - - - - - + + user-profile-filled + + + + +
<% end %> diff --git a/lib/maker_passport_web/components/layouts/root.html.heex b/lib/maker_passport_web/components/layouts/root.html.heex index f2658a5..654a150 100644 --- a/lib/maker_passport_web/components/layouts/root.html.heex +++ b/lib/maker_passport_web/components/layouts/root.html.heex @@ -12,12 +12,14 @@ -
+
- -
- -
diff --git a/lib/maker_passport_web/live/home_live/index.ex b/lib/maker_passport_web/live/home_live/index.ex index af52ff1..abe58d9 100644 --- a/lib/maker_passport_web/live/home_live/index.ex +++ b/lib/maker_passport_web/live/home_live/index.ex @@ -6,11 +6,15 @@ defmodule MakerPassportWeb.HomeLive.Index do import Ecto.Query, warn: false alias MakerPassport.Maker - alias MakerPassport.Maker.Skill @impl true def mount(_params, _session, socket) do - latest_profiles = Maker.list_profiles(limit: 4, sort: %{sort_by: :updated_at, sort_order: :desc}, preload: [:skills]) + latest_profiles = + Maker.list_profiles_by_criteria( + limit: 4, + sort: %{sort_by: :updated_at, sort_order: :desc}, + preload: [:skills] + ) socket = socket diff --git a/lib/maker_passport_web/live/home_live/index.html.heex b/lib/maker_passport_web/live/home_live/index.html.heex index 9649b6b..d346c45 100644 --- a/lib/maker_passport_web/live/home_live/index.html.heex +++ b/lib/maker_passport_web/live/home_live/index.html.heex @@ -3,24 +3,21 @@
+ style="background-image: url(https://cdn.usegalileo.ai/sdxl10/c0eec962-8d9c-4ec3-b77a-c3681f6fbb62.png);" + >
-

Connect with makers from around the world

+

+ Connect with makers from around the world +

<%= if !@current_user do %>
- <.link - href={~p"/users/register"} - class="btn btn-lg btn-primary me-5" - > + <.link href={~p"/users/register"} class="btn btn-lg btn-info me-5"> Join or - <.link - href={~p"/users/log_in"} - class="btn btn-lg btn-accent ms-5" - > + <.link href={~p"/users/log_in"} class="btn btn-lg btn-accent ms-5"> Log in
@@ -28,10 +25,15 @@
-

Recently Active Makers

+

+ Recently Active Makers +

<%= for profile <- @latest_profiles do %> - <.link href={~p"/profiles/#{profile.id}"} class="block hover:drop-shadow-lg hover:-mt-0.5 hover:-ms-0.5 active:-mb-0.5 active:-me-0.5 active:mt-0.5 active:ms-0.5"> + <.link + href={~p"/profiles/#{profile.id}"} + class="block hover:drop-shadow-lg hover:-mt-0.5 hover:-ms-0.5 active:-mb-0.5 active:-me-0.5 active:mt-0.5 active:ms-0.5" + >
<%= if profile.profile_image_location do %> @@ -40,23 +42,35 @@
<% else %>
- - + +
- <% end %> + <% end %>

<%= profile.name %>

<%= if profile.skills != [] do %>

- <%= profile.skills |> Enum.take(3) |> Enum.map(&(&1.name)) |> Enum.join(", ") %><%= if length(profile.skills) > 3 do %>, …<% end %> + <%= profile.skills |> Enum.take(3) |> Enum.map(& &1.name) |> Enum.join(", ") %> + <%= if length(profile.skills) > 3 do %> + , … + <% end %>

- <% end %> + <% end %>
<% end %>
-
\ No newline at end of file +
diff --git a/lib/maker_passport_web/live/profile_live/index.ex b/lib/maker_passport_web/live/profile_live/index.ex index 1a4242d..65e5193 100644 --- a/lib/maker_passport_web/live/profile_live/index.ex +++ b/lib/maker_passport_web/live/profile_live/index.ex @@ -1,37 +1,38 @@ defmodule MakerPassportWeb.ProfileLive.Index do use MakerPassportWeb, :live_view - alias MakerPassport.Maker - alias MakerPassport.Maker.Profile + on_mount {MakerPassportWeb.UserAuth, :mount_current_user} + alias MakerPassport.Maker + import MakerPassportWeb.ProfileLive.TypeaheadComponent, only: [typeahead: 1] import MakerPassportWeb.CustomComponents @impl true def mount(_params, _session, socket) do - {:ok, stream(socket, :profiles, Maker.list_profiles())} - end + profiles = + case socket.assigns.current_user do + nil -> + Maker.list_profiles() - @impl true - def handle_params(params, _url, socket) do - {:noreply, apply_action(socket, socket.assigns.live_action, params)} - end + user -> + Maker.list_profiles() + |> Enum.reject(fn profile -> profile.user && profile.user.id == user.id end) + end - defp apply_action(socket, :edit, %{"id" => id}) do - socket - |> assign(:page_title, "Edit Profile") - |> assign(:profile, Maker.get_profile!(id)) - end + socket = + socket + |> assign(:page_title, "Maker Profiles") + |> assign(:search_skills, []) + |> assign(:no_skills_results, false) + |> assign(:profile, nil) + |> stream(:profiles, profiles) - defp apply_action(socket, :new, _params) do - socket - |> assign(:page_title, "New Profile") - |> assign(:profile, %Profile{}) + {:ok, socket} end - defp apply_action(socket, :index, _params) do - socket - |> assign(:page_title, "Listing Profiles") - |> assign(:profile, nil) + @impl true + def handle_params(_params, _url, socket) do + {:noreply, socket} end @impl true @@ -39,6 +40,32 @@ defmodule MakerPassportWeb.ProfileLive.Index do {:noreply, stream_insert(socket, :profiles, profile)} end + @impl true + def handle_info({:typeahead, {name, _}, _}, socket) do + socket = + socket + |> update(:search_skills, fn skills -> [name | skills] end) + + profiles = + case socket.assigns.current_user do + nil -> + Maker.list_profiles(socket.assigns.search_skills) + + user -> + Maker.list_profiles(socket.assigns.search_skills) + |> Enum.reject(fn profile -> profile.user && profile.user.id == user.id end) + end + + IO.inspect(profiles) + + socket = + socket + |> assign(:no_skills_results, profiles == []) + |> stream(:profiles, profiles) + + {:noreply, socket} + end + @impl true def handle_event("delete", %{"id" => id}, socket) do profile = Maker.get_profile!(id) @@ -46,4 +73,31 @@ defmodule MakerPassportWeb.ProfileLive.Index do {:noreply, stream_delete(socket, :profiles, profile)} end + + 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 + + socket = + socket + |> assign(:no_skills_results, profiles == []) + |> assign(:search_skills, updated_skills) + |> stream(:profiles, profiles) + + {:noreply, socket} + end + + defp remove_selected_skill(skills, selected_skills) do + Enum.filter(skills, fn {skill, _} -> skill not in selected_skills end) + 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 16ea488..00d6ba9 100644 --- a/lib/maker_passport_web/live/profile_live/index.html.heex +++ b/lib/maker_passport_web/live/profile_live/index.html.heex @@ -1,9 +1,58 @@ -<.header> - Makers - +
+
+ Makers +
+ +
+
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} + /> +
+
+
+
+ Showing makers with these skills: +
+
+ <%= skill %> + + <.icon name="hero-x-circle" class="w-6 h-6 mb-1 hover:text-red-500 cursor-pointer" /> + +
+
-
-
+
+ No makers found with this combination of skills +
+
+
<.avatar profile={profile} size={36} /> @@ -12,13 +61,21 @@

<%= profile.name %>

- <%= profile.skills |> Enum.take(3) |> Enum.map(&(&1.name)) |> Enum.join(", ") %><%= if length(profile.skills) > 3 do %>, …<% end %> + <%= profile.skills |> Enum.take(3) |> Enum.map(& &1.name) |> Enum.join(", ") %> + <%= if length(profile.skills) > 3 do %> + , … + <% end %>
-<.modal :if={@live_action in [:new, :edit]} id="profile-modal" show on_cancel={JS.patch(~p"/profiles")}> +<.modal + :if={@live_action in [:new, :edit]} + id="profile-modal" + show + on_cancel={JS.patch(~p"/profiles")} +> <.live_component module={MakerPassportWeb.ProfileLive.FormComponent} id={@profile.id || :new} 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 5f5142d..8aec4d6 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 @@ -2,6 +2,8 @@ defmodule MakerPassportWeb.ProfileLive.ProfileFormComponent do use MakerPassportWeb, :live_component alias MakerPassport.Maker + import MakerPassportWeb.ProfileLive.TypeaheadComponent, only: [typeahead: 1] + import LiveSelect @do_space "maker-profiles" @do_region "fra1" @@ -23,7 +25,8 @@ defmodule MakerPassportWeb.ProfileLive.ProfileFormComponent do |> assign(assigns) |> assign_new(:form, fn -> to_form(Maker.change_profile(profile)) - end)} + end) + } end @impl true @@ -45,6 +48,7 @@ defmodule MakerPassportWeb.ProfileLive.ProfileFormComponent do case length(profile_image_location) do 0 -> profile_params + _ -> Map.put(profile_params, "profile_image_location", Enum.at(profile_image_location, 0)) end @@ -52,7 +56,23 @@ defmodule MakerPassportWeb.ProfileLive.ProfileFormComponent do save_profile(socket, profile_params) end + def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do + # Get the list of countries + countries = country_options() + + filtered_countries = + Enum.filter(countries, fn {name, _code} -> + String.contains?(String.downcase(name), String.downcase(text)) + end) + + send_update(LiveSelect.Component, id: live_select_id, options: filtered_countries) + + {:noreply, socket} + end + + defp save_profile(socket, profile_params) do + profile_params = check_or_create_location(profile_params) case Maker.update_profile(socket.assigns.profile, profile_params) do {:ok, _profile} -> {:noreply, @@ -65,6 +85,35 @@ defmodule MakerPassportWeb.ProfileLive.ProfileFormComponent do end end + defp check_or_create_location(%{"country" => ""} = profile_params) do + Map.merge(profile_params, %{"location_id" => ""}) + end + + defp check_or_create_location(%{"country" => country} = profile_params) + when country != "" do + location = Maker.get_or_create_location(country, profile_params["city"]) + Map.merge(profile_params, %{"location_id" => location.id}) + end + + defp check_or_create_location(profile_params), do: profile_params + + defp country_options do + Countries.all() + |> Enum.map(fn country -> + {country.name, country.alpha2} + end) + |> 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, @@ -80,7 +129,6 @@ defmodule MakerPassportWeb.ProfileLive.ProfileFormComponent do expires_in: :timer.hours(1) ) - metadata = %{ uploader: "S3", key: filename(entry), 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 d7684f6..e85a055 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 @@ -1,13 +1,53 @@
<.simple_form - for={@form} - id="profile-form" - phx-target={@myself} - phx-change="validate" - phx-submit="save" + for={@form} + id="profile-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" > - <.input field={@form[:name]} type="text" label="Name" class="" /> - <.input field={@form[:bio]} type="textarea" label="Bio" class="h-48" /> + <.input + field={@form[:name]} + type="text" + label="Name" + class="input input-bordered w-full max-w-xs" + /> + +
+ <.label for="Country">Country + <.live_select + field={@form[:country]} + phx-target={@myself} + allow_clear={true} + id="country_search" + value={@form[:country].value || get_country_name(@profile.location)} + /> +
+ +
+ <.label for="City">City + <.typeahead + id="cities-picker" + class="w-full rounded-s-lg" + value={@form[:city].value || (@profile.location && @profile.location.city)} + name="profile[city]" + disabled={is_nil(@form[:country].value) || @form[:country].value == ""} + on_search={ + fn search_text -> + Maker.search_cities(search_text) + |> Maker.to_city_list(@profile.location_id) + end + } + on_select={fn city -> send(self(), {:typeahead, city, "cities-picker"}) end} + /> +
+ + <.input + field={@form[:bio]} + type="textarea" + label="Bio" + class="textarea textarea-bordered h-48" + />
<%= unless @profile.profile_image_location == nil || @profile.profile_image_location == "" do %> @@ -18,8 +58,14 @@
<% end %>
-
- <.live_file_input upload={@uploads.profile_image} class="file-input file-input-bordered w-full max-w-sm mr-1" /> +
+ <.live_file_input + upload={@uploads.profile_image} + class="file-input file-input-bordered w-full max-w-sm mr-1" + />

or drag and drop here

@@ -40,13 +86,20 @@
- <%= entry.progress %>% + <%= entry.progress %>%
- + +
- +
<.error :for={err <- upload_errors(@uploads.profile_image, entry)}> diff --git a/lib/maker_passport_web/live/profile_live/show.ex b/lib/maker_passport_web/live/profile_live/show.ex index 3411d44..75c82d2 100644 --- a/lib/maker_passport_web/live/profile_live/show.ex +++ b/lib/maker_passport_web/live/profile_live/show.ex @@ -4,18 +4,18 @@ defmodule MakerPassportWeb.ProfileLive.Show do import MakerPassportWeb.CustomComponents import MakerPassportWeb.ProfileLive.TypeaheadComponent, only: [typeahead: 1] - alias MakerPassport.Repo alias MakerPassport.Maker - alias MakerPassport.Maker.Skill + alias MakerPassport.Maker.{Certification, Skill, Website} + alias MakerPassport.Repo @impl true - def mount(_params, session, socket) do + def mount(_params, _session, socket) do {:ok, socket} end @impl true def handle_params(%{"id" => id} = params, _, socket) do - profile = Maker.get_profile!(id) + profile = Maker.get_profile!(id) |> Repo.preload(:location) skills = ordered_skills(profile) socket = @@ -39,6 +39,12 @@ defmodule MakerPassportWeb.ProfileLive.Show do |> assign_new(:skills_form, fn -> to_form(Skill.changeset(%Skill{})) end) + |> assign_new(:website_form, fn -> + to_form(Website.changeset(%Website{})) + end) + |> assign_new(:certification_form, fn -> + to_form(Certification.changeset(%Certification{})) + end) |> assign(:page_title, "Edit Profile") end @@ -47,10 +53,11 @@ defmodule MakerPassportWeb.ProfileLive.Show do end @impl true - def handle_info({:typeahead, {skill_name, _}}, socket) do + def handle_info({:typeahead, {name, _}, id}, socket) do socket = socket - |> push_event("set-input-value", %{id: "skills-picker", label: skill_name}) + |> push_event("set-input-value", %{id: id, label: name}) + {:noreply, socket} end @@ -65,8 +72,49 @@ defmodule MakerPassportWeb.ProfileLive.Show do {:noreply, push_navigate(socket, to: ~p"/profiles/#{socket.assigns.profile.id}/edit-profile")} end + @impl true + def handle_event("save-website", %{"website" => website_params}, socket) do + Maker.add_website(socket.assigns.profile.id, website_params) + {:noreply, push_navigate(socket, to: ~p"/profiles/#{socket.assigns.profile.id}/edit-profile")} + end + + @impl true + def handle_event("validate-website", %{"website" => website_params}, socket) do + changeset = Maker.change_website(%Website{}, website_params) + {:noreply, assign(socket, website_form: to_form(changeset, action: :validate))} + end + + def handle_event("remove-website", %{"website_id" => website_id}, socket) do + remove_website(socket, String.to_integer(website_id)) + {:noreply, push_navigate(socket, to: ~p"/profiles/#{socket.assigns.profile.id}/edit-profile")} + end + + @impl true + def handle_event("save-certification", %{"certification" => certification_params}, socket) do + save_certification(socket, certification_params, socket.assigns.profile) + {:noreply, push_navigate(socket, to: ~p"/profiles/#{socket.assigns.profile.id}/edit-profile")} + end + + @impl true + def handle_event("validate-certification", %{"certification" => certification_params}, socket) do + changeset = Maker.change_certification(%Certification{}, certification_params) + {:noreply, assign(socket, certification_form: to_form(changeset, action: :validate))} + end + + def handle_event("remove-certification", %{"certification_id" => certification_id}, socket) do + remove_certification(socket, String.to_integer(certification_id)) + {: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 = check_or_create_skill(skill_name) + skill = skill_name |> String.trim() |> check_or_create_skill() add_or_update_skill(socket, skill, profile) assign(socket, :profile, profile) {:noreply, socket} @@ -98,6 +146,21 @@ defmodule MakerPassportWeb.ProfileLive.Show do socket end + defp save_certification(socket, certification_params, profile) do + Maker.add_certification(profile.id, certification_params) + socket + end + + defp remove_website(socket, website_id) do + Maker.remove_website(website_id) + socket + end + + defp remove_certification(socket, certification_id) do + Maker.remove_certification(certification_id) + socket + end + defp page_title(:show), do: "Show Profile" defp page_title(:edit_profile), do: "Edit Profile" end 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 2eeece0..4380d7e 100644 --- a/lib/maker_passport_web/live/profile_live/show.html.heex +++ b/lib/maker_passport_web/live/profile_live/show.html.heex @@ -1,82 +1,328 @@ -
-
- <%= if @current_user && @profile.user.id == @current_user.id do %> -
- My Profile - <.link :if={@live_action != :edit_profile} patch={~p"/profiles/#{@profile}/edit-profile"} phx-click={JS.push_focus()}> - <.icon name="hero-pencil-square" class="w-5 h-5 mb-3 hover:text-blue-600" /> - -
- <% end %> -
-
-
-
+
+ <%= if @current_user && @profile.user.id == @current_user.id do %> +
+ My Profile + <.link + :if={@live_action != :edit_profile} + patch={~p"/profiles/#{@profile}/edit-profile"} + phx-click={JS.push_focus()} + > + <.icon name="hero-pencil-square" class="w-5 h-5 hover:text-blue-600" /> + +
+ <% end %> +
+ +
+
+
+
<.avatar profile={@profile} size={36} /> -
-

<%= @profile.name %>

-
-
-

- <%= @profile.bio %> +

+
+

+ <%= @profile.name %> +

+
+

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

+

<%= @profile.bio %>

+
+
+
+ <%= if @skills != [] do %> +
+
+

Skills

-
-

Skills

-
- <%= if @skills != [] do %> -
- <%= skill.name %> +
+
+ <%= skill.name %> +
+
+
+ <% end %> + + <%= if @profile.certifications != [] do %> +
+
+

Certifications

+
+
+
+
+
+
Title
+
+ <%= certificate.title %> +
+ +
Issuer
+
+ <%= certificate.issuer_name %> +
+ +
Issue Date
+
+ <%= certificate.issue_date %> +
+ + <%= if certificate.url do %> +
URL
+ + <% end %>
- <% end %> +
-
+ <% end %> + <%= if @profile.websites != [] do %> +
+
+

Websites

+
+
+
+ <.icon name="hero-globe-alt" class="w-5 h-5" /> + + <%= website.title %> + +
+
+
+ <% end %>
-
- <.live_component - module={MakerPassportWeb.ProfileLive.ProfileFormComponent} - id={@profile.id} - title={@page_title} - action={@live_action} - profile={@profile} - patch={~p"/profiles/#{@profile}"} - /> +
+
+ <.live_component + module={MakerPassportWeb.ProfileLive.ProfileFormComponent} + id={@profile.id} + title={@page_title} + action={@live_action} + profile={@profile} + patch={~p"/profiles/#{@profile}"} + /> -
+
-
- <.simple_form - for={@skills_form} - id="skills-form" - phx-submit="save_skill" +
+

Skills

+ <.simple_form for={@skills_form} id="skills-form" phx-submit="save_skill"> +
+ <.typeahead + id="skills-picker" + class="w-full rounded-s-lg" + placeholder="Add a skill..." + on_search={ + fn search_text -> + Maker.search_skills(search_text) + |> Maker.to_skill_list(@profile.id) + end + } + on_select={fn skill -> send(self(), {:typeahead, skill, "skills-picker"}) end} + /> + +
+ +
+
-
- <.typeahead - id="skills-picker" - class="w-full rounded-s-lg" - placeholder="Add a skill..." - on_search={fn search_text -> - Maker.search_skills(search_text) - |> Maker.to_skill_list(@profile.id) - end} - on_select={fn skill -> send(self(), {:typeahead, skill}) end} - /> - -
- -
-
- <%= skill.name %> - - <.icon name="hero-x-circle" class="w-6 h-6 mb-1 hover:text-red-500 cursor-pointer" /> - -
+ <%= skill.name %> + + <.icon name="hero-x-circle" class="w-6 h-6 mb-1 hover:text-red-500 cursor-pointer" /> +
+ +
+ +
+

Certifications

+ <.simple_form + for={@certification_form} + id="certification-form" + phx-change="validate-certification" + phx-submit="save-certification" + class="form-control border-2 rounded-lg p-3" + > + <.input + field={@certification_form[:title]} + type="text" + label="Title" + class="input input-bordered" + /> + <.input + field={@certification_form[:issue_date]} + type="date" + label="Date" + class="input input-bordered" + /> + <.input + field={@certification_form[:issuer_name]} + type="text" + label="Issuer" + class="input input-bordered" + /> + <.input + field={@certification_form[:url]} + type="url" + label="URL" + class="input input-bordered" + /> + + + + +
+ + + + + + + + + + + + + + + + + + + + +
TitleIssuerDateURL
<%= certification.title %><%= certification.issuer_name %><%= certification.issue_date %> + + <%= certification.url %> + + + + <.icon + name="hero-x-circle" + class="w-6 h-6 mb-1 hover:text-red-500 cursor-pointer" + /> + +
+
+
+ +
+ +
+

Websites

+ <.simple_form + for={@website_form} + id="website-form" + phx-change="validate-website" + phx-submit="save-website" + class="form-control border-2 rounded-lg p-3" + > + <.input + field={@website_form[:title]} + type="text" + label="Title" + class="input input-bordered" + /> + <.input field={@website_form[:url]} type="url" label="URL" class="input input-bordered" /> + + + + +
+ + + + + + + + + + + + + + + + +
TitleURL
<%= website.title %> + + <%= website.url %> + + + + <.icon + name="hero-x-circle" + class="w-6 h-6 mb-1 hover:text-red-500 cursor-pointer" + /> + +
+
+
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 be6514e..879fe9f 100644 --- a/lib/maker_passport_web/live/profile_live/typeahead_component.ex +++ b/lib/maker_passport_web/live/profile_live/typeahead_component.ex @@ -5,27 +5,35 @@ defmodule MakerPassportWeb.ProfileLive.TypeaheadComponent do import MakerPassportWeb.CoreComponents attr :id, :string, default: "typeahead" + attr :name, :string, default: "search-field" + attr :value, :string, default: "" attr :label, :string, default: nil attr :class, :string, default: "" attr :rest, :global attr :on_select, :any, default: nil attr :on_search, :any, required: true + attr :disabled, :boolean, default: false def typeahead(assigns) do ~H""" <.live_component module={__MODULE__} id={@id} + name={@name} + value={@value} label={@label} rest={@rest} class={@class} on_select={@on_select} on_search={@on_search} + disabled={@disabled} /> """ end attr :id, :string + attr :name, :string + attr :value, :string attr :myself, :any attr :focused_option, :any attr :search_options, :list, default: [] @@ -67,9 +75,10 @@ defmodule MakerPassportWeb.ProfileLive.TypeaheadComponent do phx-target={@myself}> <.dropdown_options diff --git a/lib/maker_passport_web/router.ex b/lib/maker_passport_web/router.ex index 9685051..676e000 100644 --- a/lib/maker_passport_web/router.ex +++ b/lib/maker_passport_web/router.ex @@ -21,7 +21,7 @@ defmodule MakerPassportWeb.Router do pipe_through :browser live "/", HomeLive.Index, :index - live "/profiles", ProfileLive.Index, :index + live "/profiles", ProfileLive.Index end # Other scopes may use custom stacks. @@ -52,7 +52,7 @@ defmodule MakerPassportWeb.Router do pipe_through [:browser, :redirect_if_user_is_authenticated] live_session :redirect_if_user_is_authenticated, - on_mount: [{MakerPassportWeb.UserAuth, :redirect_if_user_is_authenticated}] do + on_mount: [{MakerPassportWeb.UserAuth, :redirect_if_user_is_authenticated}] do live "/users/register", UserRegistrationLive, :new live "/users/log_in", UserLoginLive, :new live "/users/reset_password", UserForgotPasswordLive, :new @@ -66,7 +66,7 @@ defmodule MakerPassportWeb.Router do pipe_through [:browser, :require_authenticated_user] live_session :require_authenticated_user, - on_mount: [{MakerPassportWeb.UserAuth, :ensure_authenticated}] do + on_mount: [{MakerPassportWeb.UserAuth, :ensure_authenticated}] do live "/users/settings", UserSettingsLive, :edit live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email diff --git a/mix.exs b/mix.exs index 885e8f6..60c34b7 100644 --- a/mix.exs +++ b/mix.exs @@ -61,7 +61,9 @@ defmodule MakerPassport.MixProject do {:dns_cluster, "~> 0.1.1"}, {:plug_crypto, "~> 1.0"}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:bandit, "~> 1.5"} + {:bandit, "~> 1.5"}, + {:countries, "~> 1.6"}, + {:live_select, "~> 1.0"} ] end diff --git a/mix.lock b/mix.lock index ace3d8e..0001cd3 100644 --- a/mix.lock +++ b/mix.lock @@ -1,25 +1,27 @@ %{ - "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, + "bandit": {:hex, :bandit, "1.6.0", "9cb6c67c27cecab2d0c93968cb957fa8decccb7275193c8bf33f97397b3ac25d", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "fd2491e564a7c5e11ff8496ebf530c342c742452c59de17ac0fb1f814a0ab01a"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, + "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, + "countries": {:hex, :countries, "1.6.0", "0776d78e80105944a4ea4d9d4286e852f83497e57222844ca51f9a22ed2d8fd9", [:mix], [{:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "a1e4d0fdd2a799f16a95ae2e842edeaabd9ac7639624ac5e139c54da7a6bccb0"}, "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, - "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 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.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, + "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]}, "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"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, @@ -27,20 +29,22 @@ "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, + "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, - "postgrex": {:hex, :postgrex, "0.19.2", "34d6884a332c7bf1e367fc8b9a849d23b43f7da5c6e263def92784d03f9da468", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "618988886ab7ae8561ebed9a3c7469034bf6a88b8995785a3378746a4b9835ec"}, - "swoosh": {:hex, :swoosh, "1.17.2", "73611f08fc7cb9fa15f4909db36eeb12b70727d5c8b6a7fa0d4a31c6575db29e", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de914359f0ddc134dc0d7735e28922d49d0503f31e4bd66b44e26039c2226d39"}, + "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"}, + "swoosh": {:hex, :swoosh, "1.17.3", "5cda7bff6bc1121cc5b58db8ed90ef33261b373425ae3e32dd599688037a0482", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "14ad57cfbb70af57323e17f569f5840a33c01f8ebc531dd3846beef3c9c95e55"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, - "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, + "thousand_island": {:hex, :thousand_island, "1.3.6", "835a626a8a6f6a1e681b63e1132a8427e87ce443aaf4888fbf63b2df77539b97", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0ed8798084c8c49a223840b20598b022e4eb8c9f390fb6701864c307fc9aa2cd"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, } diff --git a/priv/repo/migrations/20241126121721_create_maker_websites.exs b/priv/repo/migrations/20241126121721_create_maker_websites.exs new file mode 100644 index 0000000..66e0d4f --- /dev/null +++ b/priv/repo/migrations/20241126121721_create_maker_websites.exs @@ -0,0 +1,16 @@ +defmodule MakerPassport.Repo.Migrations.CreateMakerWebsites do + @moduledoc false + use Ecto.Migration + + def change do + create table(:maker_websites) do + add :title, :string, null: false + add :url, :string, null: false + add :profile_id, references(:profiles, on_delete: :nothing) + + timestamps(type: :utc_datetime) + end + + create index(:maker_websites, [:profile_id]) + end +end diff --git a/priv/repo/migrations/20241127000243_create_maker_certifications.exs b/priv/repo/migrations/20241127000243_create_maker_certifications.exs new file mode 100644 index 0000000..9a8e062 --- /dev/null +++ b/priv/repo/migrations/20241127000243_create_maker_certifications.exs @@ -0,0 +1,18 @@ +defmodule MakerPassport.Repo.Migrations.CreateMakerCertifications do + @moduledoc false + use Ecto.Migration + + def change do + create table(:maker_certifications) do + add :title, :string, null: false + add :issuer_name, :string, null: false + add :issue_date, :date, null: false + add :url, :string + add :profile_id, references(:profiles, on_delete: :nothing) + + timestamps(type: :utc_datetime) + end + + create index(:maker_certifications, [:profile_id]) + end +end diff --git a/priv/repo/migrations/20241129125126_create_locations.exs b/priv/repo/migrations/20241129125126_create_locations.exs new file mode 100644 index 0000000..cf794e0 --- /dev/null +++ b/priv/repo/migrations/20241129125126_create_locations.exs @@ -0,0 +1,16 @@ +defmodule MakerPassport.Repo.Migrations.CreateLocations do + use Ecto.Migration + + def change do + create table(:locations) do + add :country, :string + add :city, :string + add :latitude, :float + add :longitude, :float + + timestamps(type: :utc_datetime) + end + + create unique_index(:locations, [:country, :city]) + end +end diff --git a/priv/repo/migrations/20241130074406_alter_profiles_add_location_id.exs b/priv/repo/migrations/20241130074406_alter_profiles_add_location_id.exs new file mode 100644 index 0000000..ca37bfa --- /dev/null +++ b/priv/repo/migrations/20241130074406_alter_profiles_add_location_id.exs @@ -0,0 +1,9 @@ +defmodule MakerPassport.Repo.Migrations.AlterProfilesAddLocationId do + use Ecto.Migration + + def change do + alter table(:profiles) do + add :location_id, references(:locations) + end + end +end