From 0d199c669884d7ba5f87edbf65614ed6c02c89e2 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 3 Jun 2025 18:44:24 +0300 Subject: [PATCH 1/4] new onboarding --- lib/algora_web/live/bounties_live.ex | 6 +- lib/algora_web/live/onboarding/org.ex | 893 ++++---------------------- lib/algora_web/live/platform_live.ex | 6 +- 3 files changed, 132 insertions(+), 773 deletions(-) diff --git a/lib/algora_web/live/bounties_live.ex b/lib/algora_web/live/bounties_live.ex index 9a14a4a3d..e6120c31d 100644 --- a/lib/algora_web/live/bounties_live.ex +++ b/lib/algora_web/live/bounties_live.ex @@ -192,7 +192,8 @@ defmodule AlgoraWeb.BountiesLive do
<.button - navigate={~p"/onboarding/org"} + href={AlgoraWeb.Constants.get(:calendar_url)} + rel="noopener" size="xl" class="w-full text-lg drop-shadow-[0_1px_5px_#34d39980]" > @@ -554,7 +555,8 @@ defmodule AlgoraWeb.BountiesLive do
<.button - navigate={~p"/onboarding/org"} + href={AlgoraWeb.Constants.get(:calendar_url)} + rel="noopener" class="h-10 sm:h-14 rounded-md px-8 sm:px-12 text-sm sm:text-xl" > Companies diff --git a/lib/algora_web/live/onboarding/org.ex b/lib/algora_web/live/onboarding/org.ex index 5407b41b7..ce1134b6c 100644 --- a/lib/algora_web/live/onboarding/org.ex +++ b/lib/algora_web/live/onboarding/org.ex @@ -3,812 +3,167 @@ defmodule AlgoraWeb.Onboarding.OrgLive do use AlgoraWeb, :live_view use LiveSvelte.Components - import Ecto.Changeset - - alias Algora.Accounts - alias Algora.Accounts.User - alias Algora.Organizations - alias AlgoraWeb.Components.Wordmarks - alias AlgoraWeb.LocalStore - alias Phoenix.LiveView.AsyncResult - require Logger - @steps [:tech_stack, :email, :preferences] - - # === SCHEMAS === # - - defmodule TechStackForm do + defmodule Form do @moduledoc false use Ecto.Schema - import Ecto.Changeset - - @primary_key false - embedded_schema do - field :tech_stack, {:array, :string} - end - - def init do - to_form(TechStackForm.changeset(%TechStackForm{}, %{tech_stack: []})) - end - - def changeset(form, attrs) do - cast(form, attrs, [:tech_stack]) - end - end - - defmodule EmailForm do - @moduledoc false - use Ecto.Schema - - import Ecto.Changeset - @primary_key false + @derive {Jason.Encoder, only: [:email, :job_description, :candidate_description]} embedded_schema do field :email, :string - field :domain, :string - end - - def init do - to_form(EmailForm.changeset(%EmailForm{}, %{})) - end - - def validate_domain_not_blacklisted(changeset) do - domain = get_field(changeset, :domain) - - if not is_nil(domain) and Algora.Crawler.blacklisted?(domain) do - add_error(changeset, :domain, "You can only use a company domain") - else - changeset - end + field :job_description, :string + field :candidate_description, :string end - def validate_email_is_company_domain(changeset) do - domain = get_field(changeset, :domain) - email = get_field(changeset, :email) - - if is_nil(email) or is_nil(domain) do - changeset - else - case String.split(email, "@") do - [_, ^domain] -> - changeset - - [_, _not_email_domain] -> - add_error(changeset, :email, "Your email address must match your company domain") - end - end - end - - def changeset(form, attrs) do + def changeset(form, attrs \\ %{}) do form - |> cast(attrs, [:email, :domain]) - |> validate_required([:email, :domain]) - |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must be a valid email address") - |> validate_format(:domain, ~r/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/, - message: "must be a valid domain" - ) - |> validate_domain_not_blacklisted() - end - end - - defmodule VerificationForm do - @moduledoc false - use Ecto.Schema - - import Ecto.Changeset - - @primary_key false - embedded_schema do - field :code, :string - end - - def init do - to_form(VerificationForm.changeset(%VerificationForm{}, %{})) - end - - def changeset(form, attrs) do - form - |> cast(attrs, [:code]) - |> validate_required([:code]) - end - end - - defmodule PreferencesForm do - @moduledoc false - use Ecto.Schema - - import Ecto.Changeset - - @primary_key false - embedded_schema do - field :hiring, :boolean - field :categories, {:array, :string} - end - - def hiring_options do - [{"Yes", "true"}, {"No", "false"}] - end - - def categories_options do - [ - {"Open source company", "open_source"}, - {"Closed source company", "closed_source"}, - {"Agency / consultancy / studio", "agency"}, - {"Non-profit / FOSS", "nonprofit"} - ] - end - - def init do - to_form(PreferencesForm.changeset(%PreferencesForm{}, %{categories: []})) - end - - def changeset(form, attrs) do - cast(form, attrs, [:hiring, :categories]) + |> Ecto.Changeset.cast(attrs, [:email, :job_description, :candidate_description]) + |> Ecto.Changeset.validate_required([:email, :job_description]) + |> Ecto.Changeset.validate_format(:email, ~r/@/) end end - # === LIFECYCLE === # - + @impl true def mount(_params, _session, socket) do - {:ok, - socket - |> assign(:ip_address, AlgoraWeb.Util.get_ip(socket)) - |> assign(:tech_stack_form, TechStackForm.init()) - |> assign(:email_form, EmailForm.init()) - |> assign(:verification_form, VerificationForm.init()) - |> assign(:preferences_form, PreferencesForm.init()) - |> assign(:step, Enum.at(@steps, 0)) - |> assign(:steps, @steps) - |> assign(:code_sent?, false) - |> assign(:code_valid?, nil) - |> assign(:timezone, nil) - |> assign(:secret, nil) - |> assign(:user_metadata, AsyncResult.loading()) - |> assign_matching_devs()} - end - - # === EVENT HANDLERS === # - - def handle_params(params, _uri, socket) do - socket = - LocalStore.init(socket, - key: __MODULE__, - checkpoint_url: ~p"/onboarding/org?#{%{checkpoint: "1"}}" - ) - - socket = if params["checkpoint"] == "1", do: LocalStore.subscribe(socket), else: socket - - {:noreply, socket} - end - - def handle_event("restore_settings", params, socket) do - {:noreply, LocalStore.restore(socket, params)} - end - - def handle_event("prev_step", _, 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 - - def handle_event("submit_tech_stack", %{"tech_stack_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 = - %TechStackForm{} - |> TechStackForm.changeset(%{tech_stack: tech_stack}) - |> Map.put(:action, :validate) - - case changeset do - %{valid?: true} -> - {:noreply, - socket - |> LocalStore.assign_cached(:tech_stack_form, to_form(changeset)) - |> LocalStore.assign_cached(:step, :email) - |> assign_matching_devs()} - - %{valid?: false} -> - {:noreply, LocalStore.assign_cached(socket, :tech_stack_form, to_form(changeset))} - end - end - - def handle_event("submit_email", %{"email_form" => params}, socket) do - changeset = - %EmailForm{} - |> EmailForm.changeset(params) - |> Map.put(:action, :validate) - - case changeset do - %{valid?: true} = changeset -> - email = get_field(changeset, :email) - {secret, code} = AlgoraWeb.UserAuth.generate_totp() - - {:ok, _} = Accounts.deliver_totp_signup_email(email, code) - - {:noreply, - socket - |> LocalStore.assign_cached(:secret, secret) - |> LocalStore.assign_cached(:email_form, to_form(changeset)) - |> LocalStore.assign_cached(:code_sent?, true) - |> assign_matching_devs() - |> start_async(:fetch_metadata, fn -> Algora.Crawler.fetch_user_metadata(email) end) - |> assign(:user_metadata, AsyncResult.loading())} - - %{valid?: false} = changeset -> - {:noreply, LocalStore.assign_cached(socket, :email_form, to_form(changeset))} - end + {:ok, assign(socket, :form, to_form(Form.changeset(%Form{}, %{})))} end - def handle_event("submit_preferences", params, socket) do - changeset = - %PreferencesForm{} - |> PreferencesForm.changeset(params["preferences_form"] || %{}) - |> Map.put(:action, :validate) - - case changeset do - %{valid?: true} -> - # Get all the form data - email = get_field(socket.assigns.email_form.source, :email) - domain = get_field(socket.assigns.email_form.source, :domain) - tech_stack = get_field(socket.assigns.tech_stack_form.source, :tech_stack) - preferences = changeset.changes - - if socket.assigns.code_valid? do - metadata = - case socket.assigns.user_metadata do - %AsyncResult{ok?: true, result: metadata} -> metadata - _ -> %{} - end - - org_name = - case get_in(metadata, [:org, :display_name]) do - nil -> - domain - |> String.split(".") - |> List.first() - |> String.capitalize() - - name -> - name - end - - org_handle = - case get_in(metadata, [:org, :handle]) do - nil -> - domain - |> String.split(".") - |> List.first() - |> String.downcase() - - handle -> - handle - end - - user_handle = Organizations.generate_handle_from_email(email) - - org_params = - %{ - display_name: org_name, - bio: - get_in(metadata, [:org, :bio]) || - get_in(metadata, [:org, :og_description]) || - get_in(metadata, [:org, :og_title]), - avatar_url: get_in(metadata, [:org, :avatar_url]) || get_in(metadata, [:org, :favicon_url]), - handle: org_handle, - domain: domain, - og_title: get_in(metadata, [:org, :og_title]), - og_image_url: get_in(metadata, [:org, :og_image_url]), - tech_stack: tech_stack, - hiring: get_in(preferences, [:hiring]), - categories: get_in(preferences, [:categories]), - website_url: get_in(metadata, [:org, :website_url]), - twitter_url: get_in(metadata, [:org, :socials, :twitter]), - github_url: get_in(metadata, [:org, :socials, :github]), - youtube_url: get_in(metadata, [:org, :socials, :youtube]), - twitch_url: get_in(metadata, [:org, :socials, :twitch]), - discord_url: get_in(metadata, [:org, :socials, :discord]), - slack_url: get_in(metadata, [:org, :socials, :slack]), - linkedin_url: get_in(metadata, [:org, :socials, :linkedin]) - } - - user_params = - %{ - email: email, - display_name: user_handle, - avatar_url: get_in(metadata, [:avatar_url]), - handle: user_handle, - tech_stack: tech_stack, - timezone: socket.assigns.timezone - } - - member_params = - %{ - role: :admin - } - - params = - %{ - organization: org_params, - user: user_params, - member: member_params - } - - socket = - case Algora.Organizations.onboard_organization(params) do - {:ok, _} -> - redirect(socket, to: AlgoraWeb.UserAuth.generate_login_path(email)) - - {:error, name, changeset, _created} -> - Logger.error("error onboarding organization: #{inspect(name)} #{inspect(changeset)}") - - socket - |> put_flash(:error, "Something went wrong. Please try again.") - |> redirect(to: "/") - end - - {:noreply, socket} - else - throttle() - - {:noreply, - socket - |> put_flash(:error, "Invalid verification code") - |> LocalStore.assign_cached(:step, :email)} - end - - %{valid?: false} -> - {:noreply, LocalStore.assign_cached(socket, :preferences_form, to_form(changeset))} - end - end - - def handle_event("submit_verification", %{"verification_form" => params}, socket) do - changeset = - %VerificationForm{} - |> VerificationForm.changeset(params) - |> Map.put(:action, :validate) - - case changeset do - %{valid?: true} = changeset -> - code = get_field(changeset, :code) - - case AlgoraWeb.UserAuth.verify_totp(socket.assigns.ip_address, socket.assigns.secret, String.trim(code)) do - :ok -> - {:noreply, - socket - |> LocalStore.assign_cached(:verification_form, to_form(changeset)) - |> LocalStore.assign_cached(:code_valid?, true) - |> LocalStore.assign_cached(:step, :preferences)} - - {:error, :rate_limit_exceeded} -> - throttle() - {:noreply, put_flash(socket, :error, "Too many attempts. Please try again later.")} - - {:error, :invalid_totp} -> - throttle() - {:noreply, put_flash(socket, :error, "Invalid verification code")} - end - - %{valid?: false} = changeset -> - {:noreply, LocalStore.assign_cached(socket, :verification_form, to_form(changeset))} - end - end - - def handle_event("tech_stack_changed", %{"tech_stack" => tech_stack}, socket) do - changeset = TechStackForm.changeset(%TechStackForm{}, %{tech_stack: tech_stack}) - - {:noreply, - socket - |> LocalStore.assign_cached(:tech_stack_form, to_form(changeset)) - |> assign_matching_devs()} - end - - def handle_event("timezone_changed", %{"timezone" => timezone}, socket) do - {:noreply, assign(socket, :timezone, timezone)} - end - - # === PRIVATE HELPERS === # - - defp assign_matching_devs(socket) do - tech_stack = get_field(socket.assigns.tech_stack_form.source, :tech_stack) - - matching_devs = - Accounts.list_developers( - limit: 5, - sort_by_tech_stack: tech_stack, - sort_by_country: socket.assigns.current_country, - earnings_gt: Money.new!(200, "USD") - ) - - assign(socket, :matching_devs, matching_devs) - end - - # === TEMPLATES === # - - defp main_content(%{step: :tech_stack} = assigns) do - ~H""" -
- <.form - for={@tech_stack_form} - phx-submit="submit_tech_stack" - class="space-y-6" - onkeydown="if(event.key === 'Enter') { event.preventDefault(); return false; }" - > -
-

