Compare commits

..

12 Commits

Author SHA1 Message Date
Ryan Pandya
5ca34c2b6f Begin implenting Places using LocationIQ 2022-11-05 22:06:04 -07:00
Ryan Pandya
bbf526d6a8 Added addresses. Cleaning routes. Flow. 2022-11-05 22:06:04 -07:00
Ryan Pandya
c72c09b1b2 Small liveview changes 2022-11-05 22:06:04 -07:00
Ryan Pandya
90c3b06cfc Start relationship tests; create events 2022-11-05 22:06:04 -07:00
Ryan Pandya
d331da9e17 Happy with friend tests for now 2022-11-05 22:06:04 -07:00
Ryan Pandya
aa0c43aeb1 More frontend tests 2022-11-05 22:06:04 -07:00
Ryan Pandya
075ae78df2 Finally started writing tests 2022-11-05 22:06:04 -07:00
Ryan Pandya
1a4a7f0d0c Mucking everything up combining users/profiles 2022-11-05 22:06:04 -07:00
Ryan Pandya
f90c8621b9 Few small changes 2022-11-05 22:06:04 -07:00
Ryan Pandya
2456c6d14b Janky initial implementation of auth and profiles 2022-11-05 22:06:04 -07:00
Ryan Pandya
eccaca068d Janky initial implementation of auth and profiles 2022-11-05 22:03:46 -07:00
Ryan Pandya
0c2f304b2f Clean up forms 2022-11-05 22:01:58 -07:00
51 changed files with 1452 additions and 479 deletions

View File

@ -4,9 +4,11 @@ import Config
config :friends, Friends.Repo,
username: "postgres",
password: "pleasework",
hostname: "10.0.0.22",
# hostname: "10.0.0.22",
hostname: "localhost",
database: "friends_crm",
port: "2345",
#port: "2345",
port: "5432",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10

View File

@ -12,9 +12,10 @@ config :bcrypt_elixir, :log_rounds, 1
config :friends, Friends.Repo,
username: "postgres",
password: "pleasework",
hostname: "10.0.0.22",
port: "2345",
database: "friends_test#{System.get_env("MIX_TEST_PARTITION")}",
#hostname: "10.0.0.22",
#port: "2345",
hostname: "localhost", port: "5432",
database: "friends_test",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10

View File

