From 548ef595e345c936e6e29e4e7e556c6de0b9439c Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 17:04:23 +0300 Subject: [PATCH 01/11] crud --- lib/algora/accounts/accounts.ex | 6 +- lib/algora/accounts/schemas/user.ex | 17 +- lib/algora/jobs/jobs.ex | 95 ++++++++++++ lib/algora/jobs/schemas/job_posting.ex | 42 +++++ lib/algora/payments/payments.ex | 12 +- lib/algora/payments/schemas/transaction.ex | 1 + lib/algora_web/components/core_components.ex | 2 +- .../controllers/webhooks/stripe_controller.ex | 9 ++ lib/algora_web/live/jobs_live.ex | 145 ++++++++++++++++++ lib/algora_web/router.ex | 1 + .../20250424152036_create_job_postings.exs | 28 ++++ priv/repo/seeds.exs | 27 ++++ test/support/factory.ex | 15 ++ 13 files changed, 386 insertions(+), 14 deletions(-) create mode 100644 lib/algora/jobs/jobs.ex create mode 100644 lib/algora/jobs/schemas/job_posting.ex create mode 100644 lib/algora_web/live/jobs_live.ex create mode 100644 priv/repo/migrations/20250424152036_create_job_postings.exs diff --git a/lib/algora/accounts/accounts.ex b/lib/algora/accounts/accounts.ex index 67b938605..eb9f714cf 100644 --- a/lib/algora/accounts/accounts.ex +++ b/lib/algora/accounts/accounts.ex @@ -410,7 +410,7 @@ defmodule Algora.Accounts do end def register_org(params) do - params |> User.org_registration_changeset() |> Repo.insert() + params |> User.org_registration_changeset() |> Repo.insert(returning: true) end def auto_join_orgs(user) do @@ -441,10 +441,10 @@ defmodule Algora.Accounts do orgs end - def get_or_register_user(email) do + def get_or_register_user(email, attr \\ %{}) do res = case get_user_by_email(email) do - nil -> register_org(%{email: email}) + nil -> attr |> Map.put(:email, email) |> register_org() user -> {:ok, user} end diff --git a/lib/algora/accounts/schemas/user.ex b/lib/algora/accounts/schemas/user.ex index f8262991d..31710ebf0 100644 --- a/lib/algora/accounts/schemas/user.ex +++ b/lib/algora/accounts/schemas/user.ex @@ -114,10 +114,10 @@ defmodule Algora.Accounts.User do def org_registration_changeset(params) do %User{} - |> cast(params, [:email]) + |> cast(params, [:email, :display_name, :type]) |> generate_id() |> validate_required([:email]) - |> validate_email() + |> validate_unique_email() end @doc """ @@ -161,7 +161,7 @@ defmodule Algora.Accounts.User do |> generate_id() |> validate_required([:email, :handle]) |> validate_handle() - |> validate_email() + |> validate_unique_email() |> unique_constraint(:email) |> unique_constraint(:handle) |> put_assoc(:identities, [identity_changeset]) @@ -219,7 +219,7 @@ defmodule Algora.Accounts.User do |> generate_id() |> validate_required([:email, :handle]) |> validate_handle() - |> validate_email() + |> validate_unique_email() |> unique_constraint(:email) |> unique_constraint(:handle) else @@ -249,7 +249,7 @@ defmodule Algora.Accounts.User do ]) |> generate_id() |> validate_required([:type, :handle, :email]) - |> validate_email() + |> validate_unique_email() |> unique_constraint(:handle) |> unique_constraint(:email) end @@ -286,11 +286,16 @@ defmodule Algora.Accounts.User do cast(user, params, [:email, :signup_token]) end - defp validate_email(changeset) do + def validate_email(changeset) do changeset |> validate_required([:email]) |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") |> validate_length(:email, max: 160) + end + + def validate_unique_email(changeset) do + changeset + |> validate_email() |> unsafe_validate_unique(:email, Algora.Repo) |> unique_constraint(:email) end diff --git a/lib/algora/jobs/jobs.ex b/lib/algora/jobs/jobs.ex new file mode 100644 index 000000000..3f2b5ded9 --- /dev/null +++ b/lib/algora/jobs/jobs.ex @@ -0,0 +1,95 @@ +defmodule Algora.Jobs do + @moduledoc false + + import Ecto.Changeset + import Ecto.Query + + alias Algora.Accounts + alias Algora.Bounties.LineItem + alias Algora.Jobs.JobPosting + alias Algora.Payments + alias Algora.Payments.Transaction + alias Algora.Repo + alias Algora.Util + + require Logger + + def price, do: Money.new(:USD, 499, no_fraction_if_integer: true) + + def list_jobs(opts \\ []) do + JobPosting + |> where([j], j.status == :active) + |> order_by([j], desc: j.inserted_at) + |> maybe_filter_by_tech_stack(opts[:tech_stack]) + |> maybe_limit(opts[:limit]) + |> Repo.all() + |> Repo.preload(:user) + end + + def create_job_posting(attrs) do + %JobPosting{} + |> JobPosting.changeset(attrs) + |> Repo.insert() + end + + defp maybe_filter_by_tech_stack(query, nil), do: query + defp maybe_filter_by_tech_stack(query, []), do: query + + defp maybe_filter_by_tech_stack(query, tech_stack) do + where(query, [j], fragment("? && ?", j.tech_stack, ^tech_stack)) + end + + defp maybe_limit(query, nil), do: query + defp maybe_limit(query, limit), do: limit(query, ^limit) + + @spec create_payment_session(job_posting: JobPosting.t()) :: + {:ok, String.t()} | {:error, atom()} + def create_payment_session(job_posting) do + line_items = [%LineItem{amount: price(), title: "Job posting - #{job_posting.company_name}"}] + + gross_amount = LineItem.gross_amount(line_items) + group_id = Nanoid.generate() + + Repo.transact(fn -> + with {:ok, user} <- + Accounts.get_or_register_user(job_posting.email, %{ + type: :organization, + display_name: job_posting.company_name + }), + {:ok, _charge} <- + %Transaction{} + |> change(%{ + id: Nanoid.generate(), + provider: "stripe", + type: :charge, + status: :initialized, + user_id: user.id, + job_id: job_posting.id, + gross_amount: gross_amount, + net_amount: gross_amount, + total_fee: Money.zero(:USD), + line_items: Util.normalize_struct(line_items), + group_id: group_id, + idempotency_key: "session-#{Nanoid.generate()}" + }) + |> Algora.Validations.validate_positive(:gross_amount) + |> Algora.Validations.validate_positive(:net_amount) + |> foreign_key_constraint(:user_id) + |> unique_constraint([:idempotency_key]) + |> Repo.insert(), + {:ok, session} <- + Payments.create_stripe_session( + user, + Enum.map(line_items, &LineItem.to_stripe/1), + %{ + description: "Job posting - #{job_posting.company_name}", + metadata: %{"version" => Payments.metadata_version(), "group_id" => group_id} + }, + success_url: "#{AlgoraWeb.Endpoint.url()}/jobs?status=paid", + cancel_url: "#{AlgoraWeb.Endpoint.url()}/jobs?status=canceled" + ) do + {:ok, session.url} + end + end) + end +end diff --git a/lib/algora/jobs/schemas/job_posting.ex b/lib/algora/jobs/schemas/job_posting.ex new file mode 100644 index 000000000..1e55ddd84 --- /dev/null +++ b/lib/algora/jobs/schemas/job_posting.ex @@ -0,0 +1,42 @@ +defmodule Algora.Jobs.JobPosting do + @moduledoc false + use Algora.Schema + + alias Algora.Accounts.User + + typed_schema "job_postings" do + field :title, :string + field :description, :string + field :tech_stack, {:array, :string}, default: [] + field :url, :string, null: false + field :company_name, :string, null: false + field :company_url, :string, null: false + field :email, :string, null: false + field :status, Ecto.Enum, values: [:initialized, :processing, :active, :expired], null: false, default: :initialized + field :expires_at, :utc_datetime_usec + + belongs_to :user, User + + timestamps() + end + + def changeset(job_posting, attrs) do + job_posting + |> cast(attrs, [ + :title, + :description, + :tech_stack, + :url, + :company_name, + :company_url, + :email, + :status, + :expires_at, + :user_id + ]) + |> generate_id() + |> validate_required([:url, :company_name, :company_url, :email]) + |> User.validate_email() + |> foreign_key_constraint(:user) + end +end diff --git a/lib/algora/payments/payments.ex b/lib/algora/payments/payments.ex index 5294ef1fe..fca0fcd58 100644 --- a/lib/algora/payments/payments.ex +++ b/lib/algora/payments/payments.ex @@ -31,10 +31,14 @@ defmodule Algora.Payments do @spec create_stripe_session( user :: User.t(), line_items :: [PSP.Session.line_item_data()], - payment_intent_data :: PSP.Session.payment_intent_data() + payment_intent_data :: PSP.Session.payment_intent_data(), + opts :: [ + {:success_url, String.t()}, + {:cancel_url, String.t()} + ] ) :: {:ok, PSP.session()} | {:error, PSP.error()} - def create_stripe_session(user, line_items, payment_intent_data) do + def create_stripe_session(user, line_items, payment_intent_data, opts \\ []) do with {:ok, customer} <- fetch_or_create_customer(user) do PSP.Session.create(%{ mode: "payment", @@ -42,8 +46,8 @@ defmodule Algora.Payments do billing_address_collection: "required", line_items: line_items, invoice_creation: %{enabled: true}, - success_url: "#{AlgoraWeb.Endpoint.url()}/payment/success", - cancel_url: "#{AlgoraWeb.Endpoint.url()}/payment/canceled", + success_url: opts[:success_url] || "#{AlgoraWeb.Endpoint.url()}/payment/success", + cancel_url: opts[:cancel_url] || "#{AlgoraWeb.Endpoint.url()}/payment/canceled", payment_intent_data: payment_intent_data }) end diff --git a/lib/algora/payments/schemas/transaction.ex b/lib/algora/payments/schemas/transaction.ex index 450b1f9ec..629c12515 100644 --- a/lib/algora/payments/schemas/transaction.ex +++ b/lib/algora/payments/schemas/transaction.ex @@ -40,6 +40,7 @@ defmodule Algora.Payments.Transaction do belongs_to :claim, Algora.Bounties.Claim belongs_to :bounty, Algora.Bounties.Bounty belongs_to :tip, Algora.Bounties.Tip + belongs_to :job, Algora.Jobs.JobPosting belongs_to :linked_transaction, Algora.Payments.Transaction has_many :activities, {"transaction_activities", Activity}, foreign_key: :assoc_id diff --git a/lib/algora_web/components/core_components.ex b/lib/algora_web/components/core_components.ex index 522fd7153..39318881b 100644 --- a/lib/algora_web/components/core_components.ex +++ b/lib/algora_web/components/core_components.ex @@ -595,7 +595,7 @@ defmodule AlgoraWeb.CoreComponents do <% body -> %> -

+

<.icon :if={@kind == :info} name="tabler-circle-check" class="h-6 w-6 text-success" /> <.icon :if={@kind == :warning} diff --git a/lib/algora_web/controllers/webhooks/stripe_controller.ex b/lib/algora_web/controllers/webhooks/stripe_controller.ex index 3f2ef66e2..e2b0e0348 100644 --- a/lib/algora_web/controllers/webhooks/stripe_controller.ex +++ b/lib/algora_web/controllers/webhooks/stripe_controller.ex @@ -9,6 +9,7 @@ defmodule AlgoraWeb.Webhooks.StripeController do alias Algora.Bounties.Claim alias Algora.Bounties.Tip alias Algora.Contracts.Contract + alias Algora.Jobs.JobPosting alias Algora.Payments alias Algora.Payments.Customer alias Algora.Payments.Transaction @@ -147,6 +148,7 @@ defmodule AlgoraWeb.Webhooks.StripeController do tip_ids = txs |> Enum.map(& &1.tip_id) |> Enum.reject(&is_nil/1) |> Enum.uniq() contract_ids = txs |> Enum.map(& &1.contract_id) |> Enum.reject(&is_nil/1) |> Enum.uniq() claim_ids = txs |> Enum.map(& &1.claim_id) |> Enum.reject(&is_nil/1) |> Enum.uniq() + job_ids = txs |> Enum.map(& &1.job_id) |> Enum.reject(&is_nil/1) |> Enum.uniq() Repo.update_all(from(b in Bounty, where: b.id in ^bounty_ids), set: [status: :paid]) Repo.update_all(from(t in Tip, where: t.id in ^tip_ids), set: [status: :paid]) @@ -154,6 +156,13 @@ defmodule AlgoraWeb.Webhooks.StripeController do # TODO: add and use a new "paid" status for claims Repo.update_all(from(c in Claim, where: c.id in ^claim_ids), set: [status: :approved]) + {_, job_postings} = + Repo.update_all(from(j in JobPosting, where: j.id in ^job_ids, select: j), set: [status: :processing]) + + for job <- job_postings do + Algora.Admin.alert("Job payment received! #{job.company_name} #{job.email} #{job.url}", :info) + end + activities_result = txs |> Enum.filter(&(&1.type == :credit)) diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex new file mode 100644 index 000000000..f6aa877c8 --- /dev/null +++ b/lib/algora_web/live/jobs_live.ex @@ -0,0 +1,145 @@ +defmodule AlgoraWeb.JobsLive do + @moduledoc false + use AlgoraWeb, :live_view + + alias Algora.Jobs + alias Algora.Jobs.JobPosting + + require Logger + + @impl true + def mount(_params, _session, socket) do + jobs = Jobs.list_jobs() + changeset = JobPosting.changeset(%JobPosting{}, %{}) + + {:ok, + socket + |> assign(:page_title, "Jobs") + |> assign(:jobs, jobs) + |> assign(:form, to_form(changeset))} + end + + @impl true + def render(assigns) do + ~H""" +

