From 5beea6135ea7e8990e64600ae2c0190ca556f878 Mon Sep 17 00:00:00 2001 From: zafer Date: Sun, 2 Mar 2025 21:22:25 +0300 Subject: [PATCH 01/35] init bounty page --- lib/algora/accounts/accounts.ex | 1 + lib/algora/admin/admin.ex | 52 ++++ lib/algora/bounties/bounties.ex | 5 +- lib/algora_web/live/bounty_live.ex | 467 +++++++++++++++++++++++++++++ lib/algora_web/router.ex | 2 + 5 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 lib/algora_web/live/bounty_live.ex diff --git a/lib/algora/accounts/accounts.ex b/lib/algora/accounts/accounts.ex index 0c75523b8..df3cc4eeb 100644 --- a/lib/algora/accounts/accounts.ex +++ b/lib/algora/accounts/accounts.ex @@ -132,6 +132,7 @@ defmodule Algora.Accounts do type: u.type, id: u.id, handle: u.handle, + provider_login: u.provider_login, name: u.name, avatar_url: u.avatar_url, bio: u.bio, diff --git a/lib/algora/admin/admin.ex b/lib/algora/admin/admin.ex index 2c2216e54..39b7fbb3a 100644 --- a/lib/algora/admin/admin.ex +++ b/lib/algora/admin/admin.ex @@ -2,6 +2,7 @@ defmodule Algora.Admin do @moduledoc false import Ecto.Query + alias Algora.Accounts alias Algora.Accounts.User alias Algora.Bounties.Claim alias Algora.Parser @@ -12,6 +13,57 @@ defmodule Algora.Admin do require Logger + def seed do + import Algora.Factory + + title = "Monthly PR Review & Triage Bounty" + + description = """ + ## What needs doing + + Review open pull requests across our repos and: + + 1. Test them locally + 2. Leave helpful comments + 3. Merge or close stale PRs + 4. Tag relevant people when needed + + ## Success looks like + + - [ ] All PRs older than 2 weeks have been reviewed + - [ ] Clear comments left on PRs needing changes + - [ ] Stale PRs (>30 days old) closed with explanation + - [ ] Weekly summary posted in #dev channel + """ + + org = Repo.get_by!(User, handle: "piedpiper0") + user = Repo.get_by!(User, handle: "zcesur") + + repository = insert!(:repository, user: org) + ticket = insert!(:ticket, title: title, repository: repository, description: description) + bounty = insert!(:bounty, ticket: ticket, owner: org, creator: user, amount: Money.new(1000, :USD)) + + for contributor <- Accounts.list_featured_developers() do + insert!(:transaction, + type: :debit, + net_amount: Money.new(1000, :USD), + user_id: org.id, + bounty_id: bounty.id, + status: :succeeded + ) + + insert!(:transaction, + type: :credit, + net_amount: Money.new(1000, :USD), + user_id: contributor.id, + bounty_id: bounty.id, + status: :succeeded + ) + end + + IO.puts("#{AlgoraWeb.Endpoint.url()}/org/#{org.handle}/bounties/#{bounty.id}") + end + def token!, do: System.fetch_env!("ADMIN_GITHUB_TOKEN") def run(worker) do diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index dd5b83120..e64e4e129 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -628,7 +628,7 @@ defmodule Algora.Bounties do bounty_id: String.t(), claims: [Claim.t()] }, - opts :: [ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}] + opts :: [ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()}, recipient: User.t()] ) :: {:ok, String.t()} | {:error, atom()} def reward_bounty(%{owner: owner, amount: amount, bounty_id: bounty_id, claims: claims}, opts \\ []) do @@ -640,7 +640,8 @@ defmodule Algora.Bounties do %{owner: owner, amount: amount, description: "Bounty payment for OSS contributions"}, ticket_ref: opts[:ticket_ref], bounty_id: bounty_id, - claims: claims + claims: claims, + recipient: opts[:recipient] ) end end) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex new file mode 100644 index 000000000..6765cad99 --- /dev/null +++ b/lib/algora_web/live/bounty_live.ex @@ -0,0 +1,467 @@ +defmodule AlgoraWeb.BountyLive do + @moduledoc false + use AlgoraWeb, :live_view + + import Ecto.Changeset + + alias Algora.Accounts + alias Algora.Accounts.User + alias Algora.Bounties + alias Algora.Bounties.Bounty + alias Algora.Bounties.LineItem + alias Algora.Repo + + require Logger + + defp tip_options do + [ + {"None", 0}, + {"10%", 10}, + {"20%", 20}, + {"50%", 50} + ] + end + + defmodule RewardBountyForm do + @moduledoc false + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field :amount, :decimal + field :tip_percentage, :decimal + end + + def changeset(form, attrs) do + form + |> cast(attrs, [:amount, :tip_percentage]) + |> validate_required([:amount]) + |> validate_number(:tip_percentage, greater_than_or_equal_to: 0) + |> validate_number(:amount, greater_than: 0) + end + end + + @impl true + def mount(%{"id" => bounty_id}, _session, socket) do + bounty = + Bounty + |> Repo.get!(bounty_id) + |> Repo.preload([ + :owner, + :transactions, + ticket: [repository: [:user]] + ]) + + debits = Enum.filter(bounty.transactions, &(&1.type == :debit and &1.status == :succeeded)) + + total_paid = + debits + |> Enum.map(& &1.net_amount) + |> Enum.reduce(Money.zero(:USD, no_fraction_if_integer: true), &Money.add!(&1, &2)) + + ticket_body_html = Algora.Markdown.render(bounty.ticket.description) + + contexts = contexts(bounty) + + changeset = + RewardBountyForm.changeset(%RewardBountyForm{}, %{ + tip_percentage: 0, + amount: Money.to_decimal(bounty.amount) + }) + + {:ok, + socket + |> assign(:page_title, bounty.ticket.title) + |> assign(:bounty, bounty) + |> assign(:ticket, bounty.ticket) + |> assign(:total_paid, total_paid) + |> assign(:ticket_body_html, ticket_body_html) + |> assign(:contexts, contexts) + |> assign(:show_reward_modal, false) + |> assign(:selected_context, nil) + |> assign(:line_items, []) + |> assign(:reward_form, to_form(changeset))} + end + + @impl true + def handle_params(_params, _url, %{assigns: %{current_user: nil}} = socket) do + {:noreply, socket} + end + + def handle_params(%{"context" => context_id}, _url, socket) do + {:noreply, socket |> assign_selected_context(context_id) |> assign_line_items()} + end + + def handle_params(_params, _url, socket) do + {:noreply, socket} + end + + @impl true + def handle_event("reward", _params, socket) do + {:noreply, assign(socket, :show_reward_modal, true)} + end + + def handle_event("close_drawer", _params, socket) do + {:noreply, assign(socket, :show_reward_modal, false)} + end + + def handle_event("validate_reward", %{"reward_bounty_form" => params}, socket) do + {:noreply, + socket + |> assign(:reward_form, to_form(RewardBountyForm.changeset(%RewardBountyForm{}, params))) + |> assign_line_items()} + end + + def handle_event("pay_with_stripe", %{"reward_bounty_form" => params}, socket) do + changeset = RewardBountyForm.changeset(%RewardBountyForm{}, params) + + case apply_action(changeset, :save) do + {:ok, data} -> + case create_payment_session(socket, data) do + {:ok, session_url} -> + {:noreply, redirect(socket, external: session_url)} + + {:error, reason} -> + Logger.error("Failed to create payment session: #{inspect(reason)}") + {:noreply, put_flash(socket, :error, "Something went wrong")} + end + + {:error, changeset} -> + {:noreply, assign(socket, :reward_form, to_form(changeset))} + end + end + + defp assign_selected_context(socket, context_id) do + case Enum.find(socket.assigns.contexts, &(&1.id == context_id)) do + nil -> + socket + + context -> + assign(socket, :selected_context, context) + end + end + + defp assign_line_items(socket) do + line_items = + Bounties.generate_line_items( + %{amount: calculate_final_amount(socket.assigns.reward_form.source)}, + ticket_ref: ticket_ref(socket), + recipient: socket.assigns.selected_context + ) + + assign(socket, :line_items, line_items) + end + + defp ticket_ref(socket) do + %{ + owner: socket.assigns.ticket.repository.user.provider_login, + repo: socket.assigns.ticket.repository.name, + number: socket.assigns.ticket.number + } + end + + defp create_payment_session(socket, data) do + final_amount = calculate_final_amount(data) + + Bounties.reward_bounty( + %{ + owner: socket.assigns.current_user, + amount: final_amount, + bounty_id: socket.assigns.bounty.id, + claims: [] + }, + ticket_ref: ticket_ref(socket), + recipient: socket.assigns.selected_context + ) + end + + defp calculate_final_amount(data_or_changeset) do + tip_percentage = get_field(data_or_changeset, :tip_percentage) || Decimal.new(0) + amount = get_field(data_or_changeset, :amount) || Decimal.new(0) + + multiplier = tip_percentage |> Decimal.div(100) |> Decimal.add(1) + amount |> Money.new!(:USD) |> Money.mult!(multiplier) + end + + @impl true + def render(assigns) do + ~H""" +
+
+
+ <.card> + <.card_header> +
+ <.avatar class="h-12 w-12 rounded-full"> + <.avatar_image src={@ticket.repository.user.avatar_url} /> + <.avatar_fallback> + {String.first(@ticket.repository.user.provider_login)} + + +
+ <.link + href={@ticket.url} + class="text-xl font-semibold hover:underline" + target="_blank" + > + {@ticket.title} + +
+ {@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number} +
+
+
+ + <.card_content> +
+ {Phoenix.HTML.raw(@ticket_body_html)} +
+ + +
+ +
+ <.card> + <.card_header> +
+ <.card_title> + Bounty + + <.button phx-click="reward"> + Reward + +
+ + <.card_content> +
+
+ Frequency + + {bounty_frequency(@bounty)} + +
+
+ Amount + + {Money.to_string!(@bounty.amount)} + +
+
+ Total paid + + {Money.to_string!(@total_paid)} + +
+
+ + + + <.card> + <.card_header> +
+ <.card_title> + Shared with + + <.button phx-click="invite"> + Invite + +
+ + <.card_content> +
+
+ +
+ <.avatar> + <.avatar_image src={@bounty.owner.avatar_url} /> + <.avatar_fallback>{String.first(@bounty.owner.name)} + +
+

{@bounty.owner.name} Contributors

+

+ <% names = + org_contributors(@bounty) + |> Enum.map(&"@#{&1.handle}") %> + {if length(names) > 3, + do: "#{names |> Enum.take(3) |> Enum.join(", ")} and more", + else: "#{names |> Algora.Util.format_name_list()}"} +

+
+
+
+
+ <%= for user <- shared_users(@bounty) do %> +
+ +
+ <.avatar> + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback>{String.first(user.name)} + +
+

{user.name}

+

@{User.handle(user)}

+
+
+
+
+ <% end %> + <%= for user <- invited_users(@bounty) do %> +
+ +
+ <.icon name="tabler-mail" class="h-10 w-10 text-muted-foreground" /> +
+

{user}

+
+
+
+
+ <% end %> +
+ + +
+
+
+ + <.drawer :if={@current_user} show={@show_reward_modal} on_cancel="close_drawer"> + <.drawer_header> + <.drawer_title>Reward Bounty + <.drawer_description> + You can pay the full bounty now or start with a partial amount - it's up to you! + + + <.drawer_content class="mt-4"> + <.form for={@reward_form} phx-change="validate_reward" phx-submit="pay_with_stripe"> +
+
+ <.card> + <.card_header> + <.card_title>Payment Details + + <.card_content> +
+ <.input + label="Amount" + icon="tabler-currency-dollar" + field={@reward_form[:amount]} + /> + +
+ <.label>Recipient + <.dropdown2 id="context-dropdown" class="mt-2"> + <:img :if={@selected_context} src={@selected_context.avatar_url} /> + <:title :if={@selected_context}>{@selected_context.name} + <:subtitle :if={@selected_context}>@{@selected_context.handle} + + <:link :for={context <- @contexts} patch={"?context=#{context.id}"}> +
+ {context.name} +
+
{context.name}
+
@{context.handle}
+
+
+ + +
+ +
+ <.label>Tip +
+ <.radio_group + class="grid grid-cols-4 gap-4" + field={@reward_form[:tip_percentage]} + options={tip_options()} + /> +
+
+
+ + + <.card> + <.card_header> + <.card_title>Payment Summary + + <.card_content> +
+ <%= for line_item <- @line_items do %> +
+
+ <%= if line_item.image do %> + <.avatar> + <.avatar_image src={line_item.image} /> + + <% else %> +
+ <% end %> +
+
{line_item.title}
+
{line_item.description}
+
+
+
+ {Money.to_string!(line_item.amount)} +
+
+ <% end %> +
+
+
+
+
Total due
+
+
+ {LineItem.gross_amount(@line_items)} +
+
+
+ + +
+
+ <.button variant="secondary" phx-click="close_drawer" type="button"> + Cancel + + <.button type="submit"> + Pay with Stripe <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" /> + +
+
+ + + + """ + end + + # TODO: implement this + defp shared_users(_bounty) do + Enum.drop(Accounts.list_featured_developers(), 3) + end + + # TODO: implement this + defp invited_users(_bounty) do + ["alice@example.com", "bob@example.com"] + end + + # TODO: implement this + defp org_contributors(_bounty) do + Enum.take(Accounts.list_featured_developers(), 3) + end + + defp contexts(_bounty) do + Accounts.list_featured_developers() + end + + # TODO: implement this + defp bounty_frequency(_bounty) do + "Monthly" + end +end diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex index a27adf879..a7be02f47 100644 --- a/lib/algora_web/router.ex +++ b/lib/algora_web/router.ex @@ -96,6 +96,8 @@ defmodule AlgoraWeb.Router do ] do live "/org/:org_handle/settings", Org.SettingsLive, :edit live "/org/:org_handle/transactions", Org.TransactionsLive, :index + # TODO: allow access to invited users + live "/org/:org_handle/bounties/:id", BountyLive end live_session :org2, From 70a2b0fbf3bd4ca67f8b539ed935f8ab2f7adbd0 Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 20:26:20 +0300 Subject: [PATCH 02/35] feat: add exclusive bounty sharing functionality - Introduced a new `ExclusiveBountyForm` for sharing bounties with specific users. - Implemented changeset validation for the exclusive bounty form. - Updated `BountyLive` to handle exclusive sharing events and manage the corresponding modal. - Enhanced the UI to include a drawer for exclusive sharing with GitHub handle and deadline inputs. --- lib/algora/bounties/schemas/bounty.ex | 6 ++ lib/algora_web/live/bounty_live.ex | 134 ++++++++++++++++++++++---- 2 files changed, 123 insertions(+), 17 deletions(-) diff --git a/lib/algora/bounties/schemas/bounty.ex b/lib/algora/bounties/schemas/bounty.ex index 14a60265e..ad222dfd5 100644 --- a/lib/algora/bounties/schemas/bounty.ex +++ b/lib/algora/bounties/schemas/bounty.ex @@ -42,6 +42,12 @@ defmodule Algora.Bounties.Bounty do |> Algora.Validations.validate_money_positive(:amount) end + def settings_changeset(bounty, attrs) do + bounty + |> cast(attrs, [:visibility, :shared_with]) + |> validate_required([:visibility, :shared_with]) + end + def url(%{repository: %{name: name, owner: %{login: login}}, ticket: %{provider: "github", number: number}}) do "https://github.com/#{login}/#{name}/issues/#{number}" end diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 5de142909..bf7d86ef2 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -5,7 +5,6 @@ defmodule AlgoraWeb.BountyLive do import Ecto.Changeset alias Algora.Accounts - alias Algora.Accounts.User alias Algora.Bounties alias Algora.Bounties.Bounty alias Algora.Bounties.LineItem @@ -43,6 +42,25 @@ defmodule AlgoraWeb.BountyLive do end end + defmodule ExclusiveBountyForm do + @moduledoc false + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field :github_handle, :string + field :deadline, :date + end + + def changeset(form, attrs) do + form + |> cast(attrs, [:github_handle, :deadline]) + |> validate_required([:github_handle, :deadline]) + end + end + @impl true def mount(%{"id" => bounty_id}, _session, socket) do bounty = @@ -65,12 +83,18 @@ defmodule AlgoraWeb.BountyLive do contexts = contexts(bounty) - changeset = + reward_changeset = RewardBountyForm.changeset(%RewardBountyForm{}, %{ tip_percentage: 0, amount: Money.to_decimal(bounty.amount) }) + exclusive_changeset = + ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, %{ + github_handle: "", + deadline: Date.utc_today() + }) + {:ok, socket |> assign(:page_title, bounty.ticket.title) @@ -80,9 +104,11 @@ defmodule AlgoraWeb.BountyLive do |> assign(:ticket_body_html, ticket_body_html) |> assign(:contexts, contexts) |> assign(:show_reward_modal, false) + |> assign(:show_exclusive_modal, false) |> assign(:selected_context, nil) |> assign(:line_items, []) - |> assign(:reward_form, to_form(changeset))} + |> assign(:reward_form, to_form(reward_changeset)) + |> assign(:exclusive_form, to_form(exclusive_changeset))} end @impl true @@ -103,8 +129,12 @@ defmodule AlgoraWeb.BountyLive do {:noreply, assign(socket, :show_reward_modal, true)} end + def handle_event("exclusive", _params, socket) do + {:noreply, assign(socket, :show_exclusive_modal, true)} + end + def handle_event("close_drawer", _params, socket) do - {:noreply, assign(socket, :show_reward_modal, false)} + {:noreply, close_drawers(socket)} end def handle_event("validate_reward", %{"reward_bounty_form" => params}, socket) do @@ -133,6 +163,35 @@ defmodule AlgoraWeb.BountyLive do end end + def handle_event("validate_exclusive", %{"exclusive_bounty_form" => params}, socket) do + {:noreply, + socket + |> assign(:exclusive_form, to_form(ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, params))) + |> assign_line_items()} + end + + def handle_event("share_exclusive", %{"exclusive_bounty_form" => params}, socket) do + changeset = ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, params) + bounty = socket.assigns.bounty + + case apply_action(changeset, :save) do + {:ok, data} -> + case bounty + |> Bounty.settings_changeset(%{shared_with: Enum.uniq(bounty.shared_with ++ [data.github_handle])}) + |> Repo.update() do + {:ok, _} -> + {:noreply, socket |> put_flash(:info, "Bounty shared") |> close_drawers()} + + {:error, _reason} -> + Logger.error("Failed to share bounty: #{inspect(_reason)}") + {:noreply, put_flash(socket, :error, "Something went wrong")} + end + + {:error, changeset} -> + {:noreply, assign(socket, :exclusive_form, to_form(changeset))} + end + end + defp assign_selected_context(socket, context_id) do case Enum.find(socket.assigns.contexts, &(&1.id == context_id)) do nil -> @@ -144,13 +203,17 @@ defmodule AlgoraWeb.BountyLive do end defp assign_line_items(socket) do - line_items = - Bounties.generate_line_items( - %{amount: calculate_final_amount(socket.assigns.reward_form.source)}, - ticket_ref: ticket_ref(socket), - recipient: socket.assigns.selected_context - ) - + # line_items = + # Bounties.generate_line_items( + # %{ + # owner: socket.assigns.selected_context, + # amount: calculate_final_amount(socket.assigns.reward_form.source) + # }, + # ticket_ref: ticket_ref(socket), + # recipient: socket.assigns.selected_context + # ) + + line_items = [] assign(socket, :line_items, line_items) end @@ -264,14 +327,14 @@ defmodule AlgoraWeb.BountyLive do <.card_title> Shared with - <.button phx-click="invite"> - Invite + <.button phx-click="exclusive"> + Share Exclusive <.card_content>
-
+ <%!--
<.avatar> @@ -307,12 +370,12 @@ defmodule AlgoraWeb.BountyLive do
- <% end %> - <%= for user <- invited_users(@bounty) do %> + <% end %> --%> + <%= for user <- @bounty.shared_with do %>
- <.icon name="tabler-mail" class="h-10 w-10 text-muted-foreground" /> + <.icon name="github" class="h-10 w-10 text-muted-foreground" />

