diff --git a/friends/assets/js/app.js b/friends/assets/js/app.js index b99021a..9457ec5 100644 --- a/friends/assets/js/app.js +++ b/friends/assets/js/app.js @@ -21,8 +21,8 @@ // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import "phoenix_html" // Establish Phoenix Socket and LiveView configuration. -import {Socket} from "phoenix" -import {LiveSocket} from "phoenix_live_view" +import { Socket } from "phoenix" +import { LiveSocket } from "phoenix_live_view" import topbar from "../vendor/topbar" import mapboxgl from "../vendor/mapbox-gl" @@ -30,12 +30,12 @@ let Hooks = {}; let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { - hooks: Hooks, - params: { _csrf_token: csrfToken } + hooks: Hooks, + params: { _csrf_token: csrfToken } }) // Show progress bar on live navigation and form submits -topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) +topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }) window.addEventListener("phx:page-loading-start", info => topbar.show()) window.addEventListener("phx:page-loading-stop", info => topbar.hide()) @@ -49,35 +49,58 @@ liveSocket.connect() window.liveSocket = liveSocket window.hideElement = function (id) { - var el = document.getElementById(id); - el.hidden = true; + var el = document.getElementById(id); + el.hidden = true; } -window.selectResult = function (latlon, display) { - var name_el = document.querySelector("[autocomplete=name]"); - var latlon_el = document.querySelector("[autocomplete=latlon]"); +window.selectMapResult = function (latlon, display) { + var name_el = document.querySelector("[autocomplete=name]"); + var latlon_el = document.querySelector("[autocomplete=latlon]"); - name_el.value = display; - latlon_el.value = latlon; + name_el.value = display; + latlon_el.value = latlon; - window.liveSocket.hooks.showMapbox.initMap(); + window.liveSocket.hooks.showMapbox.initMap(); }; +window.selectRelation = function (id, name) { + var e = new Event('eventSender'); + e.data = { + id: id, + name: name + } + window.dispatchEvent(e); +} + +Hooks.NewRelation = { + mounted() { + var list_el = document.querySelector("div#relationships"); + window.addEventListener("eventSender", e => { + this.pushEvent("select_relation", e.data, function (reply) { + console.log(reply); + }) + }) + + } +} + Hooks.showMapbox = { - initMap() { - mapboxgl.accessToken = 'pk.eyJ1IjoicnlhbnBhbmR5YSIsImEiOiJja3psM2tlcDA1MXl1Mm9uZmo5bGxpNzdxIn0.TwBKpTTypcD5fWFc8XRyHg'; - - const latlon = JSON.parse(document.querySelector("[autocomplete=latlon]").value).reverse(); - - const map = new mapboxgl.Map({ - container: 'map', - style: 'mapbox://styles/mapbox/outdoors-v11', - center: latlon, - zoom: 8 - }); - }, - - mounted() { - this.initMap(); - } + initMap() { + mapboxgl.accessToken = 'pk.eyJ1IjoicnlhbnBhbmR5YSIsImEiOiJja3psM2tlcDA1MXl1Mm9uZmo5bGxpNzdxIn0.TwBKpTTypcD5fWFc8XRyHg'; + + const latlon = JSON.parse(document.querySelector("[autocomplete=latlon]").value).reverse(); + + const map = new mapboxgl.Map({ + container: 'map', + style: 'mapbox://styles/mapbox/outdoors-v11', + center: latlon, + zoom: 8 + }); + }, + + mounted() { + if (this.el.getAttribute("latlon") != "null") { + this.initMap(); + } + } } \ No newline at end of file diff --git a/friends/lib/friends/friend.ex b/friends/lib/friends/friend.ex index da7ae34..a238e9b 100644 --- a/friends/lib/friends/friend.ex +++ b/friends/lib/friends/friend.ex @@ -23,14 +23,16 @@ defmodule Friends.Friend do :relationships, Friends.Friend, join_through: Relationship, - join_keys: [friend_id: :id, relation_id: :id] + join_keys: [friend_id: :id, relation_id: :id], + on_delete: :delete_all ) many_to_many( :reverse_relationships, Friends.Friend, join_through: Relationship, - join_keys: [relation_id: :id, friend_id: :id] + join_keys: [relation_id: :id, friend_id: :id], + on_delete: :delete_all ) end @@ -46,10 +48,10 @@ defmodule Friends.Friend do :user_id, :address_id ]) - |> Ecto.Changeset.validate_required([:name, :email, :phone, :born], + |> Ecto.Changeset.validate_required([:name], message: "This field is required." ) - |> Ecto.Changeset.validate_format(:name, ~r/\w+\ \w+/, message: "Please enter your full name.") + # |> Ecto.Changeset.validate_format(:name, ~r/\w+\ \w+/, message: "Please enter your full name.") |> Ecto.Changeset.validate_format( :email, Regex.compile!("^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$"), @@ -88,7 +90,7 @@ defmodule Friends.Friend do end def new(params \\ %{}) do - %Friend{id: :new} + %Friend{id: nil} |> struct(params) end @@ -120,13 +122,13 @@ defmodule Friends.Friend do end def create(params \\ %{id: nil}) do - %Friend{} - |> Friend.changeset(%{params | id: nil}) + Friend.new() + |> Friend.changeset(params) |> Map.put(:action, :insert) end def update(params) do - Friend.get_by_id(params.id |> String.to_integer()) + Friend.get_by_id(params.id) |> Friend.changeset(params) |> Map.put(:action, :update) end @@ -156,12 +158,12 @@ defmodule Friends.Friend do def create_or_update(params) do case params.id do - "new" -> + nil -> params |> create() |> generate_slug() - |> create_birth_event() |> commit() + |> create_birth_event() _number -> params @@ -189,7 +191,8 @@ defmodule Friends.Friend do end def can_be_edited_by(friend, user) do - if user |> is_nil(), do: false, else: friend.id == user.profile.id + true + # if user |> is_nil(), do: false, else: friend.id == user.profile.id end def assign_address(%Friend{} = friend, address) do @@ -211,8 +214,10 @@ defmodule Friends.Friend do end def assign_user!(%Friend{} = friend) do - %{friend: new_friend, user: _user} = assign_user(friend) - new_friend + case assign_user(friend) do + %{friend: new_friend, user: _user} -> new_friend + nil -> friend + end end def load_user(%Friends.Friend{user: %Ecto.Association.NotLoaded{}} = model) do @@ -260,7 +265,7 @@ defmodule Friends.Friend do end def create_birth_event(%Friend{id: id} = friend) do - solo_relationship = Relationship.get_or_new(friend, friend) + solo_relationship = Relationship.get_or_create(friend, friend) Event.get_or_create(%{ name: "Born", @@ -270,4 +275,14 @@ defmodule Friends.Friend do relationship_id: solo_relationship.id }) end + + def create_relationship(friend1, friend2) do + rel = Relationship.get_or_create(friend1, friend2) + rel |> Relationship.members() + end + + def delete_relationship(friend1, friend2) do + Relationship.delete(friend1, friend2) + friend1.id |> Friend.get_by_id() + end end diff --git a/friends/lib/friends/friend/search.ex b/friends/lib/friends/friend/search.ex new file mode 100644 index 0000000..44e20b2 --- /dev/null +++ b/friends/lib/friends/friend/search.ex @@ -0,0 +1,44 @@ +defmodule Friends.Friend.Search do + alias Friends.Friend + import Ecto.Query + + @repo Friends.Repo + + def autocomplete(str, friend) do + @repo.all( + from( + f in Friend, + where: ilike(f.name, ^"%#{str}%") + ) + ) + |> remove_self(friend) + |> remove_existing(friend) + end + + defp remove_self(list, friend) do + list + |> Enum.filter(fn result -> + result.id != friend.id + end) + end + + defp remove_existing(list, friend) do + existing = + friend + |> Helpers.relations() + |> Enum.map(& &1.id) + + list + |> Enum.filter(fn result -> + result.id not in existing + end) + end + + def parse_result(friend) do + %{ + id: "friend-#{friend.id}", + value: friend.id, + name: friend.name + } + end +end diff --git a/friends/lib/friends/places/search.ex b/friends/lib/friends/places/search.ex index a7b173f..9874182 100644 --- a/friends/lib/friends/places/search.ex +++ b/friends/lib/friends/places/search.ex @@ -1,4 +1,7 @@ defmodule Friends.Places.Search do + import Ecto.Query + @repo Friends.Repo + def api_key, do: "pk.eyJ1IjoicnlhbnBhbmR5YSIsImEiOiJja3psM2tlcDA1MXl1Mm9uZmo5bGxpNzdxIn0.TwBKpTTypcD5fWFc8XRyHg" @@ -14,6 +17,22 @@ defmodule Friends.Places.Search do ] end + def known_places(str) do + @repo.all( + from( + p in Friends.Places.Place, + where: ilike(p.name, ^"%#{str}%") + ) + ) + |> Enum.map( + &%{ + "center" => &1.latlon, + "place_name" => &1.name, + "id" => "known.#{&1.id}" + } + ) + end + def autocomplete(str, region \\ nil) do viewbox = if region do @@ -25,8 +44,9 @@ defmodule Friends.Places.Search do response = HTTPoison.get!(url) results = Poison.decode!(response.body) + IO.inspect(results["features"]) - results["features"] + known_places(str) ++ results["features"] end def parse_features(%{ @@ -36,7 +56,7 @@ defmodule Friends.Places.Search do }) do %{ name: name, - latlon: lonlat |> Enum.reverse() |> Poison.encode!(), + value: lonlat |> Enum.reverse() |> Poison.encode!(), id: id |> String.replace(".", "-") } end diff --git a/friends/lib/friends/relationship.ex b/friends/lib/friends/relationship.ex index 20ca34b..1eee859 100644 --- a/friends/lib/friends/relationship.ex +++ b/friends/lib/friends/relationship.ex @@ -82,7 +82,7 @@ defmodule Friends.Relationship do rel_type = if id1 == id2, do: 0, else: type relationship = - @repo.insert(%Relationship{ + @repo.insert!(%Relationship{ friend_id: id1, relation_id: id2, type: rel_type @@ -122,6 +122,10 @@ defmodule Friends.Relationship do end end + def delete(a, b) do + get(a, b) |> Friends.Repo.delete!() + end + def get_by_slugs([slug1, slug2]) do friend1 = slug1 |> Friend.get_by_slug() friend2 = slug2 |> Friend.get_by_slug() diff --git a/friends/lib/friends_web/live/components/autocomplete.ex b/friends/lib/friends_web/live/components/autocomplete.ex index 0e10102..d1e639e 100644 --- a/friends/lib/friends_web/live/components/autocomplete.ex +++ b/friends/lib/friends_web/live/components/autocomplete.ex @@ -4,19 +4,29 @@ defmodule FriendsWeb.Components.Autocomplete do alias Phoenix.LiveView.JS + def bolden(string, substring) do + String.replace( + string, + substring, + "#{substring}" + ) + |> Phoenix.HTML.raw() + end + def search_results(assigns) do ~H""" -
+
<%= if @search_results do %> -
- - """ end end diff --git a/friends/lib/friends_web/live/components/cards.ex b/friends/lib/friends_web/live/components/cards.ex new file mode 100644 index 0000000..865b891 --- /dev/null +++ b/friends/lib/friends_web/live/components/cards.ex @@ -0,0 +1,25 @@ +defmodule FriendsWeb.Components.Cards do + use FriendsWeb, :live_component + + def relationship_card(assigns) do + ~H""" +
+
{@relation.id}
+
+