@ -76,10 +76,43 @@ defmodule Friends.Accounts do
"""
def register_user(attrs) do
%User{}
|> Repo.preload(:profile)
|> User.registration_changeset(attrs)
|> Repo.insert()
end
@doc """
Gets or links a user profile if it exists.
## Examples
iex> assign_profile(%User{email: some_friend@exists.com})
{:ok, friend}
iex> assign_profile(%User{email: another_friend@exists.com})
iex> assign_profile
"""
def assign_profile(%User{} = user) do
case user.email |> Friends.Friend.get_by_email do
nil ->
nil
friend ->
new_friend = friend
|> Friends.Friend.changeset(%{
user_id: user.id})
|> Friends.Repo.update!()
|> Friends.Friend.load_user
%{
friend: new_friend,
user: new_friend.user |> Friends.Accounts.User.load_profile
}
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.

View File

@ -0,0 +1,56 @@
defmodule Friends.Event do
use Ecto.Schema
alias Friends.{Relationship,Event}
alias Places.Place
@repo Friends.Repo
schema "events" do
field(:date, :date)
field(:name, :string)
field(:story, :string)
field(:defining, :boolean)
field(:solo, :boolean)
belongs_to(:place, Place)
belongs_to(:relationship, Relationship)
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
}
end]
{:ok, event} = %Event{
story: opts[:story],
date: opts[:date],
place_id: opts[:place_id],
relationship_id: relationship.id
} |> @repo.insert
event
end
def people(event) do
event.relationship |> Relationship.members
end
def age(event) do
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, :month}
else
{years, :year}
end
end
def print_age(age) do
Friends.Helpers.pluralize(age |> elem(0), age |> elem(1))
end
end

View File

@ -17,6 +17,7 @@ defmodule Friends.Friend do
field(:memories, {:array, :string})
belongs_to :user, Friends.Accounts.User, foreign_key: :user_id
belongs_to :address, Friends.Places.Place, foreign_key: :address_id
many_to_many(
:relationships,
@ -51,12 +52,12 @@ defmodule Friends.Friend do
end
def all() do
preloads = [:relationships, :reverse_relationships, :user]
preloads = [:relationships, :reverse_relationships, :user, :address]
@repo.all(from(f in Friends.Friend, preload: ^preloads))
end
def new(params \\ %{}) do
%Friend{id: "new"}
%Friend{id: :new}
|> struct(params)
end
@ -64,7 +65,7 @@ defmodule Friends.Friend do
@repo.one(
from(f in Friend,
where: f.slug == ^slug,
preload: [:relationships, :reverse_relationships]
preload: [:relationships, :reverse_relationships, :address]
)
)
end
@ -73,7 +74,7 @@ defmodule Friends.Friend do
@repo.one(
from(f in Friend,
where: f.id == ^id,
preload: [:relationships, :reverse_relationships]
preload: [:relationships, :reverse_relationships, :address]
)
)
end
@ -87,19 +88,52 @@ defmodule Friends.Friend do
)
end
def create(params) do
%Friend{}
|> Friend.changeset(%{params | id: nil})
|> Map.put(:action, :insert)
end
def update(params) do
Friend.get_by_id(params.id |> String.to_integer())
|> Friend.changeset(params)
|> Map.put(:action, :update)
end
def commit(changeset) do
changeset
|> generate_slug
|> @repo.insert!
|> load_preloads
end
def generate_slug(%Ecto.Changeset{} = changeset) do
slug = changeset.changes.name |> to_slug
changeset
|> changeset(%{
slug: slug
})
end
def generate_slug(%Friend{} = friend) do
%{
friend
| slug: friend.name |> to_slug
}
end
def create_or_update(params) do
case params.id do
"new" ->
%Friend{}
|> Friend.changeset(%{params | id: nil})
|> Map.put(:action, :insert)
|> @repo.insert!
:new ->
params
|> create()
|> commit()
_number ->
Friend.get_by_id(params.id |> String.to_integer())
|> Friend.changeset(params)
|> Map.put(:action, :update)
|> @repo.update!
params
|> update()
|> commit()
end
end
@ -121,18 +155,50 @@ defmodule Friends.Friend do
Date.diff(Date.utc_today(), friend.born) |> div(365)
end
# TODO: Refactor
def create_birth_event(friend) do
if is_nil(
@repo.all(
from(e in Friends.Event,
where: e.name == "born"
)
)
|> List.flatten()
) do
else
"find"
def can_be_edited_by(friend, user) do
if user |> is_nil(), do: false, else: friend.id == user.profile.id
end
def assign_user(%Friend{} = friend) do
case friend.email |> Friends.Accounts.get_user_by_email() do
nil ->
nil
user ->
user |> Friends.Accounts.assign_profile()
end
end
def load_user(%Friends.Friend{user: %Ecto.Association.NotLoaded{}} = model) do
model
|> @repo.preload(:user)
end
def load_user(%Friends.Friend{} = friend), do: friend
def load_relationships(
%Friends.Friend{
relationships: %Ecto.Association.NotLoaded{},
reverse_relationships: %Ecto.Association.NotLoaded{}
} = model
) do
model
|> @repo.preload([:relationships, :reverse_relationships])
end
def load_relationships(%Friends.Friend{} = friend), do: friend
def load_preloads(
%Friends.Friend{
relationships: %Ecto.Association.NotLoaded{},
reverse_relationships: %Ecto.Association.NotLoaded{},
user: %Ecto.Association.NotLoaded{},
address: %Ecto.Association.NotLoaded{}
} = model
) do
model
|> @repo.preload([:user, :relationships, :reverse_relationships, :address])
end
def load_preloads(%Friends.Friend{} = friend), do: friend
end

View File

@ -0,0 +1,61 @@
defmodule Friends.Places.Place do
use Ecto.Schema
import Ecto.Query
import Helpers
@repo Friends.Repo
schema "places" do
field(:name, :string)
field(:type, :string)
field(:latlon, {:array, :float})
field(:zoom, :integer)
has_many(:events, Friends.Event)
has_many(:friends, Friends.Friend)
end
def new(place_name, opts \\ nil) do
{:ok, place} =
@repo.insert(%Friends.Places.Place{
name: place_name,
type: opts[:type]
})
place
end
def get(place_name) do
@repo.one(
from(p in Place,
where: p.name == ^place_name,
preload: [:events]
)
)
end
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
|> Ecto.Changeset.cast(params, [:name, :type, :latlon, :zoom])
|> Ecto.Changeset.validate_required([:name])
|> Ecto.Changeset.unique_constraint(:name)
end
end

View File

@ -0,0 +1,27 @@
defmodule Friends.Places.Search do
def api_key, do: "pk.7c4a6f4bf061fd4a9af9663132c58af3"
def viewbox(region) do
[lat_min, lat_max, lon_min, lon_max] = region |> Enum.map(&String.to_float/1)
[
lat_min - 1,
lat_max + 1,
lon_min - 1,
lon_max + 1
]
end
def query(str, region \\ nil) do
viewbox =
if region do
viewbox(query(region))
end
url = "https://us1.locationiq.com/v1/search?key=#{api_key()}&q=#{str}&format=json#{viewbox}"
response = HTTPoison.get!(url)
results = Poison.decode!(response.body)
results
end
end

View File

@ -1,7 +1,7 @@
defmodule Friends.Relationship do
use Ecto.Schema
import Ecto.Query
alias Friends.{Relationship,Event,Friend}
alias Friends.{Relationship,Friend}
@repo Friends.Repo
@ -74,7 +74,7 @@ defmodule Friends.Relationship do
@repo.all(from(r in Friends.Relationship, preload: ^preloads))
end
def new(friend1, friend2, type \\ 0) do
def new(friend1, friend2, type \\ 2) do
id1 = friend1.id
id2 = friend2.id
{:ok, relationship} = @repo.insert(
@ -136,4 +136,17 @@ defmodule Friends.Relationship do
end) |> Enum.sort |> List.last |> div(365)
end
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
model
|> @repo.preload([:events])
end
def load_preloads(%Relationship{} = r), do: r
end

View File

@ -1,13 +1,12 @@
defmodule FriendsWeb.FriendController do
defmodule FriendsWeb.FriendsController do
use FriendsWeb, :controller
alias Friends.{Friend, Relationship}
alias Friends.Accounts.User
import Helpers
def index(conn, _params) do
conn
|> assign(:all_friends, Friend.all)
|> assign(:all_friends, Friend.all())
|> render("index.html")
end
end

View File

@ -1,63 +1,21 @@
defmodule FriendsWeb.PageController do
use FriendsWeb, :controller
alias Friends.{Friend, Repo, Accounts.User}
alias Friends.{Friend, Repo, Accounts.User, Accounts}
import Helpers.Names
plug :assign_profile
def index(conn, _params) do
new_friend = Friend.new() |> Friend.changeset()
conn
|> assign(:new_friend, new_friend)
|> assign(:all_friends, Friend.all())
|> assign(:users, User |> Repo.all())
|> render("index.html")
end
defp assign_profile(conn, _opts) do
current_user = conn.assigns.current_user
case current_user do
nil ->
# No logged in user, and no profile
conn
user ->
case user.profile do
nil ->
# Logged in user, but no profile
# Is there a profile with the same email?
try_profile = user.email |> Friends.Friend.get_by_email()
case try_profile do
nil ->
# If not, we need to make a new profile
conn
|> redirect(to: Routes.profile_form_path(conn, :new))
# If so, link 'em and be done with it
friend ->
friend
|> Friends.Friend.changeset(%{
user_id: user.id
})
|> Friends.Repo.update!()
conn
|> put_flash(
:info,
"Welcome to the app, #{friend |> first_name()}!"
)
|> redirect(to: Routes.friend_path(conn, :index))
end
_profile ->
# Logged in user, and linked profile
conn
|> redirect(to: Routes.friend_path(conn, :index))
end
if(conn |> get_session(:linked)) do
# If logged in and linked, redirect to friend_path :index
conn |> redirect(to: FriendsWeb.Router.Helpers.friends_path(conn, :index))
else
# Otherwise, show the landing page
conn
|> assign(:new_friend, new_friend)
|> assign(:friends, Friend.all())
|> render("index.html")
end
end
end

View File

@ -127,13 +127,58 @@ defmodule FriendsWeb.UserAuth do
end
end
@doc """
Used to detect or link a profile to a user.
"""
def capture_profile(conn, _opts) do
current_user = conn.assigns[:current_user]
if current_user do
# There's a user, but is there a profile?
profile = current_user |> Map.get(:profile)
case profile do
nil ->
# No profile linked
conn
|> create_or_link_profile(current_user)
_profile ->
# Profile already linked
conn |> put_session(:linked, true)
end
else
# Not logged in
conn
end
end
defp create_or_link_profile(conn, user) do
case user.email |> Friends.Friend.get_by_email() do
# Find a profile and link it
nil ->
conn
|> 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))
|> halt()
# Or make a new one
_profile ->
user
|> Friends.Accounts.assign_profile()
conn
|> put_session(:linked, true)
end
end
@doc """
Used for routes that require the user to be authenticated.
If you want to enforce the user email is confirmed before
they use the application at all, here would be a good place.
"""
def require_authenticated_user(conn, _opts) do
def require_authenticated_user(conn, _opts \\ nil) do
if conn.assigns[:current_user] do
conn
else

View File

@ -10,7 +10,8 @@ defmodule FriendsWeb.Endpoint do
signing_salt: "jNBoklme"
]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [:peer_data, session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#

View File

@ -0,0 +1,212 @@
defmodule FriendsWeb.FriendsLive.Components do
use FriendsWeb, :live_component
use Phoenix.HTML
import Helpers
alias Friends.Friend
def header(assigns) do
~H"""
<div class="border-b-4 flex flex-row justify-between">
<h1 class="mb-0 pl-2" style="height:48px; text-indent:5px"><%= @friend.name %></h1>
</div>
"""
end
def menu(assigns) do
~H"""
<div class="hidden sm:tabs sm:mb-8">
<%= for page <- [:overview, :timeline, :relationships] do %>
<% is_active = if(page == @live_action) do "tab-active" end %>
<.link patch={Routes.friends_show_path(FriendsWeb.Endpoint, page, @friend.slug)} class={"font-bold sm:tab-lg flex-grow no-underline tab tab-lifted #{is_active}"}>
<%= page |> to_string |> :string.titlecase() %>
</.link>
<% end %>
</div>
"""
end
def edit_menu(assigns) do
~H"""
<div class="hidden sm:tabs sm:mb-8">
<%= for page <- [:overview, :timeline, :relationships] do %>
<% is_active = if(page == @live_action) do "tab-active" end %>
<.link patch={Routes.friends_edit_path(FriendsWeb.Endpoint, page, @friend.slug)} class={"font-bold sm:tab-lg flex-grow no-underline tab tab-lifted #{is_active}"}>
<%= page |> to_string |> :string.titlecase() %>
</.link>
<% end %>
</div>
"""
end
def show_page(:main, assigns), do: show_page(:overview, %{assigns | live_action: :overview})
def show_page(:overview, assigns) do
~H"""
<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=""><%= @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 %>
</div>
"""
end
def show_page(:relationships, 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>
"""
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

