Skip to content

Commit 508bad3

Browse files
committed
feat: add /admin/devs
1 parent 6aff655 commit 508bad3

File tree

3 files changed

+297
-2
lines changed

3 files changed

+297
-2
lines changed

lib/algora_web/components/tech_badge.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ defmodule AlgoraWeb.Components.TechBadge do
55
import AlgoraWeb.Components.UI.Avatar
66
import AlgoraWeb.Components.UI.Badge
77

8+
attr :tech, :string, required: true
9+
attr :variant, :string, default: "outline"
10+
attr :rest, :global
11+
812
def tech_badge(assigns) do
913
assigns =
1014
assign(assigns, :tech_lower, normalize(assigns.tech))
1115

1216
~H"""
13-
<.badge variant="outline">
17+
<.badge variant={@variant} {@rest}>
1418
<%= if @tech_lower == "rust" do %>
1519
<svg viewBox="0 0 128 128" fill="currentColor" class="w-4 h-4 mr-1">
1620
<path d="M62.96.242c-.232.135-1.203 1.528-2.16 3.097-2.4 3.94-2.426 3.942-5.65.55-2.098-2.208-2.605-2.612-3.28-2.607-.44.002-.995.152-1.235.332-.24.18-.916 1.612-1.504 3.183-1.346 3.6-1.41 3.715-2.156 3.86-.46.086-1.343-.407-3.463-1.929-1.565-1.125-3.1-2.045-3.411-2.045-1.291 0-1.655.706-2.27 4.4-.78 4.697-.754 4.681-4.988 2.758-1.71-.776-3.33-1.41-3.603-1.41-.274 0-.792.293-1.15.652-.652.652-.653.655-.475 4.246l.178 3.595-.68.364c-.602.322-1.017.283-3.684-.348-3.48-.822-4.216-.8-4.92.15l-.516.693.692 2.964c.38 1.63.745 3.2.814 3.487.067.287-.05.746-.26 1.02-.348.448-.717.49-3.94.44-5.452-.086-5.761.382-3.51 5.3.718 1.56 1.305 2.98 1.305 3.15 0 .898-.717 1.224-3.794 1.727-1.722.28-3.218.51-3.326.51-.107 0-.43.235-.717.522-.937.936-.671 1.816 1.453 4.814 2.646 3.735 2.642 3.75-1.73 5.421-4.971 1.902-5.072 2.37-1.287 5.96 3.525 3.344 3.53 3.295-.461 5.804C.208 62.8.162 62.846.085 63.876c-.093 1.253-.071 1.275 3.538 3.48 3.57 2.18 3.57 2.246.067 5.56C-.078 76.48.038 77 5.013 78.877c4.347 1.64 4.353 1.66 1.702 5.394-1.502 2.117-1.981 3-1.981 3.653 0 1.223.637 1.535 4.44 2.174 3.206.54 3.92.857 3.92 1.741 0 .182-.588 1.612-1.307 3.177-2.236 4.87-1.981 5.275 3.31 5.275 4.93 0 4.799-.15 3.737 4.294-.8 3.35-.813 3.992-.088 4.715.554.556 1.6.494 4.87-.289 2.499-.596 2.937-.637 3.516-.328l.66.354-.177 3.594c-.178 3.593-.177 3.595.475 4.248.358.36.884.652 1.165.652.282 0 1.903-.63 3.604-1.404 4.22-1.916 4.194-1.932 4.973 2.75.617 3.711.977 4.4 2.294 4.4.327 0 1.83-.88 3.34-1.958 2.654-1.893 3.342-2.19 4.049-1.74.182.115.89 1.67 1.572 3.455 1.003 2.625 1.37 3.31 1.929 3.576 1.062.51 1.72.1 4.218-2.62 3.016-3.286 3.14-3.27 5.602.72 2.72 4.406 3.424 4.396 6.212-.089 2.402-3.864 2.374-3.862 5.621-.47 2.157 2.25 2.616 2.61 3.343 2.61.464 0 1.019-.175 1.23-.388.214-.213.92-1.786 1.568-3.496.649-1.71 1.321-3.2 1.495-3.31.687-.436 1.398-.13 4.048 1.752 1.56 1.108 3.028 1.96 3.377 1.96 1.296 0 1.764-.92 2.302-4.535.46-3.082.554-3.378 1.16-3.685.596-.302.954-.2 3.75 1.07 1.701.77 3.323 1.402 3.604 1.402.282 0 .816-.302 1.184-.672l.672-.67-.184-3.448c-.177-3.29-.16-3.468.364-3.943.54-.488.596-.486 3.615.204 3.656.835 4.338.857 5.025.17.671-.67.664-.818-.254-4.69-1.03-4.346-1.168-4.19 3.78-4.19 3.374 0 3.75-.049 4.18-.523.718-.793.547-1.702-.896-4.779-.729-1.55-1.32-2.96-1.315-3.135.024-.914.743-1.227 4.065-1.767 2.033-.329 3.553-.71 3.829-.96.923-.833.584-1.918-1.523-4.873-2.642-3.703-2.63-3.738 1.599-5.297 5.064-1.866 5.209-2.488 1.419-6.09-3.51-3.335-3.512-3.317.333-5.677 4.648-2.853 4.655-3.496.082-6.335-3.933-2.44-3.93-2.406-.405-5.753 3.78-3.593 3.678-4.063-1.295-5.965-4.388-1.679-4.402-1.72-1.735-5.38 1.588-2.18 1.982-2.903 1.982-3.65 0-1.306-.586-1.598-4.436-2.22-3.216-.52-3.924-.835-3.924-1.75 0-.174.588-1.574 1.307-3.113 1.406-3.013 1.604-4.22.808-4.94-.428-.387-1-.443-4.067-.392-3.208.054-3.618.008-4.063-.439-.486-.488-.48-.557.278-3.725.931-3.88.935-3.975.17-4.694-.777-.73-1.262-.718-4.826.121-2.597.612-3.027.653-3.617.337l-.67-.36.185-3.582.186-3.58-.67-.67c-.369-.37-.891-.67-1.163-.67-.27 0-1.884.64-3.583 1.421-2.838 1.306-3.143 1.393-3.757 1.072-.612-.32-.714-.637-1.237-3.829-.603-3.693-.977-4.412-2.288-4.412-.311 0-1.853.925-3.426 2.055-2.584 1.856-2.93 2.032-3.574 1.807-.533-.186-.843-.59-1.221-1.599-.28-.742-.817-2.172-1.194-3.177-.762-2.028-1.187-2.482-2.328-2.482-.637 0-1.213.458-3.28 2.604-3.25 3.375-3.261 3.374-5.65-.545C66.073 1.78 65.075.382 64.81.24c-.597-.32-1.3-.32-1.85.002m2.96 11.798c2.83 2.014 1.326 6.75-2.144 6.75-3.368 0-5.064-4.057-2.66-6.36 1.358-1.3 3.304-1.459 4.805-.39m-3.558 12.507c1.855.705 2.616.282 6.852-3.8l3.182-3.07 1.347.18c4.225.56 12.627 4.25 17.455 7.666 4.436 3.14 10.332 9.534 12.845 13.93l.537.942-2.38 5.364c-1.31 2.95-2.382 5.673-2.382 6.053 0 .878.576 2.267 1.13 2.726.234.195 2.457 1.265 4.939 2.378l4.51 2.025.178 1.148c.23 1.495.26 5.167.052 6.21l-.163.816h-2.575c-2.987 0-2.756-.267-2.918 3.396-.118 2.656-.76 4.124-2.22 5.075-2.377 1.551-6.304 1.27-7.97-.57-.255-.284-.752-1.705-1.105-3.16-1.03-4.254-2.413-6.64-5.193-8.965-.878-.733-1.595-1.418-1.595-1.522 0-.102.965-.915 2.145-1.803 4.298-3.24 6.77-7.012 7.04-10.747.519-7.126-5.158-13.767-13.602-15.92-2.002-.51-2.857-.526-27.624-.526-14.057 0-25.56-.092-25.56-.204 0-.263 3.125-3.295 4.965-4.816 5.054-4.178 11.618-7.465 18.417-9.22l2.35-.61 3.34 3.387c1.839 1.863 3.64 3.5 4.003 3.637M20.3 46.34c1.539 1.008 2.17 3.54 1.26 5.062-1.405 2.356-4.966 2.455-6.373.178-2.046-3.309 1.895-7.349 5.113-5.24m90.672.13c4.026 2.454.906 8.493-3.404 6.586-2.877-1.273-2.97-5.206-.155-6.64 1.174-.6 2.523-.579 3.56.053M32.163 61.5v15.02h-13.28l-.526-2.285c-1.036-4.5-1.472-9.156-1.211-12.969l.182-2.679 4.565-2.047c2.864-1.283 4.706-2.262 4.943-2.625 1.038-1.584.94-2.715-.518-5.933l-.68-1.502h6.523V61.5M70.39 47.132c2.843.74 4.345 2.245 4.349 4.355.002 1.55-.765 2.52-2.67 3.38-1.348.61-1.562.625-10.063.708l-8.686.084v-8.92h7.782c6.078 0 8.112.086 9.288.393m-2.934 21.554c1.41.392 3.076 1.616 3.93 2.888.898 1.337 1.423 3.076 2.667 8.836 1.05 4.87 1.727 6.46 3.62 8.532 2.345 2.566 1.8 2.466 13.514 2.466 5.61 0 10.198.09 10.198.2 0 .197-3.863 4.764-4.03 4.764-.048 0-2.066-.422-4.484-.939-6.829-1.458-7.075-1.287-8.642 6.032l-1.008 4.702-.91.448c-1.518.75-6.453 2.292-9.01 2.82-4.228.87-8.828 1.162-12.871.821-6.893-.585-16.02-3.259-16.377-4.8-.075-.327-.535-2.443-1.018-4.704-.485-2.26-1.074-4.404-1.31-4.764-1.13-1.724-2.318-1.83-7.547-.674-1.98.44-3.708.796-3.84.796-.248 0-3.923-4.249-3.923-4.535 0-.09 8.728-.194 19.396-.23l19.395-.066.07-6.89c.05-4.865-.018-6.997-.23-7.25-.234-.284-1.485-.358-6.011-.358H53.32v-8.36l6.597.001c3.626.002 7.02.12 7.539.264M37.57 100.02c3.084 1.88 1.605 6.804-2.043 6.8-3.74 0-5.127-4.88-1.94-6.826 1.055-.643 2.908-.63 3.983.026m56.48.206c1.512 1.108 2.015 3.413 1.079 4.95-2.46 4.034-8.612.827-6.557-3.419 1.01-2.085 3.695-2.837 5.478-1.53">
@@ -44,7 +48,7 @@ defmodule AlgoraWeb.Components.TechBadge do
4448
|> String.replace(".", "")
4549
end
4650

47-
defp langs do
51+
def langs do
4852
[
4953
"JavaScript",
5054
"TypeScript",
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
defmodule AlgoraWeb.Admin.DevsLive do
2+
@moduledoc false
3+
use AlgoraWeb, :live_view
4+
5+
alias Algora.Accounts.User
6+
7+
@impl true
8+
def mount(_params, _session, socket) do
9+
# Define available options
10+
available_techs = Enum.take(AlgoraWeb.Components.TechBadge.langs(), 20)
11+
available_countries = Algora.PSP.ConnectCountries.list_codes()
12+
13+
# Start with empty selections
14+
selected_techs = []
15+
selected_countries = []
16+
17+
{:ok,
18+
socket
19+
|> assign(:page_title, "Developers")
20+
|> assign(:available_techs, available_techs)
21+
|> assign(:available_countries, available_countries)
22+
|> assign(:selected_techs, selected_techs)
23+
|> assign(:selected_countries, selected_countries)
24+
|> assign_matches()}
25+
end
26+
27+
@impl true
28+
def handle_event("toggle_tech", %{"tech" => tech}, socket) do
29+
selected_techs =
30+
if tech in socket.assigns.selected_techs do
31+
List.delete(socket.assigns.selected_techs, tech)
32+
else
33+
[tech | socket.assigns.selected_techs]
34+
end
35+
36+
{:noreply,
37+
socket
38+
|> assign(:selected_techs, selected_techs)
39+
|> assign_matches()}
40+
end
41+
42+
@impl true
43+
def handle_event("toggle_country", %{"code" => code}, socket) do
44+
selected_countries =
45+
if code in socket.assigns.selected_countries do
46+
List.delete(socket.assigns.selected_countries, code)
47+
else
48+
[code | socket.assigns.selected_countries]
49+
end
50+
51+
{:noreply,
52+
socket
53+
|> assign(:selected_countries, selected_countries)
54+
|> assign_matches()}
55+
end
56+
57+
defp assign_matches(socket) do
58+
matches =
59+
[
60+
tech_stack: socket.assigns.selected_techs,
61+
limit: 50,
62+
sort_by: [{"countries", socket.assigns.selected_countries}]
63+
]
64+
|> Algora.Cloud.list_top_matches()
65+
|> Algora.Settings.load_matches_2()
66+
67+
socket
68+
|> assign(:matches, matches)
69+
|> assign(
70+
:contributions_map,
71+
matches |> Enum.map(& &1.user) |> fetch_applicants_contributions(socket.assigns.selected_techs)
72+
)
73+
end
74+
75+
@impl true
76+
def render(assigns) do
77+
~H"""
78+
<div class="container mx-auto max-w-7xl space-y-8 p-4 sm:p-6 lg:p-8">
79+
<.section title="Developers">
80+
<h3 class="pt-4 text-sm font-semibold text-foreground">Tech stack</h3>
81+
<div class="flex items-center flex-wrap gap-4 pt-4">
82+
<%= for tech <- @available_techs do %>
83+
<.tech_badge
84+
class="cursor-pointer"
85+
phx-click="toggle_tech"
86+
phx-value-tech={tech}
87+
tech={tech}
88+
variant={if tech in @selected_techs, do: "success", else: "outline"}
89+
/>
90+
<% end %>
91+
</div>
92+
<h3 class="pt-8 text-sm font-semibold text-foreground">Countries</h3>
93+
<div class="flex items-center flex-wrap gap-4 pt-4">
94+
<%= for code <- @available_countries do %>
95+
<.badge
96+
class="cursor-pointer"
97+
phx-click="toggle_country"
98+
phx-value-code={code}
99+
variant={if code in @selected_countries, do: "success", else: "outline"}
100+
>
101+
{Algora.Misc.CountryEmojis.get(code)} {code}
102+
</.badge>
103+
<% end %>
104+
</div>
105+
106+
<div class="pt-8 grid grid-cols-1 gap-4 lg:grid-cols-3">
107+
<%= for match <- @matches do %>
108+
<div>
109+
<.match_card
110+
user={match.user}
111+
tech_stack={@selected_techs |> Enum.take(1)}
112+
contributions={Map.get(@contributions_map, match.user.id, [])}
113+
contract_type="bring_your_own"
114+
/>
115+
</div>
116+
<% end %>
117+
</div>
118+
</.section>
119+
</div>
120+
"""
121+
end
122+
123+
defp match_card(assigns) do
124+
~H"""
125+
<div class="h-full relative border ring-1 ring-transparent hover:ring-border transition-all bg-card group rounded-xl text-card-foreground shadow p-6">
126+
<div class="w-full truncate">
127+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
128+
<div class="flex items-center gap-4">
129+
<.link navigate={User.url(@user)}>
130+
<.avatar class="h-12 w-12 rounded-full">
131+
<.avatar_image src={@user.avatar_url} alt={@user.name} />
132+
<.avatar_fallback class="rounded-lg">
133+
{Algora.Util.initials(@user.name)}
134+
</.avatar_fallback>
135+
</.avatar>
136+
</.link>
137+
138+
<div>
139+
<div class="flex items-center gap-1 text-base text-foreground">
140+
<.link navigate={User.url(@user)} class="font-semibold hover:underline">
141+
{@user.name}
142+
<span :if={@user.country}>
143+
{Algora.Misc.CountryEmojis.get(@user.country)}
144+
</span>
145+
</.link>
146+
</div>
147+
<div
148+
:if={@user.provider_meta}
149+
class="pt-0.5 flex items-center gap-x-2 gap-y-1 text-xs text-muted-foreground max-w-[250px] 2xl:max-w-none truncate"
150+
>
151+
<.link
152+
:if={@user.provider_login}
153+
href={"https://github.com/#{@user.provider_login}"}
154+
target="_blank"
155+
class="flex items-center gap-1 hover:underline"
156+
>
157+
<.icon name="github" class="shrink-0 h-4 w-4" />
158+
<span class="line-clamp-1">{@user.provider_login}</span>
159+
</.link>
160+
<.link
161+
:if={@user.provider_meta["twitter_handle"]}
162+
href={"https://x.com/#{@user.provider_meta["twitter_handle"]}"}
163+
target="_blank"
164+
class="flex items-center gap-1 hover:underline"
165+
>
166+
<.icon name="tabler-brand-x" class="shrink-0 h-4 w-4" />
167+
<span class="line-clamp-1">
168+
{@user.provider_meta["twitter_handle"]}
169+
</span>
170+
</.link>
171+
</div>
172+
</div>
173+
</div>
174+
</div>
175+
176+
<div class="pt-2 flex items-center justify-center gap-2">
177+
<.button
178+
phx-click="share_opportunity"
179+
phx-value-user_id={@user.id}
180+
phx-value-type="bounty"
181+
variant="outline"
182+
size="sm"
183+
>
184+
Bounty
185+
</.button>
186+
<.button
187+
phx-click="share_opportunity"
188+
phx-value-user_id={@user.id}
189+
phx-value-type="tip"
190+
variant="outline"
191+
size="sm"
192+
>
193+
Interview
194+
</.button>
195+
<.button
196+
phx-click="share_opportunity"
197+
phx-value-user_id={@user.id}
198+
phx-value-type="contract"
199+
phx-value-contract_type={@contract_type}
200+
variant="outline"
201+
size="sm"
202+
>
203+
Contract
204+
</.button>
205+
</div>
206+
207+
<div :if={@contributions != []} class="mt-4">
208+
<p class="text-xs text-muted-foreground uppercase font-semibold">
209+
Top contributions
210+
</p>
211+
<div class="flex flex-col gap-3 mt-2">
212+
<%= for {owner, contributions} <- aggregate_contributions(@contributions) |> Enum.take(3) do %>
213+
<.link
214+
href={"https://github.com/#{owner.provider_login}/#{List.first(contributions).repository.name}/pulls?q=author%3A#{@user.provider_login}+is%3Amerged+"}
215+
target="_blank"
216+
rel="noopener"
217+
class="flex items-center gap-3 rounded-xl pr-2 bg-card/50 border border-border/50 hover:border-border transition-all"
218+
>
219+
<img
220+
src={owner.avatar_url}
221+
class="h-12 w-12 rounded-xl rounded-r-none md:saturate-0 group-hover:saturate-100 transition-all"
222+
alt={owner.name}
223+
/>
224+
<div class="w-full flex flex-col text-xs font-medium gap-0.5">
225+
<span class="flex items-start justify-between gap-5">
226+
<span class="font-display">
227+
{if owner.type == :organization do
228+
owner.name
229+
else
230+
List.first(contributions).repository.name
231+
end}
232+
</span>
233+
<%= if tech = List.first(contributions).repository.tech_stack |> List.first() do %>
234+
<span class="flex items-center text-foreground text-[11px] gap-1">
235+
<img
236+
src={"https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/#{String.downcase(tech)}/#{String.downcase(tech)}-original.svg"}
237+
class="w-4 h-4 invert saturate-0"
238+
/> {tech}
239+
</span>
240+
<% end %>
241+
</span>
242+
<div class="flex items-center gap-2 font-semibold">
243+
<span class="flex items-center text-amber-300 text-xs">
244+
<.icon name="tabler-star-filled" class="h-4 w-4 mr-1" />
245+
{Algora.Util.format_number_compact(
246+
max(owner.stargazers_count, total_stars(contributions))
247+
)}
248+
</span>
249+
<span class="flex items-center text-purple-400 text-xs">
250+
<.icon name="tabler-git-pull-request" class="h-4 w-4 mr-1" />
251+
{Algora.Util.format_number_compact(total_contributions(contributions))}
252+
</span>
253+
</div>
254+
</div>
255+
</.link>
256+
<% end %>
257+
</div>
258+
</div>
259+
</div>
260+
</div>
261+
"""
262+
end
263+
264+
defp total_stars(contributions) do
265+
contributions
266+
|> Enum.map(& &1.repository.stargazers_count)
267+
|> Enum.sum()
268+
end
269+
270+
defp total_contributions(contributions) do
271+
contributions
272+
|> Enum.map(& &1.contribution_count)
273+
|> Enum.sum()
274+
end
275+
276+
defp aggregate_contributions(contributions) do
277+
groups = Enum.group_by(contributions, fn c -> c.repository.user end)
278+
279+
contributions
280+
|> Enum.map(fn c -> {c.repository.user, groups[c.repository.user]} end)
281+
|> Enum.uniq_by(fn {owner, _} -> owner.id end)
282+
end
283+
284+
defp fetch_applicants_contributions(users, tech_stack) do
285+
users
286+
|> Enum.map(& &1.id)
287+
|> Algora.Workspace.list_user_contributions(tech_stack: tech_stack)
288+
|> Enum.group_by(& &1.user.id)
289+
end
290+
end

lib/algora_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ defmodule AlgoraWeb.Router do
5555
live "/chat/:id", Chat.ThreadLive
5656
live "/campaign", Admin.CampaignLive
5757
live "/seed", Admin.SeedLive
58+
live "/devs", Admin.DevsLive
5859
end
5960

6061
live_dashboard "/dashboard",

0 commit comments

Comments
 (0)