diff --git a/lib/algora_web/live/org/bounties_live.ex b/lib/algora_web/live/org/bounties_live.ex
index 384e329ff..b8b486769 100644
--- a/lib/algora_web/live/org/bounties_live.ex
+++ b/lib/algora_web/live/org/bounties_live.ex
@@ -287,6 +287,10 @@ defmodule AlgoraWeb.Org.BountiesLive do
end}
end
+ def handle_event(_event, _params, socket) do
+ {:noreply, socket}
+ end
+
def handle_params(params, _uri, socket) do
current_org = socket.assigns.current_org
current_status = get_current_status(params)
diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex
index 01c45b321..413c1b6ce 100644
--- a/lib/algora_web/live/org/dashboard_live.ex
+++ b/lib/algora_web/live/org/dashboard_live.ex
@@ -16,6 +16,7 @@ defmodule AlgoraWeb.Org.DashboardLive do
alias Algora.Contracts
alias Algora.Github
alias Algora.Organizations
+ alias Algora.Organizations.Member
alias Algora.Payments
alias Algora.Payments.Transaction
alias Algora.Repo
@@ -50,7 +51,7 @@ defmodule AlgoraWeb.Org.DashboardLive do
def mount(_params, _session, socket) do
%{current_org: current_org} = socket.assigns
- if socket.assigns.current_user_role in [:admin, :mod] do
+ if Member.can_create_bounty?(socket.assigns.current_user_role) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Algora.PubSub, "auth:#{socket.id}")
end
@@ -633,7 +634,7 @@ defmodule AlgoraWeb.Org.DashboardLive do
{:noreply, assign(socket, :bounty_form, to_form(changeset))}
{:error, :already_exists} ->
- {:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")}
+ {:noreply, put_flash(socket, :warning, "You already have a bounty for this ticket")}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Something went wrong")}
@@ -1427,6 +1428,7 @@ defmodule AlgoraWeb.Org.DashboardLive do
diff --git a/lib/algora_web/live/org/home_live.ex b/lib/algora_web/live/org/home_live.ex
index d8dea6c98..982502904 100644
--- a/lib/algora_web/live/org/home_live.ex
+++ b/lib/algora_web/live/org/home_live.ex
@@ -231,6 +231,11 @@ defmodule AlgoraWeb.Org.HomeLive do
"""
end
+ @impl true
+ def handle_event(_event, _params, socket) do
+ {:noreply, socket}
+ end
+
defp social_links do
[
{:website, "tabler-world"},
diff --git a/lib/algora_web/live/org/leaderboard_live.ex b/lib/algora_web/live/org/leaderboard_live.ex
index cdadbae33..01e73c46f 100644
--- a/lib/algora_web/live/org/leaderboard_live.ex
+++ b/lib/algora_web/live/org/leaderboard_live.ex
@@ -6,6 +6,7 @@ defmodule AlgoraWeb.Org.LeaderboardLive do
alias Algora.Accounts.User
alias Algora.Organizations
+ @impl true
def mount(%{"org_handle" => handle}, _session, socket) do
org = Organizations.get_org_by_handle!(handle)
top_earners = Accounts.list_developers(org_id: org.id, earnings_gt: Money.zero(:USD))
@@ -17,6 +18,7 @@ defmodule AlgoraWeb.Org.LeaderboardLive do
|> assign(:top_earners, top_earners)}
end
+ @impl true
def render(assigns) do
~H"""
@@ -83,4 +85,9 @@ defmodule AlgoraWeb.Org.LeaderboardLive do
"""
end
+
+ @impl true
+ def handle_event(_event, _params, socket) do
+ {:noreply, socket}
+ end
end
diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex
index ddd3c56d7..5251da856 100644
--- a/lib/algora_web/live/org/nav.ex
+++ b/lib/algora_web/live/org/nav.ex
@@ -1,12 +1,19 @@
defmodule AlgoraWeb.Org.Nav do
@moduledoc false
use Phoenix.Component
+ use AlgoraWeb, :verified_routes
+ import Ecto.Changeset
import Phoenix.LiveView
+ alias Algora.Bounties
alias Algora.Organizations
+ alias Algora.Organizations.Member
+ alias AlgoraWeb.Forms.BountyForm
alias AlgoraWeb.OrgAuth
+ require Logger
+
def on_mount(:default, %{"org_handle" => org_handle} = params, _session, socket) do
current_user = socket.assigns[:current_user]
current_org = Organizations.get_org_by(handle: org_handle)
@@ -31,15 +38,97 @@ defmodule AlgoraWeb.Org.Nav do
# }
# end)
+ main_bounty_form =
+ if Member.can_create_bounty?(current_user_role) do
+ to_form(BountyForm.changeset(%BountyForm{}, %{}))
+ end
+
{:cont,
socket
|> assign(:screenshot?, not is_nil(params["screenshot"]))
- |> assign(:new_bounty_form, to_form(%{"github_issue_url" => "", "amount" => ""}))
+ |> assign(:main_bounty_form, main_bounty_form)
+ |> assign(:main_bounty_form_open?, false)
|> assign(:current_org, current_org)
|> assign(:current_user_role, current_user_role)
|> assign(:nav, nav_items(current_org.handle, current_user_role))
|> assign(:contacts, contacts)
- |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)}
+ |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)
+ |> attach_hook(:handle_event, :handle_event, &handle_event/3)}
+ end
+
+ defp handle_event("create_bounty_main", %{"bounty_form" => params}, socket) do
+ changeset = BountyForm.changeset(%BountyForm{}, params)
+
+ case apply_action(changeset, :save) do
+ {:ok, data} ->
+ bounty_res =
+ case data.type do
+ :github ->
+ Bounties.create_bounty(
+ %{
+ creator: socket.assigns.current_user,
+ owner: socket.assigns.current_org,
+ amount: data.amount,
+ ticket_ref: %{
+ owner: data.ticket_ref.owner,
+ repo: data.ticket_ref.repo,
+ number: data.ticket_ref.number
+ }
+ },
+ visibility: get_field(changeset, :visibility),
+ shared_with: get_field(changeset, :shared_with)
+ )
+
+ :custom ->
+ Bounties.create_bounty(
+ %{
+ creator: socket.assigns.current_user,
+ owner: socket.assigns.current_org,
+ amount: data.amount,
+ title: data.title,
+ description: data.description
+ },
+ visibility: get_field(changeset, :visibility),
+ shared_with: get_field(changeset, :shared_with)
+ )
+ end
+
+ case bounty_res do
+ {:ok, bounty} ->
+ to =
+ case data.type do
+ :github ->
+ ~p"/#{data.ticket_ref.owner}/#{data.ticket_ref.repo}/issues/#{data.ticket_ref.number}"
+
+ :custom ->
+ ~p"/org/#{socket.assigns.current_org.handle}/bounties/#{bounty.id}"
+ end
+
+ {:cont, redirect(socket, to: to)}
+
+ {:error, :already_exists} ->
+ {:cont, put_flash(socket, :warning, "You already have a bounty for this ticket")}
+
+ {:error, reason} ->
+ Logger.error("Failed to create bounty: #{inspect(reason)}")
+ {:cont, put_flash(socket, :error, "Something went wrong")}
+ end
+
+ {:error, changeset} ->
+ {:cont, assign(socket, :main_bounty_form, to_form(changeset))}
+ end
+ end
+
+ defp handle_event("open_main_bounty_form", _params, socket) do
+ {:cont, assign(socket, :main_bounty_form_open?, true)}
+ end
+
+ defp handle_event("close_main_bounty_form", _params, socket) do
+ {:cont, assign(socket, :main_bounty_form_open?, false)}
+ end
+
+ defp handle_event(_event, _params, socket) do
+ {:cont, socket}
end
defp handle_active_tab_params(_params, _url, socket) do
diff --git a/lib/algora_web/live/org/repo_nav.ex b/lib/algora_web/live/org/repo_nav.ex
index f42fcf2e1..92dcaedaf 100644
--- a/lib/algora_web/live/org/repo_nav.ex
+++ b/lib/algora_web/live/org/repo_nav.ex
@@ -1,26 +1,115 @@
defmodule AlgoraWeb.Org.RepoNav do
@moduledoc false
use Phoenix.Component
+ use AlgoraWeb, :verified_routes
+ import Ecto.Changeset
import Phoenix.LiveView
+ alias Algora.Bounties
alias Algora.Organizations
+ alias Algora.Organizations.Member
+ alias AlgoraWeb.Forms.BountyForm
alias AlgoraWeb.OrgAuth
+ require Logger
+
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)
+ main_bounty_form =
+ if Member.can_create_bounty?(current_user_role) do
+ to_form(BountyForm.changeset(%BountyForm{}, %{}))
+ end
+
{:cont,
socket
|> assign(:screenshot?, not is_nil(params["screenshot"]))
- |> assign(:new_bounty_form, to_form(%{"github_issue_url" => "", "amount" => ""}))
+ |> assign(:main_bounty_form, main_bounty_form)
+ |> assign(:main_bounty_form_open?, false)
|> 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)}
+ |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)
+ |> attach_hook(:handle_event, :handle_event, &handle_event/3)}
+ end
+
+ defp handle_event("create_bounty_main", %{"bounty_form" => params}, socket) do
+ changeset = BountyForm.changeset(%BountyForm{}, params)
+
+ case apply_action(changeset, :save) do
+ {:ok, data} ->
+ bounty_res =
+ case data.type do
+ :github ->
+ Bounties.create_bounty(
+ %{
+ creator: socket.assigns.current_user,
+ owner: socket.assigns.current_org,
+ amount: data.amount,
+ ticket_ref: %{
+ owner: data.ticket_ref.owner,
+ repo: data.ticket_ref.repo,
+ number: data.ticket_ref.number
+ }
+ },
+ visibility: get_field(changeset, :visibility),
+ shared_with: get_field(changeset, :shared_with)
+ )
+
+ :custom ->
+ Bounties.create_bounty(
+ %{
+ creator: socket.assigns.current_user,
+ owner: socket.assigns.current_org,
+ amount: data.amount,
+ title: data.title,
+ description: data.description
+ },
+ visibility: get_field(changeset, :visibility),
+ shared_with: get_field(changeset, :shared_with)
+ )
+ end
+
+ case bounty_res do
+ {:ok, bounty} ->
+ to =
+ case data.type do
+ :github ->
+ ~p"/#{data.ticket_ref.owner}/#{data.ticket_ref.repo}/issues/#{data.ticket_ref.number}"
+
+ :custom ->
+ ~p"/org/#{socket.assigns.current_org.handle}/bounties/#{bounty.id}"
+ end
+
+ {:cont, redirect(socket, to: to)}
+
+ {:error, :already_exists} ->
+ {:cont, put_flash(socket, :warning, "You already have a bounty for this ticket")}
+
+ {:error, reason} ->
+ Logger.error("Failed to create bounty: #{inspect(reason)}")
+ {:cont, put_flash(socket, :error, "Something went wrong")}
+ end
+
+ {:error, changeset} ->
+ {:cont, assign(socket, :main_bounty_form, to_form(changeset))}
+ end
+ end
+
+ defp handle_event("open_main_bounty_form", _params, socket) do
+ {:cont, assign(socket, :main_bounty_form_open?, true)}
+ end
+
+ defp handle_event("close_main_bounty_form", _params, socket) do
+ {:cont, assign(socket, :main_bounty_form_open?, false)}
+ end
+
+ defp handle_event(_event, _params, socket) do
+ {:cont, socket}
end
defp handle_active_tab_params(_params, _url, socket) do
diff --git a/lib/algora_web/live/org/settings_live.ex b/lib/algora_web/live/org/settings_live.ex
index d94496902..c2a76306d 100644
--- a/lib/algora_web/live/org/settings_live.ex
+++ b/lib/algora_web/live/org/settings_live.ex
@@ -325,6 +325,11 @@ defmodule AlgoraWeb.Org.SettingsLive do
end
end
+ @impl true
+ def handle_event(_event, _params, socket) do
+ {:noreply, socket}
+ end
+
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
diff --git a/lib/algora_web/live/org/team_live.ex b/lib/algora_web/live/org/team_live.ex
index 99664d14d..e4a10d4f8 100644
--- a/lib/algora_web/live/org/team_live.ex
+++ b/lib/algora_web/live/org/team_live.ex
@@ -5,6 +5,7 @@ defmodule AlgoraWeb.Org.TeamLive do
alias Algora.Accounts.User
alias Algora.Organizations
+ @impl true
def mount(%{"org_handle" => handle}, _session, socket) do
org = Organizations.get_org_by_handle!(handle)
members = Organizations.list_org_members(org)
@@ -18,6 +19,7 @@ defmodule AlgoraWeb.Org.TeamLive do
|> assign(:contractors, contractors)}
end
+ @impl true
def render(assigns) do
~H"""
@@ -115,4 +117,9 @@ defmodule AlgoraWeb.Org.TeamLive do
"""
end
+
+ @impl true
+ def handle_event(_event, _params, socket) do
+ {:noreply, socket}
+ end
end
diff --git a/lib/algora_web/live/org/transactions_live.ex b/lib/algora_web/live/org/transactions_live.ex
index d24b4a188..276dc54c8 100644
--- a/lib/algora_web/live/org/transactions_live.ex
+++ b/lib/algora_web/live/org/transactions_live.ex
@@ -131,6 +131,10 @@ defmodule AlgoraWeb.Org.TransactionsLive do
end
end
+ def handle_event(_event, _params, socket) do
+ {:noreply, socket}
+ end
+
defp assign_transactions(socket) do
transactions =
Payments.list_transactions(
diff --git a/lib/algora_web/live/swift_bounties_live.ex b/lib/algora_web/live/swift_bounties_live.ex
index 32e71469b..c75133a89 100644
--- a/lib/algora_web/live/swift_bounties_live.ex
+++ b/lib/algora_web/live/swift_bounties_live.ex
@@ -575,7 +575,7 @@ defmodule AlgoraWeb.SwiftBountiesLive do
|> redirect(to: AlgoraWeb.UserAuth.generate_login_path(user.email))}
{:error, :already_exists} ->
- {:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")}
+ {:noreply, put_flash(socket, :warning, "You already have a bounty for this ticket")}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Something went wrong")}
diff --git a/lib/algora_web/live/user/dashboard_live.ex b/lib/algora_web/live/user/dashboard_live.ex
index fcd5b9948..3ed0b0cb1 100644
--- a/lib/algora_web/live/user/dashboard_live.ex
+++ b/lib/algora_web/live/user/dashboard_live.ex
@@ -300,6 +300,7 @@ defmodule AlgoraWeb.User.DashboardLive do
src={~p"/og/@/#{@current_user.handle}"}
alt={@current_user.name}
class="mt-3 w-full aspect-[1200/630] rounded-lg ring-1 ring-input bg-black"
+ loading="lazy"
/>
diff --git a/priv/repo/migrations/20250401172828_make_ticket_url_nullable.exs b/priv/repo/migrations/20250401172828_make_ticket_url_nullable.exs
new file mode 100644
index 000000000..f9ac8bc39
--- /dev/null
+++ b/priv/repo/migrations/20250401172828_make_ticket_url_nullable.exs
@@ -0,0 +1,9 @@
+defmodule Algora.Repo.Migrations.MakeTicketUrlNullable do
+ use Ecto.Migration
+
+ def change do
+ alter table(:tickets) do
+ modify :url, :string, null: true
+ end
+ end
+end