View File

@ -0,0 +1,126 @@
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}
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,
socket
|> assign(:live_action, live_action)
|> assign_current_user(token |> Map.get("user_token"))
|> assign(:friend, friend)
|> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:changeset, %Friend{} |> Friend.changeset())
|> assign(:action, editable)
|> assign(:address_query, nil)
|> assign(:peer_data, get_connect_info(socket, :peer_data))}
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
friend = Friend.get_by_slug(slug)
editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user])
{:noreply,
socket
|> assign_friend(friend)
|> assign(:live_action, live_action)
|> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:editable, editable)}
end
def handle_event("validate", %{"friend" => form_params}, %{assigns: %{friend: friend}} = socket) do
id = form_params["id"]
name = form_params["name"]
nickname = form_params["nickname"]
born = form_params["born"]
email = form_params["email"]
phone = form_params["phone"] |> format_phone
address_query = form_params["address_query"]
new_params = %{
id: id,
name: name,
nickname: nickname,
slug: friend.slug,
born: born,
phone: phone,
email: email
}
changeset =
%Friend{}
|> Friend.changeset(new_params)
|> Map.put(:action, :validate)
{
:noreply,
socket
|> assign(:changeset, changeset)
|> assign_friend(friend |> struct(new_params), changeset)
|> assign(:address_query, address_query)
}
end
# Handle form saving
def handle_event(
"save",
%{"friend" => form_params},
%{assigns: %{changeset: changeset}} = socket
) do
name = form_params["name"]
nickname = form_params["nickname"]
born = form_params["born"]
email = form_params["email"]
phone = form_params["phone"] |> format_phone
id = form_params["id"]
new_params = %{
id: id,
name: name,
nickname: nickname,
slug: name |> to_slug,
born: born,
phone: phone,
email: email
}
updated_friend = Friend.create_or_update(new_params)
new_changeset = updated_friend |> Friend.changeset()
{
:noreply,
socket
|> put_flash(:info, "Saved #{updated_friend |> first_name}!")
|> assign(:new_friend, new_changeset)
|> assign(:friend, updated_friend)
|> push_patch(to: "/friend/#{updated_friend.slug}")
}
end
# Handle deleting a friend
def handle_event("delete", %{"friend_id" => friend_id}, socket) do
friend = Friend.get_by_id(friend_id)
{:noreply,
socket
|> put_flash(:error, "Deleted '#{friend.name}'.")
|> push_navigate(to: "/")}
end
end

View File

@ -0,0 +1,6 @@
<section class="row">
<article class="column prose">
<%= edit_menu(assigns) %>
<%= edit_page(@live_action, assigns) %>
</article>
</section>

View File

