Compare commits

...
23 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
Ryan Pandya 5339fdb2b0 Merge branch 'dev' 2022-11-23 14:22:53 -05:00
Ryan Pandya 2f21ccd9de Validations, welcome flow bug fixes 2022-11-23 13:59:17 -05:00
Ryan Pandya afa816d2c2 Addresses and places in a semi working way! 2022-11-09 19:11:46 -08:00
Ryan Pandya c495373a81 Progress on address edit form 2022-11-08 01:13:40 -08:00
Ryan Pandya ce011e7d87 Remove temp peer info code 2022-11-05 22:18:37 -07:00
Ryan Pandya a86e5c9409 Begin implenting Places using LocationIQ 2022-11-05 21:53:48 -07:00
Ryan Pandya fe0d748a53 Added addresses. Cleaning routes. Flow. 2022-11-05 18:12:02 -07:00
Ryan Pandya bb6d7e1e2d Small liveview changes 2022-11-05 14:04:07 -07:00
Ryan Pandya 684adddc55 Start relationship tests; create events 2022-11-03 18:19:17 -04:00
Ryan Pandya 82e86de969 Happy with friend tests for now 2022-11-03 17:47:08 -04:00
Ryan Pandya f49092a08a More frontend tests 2022-11-03 15:48:43 -04:00
Ryan Pandya fc6ecbe9c8 Finally started writing tests 2022-11-03 02:17:26 -04:00
Ryan Pandya 120cb288d2 Mucking everything up combining users/profiles 2022-10-30 00:47:32 -07:00
Ryan Pandya c67fbf6733 Few small changes 2022-10-30 00:30:21 -07:00
Ryan Pandya 8577c9dddb Merge branch 'dev' of http://git.ryanpandya.com:3003/ryan/friends into dev 2022-10-30 00:26:30 -07:00
Ryan Pandya f2ff002b98 Clean up forms 2022-10-30 00:25:34 -07:00
Ryan Pandya 23e738ddc9 Janky initial implementation of auth and profiles 2022-10-29 17:21:52 -07:00
Ryan Pandya fe8fe1b9f7 Janky initial implementation of auth and profiles 2022-10-29 17:20:14 -07:00
Ryan Pandya b3aff0d742 Merge branch 'dev' of http://git.ryanpandya.com:3003/ryan/friends into dev 2022-10-24 00:34:10 -07:00
Ryan Pandya 6430858f92 swoosh -> postmark 2022-10-24 00:33:44 -07:00
35 changed files with 3467 additions and 496 deletions
+5
View File
@@ -3,6 +3,11 @@
@import "tailwindcss/components"; @import "tailwindcss/components";
@import "tailwindcss/utilities"; @import "tailwindcss/utilities";
/* mapbox */
.mapboxgl-control-container{
display:none;
}
/* Override some defaults I don't like */ /* Override some defaults I don't like */
.input{ .input{
border-radius: inherit !important; border-radius: inherit !important;
+104 -4
View File
@@ -21,15 +21,21 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html" import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration. // Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix" import { Socket } from "phoenix"
import {LiveSocket} from "phoenix_live_view" import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
import mapboxgl from "../vendor/mapbox-gl"
let Hooks = {};
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks,
params: { _csrf_token: csrfToken }
})
// Show progress bar on live navigation and form submits // 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-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide()) window.addEventListener("phx:page-loading-stop", info => topbar.hide())
@@ -42,3 +48,97 @@ liveSocket.connect()
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket
window.hideElement = function (id) {
var el = document.getElementById(id);
el.hidden = true;
}
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;
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() {
if (this.el.getAttribute("latlon") != "null") {
this.initMap();
}
}
}
+2005
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -7,8 +7,8 @@
"acorn": "^7.4.1", "acorn": "^7.4.1",
"acorn-node": "^1.8.2", "acorn-node": "^1.8.2",
"acorn-walk": "^7.2.0", "acorn-walk": "^7.2.0",
"arg": "^5.0.2",
"anymatch": "^3.1.2", "anymatch": "^3.1.2",
"arg": "^5.0.2",
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.12",
"binary-extensions": "^2.2.0", "binary-extensions": "^2.2.0",
"braces": "^3.0.2", "braces": "^3.0.2",
@@ -44,6 +44,7 @@
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"is-number": "^7.0.0", "is-number": "^7.0.0",
"lilconfig": "^2.0.6", "lilconfig": "^2.0.6",
"mapbox-gl": "^2.10.0",
"merge2": "^1.4.1", "merge2": "^1.4.1",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"minimist": "^1.2.7", "minimist": "^1.2.7",
@@ -63,10 +64,10 @@
"postcss-nested": "^6.0.0", "postcss-nested": "^6.0.0",
"postcss-selector-parser": "^6.0.10", "postcss-selector-parser": "^6.0.10",
"postcss-value-parser": "^4.2.0", "postcss-value-parser": "^4.2.0",
"queue-microtask": "^1.2.3",
"quick-lru": "^5.1.1", "quick-lru": "^5.1.1",
"read-cache": "^1.0.0", "read-cache": "^1.0.0",
"readdirp": "^3.6.0", "readdirp": "^3.6.0",
"queue-microtask": "^1.2.3",
"resolve": "^1.22.1", "resolve": "^1.22.1",
"reusify": "^1.0.4", "reusify": "^1.0.4",
"run-parallel": "^1.2.0", "run-parallel": "^1.2.0",
@@ -80,7 +81,6 @@
"xtend": "^4.0.2", "xtend": "^4.0.2",
"yaml": "^1.10.2" "yaml": "^1.10.2"
}, },
"devDependencies": {},
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+134 -10
View File
@@ -1,6 +1,7 @@
defmodule Friends.Event do defmodule Friends.Event do
use Ecto.Schema use Ecto.Schema
alias Friends.{Relationship,Event} import Ecto.Query
alias Friends.{Relationship, Event}
alias Places.Place alias Places.Place
@repo Friends.Repo @repo Friends.Repo
@@ -16,33 +17,157 @@ defmodule Friends.Event do
belongs_to(:relationship, Relationship) belongs_to(:relationship, Relationship)
end 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 def meet(friend1, friend2, opts \\ nil) do
relationship = Relationship.get_or_new(friend1, friend2) relationship = Relationship.get_or_new(friend1, friend2)
opts = opts ++ [if opts[:place] do
opts =
opts ++
[
if opts[:place] do
{ {
:place_id, :place_id,
Places.Place.get_or_new(opts[:place]).id Places.Place.get_or_new(opts[:place]).id
} }
end] end
{:ok, event} = %Event{ ]
{:ok, event} =
%Event{
story: opts[:story], story: opts[:story],
date: opts[:date], date: opts[:date],
place_id: opts[:place_id], place_id: opts[:place_id],
relationship_id: relationship.id relationship_id: relationship.id
} |> @repo.insert }
|> @repo.insert
event event
end end
def person(event) do
Friends.Friend.get_by_id(event.relationship.friend_id)
end
def people(event) do def people(event) do
event.relationship |> Relationship.members if event.solo, do: event.person, else: event.relationship |> Relationship.members()
end end
def age(event) do def age(event) do
years = Date.diff(Date.utc_today, event.date) years =
Date.diff(Date.utc_today(), event.date)
|> div(365) |> div(365)
if years == 0 do if years == 0 do
months = Date.diff(Date.utc_today, event.date) months = Date.diff(Date.utc_today(), event.date) |> rem(365) |> div(12)
|> rem(365) |> div(12)
{months, :month} {months, :month}
else else
{years, :year} {years, :year}
@@ -52,5 +177,4 @@ defmodule Friends.Event do
def print_age(age) do def print_age(age) do
Friends.Helpers.pluralize(age |> elem(0), age |> elem(1)) Friends.Helpers.pluralize(age |> elem(0), age |> elem(1))
end end
end end
+111 -27
View File
@@ -1,7 +1,7 @@
defmodule Friends.Friend do defmodule Friends.Friend do
use Ecto.Schema use Ecto.Schema
alias Friends.{Relationship, Friend} alias Friends.{Relationship, Friend, Event}
import Helpers import Helpers
import Ecto.Query import Ecto.Query
@@ -23,51 +23,84 @@ defmodule Friends.Friend do
:relationships, :relationships,
Friends.Friend, Friends.Friend,
join_through: Relationship, 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( many_to_many(
:reverse_relationships, :reverse_relationships,
Friends.Friend, Friends.Friend,
join_through: Relationship, join_through: Relationship,
join_keys: [relation_id: :id, friend_id: :id] join_keys: [relation_id: :id, friend_id: :id],
on_delete: :delete_all
) )
end end
def changeset(friend, params \\ %{}) do def changeset(friend, params \\ %{}) do
friend friend
|> Ecto.Changeset.cast(params, [:name, :born, :nickname, :email, :phone, :slug, :user_id]) |> Ecto.Changeset.cast(params, [
|> Ecto.Changeset.validate_required([:name, :email, :phone, :born]) :name,
|> Ecto.Changeset.validate_format(:name, ~r/\w+\ \w+/) :born,
:nickname,
:email,
:phone,
:slug,
:user_id,
:address_id
])
|> 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( |> Ecto.Changeset.validate_format(
:email, :email,
Regex.compile!("^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$") Regex.compile!("^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$"),
message: "Invalid email format."
) )
|> Ecto.Changeset.validate_format( |> Ecto.Changeset.validate_format(
:phone, :phone,
Regex.compile!("^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$") Regex.compile!("^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$"),
message: "Invalid phone format."
) )
|> validate_birthdate()
|> Ecto.Changeset.unique_constraint(:name) |> Ecto.Changeset.unique_constraint(:name)
|> Ecto.Changeset.unique_constraint(:email) |> Ecto.Changeset.unique_constraint(:email)
end end
defp validate_birthdate(%{changes: %{born: born}} = changeset) do
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
end
end
defp validate_birthdate(changeset), do: changeset
def all() do def all() do
preloads = [:relationships, :reverse_relationships, :user, :address] preloads = [:relationships, :reverse_relationships, :user, :address]
@repo.all(from(f in Friends.Friend, preload: ^preloads)) @repo.all(from(f in Friends.Friend, preload: ^preloads))
end end
def new(params \\ %{}) do def new(params \\ %{}) do
%Friend{id: :new} %Friend{id: nil}
|> struct(params) |> struct(params)
end end
def get_by_slug(slug) do def get_by_slug(slug) do
@repo.one( @repo.one(
from(f in Friend, from(f in Friend,
where: f.slug == ^slug, where: f.slug == ^slug
preload: [:relationships, :reverse_relationships, :address]
) )
) )
|> load_preloads()
end end
def get_by_id(id) do def get_by_id(id) do
@@ -82,29 +115,29 @@ defmodule Friends.Friend do
def get_by_email(email) do def get_by_email(email) do
@repo.one( @repo.one(
from(f in Friend, from(f in Friend,
where: f.email == ^email, where: f.email == ^email
preload: [:relationships, :reverse_relationships]
) )
) )
|> load_preloads()
end end
def create(params) do def create(params \\ %{id: nil}) do
%Friend{} Friend.new()
|> Friend.changeset(%{params | id: nil}) |> Friend.changeset(params)
|> Map.put(:action, :insert) |> Map.put(:action, :insert)
end end
def update(params) do def update(params) do
Friend.get_by_id(params.id |> String.to_integer()) Friend.get_by_id(params.id)
|> Friend.changeset(params) |> Friend.changeset(params)
|> Map.put(:action, :update) |> Map.put(:action, :update)
end end
def commit(changeset) do def commit(changeset) do
changeset changeset
|> generate_slug |> @repo.commit!
|> @repo.insert!
|> load_preloads |> load_preloads
|> assign_user!()
end end
def generate_slug(%Ecto.Changeset{} = changeset) do def generate_slug(%Ecto.Changeset{} = changeset) do
@@ -125,10 +158,12 @@ defmodule Friends.Friend do
def create_or_update(params) do def create_or_update(params) do
case params.id do case params.id do
:new -> nil ->
params params
|> create() |> create()
|> generate_slug()
|> commit() |> commit()
|> create_birth_event()
_number -> _number ->
params params
@@ -137,17 +172,17 @@ defmodule Friends.Friend do
end end
end end
def get_relationships(friend) do def get_relationships(friend, include_self \\ false) do
friend friend
|> relations |> relations(include_self)
|> Enum.map(&relation(friend, &1)) |> Enum.map(&relation(friend, &1))
end end
def get_events(friend) do def get_events(friend) do
friend friend
|> get_relationships |> get_relationships(:all)
|> Enum.map(& &1.events) |> Enum.flat_map(& &1.events)
|> List.flatten() |> Enum.dedup()
end end
def age(friend) do def age(friend) do
@@ -156,7 +191,16 @@ defmodule Friends.Friend do
end end
def can_be_edited_by(friend, user) do 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
friend
|> Friends.Friend.changeset(%{
address_id: address.id
})
|> Friends.Repo.update!()
end end
def assign_user(%Friend{} = friend) do def assign_user(%Friend{} = friend) do
@@ -169,6 +213,13 @@ defmodule Friends.Friend do
end end
end end
def assign_user!(%Friend{} = friend) do
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 def load_user(%Friends.Friend{user: %Ecto.Association.NotLoaded{}} = model) do
model model
|> @repo.preload(:user) |> @repo.preload(:user)
@@ -200,5 +251,38 @@ defmodule Friends.Friend do
|> @repo.preload([:user, :relationships, :reverse_relationships, :address]) |> @repo.preload([:user, :relationships, :reverse_relationships, :address])
end end
def load_preloads(%Friends.Friend{} = friend), do: friend def load_preloads(friend), do: friend
def get_address(%Friend{} = friend) do
if friend.address do
{
friend.address.latlon,
friend.address.name
}
else
{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 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
+19 -38
View File
@@ -1,8 +1,11 @@
defmodule Friends.Places.Place do defmodule Friends.Places.Place do
use Ecto.Schema use Ecto.Schema
import Ecto.Query import Ecto.Query
import Ecto.Changeset
import Helpers import Helpers
alias Friends.Places.{Place, Search}
@repo Friends.Repo @repo Friends.Repo
schema "places" do schema "places" do
@@ -12,50 +15,28 @@ defmodule Friends.Places.Place do
field(:zoom, :integer) field(:zoom, :integer)
has_many(:events, Friends.Event) has_many(:events, Friends.Event)
has_many(:friends, Friends.Friend) has_many(:friends, Friends.Friend, foreign_key: :address_id)
end end
def new(place_name, opts \\ nil) do def validate(place, params \\ %{}) do
{:ok, place} =
@repo.insert(%Friends.Places.Place{
name: place_name,
type: opts[:type]
})
place place
|> cast(params, [:name, :type, :latlon, :zoom])
|> unique_constraint(:name)
|> validate_required(:name)
end end
def get(place_name) do def get_or_create(place) do
@repo.one( case @repo.one(
from(p in Place, from(
where: p.name == ^place_name, p in Friends.Places.Place,
preload: [:events] where: p.name == ^place.name
) )
) ) do
end nil ->
def get_or_new(name, opts \\ nil) do
case get(name) do
nil -> new(name, opts)
place -> place
end
end
def get_by_slug(slug) do
name = slug |> from_slug
@repo.one(
from(p in Place,
where: p.name == ^name,
preload: [:events]
)
)
end
def changeset(place, params \\ %{}) do
place place
|> Ecto.Changeset.cast(params, [:name, :type, :latlon, :zoom]) |> Friends.Places.Place.validate()
|> Ecto.Changeset.validate_required([:name]) |> @repo.insert!()
|> Ecto.Changeset.unique_constraint(:name) found -> found
end
end end
end end
+43 -7
View File
@@ -1,5 +1,10 @@
defmodule Friends.Places.Search do defmodule Friends.Places.Search do
def api_key, do: "pk.7c4a6f4bf061fd4a9af9663132c58af3" import Ecto.Query
@repo Friends.Repo
def api_key,
do:
"pk.eyJ1IjoicnlhbnBhbmR5YSIsImEiOiJja3psM2tlcDA1MXl1Mm9uZmo5bGxpNzdxIn0.TwBKpTTypcD5fWFc8XRyHg"
def viewbox(region) do def viewbox(region) do
[lat_min, lat_max, lon_min, lon_max] = region |> Enum.map(&String.to_float/1) [lat_min, lat_max, lon_min, lon_max] = region |> Enum.map(&String.to_float/1)
@@ -12,16 +17,47 @@ defmodule Friends.Places.Search do
] ]
end end
def query(str, region \\ nil) do def known_places(str) do
viewbox = @repo.all(
if region do from(
viewbox(query(region)) p in Friends.Places.Place,
where: ilike(p.name, ^"%#{str}%")
)
)
|> Enum.map(
&%{
"center" => &1.latlon,
"place_name" => &1.name,
"id" => "known.#{&1.id}"
}
)
end end
url = "https://us1.locationiq.com/v1/search?key=#{api_key()}&q=#{str}&format=json#{viewbox}" def autocomplete(str, region \\ nil) do
viewbox =
if region do
viewbox(autocomplete(region))
end
url =
"https://api.mapbox.com/geocoding/v5/mapbox.places/#{str}.json?proximity=ip&types=place%2Cpostcode%2Caddress&access_token=#{api_key}"
response = HTTPoison.get!(url) response = HTTPoison.get!(url)
results = Poison.decode!(response.body) results = Poison.decode!(response.body)
results IO.inspect(results["features"])
known_places(str) ++ results["features"]
end
def parse_features(%{
"center" => lonlat,
"place_name" => name,
"id" => id
}) do
%{
name: name,
value: lonlat |> Enum.reverse() |> Poison.encode!(),
id: id |> String.replace(".", "-")
}
end end
end end
+103 -26
View File
@@ -1,9 +1,10 @@
defmodule Friends.Relationship do defmodule Friends.Relationship do
use Ecto.Schema use Ecto.Schema
import Ecto.Query import Ecto.Query
alias Friends.{Relationship,Friend} alias Friends.{Relationship, Friend}
@repo Friends.Repo @repo Friends.Repo
@default_type 3
schema "relationships" do schema "relationships" do
field(:friend_id, :id) field(:friend_id, :id)
@@ -13,7 +14,7 @@ defmodule Friends.Relationship do
has_many(:events, Friends.Event) has_many(:events, Friends.Event)
end end
@attrs [:friend_id, :relation_id] @attrs [:friend_id, :relation_id, :type]
def types(index) do def types(index) do
types() |> elem(index) types() |> elem(index)
@@ -22,11 +23,12 @@ defmodule Friends.Relationship do
def types do def types do
# Tuple: name of the type, associated color, and what that person "is" to the other # 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}, {:family, :primary, :relative},
{:friends, :secondary, :friend}, {:friends, :secondary, :friend},
{:partners, :info, :partner}, {:partners, :info, :partner},
{:dating, :success, :date}, {:dating, :success, :dating},
{:engaged, :success, :fiancé}, {:engaged, :success, :fiancé},
{:married, :success, :spouse}, {:married, :success, :spouse},
{:divorced, :error, :ex}, {:divorced, :error, :ex},
@@ -35,6 +37,16 @@ defmodule Friends.Relationship do
} }
end 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 def get_type(rel) do
rel.type |> types |> elem(0) rel.type |> types |> elem(0)
end end
@@ -55,9 +67,27 @@ defmodule Friends.Relationship do
) )
end 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 def changeset(struct, params \\ %{}) do
struct struct
|> Ecto.Changeset.cast(params, @attrs) |> Ecto.Changeset.cast(params, @attrs)
|> validate_type
|> Ecto.Changeset.unique_constraint( |> Ecto.Changeset.unique_constraint(
[:friend_id, :relation_id], [:friend_id, :relation_id],
name: :relationships_friend_id_relation_id_index name: :relationships_friend_id_relation_id_index
@@ -68,34 +98,39 @@ defmodule Friends.Relationship do
) )
end end
def all() do def all() do
preloads = [] preloads = []
@repo.all(from(r in Friends.Relationship, preload: ^preloads)) @repo.all(from(r in Friends.Relationship, where: r.type != 0, preload: ^preloads))
end end
def new(friend1, friend2, type \\ 2) do def new(friend1, friend2, type \\ @default_type) do
id1 = friend1.id id1 = friend1.id
id2 = friend2.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, friend_id: id1,
relation_id: id2, relation_id: id2,
type: type type: rel_type
} })
)
relationship relationship
end end
def get(friend1, friend2) do def get(friend1, friend2) do
id1 = friend1.id id1 = friend1.id
id2 = friend2.id id2 = friend2.id
rel = @repo.one(
rel =
@repo.one(
from(r in Relationship, from(r in Relationship,
where: r.friend_id == ^id1 and r.relation_id == ^id2, where: r.friend_id == ^id1 and r.relation_id == ^id2,
preload: [:events] preload: [:events]
) )
) )
if rel == nil do if rel == nil do
@repo.one( @repo.one(
from(r in Relationship, from(r in Relationship,
@@ -108,16 +143,41 @@ defmodule Friends.Relationship do
end end
end end
def get_or_new(a,b) do def get_or_create(a, b) do
case get(a, b) do case get(a, b) do
nil -> new(a,b) nil -> new(a, b)
relationship -> relationship relationship -> relationship
end end
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 def get_by_slugs([slug1, slug2]) do
friend1 = slug1 |> Friend.get_by_slug friend1 = slug1 |> Friend.get_by_slug()
friend2 = slug2 |> Friend.get_by_slug friend2 = slug2 |> Friend.get_by_slug()
get(friend1, friend2) get(friend1, friend2)
end end
@@ -130,23 +190,40 @@ defmodule Friends.Relationship do
end end
def age(relationship) do def age(relationship) do
relationship.events case relationship.events do
|> Enum.map(fn(event) -> [] ->
Date.diff(Date.utc_today, event.date) nil
end) |> Enum.sort |> List.last |> div(365)
e ->
e
|> Enum.map(fn event ->
Date.diff(Date.utc_today(), event.date)
end)
|> Enum.sort()
|> List.last()
|> div(365)
end
end end
def load_events(%Relationship{ def load_events(
events: %Ecto.Association.NotLoaded{}} = model) do %Relationship{
events: %Ecto.Association.NotLoaded{}
} = model
) do
model model
|> @repo.preload([:events]) |> @repo.preload([:events])
end end
def load_events(%Relationship{} = r), do: r 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 model
|> @repo.preload([:events]) |> @repo.preload([:events])
end end
def load_preloads(%Relationship{} = r), do: r
def load_preloads(%Relationship{} = r), do: r
end end
+9
View File
@@ -2,4 +2,13 @@ defmodule Friends.Repo do
use Ecto.Repo, use Ecto.Repo,
otp_app: :friends, otp_app: :friends,
adapter: Ecto.Adapters.Postgres adapter: Ecto.Adapters.Postgres
def commit!(changeset) do
IO.inspect(changeset)
case changeset.action do
:update -> update!(changeset)
:insert -> insert!(changeset)
end
end
end end
+23 -111
View File
@@ -24,6 +24,10 @@ defmodule FriendsWeb do
import Plug.Conn import Plug.Conn
import FriendsWeb.Gettext import FriendsWeb.Gettext
alias FriendsWeb.Router.Helpers, as: Routes alias FriendsWeb.Router.Helpers, as: Routes
alias Friends.{Friend, Relationship}
alias Friends.Accounts.User
import Helpers
end end
end end
@@ -48,6 +52,15 @@ defmodule FriendsWeb do
use Phoenix.LiveView, use Phoenix.LiveView,
layout: {FriendsWeb.LayoutView, "live.html"} 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()) unquote(view_helpers())
end end
end end
@@ -55,6 +68,14 @@ defmodule FriendsWeb do
def live_component do def live_component do
quote do quote do
use Phoenix.LiveComponent 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()) unquote(view_helpers())
end end
@@ -63,117 +84,8 @@ defmodule FriendsWeb do
def component do def component do
quote do quote do
use Phoenix.Component use Phoenix.Component
import Helpers
unquote(view_helpers()) import FriendsWeb.LiveHelpers
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
unquote(view_helpers()) unquote(view_helpers())
end end
@@ -1,8 +1,5 @@
defmodule FriendsWeb.FriendsController do defmodule FriendsWeb.FriendsController do
use FriendsWeb, :controller use FriendsWeb, :controller
alias Friends.{Friend, Relationship}
alias Friends.Accounts.User
import Helpers
def index(conn, _params) do def index(conn, _params) do
conn 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
@@ -159,7 +159,7 @@ defmodule FriendsWeb.UserAuth do
nil -> nil ->
conn conn
|> put_flash(:info, "You're logged in but we still need to make you a profile!") |> put_flash(:info, "You're logged in but we still need to make you a profile!")
|> redirect(to: Routes.friends_edit_path(conn, :overview, :new)) |> redirect(to: Routes.friends_edit_path(conn, :welcome))
|> halt() |> halt()
# Or make a new one # Or make a new one
@@ -0,0 +1,39 @@
defmodule FriendsWeb.Components.Autocomplete do
use FriendsWeb, :live_component
import Helpers
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 bottom-16 left-0">
<%= if @search_results do %>
<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="p-0 m-0">
<.link id={r[:id]}
phx_value={r[:value]}
class="search_result"
phx-hook="NewRelation"
onMouseDown={"#{@select_fxn}('#{r[:value]}', '#{r[:name]}')"}>
<%= r[:name] |> bolden(@search_query) %>
</.link>
</li>
<% end %>
</ul>
<% end %>
</div>
"""
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,8 +1,5 @@
defmodule FriendsWeb.FriendsLive.Components do defmodule FriendsWeb.Components do
use FriendsWeb, :live_component use FriendsWeb, :live_component
use Phoenix.HTML
import Helpers
alias Friends.Friend
def header(assigns) do def header(assigns) do
~H""" ~H"""
@@ -25,8 +22,11 @@ defmodule FriendsWeb.FriendsLive.Components do
""" """
end end
@spec edit_menu(any) :: Phoenix.LiveView.Rendered.t()
def edit_menu(assigns) do def edit_menu(assigns) do
~H""" if assigns.live_action == :welcome,
do: "",
else: ~H"""
<div class="hidden sm:tabs sm:mb-8"> <div class="hidden sm:tabs sm:mb-8">
<%= for page <- [:overview, :timeline, :relationships] do %> <%= for page <- [:overview, :timeline, :relationships] do %>
<% is_active = if(page == @live_action) do "tab-active" end %> <% is_active = if(page == @live_action) do "tab-active" end %>
@@ -38,175 +38,73 @@ defmodule FriendsWeb.FriendsLive.Components do
""" """
end end
def show_page(:main, assigns), do: show_page(:overview, %{assigns | live_action: :overview}) def relationship_details(assigns) do
def show_page(:overview, assigns) do
~H""" ~H"""
<ul class="py-4 pl-0 md:text-xl h-1/2"> <div class="flex flex-row items-center gap-3">
<li class="flex flex-row mb-8 gap-6"> <div class="dropdown" id={"type-selector-#{@relationship.id}"}>
<strong class="w-28 text-right">Nickname:</strong> <label tabindex="0" class={"hover:badge-ghost badge badge-#{@relationship |> Friends.Relationship.get_color} m-1 text-white"}>
<div class=""> <%= @relationship |> Friends.Relationship.get_relation %>
<%= if is_nil(@friend.nickname) do %>
<span class="italic">none</span> <%= 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 %> <% else %>
<%= @friend.nickname %> <div class={"tooltip tooltip-#{@relationship |> Relationship.get_color}"} data-tip="add milestone dates on the detail page.">
<% end %> <a class="hover:cursor-help" style="text-decoration-style:dashed">(no date)</a>
</div> </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=""><%= @friend.address %></div>
</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 %> <% end %>
</div> </div>
""" """
end end
def show_page(:relationships, assigns) do def relationship_type_selector(assigns) do
~H""" ~H"""
<div id="relationships" class="flex md:flex-row flex-col gap-8"> <ul tabindex="0" class="absolute dropdown-content menu p-0 shadow bg-base-100 rounded-box w-52 justify-start">
<%= for relation <- @friend |> relations do %> <%= Relationship.types()
<% relationship = relation(@friend, relation) %> |> Tuple.to_list()
<div id={"relation-#{relation.id}"} class="card card-compact w-96 bg-base-100 shadow-xl"> |> Enum.map(fn(tuple) ->
<figure><img src="https://placeimg.com/400/225/people" alt={relation.id} /></figure> type = tuple |> elem(0)
<div class="card-body"> class = tuple |> elem(1) |> to_string
<h3 class="card-title">
<%= relation.name %> selected = (@relationship |> Relationship.get_type) == type
<%= if relationship |> Friends.Relationship.get_relation do %>
<div class={"badge badge-#{relationship |> Friends.Relationship.get_color}"}><%= relationship |> Friends.Relationship.get_relation %></div> if class != "hidden" do
<% end %> if selected do
</h3> "<li class='p-2 m-0 text-sm bg-slate-200 select-none' style='font-weight:normal;'>#{type}</li>"
<p>If a dog chews shoes whose shoes does he choose?</p> else
</div> "<li
</div> class='p-2 m-0 text-sm hover:bg-slate-400 hover:text-black hover:cursor-pointer'
<% end %> style='font-weight:normal;'
<%= if @friend |> relations |> Enum.empty? do %> onClick='#{relationship_type_function(@relationship.id, type)}'
<div class="italic p-4">No relationships on record yet.</div> >
<% end %> #{type}
</div> </li>"
end
end
end) |> Enum.join
|> raw
%>
</ul>
"""
end
def relationship_type_function(id, type) do
"""
relationType("#{id}", "#{type}")
""" """
end end
### ###
def edit_page(:overview, assigns) do
~H"""
<%= @peer_data.address |> Tuple.to_list |> Enum.join(".") %>
<.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>
<ul class="py-4 pl-0 h-1/2">
<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>
<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">Email:</strong>
<div class="flex flex-col h-16">
<%= text_input f, :email, class: "input input-primary input-sm md:input-md", phx_debounce: "blur", value: @friend.email %>
<div class="min-w-fit flex place-items-center mr-4"><%= error_tag f, :email %></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 %>
<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">
<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, class: "input input-primary input-sm md:input-md", phx_throttle: "500", value: @address_query %>
<%= hidden_input f, :address_id, value: 0 %>
</div>
</li>
</ul>
<div class="form-control flex flex-row gap-x-4 md:justify-end mb-4 md:w-1/2">
<div class="flex-1">
<.link patch={Routes.friends_show_path(FriendsWeb.Endpoint, :overview, @friend.slug)} class="btn btn-block btn-outline">back</.link>
</div>
<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 != :new do %>
<div class="flex-1">
<button phx-click="delete" phx-value-friend_id={@friend.id} class="btn btn-block btn-error">Delete</button>
</div>
<% end %>
</div>
</.form>
"""
end
def edit_page(:relationships, assigns) do
~H"""
"""
end
def edit_page(:timeline, assigns) do
~H"""
"""
end
end end
@@ -0,0 +1,9 @@
defmodule FriendsWeb.Components.Map do
use FriendsWeb, :live_component
def show(assigns) do
~H"""
<div id="map" phx-hook={"showMapbox"} latlon={@address_latlon}></div>
"""
end
end
+150 -32
View File
@@ -1,58 +1,148 @@
defmodule FriendsWeb.FriendsLive.Edit do defmodule FriendsWeb.FriendsLive.Edit do
use FriendsWeb, :live_view 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
friend = Friend.new()
live_action = socket.assigns.live_action || :overview
def mount(%{"slug" => slug} = _attrs, token, socket) do
live_action = socket.assigns.live_action || false
friend = Friend.get_by_slug(slug)
editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user])
# address_viewbox = Places.Search.query()
if(live_action) do
{:ok, {:ok,
socket socket
|> assign(:live_action, live_action) |> assign(:live_action, live_action)
|> assign_current_user(token |> Map.get("user_token")) |> assign_current_user(token |> Map.get("user_token"))
|> assign(:friend, friend) |> assign(:friend, friend)
|> assign(:address_latlon, nil)
|> assign(:search_results, nil)
|> title("Welcome")
|> assign(:changeset, %Friend{} |> Friend.changeset())}
end
# Has a slug means it's an edit profile form
def mount(%{"slug" => slug} = _attrs, token, socket) 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)) |> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:changeset, %Friend{} |> Friend.changeset()) |> assign(:changeset, %Friend{} |> Friend.changeset())
|> assign(:action, editable) |> assign(:search_query, nil)
|> assign(:address_query, nil) |> assign(:search_results, nil)}
|> assign(:peer_data, get_connect_info(socket, :peer_data))}
else else
{:ok, socket |> redirect(to: Routes.friends_show_path(socket, :overview, friend.slug))} {:ok, socket |> redirect(to: Routes.friends_show_path(socket, :overview, friend.slug))}
end end
end end
def handle_params(%{"slug" => slug} = attrs, _url, socket) do # Overview form page
live_action = socket.assigns.live_action || false 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])
{address_latlon, address_query} = friend |> Friend.get_address()
{:noreply,
socket
|> assign(:mode, :edit)
|> assign_friend(friend)
|> assign(:action, Routes.friends_path(socket, :update))
|> assign(:live_action, live_action)
|> 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) friend = Friend.get_by_slug(slug)
editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user])
{:noreply, {:noreply,
socket socket
|> assign(:mode, :edit)
|> assign_friend(friend) |> assign_friend(friend)
|> assign(:relationships, friend |> relations)
|> assign(:live_action, live_action) |> 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)) |> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:editable, editable)} |> push_navigate(
to: Routes.friends_show_path(FriendsWeb.Endpoint, :relationships, friend.slug)
)}
end end
def handle_event("validate", %{"friend" => form_params}, %{assigns: %{friend: friend}} = socket) do # Timeline edit
id = form_params["id"] 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()
{:noreply,
socket
|> assign_friend(friend)
|> assign(:live_action, socket.assigns.live_action)
|> assign(:address_query, nil)
|> assign(:action, Routes.friends_path(socket, :update))
|> title("Welcome")}
end
# 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"] name = form_params["name"]
nickname = form_params["nickname"] nickname = form_params["nickname"]
born = form_params["born"] born = form_params["born"]
email = form_params["email"] email = form_params["email"]
phone = form_params["phone"] |> format_phone phone = form_params["phone"] |> format_phone
address_query = form_params["address_query"] address_query = form_params["address_query"]
address_latlon = form_params["address_latlon"]
new_params = %{ new_params = %{
id: id, id: id,
@@ -75,6 +165,7 @@ defmodule FriendsWeb.FriendsLive.Edit do
|> assign(:changeset, changeset) |> assign(:changeset, changeset)
|> assign_friend(friend |> struct(new_params), changeset) |> assign_friend(friend |> struct(new_params), changeset)
|> assign(:address_query, address_query) |> assign(:address_query, address_query)
|> assign(:address_latlon, address_latlon)
} }
end end
@@ -82,23 +173,42 @@ defmodule FriendsWeb.FriendsLive.Edit do
def handle_event( def handle_event(
"save", "save",
%{"friend" => form_params}, %{"friend" => form_params},
%{assigns: %{changeset: changeset}} = socket %{assigns: %{changeset: _changeset}} = socket
) do ) do
name = form_params["name"] name = form_params["name"]
nickname = form_params["nickname"] nickname = form_params["nickname"]
born = form_params["born"] born = form_params["born"]
email = form_params["email"] email = form_params["email"]
phone = form_params["phone"] |> format_phone phone = form_params["phone"] |> format_phone
id = form_params["id"] slug = form_params["slug"] || name |> to_slug
id = form_params["id"] || :new
address_latlon = form_params["address_latlon"] |> Poison.decode!()
address_query = form_params["search_query"]
address = %Friends.Places.Place{
name: address_query,
latlon: address_latlon
}
new_address =
case address.latlon do
nil ->
%{id: nil}
_ ->
address
|> Friends.Places.Place.get_or_create()
end
new_params = %{ new_params = %{
id: id, id: id,
name: name, name: name,
nickname: nickname, nickname: nickname,
slug: name |> to_slug, slug: slug,
born: born, born: born,
phone: phone, phone: phone,
email: email email: email,
address_id: new_address.id
} }
updated_friend = Friend.create_or_update(new_params) updated_friend = Friend.create_or_update(new_params)
@@ -110,17 +220,25 @@ defmodule FriendsWeb.FriendsLive.Edit do
|> put_flash(:info, "Saved #{updated_friend |> first_name}!") |> put_flash(:info, "Saved #{updated_friend |> first_name}!")
|> assign(:new_friend, new_changeset) |> assign(:new_friend, new_changeset)
|> assign(:friend, updated_friend) |> assign(:friend, updated_friend)
|> push_patch(to: "/friend/#{updated_friend.slug}") |> push_navigate(to: "/friend/#{updated_friend.slug}")
} }
end end
# Handle deleting a friend def handle_event("address_search", %{"friend" => %{"search_query" => query}}, socket) do
def handle_event("delete", %{"friend_id" => friend_id}, socket) do results = Places.Search.autocomplete(query) |> Enum.map(&Places.Search.parse_features/1)
friend = Friend.get_by_id(friend_id)
if query == "" do
{:noreply, socket |> assign(:search_results, nil)}
else
{:noreply, {:noreply,
socket socket
|> put_flash(:error, "Deleted '#{friend.name}'.") |> assign(:search_results, results)
|> push_navigate(to: "/")} |> assign(:search_query, query)
|> assign(:select_fxn, "selectMapResult")}
end
end
def handle_event(_event, _unsigned_params, socket) do
{:noreply, socket}
end end
end end
+1 -1
View File
@@ -1,6 +1,6 @@
<section class="row"> <section class="row">
<article class="column prose"> <article class="column prose">
<%= edit_menu(assigns) %> <%= edit_menu(assigns) %>
<%= edit_page(@live_action, assigns) %> <%= apply(FriendsWeb.LiveViews.Edit, @live_action, [assigns]) %>
</article> </article>
</section> </section>
-8
View File
@@ -1,14 +1,6 @@
defmodule FriendsWeb.FriendsLive.Friend do defmodule FriendsWeb.FriendsLive.Friend do
use FriendsWeb, :live_view 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 # Initialize variables on first load
def mount(%{}, token, socket) do def mount(%{}, token, socket) do
{:ok, {: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
+115 -9
View File
@@ -1,40 +1,146 @@
defmodule FriendsWeb.FriendsLive.Show do defmodule FriendsWeb.FriendsLive.Show do
use FriendsWeb, :live_view 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 live_action = socket.assigns.live_action || false
friend = Friend.get_by_slug(slug) friend = Friend.get_by_slug(slug)
editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user])
{latlon, address} = friend |> Friend.get_address()
if(live_action) do if(live_action) do
{:ok, {:ok,
socket socket
|> assign(:mode, :show)
|> assign(:live_action, live_action) |> assign(:live_action, live_action)
|> assign_current_user(token |> Map.get("user_token")) |> assign_current_user(token |> Map.get("user_token"))
|> assign(:friend, friend) |> assign(:friend, friend)
|> assign(:address, address)
|> assign(:address_latlon, latlon |> Poison.encode!())
|> title(friend.name <> " - " <> (live_action |> titlecase)) |> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:changeset, %Friend{} |> Friend.changeset()) |> assign(:changeset, %Friend{} |> Friend.changeset())
|> assign(:action, editable)} |> assign(:editable, editable)
|> assign(:action, :moot)}
else else
{:ok, socket |> redirect(to: Routes.friends_show_path(socket, :overview, friend.slug))} {:ok, socket |> redirect(to: Routes.friends_show_path(socket, :overview, friend.slug))}
end end
end end
def handle_params(%{"slug" => slug} = attrs, _url, socket) do def handle_params(
live_action = socket.assigns.live_action || false %{"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) friend = Friend.get_by_slug(slug)
editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user]) editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user])
{:noreply, {:noreply,
socket socket
|> assign(:mode, :edit)
|> assign_friend(friend) |> assign_friend(friend)
|> assign(:relationships, friend |> relations)
|> assign(:live_action, live_action) |> assign(:live_action, live_action)
|> title(friend.name <> " - " <> (live_action |> titlecase)) |> assign(:search_query, nil)
|> assign(:editable, editable)} |> 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
end end
+3 -3
View File
@@ -2,11 +2,11 @@
<article class="column prose"> <article class="column prose">
<%= menu(assigns) %> <%= menu(assigns) %>
<%= header(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"> <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> </div>
<% end %> <% end %>
</article> </article>
+17 -2
View File
@@ -73,6 +73,8 @@ defmodule FriendsWeb.Router do
post "/confirm", UserConfirmationController, :create post "/confirm", UserConfirmationController, :create
get "/confirm/:token", UserConfirmationController, :edit get "/confirm/:token", UserConfirmationController, :edit
post "/confirm/:token", UserConfirmationController, :update post "/confirm/:token", UserConfirmationController, :update
live "/welcome", FriendsLive.Edit, :welcome
end end
# Routes that require the user be authenticated: # Routes that require the user be authenticated:
@@ -103,14 +105,27 @@ defmodule FriendsWeb.Router do
live "/:slug/relationships", FriendsLive.Show, :relationships live "/:slug/relationships", FriendsLive.Show, :relationships
end 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) # Edit modes (require being logged in and having a profile)
scope "/friend/update/", FriendsWeb do scope "/edit/", FriendsWeb do
pipe_through [:browser, :require_authenticated_user, :capture_profile] pipe_through [:browser, :require_authenticated_user, :capture_profile]
post "/", FriendsController, :update
live "/:slug", FriendsLive.Edit
live "/:slug/overview", FriendsLive.Edit, :overview live "/:slug/overview", FriendsLive.Edit, :overview
live "/:slug/timeline", FriendsLive.Edit, :timeline live "/:slug/timeline", FriendsLive.Edit, :timeline
live "/:slug/relationships", FriendsLive.Edit, :relationships live "/:slug/relationships", FriendsLive.Edit, :relationships
end
post "/:slug/update", FriendsController, :update # API
scope "/api", FriendsWeb do
pipe_through :api
delete "/relationship/:id", RelationshipsController, :delete
end end
end end
@@ -1,12 +1,16 @@
<ul class="p-2 shadow menu menu-compact dropdown-content bg-base-100 text-neutral rounded-box w-52 flex flex-col gap-4">
<%= if @current_user do %> <%= if @current_user do %>
<li class="p-2 pb-4 border-b-2"><%= @current_user.email %></li> <label tabindex="0" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img src="https://placeimg.com/80/80/people" />
</div>
</label>
<ul class="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 text-neutral rounded-box w-52">
<li><%= @current_user.email %></li>
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li> <li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
<li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li> <li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
</ul>
<% else %> <% else %>
<%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "btn" %> <%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "btn" %>
<%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "btn btn-primary" %> <%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "btn btn-primary" %>
<%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "btn" %>
<%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "btn btn-primary" %>
<% end %> <% end %>
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="corporate"> <!-- pastel --> <html lang="en" data-theme="cupcake"> <!-- pastel -->
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
@@ -10,10 +10,8 @@ defmodule FriendsWeb.ErrorHelpers do
""" """
def error_tag(form, field) do def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error -> Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error), field = field |> Atom.to_string() |> String.capitalize()
class: "invalid-feedback", content_tag(:span, "#{translate_error(error)}", class: "block mt-1 text-sm text-red-700")
phx_feedback_for: input_name(form, field)
)
end) end)
end end
+51 -3
View File
@@ -1,11 +1,59 @@
defmodule FriendsWeb.LiveHelpers do defmodule FriendsWeb.LiveHelpers do
use FriendsWeb, :live_component
alias Friends.Friend alias Friends.Friend
use Phoenix.LiveComponent
def titlecase(atom) do def titlecase(atom) do
atom |> to_string |> :string.titlecase() atom |> to_string |> :string.titlecase()
end end
def parse_id("new"), do: "new"
def parse_id(int), do: int |> String.to_integer()
def display_phone(phone, changeset) do
if changeset.errors[:phone] do
phone
else
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
10 ->
country = "+1 "
[area, first, sec1, sec2] =
phone |> to_charlist |> Enum.chunk_every(3) |> Enum.map(&(&1 |> to_string))
"#{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 def assign_current_user(socket, user_token) do
user = user =
case user_token do case user_token do
@@ -35,15 +83,15 @@ defmodule FriendsWeb.LiveHelpers do
def assign_friend(socket, friend) do def assign_friend(socket, friend) do
socket socket
|> assign(:friend, friend) |> assign(:friend, friend)
|> assign(:editable, friend |> Friend.can_be_edited_by(socket.assigns[:current_user]))
|> assign(:changeset, friend |> Friend.changeset()) |> assign(:changeset, friend |> Friend.changeset())
|> assign(:relationships, friend.relationships)
end end
# Same thing, but this time we have a changeset we want to keep # Same thing, but this time we have a changeset we want to keep
def assign_friend(socket, friend, changeset) do def assign_friend(socket, friend, changeset) do
socket socket
|> assign(:friend, friend) |> assign(:friend, friend)
|> assign(:editable, friend |> Friend.can_be_edited_by(socket.assigns[:current_user]))
|> assign(:changeset, changeset) |> assign(:changeset, changeset)
|> assign(:relationships, friend.relationships)
end end
end end
+34 -2
View File
@@ -74,16 +74,48 @@ defmodule Helpers do
birthday(friend) |> Date.diff(Date.utc_today()) birthday(friend) |> Date.diff(Date.utc_today())
end end
def relations(friend) do def relations(friend, include_self \\ false) do
list =
[friend.relationships, friend.reverse_relationships] [friend.relationships, friend.reverse_relationships]
|> List.flatten() |> List.flatten()
|> Enum.dedup()
if include_self, do: list, else: list |> Enum.filter(&(&1.id != friend.id))
end end
def relation(friend, friend2) do def relation(friend, friend2) do
Friends.Relationship.get(friend, friend2) Friends.Relationship.get(friend, friend2)
end end
def events(relationship) do def events(%Friends.Relationship{} = relationship) do
relationship.events relationship.events
end 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 end
@@ -9,9 +9,14 @@ defmodule FriendsWeb.FriendsLiveTest do
} = friend_fixture(%{email: user_fixture().email}) |> Friends.Friend.assign_user() } = friend_fixture(%{email: user_fixture().email}) |> Friends.Friend.assign_user()
end end
describe "GET '/friends/:slug'" do describe "GET '/friend/:slug'" do
test "redirects if live_action not specified", %{conn: conn, friend: friend} do
conn = conn |> get("/friend/#{friend.slug}/")
assert redirected_to(conn) == Routes.friends_show_path(conn, :overview, friend.slug)
end
test "shows the friend overview", %{conn: conn, friend: friend} do test "shows the friend overview", %{conn: conn, friend: friend} do
conn = conn |> get("/friends/#{friend.slug}") conn = conn |> get("/friend/#{friend.slug}/overview")
assert html_response(conn, 200) =~ friend.name assert html_response(conn, 200) =~ friend.name
end end
end end