Skip to content

Commit

Permalink
Add championship draft chat MVP
Browse files Browse the repository at this point in the history
* Add chats and messages
* Add fantasy league drafts
* Add liveview test and chat component
* broadcast messages
  • Loading branch information
axelclark committed Apr 6, 2024
1 parent 219809c commit 1853aa7
Show file tree
Hide file tree
Showing 18 changed files with 693 additions and 7 deletions.
16 changes: 16 additions & 0 deletions assets/js/hooks/chat-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const ChatScrollToBottom = {
mounted() {
this.el.scrollTo(0, this.el.scrollHeight)
},

updated() {
const pixelsBelowBottom =
this.el.scrollHeight - this.el.clientHeight - this.el.scrollTop

if (pixelsBelowBottom < this.el.clientHeight * 0.3) {
this.el.scrollTo(0, this.el.scrollHeight)
}
},
}

export default ChatScrollToBottom
2 changes: 2 additions & 0 deletions assets/js/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import SortableInputsFor from "./sortable-hooks"
import ChatScrollToBottom from "./chat-hooks"

export default {
SortableInputsFor,
ChatScrollToBottom,
}
63 changes: 63 additions & 0 deletions lib/ex338/chats.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
defmodule Ex338.Chats do
@moduledoc """
The Chats context.
"""

import Ecto.Query

alias Ex338.Accounts.User
alias Ex338.Chats.Chat
alias Ex338.Chats.Message
alias Ex338.Repo

def subscribe(%Chat{} = chat, %User{}) do
Phoenix.PubSub.subscribe(Ex338.PubSub, topic(chat))
end

def subscribe(_chat, nil), do: nil

def create_chat(chat_params) do
%Chat{}
|> Chat.changeset(chat_params)
|> Repo.insert()
end

def create_message(message_params) do
%Message{}
|> Message.changeset(message_params)
|> Repo.insert()
|> case do
{:ok, message} ->
message = Repo.preload(message, :user)
broadcast(message, %Ex338.Events.MessageCreated{message: message})
{:ok, message}

other ->
other
end
end

def change_message(%Message{} = message, attrs \\ %{}) do
Message.changeset(message, attrs)
end

def list_chats do
Repo.all(Chat)
end

def get_chat(chat_id) do
query =
from c in Chat,
where: c.id == ^chat_id,
preload: [messages: :user]

Repo.one(query)
end

defp broadcast(%Message{} = message, event) do
Phoenix.PubSub.broadcast(Ex338.PubSub, topic(message), {__MODULE__, event})
end

defp topic(%Message{} = message), do: "chat:#{message.chat_id}"
defp topic(%Chat{} = chat), do: "chat:#{chat.id}"
end
25 changes: 25 additions & 0 deletions lib/ex338/chats/chat.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Ex338.Chats.Chat do
@moduledoc false
use Ecto.Schema

import Ecto.Changeset

alias Ex338.Chats.Message

schema "chats" do
field :room_name, :string

has_many :messages, Message, preload_order: [asc: :inserted_at]
timestamps()
end

@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:room_name])
|> validate_required([:room_name])
|> unique_constraint(:room_name)
end
end
22 changes: 22 additions & 0 deletions lib/ex338/chats/message.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Ex338.Chats.Message do
@moduledoc false
use Ecto.Schema

import Ecto.Changeset

schema "messages" do
field :content, :string
belongs_to :user, Ex338.Accounts.User
belongs_to :chat, Ex338.Chats.Chat

timestamps()
end

@doc false
def changeset(message, attrs \\ %{}) do
message
|> cast(attrs, [:content, :user_id, :chat_id])
|> validate_required([:content, :chat_id])
|> validate_length(:content, min: 1, max: 280)
end
end
9 changes: 9 additions & 0 deletions lib/ex338/events.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Ex338.Events do
@moduledoc """
Defines Event structs for use within the pubsub system.
"""
defmodule MessageCreated do
@moduledoc false
defstruct message: nil
end
end
20 changes: 20 additions & 0 deletions lib/ex338/fantasy_leagues.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
defmodule Ex338.FantasyLeagues do
@moduledoc false

import Ecto.Query

alias Ex338.DraftPicks
alias Ex338.FantasyLeagues.FantasyLeague
alias Ex338.FantasyLeagues.FantasyLeagueDraft
alias Ex338.FantasyLeagues.HistoricalRecord
alias Ex338.FantasyLeagues.HistoricalWinning
alias Ex338.FantasyTeams
Expand Down Expand Up @@ -91,4 +94,21 @@ defmodule Ex338.FantasyLeagues do
{league.fantasy_league_name, league.id}
end)
end

def create_fantasy_league_draft!(attrs) do
%FantasyLeagueDraft{}
|> FantasyLeagueDraft.changeset(attrs)
|> Repo.insert!()
end

def get_draft_by_league_and_championship(fantasy_league, championship) do
query =
from(d in FantasyLeagueDraft,
where:
d.fantasy_league_id == ^fantasy_league.id and d.championship_id == ^championship.id,
preload: [chat: [messages: :user]]
)