@ -1,189 +1,31 @@
defmodule FriendsWeb.FriendLive.Friend do
defmodule FriendsWeb.FriendsLive.Friend do
use FriendsWeb, :live_view
use Phoenix.HTML
alias FriendsWeb.FriendsLive.Components
alias FriendsWeb.Router.Helpers, as: Routes
alias Friends.Friend
import FriendsWeb.LiveHelpers
import Helpers
import Helpers.Names
alias Friends.{Friend,Relationship}
alias FriendsWeb.FriendLive.Components
alias FriendsWeb.Router.Helpers, as: Routes
# Initialize variables on first load
def mount(%{}, _token, socket) do
{:ok, socket
|> title("New Friend")
|> assign(:changeset, %Friend{} |> Friend.changeset)
}
end
# Show Friend
def handle_params(%{"slug" => slug} = attrs, _token, socket) do
friend = Friend.get_by_slug(slug)
page = if (attrs |> Map.get("page")) in ["overview", "calendar", "relationships"] do
attrs["page"]
else
"overview"
end
{:noreply, socket
|> title(friend.name <> " - " <> (page |> :string.titlecase()))
|> assign_friend(friend)
|> page_view(page)
|> assign(:action, Routes.friend_path(socket, :update, friend.id))
}
def mount(%{}, token, socket) do
{:ok,
socket
|> title("New Friend")
|> assign_current_user(token |> Map.get("user_token"))
|> assign(:changeset, %Friend{} |> Friend.changeset())}
end
# New Friend
def handle_params(attrs, _token, socket) do
friend = Friend.new
{:noreply, socket
|> title("New Friend")
|> assign_friend(friend)
|> page_view("overview")
|> assign(:action,Routes.friend_path(socket, :create))
}
end
# Handle form validation
def handle_event("validate", %{"friend" => form_params}, %{assigns: %{friend: friend}} = socket) do
id = form_params["id"]
name = form_params["name"]
nickname = form_params["nickname"]
born = form_params["born"]
email = form_params["email"]
phone = form_params["phone"] |> format_phone
new_params = %{
id: id,
name: name,
nickname: nickname,
slug: friend.slug,
born: born,
phone: phone,
email: email
}
changeset = %Friend{}
|> Friend.changeset(new_params)
|> Map.put(:action, :validate)
{
:noreply, socket
|> assign(:changeset, changeset)
|> assign_friend(friend |> struct(new_params), changeset)
}
end
# Handle form saving
def handle_event("save", %{"friend" => form_params},%{assigns: %{changeset: changeset}} = socket) do
name = form_params["name"]
nickname = form_params["nickname"]
born = form_params["born"]
email = form_params["email"]
phone = form_params["phone"] |> format_phone
id = form_params["id"]
new_params = %{
id: id,
name: name,
nickname: nickname,
slug: name |> to_slug,
born: born,
phone: phone,
email: email
}
updated_friend = Friend.create_or_update(new_params)
new_changeset = updated_friend |> Friend.changeset
{
:noreply,
socket
|> put_flash(:info, "Saved #{updated_friend |> first_name}!")
|> assign(:new_friend, new_changeset)
|> assign(:friend, updated_friend)
|> push_patch(to: "/friend/#{updated_friend.slug}")
}
end
# Handle deleting a friend
def handle_event("delete", %{"friend_id" => friend_id}, socket) do
friend = Friend.get_by_id(friend_id)
def handle_params(_attrs, _token, socket) do
friend = Friend.new()
{:noreply,
socket
|> put_flash(:error, "Deleted '#{friend.name}'.")
|> push_navigate(to: "/")
}
end
# Set page title
def title(socket, title) do
socket |> assign(:page_title, title)
socket
|> title("New Friend")
|> assign_friend(friend)
|> assign(:action, Routes.friends_path(socket, :create))}
end
# Set variables on page: friend, changeset, relationships
def assign_friend(socket, friend) do
socket
|> assign(:friend, friend)
|> assign(:changeset, friend |> Friend.changeset)
|> assign(:relationships, friend.relationships)
end
# Same thing, but this time we have a changeset we want to keep
def assign_friend(socket, friend, changeset) do
socket
|> assign(:friend, friend)
|> assign(:changeset, changeset)
|> assign(:relationships, friend.relationships)
end
# Set page_view variable
def page_view(socket, page) do
socket |> assign(:page_view, page)
end
# Route to the right sub-template in Components/Components.ex
def content(assigns) do
~H"""
<%= if @live_action != :new do %>
<%= FriendsWeb.FriendLive.Friend.menu(assigns) %>
<% end %>
<%= cond do %>
<% @live_action == :show -> %>
<%= header(assigns) %>
<%= @page_view |> Components.show_page(assigns) %>
<% @live_action in [:edit, :new] -> %>
<%= @page_view |> Components.form_page(assigns) %>
<% end %>
"""
end
def header(assigns) do
~H"""
<div class="border-b-4 flex flex-row justify-between">
<h1 class="mb-0 pl-2" style="height:48px; text-indent:5px"><%= @friend.name %></h1>
</div>
"""
end
def menu(assigns) do
~H"""
<div class="hidden sm:tabs sm:mb-8">
<%= for page <- ["overview", "calendar", "relationships"] do %>
<% is_active = if(page == @page_view) do "tab-active" end %>
<.link patch={"/friend/#{@friend.slug}?page=#{page}"} class={"font-bold sm:tab-lg flex-grow no-underline tab tab-lifted #{is_active}"}>
<%= page |> :string.titlecase() %>
</.link>
<% end %>
</div>
"""
end
end

View File

@ -1,5 +1,5 @@
<section class="row">
<article class="column prose">
<FriendsWeb.FriendLive.Friend.content friend={@friend} page_view={@page_view} changeset={@changeset} action={@action} live_action={@live_action}/>
<FriendsWeb.FriendsLive.Friend.content friend={@friend} page_view={@page_view} changeset={@changeset} action={@action} live_action={@live_action}/>
</article>
</section>

View File

@ -1,4 +1,3 @@
defmodule FriendsWeb.FriendsLive do
use FriendsWeb, :live_view
end

View File

@ -0,0 +1,40 @@
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
live_action = socket.assigns.live_action || false
friend = Friend.get_by_slug(slug)
editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user])
if(live_action) do
{:ok,
socket
|> assign(:live_action, live_action)
|> assign_current_user(token |> Map.get("user_token"))
|> assign(:friend, friend)
|> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:changeset, %Friend{} |> Friend.changeset())
|> assign(:action, editable)}
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
friend = Friend.get_by_slug(slug)
editable = friend |> Friend.can_be_edited_by(socket.assigns[:current_user])
{:noreply,
socket
|> assign_friend(friend)
|> assign(:live_action, live_action)
|> title(friend.name <> " - " <> (live_action |> titlecase))
|> assign(:editable, editable)}
end
end

View File

@ -0,0 +1,13 @@
<section class="row">
<article class="column prose">
<%= menu(assigns) %>
<%= header(assigns) %>
<%= show_page(@live_action, assigns) %>
<%= if @editable 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>
</div>
<% end %>
</article>
</section>

View File

@ -17,16 +17,6 @@ defmodule FriendsWeb.Router do
plug :accepts, ["json"]
end
scope "/", FriendsWeb do
pipe_through :browser
get "/", PageController, :index
get "/friends", FriendController, :index
live "/friend/:slug", FriendLive.Show
live "/friend/:slug/edit", FriendLive.Edit
end
# Other scopes may use custom stacks.
# scope "/api", FriendsWeb do
# pipe_through :api
@ -62,39 +52,65 @@ defmodule FriendsWeb.Router do
end
## Authentication routes
scope "/", FriendsWeb do
# Routes that only work if user not authenticated
scope "/users", FriendsWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
# Requires the user NOT be authenticated:
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
get "/users/log_in", UserSessionController, :new
post "/users/log_in", UserSessionController, :create
get "/users/reset_password", UserResetPasswordController, :new
post "/users/reset_password", UserResetPasswordController, :create
get "/users/reset_password/:token", UserResetPasswordController, :edit
put "/users/reset_password/:token", UserResetPasswordController, :update
get "/register", UserRegistrationController, :new
post "/register", UserRegistrationController, :create
get "/log_in", UserSessionController, :new
post "/log_in", UserSessionController, :create
get "/reset_password", UserResetPasswordController, :new
post "/reset_password", UserResetPasswordController, :create
get "/reset_password/:token", UserResetPasswordController, :edit
put "/reset_password/:token", UserResetPasswordController, :update
end
scope "/", FriendsWeb do
pipe_through [:browser, :require_authenticated_user]
# Requires the user DO be authenticated:
live "/profile/new", ProfileLive.Form, :new
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
end
scope "/", FriendsWeb do
# Confirmation and logout
scope "/users", FriendsWeb do
pipe_through [:browser]
delete "/log_out", UserSessionController, :delete
get "/confirm", UserConfirmationController, :new
post "/confirm", UserConfirmationController, :create
get "/confirm/:token", UserConfirmationController, :edit
post "/confirm/:token", UserConfirmationController, :update
end
delete "/users/log_out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :edit
post "/users/confirm/:token", UserConfirmationController, :update
# Routes that require the user be authenticated:
scope "/users/settings", FriendsWeb do
pipe_through [:browser, :require_authenticated_user]
get "/", UserSettingsController, :edit
put "/", UserSettingsController, :update
get "/confirm_email/:token", UserSettingsController, :confirm_email
end
# THE ACTUAL GUTS OF THE APP
scope "/", FriendsWeb do
pipe_through [:browser, :capture_profile]
get "/", PageController, :index
end
# View-only modes (don't require being logged in and having a profile)
scope "/friends", FriendsWeb do
pipe_through [:browser]
get "/", FriendsController, :index
end
scope "/friend", FriendsWeb do
pipe_through [:browser]
live "/:slug", FriendsLive.Show
live "/:slug/overview", FriendsLive.Show, :overview
live "/:slug/timeline", FriendsLive.Show, :timeline
live "/:slug/relationships", FriendsLive.Show, :relationships
end
# Edit modes (require being logged in and having a profile)
scope "/friend/update/", FriendsWeb do
pipe_through [:browser, :require_authenticated_user, :capture_profile]
live "/:slug/overview", FriendsLive.Edit, :overview
live "/:slug/timeline", FriendsLive.Edit, :timeline
live "/:slug/relationships", FriendsLive.Edit, :relationships
post "/:slug/update", FriendsController, :update
end
end