{user}

@@ -327,6 +390,37 @@ defmodule AlgoraWeb.BountyLive do
+ <.drawer + :if={@current_user} + show={@show_exclusive_modal} + on_cancel="close_drawer" + direction="right" + > + <.drawer_header> + <.drawer_title>Share Exclusive + <.drawer_description> + Make this bounty exclusive to specific users + + + <.drawer_content class="mt-4"> + <.form for={@exclusive_form} phx-change="validate_exclusive" phx-submit="share_exclusive"> +
+
+ <.input label="GitHub handle" field={@exclusive_form[:github_handle]} /> + <.input type="date" label="Deadline" field={@exclusive_form[:deadline]} /> +
+
+ <.button variant="secondary" phx-click="close_drawer" type="button"> + Cancel + + <.button type="submit"> + Submit + +
+
+ + + <.drawer :if={@current_user} show={@show_reward_modal} on_cancel="close_drawer"> <.drawer_header> <.drawer_title>Reward Bounty @@ -464,4 +558,10 @@ defmodule AlgoraWeb.BountyLive do defp bounty_frequency(_bounty) do "Monthly" end + + defp close_drawers(socket) do + socket + |> assign(:show_reward_modal, false) + |> assign(:show_exclusive_modal, false) + end end From 60a4eb49fd1af5d7c2ae9db383dfb75138f2822c Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 20:39:55 +0300 Subject: [PATCH 03/35] feat: implement exclusive sharing logic in BountyLive - Added a new private function `assign_exclusives` to manage users with exclusive access to bounties. - Updated the bounty sharing logic to include the assignment of exclusives after sharing. - Enhanced the UI to display the names of users with exclusive access, replacing previous shared user displays. - Refactored the drawer title from "Share Exclusive" to "Share" for clarity. --- lib/algora_web/live/bounty_live.ex | 100 +++++++++++------------------ 1 file changed, 36 insertions(+), 64 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index bf7d86ef2..3e0df739b 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -9,6 +9,7 @@ defmodule AlgoraWeb.BountyLive do alias Algora.Bounties.Bounty alias Algora.Bounties.LineItem alias Algora.Repo + alias Algora.Workspace require Logger @@ -108,7 +109,22 @@ defmodule AlgoraWeb.BountyLive do |> assign(:selected_context, nil) |> assign(:line_items, []) |> assign(:reward_form, to_form(reward_changeset)) - |> assign(:exclusive_form, to_form(exclusive_changeset))} + |> assign(:exclusive_form, to_form(exclusive_changeset)) + |> assign_exclusives(bounty.shared_with)} + end + + defp assign_exclusives(socket, shared_with) do + exclusives = + Enum.flat_map(shared_with, fn github_handle -> + with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user), + {:ok, user} <- Workspace.ensure_user(token, github_handle) do + [user] + else + _ -> [] + end + end) + + assign(socket, :exclusives, exclusives) end @impl true @@ -176,14 +192,20 @@ defmodule AlgoraWeb.BountyLive do case apply_action(changeset, :save) do {:ok, data} -> + shared_with = Enum.uniq(bounty.shared_with ++ [data.github_handle]) + case bounty - |> Bounty.settings_changeset(%{shared_with: Enum.uniq(bounty.shared_with ++ [data.github_handle])}) + |> Bounty.settings_changeset(%{shared_with: shared_with}) |> Repo.update() do {:ok, _} -> - {:noreply, socket |> put_flash(:info, "Bounty shared") |> close_drawers()} + {:noreply, + socket + |> put_flash(:info, "Bounty shared!") + |> assign_exclusives(shared_with) + |> close_drawers()} - {:error, _reason} -> - Logger.error("Failed to share bounty: #{inspect(_reason)}") + {:error, reason} -> + Logger.error("Failed to share bounty: #{inspect(reason)}") {:noreply, put_flash(socket, :error, "Something went wrong")} end @@ -328,62 +350,27 @@ defmodule AlgoraWeb.BountyLive do Shared with <.button phx-click="exclusive"> - Share Exclusive + Share
<.card_content> -
- <%!--
+ <%= for user <- @exclusives do %> +
<.avatar> - <.avatar_image src={@bounty.owner.avatar_url} /> - <.avatar_fallback>{String.first(@bounty.owner.name)} + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback>{String.first(user.name)}
-

{@bounty.owner.name} Contributors

-

- <% names = - org_contributors(@bounty) - |> Enum.map(&"@#{&1.handle}") %> - {if length(names) > 3, - do: "#{names |> Enum.take(3) |> Enum.join(", ")} and more", - else: "#{names |> Algora.Util.format_name_list()}"} -

+

{user.name}

+

@{user.provider_login}

- <%= for user <- shared_users(@bounty) do %> -
- -
- <.avatar> - <.avatar_image src={user.avatar_url} /> - <.avatar_fallback>{String.first(user.name)} - -
-

{user.name}

-

@{User.handle(user)}

-
-
-
-
- <% end %> --%> - <%= for user <- @bounty.shared_with do %> -
- -
- <.icon name="github" class="h-10 w-10 text-muted-foreground" /> -
-

{user}

-
-
-
-
- <% end %> -
+ <% end %>
@@ -397,7 +384,7 @@ defmodule AlgoraWeb.BountyLive do direction="right" > <.drawer_header> - <.drawer_title>Share Exclusive + <.drawer_title>Share <.drawer_description> Make this bounty exclusive to specific users @@ -535,21 +522,6 @@ defmodule AlgoraWeb.BountyLive do """ end - # TODO: implement this - defp shared_users(_bounty) do - Enum.drop(Accounts.list_featured_developers(), 3) - end - - # TODO: implement this - defp invited_users(_bounty) do - ["alice@example.com", "bob@example.com"] - end - - # TODO: implement this - defp org_contributors(_bounty) do - Enum.take(Accounts.list_featured_developers(), 3) - end - defp contexts(_bounty) do Accounts.list_featured_developers() end From 2ed1d50d116a79c64e37020818eb195fa5cfe10e Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 20:54:32 +0300 Subject: [PATCH 04/35] feat: enhance bounty sharing UI with social media options - Removed the previous `assign_exclusives` function and reintroduced it later in the file for better organization. - Added a new section in the UI for sharing bounties on social media platforms including Twitter, Reddit, LinkedIn, and Hacker News. - Implemented a new `social_share_button` component to handle the sharing functionality and user interactions. - Updated the layout to improve the visibility and accessibility of sharing options. --- lib/algora_web/live/bounty_live.ex | 97 +++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 14 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 3e0df739b..f8ea4f6a6 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -113,20 +113,6 @@ defmodule AlgoraWeb.BountyLive do |> assign_exclusives(bounty.shared_with)} end - defp assign_exclusives(socket, shared_with) do - exclusives = - Enum.flat_map(shared_with, fn github_handle -> - with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user), - {:ok, user} <- Workspace.ensure_user(token, github_handle) do - [user] - else - _ -> [] - end - end) - - assign(socket, :exclusives, exclusives) - end - @impl true def handle_params(_params, _url, %{assigns: %{current_user: nil}} = socket) do {:noreply, socket} @@ -373,6 +359,45 @@ defmodule AlgoraWeb.BountyLive do <% end %> + + <.card> + <.card_header> +
+ <.card_title> + Share on socials + +
+ <.social_share_button + id="twitter-share-url" + icon="tabler-brand-x" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="reddit-share-url" + icon="tabler-brand-reddit" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="linkedin-share-url" + icon="tabler-brand-linkedin" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="hackernews-share-url" + icon="tabler-brand-ycombinator" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> +
+
+ + <.card_content> + {@bounty.ticket.title} + +
@@ -522,6 +547,36 @@ defmodule AlgoraWeb.BountyLive do """ end + defp social_share_button(assigns) do + ~H""" +
JS.hide( + to: "##{@id}-copy-icon", + transition: {"transition-opacity", "opacity-100", "opacity-0"} + ) + |> JS.show( + to: "##{@id}-check-icon", + transition: {"transition-opacity", "opacity-0", "opacity-100"} + ) + } + class="size-6 relative cursor-pointer mt-3 text-foreground/90 hover:text-foreground" + variant="outline" + > + <.icon id={@id <> "-copy-icon"} name={@icon} class="absolute my-auto size-6 mr-2" /> + <.icon + id={@id <> "-check-icon"} + name="tabler-check" + class="absolute my-auto hidden size-6 mr-2" + /> +
+ """ + end + defp contexts(_bounty) do Accounts.list_featured_developers() end @@ -536,4 +591,18 @@ defmodule AlgoraWeb.BountyLive do |> assign(:show_reward_modal, false) |> assign(:show_exclusive_modal, false) end + + defp assign_exclusives(socket, shared_with) do + exclusives = + Enum.flat_map(shared_with, fn github_handle -> + with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user), + {:ok, user} <- Workspace.ensure_user(token, github_handle) do + [user] + else + _ -> [] + end + end) + + assign(socket, :exclusives, exclusives) + end end From 5ea24570e7172d1332c18d8a40c08a07dfbc80e0 Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 21:05:14 +0300 Subject: [PATCH 05/35] update layout to make space for chat --- lib/algora_web/components/ui/card.ex | 6 +- lib/algora_web/live/bounty_live.ex | 216 ++++++++++++--------------- 2 files changed, 99 insertions(+), 123 deletions(-) diff --git a/lib/algora_web/components/ui/card.ex b/lib/algora_web/components/ui/card.ex index 52c61459b..c3b88e75c 100644 --- a/lib/algora_web/components/ui/card.ex +++ b/lib/algora_web/components/ui/card.ex @@ -41,7 +41,7 @@ defmodule AlgoraWeb.Components.UI.Card do def card_header(assigns) do ~H""" -
+
{render_slot(@inner_block)}
""" @@ -77,7 +77,7 @@ defmodule AlgoraWeb.Components.UI.Card do def card_content(assigns) do ~H""" -
+
{render_slot(@inner_block)}
""" @@ -89,7 +89,7 @@ defmodule AlgoraWeb.Components.UI.Card do def card_footer(assigns) do ~H""" -
+
{render_slot(@inner_block)}
""" diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index f8ea4f6a6..7447afda5 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -263,139 +263,115 @@ defmodule AlgoraWeb.BountyLive do
<.card> - <.card_header> -
- <.avatar class="h-12 w-12 rounded-full"> - <.avatar_image src={@ticket.repository.user.avatar_url} /> - <.avatar_fallback> - {String.first(@ticket.repository.user.provider_login)} - - -
- <.link - href={@ticket.url} - class="text-xl font-semibold hover:underline" - target="_blank" - > - {@ticket.title} - -
- {@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number} + <.card_content> +
+
+ <.avatar class="h-12 w-12 rounded-full"> + <.avatar_image src={@ticket.repository.user.avatar_url} /> + <.avatar_fallback> + {String.first(@ticket.repository.user.provider_login)} + + +
+ <.link + href={@ticket.url} + class="text-xl font-semibold hover:underline" + target="_blank" + > + {@ticket.title} + +
+ {@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number} +
+
+ {Money.to_string!(@bounty.amount)} +
- - <.card_content> -
- {Phoenix.HTML.raw(@ticket_body_html)} -
- - -
- -
- <.card> - <.card_header> -
- <.card_title> - Bounty - +
<.button phx-click="reward"> Reward -
- - <.card_content> -
-
- Frequency - - {bounty_frequency(@bounty)} - -
-
- Amount - - {Money.to_string!(@bounty.amount)} - -
-
- Total paid - - {Money.to_string!(@total_paid)} - -
+ <.button variant="secondary" phx-click="exclusive"> + <.icon name="tabler-lock" class="h-4 w-4 mr-2 -ml-1" /> Exclusive +
- - <.card> - <.card_header> -
- <.card_title> - Shared with - - <.button phx-click="exclusive"> - Share - -
- - <.card_content> - <%= for user <- @exclusives do %> -
- -
- <.avatar> - <.avatar_image src={user.avatar_url} /> - <.avatar_fallback>{String.first(user.name)} - -
-

