Compare commits
19
Commits
2f21ccd9de
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c7cb7b84 | ||
|
|
b85e3cc96c | ||
|
|
fd5bcbfc05 | ||
|
|
5339fdb2b0 | ||
|
|
5ca34c2b6f | ||
|
|
bbf526d6a8 | ||
|
|
c72c09b1b2 | ||
|
|
90c3b06cfc | ||
|
|
d331da9e17 | ||
|
|
aa0c43aeb1 | ||
|
|
075ae78df2 | ||
|
|
1a4a7f0d0c | ||
|
|
f90c8621b9 | ||
|
|
2456c6d14b | ||
|
|
eccaca068d | ||
|
|
0c2f304b2f | ||
|
|
90007d40e7 | ||
|
|
2284a437fa | ||
|
|
117ccf5e16 |
+90
-29
@@ -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,96 @@ 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('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';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
-18
@@ -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
|
||||
{
|
||||
:place_id,
|
||||
Places.Place.get_or_new(opts[:place]).id
|
||||
|
||||
opts =
|
||||
opts ++
|
||||
[
|
||||
if opts[:place] do
|
||||
{
|
||||
:place_id,
|
||||
Places.Place.get_or_new(opts[:place]).id
|
||||
}
|
||||
end
|
||||
]
|
||||
|
||||
{:ok, event} =
|
||||
%Event{
|
||||
story: opts[:story],
|
||||
date: opts[:date],
|
||||
place_id: opts[:place_id],
|
||||
relationship_id: relationship.id
|
||||
}
|
||||
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)
|
||||
|> div(365)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -26,8 +26,17 @@ defmodule Friends.Places.Place do
|
||||
end
|
||||
|
||||
def get_or_create(place) do
|
||||
place
|
||||
|> Friends.Places.Place.validate()
|
||||
|> Friends.Repo.insert!()
|
||||
case @repo.one(
|
||||
from(
|
||||
p in Friends.Places.Place,
|
||||
where: p.name == ^place.name
|
||||
)
|
||||
) do
|
||||
nil ->
|
||||
place
|
||||
|> Friends.Places.Place.validate()
|
||||
|> @repo.insert!()
|
||||
found -> found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,56 +98,86 @@ 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(
|
||||
from(r in Relationship,
|
||||
where: r.friend_id == ^id1 and r.relation_id == ^id2,
|
||||
preload: [:events]
|
||||
|
||||
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,
|
||||
where: r.friend_id == ^id2 and r.relation_id == ^id1,
|
||||
preload: [:events]
|
||||
)
|
||||
from(r in Relationship,
|
||||
where: r.friend_id == ^id2 and r.relation_id == ^id1,
|
||||
preload: [:events]
|
||||
)
|
||||
)
|
||||
else
|
||||
rel
|
||||
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
@@ -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>
|
||||
<% 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>
|
||||
<div class=""><%= @address %></div>
|
||||
<input type="hidden" autocomplete="latlon" value={@latlon}/>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
end
|
||||
<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 %>
|
||||
|
||||
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 %>
|
||||
<%= if @editable do %>
|
||||
|
||||
<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 %>
|
||||
<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>
|
||||
<% 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
|
||||
|
||||
@@ -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 =
|
||||
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 +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,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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -74,16 +74,48 @@ defmodule Helpers do
|
||||
birthday(friend) |> Date.diff(Date.utc_today())
|
||||
end
|
||||
|
||||
def relations(friend) do
|
||||
[friend.relationships, friend.reverse_relationships]
|
||||
|> List.flatten()
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user