Skip to content

Commit c93e731

Browse files
refactor channels; use static HTML template
1 parent 68252f5 commit c93e731

File tree

5 files changed

+195
-87
lines changed

5 files changed

+195
-87
lines changed

assets/js/socket.js

Lines changed: 77 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,55 +17,48 @@ if (window.pointingParty.username) {
1717
.receive('error', resp => { console.log('Unable to join', resp) })
1818
}
1919

20-
const renderTemplate = function(parent, template) {
21-
while (parent.firstChild) {
22-
parent.removeChild(parent.firstChild)
23-
}
24-
25-
const fragment = document.createRange().createContextualFragment(template)
26-
parent.appendChild(fragment)
27-
}
28-
2920
const startButton = document.querySelector('.start-button')
3021
startButton.addEventListener('click', e => {
3122
driving = true;
3223
channel.push('start_pointing', {})
3324
})
3425

35-
const cardContainer = document.querySelector('.card-container')
36-
channel.on('new_card', state => {
37-
const template =
38-
'<div class="card text-left">' +
39-
' <div class="card-header">' +
40-
' <h2>' + state.card.title + '</h2>' +
41-
' </div>' +
42-
' <div class="card-body">' +
43-
' <p class="card-text">' + state.card.description + '</p>' +
44-
' <div class="form-group text-left points-container">' +
45-
' <div class="form-row align-items-center">' +
46-
' <div class="col-2">' +
47-
' <label for="story-points">Story Points</label>' +
48-
' <select class="form-control story-points" id="story-points">' +
49-
' <option>1</option>' +
50-
' <option>2</option>' +
51-
' <option>3</option>' +
52-
' <option>5</option>' +
53-
' </select>' +
54-
' </div>' +
55-
' </div>' +
56-
' <a href="#" class="btn btn-primary calculate-points">Vote!</a>' +
57-
' </div>' +
58-
' </div>' +
59-
'</div>'
26+
const nextCardButtons = document.getElementsByClassName('next-card')
27+
for (let i = 0;i < nextCardButtons.length; i++) {
28+
nextCardButtons[i].addEventListener('click', e => {
29+
channel.push('finalized_points', {points: e.target.value})
30+
})
31+
}
6032

61-
renderTemplate(cardContainer, template)
33+
document
34+
.querySelector('.calculate-points')
35+
.addEventListener('click', event => {
36+
const storyPoints = document.querySelector('.story-points')
37+
channel.push('user_estimated', { points: storyPoints.value })
38+
})
6239

40+
channel.on('new_card', state => {
41+
document
42+
.querySelector('.start-button')
43+
.style.display = "none"
44+
document
45+
.querySelector('.winner')
46+
.style.display = "none"
47+
document
48+
.querySelector('.tie')
49+
.style.display = "none"
6350
document
6451
.querySelector('.calculate-points')
65-
.addEventListener('click', event => {
66-
const storyPoints = document.querySelector('.story-points')
67-
channel.push('user_estimated', { points: storyPoints.value })
68-
})
52+
.style.display = "inline-block"
53+
document
54+
.querySelector('.ticket')
55+
.style.display = "block"
56+
document
57+
.querySelector('.ticket-title')
58+
.innerHTML = state.card.title
59+
document
60+
.querySelector('.ticket-description')
61+
.innerHTML = state.card.description
6962
})
7063

7164
const renderVotingResults = function(template) {
@@ -80,20 +73,54 @@ const renderVotingResults = function(template) {
8073
}
8174

8275
channel.on('winner', state => {
83-
const template =
84-
'<p>' + state.points + ' Points </p>' +
85-
'<button ' + (driving ? '' : 'disabled=true') + ' class="btn btn-primary next-card" value=' + state.points + '>Next Card</a>'
86-
87-
renderVotingResults(template)
76+
document
77+
.querySelector('.winner')
78+
.style.display = "block"
79+
document
80+
.querySelector('.calculate-points')
81+
.style.display = "none"
82+
document
83+
.querySelector('.final-points')
84+
.innerHTML = "Winner: " + state.points + " Points"
85+
document
86+
.querySelector('.next-card')
87+
.value = state.points
88+
document
89+
.querySelector('.next-card')
90+
.disabled = !driving
8891
})
8992

9093
channel.on('tie', state => {
91-
const template =
92-
'<p class="card-text"> TIE! </p>' +
93-
'<button ' + (driving ? '' : 'disabled=true') + ' class="btn btn-primary next-card" value=' + state.points[0] + '>Pick ' + state.points[0] + ' </a>' +
94-
'<button ' + (driving ? '' : 'disabled=true') + ' class="btn btn-primary next-card" value=' + state.points[1] + '>Pick ' + state.points[1] + ' </a>'
95-
96-
renderVotingResults(template)
94+
document
95+
.querySelector('.tie')
96+
.style.display = "block"
97+
document
98+
.querySelector('.calculate-points')
99+
.style.display = "none"
100+
document
101+
.querySelector('.tie')
102+
.getElementsByClassName('next-card')[0]
103+
.value = state.points[0]
104+
document
105+
.querySelector('.tie')
106+
.getElementsByClassName('next-card')[0]
107+
.innerHTML = state.points[0] + " Points"
108+
document
109+
.querySelector('.tie')
110+
.getElementsByClassName('next-card')[0]
111+
.disabled = !driving
112+
document
113+
.querySelector('.tie')
114+
.getElementsByClassName('next-card')[1]
115+
.value = state.points[1]
116+
document
117+
.querySelector('.tie')
118+
.getElementsByClassName('next-card')[1]
119+
.innerHTML = state.points[1] + " Points"
120+
document
121+
.querySelector('.tie')
122+
.getElementsByClassName('next-card')[1]
123+
.disabled = !driving
97124
})
98125

99126
export default socket

lib/pointing_party/vote_calculator.ex

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
defmodule PointingParty.VoteCalculator do
2+
def calculate_votes(users) do
3+
case winning_vote(users) do
4+
top_two when is_list(top_two) -> {"tie", top_two}
5+
winner -> {"winner", winner}
6+
end
7+
end
8+
9+
def winning_vote(users) do
10+
initial_score_card()
11+
|> get_points(users)
12+
|> calculate_vote_ratios()
13+
|> calculate_majority()
14+
|> handle_tie()
15+
end
16+
17+
def initial_score_card do
18+
%{votes: nil, calculated_votes: nil, majority: nil}
19+
end
20+
21+
defp get_points(score_card, users) do
22+
votes =
23+
users
24+
|> Enum.map(fn {_username, %{metas: [%{points: points}]}} ->
25+
points
26+
end)
27+
28+
update_score_card(score_card, :votes, votes)
29+
end
30+
31+
defp calculate_vote_ratios(%{votes: votes} = score_card) do
32+
calculated_votes =
33+
Enum.reduce(votes, %{}, fn vote, acc ->
34+
acc
35+
|> Map.get_and_update(vote, &{&1, (&1 || 0) + 1})
36+
|> elem(1)
37+
end)
38+
39+
update_score_card(score_card, :calculated_votes, calculated_votes)
40+
end
41+
42+
defp calculate_majority(score_card) do
43+
total_votes = length(score_card.votes)
44+
majority_share = (total_votes / 2) |> Float.floor()
45+
46+
majority =
47+
Enum.reduce_while(score_card.calculated_votes, nil, fn {point, vote_count}, _acc ->
48+
if vote_count == total_votes or rem(vote_count, total_votes) > majority_share do
49+
{:halt, point}
50+
else
51+
{:cont, nil}
52+
end
53+
end)
54+
55+
update_score_card(score_card, :majority, majority)
56+
end
57+
58+
defp handle_tie(%{majority: nil, calculated_votes: calculated_votes}) do
59+
calculated_votes
60+
|> Enum.sort_by(&elem(&1, 1))
61+
|> Enum.take(2)
62+
|> Enum.map(&elem(&1, 0))
63+
end
64+
65+
defp handle_tie(%{majority: majority}), do: majority
66+
67+
defp update_score_card(score_card, key, value) do
68+
Map.put(score_card, key, value)
69+
end
70+
end

lib/pointing_party_web/channels/room_channel.ex

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule PointingPartyWeb.RoomChannel do
33

44
alias PointingParty.Card
55
alias PointingPartyWeb.Presence
6+
alias PointingParty.VoteCalculator
67

78
def join("room:lobby", _payload, socket) do
89
send(self(), :after_join)
@@ -29,6 +30,7 @@ defmodule PointingPartyWeb.RoomChannel do
2930

3031
def handle_in("finalized_points", %{"points" => points}, socket) do
3132
updated_socket = save_vote_next_card(points, socket)
33+
# clear_user_points(socket)
3234
broadcast!(updated_socket, "new_card", %{card: current_card(updated_socket)})
3335
{:reply, :ok, updated_socket}
3436
end
@@ -39,6 +41,15 @@ defmodule PointingPartyWeb.RoomChannel do
3941
{:reply, :ok, updated_socket}
4042
end
4143

44+
intercept ["new_card"]
45+
46+
def handle_out("new_card", payload, socket) do
47+
Presence.update(socket, socket.assigns.username, &(Map.put(&1, :points, nil)))
48+
push(socket, "new_card", payload)
49+
{:noreply, socket}
50+
end
51+
52+
4253
defp current_card(socket) do
4354
socket.assigns
4455
|> Map.get(:current)
@@ -56,12 +67,7 @@ defmodule PointingPartyWeb.RoomChannel do
5667
defp finalize_voting(socket) do
5768
current_users = Presence.list(socket)
5869

59-
{event, points} =
60-
case winning_vote(current_users) do
61-
top_two when is_list(top_two) -> {"tie", top_two}
62-
winner -> {"winner", winner}
63-
end
64-
70+
{event, points} = VoteCalculator.calculate_votes(current_users)
6571
broadcast!(socket, event, %{points: points})
6672
end
6773

@@ -91,32 +97,4 @@ defmodule PointingPartyWeb.RoomChannel do
9197
|> assign(:current, next)
9298
|> assign(:voted, [latest_card | socket.assigns[:voted]])
9399
end
94-
95-
defp winning_vote(users) do
96-
votes = Enum.map(users, fn {_username, %{metas: [%{points: points}]}} -> points end)
97-
calculated_votes = Enum.reduce(votes, %{}, fn vote, acc ->
98-
acc
99-
|> Map.get_and_update(vote, &({&1, (&1 || 0) + 1}))
100-
|> elem(1)
101-
end)
102-
103-
total_votes = length(votes)
104-
105-
majority = Enum.reduce_while(calculated_votes, nil, fn {point, vote_count}, _acc ->
106-
if vote_count == total_votes or rem(vote_count, total_votes) > 5 do
107-
{:halt, point}
108-
else
109-
{:cont, nil}
110-
end
111-
end)
112-
113-
if is_nil(majority) do
114-
calculated_votes
115-
|> Enum.sort_by(&elem(&1, 1))
116-
|> Enum.take(2)
117-
|> Enum.map(&elem(&1, 0))
118-
else
119-
majority
120-
end
121-
end
122100
end

lib/pointing_party_web/templates/card/index.html.eex

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
<div class="row">
22
<div class="card-container col-10">
3-
<div class="col-md-4 text-center">
4-
<button class="start-button btn btn-primary">Start the Party!</button>
3+
<div class="col-md-12 text-center">
4+
<button display="block" class="start-button btn btn-primary">Start the Party!</button>
5+
<div class="ticket" style="display: none">
6+
<div class="card text-left">
7+
<div class="card-header">
8+
<h2 class="ticket-title"></h2>
9+
</div>
10+
<div class="card-body">
11+
<p class="card-text ticket-description"></p>
12+
<div class="form-group text-left points-container">
13+
<div class="form-row align-items-center">
14+
<div class="col-2">
15+
<label for="story-points">Story Points</label>
16+
<select class="form-control story-points" id="story-points">
17+
<option>1</option>
18+
<option>2</option>
19+
<option>3</option>
20+
<option>5</option>
21+
</select>
22+
</div>
23+
</div>
24+
<a href="#" class="btn btn-primary calculate-points">Vote!</a>
25+
<div class="winner" style="display: none;">
26+
<p class="final-points"></p>
27+
<button disabled=true class="btn btn-primary next-card" value="">Next Card</a>
28+
</div>
29+
<div class="tie" style="display: none;">
30+
<p class="card-text"> TIE! </p>
31+
<button disabled=true class="btn btn-primary next-card" value=""></a>
32+
<button disabled=true class="btn btn-primary next-card" value=""></a>
33+
</div>
34+
</div>
35+
</div>
36+
</div>
37+
</div>
538
</div>
639
</div>
740

priv/static/js/app.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)