+ <.section title="Jobs" subtitle="Open positions at top companies"> + <%= if Enum.empty?(@jobs) do %> + <.card class="rounded-lg bg-card py-12 text-center lg:rounded-[2rem]"> + <.card_header> +
+ <.icon name="tabler-briefcase" class="h-8 w-8 text-muted-foreground" /> +
+ <.card_title>No jobs yet + <.card_description> + Open positions will appear here once created + + + + <% else %> +
+ <%= for job <- @jobs do %> + <.card class="flex flex-col gap-4 p-6"> +
+
+ <.avatar class="h-12 w-12"> + <.avatar_image src={job.user.avatar_url} /> + <.avatar_fallback> + {Algora.Util.initials(job.user.name)} + + +
+ <.link + href={job.url} + class="text-lg font-semibold hover:underline" + target="_blank" + > + {job.title} + +
+ {job.company_name} • + <.link href={job.company_url} rel="noopener" target="_blank"> + {job.company_url |> String.replace("https://", "")} + +
+
+
+
+
+ {job.description} +
+
+ <%= for tech <- job.tech_stack do %> + <.badge variant="outline">{tech} + <% end %> +
+ + <% end %> +
+ <% end %> + + + <.section> +
+
+
+
+ Post your job
+ + in seconds + +
+
+ Reach thousands of developers looking for their next opportunity versed in your tech stack +
+ <.simple_form for={@form} phx-submit="create_job" class="mt-4 space-y-4"> +
+ <.input field={@form[:email]} label="Email" /> + <.input field={@form[:company_name]} label="Company Name" /> + <.input field={@form[:company_url]} label="Company Website" /> + <.input field={@form[:url]} label="Job Posting URL" /> +
+ +
+ <.button size="lg" class="flex items-center gap-2" phx-disable-with="Processing..."> + Post Job + +
+ +
+
+
+ +
+ """ + end + + @impl true + def handle_params(%{"status" => "paid"}, _uri, socket) do + {:noreply, put_flash(socket, :info, "Payment received, your job will go live shortly!")} + end + + @impl true + def handle_params(_params, _uri, socket) do + {:noreply, socket} + end + + @impl true + def handle_event("create_job", %{"job_posting" => params}, socket) do + case Jobs.create_job_posting(params) do + {:ok, job} -> + case Jobs.create_payment_session(job) do + {:ok, url} -> + {:noreply, redirect(socket, external: url)} + + {:error, reason} -> + Logger.error("Failed to create payment session: #{inspect(reason)}") + {:noreply, put_flash(socket, :error, "Something went wrong. Please try again.")} + end + + {:error, changeset} -> + Logger.error("Failed to create job posting: #{inspect(changeset)}") + {:noreply, assign(socket, :form, to_form(changeset))} + end + end +end diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex index 6f124aaaf..bc9e8661c 100644 --- a/lib/algora_web/router.ex +++ b/lib/algora_web/router.ex @@ -105,6 +105,7 @@ defmodule AlgoraWeb.Router do on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.User.Nav] do live "/bounties", BountiesLive, :index live "/bounties/:tech", BountiesLive, :index + live "/jobs", JobsLive, :index live "/community", CommunityLive, :index live "/leaderboard", LeaderboardLive, :index live "/projects", OrgsLive, :index diff --git a/priv/repo/migrations/20250424152036_create_job_postings.exs b/priv/repo/migrations/20250424152036_create_job_postings.exs new file mode 100644 index 000000000..3b2f9c061 --- /dev/null +++ b/priv/repo/migrations/20250424152036_create_job_postings.exs @@ -0,0 +1,28 @@ +defmodule Algora.Repo.Migrations.CreateJobPostings do + use Ecto.Migration + + def change do + create table(:job_postings, primary_key: false) do + add :id, :string, primary_key: true + add :title, :string + add :description, :text + add :tech_stack, {:array, :string}, default: [] + add :url, :string, null: false + add :company_name, :string, null: false + add :company_url, :string, null: false + add :email, :string, null: false + add :status, :string, null: false, default: "initialized" + add :expires_at, :utc_datetime_usec + add :user_id, references(:users, type: :string, on_delete: :restrict) + + timestamps() + end + + create index(:job_postings, [:user_id]) + create index(:job_postings, [:status]) + + alter table(:transactions) do + add :job_id, references(:job_postings, type: :string, on_delete: :restrict) + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index c74967a72..bf493241e 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -495,3 +495,30 @@ for {repo_name, issues} <- repos do end end end + +job_postings = [ + %{ + title: "Senior Backend Engineer", + description: "Help us scale our middle-out compression platform", + tech_stack: ["Python", "C++", "Rust"], + url: "https://piedpiper.com/careers/backend-engineer", + company_name: "Pied Piper", + company_url: "https://piedpiper.com", + email: "jobs@piedpiper.com", + user_id: pied_piper.id + }, + %{ + title: "Frontend Developer", + description: "Build beautiful interfaces for our compression platform", + tech_stack: ["JavaScript", "React", "TypeScript"], + url: "https://piedpiper.com/careers/frontend-developer", + company_name: "Pied Piper", + company_url: "https://piedpiper.com", + email: "jobs@piedpiper.com", + user_id: pied_piper.id + } +] + +for job_attrs <- job_postings do + insert!(:job_posting, Map.put(job_attrs, :status, :active)) +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 38f016f89..6ecf0080c 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -299,6 +299,21 @@ defmodule Algora.Factory do } end + def job_posting_factory do + %Algora.Jobs.JobPosting{ + id: Nanoid.generate(), + title: "Senior Software Engineer", + description: "Join our team to build the next generation of compression technology", + tech_stack: ["Python", "C++", "Rust"], + url: "https://piedpiper.com/careers/senior-engineer", + company_name: "Pied Piper", + company_url: "https://piedpiper.com", + email: "jobs@piedpiper.com", + status: :active, + expires_at: days_from_now(30) + } + end + # Convenience API def insert!(factory_name, attributes \\ []) From 4797e7abf877f63cb1806dd0a5379fe1b5df9365 Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 18:15:22 +0300 Subject: [PATCH 02/11] feat: job applications --- lib/algora/jobs/jobs.ex | 15 +++++++ lib/algora/jobs/schemas/job_application.ex | 26 +++++++++++ lib/algora/jobs/schemas/job_posting.ex | 2 +- lib/algora_web/live/jobs_live.ex | 43 ++++++++++++++++++- .../20250424152036_create_job_postings.exs | 2 +- ...20250424152037_create_job_applications.exs | 18 ++++++++ 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 lib/algora/jobs/schemas/job_application.ex create mode 100644 priv/repo/migrations/20250424152037_create_job_applications.exs diff --git a/lib/algora/jobs/jobs.ex b/lib/algora/jobs/jobs.ex index 3f2b5ded9..ac4c92aaf 100644 --- a/lib/algora/jobs/jobs.ex +++ b/lib/algora/jobs/jobs.ex @@ -6,6 +6,7 @@ defmodule Algora.Jobs do alias Algora.Accounts alias Algora.Bounties.LineItem + alias Algora.Jobs.JobApplication alias Algora.Jobs.JobPosting alias Algora.Payments alias Algora.Payments.Transaction @@ -92,4 +93,18 @@ defmodule Algora.Jobs do end end) end + + def create_application(job_id, user) do + %JobApplication{} + |> JobApplication.changeset(%{job_id: job_id, user_id: user.id}) + |> Repo.insert() + end + + def list_user_applications(user) do + JobApplication + |> where([a], a.user_id == ^user.id) + |> select([a], a.job_id) + |> Repo.all() + |> MapSet.new() + end end diff --git a/lib/algora/jobs/schemas/job_application.ex b/lib/algora/jobs/schemas/job_application.ex new file mode 100644 index 000000000..c5d8984d5 --- /dev/null +++ b/lib/algora/jobs/schemas/job_application.ex @@ -0,0 +1,26 @@ +defmodule Algora.Jobs.JobApplication do + @moduledoc false + use Algora.Schema + + alias Algora.Accounts.User + alias Algora.Jobs.JobPosting + + typed_schema "job_applications" do + field :status, Ecto.Enum, values: [:pending], null: false, default: :pending + + belongs_to :job, JobPosting, null: false + belongs_to :user, User, null: false + + timestamps() + end + + def changeset(job_application, attrs) do + job_application + |> cast(attrs, [:status, :job_id, :user_id]) + |> generate_id() + |> validate_required([:status, :job_id, :user_id]) + |> unique_constraint([:job_id, :user_id]) + |> foreign_key_constraint(:job_id) + |> foreign_key_constraint(:user_id) + end +end diff --git a/lib/algora/jobs/schemas/job_posting.ex b/lib/algora/jobs/schemas/job_posting.ex index 1e55ddd84..f35059b45 100644 --- a/lib/algora/jobs/schemas/job_posting.ex +++ b/lib/algora/jobs/schemas/job_posting.ex @@ -15,7 +15,7 @@ defmodule Algora.Jobs.JobPosting do field :status, Ecto.Enum, values: [:initialized, :processing, :active, :expired], null: false, default: :initialized field :expires_at, :utc_datetime_usec - belongs_to :user, User + belongs_to :user, User, null: false timestamps() end diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index f6aa877c8..092c0fd02 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -2,6 +2,7 @@ defmodule AlgoraWeb.JobsLive do @moduledoc false use AlgoraWeb, :live_view + alias Algora.Accounts alias Algora.Jobs alias Algora.Jobs.JobPosting @@ -16,7 +17,8 @@ defmodule AlgoraWeb.JobsLive do socket |> assign(:page_title, "Jobs") |> assign(:jobs, jobs) - |> assign(:form, to_form(changeset))} + |> assign(:form, to_form(changeset)) + |> assign_user_applications()} end @impl true @@ -64,6 +66,15 @@ defmodule AlgoraWeb.JobsLive do + <%= if MapSet.member?(@user_applications, job.id) do %> + <.button disabled class="opacity-50"> + <.icon name="tabler-check" class="h-4 w-4 mr-2 -ml-1" /> Applied + + <% else %> + <.button phx-click="apply_job" phx-value-job-id={job.id}> + <.icon name="github" class="h-4 w-4 mr-2" /> Apply with GitHub + + <% end %>
{job.description} @@ -142,4 +153,34 @@ defmodule AlgoraWeb.JobsLive do {:noreply, assign(socket, :form, to_form(changeset))} end end + + @impl true + def handle_event("apply_job", %{"job-id" => job_id}, socket) do + if socket.assigns[:current_user] do + if Accounts.has_fresh_token?(socket.assigns.current_user) do + case Jobs.create_application(job_id, socket.assigns.current_user) do + {:ok, _application} -> + {:noreply, assign_user_applications(socket)} + + {:error, _changeset} -> + {:noreply, put_flash(socket, :error, "Failed to submit application. Please try again.")} + end + else + {:noreply, redirect(socket, external: Algora.Github.authorize_url())} + end + else + {:noreply, redirect(socket, external: Algora.Github.authorize_url())} + end + end + + defp assign_user_applications(socket) do + user_applications = + if socket.assigns[:current_user] do + Jobs.list_user_applications(socket.assigns.current_user) + else + MapSet.new() + end + + assign(socket, :user_applications, user_applications) + end end diff --git a/priv/repo/migrations/20250424152036_create_job_postings.exs b/priv/repo/migrations/20250424152036_create_job_postings.exs index 3b2f9c061..b4cc1f3ac 100644 --- a/priv/repo/migrations/20250424152036_create_job_postings.exs +++ b/priv/repo/migrations/20250424152036_create_job_postings.exs @@ -13,7 +13,7 @@ defmodule Algora.Repo.Migrations.CreateJobPostings do add :email, :string, null: false add :status, :string, null: false, default: "initialized" add :expires_at, :utc_datetime_usec - add :user_id, references(:users, type: :string, on_delete: :restrict) + add :user_id, references(:users, type: :string, on_delete: :restrict), null: false timestamps() end diff --git a/priv/repo/migrations/20250424152037_create_job_applications.exs b/priv/repo/migrations/20250424152037_create_job_applications.exs new file mode 100644 index 000000000..01317d1ac --- /dev/null +++ b/priv/repo/migrations/20250424152037_create_job_applications.exs @@ -0,0 +1,18 @@ +defmodule Algora.Repo.Migrations.CreateJobApplications do + use Ecto.Migration + + def change do + create table(:job_applications, primary_key: false) do + add :id, :string, primary_key: true + add :status, :string, null: false, default: "pending" + add :job_id, references(:job_postings, type: :string, on_delete: :restrict), null: false + add :user_id, references(:users, type: :string, on_delete: :restrict), null: false + + timestamps() + end + + create index(:job_applications, [:job_id]) + create index(:job_applications, [:user_id]) + create unique_index(:job_applications, [:job_id, :user_id]) + end +end From 6b7cacce35b993d4a4ddd05298b77b3fc9ba34d2 Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 18:20:20 +0300 Subject: [PATCH 03/11] autofill website url from email --- lib/algora_web/live/jobs_live.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index 092c0fd02..b44d3530d 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -103,16 +103,21 @@ defmodule AlgoraWeb.JobsLive do
Reach thousands of developers looking for their next opportunity versed in your tech stack
- <.simple_form for={@form} phx-submit="create_job" class="mt-4 space-y-4"> + <.simple_form for={@form} phx-submit="create_job" class="mt-4 space-y-6">
- <.input field={@form[:email]} label="Email" /> + <.input + field={@form[:email]} + label="Email" + data-domain-target + phx-hook="DeriveDomain" + /> <.input field={@form[:company_name]} label="Company Name" /> - <.input field={@form[:company_url]} label="Company Website" /> + <.input field={@form[:company_url]} label="Company Website" data-domain-source /> <.input field={@form[:url]} label="Job Posting URL" />
- <.button size="lg" class="flex items-center gap-2" phx-disable-with="Processing..."> + <.button class="flex items-center gap-2" phx-disable-with="Processing..."> Post Job
From caf85865725e2623f0a6da9021bfc207f2507b39 Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 18:38:32 +0300 Subject: [PATCH 04/11] preview job --- lib/algora_web/live/jobs_live.ex | 57 ++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index b44d3530d..5a227c7f5 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -2,9 +2,12 @@ defmodule AlgoraWeb.JobsLive do @moduledoc false use AlgoraWeb, :live_view + import Ecto.Changeset + alias Algora.Accounts alias Algora.Jobs alias Algora.Jobs.JobPosting + alias Phoenix.LiveView.AsyncResult require Logger @@ -18,6 +21,7 @@ defmodule AlgoraWeb.JobsLive do |> assign(:page_title, "Jobs") |> assign(:jobs, jobs) |> assign(:form, to_form(changeset)) + |> assign(:user_metadata, AsyncResult.loading()) |> assign_user_applications()} end @@ -103,20 +107,43 @@ defmodule AlgoraWeb.JobsLive do
Reach thousands of developers looking for their next opportunity versed in your tech stack
- <.simple_form for={@form} phx-submit="create_job" class="mt-4 space-y-6"> + <.simple_form + for={@form} + phx-change="validate_job" + phx-submit="create_job" + class="mt-4 space-y-6" + >
<.input field={@form[:email]} label="Email" data-domain-target phx-hook="DeriveDomain" + phx-blur="email_changed" /> <.input field={@form[:company_name]} label="Company Name" /> <.input field={@form[:company_url]} label="Company Website" data-domain-source /> <.input field={@form[:url]} label="Job Posting URL" />
-
+
+
+
+ +
+
+ {get_change(@form.source, :company_name)} +
+
+ {get_change(@form.source, :company_url)} +
+
+
+
<.button class="flex items-center gap-2" phx-disable-with="Processing..."> Post Job @@ -140,6 +167,22 @@ defmodule AlgoraWeb.JobsLive do {:noreply, socket} end + def handle_event("email_changed", %{"value" => email}, socket) do + if socket.assigns.user_metadata.ok? do + {:noreply, socket} + else + {:noreply, + socket + |> start_async(:fetch_metadata, fn -> Algora.Crawler.fetch_user_metadata(email) end) + |> assign(:user_metadata, AsyncResult.loading())} + end + end + + @impl true + def handle_event("validate_job", %{"job_posting" => params}, socket) do + {:noreply, assign(socket, :form, to_form(JobPosting.changeset(socket.assigns.form.source, params)))} + end + @impl true def handle_event("create_job", %{"job_posting" => params}, socket) do case Jobs.create_job_posting(params) do @@ -178,6 +221,16 @@ defmodule AlgoraWeb.JobsLive do end end + @impl true + def handle_async(:fetch_metadata, {:ok, metadata}, socket) do + {:noreply, assign(socket, :user_metadata, AsyncResult.ok(socket.assigns.user_metadata, metadata))} + end + + @impl true + def handle_async(:fetch_metadata, {:exit, reason}, socket) do + {:noreply, assign(socket, :user_metadata, AsyncResult.failed(socket.assigns.user_metadata, reason))} + end + defp assign_user_applications(socket) do user_applications = if socket.assigns[:current_user] do From c882f940cf4de9d9dec387ec659dd004a7ae23a1 Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 18:50:54 +0300 Subject: [PATCH 05/11] store user return to in session --- assets/js/app.ts | 18 ++++++++++++++++++ .../controllers/store_session_controller.ex | 14 ++++++++++++++ lib/algora_web/live/jobs_live.ex | 10 ++++++++-- lib/algora_web/router.ex | 2 ++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 lib/algora_web/controllers/store_session_controller.ex diff --git a/assets/js/app.ts b/assets/js/app.ts index c25b14833..47de2a739 100644 --- a/assets/js/app.ts +++ b/assets/js/app.ts @@ -766,4 +766,22 @@ window.addEventListener("phx:open_popup", (e: CustomEvent) => { } }); +// Add event listener for storing session values +window.addEventListener("phx:store-session", (event) => { + const token = document + .querySelector('meta[name="csrf-token"]') + .getAttribute("content"); + + console.log(event.detail); + + fetch("/store-session", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, + body: JSON.stringify(event.detail), + }); +}); + export default Hooks; diff --git a/lib/algora_web/controllers/store_session_controller.ex b/lib/algora_web/controllers/store_session_controller.ex new file mode 100644 index 000000000..65439c48c --- /dev/null +++ b/lib/algora_web/controllers/store_session_controller.ex @@ -0,0 +1,14 @@ +defmodule AlgoraWeb.StoreSessionController do + use AlgoraWeb, :controller + + def create(conn, params) do + dbg(params) + + updated_conn = + Enum.reduce(params, conn, fn {key, value}, acc_conn -> + put_session(acc_conn, String.to_existing_atom(key), value) + end) + + send_resp(updated_conn, 200, "") + end +end diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index 5a227c7f5..728f5ffac 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -214,10 +214,16 @@ defmodule AlgoraWeb.JobsLive do {:noreply, put_flash(socket, :error, "Failed to submit application. Please try again.")} end else - {:noreply, redirect(socket, external: Algora.Github.authorize_url())} + {:noreply, + socket + |> push_event("store-session", %{user_return_to: "/jobs"}) + |> redirect(external: Algora.Github.authorize_url())} end else - {:noreply, redirect(socket, external: Algora.Github.authorize_url())} + {:noreply, + socket + |> push_event("store-session", %{user_return_to: "/jobs"}) + |> redirect(external: Algora.Github.authorize_url())} end end diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex index bc9e8661c..dbcd2c412 100644 --- a/lib/algora_web/router.ex +++ b/lib/algora_web/router.ex @@ -145,6 +145,8 @@ defmodule AlgoraWeb.Router do live "/0/bounties/:id", OG.BountyLive, :show get "/og/*path", OGImageController, :generate + + post "/store-session", StoreSessionController, :create end scope "/api", AlgoraWeb.API do From c78796abdb08032912971ce776a445aba0fd5f0a Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 18:55:26 +0300 Subject: [PATCH 06/11] group jobs from same user --- lib/algora_web/live/jobs_live.ex | 96 ++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index 728f5ffac..d1cad1b09 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -13,13 +13,14 @@ defmodule AlgoraWeb.JobsLive do @impl true def mount(_params, _session, socket) do - jobs = Jobs.list_jobs() + # Group jobs by user + jobs_by_user = Enum.group_by(Jobs.list_jobs(), & &1.user) changeset = JobPosting.changeset(%JobPosting{}, %{}) {:ok, socket |> assign(:page_title, "Jobs") - |> assign(:jobs, jobs) + |> assign(:jobs_by_user, jobs_by_user) |> assign(:form, to_form(changeset)) |> assign(:user_metadata, AsyncResult.loading()) |> assign_user_applications()} @@ -30,7 +31,7 @@ defmodule AlgoraWeb.JobsLive do ~H"""
<.section title="Jobs" subtitle="Open positions at top companies"> - <%= if Enum.empty?(@jobs) do %> + <%= if Enum.empty?(@jobs_by_user) do %> <.card class="rounded-lg bg-card py-12 text-center lg:rounded-[2rem]"> <.card_header>
@@ -43,49 +44,58 @@ defmodule AlgoraWeb.JobsLive do <% else %> -
- <%= for job <- @jobs do %> - <.card class="flex flex-col gap-4 p-6"> -
-
- <.avatar class="h-12 w-12"> - <.avatar_image src={job.user.avatar_url} /> - <.avatar_fallback> - {Algora.Util.initials(job.user.name)} - - -
- <.link - href={job.url} - class="text-lg font-semibold hover:underline" - target="_blank" - > - {job.title} - -
- {job.company_name} • - <.link href={job.company_url} rel="noopener" target="_blank"> - {job.company_url |> String.replace("https://", "")} - -
+
+ <%= for {user, jobs} <- @jobs_by_user do %> + <.card class="flex flex-col gap-6 p-6"> +
+ <.avatar class="h-12 w-12"> + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback> + {Algora.Util.initials(user.name)} + + +
+
+ {user.name} +
+
+ {user.bio}
- <%= if MapSet.member?(@user_applications, job.id) do %> - <.button disabled class="opacity-50"> - <.icon name="tabler-check" class="h-4 w-4 mr-2 -ml-1" /> Applied - - <% else %> - <.button phx-click="apply_job" phx-value-job-id={job.id}> - <.icon name="github" class="h-4 w-4 mr-2" /> Apply with GitHub - - <% end %> -
-
- {job.description}
-
- <%= for tech <- job.tech_stack do %> - <.badge variant="outline">{tech} + +
+ <%= for job <- jobs do %> +
+
+
+ <.link + href={job.url} + class="text-lg font-semibold hover:underline" + target="_blank" + > + {job.title} + +
+ <%= if MapSet.member?(@user_applications, job.id) do %> + <.button disabled class="opacity-50"> + <.icon name="tabler-check" class="h-4 w-4 mr-2 -ml-1" /> Applied + + <% else %> + <.button phx-click="apply_job" phx-value-job-id={job.id}> + <.icon name="github" class="h-4 w-4 mr-2" /> Apply with GitHub + + <% end %> +
+
+ {job.description} +
+
+ <%= for tech <- job.tech_stack do %> + <.badge variant="outline">{tech} + <% end %> +
+
<% end %>
From bf90cf6ea1457e1b2bf2ff2b89b15105a903571f Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 19:19:48 +0300 Subject: [PATCH 07/11] update layout --- lib/algora_web/components/core_components.ex | 3 +- lib/algora_web/live/jobs_live.ex | 266 ++++++++++--------- 2 files changed, 148 insertions(+), 121 deletions(-) diff --git a/lib/algora_web/components/core_components.ex b/lib/algora_web/components/core_components.ex index 39318881b..3538dd97c 100644 --- a/lib/algora_web/components/core_components.ex +++ b/lib/algora_web/components/core_components.ex @@ -1236,11 +1236,12 @@ defmodule AlgoraWeb.CoreComponents do attr :title, :string, default: nil attr :subtitle, :string, default: nil attr :link, :string, default: nil + attr :class, :string, default: nil slot :inner_block def section(assigns) do ~H""" -
+

{@title}

diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index d1cad1b09..95a713573 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -29,140 +29,166 @@ defmodule AlgoraWeb.JobsLive do @impl true def render(assigns) do ~H""" -
- <.section title="Jobs" subtitle="Open positions at top companies"> - <%= if Enum.empty?(@jobs_by_user) do %> - <.card class="rounded-lg bg-card py-12 text-center lg:rounded-[2rem]"> - <.card_header> -
- <.icon name="tabler-briefcase" class="h-8 w-8 text-muted-foreground" /> -
- <.card_title>No jobs yet - <.card_description> - Open positions will appear here once created - - - - <% else %> -
- <%= for {user, jobs} <- @jobs_by_user do %> - <.card class="flex flex-col gap-6 p-6"> -
- <.avatar class="h-12 w-12"> - <.avatar_image src={user.avatar_url} /> - <.avatar_fallback> - {Algora.Util.initials(user.name)} - - -
-
- {user.name} -
-
- {user.bio} -
-
-
+
+
+
+
+