- What's your tech stack? -

-

- Enter a comma-separated list -

- - <.TechStack - classes="mt-4 border-2 border-foreground/50" - tech={get_field(@tech_stack_form.source, :tech_stack) || []} - socket={@socket} - form="tech_stack_form" - /> - - <.error :for={msg <- @tech_stack_form[:tech_stack].errors |> Enum.map(&translate_error(&1))}> - {msg} - -
- -
- <.button type="submit" variant="secondary"> - Skip - - <.button type="submit"> - Next - -
- -
+ defp placeholder_text do """ - end - - defp main_content(%{step: :email, code_sent?: false} = assigns) do - ~H""" -
-

- Join with your team -

- - <.form for={@email_form} phx-submit="submit_email" class="space-y-6"> - <.input - field={@email_form[:email]} - label="Work Email" - icon="tabler-mail" - type="text" - placeholder="you@company.com" - class="w-full border-input bg-background" - data-domain-target - phx-hook="DeriveDomain" - autocomplete="email" - /> - <.input - field={@email_form[:domain]} - icon="tabler-at" - label="Company Domain" - helptext="This will let your teammates auto-join your org" - type="text" - placeholder="company.com" - class="w-full border-input bg-background" - data-domain-source - /> -

- By continuing, you agree to Algora's - <.link href={AlgoraWeb.Constants.get(:terms_url)} class="text-primary hover:underline"> - Terms of Service - - and <.link href={AlgoraWeb.Constants.get(:privacy_url)} class="text-primary hover:underline">Privacy Policy. -