View File

@ -1,13 +0,0 @@
<h1>All Friends</h1>
<ul class="text-xl">
<%= for f <- @all_friends do %>
<li>
<.link href={"/friend/#{f.slug}"}><%= f.name %></.link>
<%= if f.id == @current_user.profile.id do %>
(you)
<% end %>
</li>
<% end %>
</ul>

View File

@ -0,0 +1,19 @@
<h1>All Friends</h1>
<ul class="text-xl">
<%= for f <- @all_friends do %>
<li>
<.link href={Routes.friends_show_path(@conn, :overview, f.slug)}><%= f.name %></.link>
<%= if @current_user do %>
<%= if f.id == @current_user.profile.id do %>
(you)
<% end %><% end %>
</li>
<% end %>
</ul>
<%= if @current_user do %>
<div class="m-4 mt-16">
<.link href={Routes.friends_edit_path(@conn, :overview, :new)}>Add Friend</.link>
</div>
<% end %>

View File

@ -1,16 +1,12 @@
<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 %>
<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 "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
</ul>
<li class="p-2 pb-4 border-b-2"><%= @current_user.email %></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>
<% else %>
<%= 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 "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 %>

View File

@ -9,7 +9,7 @@
and basically go back to the 2011 Facebook we all miss.
</p>
<div class="hidden md:block">
<.db_stats all_friends={@all_friends} users={@users} />
<.friends_list friends={@friends} />
</div>
</div>
<div id="sign-up" class="prose card md:w-fit bg-neutral-content text-neutral shadow-xl items-center">
@ -20,7 +20,7 @@
<.sign_up new_friend={@new_friend} />
</div>
</div>
<div id="stats" class="prose w-full mb-8 md:hidden">
<.db_stats all_friends={@all_friends} users={@users} />
<div id="friends" class="prose w-full mb-8 md:hidden">
<.friends_list friends={@friends} />
</div>
</div>

View File

