diff --git a/lib/algora_web/live/onboarding/dev.ex b/lib/algora_web/live/onboarding/dev.ex index f345be9af..0ea9b1055 100644 --- a/lib/algora_web/live/onboarding/dev.ex +++ b/lib/algora_web/live/onboarding/dev.ex @@ -1,14 +1,62 @@ defmodule AlgoraWeb.Onboarding.DevLive do @moduledoc false use AlgoraWeb, :live_view + use LiveSvelte.Components + import Ecto.Changeset import Ecto.Query + alias Algora.Accounts.User + alias Algora.Github alias Algora.Payments.Transaction alias Algora.Repo alias AlgoraWeb.Components.Logos + require Logger + + @steps [:info, :oauth] + + defmodule InfoForm do + @moduledoc false + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field :tech_stack, {:array, :string} + field :intentions, {:array, :string} + end + + def init do + to_form(InfoForm.changeset(%InfoForm{}, %{tech_stack: [], intentions: []})) + end + + def changeset(form, attrs) do + form + |> cast(attrs, [:tech_stack, :intentions]) + |> validate_required(:tech_stack, message: "Please select at least one technology") + |> validate_required(:intentions, message: "Please select at least one intention") + |> validate_length(:tech_stack, min: 1, message: "Please enter at least one technology") + |> validate_length(:intentions, min: 1, message: "Please select at least one intention") + |> validate_subset(:intentions, Enum.map(intentions_options(), &elem(&1, 0))) + end + + def intentions_options do + [ + {"bounties", "Solve Bounties", "Work on open source issues and earn rewards", "tabler-diamond"}, + {"jobs", "Find Full-time Work", "Get matched with companies hiring developers", "tabler-briefcase"}, + {"contracts", "Freelance Work", "Take on flexible contract-based projects", "tabler-clock"} + ] + end + end + + @impl true def mount(_params, _session, socket) do + if connected?(socket) do + Phoenix.PubSub.subscribe(Algora.PubSub, "auth:#{socket.id}") + end + context = %{ country: socket.assigns.current_country, tech_stack: [], @@ -37,179 +85,185 @@ defmodule AlgoraWeb.Onboarding.DevLive do {:ok, socket - |> assign(:step, 1) - |> assign(:total_steps, 2) + |> assign(:step, Enum.at(@steps, 0)) + |> assign(:steps, @steps) + |> assign(:total_steps, length(@steps)) |> assign(:context, context) - |> assign(:transactions, transactions)} + |> assign(:transactions, transactions) + |> assign(:info_form, InfoForm.init())} end + @impl true def render(assigns) do ~H"""
-
+
- {@step} / {@total_steps} + {Enum.find_index(@steps, &(&1 == @step)) + 1} / {length(@steps)} -

Get started

+

+ <%= if @step == Enum.at(@steps, -1) do %> + Last step + <% else %> + Get started + <% end %> +

- {render_step(assigns)} -
- -
- <%= if @step > 1 do %> - <.button phx-click="prev_step" variant="secondary"> - Previous - - <% else %> -
- <% end %> - <%= if @step < @total_steps do %> - <.button phx-click="next_step" variant="default"> - Next - - <% else %> - <.button - href={Algora.Github.authorize_url()} - rel="noopener" - class="inline-flex items-center" - > - Sign in with GitHub - - <% end %> + {main_content(assigns)}
-

- Recently Completed Bounties -

- <%= if @transactions == [] do %> -

No completed bounties available

- <% else %> - <%= for transaction <- @transactions do %> -
-
-
-
- {Money.to_string!(transaction.bounty.amount)} -
-
- {transaction.bounty.ticket.repository.user.provider_login}/{transaction.bounty.ticket.repository.name}#{transaction.bounty.ticket.number} -
-
- {transaction.bounty.ticket.title} -
-
- {Algora.Util.time_ago(transaction.succeeded_at)} -
-
- -
-

- Awarded to -

- {transaction.user.name} -
- {transaction.user.name} -
- {Algora.Misc.CountryEmojis.get(transaction.user.country, "🌎")} -
-
-
-
-
- <% end %> - <% end %> + {sidebar_content(assigns)}
""" end - def render_step(%{step: 1} = assigns) do + @impl true + def handle_info({:authenticated, user}, socket) do + tech_stack = get_field(socket.assigns.info_form.source, :tech_stack) + intentions = get_field(socket.assigns.info_form.source, :intentions) + + case user + |> change( + tech_stack: tech_stack, + seeking_bounties: "bounties" in intentions, + seeking_contracts: "contracts" in intentions, + seeking_jobs: "jobs" in intentions + ) + |> Repo.update() do + {:ok, _user} -> + :ok + + {:error, changeset} -> + Logger.error("Failed to update user #{user.id} on onboarding: #{inspect(changeset)}") + end + + {:noreply, + socket + |> assign(:current_user, user) + |> redirect(to: ~p"/")} + end + + @impl true + def handle_event("sign_in_with_github", _params, socket) do + popup_url = Github.authorize_url(%{socket_id: socket.id}) + {:noreply, push_event(socket, "open_popup", %{url: popup_url})} + end + + @impl true + def handle_event("prev_step", _params, socket) do + current_step_index = Enum.find_index(socket.assigns.steps, &(&1 == socket.assigns.step)) + prev_step = Enum.at(socket.assigns.steps, current_step_index - 1) + {:noreply, assign(socket, :step, prev_step)} + end + + @impl true + def handle_event("tech_stack_changed", _params, socket) do + {:noreply, socket} + end + + @impl true + def handle_event("submit_info", %{"info_form" => params}, socket) do + tech_stack = + Jason.decode!(params["tech_stack"]) ++ + case String.trim(params["tech_stack_input"]) do + "" -> [] + tech_stack_input -> String.split(tech_stack_input, ",") + end + + changeset = + %InfoForm{} + |> InfoForm.changeset(%{tech_stack: tech_stack, intentions: params["intentions"]}) + |> Map.put(:action, :validate) + + case changeset do + %{valid?: true} -> + {:noreply, + socket + |> assign(:info_form, to_form(changeset)) + |> assign(step: :oauth)} + + %{valid?: false} -> + {:noreply, assign(socket, info_form: to_form(changeset))} + end + end + + defp main_content(%{step: :info} = assigns) do ~H"""
-
-

- What is your tech stack? -

-

Select the technologies you work with

- -
- <.input - type="text" - name="tech_input" - value="" - placeholder="Elixir, Phoenix, PostgreSQL, etc." - phx-keydown="handle_tech_input" - phx-debounce="200" - class="w-full border-input bg-background" + <.form for={@info_form} phx-submit="submit_info" class="space-y-8"> +
+

+ What is your tech stack? +

+

Select the technologies you work with

+ + <.TechStack + class="mt-4" + tech={get_field(@info_form.source, :tech_stack) || []} + socket={@socket} + form="info_form" /> -
-
- <%= for tech <- @context.tech_stack do %> -
- {tech} - -
- <% end %> + <.error :for={msg <- @info_form[:tech_stack].errors |> Enum.map(&translate_error(&1))}> + {msg} +
-
-
-

- What are you looking to do? -

-

Select all that apply

- -
- <%= for {intention, label, description, icon} <- [ - {"bounties", "Solve Bounties", "Work on open source issues and earn rewards", "tabler-diamond"}, - {"jobs", "Find Full-time Work", "Get matched with companies hiring developers", "tabler-briefcase"}, - {"projects", "Freelance Work", "Take on flexible contract-based projects", "tabler-clock"} - ] do %> -
""" end - def render_step(%{step: 2} = assigns) do + defp main_content(%{step: :oauth} = assigns) do ~H"""
@@ -226,80 +280,64 @@ defmodule AlgoraWeb.Onboarding.DevLive do and <.link href="/privacy" class="text-primary hover:underline">Privacy Policy.

+
+ <.button phx-click="prev_step" variant="secondary"> + Previous + + <.button phx-click="sign_in_with_github" class="inline-flex items-center"> + Sign in with GitHub + +
""" end - defp update_context_field(context, "tech_stack", _value, %{"tech" => tech}) do - tech_stack = - if tech in context.tech_stack, - do: List.delete(context.tech_stack, tech), - else: [tech | context.tech_stack] - - %{context | tech_stack: tech_stack} - end - - defp update_context_field(context, "email" = _field, value, _params) do - domain = value |> String.split("@") |> List.last() - - context - |> Map.put(:email, value) - |> Map.put(:domain, domain) - end - - defp update_context_field(context, field, value, _params) do - Map.put(context, String.to_atom(field), value) - end - - def handle_event("next_step", _, socket) do - {:noreply, assign(socket, step: socket.assigns.step + 1)} - end - - def handle_event("prev_step", _, socket) do - {:noreply, assign(socket, step: socket.assigns.step - 1)} - end - - def handle_event("submit", _, socket) do - # Handle context submission - {:noreply, socket} - end - - def handle_event("add_tech", %{"tech" => tech}, socket) do - updated_tech_stack = Enum.uniq([tech | socket.assigns.context.tech_stack]) - updated_context = Map.put(socket.assigns.context, :tech_stack, updated_tech_stack) - {:noreply, assign(socket, context: updated_context)} - end - - def handle_event("remove_tech", %{"tech" => tech}, socket) do - updated_tech_stack = List.delete(socket.assigns.context.tech_stack, tech) - updated_context = Map.put(socket.assigns.context, :tech_stack, updated_tech_stack) - {:noreply, assign(socket, context: updated_context)} - end - - def handle_event("update_context", %{"field" => field, "value" => value} = params, socket) do - updated_context = update_context_field(socket.assigns.context, field, value, params) - {:noreply, assign(socket, context: updated_context)} - end - - def handle_event("toggle_intention", %{"intention" => intention}, socket) do - updated_intentions = - if intention in socket.assigns.context.intentions do - List.delete(socket.assigns.context.intentions, intention) - else - [intention | socket.assigns.context.intentions] - end - - updated_context = Map.put(socket.assigns.context, :intentions, updated_intentions) - {:noreply, assign(socket, context: updated_context)} - end - - def handle_event("handle_tech_input", %{"key" => "Enter", "value" => tech}, socket) when byte_size(tech) > 0 do - updated_tech_stack = Enum.uniq([String.trim(tech) | socket.assigns.context.tech_stack]) - updated_context = Map.put(socket.assigns.context, :tech_stack, updated_tech_stack) - {:noreply, assign(socket, context: updated_context)} - end + defp sidebar_content(assigns) do + ~H""" +