- -
- <.button type="button" phx-click="prev_step" variant="secondary"> - <.icon name="tabler-arrow-left" class="mr-2 size-4" /> Previous - - <.button type="submit"> - Next <.icon name="tabler-arrow-right" class="ml-2 size-4" /> - -
- -
+ - GitHub looks like a green carpet, red flag if wearing suit in pfp + - Has contributions to open source inference engines (like vLLM) + - Posts regularly on X and LinkedIn """ end - defp main_content(%{step: :email, code_sent?: true} = assigns) do - ~H""" -
-
-

- Verify your email -

-

- We've sent a code to {get_field(@email_form.source, :email)} -

+ @impl true + def handle_event("submit", %{"form" => params}, socket) do + case %Form{} |> Form.changeset(params) |> Ecto.Changeset.apply_action(:save) do + {:ok, data} -> + Algora.Activities.alert(Jason.encode!(data), :critical) + {:noreply, put_flash(socket, :info, "We'll send you matching candidates within the next few hours.")} -
- <.form for={@verification_form} phx-submit="submit_verification" class="space-y-6"> - - <.input - field={@verification_form[:code]} - type="text" - placeholder="Enter verification code" - class="w-full border-input bg-background text-center text-xl sm:text-2xl tracking-widest" - /> - - <%= if @code_valid? == false do %> -