Repo.one(query)
end
end
23 changes: 23 additions & 0 deletions lib/ex338/fantasy_leagues/fantasy_league_draft.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Ex338.FantasyLeagues.FantasyLeagueDraft do
@moduledoc false
use Ecto.Schema

import Ecto.Changeset

schema "fantasy_league_drafts" do
belongs_to(:fantasy_league, Ex338.FantasyLeagues.FantasyLeague)
belongs_to(:championship, Ex338.Championships.Championship)
belongs_to(:chat, Ex338.Chats.Chat)

timestamps()
end

@doc false
def changeset(fantasy_league_draft, attrs) do
fantasy_league_draft
|> cast(attrs, [:chat_id, :championship_id, :fantasy_league_id])
|> validate_required([:fantasy_league_id, :chat_id])
|> unique_constraint([:championship_id, :fantasy_league_id])
|> unique_constraint([:chat_id])
end
end
21 changes: 20 additions & 1 deletion lib/ex338_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ defmodule Ex338Web.CoreComponents do
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
range radio search select tel text textarea time url week)
range radio search select tel text textarea time url week commenttextarea)

attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
Expand Down Expand Up @@ -459,6 +459,25 @@ defmodule Ex338Web.CoreComponents do
"""
end

def input(%{type: "commenttextarea"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<textarea
id={@id}
name={@name}
class={[
@class,
@errors == [] && "border-gray-300 focus:border-indigo-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end

# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
Expand Down
155 changes: 155 additions & 0 deletions lib/ex338_web/live/championship_live/chat_component.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
defmodule Ex338Web.ChampionshipLive.ChatComponent do
@moduledoc false
use Ex338Web, :live_component

alias Ex338.Chats

@impl true
def update(%{message: message} = assigns, socket) do
changeset = Chats.change_message(message)

{:ok,
socket
|> assign(assigns)
|> assign(:message, message)
|> assign_form(changeset)}
end

defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, :form, to_form(changeset))
end

@impl true
def handle_event("validate", %{"message" => message_params}, socket) do
message_params = add_chat_and_user_to_params(message_params, socket)

changeset =
socket.assigns.message
|> Chats.change_message(message_params)
|> Map.put(:action, :validate)

{:noreply, assign_form(socket, changeset)}
end

def handle_event("save", %{"message" => message_params}, socket) do
message_params = add_chat_and_user_to_params(message_params, socket)

case Chats.create_message(message_params) do
{:ok, _message} ->
{:noreply, push_patch(socket, to: socket.assigns.patch)}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end

defp add_chat_and_user_to_params(params, socket) do
params
|> Map.put("user_id", socket.assigns.current_user.id)
|> Map.put("chat_id", socket.assigns.chat.id)
end

@impl true
def render(assigns) do
~H"""
<div class="overflow-hidden bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<ul
id="messages"
phx-update="stream"
role="list"
phx-hook="ChatScrollToBottom"
class="space-y-4 flex flex-col h-[800px] overflow-y-auto overflow-x-hidden pb-6"
>
<.comment :for={{id, message} <- @messages} id={id} message={message} />
</ul>
<div class="flex gap-x-3">
<.user_icon name={@current_user.name} class="!mt-0" />
<.form
id="create-message"
for={@form}
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="relative flex-auto"
>
<div class="overflow-hidden rounded-lg pb-12 shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
<label for="comment" class="sr-only">Add your comment</label>
<.input
field={@form[:content]}
phx-debounce="blur"
type="commenttextarea"
rows="2"
class="block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="Add your comment..."
/>
</div>
<div class="absolute inset-x-0 bottom-0 flex justify-end py-2 pl-3 pr-2">
<button
type="submit"
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
Comment
</button>
</div>
</.form>
</div>
</div>
</div>
"""
end

defp comment(%{message: %{user: nil}} = assigns) do
~H"""
<li id={@id} class="flex gap-x-4">
<div class="flex h-6 w-6 flex-none items-center justify-center bg-white">
<.icon name="hero-check-circle" class="h-6 w-6 text-indigo-600" />
</div>
<p class="flex-auto py-0.5 text-xs leading-5 text-gray-500">
<%= @message.content %>
</p>
</li>
"""
end

defp comment(assigns) do
~H"""
<li id={@id} class="flex gap-x-4">
<.user_icon name={@message.user.name} />
<div class="flex-auto">
<div class="flex justify-between items-start gap-x-4">
<div class="text-xs leading-5 font-medium text-gray-900">
<%= @message.user.name %>
</div>
</div>
<p class="text-sm leading-6 text-gray-500">
<%= @message.content %>
</p>
</div>
</li>
"""
end

attr :name, :string, required: true
attr :class, :string, default: nil

defp user_icon(assigns) do
~H"""
<div class={[
"h-6 w-6 flex flex-shrink-0 items-center justify-center bg-gray-600 rounded-full text-xs font-medium text-white",
@class
]}>
<%= get_initials(@name) %>
</div>
"""
end

defp get_initials(name) do
name
|> String.split(" ")
|> Enum.take(2)
|> Enum.map_join("", &String.at(&1, 0))
end
end
Loading

0 comments on commit 1853aa7

Please sign in to comment.