+ Jobs +

+

+ Open positions at top companies +

+
-
- <%= for job <- jobs do %> -
-
-
- <.link - href={job.url} - class="text-lg font-semibold hover:underline" - target="_blank" - > - {job.title} - -
- <%= if MapSet.member?(@user_applications, job.id) do %> - <.button disabled class="opacity-50"> - <.icon name="tabler-check" class="h-4 w-4 mr-2 -ml-1" /> Applied - - <% else %> - <.button phx-click="apply_job" phx-value-job-id={job.id}> - <.icon name="github" class="h-4 w-4 mr-2" /> Apply with GitHub - - <% end %> + <.section class="pt-8"> + <%= if Enum.empty?(@jobs_by_user) do %> + <.card class="rounded-lg bg-card py-12 text-center lg:rounded-[2rem]"> + <.card_header> +
+ <.icon name="tabler-briefcase" class="h-8 w-8 text-muted-foreground" /> +
+ <.card_title>No jobs yet + <.card_description> + Open positions will appear here once created + + + + <% else %> +
+ <%= for {user, jobs} <- @jobs_by_user do %> + <.card class="flex flex-col gap-6 p-6"> +
+ <.avatar class="h-12 w-12"> + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback> + {Algora.Util.initials(user.name)} + + +
+
+ {user.name}
- {job.description} -
-
- <%= for tech <- job.tech_stack do %> - <.badge variant="outline">{tech} - <% end %> + {user.bio}
- <% end %> -
- - <% end %> -
- <% end %> - - - <.section> -
-
-
-
- Post your job
- - in seconds - -
-
- Reach thousands of developers looking for their next opportunity versed in your tech stack -
- <.simple_form - for={@form} - phx-change="validate_job" - phx-submit="create_job" - class="mt-4 space-y-6" - > -
- <.input - field={@form[:email]} - label="Email" - data-domain-target - phx-hook="DeriveDomain" - phx-blur="email_changed" - /> - <.input field={@form[:company_name]} label="Company Name" /> - <.input field={@form[:company_url]} label="Company Website" data-domain-source /> - <.input field={@form[:url]} label="Job Posting URL" /> -
+
-
-
-
- -
-
- {get_change(@form.source, :company_name)} +
+ <%= for job <- jobs do %> +
+
+
+ <.link + href={job.url} + class="text-lg font-semibold hover:underline" + target="_blank" + > + {job.title} + +
+ <%= if MapSet.member?(@user_applications, job.id) do %> + <.button disabled class="opacity-50"> + <.icon name="tabler-check" class="h-4 w-4 mr-2 -ml-1" /> Applied + + <% else %> + <.button phx-click="apply_job" phx-value-job-id={job.id}> + <.icon name="github" class="h-4 w-4 mr-2" /> Apply with GitHub + + <% end %>
- {get_change(@form.source, :company_url)} + {job.description} +
+
+ <%= for tech <- job.tech_stack do %> + <.badge variant="outline">{tech} + <% end %>
-
+ <% end %>
- <.button class="flex items-center gap-2" phx-disable-with="Processing..."> - Post Job - + + <% end %> +
+ <% end %> + + + <.section class="pt-12"> +
+
+
+
+ Post your job
+ + in seconds + +
+
+ Reach thousands of developers looking for their next opportunity versed in your tech stack
- + <.simple_form + for={@form} + phx-change="validate_job" + phx-submit="create_job" + class="mt-4 space-y-6" + > +
+ <.input + field={@form[:email]} + label="Email" + data-domain-target + phx-hook="DeriveDomain" + phx-blur="email_changed" + /> + <.input field={@form[:company_name]} label="Company Name" /> + <.input field={@form[:company_url]} label="Company Website" data-domain-source /> + <.input field={@form[:url]} label="Job Posting URL" /> +
+ +
+
+
+ +
+
+ {get_change(@form.source, :company_name)} +
+
+ {get_change(@form.source, :company_url)} +
+
+
+
+ <.button class="flex items-center gap-2" phx-disable-with="Processing..."> + Post Job + +
+ +
-
- + +
""" end From c2786bca51607e47075ac760e7680c3aba231211 Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 19:21:33 +0300 Subject: [PATCH 08/11] fix alignment --- lib/algora_web/live/jobs_live.ex | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index 95a713573..a6733a847 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -91,8 +91,8 @@ defmodule AlgoraWeb.JobsLive do
<%= for job <- jobs do %> -
-
+
+
<.link href={job.url} @@ -102,24 +102,24 @@ defmodule AlgoraWeb.JobsLive do {job.title}
- <%= if MapSet.member?(@user_applications, job.id) do %> - <.button disabled class="opacity-50"> - <.icon name="tabler-check" class="h-4 w-4 mr-2 -ml-1" /> Applied - - <% else %> - <.button phx-click="apply_job" phx-value-job-id={job.id}> - <.icon name="github" class="h-4 w-4 mr-2" /> Apply with GitHub - - <% end %> -
-
- {job.description} -
-
- <%= for tech <- job.tech_stack do %> - <.badge variant="outline">{tech} - <% end %> +
+ {job.description} +
+
+ <%= for tech <- job.tech_stack do %> + <.badge variant="outline">{tech} + <% end %> +
+ <%= if MapSet.member?(@user_applications, job.id) do %> + <.button disabled class="opacity-50"> + <.icon name="tabler-check" class="h-4 w-4 mr-2 -ml-1" /> Applied + + <% else %> + <.button phx-click="apply_job" phx-value-job-id={job.id}> + <.icon name="github" class="h-4 w-4 mr-2" /> Apply with GitHub + + <% end %>
<% end %>
From 77e71c72b0dd9a5c97ae7d7b8af1622a912faa7b Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 20:26:49 +0300 Subject: [PATCH 09/11] minor improvements --- lib/algora_web/live/jobs_live.ex | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index a6733a847..d0ca5b0df 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -71,7 +71,7 @@ defmodule AlgoraWeb.JobsLive do <% else %>
<%= for {user, jobs} <- @jobs_by_user do %> - <.card class="flex flex-col gap-6 p-6"> + <.card class="flex flex-col p-6">
<.avatar class="h-12 w-12"> <.avatar_image src={user.avatar_url} /> @@ -89,10 +89,10 @@ defmodule AlgoraWeb.JobsLive do
-
+
<%= for job <- jobs do %>
-
+
<.link href={job.url} @@ -102,10 +102,10 @@ defmodule AlgoraWeb.JobsLive do {job.title}
-
+
{job.description}
-
+
<%= for tech <- job.tech_stack do %> <.badge variant="outline">{tech} <% end %> @@ -163,7 +163,12 @@ defmodule AlgoraWeb.JobsLive do
-
+
email}, socket) do - if socket.assigns.user_metadata.ok? do - {:noreply, socket} - else - {:noreply, - socket - |> start_async(:fetch_metadata, fn -> Algora.Crawler.fetch_user_metadata(email) end) - |> assign(:user_metadata, AsyncResult.loading())} - end + {:noreply, + socket + |> start_async(:fetch_metadata, fn -> Algora.Crawler.fetch_user_metadata(email) end) + |> assign(:user_metadata, AsyncResult.loading())} end @impl true From 41fca0e275d1d3b09757aa21d866085dff6a1c23 Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 20:41:44 +0300 Subject: [PATCH 10/11] misc improvements --- lib/algora/jobs/schemas/job_posting.ex | 8 ++++---- lib/algora_web/live/jobs_live.ex | 15 +++++++++++++-- .../20250424152036_create_job_postings.exs | 8 ++++---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/algora/jobs/schemas/job_posting.ex b/lib/algora/jobs/schemas/job_posting.ex index f35059b45..00442e9c0 100644 --- a/lib/algora/jobs/schemas/job_posting.ex +++ b/lib/algora/jobs/schemas/job_posting.ex @@ -8,10 +8,10 @@ defmodule Algora.Jobs.JobPosting do field :title, :string field :description, :string field :tech_stack, {:array, :string}, default: [] - field :url, :string, null: false - field :company_name, :string, null: false - field :company_url, :string, null: false - field :email, :string, null: false + field :url, :string + field :company_name, :string + field :company_url, :string + field :email, :string field :status, Ecto.Enum, values: [:initialized, :processing, :active, :expired], null: false, default: :initialized field :expires_at, :utc_datetime_usec diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index d0ca5b0df..073e0a832 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -156,9 +156,9 @@ defmodule AlgoraWeb.JobsLive do phx-hook="DeriveDomain" phx-blur="email_changed" /> - <.input field={@form[:company_name]} label="Company Name" /> - <.input field={@form[:company_url]} label="Company Website" data-domain-source /> <.input field={@form[:url]} label="Job Posting URL" /> + <.input field={@form[:company_url]} label="Company URL" data-domain-source /> + <.input field={@form[:company_name]} label="Company Name" />
@@ -266,6 +266,17 @@ defmodule AlgoraWeb.JobsLive do @impl true def handle_async(:fetch_metadata, {:ok, metadata}, socket) do + socket = + case get_change(socket.assigns.form.source, :company_name) do + nil -> + assign(socket, + form: to_form(change(socket.assigns.form.source, company_name: get_in(metadata, [:org, :og_title]))) + ) + + _company_name -> + socket + end + {:noreply, assign(socket, :user_metadata, AsyncResult.ok(socket.assigns.user_metadata, metadata))} end diff --git a/priv/repo/migrations/20250424152036_create_job_postings.exs b/priv/repo/migrations/20250424152036_create_job_postings.exs index b4cc1f3ac..cfba36183 100644 --- a/priv/repo/migrations/20250424152036_create_job_postings.exs +++ b/priv/repo/migrations/20250424152036_create_job_postings.exs @@ -7,10 +7,10 @@ defmodule Algora.Repo.Migrations.CreateJobPostings do add :title, :string add :description, :text add :tech_stack, {:array, :string}, default: [] - add :url, :string, null: false - add :company_name, :string, null: false - add :company_url, :string, null: false - add :email, :string, null: false + add :url, :string + add :company_name, :string + add :company_url, :string + add :email, :string add :status, :string, null: false, default: "initialized" add :expires_at, :utc_datetime_usec add :user_id, references(:users, type: :string, on_delete: :restrict), null: false From 1cf0c5ac637d8d487ba5af73f39ac2942bb5dcfa Mon Sep 17 00:00:00 2001 From: zafer Date: Thu, 24 Apr 2025 20:55:35 +0300 Subject: [PATCH 11/11] misc improvements --- lib/algora_web/live/jobs_live.ex | 92 +++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/lib/algora_web/live/jobs_live.ex b/lib/algora_web/live/jobs_live.ex index 073e0a832..ae04144af 100644 --- a/lib/algora_web/live/jobs_live.ex +++ b/lib/algora_web/live/jobs_live.ex @@ -73,19 +73,32 @@ defmodule AlgoraWeb.JobsLive do <%= for {user, jobs} <- @jobs_by_user do %> <.card class="flex flex-col p-6">
- <.avatar class="h-12 w-12"> + <.avatar class="h-16 w-16"> <.avatar_image src={user.avatar_url} /> <.avatar_fallback> {Algora.Util.initials(user.name)}
-
+
{user.name}
-
+
{user.bio}
+
+ <%= for {platform, icon} <- social_icons(), + url = social_link(user, platform), + not is_nil(url) do %> + <.link + href={url} + target="_blank" + class="text-muted-foreground hover:text-foreground" + > + <.icon name={icon} class="size-4" /> + + <% end %> +
@@ -161,25 +174,48 @@ defmodule AlgoraWeb.JobsLive do <.input field={@form[:company_name]} label="Company Name" />
-
+
- + <%= if logo = get_in(@user_metadata.result, [:org, :favicon_url]) do %> + + <% end %>
{get_change(@form.source, :company_name)}
-
- {get_change(@form.source, :company_url)} + <%= if description = get_in(@user_metadata.result, [:org, :og_description]) do %> +
+ {description} +
+ <% end %> +
+ <%= if url = get_change(@form.source, :company_url) do %> + <.link + href={url} + target="_blank" + class="text-muted-foreground hover:text-foreground" + > + <.icon name="tabler-world" class="size-4" /> + + <% end %> + + <%= for {platform, icon} <- social_icons(), + url = get_in(@user_metadata.result, [:org, :socials, platform]), + not is_nil(url) do %> + <.link + href={url} + target="_blank" + class="text-muted-foreground hover:text-foreground" + > + <.icon name={icon} class="size-4" /> + + <% end %>
@@ -266,18 +302,10 @@ defmodule AlgoraWeb.JobsLive do @impl true def handle_async(:fetch_metadata, {:ok, metadata}, socket) do - socket = - case get_change(socket.assigns.form.source, :company_name) do - nil -> - assign(socket, - form: to_form(change(socket.assigns.form.source, company_name: get_in(metadata, [:org, :og_title]))) - ) - - _company_name -> - socket - end - - {:noreply, assign(socket, :user_metadata, AsyncResult.ok(socket.assigns.user_metadata, metadata))} + {:noreply, + socket + |> assign(:user_metadata, AsyncResult.ok(socket.assigns.user_metadata, metadata)) + |> assign(:form, to_form(change(socket.assigns.form.source, company_name: get_in(metadata, [:org, :og_title]))))} end @impl true @@ -295,4 +323,20 @@ defmodule AlgoraWeb.JobsLive do assign(socket, :user_applications, user_applications) end + + defp social_icons do + %{ + website: "tabler-world", + github: "github", + twitter: "tabler-brand-x", + youtube: "tabler-brand-youtube", + twitch: "tabler-brand-twitch", + discord: "tabler-brand-discord", + slack: "tabler-brand-slack", + linkedin: "tabler-brand-linkedin" + } + end + + defp social_link(user, :github), do: if(login = user.provider_login, do: "https://github.com/#{login}") + defp social_link(user, platform), do: Map.get(user, :"#{platform}_url") end