Invalid verification code

- <% end %> - -
- <.button type="submit"> - Next <.icon name="tabler-arrow-right" class="ml-2 size-4" /> - -
- -
-
-
- """ + {:error, changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end end - defp main_content(%{step: :preferences} = assigns) do + @impl true + def render(assigns) do ~H""" -
- <.form for={@preferences_form} phx-submit="submit_preferences" class="space-y-8"> -
-

- Let's personalize your experience -

-

- We'll use this information to match you with the best developers -

-
- -
-
- -

- We will match you with developers who are looking for full-time work -

-
- <%= for {label, value} <- PreferencesForm.hiring_options() do %> - - <% end %> -
- <.error :for={msg <- @preferences_form[:hiring].errors |> Enum.map(&translate_error(&1))}> - {msg} - +
+
+
+
+ <.wordmark class="h-8 w-auto" />
- -
- -

- Select all that apply -

-
- <%= for {label, value} <- PreferencesForm.categories_options() do %> - - <% end %> +
+ Trusted by +
+
+ Keep + Trigger.dev + Traceloop + Million + moon + Dittofeed + Highlight +
- <.error :for={ - msg <- @preferences_form[:categories].errors |> Enum.map(&translate_error(&1)) - }> - {msg} -
+
-
- <.button type="submit" variant="secondary"> - Skip - - <.button type="submit"> - Meet developers <.icon name="tabler-users" class="ml-2 size-4" /> - -
- -
- """ - end - - def render(assigns) do - ~H""" -
-
-
-
-
- {main_content(assigns)} +
+
+ <.form for={@form} phx-submit="submit" class="flex flex-col gap-6"> + <.input + field={@form[:email]} + type="email" + label="Work email" + placeholder="you@company.com" + /> + <.input + field={@form[:job_description]} + type="textarea" + label="Job description / careers URL" + rows="3" + class="resize-none" + placeholder="Tell us about the role and your requirements..." + /> + <.input + field={@form[:candidate_description]} + type="textarea" + label="Describe your ideal candidate, heuristics, green/red flags etc." + rows="3" + class="resize-none" + placeholder={placeholder_text()} + /> +
+ <.button class="w-full" type="submit">Receive your candidates +
+ No credit card required - only pay when you hire +
-
-
-
- {sidebar_content(assigns)} - -
- {sidebar_content(%{assigns | step: :email})} -
+
-
- <.Timezone socket={@socket} /> - """ - end - - defp sidebar_content(%{step: :email} = assigns) do - ~H""" -
-