{user.name}

-

@{user.provider_login}

+
+ <.card> + <.card_header> +
+ <.card_title> + Exclusives + +
+ + <.card_content> + <%= for user <- @exclusives do %> +
+ +
+ <.avatar> + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback>{String.first(user.name)} + +
+

{user.name}

+

@{user.provider_login}

+
-
- + +
+ <% end %> + + + + <.card> + <.card_header> +
+ <.card_title> + Share on socials + +
+ <.social_share_button + id="twitter-share-url" + icon="tabler-brand-x" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="reddit-share-url" + icon="tabler-brand-reddit" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="linkedin-share-url" + icon="tabler-brand-linkedin" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="hackernews-share-url" + icon="tabler-brand-ycombinator" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> +
- <% end %> - - - + + <.card_content> + {@bounty.ticket.title} + + +
<.card> - <.card_header> -
- <.card_title> - Share on socials - -
- <.social_share_button - id="twitter-share-url" - icon="tabler-brand-x" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> - <.social_share_button - id="reddit-share-url" - icon="tabler-brand-reddit" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> - <.social_share_button - id="linkedin-share-url" - icon="tabler-brand-linkedin" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> - <.social_share_button - id="hackernews-share-url" - icon="tabler-brand-ycombinator" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> -
-
- <.card_content> - {@bounty.ticket.title} +
+ {Phoenix.HTML.raw(@ticket_body_html)} +
From 9c3a6a0bc2550cef153632eabb8ab082c0b036c4 Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 21:12:25 +0300 Subject: [PATCH 06/35] add chatbox --- lib/algora_web/live/bounty_live.ex | 110 ++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 7447afda5..c7d724709 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -69,6 +69,7 @@ defmodule AlgoraWeb.BountyLive do |> Repo.get!(bounty_id) |> Repo.preload([ :owner, + :creator, :transactions, ticket: [repository: [:user]] ]) @@ -108,6 +109,7 @@ defmodule AlgoraWeb.BountyLive do |> assign(:show_exclusive_modal, false) |> assign(:selected_context, nil) |> assign(:line_items, []) + |> assign(:messages, []) |> assign(:reward_form, to_form(reward_changeset)) |> assign(:exclusive_form, to_form(exclusive_changeset)) |> assign_exclusives(bounty.shared_with)} @@ -259,9 +261,9 @@ defmodule AlgoraWeb.BountyLive do @impl true def render(assigns) do ~H""" -
-
-
+
+ <.scroll_area class="h-[calc(100vh-64px)] flex-1 p-4"> +
<.card> <.card_content>
@@ -375,6 +377,108 @@ defmodule AlgoraWeb.BountyLive do
+ + +
+
+
+
+ <.avatar> + <.avatar_image src={@bounty.owner.avatar_url} alt="Developer avatar" /> + <.avatar_fallback> + {Algora.Util.initials(@bounty.owner.name)} + + +
+
+
+
+

{@bounty.owner.name}

+
+
+
+ + <.scroll_area + class="flex h-full flex-1 flex-col-reverse gap-6 p-4" + id="messages-container" + phx-hook="ScrollToBottom" + > +
+ <%= for {date, messages} <- @messages + |> Enum.group_by(fn msg -> + case Date.diff(Date.utc_today(), DateTime.to_date(msg.inserted_at)) do + 0 -> "Today" + 1 -> "Yesterday" + n when n <= 7 -> Calendar.strftime(msg.inserted_at, "%A") + _ -> Calendar.strftime(msg.inserted_at, "%b %d") + end + end) + |> Enum.sort_by(fn {_, msgs} -> hd(msgs).inserted_at end, Date) do %> +
+
+ {date} +
+
+ +
+ <%= for message <- Enum.sort_by(messages, & &1.inserted_at, Date) do %> +
+ <.avatar class="h-8 w-8"> + <.avatar_image src={message.sender.avatar_url} /> + <.avatar_fallback> + {Util.initials(message.sender.name)} + + +
+ {message.content} +
+ {message.inserted_at + |> DateTime.to_time() + |> Time.to_string() + |> String.slice(0..4)} +
+
+
+ <% end %> +
+ <% end %> +
+ + +
+
+
+ <.input + id="message-input" + type="text" + name="message" + value="" + placeholder="Type a message..." + autocomplete="off" + class="flex-1 pr-24" + phx-hook="ClearInput" + /> +
+ <.button + type="button" + variant="ghost" + size="icon-sm" + phx-hook="EmojiPicker" + id="emoji-trigger" + > + <.icon name="tabler-mood-smile" class="h-4 w-4" /> + +
+
+ <.button type="submit" size="icon"> + <.icon name="tabler-send" class="h-4 w-4" /> + +
+ + +
From 4c8c5203cea12f1468708f691bf5956002245d0f Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 21:15:11 +0300 Subject: [PATCH 07/35] refactor: update chat section layout in BountyLive - Replaced the previous owner display with a chat header and a dynamic list of exclusive users. - Improved the layout for better alignment and spacing in the chat section. - Removed the unused `bounty_frequency` function as part of the cleanup. --- lib/algora_web/live/bounty_live.ex | 31 +++++++++++++----------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index c7d724709..ac8a07752 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -381,19 +381,19 @@ defmodule AlgoraWeb.BountyLive do
-
-
- <.avatar> - <.avatar_image src={@bounty.owner.avatar_url} alt="Developer avatar" /> - <.avatar_fallback> - {Algora.Util.initials(@bounty.owner.name)} - - -
-
-
-
-

{@bounty.owner.name}

+
+

+ Chat +

+
+ <%= for user <- @exclusives do %> + <.avatar> + <.avatar_image src={user.avatar_url} alt="Developer avatar" /> + <.avatar_fallback> + {Algora.Util.initials(@bounty.owner.name)} + + + <% end %>
@@ -661,11 +661,6 @@ defmodule AlgoraWeb.BountyLive do Accounts.list_featured_developers() end - # TODO: implement this - defp bounty_frequency(_bounty) do - "Monthly" - end - defp close_drawers(socket) do socket |> assign(:show_reward_modal, false) From e0986e5e8d85aff4709662173fbdea995a55e008 Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 21:26:49 +0300 Subject: [PATCH 08/35] refactor: improve layout and styling in BountyLive - Adjusted padding in the scroll area for better spacing. - Simplified the structure of the bounty display by consolidating elements. - Enhanced button styling and layout for social share functionality. - Updated the social share button to use a more consistent design. --- lib/algora_web/live/bounty_live.ex | 42 ++++++++++++++++-------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index ac8a07752..abeab652f 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -262,12 +262,12 @@ defmodule AlgoraWeb.BountyLive do def render(assigns) do ~H"""
- <.scroll_area class="h-[calc(100vh-64px)] flex-1 p-4"> + <.scroll_area class="h-[calc(100vh-64px)] flex-1 p-4 pr-6">
<.card> <.card_content>
-
+
<.avatar class="h-12 w-12 rounded-full"> <.avatar_image src={@ticket.repository.user.avatar_url} /> <.avatar_fallback> @@ -287,18 +287,15 @@ defmodule AlgoraWeb.BountyLive do
-
- {Money.to_string!(@bounty.amount)} +
+
+ {Money.to_string!(@bounty.amount)} +
+ <.button phx-click="reward"> + Reward +
-
- <.button phx-click="reward"> - Reward - - <.button variant="secondary" phx-click="exclusive"> - <.icon name="tabler-lock" class="h-4 w-4 mr-2 -ml-1" /> Exclusive - -
@@ -308,6 +305,9 @@ defmodule AlgoraWeb.BountyLive do <.card_title> Exclusives + <.button variant="secondary" phx-click="exclusive"> + <.icon name="tabler-user-plus" class="h-4 w-4 mr-2 -ml-1" /> Add +
<.card_content> @@ -336,7 +336,7 @@ defmodule AlgoraWeb.BountyLive do <.card_title> Share on socials -
+
<.social_share_button id="twitter-share-url" icon="tabler-brand-x" @@ -629,10 +629,11 @@ defmodule AlgoraWeb.BountyLive do defp social_share_button(assigns) do ~H""" -
JS.hide( @@ -644,16 +645,19 @@ defmodule AlgoraWeb.BountyLive do transition: {"transition-opacity", "opacity-0", "opacity-100"} ) } - class="size-6 relative cursor-pointer mt-3 text-foreground/90 hover:text-foreground" - variant="outline" + class="size-8 relative cursor-pointer text-foreground/90 hover:text-foreground bg-muted" > - <.icon id={@id <> "-copy-icon"} name={@icon} class="absolute my-auto size-6 mr-2" /> + <.icon + id={@id <> "-copy-icon"} + name={@icon} + class="absolute inset-0 m-auto size-5 flex items-center justify-center" + /> <.icon id={@id <> "-check-icon"} name="tabler-check" - class="absolute my-auto hidden size-6 mr-2" + class="absolute inset-0 m-auto hidden size-5 flex items-center justify-center" /> -
+ """ end From 83f9332c66af420b95458128283a71b71cfdabe5 Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 21:57:12 +0300 Subject: [PATCH 09/35] refactor: improve exclusives and social sharing layout in BountyLive - Enhanced the layout of the exclusives section, adding expiration date visibility and reorganizing user display. - Updated the social sharing section for better alignment and added a title for clarity. - Improved button styling and layout for a more cohesive user experience. --- lib/algora_web/live/bounty_live.ex | 135 ++++++++++++++++------------- 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index abeab652f..f7a0d15d8 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -299,73 +299,90 @@ defmodule AlgoraWeb.BountyLive do
- <.card> - <.card_header> -
- <.card_title> - Exclusives - - <.button variant="secondary" phx-click="exclusive"> - <.icon name="tabler-user-plus" class="h-4 w-4 mr-2 -ml-1" /> Add - -
- + <.card class="col"> <.card_content> - <%= for user <- @exclusives do %> -
- -
- <.avatar> - <.avatar_image src={user.avatar_url} /> - <.avatar_fallback>{String.first(user.name)} - -
-

{user.name}

-

@{user.provider_login}

-
+
+
+ <.card_title> + Exclusives + +
+ + Expires on {Calendar.strftime(@bounty.inserted_at, "%b %d, %Y")} + + <.button + variant="ghost" + size="icon-sm" + phx-click="exclusive" + class="group h-6 w-6" + > + <.icon + name="tabler-pencil" + class="h-4 w-4 text-muted-foreground group-hover:text-foreground" + /> + +
+ <.button variant="secondary" phx-click="exclusive" class="mt-3"> + <.icon name="tabler-user-plus" class="h-4 w-4 mr-2 -ml-1" /> Add + +
+
+ <%= for user <- @exclusives do %> +
+ +
+ <.avatar> + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback>{String.first(user.name)} + +
+

{user.name}

+

@{user.provider_login}

+
+
+
- + <% end %>
- <% end %> +
- <.card> - <.card_header> + <.card_content>
- <.card_title> - Share on socials - -
- <.social_share_button - id="twitter-share-url" - icon="tabler-brand-x" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> - <.social_share_button - id="reddit-share-url" - icon="tabler-brand-reddit" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> - <.social_share_button - id="linkedin-share-url" - icon="tabler-brand-linkedin" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> - <.social_share_button - id="hackernews-share-url" - icon="tabler-brand-ycombinator" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} - /> +
+ <.card_title> + Share on socials + +
+ <.social_share_button + id="twitter-share-url" + icon="tabler-brand-x" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="reddit-share-url" + icon="tabler-brand-reddit" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="linkedin-share-url" + icon="tabler-brand-linkedin" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> + <.social_share_button + id="hackernews-share-url" + icon="tabler-brand-ycombinator" + value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + /> +
+ {@bounty.ticket.title}
- - <.card_content> - {@bounty.ticket.title}
@@ -383,7 +400,7 @@ defmodule AlgoraWeb.BountyLive do

- Chat + Contributor chat

