Compare commits

...
3 Commits
Author SHA1 Message Date
Ryan Pandya b9c7cb7b84 Lots of changes! 2022-11-27 20:59:46 -05:00
Ryan Pandya b85e3cc96c Refactor & fix friend search + address search 2022-11-26 00:16:31 -05:00
Ryan Pandya fd5bcbfc05 solo relationships, birth events, formatting 2022-11-23 17:42:49 -05:00
24 changed files with 1139 additions and 491 deletions
+65 -4
View File
@@ -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"
@@ -35,7 +35,7 @@ let liveSocket = new LiveSocket("/live", Socket, {
})
// 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())
@@ -53,7 +53,7 @@ window.hideElement = function (id) {
el.hidden = true;
}
window.selectResult = function (latlon, display) {
window.selectMapResult = function (latlon, display) {
var name_el = document.querySelector("[autocomplete=name]");
var latlon_el = document.querySelector("[autocomplete=latlon]");
@@ -63,6 +63,65 @@ window.selectResult = function (latlon, display) {
window.liveSocket.hooks.showMapbox.initMap();
};
window.selectRelation = function (id, name) {
var e = new Event('selectRelation');
e.data = {
id: id,
name: name
}
window.dispatchEvent(e);
}
window.deleteRelation = function (id) {
var e = new Event('deleteRelation');
e.data = {
id: id
}
window.dispatchEvent(e);
}
window.relationType = function (rel_id, type) {
var e = new Event('relationType');
e.data = {
rel_id: rel_id,
type: type
}
window.dispatchEvent(e);
}
Hooks.NewRelation = {
mounted() {
var list_el = document.querySelector("div#relationships");
window.addEventListener("selectRelation", e => {
this.pushEvent("phx:select_relation", e.data, function (reply) {
console.log(reply);
})
})
}
}
Hooks.RelationshipCard = {
mounted() {
console.log("Mounted card for relationship " + this.el.getAttribute("relationship-id"));
window.addEventListener("deleteRelation", e => {
this.pushEvent("phx:delete_relation", e.data, function (reply) {
console.log(reply);
})
})
window.addEventListener("relationType", e => {
this.pushEvent("phx:relation_type", e.data, function (reply) {
document.querySelector("#type-selector-" + e.data.rel_id).hidden = true;
})
})
}
}
Hooks.showMapbox = {
initMap() {
mapboxgl.accessToken = 'pk.eyJ1IjoicnlhbnBhbmR5YSIsImEiOiJja3psM2tlcDA1MXl1Mm9uZmo5bGxpNzdxIn0.TwBKpTTypcD5fWFc8XRyHg';
@@ -78,6 +137,8 @@ Hooks.showMapbox = {
},
mounted() {
if (this.el.getAttribute("latlon") != "null") {
this.initMap();
}
}
}
+134 -10
View File
@@ -1,6 +1,7 @@
defmodule Friends.Event do
use Ecto.Schema
alias Friends.{Relationship,Event}
import Ecto.Query
alias Friends.{Relationship, Event}
alias Places.Place
@repo Friends.Repo
@@ -16,33 +17,157 @@ defmodule Friends.Event do
belongs_to(:relationship, Relationship)
end
def changeset(event, params \\ %{}) do
event
|> Ecto.Changeset.cast(params, [
:name,
:date,
:story,
:defining,
:solo,
:place_id,
:relationship_id
])
|> Ecto.Changeset.validate_required([:name, :date, :solo, :relationship_id],
message: "This field is required."
)
|> validate_date_in_past()
|> validate_unique_event()
end
defp validate_unique_event(changeset) do
changeset
end
defp validate_date_in_past(%{changes: %{date: date}} = changeset) do
today = DateTime.utc_now() |> DateTime.to_date()
case date |> Date.diff(today) do
age when age < 0 ->
changeset |> Ecto.Changeset.add_error(:born, "Please enter a date in the past.")
_ ->
changeset
end
end
defp validate_date_in_past(changeset), do: changeset
def new(params \\ %{}) do
%Event{id: nil}
|> struct(params)
end
def get_by_id(id) do
@repo.one(
from(e in Event,
where: e.id == ^id,
preload: [:relationship, :place]
)
)
end
def get(%{relationship_id: rel, date: date, name: name}) do
@repo.one(
from(e in Event,
where: e.relationship_id == ^rel and e.date == ^date and e.name == ^name,
preload: [:relationship, :place]
)
)
end
def get_or_create(params) do
case get(params) do
nil -> create(params) |> commit()
event -> event
end
end
def create(params \\ %{id: nil}) do
Event.new(params)
|> Event.changeset()
|> Map.put(:action, :insert)
end
def update(params) do
Event.get_by_id(params.id |> String.to_integer())
|> Event.changeset(params)
|> Map.put(:action, :update)
end
def commit(changeset) do
changeset
|> @repo.commit!
|> load_preloads
end
def load_preloads(
%Event{
place: %Ecto.Association.NotLoaded{},
relationship: %Ecto.Association.NotLoaded{}
} = model
) do
model
|> @repo.preload([:place, :relationship])
end
def load_preloads(event), do: event
def create_or_update(params) do
case params.id do
"new" ->
params
|> create()
|> commit()
_number ->
params
|> update()
|> commit()
end
end
def meet(friend1, friend2, opts \\ nil) do
relationship = Relationship.get_or_new(friend1, friend2)
opts = opts ++ [if opts[:place] do
opts =
opts ++
[
if opts[:place] do
{
:place_id,
Places.Place.get_or_new(opts[:place]).id
}
end]
{:ok, event} = %Event{
end
]
{:ok, event} =
%Event{
story: opts[:story],
date: opts[:date],
place_id: opts[:place_id],
relationship_id: relationship.id
} |> @repo.insert
}
|> @repo.insert
event
end
def person(event) do
Friends.Friend.get_by_id(event.relationship.friend_id)
end
def people(event) do
event.relationship |> Relationship.members
if event.solo, do: event.person, else: event.relationship |> Relationship.members()
end
def age(event) do
years = Date.diff(Date.utc_today, event.date)
years =
Date.diff(Date.utc_today(), event.date)
|> div(365)
if years == 0 do
months = Date.diff(Date.utc_today, event.date)
|> rem(365) |> div(12)
months = Date.diff(Date.utc_today(), event.date) |> rem(365) |> div(12)
{months, :month}
else
{years, :year}
@@ -52,5 +177,4 @@ defmodule Friends.Event do
def print_age(age) do
Friends.Helpers.pluralize(age |> elem(0), age |> elem(1))
end
end
+56 -21
View File
@@ -1,7 +1,7 @@
defmodule Friends.Friend do
use Ecto.Schema
alias Friends.{Relationship, Friend}
alias Friends.{Relationship, Friend, Event}
import Helpers
import Ecto.Query
@@ -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,8 +48,10 @@ defmodule Friends.Friend do
:user_id,
:address_id
])
|> Ecto.Changeset.validate_required([:name, :email, :phone, :born], message: "This field is required.")
|> Ecto.Changeset.validate_format(:name, ~r/\w+\ \w+/, message: "Please enter your full name.")
|> 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(
:email,
Regex.compile!("^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$"),
@@ -64,15 +68,20 @@ defmodule Friends.Friend do
end
defp validate_birthdate(%{changes: %{born: born}} = changeset) do
today = DateTime.utc_now |> DateTime.to_date
today = DateTime.utc_now() |> DateTime.to_date()
case born |> Date.diff(today) |> div(-365) do
age when age < 0 ->
changeset |> Ecto.Changeset.add_error(:born, "Please enter a date in the past.")
age when age > 90 ->
changeset |> Ecto.Changeset.add_error(:born, "Are you sure you're #{age} years old?")
_ -> changeset
_ ->
changeset
end
end
defp validate_birthdate(changeset), do: changeset
def all() do
@@ -81,7 +90,7 @@ defmodule Friends.Friend do
end
def new(params \\ %{}) do
%Friend{id: :new}
%Friend{id: nil}
|> struct(params)
end
@@ -113,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
@@ -149,11 +158,12 @@ defmodule Friends.Friend do
def create_or_update(params) do
case params.id do
"new" ->
nil ->
params
|> create()
|> generate_slug
|> generate_slug()
|> commit()
|> create_birth_event()
_number ->
params
@@ -162,17 +172,17 @@ defmodule Friends.Friend do
end
end
def get_relationships(friend) do
def get_relationships(friend, include_self \\ false) do
friend
|> relations
|> relations(include_self)
|> Enum.map(&relation(friend, &1))
end
def get_events(friend) do
friend
|> get_relationships
|> Enum.map(& &1.events)
|> List.flatten()
|> get_relationships(:all)
|> Enum.flat_map(& &1.events)
|> Enum.dedup()
end
def age(friend) do
@@ -181,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
@@ -203,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
@@ -250,4 +263,26 @@ defmodule Friends.Friend do
{nil, nil}
end
end
def create_birth_event(%Friend{id: id} = friend) do
solo_relationship = Relationship.get_or_create(friend, friend)
Event.get_or_create(%{
name: "Born",
date: friend.born,
solo: true,
defining: true,
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
+44
View File
@@ -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
+10 -1
View File
@@ -26,8 +26,17 @@ defmodule Friends.Places.Place do
end
def get_or_create(place) do
case @repo.one(
from(
p in Friends.Places.Place,
where: p.name == ^place.name
)
) do
nil ->
place
|> Friends.Places.Place.validate()
|> Friends.Repo.insert!()
|> @repo.insert!()
found -> found
end
end
end
+22 -2
View File
@@ -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
+103 -26
View File
@@ -1,9 +1,10 @@
defmodule Friends.Relationship do
use Ecto.Schema
import Ecto.Query
alias Friends.{Relationship,Friend}
alias Friends.{Relationship, Friend}
@repo Friends.Repo
@default_type 3
schema "relationships" do
field(:friend_id, :id)
@@ -13,7 +14,7 @@ defmodule Friends.Relationship do
has_many(:events, Friends.Event)
end
@attrs [:friend_id, :relation_id]
@attrs [:friend_id, :relation_id, :type]
def types(index) do
types() |> elem(index)
@@ -22,11 +23,12 @@ defmodule Friends.Relationship do
def types do
# Tuple: name of the type, associated color, and what that person "is" to the other
{
{:acquaintances, :info, nil},
{:self, :hidden, :self},
{:acquaintances, :info, :known},
{:family, :primary, :relative},
{:friends, :secondary, :friend},
{:partners, :info, :partner},
{:dating, :success, :date},
{:dating, :success, :dating},
{:engaged, :success, :fiancé},
{:married, :success, :spouse},
{:divorced, :error, :ex},
@@ -35,6 +37,16 @@ defmodule Friends.Relationship do
}
end
def type_index(type) do
types()
|> Tuple.to_list()
|> Enum.find_index(
&(&1
|> elem(0)
|> to_string() == type)
)
end
def get_type(rel) do
rel.type |> types |> elem(0)
end
@@ -55,9 +67,27 @@ defmodule Friends.Relationship do
)
end
def validate_type(%{changes: %{type: type}} = changeset) do
if type |> is_integer() and type >= 0 and type < types() |> Tuple.to_list() |> length do
changeset
else
changeset |> Ecto.Changeset.add_error(:type, "Invalid type")
end
end
def validate_type(changeset), do: changeset
def update(rel, params \\ %{}) do
rel
|> changeset(params)
|> Map.put(:action, :update)
|> @repo.update!()
end
def changeset(struct, params \\ %{}) do
struct
|> Ecto.Changeset.cast(params, @attrs)
|> validate_type
|> Ecto.Changeset.unique_constraint(
[:friend_id, :relation_id],
name: :relationships_friend_id_relation_id_index
@@ -68,34 +98,39 @@ defmodule Friends.Relationship do
)
end
def all() do
preloads = []
@repo.all(from(r in Friends.Relationship, preload: ^preloads))
@repo.all(from(r in Friends.Relationship, where: r.type != 0, preload: ^preloads))
end
def new(friend1, friend2, type \\ 2) do
def new(friend1, friend2, type \\ @default_type) do
id1 = friend1.id
id2 = friend2.id
{:ok, relationship} = @repo.insert(
%Relationship{
rel_type = if id1 == id2, do: 0, else: type
relationship =
@repo.insert!(%Relationship{
friend_id: id1,
relation_id: id2,
type: type
}
)
type: rel_type
})
relationship
end
def get(friend1, friend2) do
id1 = friend1.id
id2 = friend2.id
rel = @repo.one(
rel =
@repo.one(
from(r in Relationship,
where: r.friend_id == ^id1 and r.relation_id == ^id2,
preload: [:events]
)
)
if rel == nil do
@repo.one(
from(r in Relationship,
@@ -108,16 +143,41 @@ defmodule Friends.Relationship do
end
end
def get_or_new(a,b) do
def get_or_create(a, b) do
case get(a, b) do
nil -> new(a,b)
nil -> new(a, b)
relationship -> relationship
end
end
def get_by_id(id) do
case @repo.one(
from(r in Relationship,
where: r.id == ^id
)
) do
nil -> nil
rel -> rel |> load_preloads()
end
end
def delete(rel) do
rel |> Friends.Repo.delete!()
end
def delete(a, b) do
get(a, b) |> delete
end
def change_type(rel, type) do
rel
|> changeset(%{type: type})
|> update()
end
def get_by_slugs([slug1, slug2]) do
friend1 = slug1 |> Friend.get_by_slug
friend2 = slug2 |> Friend.get_by_slug
friend1 = slug1 |> Friend.get_by_slug()
friend2 = slug2 |> Friend.get_by_slug()
get(friend1, friend2)
end
@@ -130,23 +190,40 @@ defmodule Friends.Relationship do
end
def age(relationship) do
relationship.events
|> Enum.map(fn(event) ->
Date.diff(Date.utc_today, event.date)
end) |> Enum.sort |> List.last |> div(365)
case relationship.events do
[] ->
nil
e ->
e
|> Enum.map(fn event ->
Date.diff(Date.utc_today(), event.date)
end)
|> Enum.sort()
|> List.last()
|> div(365)
end
end
def load_events(%Relationship{
events: %Ecto.Association.NotLoaded{}} = model) do
def load_events(
%Relationship{
events: %Ecto.Association.NotLoaded{}
} = model
) do
model
|> @repo.preload([:events])
end
def load_events(%Relationship{} = r), do: r
def load_preloads(%Relationship{
events: %Ecto.Association.NotLoaded{}} = model) do
def load_preloads(
%Relationship{
events: %Ecto.Association.NotLoaded{}
} = model
) do
model
|> @repo.preload([:events])
end
def load_preloads(%Relationship{} = r), do: r
def load_preloads(%Relationship{} = r), do: r
end
+23 -111
View File
@@ -24,6 +24,10 @@ defmodule FriendsWeb do
import Plug.Conn
import FriendsWeb.Gettext
alias FriendsWeb.Router.Helpers, as: Routes
alias Friends.{Friend, Relationship}
alias Friends.Accounts.User
import Helpers
end
end
@@ -48,6 +52,15 @@ defmodule FriendsWeb do
use Phoenix.LiveView,
layout: {FriendsWeb.LayoutView, "live.html"}
alias FriendsWeb.Router.Helpers, as: Routes
import Helpers
import Helpers.Names
import FriendsWeb.LiveHelpers
import FriendsWeb.Components
alias Friends.{Friend, Relationship, Places}
unquote(view_helpers())
end
end
@@ -55,6 +68,14 @@ defmodule FriendsWeb do
def live_component do
quote do
use Phoenix.LiveComponent
import Helpers
import FriendsWeb.LiveHelpers
alias Friends.{Friend, Relationship, Places}
alias FriendsWeb.Components.{Autocomplete, Map, Cards}
alias FriendsWeb.Components
alias FriendsWeb.Router.Helpers, as: Routes
alias FriendsWeb.LiveViews
alias Phoenix.LiveView.JS
unquote(view_helpers())
end
@@ -63,117 +84,8 @@ defmodule FriendsWeb do
def component do
quote do
use Phoenix.Component
unquote(view_helpers())
end
end
def router do
quote do
use Phoenix.Router
import Phoenix.Component
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
def channel do
quote do
use Phoenix.Channel
import FriendsWeb.Gettext
end
end
defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import FriendsWeb.ErrorHelpers
import FriendsWeb.Gettext
alias FriendsWeb.Router.Helpers, as: Routes
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
defmodule FriendsWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
This can be used in your application as:
use FriendsWeb, :controller
use FriendsWeb, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define any helper function in modules
and import those modules here.
"""
def controller do
quote do
use Phoenix.Controller, namespace: FriendsWeb
import Plug.Conn
import FriendsWeb.Gettext
alias FriendsWeb.Router.Helpers, as: Routes
end
end
def view do
quote do
use Phoenix.View,
root: "lib/friends_web/templates",
namespace: FriendsWeb
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
import Phoenix.Component
# Include shared imports and aliases for views
unquote(view_helpers())
end
end
def live_view do
quote do
use Phoenix.LiveView,
layout: {FriendsWeb.LayoutView, "live.html"}
unquote(view_helpers())
end
end
def live_component do
quote do
use Phoenix.LiveComponent
unquote(view_helpers())
end
end
def component do
quote do
use Phoenix.Component
import Helpers
import FriendsWeb.LiveHelpers
unquote(view_helpers())
end
@@ -1,8 +1,5 @@
defmodule FriendsWeb.FriendsController do
use FriendsWeb, :controller
alias Friends.{Friend, Relationship}
alias Friends.Accounts.User
import Helpers
def index(conn, _params) do
conn
@@ -0,0 +1,9 @@
defmodule FriendsWeb.RelationshipsController do
use FriendsWeb, :controller
def delete(id) do
rel = Relationship.get_by_id(id)
IO.inspect("Deleting #{rel}")
end
end
@@ -4,19 +4,29 @@ defmodule FriendsWeb.Components.Autocomplete do
alias Phoenix.LiveView.JS
def bolden(string, substring) do
String.replace(
string,
substring,
"<b>#{substring}</b>"
)
|> Phoenix.HTML.raw()
end
def search_results(assigns) do
~H"""
<div id="search-results" class="absolute w-full md:w-max bottom-16">
<div id="search-results" class="absolute w-full bottom-16 left-0">
<%= if @search_results do %>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-2/3 overflow-auto">
<ul tabindex="0" class="dropdown-content menu p-0 m-0 shadow bg-base-200 rounded-box overflow-auto">
<%= for r <- @search_results do %>
<li class="py-0 my-0">
<li class="p-0 m-0">
<.link id={r[:id]}
latlon={r[:latlon]}
phx_value={r[:value]}
class="search_result"
onMouseDown={"selectResult('#{r[:latlon]}', '#{r[:name]}')"}>
<%= r[:name] %>
phx-hook="NewRelation"
onMouseDown={"#{@select_fxn}('#{r[:value]}', '#{r[:name]}')"}>
<%= r[:name] |> bolden(@search_query) %>
</.link>
</li>
<% end %>
@@ -24,29 +34,6 @@ defmodule FriendsWeb.Components.Autocomplete do
<% end %>
</div>
<script language="javascript">
window.elSearch_results = document.querySelector("#search-results");
window.elSearchInput = document.querySelector("input[autocomplete=value]");
window.elSearchResultID = document.querySelector("input[autocomplete=id]");
window.hideResults = function () {
window.elSearch_results.classList.add("hidden");
}
window.showResults = function () {
document.getElementById("search-results").classList.remove("hidden");
}
window.selectSearchResult = function (result) {
const elSelectedResult = document.querySelector("#" + result);
const searchResultID = elSelectedResult.attributes.id.value;
const display = elSelectedResult.innerText;
window.elSearchInput.value = display;
window.elSearchResultID.value = searchResultID;
}
</script>
"""
end
end
@@ -0,0 +1,43 @@
defmodule FriendsWeb.Components.Cards do
use FriendsWeb, :live_component
import Helpers.Names
def relationship_card(assigns) do
~H"""
<div id={"relation-#{@relation.id}"} class="relative overflow-visible card card-compact w-96 bg-base-100 shadow-xl" phx-hook="RelationshipCard" relationship-id={@relationship.id}>
<%= if @editable do %>
<!-- The button to open modal -->
<label for={"delete-relationship-#{@relation.id}"} class="btn btn-error absolute top-2 right-2">delete</label>
<% end %>
<figure class="p-0 m-0"><img class="py-0 my-0" src="https://placeimg.com/400/225/people" alt={@relation.id} /></figure>
<div class="card-body">
<div class="flex flex-row justify-between items-center">
<h2 class="card-title py-0 my-0"><.link navigate={Routes.friends_show_path(FriendsWeb.Endpoint, :overview, @relation.slug)} class="no-underline font-bold hover:underline"><%=@relation.name%></.link></h2>
<.link patch={Routes.relationship_show_path(FriendsWeb.Endpoint, :overview, @friend.slug, @relation.slug)}>(details)</.link>
</div>
<Components.relationship_details editable={@editable} relationship={@relationship} />
</div>
</div>
<input type="checkbox" id={"delete-relationship-#{@relation.id}"} class="modal-toggle" />
<div class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<h3 class="font-bold text-lg mt-0 pt-0">Are you sure you want to delete <%=@friend |> first_name%>'s relationship with <%=@relation |> first_name%>?</h3>
<p class="py-4">Unless these two people really don't know each other, you probably want to change the relationship type, e.g. from "dating" to "ex".</p>
<div class="modal-action">
<label for={"delete-relationship-#{@relation.id}"} class="btn btn-sm btn-ghost">Never mind</label>
<label for={"delete-relationship-#{@relation.id}"} class="btn btn-sm btn-primary" onClick={"javascript:deleteRelation(#{@relationship.id})"}>Accept</label>
</div>
</div>
</div>
"""
end
def confirm_dialog(id) do
JS.show(to: "#warning-relation-#{id}")
end
end
@@ -1,10 +1,5 @@
defmodule FriendsWeb.FriendsLive.Components do
defmodule FriendsWeb.Components do
use FriendsWeb, :live_component
use Phoenix.HTML
import Helpers
alias Friends.Friend
alias FriendsWeb.Components.{Autocomplete, Map}
alias Phoenix.LiveView.JS
def header(assigns) do
~H"""
@@ -43,200 +38,73 @@ defmodule FriendsWeb.FriendsLive.Components do
"""
end
def show_page(:main, assigns), do: show_page(:overview, %{assigns | live_action: :overview})
def show_page(:overview, assigns) do
def relationship_details(assigns) do
~H"""
<Map.show address_latlon={@latlon} />
<ul class="py-4 pl-0 md:text-xl h-1/2">
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Nickname:</strong>
<div class="">
<%= if is_nil(@friend.nickname) do %>
<span class="italic">none</span>
<div class="flex flex-row items-center gap-3">
<div class="dropdown" id={"type-selector-#{@relationship.id}"}>
<label tabindex="0" class={"hover:badge-ghost badge badge-#{@relationship |> Friends.Relationship.get_color} m-1 text-white"}>
<%= @relationship |> Friends.Relationship.get_relation %>
<%= if @editable do %>
&nbsp;
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/> </svg>
<% end %>
</label>
<Components.relationship_type_selector relationship={@relationship} />
</div>
<span>since</span>
<%= if @relationship |> Relationship.age do %>
<%=
@relationship |> Relationship.age
%>
<% else %>
<%= @friend.nickname %>
<% end %>
<div class={"tooltip tooltip-#{@relationship |> Relationship.get_color}"} data-tip="add milestone dates on the detail page.">
<a class="hover:cursor-help" style="text-decoration-style:dashed">(no date)</a>
</div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Birthday:</strong>
<div class=""><%= @friend.born |> Calendar.strftime("%B %d, %Y") %>
<br class="md:hidden"/>
<span class="font-light">(<%= @friend |> Friend.age %> years old)</span>
</div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Email:</strong>
<div class=""><%= @friend.email %></div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Phone:</strong>
<div class=""><%= @friend.phone %></div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Address:</strong>
<div class=""><%= @address %></div>
<input type="hidden" autocomplete="latlon" value={@latlon}/>
</li>
</ul>
"""
end
def show_page(:timeline, assigns) do
~H"""
<div id="timeline" class="flex md:flex-row flex-col gap-8 p-8">
<%= for event <- @friend |> Friends.Friend.get_events do %>
<ul>
<li>
<%= event.name %>
</li>
</ul>
<% end %>
<%= if @friend |> Friends.Friend.get_events |> Enum.empty? do %>
<div class="italic">None yet.</div>
<% end %>
</div>
"""
end
def show_page(:relationships, assigns) do
def relationship_type_selector(assigns) do
~H"""
<div id="relationships" class="flex md:flex-row flex-col gap-8">
<%= for relation <- @friend |> relations do %>
<% relationship = relation(@friend, relation) %>
<div id={"relation-#{relation.id}"} class="card card-compact w-96 bg-base-100 shadow-xl">
<figure><img src="https://placeimg.com/400/225/people" alt={relation.id} /></figure>
<div class="card-body">
<h3 class="card-title">
<%= relation.name %>
<%= if relationship |> Friends.Relationship.get_relation do %>
<div class={"badge badge-#{relationship |> Friends.Relationship.get_color}"}><%= relationship |> Friends.Relationship.get_relation %></div>
<% end %>
</h3>
<p>If a dog chews shoes whose shoes does he choose?</p>
</div>
</div>
<% end %>
<%= if @friend |> relations |> Enum.empty? do %>
<div class="italic p-4">No relationships on record yet.</div>
<% end %>
</div>
<ul tabindex="0" class="absolute dropdown-content menu p-0 shadow bg-base-100 rounded-box w-52 justify-start">
<%= Relationship.types()
|> Tuple.to_list()
|> Enum.map(fn(tuple) ->
type = tuple |> elem(0)
class = tuple |> elem(1) |> to_string
selected = (@relationship |> Relationship.get_type) == type
if class != "hidden" do
if selected do
"<li class='p-2 m-0 text-sm bg-slate-200 select-none' style='font-weight:normal;'>#{type}</li>"
else
"<li
class='p-2 m-0 text-sm hover:bg-slate-400 hover:text-black hover:cursor-pointer'
style='font-weight:normal;'
onClick='#{relationship_type_function(@relationship.id, type)}'
>
#{type}
</li>"
end
end
end) |> Enum.join
|> raw
%>
</ul>
"""
end
def relationship_type_function(id, type) do
"""
relationType("#{id}", "#{type}")
"""
end
###
def edit_page(:welcome, assigns) do
top = ~H"""
<h1>Welcome!</h1>
<p>Before we get started, we just need some basic info about you:</p>
<%= edit_page(:overview, assigns) %>
"""
end
def edit_page(:overview, assigns) do
~H"""
<.form
for={@changeset}
let={f}
action={@action}
phx_change= "validate"
phx_submit= "save"
>
<%= hidden_input f, :id, value: @friend.id %>
<div class="border-b-4 flex flex-row">
<%= text_input f, :name, placeholder: "Full Name",
class: "m-0 p-0 pb-2 pl-2 input input-bordered border-dashed",
style: "color: var(--tw-prose-headings);
font-weight: 800;
font-size: 2.25em;
min-width: 50%;
text-indent: 4px;
line-height: 1.1111111;",
value: @friend.name,
phx_debounce: :blur %>
<div class="min-w-fit flex place-items-center mx-4"><%= error_tag f, :name %></div>
</div>
<%= if @address_latlon do %>
<Map.show address_latlon={@address_latlon} />
<% end %>
<ul class="py-4 pl-0 h-1/2">
<li class="flex flex-row gap-x-6">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Email:</strong>
<div class="flex flex-col h-16">
<%= text_input f, :email, class: "input input-primary input-sm md:input-md input-disabled", phx_debounce: "blur", value: @current_user.email %>
<div class="min-w-fit flex place-items-center mr-4"><%= error_tag f, :email %></div>
</div>
</li>
<%= if @live_action != :welcome do %>
<li class="flex flex-row gap-x-6 h-16">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Nickname:</strong>
<div class=""><%= text_input f, :nickname, class: "input input-primary input-sm md:input-md", phx_debounce: "blur", value: @friend.nickname %></div>
</li>
<% end %>
<li class="flex flex-row gap-x-6">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Birthday:</strong>
<div class="flex flex-col h-16">
<%= date_input f, :born, class: "input input-primary input-sm md:input-md", phx_debounce: "blur", value: @friend.born %>
<div class="min-w-fit flex place-items-center mr-4"><%= error_tag f, :born %></div>
</div>
</li>
<li class="flex flex-row gap-x-6">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Phone:</strong>
<div class="flex flex-col h-16">
<%= text_input f, :phone, class: "input input-primary input-sm md:input-md", phx_debounce: "blur", value: @friend.phone |> FriendsWeb.LiveHelpers.display_phone(@changeset) %>
<div class="min-w-fit flex place-items-center mr-4"><%= error_tag f, :phone %></div>
</div>
</li>
<li class="flex flex-row gap-x-6 relative">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Address:</strong>
<div class="flex flex-col h-16">
<%= text_input f, :address_query, value: @address_query,
class: "input input-primary input-sm md:input-md",
phx_debounce: "500",
phx_change: :address_search,
phx_click: JS.show(to: "#search-results"),
phx_blur: JS.hide(to: "#search-results"),
autocomplete: "name" %>
<%= hidden_input f, :address_latlon, value: @address_latlon,
id: "address-latlon", autocomplete: "latlon",
phx_change: "validate"
%>
</div>
<Autocomplete.search_results search_results={@search_results}/>
</li>
</ul>
<div class="form-control flex flex-row gap-x-4 md:justify-end mb-4 md:w-1/2">
<%= if @live_action != :welcome do %>
<div class="flex-1">
<.link patch={Routes.friends_show_path(FriendsWeb.Endpoint, :overview, @friend.slug)} class="btn btn-block btn-outline">back</.link>
</div>
<% end %>
<div class="flex-1">
<%= if @changeset.valid? do %>
<%= submit "Save", phx_disable_with: "Saving...", class: "btn btn-block" %>
<% else %>
<%= submit "Save", class: "btn btn-block btn-disabled" %>
<% end %>
</div>
<%= if @live_action != :welcome and @current_user.profile.id == @friend.id do %>
<div class="flex-1">
<.link href={Routes.user_settings_path(FriendsWeb.Endpoint, :edit)} class="btn btn-block btn-error">Delete</.link>
</div>
<% end %>
</div>
</.form>
"""
end
def edit_page(:relationships, assigns) do
~H"""
"""
end
def edit_page(:timeline, assigns) do
~H"""
"""
end
end
+82 -15
View File
@@ -1,11 +1,5 @@
defmodule FriendsWeb.FriendsLive.Edit do
use FriendsWeb, :live_view
import FriendsWeb.LiveHelpers
import FriendsWeb.FriendsLive.Components
import Helpers
import Helpers.Names
alias Friends.{Friend, Places}
# No slug means it's a new profile form
def mount(%{}, token, socket) do
@@ -28,25 +22,33 @@ 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,
socket
|> assign(:mode, :edit)
|> 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])
@@ -55,16 +57,66 @@ defmodule FriendsWeb.FriendsLive.Edit do
{:noreply,
socket
|> assign(:mode, :edit)
|> 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(:mode, :edit)
|> 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))
|> push_navigate(
to: Routes.friends_show_path(FriendsWeb.Endpoint, :relationships, friend.slug)
)}
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(:mode, :edit)
|> 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 +129,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 +183,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 +191,14 @@ defmodule FriendsWeb.FriendsLive.Edit do
}
new_address =
case address.latlon do
nil ->
%{id: nil}
_ ->
address
|> Friends.Places.Place.get_or_create()
end
new_params = %{
id: id,
@@ -161,17 +224,21 @@ 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(_event, _unsigned_params, socket) do
{:noreply, socket}
end
end
+1 -1
View File
@@ -1,6 +1,6 @@
<section class="row">
<article class="column prose">
<%= edit_menu(assigns) %>
<%= edit_page(@live_action, assigns) %>
<%= apply(FriendsWeb.LiveViews.Edit, @live_action, [assigns]) %>
</article>
</section>
-8
View File
@@ -1,14 +1,6 @@
defmodule FriendsWeb.FriendsLive.Friend do
use FriendsWeb, :live_view
alias FriendsWeb.FriendsLive.Components
alias FriendsWeb.Router.Helpers, as: Routes
alias Friends.Friend
import FriendsWeb.LiveHelpers
import Helpers
import Helpers.Names
# Initialize variables on first load
def mount(%{}, token, socket) do
{:ok,
@@ -0,0 +1,121 @@
defmodule FriendsWeb.LiveViews.Edit do
use FriendsWeb, :live_component
def welcome(assigns) do
top = ~H"""
<h1>Welcome!</h1>
<p>Before we get started, we just need some basic info about you:</p>
<%= overview(assigns) %>
"""
end
def overview(assigns) do
~H"""
<.form
for={@changeset}
let={f}
action={@action}
phx_change= "validate"
phx_submit= "save"
>
<%= hidden_input f, :id, value: @friend.id %>
<div class="border-b-4 flex flex-row">
<%= text_input f, :name, placeholder: "Full Name",
class: "m-0 p-0 pb-2 pl-2 input input-bordered border-dashed",
style: "color: var(--tw-prose-headings);
font-weight: 800;
font-size: 2.25em;
min-width: 50%;
text-indent: 4px;
line-height: 1.1111111;",
value: @friend.name,
phx_debounce: :blur %>
<div class="min-w-fit flex place-items-center mx-4"><%= error_tag f, :name %></div>
</div>
<Map.show address_latlon={@address_latlon} />
<ul class="py-4 pl-0 h-1/2">
<li class="flex flex-row gap-x-6">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Email:</strong>
<div class="flex flex-col h-16">
<%= text_input f, :email, class: "input input-primary input-sm md:input-md input-disabled", phx_debounce: "blur", value: @friend.email %>
<div class="min-w-fit flex place-items-center mr-4"><%= error_tag f, :email %></div>
</div>
</li>
<%= if @live_action != :welcome do %>
<li class="flex flex-row gap-x-6 h-16">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Nickname:</strong>
<div class=""><%= text_input f, :nickname, class: "input input-primary input-sm md:input-md", phx_debounce: "blur", value: @friend.nickname %></div>
</li>
<% end %>
<li class="flex flex-row gap-x-6">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Birthday:</strong>
<div class="flex flex-col h-16">
<%= date_input f, :born, class: "input input-primary input-sm md:input-md", phx_debounce: "blur", value: @friend.born %>
<div class="min-w-fit flex place-items-center mr-4"><%= error_tag f, :born %></div>
</div>
</li>
<li class="flex flex-row gap-x-6">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Phone:</strong>
<div class="flex flex-col h-16">
<%= text_input f, :phone, class: "input input-primary input-sm md:input-md", phx_debounce: "blur", value: @friend.phone |> FriendsWeb.LiveHelpers.display_phone(@changeset) %>
<div class="min-w-fit flex place-items-center mr-4"><%= error_tag f, :phone %></div>
</div>
</li>
<li class="flex flex-row gap-x-6 relative">
<strong class="md:text-xl w-20 md:w-28 shrink-0 text-right">Address:</strong>
<div class="flex flex-col h-16">
<%= text_input f, :search_query, value: @search_query,
class: "input input-primary input-sm md:input-md",
phx_debounce: "500",
phx_change: :address_search,
phx_click: JS.show(to: "#search-results"),
phx_blur: JS.hide(to: "#search-results"),
autocomplete: "name" %>
<%= hidden_input f, :address_latlon, value: @address_latlon,
id: "address-latlon", autocomplete: "latlon",
phx_change: "validate"
%>
</div>
<Autocomplete.search_results
search_results={@search_results}
search_query={@search_query}
select_fxn="selectMapResult"
/>
</li>
</ul>
<div class="form-control flex flex-row gap-x-4 md:justify-end mb-4 md:w-1/2">
<%= if @live_action != :welcome do %>
<div class="flex-1">
<.link patch={Routes.friends_show_path(FriendsWeb.Endpoint, :overview, @friend.slug)} class="btn btn-block btn-outline">back</.link>
</div>
<% end %>
<div class="flex-1">
<%= if @changeset.valid? do %>
<%= submit "Save", phx_disable_with: "Saving...", class: "btn btn-block" %>
<% else %>
<%= submit "Save", class: "btn btn-block btn-disabled" %>
<% end %>
</div>
<%= if @live_action != :welcome and @current_user.profile.id == @friend.id do %>
<div class="flex-1">
<.link href={Routes.user_settings_path(FriendsWeb.Endpoint, :edit)} class="btn btn-block btn-error">Delete</.link>
</div>
<% end %>
</div>
</.form>
"""
end
def relationships(assigns) do
# Just for illustration; this will never run
# as it's redirected via FriendsWeb.FriendsLive.Edit's
# handle_params function
FriendsWeb.LiveViews.Show.relationships(assigns)
end
def timeline(assigns) do
~H"""
"""
end
end
@@ -0,0 +1,115 @@
defmodule FriendsWeb.LiveViews.Show do
use FriendsWeb, :live_component
alias FriendsWeb.Components.Cards
def main(assigns), do: overview(%{assigns | live_action: :overview})
def overview(assigns) do
~H"""
<%= if @address_latlon != "null" do %>
<Map.show address_latlon={@address_latlon} />
<% end %>
<ul class="py-4 pl-0 md:text-xl h-1/2">
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Nickname:</strong>
<div class="">
<%= if is_nil(@friend.nickname) do %>
<span class="italic">none</span>
<% else %>
<%= @friend.nickname %>
<% end %>
</div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Birthday:</strong>
<div class=""><%= @friend.born |> Calendar.strftime("%B %d, %Y") %>
<br class="md:hidden"/>
<span class="font-light">(<%= @friend |> Friend.age %> years old)</span>
</div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Email:</strong>
<div class=""><%= @friend.email %></div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Phone:</strong>
<div class=""><%= @friend.phone %></div>
</li>
<li class="flex flex-row mb-8 gap-6">
<strong class="w-28 text-right">Address:</strong>
<%= if @address_latlon == "null" do %>
<span class="italic">none</span>
<% else %>
<div class=""><%= @address %></div>
<% end %>
<input type="hidden" autocomplete="latlon" value={@address_latlon}/>
</li>
</ul>
"""
end
def relationships(assigns) do
~H"""
<div id="relationships" class="flex md:flex-row flex-col gap-8 p-8">
<%= for relation <- @relationships do %>
<% relationship = relation(@friend, relation) %>
<Cards.relationship_card
friend={@friend}
relation={relation}
relationship={relationship}
editable={@editable}
mode={@mode}
/>
<% end %>
<%= if @relationships |> Enum.empty? do %>
<div class="italic">No relationships on record yet.</div>
<% end %>
</div>
<%= if @editable do %>
<.form for={@changeset} let={f} class="border-t-4">
<ul class="py-4 pl-0 h-1/2">
<li class="flex flex-row gap-x-6 relative items-center">
<strong class="md:text-xl basis-auto shrink-0 text-right">Type a name:</strong>
<div class="flex flex-col relative">
<%= 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"
%>
<Autocomplete.search_results
search_results={@search_results}
search_query={@search_query}
select_fxn="selectRelation"
/>
</div>
</li>
</ul>
</.form>
<% end %>
"""
end
def timeline(assigns) do
~H"""
<div id="timeline" class="flex md:flex-row flex-col gap-8 p-8">
<%= for event <- @friend |> Friends.Friend.get_events do %>
<ul>
<li>
<b><%= event.name %></b> |
<span><%= event.date |> format_date %></span>
</li>
</ul>
<% end %>
<%= if @friend |> Friends.Friend.get_events |> Enum.empty? do %>
<div class="italic">No events on record yet.</div>
<% end %>
</div>
"""
end
end
+112 -10
View File
@@ -1,10 +1,7 @@
defmodule FriendsWeb.FriendsLive.Show do
use FriendsWeb, :live_view
import FriendsWeb.LiveHelpers
import FriendsWeb.FriendsLive.Components
alias Friends.Friend
def mount(%{"slug" => slug} = _attrs, token, socket) do
def mount(%{"slug" => slug}, token, socket) do
live_action = socket.assigns.live_action || false
friend = Friend.get_by_slug(slug)
@@ -15,30 +12,135 @@ defmodule FriendsWeb.FriendsLive.Show do
if(live_action) do
{:ok,
socket
|> assign(:mode, :show)
|> assign(:live_action, live_action)
|> 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(:action, editable)}
|> assign(:editable, editable)
|> assign(:action, :moot)}
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
def handle_params(
%{"slug" => slug},
_url,
%{assigns: %{live_action: :overview}} = socket
) do
friend = Friend.get_by_slug(slug)
live_action = socket.assigns.live_action
{:noreply,
socket
|> assign(:mode, :show)
|> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign_friend(friend)}
end
def handle_params(
%{"slug" => slug} = params,
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(:mode, :edit)
|> assign_friend(friend)
|> assign(:relationships, friend |> relations)
|> assign(:live_action, live_action)
|> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:editable, editable)}
|> assign(:search_query, nil)
|> assign(:relation_id, nil)
|> assign(:search_results, nil)
|> assign(:editable, editable)
|> title(friend.name <> " - " <> (live_action |> titlecase))}
end
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(
"phx: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(
"phx:delete_relation",
%{"id" => rel_id},
%{assigns: %{friend: friend, relationships: relationships}} = socket
) do
rel = Relationship.get_by_id(rel_id)
IO.inspect("Deleting #{rel.id}")
rel |> Relationship.delete()
updated_friend = Friend.get_by_id(friend.id)
{:noreply,
socket
|> assign_friend(updated_friend)
|> assign(:relationships, updated_friend |> relations)}
end
def handle_event(
"phx:relation_type",
%{"rel_id" => rel_id, "type" => type},
%{assigns: %{friend: friend, relationships: relationships}} = socket
) do
rel = Relationship.get_by_id(rel_id)
IO.inspect("Changing type of relationship #{rel.id} to #{type}")
rel |> Relationship.change_type(type |> Relationship.type_index())
updated_friend = Friend.get_by_id(friend.id)
{:noreply,
socket
|> assign_friend(updated_friend)
|> assign(:relationships, updated_friend |> relations)
|> push_navigate(to: Routes.friends_show_path(socket, :relationships, friend.slug))}
end
end
+3 -3
View File
@@ -2,11 +2,11 @@
<article class="column prose">
<%= menu(assigns) %>
<%= header(assigns) %>
<%= show_page(@live_action, assigns) %>
<%= apply(FriendsWeb.LiveViews.Show, @live_action, [assigns]) %>
<%= if @editable do %>
<%= if @editable and @mode != :edit do %>
<div class="form-control flex flex-row mb-4">
<.link navigate={Routes.friends_edit_path(FriendsWeb.Endpoint, :overview, @friend.slug)} class="btn btn-block md:btn-wide text-white">edit</.link>
<.link navigate={Routes.friends_edit_path(FriendsWeb.Endpoint, @live_action, @friend.slug)} class="btn btn-block md:btn-wide text-white"><%=@live_action |> get_edit_text%></.link>
</div>
<% end %>
</article>
+12
View File
@@ -105,6 +105,11 @@ defmodule FriendsWeb.Router do
live "/:slug/relationships", FriendsLive.Show, :relationships
end
scope "/relationship", FriendsWeb do
pipe_through [:browser]
live "/:slug1/:slug2", RelationshipLive.Show, :overview
end
# Edit modes (require being logged in and having a profile)
scope "/edit/", FriendsWeb do
pipe_through [:browser, :require_authenticated_user, :capture_profile]
@@ -116,4 +121,11 @@ defmodule FriendsWeb.Router do
live "/:slug/timeline", FriendsLive.Edit, :timeline
live "/:slug/relationships", FriendsLive.Edit, :relationships
end
# API
scope "/api", FriendsWeb do
pipe_through :api
delete "/relationship/:id", RelationshipsController, :delete
end
end
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" data-theme="corporate"> <!-- pastel -->
<html lang="en" data-theme="cupcake"> <!-- pastel -->
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+27 -6
View File
@@ -1,6 +1,6 @@
defmodule FriendsWeb.LiveHelpers do
use FriendsWeb, :live_component
alias Friends.Friend
use Phoenix.LiveComponent
def titlecase(atom) do
atom |> to_string |> :string.titlecase()
@@ -16,23 +16,44 @@ defmodule FriendsWeb.LiveHelpers do
display_phone(phone)
end
end
def display_phone(phone) do
"""
TODO: Actually implement this
"""
has_plus = phone |> String.starts_with?("+")
case phone |> String.length do
case phone |> String.length() do
10 ->
country = "+1 "
[area, first, sec1, sec2] = phone |> to_charlist |> Enum.chunk_every(3) |> Enum.map(&(&1 |> to_string))
[area, first, sec1, sec2] =
phone |> to_charlist |> Enum.chunk_every(3) |> Enum.map(&(&1 |> to_string))
"#{country}(#{area}) #{first}-#{sec1}#{sec2}"
IO.inspect "#{country}(#{area}) #{first}-#{sec1}#{sec2}"
11 when has_plus ->
phone
12 when has_plus ->
phone
end
end
def get_edit_text(live_action) do
case live_action do
:timeline -> "add a moment"
:relationships -> "add a relationship"
_ -> "edit"
end
end
def format_date(date) do
date
|> Calendar.strftime("%b %d, %Y")
end
def assign_current_user(socket, user_token) do
user =
case user_token do
@@ -62,15 +83,15 @@ defmodule FriendsWeb.LiveHelpers do
def assign_friend(socket, friend) do
socket
|> assign(:friend, friend)
|> assign(:editable, friend |> Friend.can_be_edited_by(socket.assigns[:current_user]))
|> assign(:changeset, friend |> Friend.changeset())
|> assign(:relationships, friend.relationships)
end
# Same thing, but this time we have a changeset we want to keep
def assign_friend(socket, friend, changeset) do
socket
|> assign(:friend, friend)
|> assign(:editable, friend |> Friend.can_be_edited_by(socket.assigns[:current_user]))
|> assign(:changeset, changeset)
|> assign(:relationships, friend.relationships)
end
end
+34 -2
View File
@@ -74,16 +74,48 @@ defmodule Helpers do
birthday(friend) |> Date.diff(Date.utc_today())
end
def relations(friend) do
def relations(friend, include_self \\ false) do
list =
[friend.relationships, friend.reverse_relationships]
|> List.flatten()
|> Enum.dedup()
if include_self, do: list, else: list |> Enum.filter(&(&1.id != friend.id))
end
def relation(friend, friend2) do
Friends.Relationship.get(friend, friend2)
end
def events(relationship) 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