- You're in good company -

- -
- """ - end - - defp sidebar_content(assigns) do - ~H""" -
-

- Matching Developers -

- <%= for dev <- @matching_devs do %> -
-
- {dev.name} -
-
-
-
- {dev.name} {Algora.Misc.CountryEmojis.get(dev.country)} -
-
@{User.handle(dev)}
-
-
-
Earned
-
- {Money.to_string!(dev.total_earned)} -
-
-
-
-
- <%= for tech <- dev.tech_stack do %> - - {tech} - - <% end %> -
-
+
+
+
+
+ © 2025 Algora PBC. All rights reserved. +
+
+ <.link + class="w-full md:w-auto flex items-center justify-center gap-2 rounded-lg border border-gray-700 py-2 pl-2 pr-3.5 text-xs text-muted-foreground hover:text-foreground transition-colors hover:border-gray-600" + href={AlgoraWeb.Constants.get(:calendar_url)} + rel="noopener" + > + <.icon name="tabler-calendar-clock" class="size-4" /> + Schedule a call + + <.link + class="w-full md:w-auto flex items-center justify-center gap-2 rounded-lg border border-gray-700 py-2 pl-2 pr-3.5 text-xs text-muted-foreground hover:text-foreground transition-colors hover:border-gray-600" + href="tel:+16504202207" + > + <.icon name="tabler-phone" class="size-4" /> US +1 (650) 420-2207 + + <.link + class="w-full md:w-auto flex items-center justify-center gap-2 rounded-lg border border-gray-700 py-2 pl-2 pr-3.5 text-xs text-muted-foreground hover:text-foreground transition-colors hover:border-gray-600" + href="tel:+306973184144" + > + <.icon name="tabler-phone" class="size-4" /> EU +30 (697) 318-4144 +
- <% end %> -
+ + """ end - - def handle_async(:fetch_metadata, {:ok, metadata}, socket) do - {:noreply, LocalStore.assign_cached(socket, :user_metadata, AsyncResult.ok(socket.assigns.user_metadata, metadata))} - end - - def handle_async(:fetch_metadata, {:exit, reason}, socket) do - {:noreply, assign(socket, :user_metadata, AsyncResult.failed(socket.assigns.user_metadata, reason))} - end - - defp throttle, do: :timer.sleep(1000) end diff --git a/lib/algora_web/live/platform_live.ex b/lib/algora_web/live/platform_live.ex index 415ad38c9..9fff70ae3 100644 --- a/lib/algora_web/live/platform_live.ex +++ b/lib/algora_web/live/platform_live.ex @@ -471,7 +471,8 @@ defmodule AlgoraWeb.PlatformLive do
<.button - navigate={~p"/onboarding/org"} + href={AlgoraWeb.Constants.get(:calendar_url)} + rel="noopener" size="xl" class="w-full text-lg drop-shadow-[0_1px_5px_#34d39980]" > @@ -833,7 +834,8 @@ defmodule AlgoraWeb.PlatformLive do
<.button - navigate={~p"/onboarding/org"} + href={AlgoraWeb.Constants.get(:calendar_url)} + rel="noopener" class="h-10 sm:h-14 rounded-md px-8 sm:px-12 text-sm sm:text-xl" > Companies From 16e37524a80a02438ce4e5384f56eaf93ae1d579 Mon Sep 17 00:00:00 2001 From: zafer Date: Sun, 6 Jul 2025 12:06:40 +0300 Subject: [PATCH 2/4] redirect org onboarding --- config/config.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.exs b/config/config.exs index 553647a48..41d885427 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,6 +22,7 @@ config :algora, {"/create/org", "/onboarding/org"}, {"/solve", "/onboarding/dev"}, {"/onboarding/solver", "/onboarding/dev"}, + {"/onboarding/org", "https://cal.com/ioannisflo"}, {"/:org/contract/:id", "/:org/contracts/:id"}, {"/org/*path", "/*path"}, {"/@/:handle", "/:handle/profile"}, From 9b26ee5630abf3f2e1ccff5a8fc0da2cd81f3bca Mon Sep 17 00:00:00 2001 From: zafer Date: Sun, 6 Jul 2025 12:21:51 +0300 Subject: [PATCH 3/4] add cal embed --- config/config.exs | 1 - lib/algora_web/live/onboarding/org.ex | 113 +++++++++++++++++++------- 2 files changed, 82 insertions(+), 32 deletions(-) diff --git a/config/config.exs b/config/config.exs index 41d885427..553647a48 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,7 +22,6 @@ config :algora, {"/create/org", "/onboarding/org"}, {"/solve", "/onboarding/dev"}, {"/onboarding/solver", "/onboarding/dev"}, - {"/onboarding/org", "https://cal.com/ioannisflo"}, {"/:org/contract/:id", "/:org/contracts/:id"}, {"/org/*path", "/*path"}, {"/@/:handle", "/:handle/profile"}, diff --git a/lib/algora_web/live/onboarding/org.ex b/lib/algora_web/live/onboarding/org.ex index ce1134b6c..211560ad6 100644 --- a/lib/algora_web/live/onboarding/org.ex +++ b/lib/algora_web/live/onboarding/org.ex @@ -97,38 +97,89 @@ defmodule AlgoraWeb.Onboarding.OrgLive do
-
-
- <.form for={@form} phx-submit="submit" class="flex flex-col gap-6"> - <.input - field={@form[:email]} - type="email" - label="Work email" - placeholder="you@company.com" - /> - <.input - field={@form[:job_description]} - type="textarea" - label="Job description / careers URL" - rows="3" - class="resize-none" - placeholder="Tell us about the role and your requirements..." - /> - <.input - field={@form[:candidate_description]} - type="textarea" - label="Describe your ideal candidate, heuristics, green/red flags etc." - rows="3" - class="resize-none" - placeholder={placeholder_text()} - /> -
- <.button class="w-full" type="submit">Receive your candidates -
- No credit card required - only pay when you hire +
+
+ +
+ + + +
From 03d47294589f1c049e1162423b4dc295203c32c3 Mon Sep 17 00:00:00 2001 From: zafer Date: Sun, 6 Jul 2025 12:26:16 +0300 Subject: [PATCH 4/4] add redirect --- config/config.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.exs b/config/config.exs index 553647a48..41d885427 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,6 +22,7 @@ config :algora, {"/create/org", "/onboarding/org"}, {"/solve", "/onboarding/dev"}, {"/onboarding/solver", "/onboarding/dev"}, + {"/onboarding/org", "https://cal.com/ioannisflo"}, {"/:org/contract/:id", "/:org/contracts/:id"}, {"/org/*path", "/*path"}, {"/@/:handle", "/:handle/profile"},