<%= for user <- @exclusives do %> From 6ab55c76cf9455da3dc269a59aff3ac50660c76b Mon Sep 17 00:00:00 2001 From: zafer Date: Mon, 31 Mar 2025 22:20:44 +0300 Subject: [PATCH 10/35] refactor: simplify bounty display in Bounties component - Removed the repository name display from the bounty section for a cleaner layout. - Maintained focus on key information such as ticket number and bounty amount. --- lib/algora_web/components/bounties.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/algora_web/components/bounties.ex b/lib/algora_web/components/bounties.ex index f6985bb9f..8e3176443 100644 --- a/lib/algora_web/components/bounties.ex +++ b/lib/algora_web/components/bounties.ex @@ -25,10 +25,6 @@ defmodule AlgoraWeb.Components.Bounties do
{bounty.repository.owner.name} - <.icon name="tabler-chevron-right" class="mr-1 size-3 text-muted-foreground" /> - - {bounty.repository.name} - #{bounty.ticket.number} {Money.to_string!(bounty.amount)} From d7c3c12efa8c066ad2ed4fe5fb37237e1a37b03a Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 08:38:26 +0300 Subject: [PATCH 11/35] add missing alias --- lib/algora_web/live/bounty_live.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index f7a0d15d8..399bc31f5 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -9,6 +9,7 @@ defmodule AlgoraWeb.BountyLive do alias Algora.Bounties.Bounty alias Algora.Bounties.LineItem alias Algora.Repo + alias Algora.Util alias Algora.Workspace require Logger From 21ea0586301c911a7b67e3503f5ecc810a7c4c7d Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 08:41:11 +0300 Subject: [PATCH 12/35] refactor: enhance bounty display styling in BountyLive - Increased avatar size and adjusted layout for better visual appeal. - Updated link and metadata text sizes for improved readability and emphasis on key information. --- lib/algora_web/live/bounty_live.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 399bc31f5..ea49bc8b4 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -268,8 +268,8 @@ defmodule AlgoraWeb.BountyLive do <.card> <.card_content>
-
- <.avatar class="h-12 w-12 rounded-full"> +
+ <.avatar class="h-20 w-20 rounded-2xl"> <.avatar_image src={@ticket.repository.user.avatar_url} /> <.avatar_fallback> {String.first(@ticket.repository.user.provider_login)} @@ -278,12 +278,12 @@ defmodule AlgoraWeb.BountyLive do
<.link href={@ticket.url} - class="text-xl font-semibold hover:underline" + class="text-4xl font-semibold hover:underline" target="_blank" > {@ticket.title} -
+
{@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number}
From d307d3d6072068b790bfa4b4c5c5a2e396a1555d Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 08:53:27 +0300 Subject: [PATCH 13/35] feat: add user retrieval by provider ID in Workspace - Introduced `ensure_user_by_provider_id` function to fetch or create a user based on GitHub provider ID. - Updated `BountyLive` to utilize the new function for user management during bounty sharing, enhancing user experience and data handling. --- lib/algora/workspace/workspace.ex | 7 +++++++ lib/algora_web/live/bounty_live.ex | 27 ++++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/algora/workspace/workspace.ex b/lib/algora/workspace/workspace.ex index c6b528e62..1e469feab 100644 --- a/lib/algora/workspace/workspace.ex +++ b/lib/algora/workspace/workspace.ex @@ -200,6 +200,13 @@ defmodule Algora.Workspace do end end + def ensure_user_by_provider_id(token, provider_id) do + case Repo.get_by(User, provider: "github", provider_id: provider_id) do + %User{} = user -> {:ok, user} + nil -> create_user_from_github(token, provider_id) + end + end + def sync_user(user, repository, owner, repo) do github_user = repository["owner"] diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index ea49bc8b4..224293e73 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -181,17 +181,18 @@ defmodule AlgoraWeb.BountyLive do case apply_action(changeset, :save) do {:ok, data} -> - shared_with = Enum.uniq(bounty.shared_with ++ [data.github_handle]) - - case bounty - |> Bounty.settings_changeset(%{shared_with: shared_with}) - |> Repo.update() do - {:ok, _} -> - {:noreply, - socket - |> put_flash(:info, "Bounty shared!") - |> assign_exclusives(shared_with) - |> close_drawers()} + with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user), + {:ok, user} <- Workspace.ensure_user(token, data.github_handle), + shared_with = Enum.uniq(bounty.shared_with ++ [user.provider_id]), + {:ok, _} <- bounty |> Bounty.settings_changeset(%{shared_with: shared_with}) |> Repo.update() do + {:noreply, + socket + |> put_flash(:info, "Bounty shared!") + |> assign_exclusives(shared_with) + |> close_drawers()} + else + nil -> + {:noreply, put_flash(socket, :error, "User not found")} {:error, reason} -> Logger.error("Failed to share bounty: #{inspect(reason)}") @@ -691,9 +692,9 @@ defmodule AlgoraWeb.BountyLive do defp assign_exclusives(socket, shared_with) do exclusives = - Enum.flat_map(shared_with, fn github_handle -> + Enum.flat_map(shared_with, fn provider_id -> with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user), - {:ok, user} <- Workspace.ensure_user(token, github_handle) do + {:ok, user} <- Workspace.ensure_user_by_provider_id(token, provider_id) do [user] else _ -> [] From 4b447f6d36d93023fd95ee3500db834e3c0f0ddc Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 08:55:40 +0300 Subject: [PATCH 14/35] refactor: update styling and layout in BountyLive - Adjusted icon sizes for better visual consistency across buttons. - Increased padding in the social sharing section for improved spacing. - Added a new card header for the description section to enhance content organization. --- lib/algora_web/live/bounty_live.ex | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 224293e73..a2fc01eec 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -325,7 +325,7 @@ defmodule AlgoraWeb.BountyLive do
<.button variant="secondary" phx-click="exclusive" class="mt-3"> - <.icon name="tabler-user-plus" class="h-4 w-4 mr-2 -ml-1" /> Add + <.icon name="tabler-user-plus" class="size-5 mr-2 -ml-1" /> Add
@@ -356,7 +356,7 @@ defmodule AlgoraWeb.BountyLive do <.card_title> Share on socials -
+
<.social_share_button id="twitter-share-url" icon="tabler-brand-x" @@ -389,6 +389,11 @@ defmodule AlgoraWeb.BountyLive do
<.card> + <.card_header> + <.card_title> + Description + + <.card_content>
{Phoenix.HTML.raw(@ticket_body_html)} @@ -664,17 +669,17 @@ defmodule AlgoraWeb.BountyLive do transition: {"transition-opacity", "opacity-0", "opacity-100"} ) } - class="size-8 relative cursor-pointer text-foreground/90 hover:text-foreground bg-muted" + class="size-9 relative cursor-pointer text-foreground/90 hover:text-foreground bg-muted" > <.icon id={@id <> "-copy-icon"} name={@icon} - class="absolute inset-0 m-auto size-5 flex items-center justify-center" + class="absolute inset-0 m-auto size-6 flex items-center justify-center" /> <.icon id={@id <> "-check-icon"} name="tabler-check" - class="absolute inset-0 m-auto hidden size-5 flex items-center justify-center" + class="absolute inset-0 m-auto hidden size-6 flex items-center justify-center" /> """ From e2d1c3f0a8c977c6d2bee19b608a0f26ae0d6146 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 09:56:05 +0300 Subject: [PATCH 15/35] feat: add deadline field to bounties and implement validation - Introduced a new `deadline` field in the Bounty schema to track expiration dates. - Added a validation function to ensure deadlines are set in the future. - Updated changesets in BountyLive to handle the new deadline field and its validation. - Created a migration to add the deadline column to the bounties table. --- lib/algora/bounties/schemas/bounty.ex | 4 +- lib/algora/shared/validations.ex | 10 ++++ lib/algora_web/live/bounty_live.ex | 48 +++++++++++-------- ...0250401060054_add_deadline_to_bounties.exs | 9 ++++ 4 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 priv/repo/migrations/20250401060054_add_deadline_to_bounties.exs diff --git a/lib/algora/bounties/schemas/bounty.ex b/lib/algora/bounties/schemas/bounty.ex index ad222dfd5..b655e17ba 100644 --- a/lib/algora/bounties/schemas/bounty.ex +++ b/lib/algora/bounties/schemas/bounty.ex @@ -14,6 +14,7 @@ defmodule Algora.Bounties.Bounty do field :autopay_disabled, :boolean, default: false field :visibility, Ecto.Enum, values: [:community, :exclusive, :public], null: false, default: :community field :shared_with, {:array, :string}, null: false, default: [] + field :deadline, :utc_datetime_usec belongs_to :ticket, Algora.Workspace.Ticket belongs_to :owner, User @@ -44,7 +45,8 @@ defmodule Algora.Bounties.Bounty do def settings_changeset(bounty, attrs) do bounty - |> cast(attrs, [:visibility, :shared_with]) + |> cast(attrs, [:visibility, :shared_with, :deadline]) + |> Algora.Validations.validate_date_in_future(:deadline) |> validate_required([:visibility, :shared_with]) end diff --git a/lib/algora/shared/validations.ex b/lib/algora/shared/validations.ex index 26231a630..1e57806b0 100644 --- a/lib/algora/shared/validations.ex +++ b/lib/algora/shared/validations.ex @@ -39,4 +39,14 @@ defmodule Algora.Validations do _ -> changeset end end + + def validate_date_in_future(changeset, field) do + validate_change(changeset, field, fn _, date -> + if date && Date.before?(date, DateTime.utc_now()) do + [{field, "must be in the future"}] + else + [] + end + end) + end end diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index a2fc01eec..0d506f64a 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -59,7 +59,8 @@ defmodule AlgoraWeb.BountyLive do def changeset(form, attrs) do form |> cast(attrs, [:github_handle, :deadline]) - |> validate_required([:github_handle, :deadline]) + |> validate_required([:github_handle]) + |> Algora.Validations.validate_date_in_future(:deadline) end end @@ -92,11 +93,7 @@ defmodule AlgoraWeb.BountyLive do amount: Money.to_decimal(bounty.amount) }) - exclusive_changeset = - ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, %{ - github_handle: "", - deadline: Date.utc_today() - }) + exclusive_changeset = ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, %{}) {:ok, socket @@ -184,10 +181,17 @@ defmodule AlgoraWeb.BountyLive do with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user), {:ok, user} <- Workspace.ensure_user(token, data.github_handle), shared_with = Enum.uniq(bounty.shared_with ++ [user.provider_id]), - {:ok, _} <- bounty |> Bounty.settings_changeset(%{shared_with: shared_with}) |> Repo.update() do + {:ok, bounty} <- + bounty + |> Bounty.settings_changeset(%{ + shared_with: shared_with, + deadline: DateTime.new!(data.deadline, ~T[00:00:00], "Etc/UTC") + }) + |> Repo.update() do {:noreply, socket |> put_flash(:info, "Bounty shared!") + |> assign(:bounty, bounty) |> assign_exclusives(shared_with) |> close_drawers()} else @@ -310,19 +314,25 @@ defmodule AlgoraWeb.BountyLive do
- Expires on {Calendar.strftime(@bounty.inserted_at, "%b %d, %Y")} + <%= if @bounty.deadline do %> + Expires on {Calendar.strftime(@bounty.deadline, "%b %d, %Y")} + <.button + variant="ghost" + size="icon-sm" + phx-click="exclusive" + class="group h-6 w-6" + > + <.icon + name="tabler-pencil" + class="h-4 w-4 text-muted-foreground group-hover:text-foreground" + /> + + <% else %> + + Add a deadline + + <% end %> - <.button - variant="ghost" - size="icon-sm" - phx-click="exclusive" - class="group h-6 w-6" - > - <.icon - name="tabler-pencil" - class="h-4 w-4 text-muted-foreground group-hover:text-foreground" - /> -
<.button variant="secondary" phx-click="exclusive" class="mt-3"> <.icon name="tabler-user-plus" class="size-5 mr-2 -ml-1" /> Add diff --git a/priv/repo/migrations/20250401060054_add_deadline_to_bounties.exs b/priv/repo/migrations/20250401060054_add_deadline_to_bounties.exs new file mode 100644 index 000000000..57f052cbc --- /dev/null +++ b/priv/repo/migrations/20250401060054_add_deadline_to_bounties.exs @@ -0,0 +1,9 @@ +defmodule Algora.Repo.Migrations.AddDeadlineToBounties do + use Ecto.Migration + + def change do + alter table(:bounties) do + add :deadline, :utc_datetime_usec + end + end +end From a6be136d9eea2fe37e3b570c7ff4684055c481d9 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 10:24:56 +0300 Subject: [PATCH 16/35] feat: enhance chat functionality and thread management - Introduced a new `MessageCreated` struct to encapsulate message and participant data for broadcasting. - Refactored the `broadcast` function to utilize the new struct for improved clarity. - Added `ensure_participant` and `insert_message` private functions to streamline message handling. - Implemented `get_or_create_bounty_thread` to manage thread creation for bounties. - Updated `BountyLive` to integrate chat features, including message and participant management. - Added `bounty_id` field to the `Thread` schema and created a migration for the database update. --- lib/algora/chat/chat.ex | 77 +++++++++++++++---- lib/algora/chat/schemas/thread.ex | 5 +- lib/algora_web/live/bounty_live.ex | 63 ++++++++++++++- lib/algora_web/live/chat/thread_live.ex | 2 +- lib/algora_web/live/contract/view_live.ex | 2 +- ...0250401065753_add_bounty_id_to_threads.exs | 11 +++ 6 files changed, 135 insertions(+), 25 deletions(-) create mode 100644 priv/repo/migrations/20250401065753_add_bounty_id_to_threads.exs diff --git a/lib/algora/chat/chat.ex b/lib/algora/chat/chat.ex index bd45843a4..47402feb7 100644 --- a/lib/algora/chat/chat.ex +++ b/lib/algora/chat/chat.ex @@ -8,8 +8,13 @@ defmodule Algora.Chat do alias Algora.Chat.Thread alias Algora.Repo - def broadcast(message) do - Phoenix.PubSub.broadcast(Algora.PubSub, "chat:thread:#{message.thread_id}", message) + defmodule MessageCreated do + @moduledoc false + defstruct message: nil, participant: nil + end + + def broadcast(%MessageCreated{} = event) do + Phoenix.PubSub.broadcast(Algora.PubSub, "chat:thread:#{event.message.thread_id}", event) end def subscribe(thread_id) do @@ -23,7 +28,6 @@ defmodule Algora.Chat do |> Thread.changeset(%{title: "#{User.handle(user_1)} <> #{User.handle(user_2)}"}) |> Repo.insert() - # Add participants for user <- [user_1, user_2] do %Participant{} |> Participant.changeset(%{ @@ -46,7 +50,7 @@ defmodule Algora.Chat do |> Repo.insert() participants = Enum.uniq_by([user | admins], & &1.id) - # Add participants + for u <- participants do %Participant{} |> Participant.changeset(%{ @@ -61,20 +65,41 @@ defmodule Algora.Chat do end) end + defp ensure_participant(thread_id, user_id) do + case Repo.fetch_by(Participant, thread_id: thread_id, user_id: user_id) do + {:ok, participant} -> + {:ok, participant} + + {:error, _} -> + %Participant{} + |> Participant.changeset(%{ + thread_id: thread_id, + user_id: user_id, + last_read_at: DateTime.utc_now() + }) + |> Repo.insert() + end + end + + defp insert_message(thread_id, sender_id, content) do + %Message{} + |> Message.changeset(%{ + thread_id: thread_id, + sender_id: sender_id, + content: content + }) + |> Repo.insert() + end + def send_message(thread_id, sender_id, content) do - case %Message{} - |> Message.changeset(%{ - thread_id: thread_id, - sender_id: sender_id, - content: content - }) - |> Repo.insert() do - {:ok, message} -> - message |> Repo.preload(:sender) |> broadcast() - {:ok, message} - - {:error, changeset} -> - {:error, changeset} + with {:ok, participant} <- ensure_participant(thread_id, sender_id), + {:ok, message} <- insert_message(thread_id, sender_id, content) do + broadcast(%MessageCreated{ + message: Repo.preload(message, :sender), + participant: Repo.preload(participant, :user) + }) + + {:ok, message} end end @@ -107,6 +132,12 @@ defmodule Algora.Chat do |> Repo.all() end + def list_participants(thread_id) do + Participant + |> where(thread_id: ^thread_id) + |> Repo.all() + end + def mark_as_read(thread_id, user_id) do Participant |> where(thread_id: ^thread_id, user_id: ^user_id) @@ -145,4 +176,16 @@ defmodule Algora.Chat do thread -> {:ok, thread} end end + + def get_or_create_bounty_thread(bounty) do + case Repo.fetch_by(Thread, bounty_id: bounty.id) do + {:ok, thread} -> + {:ok, thread} + + {:error, _} -> + %Thread{} + |> Thread.changeset(%{title: "Contributor chat", bounty_id: bounty.id}) + |> Repo.insert() + end + end end diff --git a/lib/algora/chat/schemas/thread.ex b/lib/algora/chat/schemas/thread.ex index 98475fcad..0f2da6242 100644 --- a/lib/algora/chat/schemas/thread.ex +++ b/lib/algora/chat/schemas/thread.ex @@ -6,7 +6,7 @@ defmodule Algora.Chat.Thread do typed_schema "threads" do field :title, :string - + field :bounty_id, :string has_many :messages, Algora.Chat.Message has_many :participants, Algora.Chat.Participant has_many :activities, {"thread_activities", Activity}, foreign_key: :assoc_id @@ -16,8 +16,9 @@ defmodule Algora.Chat.Thread do def changeset(thread, attrs) do thread - |> cast(attrs, [:title]) + |> cast(attrs, [:title, :bounty_id]) |> validate_required([:title]) |> generate_id() + |> unique_constraint(:bounty_id) end end diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 0d506f64a..a0cfddb1e 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -8,6 +8,7 @@ defmodule AlgoraWeb.BountyLive do alias Algora.Bounties alias Algora.Bounties.Bounty alias Algora.Bounties.LineItem + alias Algora.Chat alias Algora.Repo alias Algora.Util alias Algora.Workspace @@ -95,6 +96,14 @@ defmodule AlgoraWeb.BountyLive do exclusive_changeset = ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, %{}) + {:ok, thread} = Chat.get_or_create_bounty_thread(bounty) + messages = thread.id |> Chat.list_messages() |> Repo.preload(:sender) + participants = thread.id |> Chat.list_participants() |> Repo.preload(:user) + + if connected?(socket) do + Chat.subscribe(thread.id) + end + {:ok, socket |> assign(:page_title, bounty.ticket.title) @@ -107,7 +116,9 @@ defmodule AlgoraWeb.BountyLive do |> assign(:show_exclusive_modal, false) |> assign(:selected_context, nil) |> assign(:line_items, []) - |> assign(:messages, []) + |> assign(:thread, thread) + |> assign(:messages, messages) + |> assign(:participants, participants) |> assign(:reward_form, to_form(reward_changeset)) |> assign(:exclusive_form, to_form(exclusive_changeset)) |> assign_exclusives(bounty.shared_with)} @@ -118,27 +129,68 @@ defmodule AlgoraWeb.BountyLive do {:noreply, socket} end + @impl true def handle_params(%{"context" => context_id}, _url, socket) do {:noreply, socket |> assign_selected_context(context_id) |> assign_line_items()} end + @impl true def handle_params(_params, _url, socket) do {:noreply, socket} end + @impl true + def handle_info(%Chat.MessageCreated{message: message, participant: participant}, socket) do + socket = + if message.id in Enum.map(socket.assigns.messages, & &1.id) do + socket + else + Phoenix.Component.update(socket, :messages, &(&1 ++ [message])) + end + + socket = + if participant.id in Enum.map(socket.assigns.participants, & &1.id) do + socket + else + Phoenix.Component.update(socket, :participants, &(&1 ++ [participant])) + end + + {:noreply, socket} + end + + @impl true + def handle_event("send_message", %{"message" => content}, socket) do + {:ok, message} = + Chat.send_message( + socket.assigns.thread.id, + socket.assigns.current_user.id, + content + ) + + message = Repo.preload(message, :sender) + + {:noreply, + socket + |> update(:messages, &(&1 ++ [message])) + |> push_event("clear-input", %{selector: "#message-input"})} + end + @impl true def handle_event("reward", _params, socket) do {:noreply, assign(socket, :show_reward_modal, true)} end + @impl true def handle_event("exclusive", _params, socket) do {:noreply, assign(socket, :show_exclusive_modal, true)} end + @impl true def handle_event("close_drawer", _params, socket) do {:noreply, close_drawers(socket)} end + @impl true def handle_event("validate_reward", %{"reward_bounty_form" => params}, socket) do {:noreply, socket @@ -146,6 +198,7 @@ defmodule AlgoraWeb.BountyLive do |> assign_line_items()} end + @impl true def handle_event("pay_with_stripe", %{"reward_bounty_form" => params}, socket) do changeset = RewardBountyForm.changeset(%RewardBountyForm{}, params) @@ -165,6 +218,7 @@ defmodule AlgoraWeb.BountyLive do end end + @impl true def handle_event("validate_exclusive", %{"exclusive_bounty_form" => params}, socket) do {:noreply, socket @@ -172,6 +226,7 @@ defmodule AlgoraWeb.BountyLive do |> assign_line_items()} end + @impl true def handle_event("share_exclusive", %{"exclusive_bounty_form" => params}, socket) do changeset = ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, params) bounty = socket.assigns.bounty @@ -420,11 +475,11 @@ defmodule AlgoraWeb.BountyLive do Contributor chat
- <%= for user <- @exclusives do %> + <%= for participant <- @participants do %> <.avatar> - <.avatar_image src={user.avatar_url} alt="Developer avatar" /> + <.avatar_image src={participant.user.avatar_url} alt="Developer avatar" /> <.avatar_fallback> - {Algora.Util.initials(@bounty.owner.name)} + {Algora.Util.initials(participant.user.name)} <% end %> diff --git a/lib/algora_web/live/chat/thread_live.ex b/lib/algora_web/live/chat/thread_live.ex index ad9175081..6fba11513 100644 --- a/lib/algora_web/live/chat/thread_live.ex +++ b/lib/algora_web/live/chat/thread_live.ex @@ -38,7 +38,7 @@ defmodule AlgoraWeb.Chat.ThreadLive do end @impl true - def handle_info(%Message{} = message, socket) do + def handle_info(%{message: message}, socket) do if message.id in Enum.map(socket.assigns.messages, & &1.id) do {:noreply, socket} else diff --git a/lib/algora_web/live/contract/view_live.ex b/lib/algora_web/live/contract/view_live.ex index 634c80dfa..6878d6ee9 100644 --- a/lib/algora_web/live/contract/view_live.ex +++ b/lib/algora_web/live/contract/view_live.ex @@ -510,7 +510,7 @@ defmodule AlgoraWeb.Contract.ViewLive do end end - def handle_info(%Chat.Message{} = message, socket) do + def handle_info(%Chat.MessageCreated{message: message}, socket) do if message.id in Enum.map(socket.assigns.messages, & &1.id) do {:noreply, socket} else diff --git a/priv/repo/migrations/20250401065753_add_bounty_id_to_threads.exs b/priv/repo/migrations/20250401065753_add_bounty_id_to_threads.exs new file mode 100644 index 000000000..290a1ba70 --- /dev/null +++ b/priv/repo/migrations/20250401065753_add_bounty_id_to_threads.exs @@ -0,0 +1,11 @@ +defmodule Algora.Repo.Migrations.AddBountyIdToThreads do + use Ecto.Migration + + def change do + alter table(:threads) do + add :bounty_id, :string, null: true + end + + create unique_index(:threads, [:bounty_id]) + end +end From 6e194b7ac72b68073f044c9dfeec9d22d41d8618 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 10:46:15 +0300 Subject: [PATCH 17/35] feat: update BountyLive layout and user display - Added an alias for the Admin module to streamline user management. - Enhanced the layout of the main display area, adjusting padding and height for better usability. - Updated the avatar fallback to show initials instead of the first character of the provider login. - Refactored the exclusives assignment logic to utilize the Admin token for user retrieval, improving security and consistency. --- lib/algora_web/live/bounty_live.ex | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index a0cfddb1e..bfde613b1 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -5,6 +5,7 @@ defmodule AlgoraWeb.BountyLive do import Ecto.Changeset alias Algora.Accounts + alias Algora.Admin alias Algora.Bounties alias Algora.Bounties.Bounty alias Algora.Bounties.LineItem @@ -322,8 +323,8 @@ defmodule AlgoraWeb.BountyLive do @impl true def render(assigns) do ~H""" -
- <.scroll_area class="h-[calc(100vh-64px)] flex-1 p-4 pr-6"> +
+ <.scroll_area class="h-[calc(100vh-96px)] flex-1 pr-6">
<.card> <.card_content> @@ -332,7 +333,7 @@ defmodule AlgoraWeb.BountyLive do <.avatar class="h-20 w-20 rounded-2xl"> <.avatar_image src={@ticket.repository.user.avatar_url} /> <.avatar_fallback> - {String.first(@ticket.repository.user.provider_login)} + {Util.initials(@ticket.repository.user.provider_login)}
@@ -468,7 +469,7 @@ defmodule AlgoraWeb.BountyLive do
-
+