+ Recently Completed Bounties +

+ <%= if @transactions == [] do %> +

No completed bounties available

+ <% else %> + <%= for transaction <- @transactions do %> +
+
+
+
+ {Money.to_string!(transaction.bounty.amount)} +
+
+ {transaction.bounty.ticket.repository.user.provider_login}/{transaction.bounty.ticket.repository.name}#{transaction.bounty.ticket.number} +
+
+ {transaction.bounty.ticket.title} +
+
+ {Algora.Util.time_ago(transaction.succeeded_at)} +
+
- def handle_event("handle_tech_input", _params, socket) do - {:noreply, socket} +
+

+ Awarded to +

+ {transaction.user.name} +
+ {transaction.user.name} +
+ {Algora.Misc.CountryEmojis.get(transaction.user.country, "🌎")} +
+
+
+
+
+ <% end %> + <% end %> + """ end end diff --git a/lib/algora_web/live/swift_bounties_live.ex b/lib/algora_web/live/swift_bounties_live.ex index 66d97676d..3236d640a 100644 --- a/lib/algora_web/live/swift_bounties_live.ex +++ b/lib/algora_web/live/swift_bounties_live.ex @@ -616,7 +616,7 @@ defmodule AlgoraWeb.SwiftBountiesLive do {:noreply, socket |> put_flash(:info, "Bounty created") - |> push_navigate(to: ~p"/home")} + |> redirect(to: ~p"/")} {:error, :already_exists} -> {:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")}