+ <.link navigate={Routes.friends_show_path(FriendsWeb.Endpoint, :overview, @relation.slug)} class=""><%=@relation.name%> + <%= if @relationship |> Friends.Relationship.get_relation do %> +
Friends.Relationship.get_color}"}><%= @relationship |> Friends.Relationship.get_relation %>
+ <% end %> +

+

If a dog chews shoes whose shoes does he choose?

+
+ + <%= if @editable do %> + Delete + <% end %> + +
+ """ + end +end diff --git a/friends/lib/friends_web/live/components/components.ex b/friends/lib/friends_web/live/components/components.ex index 4a49a90..8aba494 100644 --- a/friends/lib/friends_web/live/components/components.ex +++ b/friends/lib/friends_web/live/components/components.ex @@ -4,7 +4,7 @@ defmodule FriendsWeb.FriendsLive.Components do import Helpers import FriendsWeb.LiveHelpers alias Friends.Friend - alias FriendsWeb.Components.{Autocomplete, Map} + alias FriendsWeb.Components.{Autocomplete, Map, Cards} alias Phoenix.LiveView.JS def header(assigns) do @@ -48,7 +48,9 @@ defmodule FriendsWeb.FriendsLive.Components do def show_page(:overview, assigns) do ~H""" - + <%= if @address_latlon != "null" do %> + + <% end %>
  • Nickname: @@ -77,8 +79,12 @@ defmodule FriendsWeb.FriendsLive.Components do
  • Address: + <%= if @address_latlon == "null" do %> + none + <% else %>
    <%= @address %>
    - + <% end %> +