@@ -763,10 +764,8 @@ defmodule AlgoraWeb.BountyLive do defp assign_exclusives(socket, shared_with) do exclusives = Enum.flat_map(shared_with, fn provider_id -> - with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user), - {:ok, user} <- Workspace.ensure_user_by_provider_id(token, provider_id) do - [user] - else + case Workspace.ensure_user_by_provider_id(Admin.token!(), provider_id) do + {:ok, user} -> [user] _ -> [] end end) From f5f68f58e50fe78beef3c052da66e65081228659 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 10:52:48 +0300 Subject: [PATCH 18/35] feat: enhance user avatar display in BountyLive - Updated avatar fallback to display initials using the `Util.initials` function for better user identification. - Modified participant display logic to show a maximum of three avatars, with a count of additional participants if applicable. - Improved avatar styling with additional ring effects for visual distinction. --- lib/algora_web/live/bounty_live.ex | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index bfde613b1..d2df8ad88 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -401,7 +401,7 @@ defmodule AlgoraWeb.BountyLive do
<.avatar> <.avatar_image src={user.avatar_url} /> - <.avatar_fallback>{String.first(user.name)} + <.avatar_fallback>{Util.initials(user.name)}

{user.name}

@@ -476,14 +476,25 @@ defmodule AlgoraWeb.BountyLive do Contributor chat

- <%= for participant <- @participants do %> - <.avatar> - <.avatar_image src={participant.user.avatar_url} alt="Developer avatar" /> + <%= for participant <- @participants |> Enum.take(3) do %> + <.avatar class="ring-4 ring-background"> + <.avatar_image + src={participant.user.avatar_url} + alt="Developer avatar" + class="ring-2 ring-background" + /> <.avatar_fallback> {Algora.Util.initials(participant.user.name)} <% end %> + <%= if length(@participants) > 3 do %> + <.avatar class="ring-4 ring-background"> + <.avatar_fallback> + +{length(@participants) - 3} + + + <% end %>
From 399f57419885e9f83adb0d72df3ad0ac7fb16ecf Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 10:59:44 +0300 Subject: [PATCH 19/35] feat: add avatar_group component for enhanced participant display - Introduced a new `avatar_group` function in the Avatar module to display multiple user avatars in a grouped format. - Updated BountyLive to utilize the `avatar_group` component, improving the visual representation of participants in chat. - Enhanced the logic to limit the number of displayed avatars and show a count of additional participants if applicable. --- lib/algora_web/components/core_components.ex | 1 + lib/algora_web/components/ui/avatar.ex | 29 +++++++++++++ lib/algora_web/live/bounty_live.ex | 43 ++++++-------------- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/lib/algora_web/components/core_components.ex b/lib/algora_web/components/core_components.ex index 4787b78d7..bcc87c201 100644 --- a/lib/algora_web/components/core_components.ex +++ b/lib/algora_web/components/core_components.ex @@ -1288,6 +1288,7 @@ defmodule AlgoraWeb.CoreComponents do defdelegate alert_title(assigns), to: Alert defdelegate alert(assigns), to: Alert defdelegate avatar_fallback(assigns), to: Avatar + defdelegate avatar_group(assigns), to: Avatar defdelegate avatar_image(assigns), to: Avatar defdelegate avatar(assigns), to: Avatar defdelegate badge(assigns), to: AlgoraWeb.Components.UI.Badge diff --git a/lib/algora_web/components/ui/avatar.ex b/lib/algora_web/components/ui/avatar.ex index 66472c7fa..26927a9ba 100644 --- a/lib/algora_web/components/ui/avatar.ex +++ b/lib/algora_web/components/ui/avatar.ex @@ -6,6 +6,8 @@ defmodule AlgoraWeb.Components.UI.Avatar do attr :class, :string, default: nil attr :rest, :global + slot :inner_block, required: true + def avatar(assigns) do ~H"""
@@ -48,4 +50,31 @@ defmodule AlgoraWeb.Components.UI.Avatar do """ end + + attr :class, :string, default: nil + attr :rest, :global + attr :srcs, :list, default: [] + attr :limit, :integer, default: 3 + + def avatar_group(assigns) do + ~H""" +
+ <%= for src <- @srcs |> Enum.take(@limit) do %> + <.avatar class={@class}> + <.avatar_image src={src} /> + <.avatar_fallback> + {Algora.Util.initials(src)} + + + <% end %> + <%= if length(@srcs) > @limit do %> + <.avatar class={@class}> + <.avatar_fallback> + +{length(@srcs) - @limit} + + + <% end %> +
+ """ + end end diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index d2df8ad88..c19dd64bf 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -143,18 +143,14 @@ defmodule AlgoraWeb.BountyLive do @impl true def handle_info(%Chat.MessageCreated{message: message, participant: participant}, socket) do socket = - if message.id in Enum.map(socket.assigns.messages, & &1.id) do - socket - else - Phoenix.Component.update(socket, :messages, &(&1 ++ [message])) - end + if message.id in Enum.map(socket.assigns.messages, & &1.id), + do: socket, + else: Phoenix.Component.update(socket, :messages, &(&1 ++ [message])) socket = - if participant.id in Enum.map(socket.assigns.participants, & &1.id) do - socket - else - Phoenix.Component.update(socket, :participants, &(&1 ++ [participant])) - end + if participant.id in Enum.map(socket.assigns.participants, & &1.id), + do: socket, + else: Phoenix.Component.update(socket, :participants, &(&1 ++ [participant])) {:noreply, socket} end @@ -475,27 +471,12 @@ defmodule AlgoraWeb.BountyLive do