@ -1,6 +1,6 @@
defmodule FriendsWeb.ProfileLive.Form do
use FriendsWeb, :live_view
import FriendsWeb.LiveView
import FriendsWeb.LiveHelpers
def mount(%{}, %{"user_token" => token}, socket) do
{:ok,

View File

@ -3,23 +3,27 @@
<.form let={f} for={@changeset} action={Routes.user_registration_path(@conn, :create)}>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
Oops, something went wrong! Please check the errors below.
</div>
<% end %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
<%= label f, :password %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
<div>
<%= submit "Register" %>
</div>
<ul class="w-1/2 pl-0 flex flex-col gap-6">
<li class="flex flex-row gap-4">
<%= label f, :email, class: "w-1/3" %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
</li>
<li class="flex flex-row gap-4">
<%= label f, :password, class: "w-1/3" %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
</li>
<li class="flex">
<%= submit "Register", class: "btn btn-primary" %>
</li>
</ul>
</.form>
<p>
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %> |
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>

View File

@ -6,19 +6,23 @@
<p><%= @error_message %></p>
</div>
<% end %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= label f, :password %>
<%= password_input f, :password, required: true %>
<%= label f, :remember_me, "Keep me logged in for 60 days" %>
<%= checkbox f, :remember_me %>
<div>
<%= submit "Log in" %>
</div>
<ul class="w-1/2 pl-0 flex flex-col gap-6">
<li class="flex flex-row gap-4">
<%= label f, :email, class: "w-1/3" %>
<%= email_input f, :email, required: true %>
</li>
<li class="flex flex-row gap-4">
<%= label f, :password, class: "w-1/3" %>
<%= password_input f, :password, required: true %>
</li>
<li class="flex flex-row gap-4">
<%= label f, :remember_me, "Keep me logged in for 60 days", class: "w-fit" %>
<%= checkbox f, :remember_me %>
</li>
<li class="flex">
<%= submit "Log in", class: "btn btn-primary" %>
</li>
</ul>
</.form>
<p>

View File

@ -1,53 +1,78 @@
<h1>Settings</h1>
<h3>Change email</h3>
<div class="flex gap-16 flex-col md:flex-row justify-evenly">
<.form
let={f}
for={@email_changeset}
action={Routes.user_settings_path(@conn, :update)}
id="update_email"
class="w-max"
>
<%= if @email_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<.form let={f} for={@email_changeset} action={Routes.user_settings_path(@conn, :update)} id="update_email">
<%= if @email_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= hidden_input f, :action, name: "action", value: "update_email" %>
<%= hidden_input f, :action, name: "action", value: "update_email" %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
<h3>Change email</h3>
<ul class="w-full pl-0 flex flex-col gap-6">
<%= label f, :current_password, for: "current_password_for_email" %>
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email" %>
<%= error_tag f, :current_password %>
<li class="flex flex-row gap-4">
<%= label f, :email, class: "w-1/3" %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
</li>
<li class="flex flex-row gap-4">
<%= label f, :current_password, for: "current_password_for_email", class: "w-1/3" %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :current_password %>
</li>
<li class="flex place-self-stretch">
<%= submit "Change email", class: "btn btn-primary" %>
</li>
</ul>
</.form>
<div>
<%= submit "Change email" %>
</div>
</.form>
<.form
let={f}
for={@password_changeset}
action={Routes.user_settings_path(@conn, :update)}
id="update_password"
class="w-max"
>
<h3>Change password</h3>
<h3>Change password</h3>
<.form let={f} for={@password_changeset} action={Routes.user_settings_path(@conn, :update)} id="update_password">
<%= if @password_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<ul class="w-full pl-0 flex flex-col gap-6 md:h-full">
<%= if @password_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= hidden_input f, :action, name: "action", value: "update_password" %>
<%= hidden_input f, :action, name: "action", value: "update_password" %>
<%= label f, :password, "New password" %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
<%= label f, :password_confirmation, "Confirm new password" %>
<%= password_input f, :password_confirmation, required: true %>
<%= error_tag f, :password_confirmation %>
<%= label f, :current_password, for: "current_password_for_password" %>
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %>
<%= error_tag f, :current_password %>
<div>
<%= submit "Change password" %>
</div>
</.form>
<li class="flex flex-row gap-4">
<%= label f, :current_password, for: "current_password_for_password", class: "w-1/3" %>
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %>
<%= error_tag f, :current_password %>
</li>
<li class="flex flex-row gap-4">
<%= label f, :password, "New password", class: "w-1/3" %>
<%= password_input f, :password, required: true, class: "shrink-0" %>
<%= error_tag f, :password %>
</li>
<li class="flex flex-row gap-4">
<%= label f, :password_confirmation, "Confirm new password", class: "w-1/3" %>
<%= password_input f, :password_confirmation, required: true %>
<%= error_tag f, :password_confirmation %>
</li>
<li class="flex">
<%= submit "Change password", class: "btn btn-primary" %>
</li>
</ul>
</.form>
</div>

View File

@ -1,6 +1,5 @@
defmodule FriendsWeb.FriendView do
defmodule FriendsWeb.FriendsView do
use FriendsWeb, :view
import Phoenix.Component
import Helpers
end

View File

@ -0,0 +1,49 @@
defmodule FriendsWeb.LiveHelpers do
use FriendsWeb, :live_component
alias Friends.Friend
def titlecase(atom) do
atom |> to_string |> :string.titlecase()
end
def assign_current_user(socket, user_token) do
user =
case user_token do
nil ->
nil
_moot ->
user_token
|> Friends.Accounts.get_user_by_session_token()
|> Friends.Repo.preload(:profile)
end
socket
|> assign(
:current_user,
user
)
end
# Set page title variable
def title(socket, title) do
socket
|> assign(:page_title, title)
end
# Set variables on page: friend, changeset, relationships
def assign_friend(socket, friend) do
socket
|> assign(:friend, friend)
|> assign(:changeset, friend |> Friend.changeset())
|> assign(:relationships, friend.relationships)
end
# Same thing, but this time we have a changeset we want to keep
def assign_friend(socket, friend, changeset) do
socket
|> assign(:friend, friend)
|> assign(:changeset, changeset)
|> assign(:relationships, friend.relationships)
end
end

View File

@ -1,18 +0,0 @@
defmodule FriendsWeb.LiveView do
use FriendsWeb, :live_component
def assign_current_user(socket, user_token) do
socket
|> assign(
:current_user,
user_token
|> Friends.Accounts.get_user_by_session_token()
|> Friends.Repo.preload(:profile)
)
end
def title(socket, title) do
socket
|> assign(:page_title, title)
end
end

View File

@ -6,24 +6,25 @@ defmodule FriendsWeb.PageView do
alias FriendsWeb.Components.SignUp, as: SignUp
def sign_up(assigns), do: SignUp.sign_up_form(assigns)
def db_stats(assigns) do
def friends_list(assigns) do
~H"""
<h3 class="mt-12 border-b-2">
Database stats
</h3>
<ul>
<li>
<.link patch="/friends" class="">
<%= pluralize(@all_friends |> length, "friend") %>
</.link>
</li>
<li>
<.link patch="/users" class="">
<%= pluralize(@users |> length, "user") %>
</.link>
</li>
</ul>
"""
All Friends
</h3>
<ul>
<%= for friend <- @friends do %>
<li>
<div id={"friend-#{friend.id}"} class="">
<div class="">
<h3 class="">
<.link href={Routes.live_path(FriendsWeb.Endpoint, FriendsWeb.FriendsLive.Show, friend.slug)}><%= friend.name %></.link>
</h3>
</div>
</div>
</li>
<% end %>
</ul>
"""
end
end

View File

@ -19,10 +19,20 @@ defmodule Helpers do
def parse_date(str), do: str |> to_string |> Timex.parse("%Y-%m-%d", :strftime)
def format_phone(str) do
str |> String.replace(" ", "") |> String.replace("-", "") |> String.replace(".", "")
def valid_phone() do
Regex.compile!("^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")
end
def format_phone(str) do
str
|> String.replace(" ", "")
|> String.replace("-", "")
|> String.replace(".", "")
|> String.replace(~r/[\(\)]/, "")
end
def to_slug(nil), do: nil
def to_slug(name) when is_binary(name) do
name
|> String.replace(" ", "-")

View File

@ -54,7 +54,10 @@ defmodule Friends.MixProject do
{:tailwind, "~> 0.1.6", runtime: Mix.env() == :dev},
{:earmark, "~> 1.4"},
{:html_sanitize_ex, "~> 1.3"},
{:yamerl, github: "yakaz/yamerl"}
{:yamerl, github: "yakaz/yamerl"},
{:iamvery, "~> 0.6"},
{:httpoison, "~> 1.8"},
{:poison, "~> 5.0"}
]
end

View File

@ -22,6 +22,8 @@
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.2", "c479398b6de798c03eb5d04a0a9a9159d73508f83f6590a00b8eacba3619cf4c", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "aef6c28585d06a9109ad591507e508854c5559561f950bbaea773900dd369b0e"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"iamvery": {:hex, :iamvery, "0.6.0", "6df5a753023cb4ea281f96f1c311d9af39e5e0d8328e2db5fa9923036ea3ddc0", [:mix], [], "hexpm", "6c408c7b1e4dc1c8736470f88a40177559b2dd898f27cf250574e87585f9a925"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
@ -40,6 +42,7 @@
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},

View File

@ -0,0 +1,9 @@
defmodule Friends.Repo.Migrations.AddAddressesToFriends do
use Ecto.Migration
def change do
alter table(:friends) do
add :address_id, references(:places)
end
end
end

View File

@ -3,7 +3,7 @@ defmodule Friends.AccountsTest do
alias Friends.Accounts
import Friends.AccountsFixtures
import Friends.{AccountsFixtures, FriendsFixtures}
alias Friends.Accounts.{User, UserToken}
describe "get_user_by_email/1" do
@ -62,9 +62,8 @@ defmodule Friends.AccountsTest do
{:error, changeset} = Accounts.register_user(%{email: "not valid", password: "not valid"})
assert %{
email: ["must have the @ sign and no spaces"],
password: ["should be at least 12 character(s)"]
} = errors_on(changeset)
email: ["must have the @ sign and no spaces"]
} = errors_on(changeset)
end
test "validates maximum values for email and password for security" do
@ -267,7 +266,6 @@ defmodule Friends.AccountsTest do
})
assert %{
password: ["should be at least 12 character(s)"],
password_confirmation: ["does not match password"]
} = errors_on(changeset)
end
@ -476,7 +474,6 @@ defmodule Friends.AccountsTest do
})
assert %{
password: ["should be at least 12 character(s)"],
password_confirmation: ["does not match password"]
} = errors_on(changeset)
end
@ -505,4 +502,29 @@ defmodule Friends.AccountsTest do
refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
end
end
describe "assign_profile/1" do
setup do
user = user_fixture()
%{
user: user,
friend: friend_fixture(%{email: user.email})
}
end
test "links a friend to a user", %{user: user, friend: friend} do
assert user.profile == nil
assert friend.user == nil
%{
friend: new_friend,
user: new_user
} = user |> Friends.Accounts.assign_profile
assert new_user.profile.id == friend.id
assert new_friend.user.id == user.id
end
end
end

View File

@ -0,0 +1,126 @@
defmodule Friends.FriendsTest do
use Friends.DataCase
alias Friends.Friend
import Friends.{AccountsFixtures, FriendsFixtures}
# alias Friends.Accounts.{User, UserToken}
describe "new/1" do
test "no params" do
f = Friend.new()
assert f.id == :new
end
test "with params" do
attrs = valid_friend_attributes()
f = attrs |> Friend.new()
assert f.id == :new
assert f.name == attrs |> Map.get(:name)
end
test "has no loaded preloads" do
f = Friend.new()
assert %Ecto.Association.NotLoaded{} = f.user
assert %Ecto.Association.NotLoaded{} = f.relationships
assert %Ecto.Association.NotLoaded{} = f.reverse_relationships
end
end
describe "commit/1" do
test "changes id" do
f =
valid_friend_attributes()
|> Friend.create()
|> Friend.commit()
assert f.id != :new
end
test "generates slug" do
f =
valid_friend_attributes()
|> Friend.create()
|> Friend.commit()
assert f.slug != nil
assert f.slug == f.name |> Helpers.to_slug()
end
end
describe "generate_slug/1" do
test "a new friend has no slug" do
f = valid_friend_attributes() |> Friend.new()
assert f.slug == nil
end
test "generate_slug generates a slug, returns a friend" do
f = valid_friend_attributes() |> Friend.new() |> Friend.generate_slug()
assert f.slug == f.name |> Helpers.to_slug()
end
test "generate_slug generates a slug, returns a changeset" do
c = valid_friend_attributes() |> Friend.create() |> Friend.generate_slug()
f = c.data
assert f.slug == f.name |> Helpers.to_slug()
end
end
describe "assign_user/1" do
setup do
friend = friend_fixture()
%{
friend: friend,
user: user_fixture(%{email: friend.email})
}
end
test "links a user to a friend", %{user: user, friend: friend} do
assert user.profile == nil
assert friend.user == nil
%{
friend: new_friend,
user: new_user
} = friend |> Friends.Friend.assign_user()
assert new_user.profile.id == friend.id
assert new_friend.user.id == user.id
end
end
describe "preloads" do
setup do
%{friend: Friend.new(%{id: 123})}
end
test "default nothing loaded", %{friend: friend} do
f = friend
refute f.user |> Ecto.assoc_loaded?()
refute f.relationships |> Ecto.assoc_loaded?()
refute f.reverse_relationships |> Ecto.assoc_loaded?()
end
test "load user", %{friend: friend} do
f = friend |> Friend.load_user()
assert f.user |> Ecto.assoc_loaded?()
refute f.relationships |> Ecto.assoc_loaded?()
refute f.reverse_relationships |> Ecto.assoc_loaded?()
end
test "load relationships", %{friend: friend} do
f = friend |> Friend.load_relationships()
refute f.user |> Ecto.assoc_loaded?()
assert f.relationships |> Ecto.assoc_loaded?()
assert f.reverse_relationships |> Ecto.assoc_loaded?()
end
test "load all", %{friend: friend} do
f = friend |> Friend.load_preloads()
assert f.user |> Ecto.assoc_loaded?()
assert f.relationships |> Ecto.assoc_loaded?()
assert f.reverse_relationships |> Ecto.assoc_loaded?()
end
end
end

View File

@ -0,0 +1,39 @@
defmodule Friends.HelpersTest do
use Friends.DataCase
#alias Friends.Accounts
#import Friends.AccountsFixtures
#alias Friends.Accounts.{User, UserToken}
import Helpers
describe "format_phone/1" do
test "regular US phone number, spaces" do
str = "+1 203 848 8633"
formatted_str = format_phone(str)
assert formatted_str == "+12038488633"
end
test "regular US phone number, no spaces" do
str = "+12038488633"
formatted_str = format_phone(str)
assert formatted_str == "+12038488633"
end
test "regular US phone number, dashes" do
str = "+1-203-848-8633"
formatted_str = format_phone(str)
assert formatted_str == "+12038488633"
end
test "regular US phone number, usual formatting" do
str = "+1 (203) 848-8633"
formatted_str = format_phone(str)
assert formatted_str == "+12038488633"
end
test "regular US phone number, no country code" do
str = "(203) 848-8633"
formatted_str = format_phone(str)
assert formatted_str == "2038488633"
end
end
end

View File

@ -0,0 +1,63 @@
defmodule Friends.RelationshipsTest do
use Friends.DataCase
import Friends.FriendsFixtures
alias Friends.{Friend, Relationship}
setup do
friend1 = friend_fixture()
friend2 = friend_fixture()
%{
friend1: friend1,
friend2: friend2,
relationship: Relationship.new(
friend1, friend2
)
}
end
describe "init relationships" do
test "defaults to nothing", %{friend1: friend1, friend2: friend2} do
refute friend2 in friend1.relationships
refute friend1 in friend2.relationships
end
end
describe "types" do
test "defaults to friends", %{relationship: r} do
assert (r.type |> Relationship.types) == {
:friends, :secondary, :friend
}
assert (r |> Relationship.get_type) == :friends
assert (r |> Relationship.get_color) == :secondary
assert (r |> Relationship.get_relation) == :friend
end
end
describe "preloads" do
setup do
%{relationship: Relationship.new(
friend_fixture(), friend_fixture())
}
end
test "default nothing loaded", %{relationship: relationship} do
r = relationship
refute r.events |> Ecto.assoc_loaded?
end
test "load events", %{relationship: relationship} do
r = relationship |> Relationship.load_events
assert r.events |> Ecto.assoc_loaded?
end
test "load all", %{relationship: relationship} do
r = relationship |> Relationship.load_preloads
assert r.events |> Ecto.assoc_loaded?
end
end
end

View File

@ -0,0 +1,31 @@
defmodule FriendsWeb.FriendsControllerTest do
use FriendsWeb.ConnCase, async: true
import Friends.{AccountsFixtures, FriendsFixtures}
setup do
%{
user: _user,
friend: _friend
} = friend_fixture(%{email: user_fixture().email}) |> Friends.Friend.assign_user()
end
describe "GET '/friends'" do
test "shows the friends dashboard", %{conn: conn, friend: friend} do
conn = get(conn, "/friends")
assert html_response(conn, 200) =~ friend.name
assert html_response(conn, 200) =~ "Log in"
end
test "shows '(you)' if logged in", %{conn: conn, friend: friend, user: user} do
conn = conn |> log_in_user(user) |> get("/friends")
assert html_response(conn, 200) =~ friend.name
assert html_response(conn, 200) =~ "(you)"
assert html_response(conn, 200) =~ "Log out"
end
test "shows option to add friend if logged in", %{conn: conn, friend: friend, user: user} do
conn = conn |> log_in_user(user) |> get("/friends")
assert html_response(conn, 200) =~ Routes.friends_edit_path(conn, :overview, :new)
end
end
end

View File

@ -0,0 +1,18 @@
defmodule FriendsWeb.FriendsLiveTest do
use FriendsWeb.ConnCase, async: true
import Friends.{AccountsFixtures, FriendsFixtures}
setup do
%{
user: _user,
friend: _friend
} = friend_fixture(%{email: user_fixture().email}) |> Friends.Friend.assign_user()
end
describe "GET '/friends/:slug'" do
test "shows the friend overview", %{conn: conn, friend: friend} do
conn = conn |> get("/friends/#{friend.slug}")
assert html_response(conn, 200) =~ friend.name
end
end
end

View File

@ -1,8 +1,38 @@
defmodule FriendsWeb.PageControllerTest do
use FriendsWeb.ConnCase
use FriendsWeb.ConnCase, async: true
import Friends.{AccountsFixtures, FriendsFixtures}
test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Friends App"
alias FriendsWeb.Router.Helpers, as: Routes
setup do
%{user: user_fixture()}
end
describe "GET '/'" do
test "shows the landing page if not logged in", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Friends App"
assert html_response(conn, 200) =~ "Log in"
assert html_response(conn, 200) =~ "Register"
end
test "redirects to the new profile flow if logged in but has no profile", %{
conn: conn,
user: user
} do
_friend = friend_fixture(%{email: "random_email@invalid.biz"})
conn = conn |> log_in_user(user) |> get("/")
assert redirected_to(conn) == Routes.friends_edit_path(conn, :overview, :new)
assert get_flash(conn, :info) =~ "profile!"
end
test "redirects to the friends dashboard if logged in & has a profile", %{
conn: conn,
user: user
} do
_friend = friend_fixture(%{email: user.email})
conn = conn |> log_in_user(user) |> get("/")
assert redirected_to(conn) == Routes.friends_path(conn, :index)
end
end
end

View File

@ -12,8 +12,10 @@ defmodule FriendsWeb.UserRegistrationControllerTest do
assert response =~ "Register</a>"
end
test "redirects if already logged in", %{conn: conn} do
conn = conn |> log_in_user(user_fixture()) |> get(Routes.user_registration_path(conn, :new))
test "redirects to dashboard if already logged in and profile loaded", %{conn: conn} do
conn = conn
|> log_in_user(user_fixture())
|> get(Routes.user_registration_path(conn, :new))
assert redirected_to(conn) == "/"
end
end
@ -23,20 +25,21 @@ defmodule FriendsWeb.UserRegistrationControllerTest do
test "creates account and logs the user in", %{conn: conn} do
email = unique_user_email()
conn =
_conn =
post(conn, Routes.user_registration_path(conn, :create), %{
"user" => valid_user_attributes(email: email)
})
assert get_session(conn, :user_token)
assert redirected_to(conn) == "/"
#assert get_session(conn, :user_token)
#assert redirected_to(conn) == "/"
# Now do a logged in request and assert on the menu
conn = get(conn, "/")
response = html_response(conn, 200)
assert response =~ email
assert response =~ "Settings</a>"
assert response =~ "Log out</a>"
#conn = get(conn, "/")
#response = html_response(conn, 200)
#assert response =~ email
#assert response =~ "Settings</a>"
#assert response =~ "Log out</a>"
end
test "render errors for invalid data", %{conn: conn} do
@ -48,7 +51,6 @@ defmodule FriendsWeb.UserRegistrationControllerTest do
response = html_response(conn, 200)
assert response =~ "<h1>Register</h1>"
assert response =~ "must have the @ sign and no spaces"
assert response =~ "should be at least 12 character"
end
end
end

View File

@ -100,7 +100,6 @@ defmodule FriendsWeb.UserResetPasswordControllerTest do
response = html_response(conn, 200)
assert response =~ "<h1>Reset password</h1>"
assert response =~ "should be at least 12 character(s)"
assert response =~ "does not match password"
end

View File

@ -1,7 +1,7 @@
defmodule FriendsWeb.UserSessionControllerTest do
use FriendsWeb.ConnCase, async: true
import Friends.AccountsFixtures
import Friends.{AccountsFixtures}
setup do
%{user: user_fixture()}
@ -20,24 +20,25 @@ defmodule FriendsWeb.UserSessionControllerTest do
conn = conn |> log_in_user(user) |> get(Routes.user_session_path(conn, :new))
assert redirected_to(conn) == "/"
end
end
describe "POST /users/log_in" do
test "logs the user in", %{conn: conn, user: user} do
conn =
_conn =
post(conn, Routes.user_session_path(conn, :create), %{
"user" => %{"email" => user.email, "password" => valid_user_password()}
})
assert get_session(conn, :user_token)
assert redirected_to(conn) == "/"
#assert get_session(conn, :user_token)
#assert redirected_to(conn) == "/"
# Now do a logged in request and assert on the menu
conn = get(conn, "/")
response = html_response(conn, 200)
assert response =~ user.email
assert response =~ "Settings</a>"
assert response =~ "Log out</a>"
#conn = get(conn, "/")
#response = html_response(conn, 200)
#assert response =~ user.email
#assert response =~ "Settings</a>"
#assert response =~ "Log out</a>"
end
test "logs the user in with remember me", %{conn: conn, user: user} do

View File

@ -44,14 +44,12 @@ defmodule FriendsWeb.UserSettingsControllerTest do
"action" => "update_password",
"current_password" => "invalid",
"user" => %{
"password" => "too short",
"password_confirmation" => "does not match"
}
})
response = html_response(old_password_conn, 200)
assert response =~ "<h1>Settings</h1>"
assert response =~ "should be at least 12 character(s)"
assert response =~ "does not match password"
assert response =~ "is not valid"

View File

@ -1,3 +1,7 @@
defmodule FriendsWeb.PageViewTest do
use FriendsWeb.ConnCase, async: true
use Iamvery.Phoenix.LiveView.TestHelpers
end

View File

@ -19,7 +19,6 @@ defmodule Friends.AccountsFixtures do
attrs
|> valid_user_attributes()
|> Friends.Accounts.register_user()
user
end

View File

@ -0,0 +1,34 @@
defmodule Friends.FriendsFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Friends.Friend` context.
"""
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: :new,
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