Skip to content

Commit 4dd2187

Browse files
committed
replace org dashboard
1 parent 664d485 commit 4dd2187

File tree

2 files changed

+327
-1
lines changed

2 files changed

+327
-1
lines changed
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
defmodule AlgoraWeb.Org.DashboardLive do
2+
@moduledoc false
3+
use AlgoraWeb, :live_view
4+
5+
import AlgoraWeb.Components.Achievement
6+
import AlgoraWeb.Components.Experts
7+
import Ecto.Changeset
8+
9+
alias Algora.Accounts
10+
alias Algora.Bounties
11+
alias Algora.Contracts
12+
alias Algora.Types.USD
13+
alias Algora.Validations
14+
alias Algora.Workspace
15+
16+
require Logger
17+
18+
defmodule BountyForm do
19+
@moduledoc false
20+
use Ecto.Schema
21+
22+
import Ecto.Changeset
23+
24+
embedded_schema do
25+
field :url, :string
26+
field :amount, USD
27+
28+
embeds_one :ticket_ref, TicketRef, primary_key: false do
29+
field :owner, :string
30+
field :repo, :string
31+
field :number, :integer
32+
field :type, :string
33+
end
34+
end
35+
36+
def changeset(form, attrs \\ %{}) do
37+
form
38+
|> cast(attrs, [:url, :amount])
39+
|> validate_required([:url, :amount])
40+
|> Validations.validate_money_positive(:amount)
41+
|> Validations.validate_ticket_ref(:url, :ticket_ref)
42+
end
43+
end
44+
45+
defmodule TipForm do
46+
@moduledoc false
47+
use Ecto.Schema
48+
49+
import Ecto.Changeset
50+
51+
embedded_schema do
52+
field :github_handle, :string
53+
field :amount, USD
54+
end
55+
56+
def changeset(form, attrs \\ %{}) do
57+
form
58+
|> cast(attrs, [:github_handle, :amount])
59+
|> validate_required([:github_handle, :amount])
60+
|> Validations.validate_money_positive(:amount)
61+
end
62+
end
63+
64+
def mount(_params, _session, socket) do
65+
experts =
66+
socket.assigns.current_user.tech_stack
67+
|> List.first()
68+
|> Accounts.list_experts()
69+
|> Enum.take(6)
70+
71+
if connected?(socket) do
72+
Bounties.subscribe()
73+
end
74+
75+
{:ok,
76+
socket
77+
|> assign(:bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{})))
78+
|> assign(:tip_form, to_form(TipForm.changeset(%TipForm{}, %{})))
79+
|> assign(:experts, experts)
80+
|> assign_tickets()
81+
|> assign_achievements()}
82+
end
83+
84+
def render(assigns) do
85+
~H"""
86+
<div class="lg:pr-96">
87+
<div class="container mx-auto max-w-7xl space-y-8 p-8">
88+
<.section>
89+
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
90+
{create_bounty(assigns)}
91+
{create_tip(assigns)}
92+
</div>
93+
</.section>
94+
95+
<.section
96+
:if={@experts != []}
97+
title="Experts"
98+
subtitle="Meet the experts on Algora"
99+
link={~p"/experts"}
100+
>
101+
<ul class="flex flex-col gap-8 md:grid md:grid-cols-2 xl:grid-cols-3">
102+
<.experts experts={@experts} />
103+
</ul>
104+
</.section>
105+
</div>
106+
</div>
107+
{sidebar(assigns)}
108+
"""
109+
end
110+
111+
defp create_bounty(assigns) do
112+
~H"""
113+
<.card>
114+
<.card_header>
115+
<div class="flex items-center gap-3">
116+
<.icon name="tabler-diamond" class="h-8 w-8" />
117+
<h2 class="text-2xl font-semibold">Post a bounty</h2>
118+
</div>
119+
</.card_header>
120+
<.card_content>
121+
<.simple_form for={@bounty_form} phx-submit="create_bounty">
122+
<div class="flex flex-col gap-6">
123+
<.input
124+
label="URL"
125+
field={@bounty_form[:url]}
126+
placeholder="https://github.com/swift-lang/swift/issues/1337"
127+
/>
128+
<.input label="Amount" icon="tabler-currency-dollar" field={@bounty_form[:amount]} />
129+
<p class="text-sm text-muted-foreground">
130+
<span class="font-semibold">Tip:</span>
131+
You can also create bounties directly on
132+
GitHub by commenting <code class="px-1 py-0.5 text-success">/bounty $100</code>
133+
on any issue.
134+
</p>
135+
<div class="flex justify-end gap-4">
136+
<.button>Submit</.button>
137+
</div>
138+
</div>
139+
</.simple_form>
140+
</.card_content>
141+
</.card>
142+
"""
143+
end
144+
145+
defp create_tip(assigns) do
146+
~H"""
147+
<.card>
148+
<.card_header>
149+
<div class="flex items-center gap-3">
150+
<.icon name="tabler-gift" class="h-8 w-8" />
151+
<h2 class="text-2xl font-semibold">Tip a developer</h2>
152+
</div>
153+
</.card_header>
154+
<.card_content>
155+
<.simple_form for={@tip_form} phx-submit="create_tip">
156+
<div class="flex flex-col gap-6">
157+
<.input label="GitHub handle" field={@tip_form[:github_handle]} placeholder="jsmith" />
158+
<.input label="Amount" icon="tabler-currency-dollar" field={@tip_form[:amount]} />
159+
<p class="text-sm text-muted-foreground">
160+
<span class="font-semibold">Tip:</span>
161+
You can also create tips directly on
162+
GitHub by commenting <code class="px-1 py-0.5 text-success">/tip $100 @username</code>
163+
on any pull request.
164+
</p>
165+
<div class="flex justify-end gap-4">
166+
<.button>Submit</.button>
167+
</div>
168+
</div>
169+
</.simple_form>
170+
</.card_content>
171+
</.card>
172+
"""
173+
end
174+
175+
defp sidebar(assigns) do
176+
~H"""
177+
<aside class="scrollbar-thin fixed top-16 right-0 bottom-0 hidden w-96 overflow-y-auto border-l border-border bg-background p-4 pt-6 sm:p-6 md:p-8 lg:block">
178+
<div class="flex items-center justify-between">
179+
<h2 class="text-xl font-semibold leading-none tracking-tight">Getting started</h2>
180+
</div>
181+
<nav class="pt-6">
182+
<ol role="list" class="space-y-6">
183+
<%= for achievement <- @achievements do %>
184+
<li>
185+
<.achievement achievement={achievement} />
186+
</li>
187+
<% end %>
188+
</ol>
189+
</nav>
190+
</aside>
191+
"""
192+
end
193+
194+
def handle_event("create_bounty", %{"bounty_form" => params}, socket) do
195+
changeset =
196+
%BountyForm{}
197+
|> BountyForm.changeset(params)
198+
|> Map.put(:action, :validate)
199+
200+
amount = get_field(changeset, :amount)
201+
ticket_ref = get_field(changeset, :ticket_ref)
202+
203+
with %{valid?: true} <- changeset,
204+
{:ok, _bounty} <-
205+
Bounties.create_bounty(%{
206+
creator: socket.assigns.current_user,
207+
owner: socket.assigns.current_user,
208+
amount: amount,
209+
ticket_ref: ticket_ref
210+
}) do
211+
{:noreply,
212+
socket
213+
|> assign_achievements()
214+
|> put_flash(:info, "Bounty created")}
215+
else
216+
%{valid?: false} ->
217+
{:noreply, assign(socket, :bounty_form, to_form(changeset))}
218+
219+
{:error, :already_exists} ->
220+
{:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")}
221+
222+
{:error, _reason} ->
223+
{:noreply, put_flash(socket, :error, "Something went wrong")}
224+
end
225+
end
226+
227+
def handle_event("create_tip", %{"tip_form" => params}, socket) do
228+
changeset =
229+
%TipForm{}
230+
|> TipForm.changeset(params)
231+
|> Map.put(:action, :validate)
232+
233+
with %{valid?: true} <- changeset,
234+
{:ok, token} <- Accounts.get_access_token(socket.assigns.current_user),
235+
{:ok, recipient} <- Workspace.ensure_user(token, get_field(changeset, :github_handle)),
236+
{:ok, checkout_url} <-
237+
Bounties.create_tip(%{
238+
creator: socket.assigns.current_user,
239+
owner: socket.assigns.current_user,
240+
recipient: recipient,
241+
amount: get_field(changeset, :amount)
242+
}) do
243+
{:noreply, redirect(socket, external: checkout_url)}
244+
else
245+
%{valid?: false} ->
246+
{:noreply, assign(socket, :tip_form, to_form(changeset))}
247+
248+
{:error, reason} ->
249+
Logger.error("Failed to create tip: #{inspect(reason)}")
250+
{:noreply, put_flash(socket, :error, "Something went wrong")}
251+
end
252+
end
253+
254+
def handle_info(:bounties_updated, socket) do
255+
{:noreply, assign_tickets(socket)}
256+
end
257+
258+
defp assign_tickets(socket) do
259+
tickets =
260+
Bounties.PrizePool.list(
261+
status: :open,
262+
tech_stack: socket.assigns.current_user.tech_stack,
263+
limit: 100
264+
)
265+
266+
assign(socket, :tickets, Enum.take(tickets, 6))
267+
end
268+
269+
defp assign_achievements(socket) do
270+
tech = List.first(socket.assigns.current_user.tech_stack)
271+
272+
status_fns = [
273+
{&personalize_status/1, "Personalize Algora"},
274+
{&create_bounty_status/1, "Create a bounty"},
275+
{&reward_bounty_status/1, "Reward a bounty"},
276+
{&begin_collaboration_status/1, "Contract a #{tech} developer"},
277+
{&complete_first_contract_status/1, "Complete a contract"}
278+
]
279+
280+
{achievements, _} =
281+
Enum.reduce_while(status_fns, {[], false}, fn {status_fn, name}, {acc, found_current} ->
282+
status = status_fn.(socket.assigns.current_user)
283+
284+
result =
285+
cond do
286+
found_current -> {acc ++ [%{status: status, name: name}], found_current}
287+
status == :completed -> {acc ++ [%{status: status, name: name}], false}
288+
true -> {acc ++ [%{status: :current, name: name}], true}
289+
end
290+
291+
{:cont, result}
292+
end)
293+
294+
assign(socket, :achievements, achievements)
295+
end
296+
297+
defp personalize_status(_socket), do: :completed
298+
299+
defp create_bounty_status(user) do
300+
case Bounties.list_bounties(owner_id: user.id, limit: 1) do
301+
[] -> :upcoming
302+
_ -> :completed
303+
end
304+
end
305+
306+
defp reward_bounty_status(user) do
307+
case Bounties.list_bounties(owner_id: user.id, status: :paid, limit: 1) do
308+
[] -> :upcoming
309+
_ -> :completed
310+
end
311+
end
312+
313+
defp begin_collaboration_status(user) do
314+
case Contracts.list_contracts(client_id: user.id, active_or_paid?: true, limit: 1) do
315+
[] -> :upcoming
316+
_ -> :completed
317+
end
318+
end
319+
320+
defp complete_first_contract_status(user) do
321+
case Contracts.list_contracts(client_id: user.id, status: :paid, limit: 1) do
322+
[] -> :upcoming
323+
_ -> :completed
324+
end
325+
end
326+
end

lib/algora_web/router.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ defmodule AlgoraWeb.Router do
8484
live_session :org,
8585
layout: {AlgoraWeb.Layouts, :org},
8686
on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Org.Nav] do
87-
live "/org/:org_handle", Org.DashboardAdminLive, :index
87+
live "/org/:org_handle", Org.DashboardLive, :index
8888
live "/org/:org_handle/home", Org.DashboardPublicLive, :index
8989
live "/org/:org_handle/bounties/new", Org.CreateBountyLive, :new
9090
live "/org/:org_handle/jobs/new", Org.CreateJobLive, :new

0 commit comments

Comments
 (0)