Contributor chat

-
- <%= for participant <- @participants |> Enum.take(3) do %> - <.avatar class="ring-4 ring-background"> - <.avatar_image - src={participant.user.avatar_url} - alt="Developer avatar" - class="ring-2 ring-background" - /> - <.avatar_fallback> - {Algora.Util.initials(participant.user.name)} - - - <% end %> - <%= if length(@participants) > 3 do %> - <.avatar class="ring-4 ring-background"> - <.avatar_fallback> - +{length(@participants) - 3} - - - <% end %> -
+ + <.avatar_group + srcs={Enum.map(@participants, & &1.user.avatar_url)} + limit={4} + class="ring-4 ring-background" + />
From 7b2faeb76139238d177f4170227abeea896ce2a3 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 11:00:34 +0300 Subject: [PATCH 20/35] fix: update avatar_group component to increase limit and enhance styling - Increased the default limit of displayed avatars in the `avatar_group` component from 3 to 4. - Improved avatar styling by adding a ring effect for better visual distinction. - Updated usage in `BountyLive` to reflect the new default limit and styling adjustments. --- lib/algora_web/components/ui/avatar.ex | 6 +++--- lib/algora_web/live/bounty_live.ex | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/algora_web/components/ui/avatar.ex b/lib/algora_web/components/ui/avatar.ex index 26927a9ba..b2acb965f 100644 --- a/lib/algora_web/components/ui/avatar.ex +++ b/lib/algora_web/components/ui/avatar.ex @@ -54,13 +54,13 @@ defmodule AlgoraWeb.Components.UI.Avatar do attr :class, :string, default: nil attr :rest, :global attr :srcs, :list, default: [] - attr :limit, :integer, default: 3 + attr :limit, :integer, default: 4 def avatar_group(assigns) do ~H"""
<%= for src <- @srcs |> Enum.take(@limit) do %> - <.avatar class={@class}> + <.avatar class={classes(["ring-4 ring-background", @class])}> <.avatar_image src={src} /> <.avatar_fallback> {Algora.Util.initials(src)} @@ -68,7 +68,7 @@ defmodule AlgoraWeb.Components.UI.Avatar do <% end %> <%= if length(@srcs) > @limit do %> - <.avatar class={@class}> + <.avatar class={classes(["ring-4 ring-background", @class])}> <.avatar_fallback> +{length(@srcs) - @limit} diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index c19dd64bf..1311bb4c4 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -472,11 +472,7 @@ defmodule AlgoraWeb.BountyLive do Contributor chat - <.avatar_group - srcs={Enum.map(@participants, & &1.user.avatar_url)} - limit={4} - class="ring-4 ring-background" - /> + <.avatar_group srcs={Enum.map(@participants, & &1.user.avatar_url)} />
From 3dc92a8d630091bf1316c7dda1510e2a12455d0f Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 13:07:08 +0300 Subject: [PATCH 21/35] feat: enhance bounty payment process in BountyLive - Added a recipient field to the bounty payment structure to specify the recipient's GitHub handle. - Updated the RewardBountyForm to include the new github_handle field and adjusted validation requirements. - Refactored the assign_line_items function to incorporate recipient information and calculate final amounts accordingly. - Improved the user interface by replacing the dropdown for recipient selection with an input field for GitHub handle. - Streamlined the payment process by integrating recipient assignment into the event handling logic. --- lib/algora/bounties/bounties.ex | 3 +- lib/algora_web/live/bounty_live.ex | 219 +++++++++++++++-------------- 2 files changed, 117 insertions(+), 105 deletions(-) diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index ae20bda40..97db2328a 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -703,7 +703,8 @@ defmodule Algora.Bounties do %{owner: owner, amount: amount, description: "Bounty payment for OSS contributions"}, ticket_ref: opts[:ticket_ref], bounty_id: bounty_id, - claims: claims + claims: claims, + recipient: opts[:recipient] ) end diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 1311bb4c4..2a5f11caf 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -16,14 +16,7 @@ defmodule AlgoraWeb.BountyLive do require Logger - defp tip_options do - [ - {"None", 0}, - {"10%", 10}, - {"20%", 20}, - {"50%", 50} - ] - end + defp tip_options, do: [{"None", 0}, {"10%", 10}, {"20%", 20}, {"50%", 50}] defmodule RewardBountyForm do @moduledoc false @@ -34,13 +27,14 @@ defmodule AlgoraWeb.BountyLive do @primary_key false embedded_schema do field :amount, :decimal + field :github_handle, :string field :tip_percentage, :decimal end def changeset(form, attrs) do form - |> cast(attrs, [:amount, :tip_percentage]) - |> validate_required([:amount]) + |> cast(attrs, [:amount, :tip_percentage, :github_handle]) + |> validate_required([:amount, :github_handle]) |> validate_number(:tip_percentage, greater_than_or_equal_to: 0) |> validate_number(:amount, greater_than: 0) end @@ -113,16 +107,18 @@ defmodule AlgoraWeb.BountyLive do |> assign(:total_paid, total_paid) |> assign(:ticket_body_html, ticket_body_html) |> assign(:contexts, contexts) - |> assign(:show_reward_modal, false) + |> assign(:show_reward_modal, true) |> assign(:show_exclusive_modal, false) |> assign(:selected_context, nil) + |> assign(:recipient, nil) |> assign(:line_items, []) |> assign(:thread, thread) |> assign(:messages, messages) |> assign(:participants, participants) |> assign(:reward_form, to_form(reward_changeset)) |> assign(:exclusive_form, to_form(exclusive_changeset)) - |> assign_exclusives(bounty.shared_with)} + |> assign_exclusives(bounty.shared_with) + |> assign_line_items()} end @impl true @@ -130,10 +126,10 @@ defmodule AlgoraWeb.BountyLive do {:noreply, socket} end - @impl true - def handle_params(%{"context" => context_id}, _url, socket) do - {:noreply, socket |> assign_selected_context(context_id) |> assign_line_items()} - end + # @impl true + # def handle_params(%{"context" => context_id}, _url, socket) do + # {:noreply, socket |> assign_selected_context(context_id) |> assign_line_items(nil)} + # end @impl true def handle_params(_params, _url, socket) do @@ -195,13 +191,21 @@ defmodule AlgoraWeb.BountyLive do |> assign_line_items()} end + @impl true + def handle_event("assign_line_items", %{"reward_bounty_form" => params}, socket) do + {:noreply, + socket + |> assign_recipient(params["github_handle"]) + |> assign_line_items()} + end + @impl true def handle_event("pay_with_stripe", %{"reward_bounty_form" => params}, socket) do changeset = RewardBountyForm.changeset(%RewardBountyForm{}, params) case apply_action(changeset, :save) do - {:ok, data} -> - case create_payment_session(socket, data) do + {:ok, _data} -> + case reward_bounty(socket, socket.assigns.bounty, changeset) do {:ok, session_url} -> {:noreply, redirect(socket, external: session_url)} @@ -217,10 +221,7 @@ defmodule AlgoraWeb.BountyLive do @impl true def handle_event("validate_exclusive", %{"exclusive_bounty_form" => params}, socket) do - {:noreply, - socket - |> assign(:exclusive_form, to_form(ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, params))) - |> assign_line_items()} + {:noreply, assign(socket, :exclusive_form, to_form(ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, params)))} end @impl true @@ -260,62 +261,6 @@ defmodule AlgoraWeb.BountyLive do end end - defp assign_selected_context(socket, context_id) do - case Enum.find(socket.assigns.contexts, &(&1.id == context_id)) do - nil -> - socket - - context -> - assign(socket, :selected_context, context) - end - end - - defp assign_line_items(socket) do - # line_items = - # Bounties.generate_line_items( - # %{ - # owner: socket.assigns.selected_context, - # amount: calculate_final_amount(socket.assigns.reward_form.source) - # }, - # ticket_ref: ticket_ref(socket), - # recipient: socket.assigns.selected_context - # ) - - line_items = [] - assign(socket, :line_items, line_items) - end - - defp ticket_ref(socket) do - %{ - owner: socket.assigns.ticket.repository.user.provider_login, - repo: socket.assigns.ticket.repository.name, - number: socket.assigns.ticket.number - } - end - - defp create_payment_session(socket, data) do - final_amount = calculate_final_amount(data) - - Bounties.reward_bounty( - %{ - owner: socket.assigns.current_user, - amount: final_amount, - bounty_id: socket.assigns.bounty.id, - claims: [] - }, - ticket_ref: ticket_ref(socket), - recipient: socket.assigns.selected_context - ) - end - - defp calculate_final_amount(data_or_changeset) do - tip_percentage = get_field(data_or_changeset, :tip_percentage) || Decimal.new(0) - amount = get_field(data_or_changeset, :amount) || Decimal.new(0) - - multiplier = tip_percentage |> Decimal.div(100) |> Decimal.add(1) - amount |> Money.new!(:USD) |> Money.mult!(multiplier) - end - @impl true def render(assigns) do ~H""" @@ -335,12 +280,12 @@ defmodule AlgoraWeb.BountyLive do
<.link href={@ticket.url} - class="text-4xl font-semibold hover:underline" + class="text-3xl font-semibold hover:underline" target="_blank" > {@ticket.title} -
+
{@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number}
@@ -613,30 +558,12 @@ defmodule AlgoraWeb.BountyLive do icon="tabler-currency-dollar" field={@reward_form[:amount]} /> - -
- <.label>Recipient - <.dropdown id="context-dropdown" class="mt-2"> - <:img :if={@selected_context} src={@selected_context.avatar_url} /> - <:title :if={@selected_context}>{@selected_context.name} - <:subtitle :if={@selected_context}>@{@selected_context.handle} - - <:link :for={context <- @contexts} patch={"?context=#{context.id}"}> -
- {context.name} -
-
{context.name}
-
@{context.handle}
-
-
- - -
- + <.input + label="GitHub handle" + field={@reward_form[:github_handle]} + phx-change="assign_line_items" + phx-debounce="500" + />
<.label>Tip
@@ -662,6 +589,9 @@ defmodule AlgoraWeb.BountyLive do <%= if line_item.image do %> <.avatar> <.avatar_image src={line_item.image} /> + <.avatar_fallback> + {Util.initials(line_item.title)} + <% else %>
@@ -705,6 +635,87 @@ defmodule AlgoraWeb.BountyLive do """ end + # defp assign_selected_context(socket, context_id) do + # case Enum.find(socket.assigns.contexts, &(&1.id == context_id)) do + # nil -> + # socket + + # context -> + # assign(socket, :selected_context, context) + # end + # end + + defp assign_recipient(socket, github_handle) do + case Workspace.ensure_user(Admin.token!(), github_handle) do + {:ok, recipient} -> + assign(socket, :recipient, recipient) + + _ -> + assign(socket, :recipient, nil) + end + end + + defp assign_line_items(socket) do + amount = calculate_final_amount(socket.assigns.reward_form.source) + recipient = socket.assigns.recipient + ticket_ref = ticket_ref(socket) + + line_items = + if recipient do + [] + else + [ + %LineItem{ + amount: amount, + title: "Recipient", + image: ~p"/images/placeholder-avatar.png", + description: if(ticket_ref, do: "#{ticket_ref[:repo]}##{ticket_ref[:number]}") + } + ] + end ++ + Bounties.generate_line_items( + %{ + owner: socket.assigns.bounty.owner, + amount: amount + }, + ticket_ref: ticket_ref, + recipient: recipient + ) + + assign(socket, :line_items, line_items) + end + + defp ticket_ref(socket) do + %{ + owner: socket.assigns.ticket.repository.user.provider_login, + repo: socket.assigns.ticket.repository.name, + number: socket.assigns.ticket.number + } + end + + defp reward_bounty(socket, bounty, changeset) do + final_amount = calculate_final_amount(changeset) + + Bounties.reward_bounty( + %{ + owner: bounty.owner, + amount: final_amount, + bounty_id: bounty.id, + claims: [] + }, + ticket_ref: ticket_ref(socket), + recipient: socket.assigns.recipient + ) + end + + defp calculate_final_amount(data_or_changeset) do + tip_percentage = get_field(data_or_changeset, :tip_percentage) || Decimal.new(0) + amount = get_field(data_or_changeset, :amount) || Decimal.new(0) + + multiplier = tip_percentage |> Decimal.div(100) |> Decimal.add(1) + amount |> Money.new!(:USD) |> Money.mult!(multiplier) + end + defp social_share_button(assigns) do ~H""" <.button From a0bba530d4b8caa82cbff4ea4cb04e5368c17978 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 13:17:47 +0300 Subject: [PATCH 22/35] refactor: update amount field type and validation in BountyLive - Changed the type of the amount field from :decimal to Algora.Types.USD for better currency handling. - Replaced the validation for amount to use a custom validation function for positive money values. - Simplified the reward bounty structure by directly using the amount without conversion. - Adjusted the calculation of final amounts to work with the new Money type, ensuring accurate financial computations. --- lib/algora_web/live/bounty_live.ex | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 2a5f11caf..5810d9065 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -26,7 +26,7 @@ defmodule AlgoraWeb.BountyLive do @primary_key false embedded_schema do - field :amount, :decimal + field :amount, Algora.Types.USD field :github_handle, :string field :tip_percentage, :decimal end @@ -36,7 +36,7 @@ defmodule AlgoraWeb.BountyLive do |> cast(attrs, [:amount, :tip_percentage, :github_handle]) |> validate_required([:amount, :github_handle]) |> validate_number(:tip_percentage, greater_than_or_equal_to: 0) - |> validate_number(:amount, greater_than: 0) + |> Algora.Validations.validate_money_positive(:amount) end end @@ -86,7 +86,7 @@ defmodule AlgoraWeb.BountyLive do reward_changeset = RewardBountyForm.changeset(%RewardBountyForm{}, %{ tip_percentage: 0, - amount: Money.to_decimal(bounty.amount) + amount: bounty.amount }) exclusive_changeset = ExclusiveBountyForm.changeset(%ExclusiveBountyForm{}, %{}) @@ -697,12 +697,7 @@ defmodule AlgoraWeb.BountyLive do final_amount = calculate_final_amount(changeset) Bounties.reward_bounty( - %{ - owner: bounty.owner, - amount: final_amount, - bounty_id: bounty.id, - claims: [] - }, + %{owner: bounty.owner, amount: final_amount, bounty_id: bounty.id, claims: []}, ticket_ref: ticket_ref(socket), recipient: socket.assigns.recipient ) @@ -710,10 +705,10 @@ defmodule AlgoraWeb.BountyLive do defp calculate_final_amount(data_or_changeset) do tip_percentage = get_field(data_or_changeset, :tip_percentage) || Decimal.new(0) - amount = get_field(data_or_changeset, :amount) || Decimal.new(0) + amount = get_field(data_or_changeset, :amount) || Money.zero(:USD, no_fraction_if_integer: true) multiplier = tip_percentage |> Decimal.div(100) |> Decimal.add(1) - amount |> Money.new!(:USD) |> Money.mult!(multiplier) + Money.mult!(amount, multiplier) end defp social_share_button(assigns) do From 3f7ea04f70cdaea65d3717158424fd181cec2e18 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 13:18:37 +0300 Subject: [PATCH 23/35] fix: update show_reward_modal assignment in BountyLive - Changed the assignment of the show_reward_modal from true to false to adjust the modal display logic. - Refactored the class attribute of the check icon for improved styling consistency. --- lib/algora_web/live/bounty_live.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 5810d9065..cf38d1b09 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -107,7 +107,7 @@ defmodule AlgoraWeb.BountyLive do |> assign(:total_paid, total_paid) |> assign(:ticket_body_html, ticket_body_html) |> assign(:contexts, contexts) - |> assign(:show_reward_modal, true) + |> assign(:show_reward_modal, false) |> assign(:show_exclusive_modal, false) |> assign(:selected_context, nil) |> assign(:recipient, nil) @@ -739,7 +739,7 @@ defmodule AlgoraWeb.BountyLive do <.icon id={@id <> "-check-icon"} name="tabler-check" - class="absolute inset-0 m-auto hidden size-6 flex items-center justify-center" + class="absolute inset-0 m-auto hidden size-6 items-center justify-center" /> """ From 1d19562248c4e394d47917634187782c037b4c7f Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 13:53:01 +0300 Subject: [PATCH 24/35] feat: add repository navigation and enhance BountyLive functionality - Introduced a new route for repository-specific live sessions in the router. - Added a RepoNav component for improved navigation within repository contexts. - Enhanced BountyLive to support dynamic loading of bounties based on repository owner and name. - Updated ticket reference handling to streamline data assignment in BountyLive. - Refactored the mount function in BountyLive to incorporate repository parameters for better context management. --- lib/algora_web/live/bounty_live.ex | 110 +++++++++++++++++----------- lib/algora_web/live/org/repo_nav.ex | 107 +++++++++++++++++++++++++++ lib/algora_web/router.ex | 12 ++- 3 files changed, 183 insertions(+), 46 deletions(-) create mode 100644 lib/algora_web/live/org/repo_nav.ex diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index cf38d1b09..b140e4e8b 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -3,6 +3,7 @@ defmodule AlgoraWeb.BountyLive do use AlgoraWeb, :live_view import Ecto.Changeset + import Ecto.Query alias Algora.Accounts alias Algora.Admin @@ -65,13 +66,49 @@ defmodule AlgoraWeb.BountyLive do bounty = Bounty |> Repo.get!(bounty_id) - |> Repo.preload([ - :owner, - :creator, - :transactions, - ticket: [repository: [:user]] - ]) + |> Repo.preload([:owner, :creator, :transactions, ticket: [repository: [:user]]]) + ticket_ref = %{ + owner: bounty.ticket.repository.user.provider_login, + repo: bounty.ticket.repository.name, + number: bounty.ticket.number + } + + socket + |> assign(:bounty, bounty) + |> assign(:ticket_ref, ticket_ref) + |> on_mount(bounty) + end + + @impl true + def mount(%{"repo_owner" => repo_owner, "repo_name" => repo_name, "number" => number}, _session, socket) do + number = String.to_integer(number) + + ticket_ref = %{owner: repo_owner, repo: repo_name, number: number} + + bounty = + from(b in Bounty, + join: t in assoc(b, :ticket), + join: r in assoc(t, :repository), + join: u in assoc(r, :user), + where: u.provider == "github", + where: u.provider_login == ^repo_owner, + where: r.name == ^repo_name, + where: t.number == ^number, + # TODO: pool bounties + limit: 1, + order_by: [asc: b.inserted_at] + ) + |> Repo.one() + |> Repo.preload([:owner, :creator, :transactions, ticket: [repository: [:user]]]) + + socket + |> assign(:bounty, bounty) + |> assign(:ticket_ref, ticket_ref) + |> on_mount(bounty) + end + + defp on_mount(socket, bounty) do debits = Enum.filter(bounty.transactions, &(&1.type == :debit and &1.status == :succeeded)) total_paid = @@ -81,8 +118,6 @@ defmodule AlgoraWeb.BountyLive do ticket_body_html = Algora.Markdown.render(bounty.ticket.description) - contexts = contexts(bounty) - reward_changeset = RewardBountyForm.changeset(%RewardBountyForm{}, %{ tip_percentage: 0, @@ -102,11 +137,9 @@ defmodule AlgoraWeb.BountyLive do {:ok, socket |> assign(:page_title, bounty.ticket.title) - |> assign(:bounty, bounty) |> assign(:ticket, bounty.ticket) |> assign(:total_paid, total_paid) |> assign(:ticket_body_html, ticket_body_html) - |> assign(:contexts, contexts) |> assign(:show_reward_modal, false) |> assign(:show_exclusive_modal, false) |> assign(:selected_context, nil) @@ -126,11 +159,6 @@ defmodule AlgoraWeb.BountyLive do {:noreply, socket} end - # @impl true - # def handle_params(%{"context" => context_id}, _url, socket) do - # {:noreply, socket |> assign_selected_context(context_id) |> assign_line_items(nil)} - # end - @impl true def handle_params(_params, _url, socket) do {:noreply, socket} @@ -164,7 +192,7 @@ defmodule AlgoraWeb.BountyLive do {:noreply, socket - |> update(:messages, &(&1 ++ [message])) + |> Phoenix.Component.update(:messages, &(&1 ++ [message])) |> push_event("clear-input", %{selector: "#message-input"})} end @@ -367,22 +395,38 @@ defmodule AlgoraWeb.BountyLive do <.social_share_button id="twitter-share-url" icon="tabler-brand-x" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + value={ + url( + ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" + ) + } /> <.social_share_button id="reddit-share-url" icon="tabler-brand-reddit" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + value={ + url( + ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" + ) + } /> <.social_share_button id="linkedin-share-url" icon="tabler-brand-linkedin" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + value={ + url( + ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" + ) + } /> <.social_share_button id="hackernews-share-url" icon="tabler-brand-ycombinator" - value={url(~p"/org/#{@bounty.owner.handle}/bounties/#{@bounty.id}")} + value={ + url( + ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" + ) + } />
@@ -635,16 +679,6 @@ defmodule AlgoraWeb.BountyLive do """ end - # defp assign_selected_context(socket, context_id) do - # case Enum.find(socket.assigns.contexts, &(&1.id == context_id)) do - # nil -> - # socket - - # context -> - # assign(socket, :selected_context, context) - # end - # end - defp assign_recipient(socket, github_handle) do case Workspace.ensure_user(Admin.token!(), github_handle) do {:ok, recipient} -> @@ -658,7 +692,7 @@ defmodule AlgoraWeb.BountyLive do defp assign_line_items(socket) do amount = calculate_final_amount(socket.assigns.reward_form.source) recipient = socket.assigns.recipient - ticket_ref = ticket_ref(socket) + ticket_ref = socket.assigns.ticket_ref line_items = if recipient do @@ -685,20 +719,12 @@ defmodule AlgoraWeb.BountyLive do assign(socket, :line_items, line_items) end - defp ticket_ref(socket) do - %{ - owner: socket.assigns.ticket.repository.user.provider_login, - repo: socket.assigns.ticket.repository.name, - number: socket.assigns.ticket.number - } - end - defp reward_bounty(socket, bounty, changeset) do final_amount = calculate_final_amount(changeset) Bounties.reward_bounty( %{owner: bounty.owner, amount: final_amount, bounty_id: bounty.id, claims: []}, - ticket_ref: ticket_ref(socket), + ticket_ref: socket.assigns.ticket_ref, recipient: socket.assigns.recipient ) end @@ -745,10 +771,6 @@ defmodule AlgoraWeb.BountyLive do """ end - defp contexts(_bounty) do - Accounts.list_featured_developers() - end - defp close_drawers(socket) do socket |> assign(:show_reward_modal, false) diff --git a/lib/algora_web/live/org/repo_nav.ex b/lib/algora_web/live/org/repo_nav.ex new file mode 100644 index 000000000..f42fcf2e1 --- /dev/null +++ b/lib/algora_web/live/org/repo_nav.ex @@ -0,0 +1,107 @@ +defmodule AlgoraWeb.Org.RepoNav do + @moduledoc false + use Phoenix.Component + + import Phoenix.LiveView + + alias Algora.Organizations + alias AlgoraWeb.OrgAuth + + def on_mount(:default, %{"repo_owner" => repo_owner} = params, _session, socket) do + current_user = socket.assigns[:current_user] + current_org = Organizations.get_org_by(provider_login: repo_owner, provider: "github") + current_user_role = OrgAuth.get_user_role(current_user, current_org) + + {:cont, + socket + |> assign(:screenshot?, not is_nil(params["screenshot"])) + |> assign(:new_bounty_form, to_form(%{"github_issue_url" => "", "amount" => ""})) + |> assign(:current_org, current_org) + |> assign(:current_user_role, current_user_role) + |> assign(:nav, nav_items(current_org.handle, current_user_role)) + |> assign(:contacts, []) + |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)} + end + + defp handle_active_tab_params(_params, _url, socket) do + active_tab = + case {socket.view, socket.assigns.live_action} do + {AlgoraWeb.Org.DashboardLive, _} -> :dashboard + {AlgoraWeb.Org.HomeLive, _} -> :home + {AlgoraWeb.Org.BountiesLive, _} -> :bounties + {AlgoraWeb.Org.ProjectsLive, _} -> :projects + {AlgoraWeb.Project.ViewLive, _} -> :projects + {AlgoraWeb.Org.SettingsLive, _} -> :settings + {AlgoraWeb.Org.MembersLive, _} -> :members + {_, _} -> nil + end + + {:cont, assign(socket, :active_tab, active_tab)} + end + + def nav_items(org_handle, current_user_role) do + [ + %{ + title: "Overview", + items: build_nav_items(org_handle, current_user_role) + } + ] + end + + defp build_nav_items(org_handle, current_user_role) do + Enum.filter( + [ + %{ + href: "/org/#{org_handle}", + tab: :dashboard, + icon: "tabler-sparkles", + label: "Dashboard", + roles: [:admin, :mod] + }, + %{ + href: "/org/#{org_handle}/home", + tab: :home, + icon: "tabler-home", + label: "Home", + roles: [:admin, :mod, :expert, :none] + }, + %{ + href: "/org/#{org_handle}/bounties", + tab: :bounties, + icon: "tabler-diamond", + label: "Bounties", + roles: [:admin, :mod, :expert, :none] + }, + %{ + href: "/org/#{org_handle}/leaderboard", + tab: :leaderboard, + icon: "tabler-trophy", + label: "Leaderboard", + roles: [:admin, :mod, :expert, :none] + }, + %{ + href: "/org/#{org_handle}/team", + tab: :team, + icon: "tabler-users", + label: "Team", + roles: [:admin, :mod, :expert, :none] + }, + %{ + href: "/org/#{org_handle}/transactions", + tab: :transactions, + icon: "tabler-credit-card", + label: "Transactions", + roles: [:admin] + }, + %{ + href: "/org/#{org_handle}/settings", + tab: :settings, + icon: "tabler-settings", + label: "Settings", + roles: [:admin] + } + ], + fn item -> current_user_role in item[:roles] end + ) + end +end diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex index 028eadad3..8bff44acb 100644 --- a/lib/algora_web/router.ex +++ b/lib/algora_web/router.ex @@ -88,6 +88,15 @@ defmodule AlgoraWeb.Router do end end + scope "/:repo_owner/:repo_name" do + live_session :repo, + layout: {AlgoraWeb.Layouts, :user}, + on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Org.RepoNav] do + live "/issues/:number", BountyLive + live "/pull/:number", BountyLive + end + end + scope "/org/:org_handle" do live_session :org, layout: {AlgoraWeb.Layouts, :user}, @@ -95,11 +104,10 @@ defmodule AlgoraWeb.Router do live "/", Org.DashboardLive, :index live "/home", Org.HomeLive, :index live "/bounties", Org.BountiesLive, :index + live "/bounties/:id", BountyLive, :index live "/contracts/:id", Contract.ViewLive live "/team", Org.TeamLive, :index live "/leaderboard", Org.LeaderboardLive, :index - # TODO: allow access to invited users - live "/bounties/:id", BountyLive end live_session :org_admin, From d5ad53fcfb84fbb068522829f5f495a079cf3785 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 13:57:52 +0300 Subject: [PATCH 25/35] feat: configure max_age for OGImageController in development and production - Added a max_age configuration for AlgoraWeb.OGImageController in both dev.exs and prod.exs. - Updated the OGImageController to retrieve max_age from the configuration instead of using a hardcoded value. --- config/dev.exs | 2 ++ config/prod.exs | 2 ++ lib/algora_web/controllers/og_image_controller.ex | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config/dev.exs b/config/dev.exs index 978977bfb..02c76cb37 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -142,3 +142,5 @@ config :algora, plausible_url: System.get_env("PLAUSIBLE_URL"), assets_url: System.get_env("ASSETS_URL"), ingest_url: System.get_env("INGEST_URL") + +config :algora, AlgoraWeb.OGImageController, max_age: 0 diff --git a/config/prod.exs b/config/prod.exs index e8386963b..0b8db861c 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -32,5 +32,7 @@ config :algora, assets_url: System.get_env("ASSETS_URL"), ingest_url: System.get_env("INGEST_URL") +config :algora, AlgoraWeb.OGImageController, max_age: 600 + # Runtime production configuration, including reading # of environment variables, is done on config/runtime.exs. diff --git a/lib/algora_web/controllers/og_image_controller.ex b/lib/algora_web/controllers/og_image_controller.ex index da99c408e..0d56aa7b9 100644 --- a/lib/algora_web/controllers/og_image_controller.ex +++ b/lib/algora_web/controllers/og_image_controller.ex @@ -7,7 +7,7 @@ defmodule AlgoraWeb.OGImageController do @opts [type: "png", width: 1200, height: 630, scale_factor: 1] - @max_age 600 + defp max_age, do: Algora.config([AlgoraWeb.OGImageController, :max_age]) def generate(conn, %{"path" => path}) do object_path = Path.join(["og"] ++ path ++ ["og.png"]) From 2ddc4f915347f353a9d24cc9c192463cec6d6112 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:00:32 +0300 Subject: [PATCH 26/35] fix: deadline input --- lib/algora_web/live/bounty_live.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index b140e4e8b..cd37a1627 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -266,7 +266,7 @@ defmodule AlgoraWeb.BountyLive do bounty |> Bounty.settings_changeset(%{ shared_with: shared_with, - deadline: DateTime.new!(data.deadline, ~T[00:00:00], "Etc/UTC") + deadline: if(data.deadline, do: DateTime.new!(data.deadline, ~T[00:00:00], "Etc/UTC")) }) |> Repo.update() do {:noreply, From 78c7b7ce6e08d4beb41f1efbd58f703a21d40b01 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:00:58 +0300 Subject: [PATCH 27/35] swap cols --- lib/algora_web/live/bounty_live.ex | 108 ++++++++++++++--------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index cd37a1627..cabed3ea4 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -330,60 +330,6 @@ defmodule AlgoraWeb.BountyLive do
- <.card class="col"> - <.card_content> -
-
- <.card_title> - Exclusives - -
- - <%= if @bounty.deadline do %> - Expires on {Calendar.strftime(@bounty.deadline, "%b %d, %Y")} - <.button - variant="ghost" - size="icon-sm" - phx-click="exclusive" - class="group h-6 w-6" - > - <.icon - name="tabler-pencil" - class="h-4 w-4 text-muted-foreground group-hover:text-foreground" - /> - - <% else %> - - Add a deadline - - <% end %> - -
- <.button variant="secondary" phx-click="exclusive" class="mt-3"> - <.icon name="tabler-user-plus" class="size-5 mr-2 -ml-1" /> Add - -
-
- <%= for user <- @exclusives do %> -
- -
- <.avatar> - <.avatar_image src={user.avatar_url} /> - <.avatar_fallback>{Util.initials(user.name)} - -
-