""" @@ -105,22 +111,11 @@ defmodule FriendsWeb.FriendsLive.Components do def show_page(:relationships, assigns) do ~H"""
- <%= for relation <- @friend |> relations do %> - <% relationship = relation(@friend, relation) %> -
-
{relation.id}
-
-

- <%= relation.name %> - <%= if relationship |> Friends.Relationship.get_relation do %> -
Friends.Relationship.get_color}"}><%= relationship |> Friends.Relationship.get_relation %>
- <% end %> -

-

If a dog chews shoes whose shoes does he choose?

-
-
+ <%= for relation <- @relationships do %> + <% relationship = relation(@friend, relation) %> + <% end %> - <%= if @friend |> relations |> Enum.empty? do %> + <%= if @relationships |> Enum.empty? do %>
No relationships on record yet.
<% end %>
@@ -160,14 +155,12 @@ defmodule FriendsWeb.FriendsLive.Components do
<%= error_tag f, :name %>
- <%= if @address_latlon do %> - - <% end %> +
@@ -234,6 +231,41 @@ defmodule FriendsWeb.FriendsLive.Components do def edit_page(:relationships, assigns) do ~H""" + <%= show_page(:relationships, assigns) %> + + <.form for={@changeset} let={f}> +
    +
  • + Type a name: +
    + <%= text_input f, :search_query, value: @search_query, + class: "input input-primary input-sm md:input-md", + phx_debounce: "500", + phx_change: :relation_search, + phx_click: JS.show(to: "#search-results"), + phx_blur: JS.hide(to: "#search-results"), + autocomplete: "name" %> + <%= hidden_input f, :relation_id, value: @relation_id, + id: "relation-id", autocomplete: "relation-id", + phx_change: "validate" + %> + +
    +
  • +
+
+ <%= if @live_action != :welcome do %> +
+ <.link patch={Routes.friends_show_path(FriendsWeb.Endpoint, :overview, @friend.slug)} class="btn btn-block btn-outline">back +
+ <% end %> + +
+ """ end diff --git a/friends/lib/friends_web/live/edit.ex b/friends/lib/friends_web/live/edit.ex index 25ebe2b..e962678 100644 --- a/friends/lib/friends_web/live/edit.ex +++ b/friends/lib/friends_web/live/edit.ex @@ -28,6 +28,7 @@ defmodule FriendsWeb.FriendsLive.Edit do live_action = socket.assigns.live_action || :overview friend = Friend.get_by_slug(slug) + editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) if(live_action) do {:ok, @@ -35,18 +36,24 @@ defmodule FriendsWeb.FriendsLive.Edit do |> assign(:live_action, live_action) |> assign_current_user(token |> Map.get("user_token")) |> assign(:friend, friend) + |> assign(:editable, editable) |> assign(:action, Routes.friends_path(socket, :update)) |> title(friend.name <> " - " <> (live_action |> titlecase)) |> assign(:changeset, %Friend{} |> Friend.changeset()) - |> assign(:address_query, nil) + |> assign(:search_query, nil) |> assign(:search_results, nil)} else {:ok, socket |> redirect(to: Routes.friends_show_path(socket, :overview, friend.slug))} end end - def handle_params(%{"slug" => slug} = _attrs, _url, socket) do - live_action = socket.assigns.live_action || false + # Overview form page + def handle_params( + %{"slug" => slug} = _attrs, + _url, + %{assigns: %{live_action: :overview}} = socket + ) do + live_action = socket.assigns.live_action friend = Friend.get_by_slug(slug) editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) @@ -58,13 +65,57 @@ defmodule FriendsWeb.FriendsLive.Edit do |> assign_friend(friend) |> assign(:action, Routes.friends_path(socket, :update)) |> assign(:live_action, live_action) - |> assign(:address_query, address_query) + |> assign(:search_query, address_query) |> assign(:address_latlon, address_latlon |> Poison.encode!()) |> assign(:search_results, nil) |> title(friend.name <> " - " <> (live_action |> titlecase)) |> assign(:editable, editable)} end + # Add a relationship + def handle_params( + %{"slug" => slug} = _attrs, + _url, + %{assigns: %{live_action: :relationships}} = socket + ) do + live_action = socket.assigns.live_action + + friend = Friend.get_by_slug(slug) + editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) + + {:noreply, + socket + |> assign_friend(friend) + |> assign(:relationships, friend |> relations) + |> assign(:live_action, live_action) + |> assign(:search_query, nil) + |> assign(:relation_id, nil) + |> assign(:search_results, nil) + |> assign(:editable, editable) + |> title(friend.name <> " - " <> (live_action |> titlecase))} + end + + # Timeline edit + def handle_params( + %{"slug" => slug} = _attrs, + _url, + %{assigns: %{live_action: :timeline}} = socket + ) do + live_action = socket.assigns.live_action + + friend = Friend.get_by_slug(slug) + + {:noreply, + socket + |> assign_friend(friend) + |> assign(:live_action, live_action) + |> assign(:search_query, nil) + |> assign(:relation_id, nil) + |> assign(:search_results, nil) + |> title(friend.name <> " - " <> (live_action |> titlecase))} + end + + # Catch-all (aka, new friend form) def handle_params(_attrs, _token, socket) do friend = Friend.new() @@ -77,7 +128,12 @@ defmodule FriendsWeb.FriendsLive.Edit do |> title("Welcome")} end - def handle_event("validate", %{"friend" => form_params}, %{assigns: %{friend: friend}} = socket) do + # Validate overview form page + def handle_event( + "validate", + %{"friend" => form_params}, + %{assigns: %{friend: friend, live_action: :overview}} = socket + ) do id = form_params["id"] |> parse_id name = form_params["name"] nickname = form_params["nickname"] @@ -126,7 +182,7 @@ defmodule FriendsWeb.FriendsLive.Edit do slug = form_params["slug"] || name |> to_slug id = form_params["id"] || :new address_latlon = form_params["address_latlon"] |> Poison.decode!() - address_query = form_params["address_query"] + address_query = form_params["search_query"] address = %Friends.Places.Place{ name: address_query, @@ -134,8 +190,14 @@ defmodule FriendsWeb.FriendsLive.Edit do } new_address = - address - |> Friends.Places.Place.get_or_create() + case address.latlon do + nil -> + %{id: nil} + + _ -> + address + |> Friends.Places.Place.get_or_create() + end new_params = %{ id: id, @@ -161,17 +223,61 @@ defmodule FriendsWeb.FriendsLive.Edit do } end - def handle_event("address_search", %{"friend" => %{"address_query" => query}}, socket) do + def handle_event("address_search", %{"friend" => %{"search_query" => query}}, socket) do results = Places.Search.autocomplete(query) |> Enum.map(&Places.Search.parse_features/1) if query == "" do {:noreply, socket |> assign(:search_results, nil)} else - {:noreply, socket |> assign(:search_results, results)} + {:noreply, + socket + |> assign(:search_results, results) + |> assign(:search_query, query) + |> assign(:select_fxn, "selectMapResult")} end end - def handle_event(event, unsigned_params, socket) do + def handle_event( + "relation_search", + %{"friend" => %{"search_query" => query}}, + %{assigns: %{friend: friend}} = socket + ) do + if query == "" do + {:noreply, socket |> assign(:search_results, nil)} + else + results = + (Friend.Search.autocomplete(query, friend) ++ + [Friend.new(%{name: query})]) + |> Enum.map(&Friend.Search.parse_result/1) + + {:noreply, + socket + |> assign(:search_results, results) + |> assign(:search_query, query) + |> assign(:select_fxn, "selectRelation")} + end + end + + def handle_event( + "select_relation", + %{"id" => rel_id, "name" => rel_name}, + %{assigns: %{friend: friend, relationships: relationships}} = socket + ) do + new_rel = + case rel_id do + "new" -> Friend.create(%{name: rel_name}) + _num -> Friend.get_by_id(rel_id |> String.to_integer()) + end + + [updated_friend, updated_relation] = friend |> Friend.create_relationship(new_rel) + + {:noreply, + socket + |> assign_friend(updated_friend) + |> assign(:relationships, updated_friend |> relations)} + end + + def handle_event(_event, _unsigned_params, socket) do {:noreply, socket} end end diff --git a/friends/lib/friends_web/live/show.ex b/friends/lib/friends_web/live/show.ex index dfa5377..ee17540 100644 --- a/friends/lib/friends_web/live/show.ex +++ b/friends/lib/friends_web/live/show.ex @@ -19,7 +19,7 @@ defmodule FriendsWeb.FriendsLive.Show do |> assign_current_user(token |> Map.get("user_token")) |> assign(:friend, friend) |> assign(:address, address) - |> assign(:latlon, latlon |> Poison.encode!()) + |> assign(:address_latlon, latlon |> Poison.encode!()) |> title(friend.name <> " - " <> (live_action |> titlecase)) |> assign(:changeset, %Friend{} |> Friend.changeset()) |> assign(:editable, editable) @@ -29,16 +29,35 @@ defmodule FriendsWeb.FriendsLive.Show do end end - def handle_params(%{"slug" => slug} = attrs, _url, socket) do - live_action = socket.assigns.live_action || false - + def handle_params( + %{"slug" => slug} = attrs, + _url, + %{assigns: %{friend: friend, live_action: :overview}} = socket + ) do friend = Friend.get_by_slug(slug) editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) + live_action = socket.assigns.live_action {:noreply, socket |> assign_friend(friend) - |> assign(:live_action, live_action) + |> title(friend.name <> " - " <> (live_action |> titlecase)) + |> assign(:editable, editable)} + end + + def handle_params( + %{"slug" => slug} = attrs, + _url, + %{assigns: %{friend: friend, live_action: :relationships}} = socket + ) do + friend = Friend.get_by_slug(slug) + editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) + live_action = socket.assigns.live_action + + {:noreply, + socket + |> assign_friend(friend) + |> assign(:relationships, friend |> Helpers.relations()) |> title(friend.name <> " - " <> (live_action |> titlecase)) |> assign(:editable, editable)} end diff --git a/friends/lib/friends_web/views/live_helpers.ex b/friends/lib/friends_web/views/live_helpers.ex index 731db1c..ff7e795 100644 --- a/friends/lib/friends_web/views/live_helpers.ex +++ b/friends/lib/friends_web/views/live_helpers.ex @@ -84,7 +84,6 @@ defmodule FriendsWeb.LiveHelpers do socket |> assign(:friend, friend) |> assign(:changeset, friend |> Friend.changeset()) - |> assign(:relationships, friend.relationships) end # Same thing, but this time we have a changeset we want to keep @@ -92,6 +91,5 @@ defmodule FriendsWeb.LiveHelpers do socket |> assign(:friend, friend) |> assign(:changeset, changeset) - |> assign(:relationships, friend.relationships) end end diff --git a/friends/lib/helpers/helpers.ex b/friends/lib/helpers/helpers.ex index 457fb00..0d9fd39 100644 --- a/friends/lib/helpers/helpers.ex +++ b/friends/lib/helpers/helpers.ex @@ -90,4 +90,32 @@ defmodule Helpers do def events(%Friends.Relationship{} = relationship) do relationship.events end + + def random_string(length) do + :crypto.strong_rand_bytes(length) + |> Base.url_encode64() + |> binary_part(0, length) + |> String.replace(~r/-/, "") + end + + def unique_friend_email, do: "user#{System.unique_integer()}@example.com" + def valid_friend_name, do: "#{random_string(5)} Mc#{random_string(5)}" + def valid_friend_phone, do: "+1 (917) 624 2939" |> Helpers.format_phone() + def valid_friend_birthdate, do: ~D"1990-05-05" + + def valid_friend_attributes(attrs \\ %{}) do + Enum.into(attrs, %{ + id: nil, + name: valid_friend_name(), + phone: valid_friend_phone(), + born: valid_friend_birthdate(), + email: unique_friend_email() + }) + end + + def friend_fixture(attrs \\ %{}) do + attrs + |> valid_friend_attributes() + |> Friends.Friend.create_or_update() + end end