{user.name}

-

@{user.provider_login}

-
-
-
-
- <% end %> -
-
- - <.card> <.card_content>
@@ -438,6 +384,60 @@ defmodule AlgoraWeb.BountyLive do
+ <.card class="col"> + <.card_content> +
+
+ <.card_title> + Exclusives + +
+ + <%= if @bounty.deadline do %> + Expires on {Calendar.strftime(@bounty.deadline, "%b %d, %Y")} + <.button + variant="ghost" + size="icon-sm" + phx-click="exclusive" + class="group h-6 w-6" + > + <.icon + name="tabler-pencil" + class="h-4 w-4 text-muted-foreground group-hover:text-foreground" + /> + + <% else %> + + Add a deadline + + <% end %> + +
+ <.button variant="secondary" phx-click="exclusive" class="mt-3"> + <.icon name="tabler-user-plus" class="size-5 mr-2 -ml-1" /> Add + +
+
+ <%= for user <- @exclusives do %> +
+ +
+ <.avatar> + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback>{Util.initials(user.name)} + +
+

{user.name}

+

@{user.provider_login}

+
+
+
+
+ <% end %> +
+
+ +
<.card> <.card_header> From 05c9e427ea9aadc304fedcc452e373e423904041 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:06:10 +0300 Subject: [PATCH 28/35] fix max_age --- lib/algora_web/controllers/og_image_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/algora_web/controllers/og_image_controller.ex b/lib/algora_web/controllers/og_image_controller.ex index 0d56aa7b9..0aae535da 100644 --- a/lib/algora_web/controllers/og_image_controller.ex +++ b/lib/algora_web/controllers/og_image_controller.ex @@ -49,7 +49,7 @@ defmodule AlgoraWeb.OGImageController do {_, last_modified} -> case DateTime.from_iso8601(convert_to_iso8601(last_modified)) do {:ok, modified_at, _} -> - DateTime.diff(DateTime.utc_now(), modified_at, :second) > @max_age + DateTime.diff(DateTime.utc_now(), modified_at, :second) > max_age() _error -> true @@ -86,7 +86,7 @@ defmodule AlgoraWeb.OGImageController do Task.start(fn -> Algora.S3.upload(body, object_path, content_type: "image/png", - cache_control: "public, max-age=#{@max_age}" + cache_control: "public, max-age=#{max_age()}" ) File.rm(filepath) From c3b874e7b69f7d1b5d6dc011c99c1bd1190cfe91 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:06:22 +0300 Subject: [PATCH 29/35] minor fixes --- lib/algora/bounties/bounties.ex | 6 +++++- lib/algora_web/live/og/bounty_live.ex | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index 97db2328a..d64cc6423 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -27,7 +27,8 @@ defmodule Algora.Bounties do def base_query, do: Bounty @type criterion :: - {:limit, non_neg_integer() | :infinity} + {:id, String.t()} + | {:limit, non_neg_integer() | :infinity} | {:ticket_id, String.t()} | {:owner_id, String.t()} | {:status, :open | :paid} @@ -998,6 +999,9 @@ defmodule Algora.Bounties do @spec apply_criteria(Ecto.Queryable.t(), [criterion()]) :: Ecto.Queryable.t() defp apply_criteria(query, criteria) do Enum.reduce(criteria, query, fn + {:id, id}, query -> + from([b] in query, where: b.id == ^id) + {:limit, :infinity}, query -> query diff --git a/lib/algora_web/live/og/bounty_live.ex b/lib/algora_web/live/og/bounty_live.ex index 28121731e..4e035a399 100644 --- a/lib/algora_web/live/og/bounty_live.ex +++ b/lib/algora_web/live/og/bounty_live.ex @@ -31,14 +31,14 @@ defmodule AlgoraWeb.OG.BountyLive do
Algora

- {@bounty.owner.name} + {@bounty.repository.owner.name}

From b515dad734916ba7644d3e23c1297a07525ea9ee Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:09:32 +0300 Subject: [PATCH 30/35] improvements --- lib/algora_web/live/bounty_live.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index cabed3ea4..3a800b776 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -330,7 +330,7 @@ defmodule AlgoraWeb.BountyLive do
- <.card> + <.card class="flex flex-col items-between justify-center"> <.card_content>
@@ -384,7 +384,7 @@ defmodule AlgoraWeb.BountyLive do
- <.card class="col"> + <.card class="flex flex-col items-between justify-center"> <.card_content>
@@ -407,7 +407,11 @@ defmodule AlgoraWeb.BountyLive do /> <% else %> - + Add a deadline <% end %> From d2bb4e16bdcb60f437df809c8011dfaa58b54378 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:21:44 +0300 Subject: [PATCH 31/35] refactor: enhance BountyLive mount and render logic - Updated the mount function to retrieve and preload bounty data more efficiently. - Simplified ticket reference assignment to improve clarity and maintainability. - Adjusted render logic to correctly display ticket information from the bounty structure. --- lib/algora_web/live/og/bounty_live.ex | 45 +++++++++++---------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/lib/algora_web/live/og/bounty_live.ex b/lib/algora_web/live/og/bounty_live.ex index 4e035a399..e854f3bfd 100644 --- a/lib/algora_web/live/og/bounty_live.ex +++ b/lib/algora_web/live/og/bounty_live.ex @@ -2,21 +2,25 @@ defmodule AlgoraWeb.OG.BountyLive do @moduledoc false use AlgoraWeb, :live_view - alias Algora.Bounties + alias Algora.Bounties.Bounty + alias Algora.Repo def mount(%{"id" => id}, _session, socket) do - case Bounties.list_bounties(id: id) do - [bounty | _] -> - socket = - socket - |> assign(:bounty, bounty) - |> assign(:ticket, bounty.ticket) + bounty = + Bounty + |> Repo.get!(id) + |> Repo.preload([:owner, :creator, :transactions, ticket: [repository: [:user]]]) - {:ok, socket} + ticket_ref = %{ + owner: bounty.ticket.repository.user.provider_login, + repo: bounty.ticket.repository.name, + number: bounty.ticket.number + } - [] -> - {:ok, socket |> put_flash(:error, "Bounty not found") |> redirect(to: "/")} - end + {:ok, + socket + |> assign(:bounty, bounty) + |> assign(:ticket_ref, ticket_ref)} end def render(assigns) do @@ -26,19 +30,19 @@ defmodule AlgoraWeb.OG.BountyLive do algora.io
- {@bounty.repository.name}#{@ticket.number} + {@bounty.ticket.repository.name}#{@bounty.ticket.number}
Algora

- {@bounty.repository.owner.name} + {@bounty.ticket.repository.user.provider_login}

@@ -50,19 +54,8 @@ defmodule AlgoraWeb.OG.BountyLive do

- {@ticket.title} + {@bounty.ticket.title}

- <%!--
-
-
- algora.io -
-
-
- Earn bounties for open source contributions -
-
-
--%>
""" end From 3bfddcae636961511dceeaa85087ac1a0fe8a004 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:21:55 +0300 Subject: [PATCH 32/35] refactor: improve layout and styling in BountyLive component - Updated the layout classes for responsive design, enhancing the user interface on different screen sizes. - Adjusted avatar sizes and text styles for better visual consistency. - Modified grid structure to optimize space utilization and improve overall presentation. --- lib/algora_web/live/bounty_live.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 3a800b776..69c432617 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -292,14 +292,14 @@ defmodule AlgoraWeb.BountyLive do @impl true def render(assigns) do ~H""" -
- <.scroll_area class="h-[calc(100vh-96px)] flex-1 pr-6"> +
+ <.scroll_area class="sm:h-[calc(100svh-96px)] flex-1 pr-6">
<.card> <.card_content> -
-
- <.avatar class="h-20 w-20 rounded-2xl"> +
+
+ <.avatar class="h-12 w-12 sm:h-20 sm:w-20 rounded-2xl"> <.avatar_image src={@ticket.repository.user.avatar_url} /> <.avatar_fallback> {Util.initials(@ticket.repository.user.provider_login)} @@ -308,12 +308,12 @@ defmodule AlgoraWeb.BountyLive do
<.link href={@ticket.url} - class="text-3xl font-semibold hover:underline" + class="text-xl sm:text-3xl font-semibold hover:underline" target="_blank" > {@ticket.title} -
+
{@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number}
@@ -329,7 +329,7 @@ defmodule AlgoraWeb.BountyLive do
-
+
<.card class="flex flex-col items-between justify-center"> <.card_content>
@@ -458,7 +458,7 @@ defmodule AlgoraWeb.BountyLive do
-
+

From 6c6a1dcdb394b2fe88c6c1eb8e38adca0643e81d Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:23:32 +0300 Subject: [PATCH 33/35] refactor --- lib/algora_web/live/bounty_live.ex | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 69c432617..d3b6193c6 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -134,8 +134,14 @@ defmodule AlgoraWeb.BountyLive do Chat.subscribe(thread.id) end + share_url = + url( + ~p"/#{socket.assigns.ticket_ref.owner}/#{socket.assigns.ticket_ref.repo}/issues/#{socket.assigns.ticket_ref.number}" + ) + {:ok, socket + |> assign(:share_url, share_url) |> assign(:page_title, bounty.ticket.title) |> assign(:ticket, bounty.ticket) |> assign(:total_paid, total_paid) @@ -341,38 +347,22 @@ defmodule AlgoraWeb.BountyLive do <.social_share_button id="twitter-share-url" icon="tabler-brand-x" - value={ - url( - ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" - ) - } + value={@share_url} /> <.social_share_button id="reddit-share-url" icon="tabler-brand-reddit" - value={ - url( - ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" - ) - } + value={@share_url} /> <.social_share_button id="linkedin-share-url" icon="tabler-brand-linkedin" - value={ - url( - ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" - ) - } + value={@share_url} /> <.social_share_button id="hackernews-share-url" icon="tabler-brand-ycombinator" - value={ - url( - ~p"/#{@ticket_ref.owner}/#{@ticket_ref.repo}/issues/#{@ticket_ref.number}" - ) - } + value={@share_url} />

From 7d51995c1294b6c9f223ff104fbbb51f27b1ad5f Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:36:25 +0300 Subject: [PATCH 34/35] refactor: update styling and layout in BountyLive component - Enhanced avatar styling for improved visual consistency. - Adjusted layout classes for better spacing and alignment. - Updated image container to maintain aspect ratio and improve responsiveness. - Refined text elements for better readability and truncation handling. --- lib/algora_web/live/bounty_live.ex | 38 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index d3b6193c6..a90e4252b 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -305,7 +305,7 @@ defmodule AlgoraWeb.BountyLive do <.card_content>
- <.avatar class="h-12 w-12 sm:h-20 sm:w-20 rounded-2xl"> + <.avatar class="h-12 w-12 sm:h-20 sm:w-20 rounded-lg sm:rounded-2xl"> <.avatar_image src={@ticket.repository.user.avatar_url} /> <.avatar_fallback> {Util.initials(@ticket.repository.user.provider_login)} @@ -338,8 +338,8 @@ defmodule AlgoraWeb.BountyLive do
<.card class="flex flex-col items-between justify-center"> <.card_content> -
-
+
+
<.card_title> Share on socials @@ -366,23 +366,25 @@ defmodule AlgoraWeb.BountyLive do />
- {@bounty.ticket.title} +
+ {@bounty.ticket.title} +
<.card class="flex flex-col items-between justify-center"> <.card_content> -
+
<.card_title> Exclusives
- + <%= if @bounty.deadline do %> Expires on {Calendar.strftime(@bounty.deadline, "%b %d, %Y")} <.button @@ -420,9 +422,13 @@ defmodule AlgoraWeb.BountyLive do <.avatar_image src={user.avatar_url} /> <.avatar_fallback>{Util.initials(user.name)} -
-

{user.name}

-

@{user.provider_login}

+
+

+ {user.name} +

+

+ @{user.provider_login} +

@@ -749,17 +755,17 @@ defmodule AlgoraWeb.BountyLive do transition: {"transition-opacity", "opacity-0", "opacity-100"} ) } - class="size-9 relative cursor-pointer text-foreground/90 hover:text-foreground bg-muted" + class="size-6 sm:size-9 relative cursor-pointer text-foreground/90 hover:text-foreground bg-muted" > <.icon id={@id <> "-copy-icon"} name={@icon} - class="absolute inset-0 m-auto size-6 flex items-center justify-center" + class="absolute inset-0 m-auto size-6 sm:size-6 flex items-center justify-center" /> <.icon id={@id <> "-check-icon"} name="tabler-check" - class="absolute inset-0 m-auto hidden size-6 items-center justify-center" + class="absolute inset-0 m-auto hidden size-6 sm:size-6 items-center justify-center" /> """ From d1aec5c7a420644a280420bdaa50b7b3aea164cd Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 14:39:26 +0300 Subject: [PATCH 35/35] refactor: enhance link styling and accessibility in BountyLive component - Updated link elements to include rel="noopener" for improved security. - Adjusted classes for better visual consistency and hover effects. - Ensured ticket information is wrapped in a link for better user interaction. --- lib/algora_web/live/bounty_live.ex | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index a90e4252b..92e08af83 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -314,14 +314,20 @@ defmodule AlgoraWeb.BountyLive do
<.link href={@ticket.url} - class="text-xl sm:text-3xl font-semibold hover:underline" target="_blank" + rel="noopener" + class="block text-xl sm:text-3xl font-semibold text-foreground/90 hover:underline" > {@ticket.title} -
+ <.link + href={@ticket.url} + target="_blank" + rel="noopener" + class="block text-base font-display sm:text-xl font-medium text-muted-foreground hover:underline" + > {@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number} -
+