diff --git a/assets/js/app.ts b/assets/js/app.ts index 5bbffd165..cbece913b 100644 --- a/assets/js/app.ts +++ b/assets/js/app.ts @@ -635,6 +635,167 @@ const Hooks = { }); }, }, + CompensationStrengthIndicator: { + mounted() { + const input = this.el.querySelector("input[type='text']"); + const strengthBar = this.el.querySelector("[data-strength-bar]"); + const strengthLabel = this.el.querySelector("[data-strength-label]"); + + if (!input || !strengthBar || !strengthLabel) return; + + const minAmount = 50000; + + const expandShorthand = (value: string): string => { + const trimmed = value.trim().toLowerCase(); + + // Handle 'k' for thousands (e.g., "100k" -> "100000") + if (trimmed.endsWith("k")) { + const number = parseFloat(trimmed.slice(0, -1)); + if (!isNaN(number)) { + return Math.floor(number * 1000).toString(); + } + } + + // Handle 'm' for millions (e.g., "1m" -> "1000000") + if (trimmed.endsWith("m")) { + const number = parseFloat(trimmed.slice(0, -1)); + if (!isNaN(number)) { + return Math.floor(number * 1000000).toString(); + } + } + + // Return just the digits if no shorthand + return value.replace(/[^0-9]/g, ""); + }; + + const formatWithCommas = (value: string): string => { + // First expand any shorthand notation + const expanded = expandShorthand(value); + // Add commas for thousands separators + return expanded.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + }; + + const updateStrength = () => { + const value = expandShorthand(input.value); + const amount = parseInt(value) || 0; + + let strength = 0; + let label = ""; + let color = "bg-gray-200"; + + if (amount >= 500000) { + strength = 99; + label = "Big D Energy"; + color = "bg-emerald-500"; + } else if (amount >= 400000) { + strength = 90; + label = "Baller Status"; + color = "bg-emerald-500"; + } else if (amount >= 300000) { + strength = 80; + label = "High Roller"; + color = "bg-emerald-500"; + } else if (amount >= 200000) { + strength = 70; + label = "Big League"; + color = "bg-emerald-500"; + } else if (amount >= 150000) { + strength = 60; + label = "Major League"; + color = "bg-emerald-500"; + } else if (amount >= 100000) { + strength = 50; + label = "Six Figures"; + color = "bg-emerald-500"; + } else if (amount >= 75000) { + strength = 40; + label = "Solid Pay"; + color = "bg-emerald-500"; + } else if (amount >= minAmount) { + strength = 30; + label = "Decent"; + color = "bg-emerald-500"; + } + + // Update strength bar + strengthBar.style.width = `${strength}%`; + strengthBar.className = `h-2 rounded-full transition-all duration-300 ${color}`; + + // Show/hide the entire indicator section + const indicatorSection = strengthBar.closest(".mt-2"); + if (amount >= minAmount) { + indicatorSection.style.display = "block"; + } else { + indicatorSection.style.display = "none"; + } + + // Update label + strengthLabel.textContent = label; + strengthLabel.className = `text-sm font-medium transition-colors duration-300 ${ + strength >= 80 + ? "text-emerald-500" + : strength >= 60 + ? "text-emerald-500" + : strength >= 40 + ? "text-emerald-500" + : strength >= 20 + ? "text-emerald-500" + : "text-gray-600" + }`; + }; + + const handleInput = (e: Event) => { + const target = e.target as HTMLInputElement; + const cursorPosition = target.selectionStart || 0; + const oldValue = target.value; + + // Check if user just typed 'k' or 'm' to trigger expansion + const shouldExpand = + oldValue.toLowerCase().endsWith("k") || + oldValue.toLowerCase().endsWith("m"); + + let formattedValue: string; + let newCursorPosition = cursorPosition; + + if (shouldExpand) { + // Expand shorthand and format with commas + formattedValue = formatWithCommas(oldValue); + // Place cursor at the end after expansion + newCursorPosition = formattedValue.length; + } else { + // Just format with commas, preserving user input + const digitsOnly = oldValue.replace(/[^0-9]/g, ""); + formattedValue = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + + // Adjust cursor position to account for added/removed commas + const oldCommas = (oldValue.match(/,/g) || []).length; + const newCommas = (formattedValue.match(/,/g) || []).length; + newCursorPosition = cursorPosition + (newCommas - oldCommas); + } + + // Only update if the value changed to prevent cursor jumping + if (oldValue !== formattedValue) { + target.value = formattedValue; + + // Set cursor position after the DOM updates + setTimeout(() => { + target.setSelectionRange(newCursorPosition, newCursorPosition); + }, 0); + } + + updateStrength(); + }; + + input.addEventListener("input", handleInput); + input.addEventListener("keyup", updateStrength); + + // Initial formatting and update + if (input.value) { + input.value = formatWithCommas(input.value); + } + updateStrength(); + }, + }, } satisfies Record & Record>; // Accessible focus handling diff --git a/lib/algora/accounts/schemas/user.ex b/lib/algora/accounts/schemas/user.ex index 238873169..15a827c62 100644 --- a/lib/algora/accounts/schemas/user.ex +++ b/lib/algora/accounts/schemas/user.ex @@ -25,6 +25,8 @@ defmodule Algora.Accounts.User do field :type, Ecto.Enum, values: [:individual, :organization, :bot], default: :individual field :email, :string + field :internal_email, :string + field :internal_notes, :string field :name, :string field :display_name, :string field :handle, :string @@ -62,6 +64,13 @@ defmodule Algora.Accounts.User do field :min_compensation, Money field :willing_to_relocate, :boolean, default: false field :us_work_authorization, :boolean, default: false + field :preferences, :string + + field :refer_to_company, :boolean, default: false + field :company_domain, :string + field :friends_recommendations, :boolean, default: false + field :friends_github_handles, :string + field :opt_out_algora, :boolean, default: false field :total_earned, Money, virtual: true field :transactions_count, :integer, virtual: true @@ -402,10 +411,22 @@ defmodule Algora.Accounts.User do :us_work_authorization, :linkedin_url, :twitter_url, - :location + :youtube_url, + :website_url, + :location, + :preferences, + :internal_email, + :internal_notes, + :refer_to_company, + :company_domain, + :friends_recommendations, + :friends_github_handles, + :opt_out_algora ]) |> validate_url(:linkedin_url) |> validate_url(:twitter_url) + |> validate_url(:youtube_url) + |> validate_url(:website_url) end defp validate_url(changeset, field) do diff --git a/lib/algora/application.ex b/lib/algora/application.ex index 409b4f73d..3f9a43d77 100644 --- a/lib/algora/application.ex +++ b/lib/algora/application.ex @@ -23,6 +23,7 @@ defmodule Algora.Application do Algora.Github.Poller.RootSupervisor, Algora.ScreenshotQueue, Algora.RateLimit, + AlgoraWeb.Data.HomeCache, # Start to serve requests, typically the last entry AlgoraWeb.Endpoint, Algora.Stargazer, diff --git a/lib/algora/mailer.ex b/lib/algora/mailer.ex index 9eaf638e3..ac8d7fe9f 100644 --- a/lib/algora/mailer.ex +++ b/lib/algora/mailer.ex @@ -37,12 +37,7 @@ defmodule Algora.Mailer do - -
- ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ -
+ #{preheader_section(opts[:preheader])}
@@ -123,4 +118,16 @@ defmodule Algora.Mailer do defp html_section_by_type(_, text) do text end + + defp preheader_section(nil), do: "" + + defp preheader_section(preheader), + do: """ + +
+ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ ͏ ‌     ­ +
+ """ end diff --git a/lib/algora/psp/connect_countries.ex b/lib/algora/psp/connect_countries.ex index d644ebd7e..3293722fe 100644 --- a/lib/algora/psp/connect_countries.ex +++ b/lib/algora/psp/connect_countries.ex @@ -136,6 +136,15 @@ defmodule Algora.PSP.ConnectCountries do end end + def abbr_from_code("US"), do: "US" + + def abbr_from_code(code) do + case Enum.find(list(), &(elem(&1, 1) == code)) do + nil -> code + {name, _} -> name + end + end + @spec list_codes() :: [String.t()] def list_codes, do: Enum.map(list(), &elem(&1, 1)) diff --git a/lib/algora/settings/settings.ex b/lib/algora/settings/settings.ex index e5c18059b..26bdcd68c 100644 --- a/lib/algora/settings/settings.ex +++ b/lib/algora/settings/settings.ex @@ -106,44 +106,69 @@ defmodule Algora.Settings do opts = Keyword.put_new(opts, :limit, 1000) case get("job_matches:#{job.id}") do + %{"matches_2" => matches} when is_list(matches) -> + matches + |> Enum.map(fn %{"user_id" => id} -> %{user_id: id} end) + |> load_matches_2() + %{"matches" => matches} when is_list(matches) -> matches |> load_matches() |> Enum.take(opts[:limit]) _ -> - [ - tech_stack: job.tech_stack, - email_required: false, - sort_by: - case get_job_criteria(job) do - criteria when map_size(criteria) > 0 -> criteria - _ -> [{"solver", true}] - end - ] - |> Keyword.merge(opts) - |> Algora.Cloud.list_top_matches() - |> load_matches_2() + matches = + [ + tech_stack: job.tech_stack, + email_required: false, + sort_by: + case get_job_criteria(job) do + criteria when map_size(criteria) > 0 -> criteria + _ -> [{"solver", true}] + end + ] + |> Keyword.merge(opts) + |> Algora.Cloud.list_top_matches() + + # Cache the raw matches for future calls + _count = get_job_matches_count(job, opts) + set_job_matches_2(job.id, matches) + + load_matches_2(matches) end end + def set_job_matches_count(job_id, count) when is_binary(job_id) and is_integer(count) do + set("job_matches_count:#{job_id}", %{"count" => count}) + end + def get_job_matches_count(job, opts \\ []) do - case get("job_matches:#{job.id}") do - %{"matches" => matches} when is_list(matches) -> - length(matches) + case get("job_matches_count:#{job.id}") do + %{"count" => count} when is_integer(count) -> + count _ -> - [ - tech_stack: job.tech_stack, - email_required: false, - sort_by: - case get_job_criteria(job) do - criteria when map_size(criteria) > 0 -> criteria - _ -> [{"solver", true}] - end - ] - |> Keyword.merge(opts) - |> Algora.Cloud.count_top_matches() + count = + case get("job_matches:#{job.id}") do + %{"matches" => matches} when is_list(matches) -> + length(matches) + + _ -> + [ + tech_stack: job.tech_stack, + email_required: false, + sort_by: + case get_job_criteria(job) do + criteria when map_size(criteria) > 0 -> criteria + _ -> [{"solver", true}] + end + ] + |> Keyword.merge(opts) + |> Algora.Cloud.count_top_matches() + end + + set_job_matches_count(job.id, count) + count end end @@ -182,6 +207,10 @@ defmodule Algora.Settings do set("job_matches:#{job_id}", %{"matches" => matches}) end + def set_job_matches_2(job_id, matches) when is_binary(job_id) and is_list(matches) do + set("job_matches:#{job_id}", %{"matches_2" => matches}) + end + def get_tech_matches(tech) do case get("tech_matches:#{String.downcase(tech)}") do %{"matches" => matches} when is_list(matches) -> load_matches(matches) diff --git a/lib/algora/workspace/workspace.ex b/lib/algora/workspace/workspace.ex index 48b18ee75..758b47e72 100644 --- a/lib/algora/workspace/workspace.ex +++ b/lib/algora/workspace/workspace.ex @@ -653,13 +653,13 @@ defmodule Algora.Workspace do join: u in assoc(uc, :user), join: r in assoc(uc, :repository), join: repo_owner in assoc(r, :user), - where: repo_owner.type == :organization or r.stargazers_count > 200, # where: fragment("? && ?::citext[]", r.tech_stack, ^(opts[:tech_stack] || [])), where: not (ilike(r.name, "%awesome%") or ilike(r.name, "%algorithms%") or ilike(r.name, "%exercises%") or ilike(r.name, "%tutorials%") or + r.name == "DefinitelyTyped" or r.name == "developer-roadmap"), where: not (ilike(repo_owner.provider_login, "%algorithms%") or @@ -681,6 +681,13 @@ defmodule Algora.Workspace do } } + query = + if opts[:display_all] do + query + else + where(query, [uc, u, r, repo_owner], repo_owner.type == :organization or r.stargazers_count > 200) + end + query = if opts[:exclude_personal] do where(query, [uc, u, r, repo_owner], repo_owner.type == :organization) @@ -690,7 +697,11 @@ defmodule Algora.Workspace do query = if strict_tech_stack do - where(query, [uc, u, r, repo_owner], fragment("? && ?::citext[]", r.tech_stack, ^tech_stack)) + where( + query, + [uc, u, r, repo_owner], + fragment("(SELECT ARRAY(SELECT unnest(?) LIMIT 1)) && ?::citext[]", r.tech_stack, ^tech_stack) + ) else query end diff --git a/lib/algora_web/components/core_components.ex b/lib/algora_web/components/core_components.ex index 90fde92fe..6da5cfd24 100644 --- a/lib/algora_web/components/core_components.ex +++ b/lib/algora_web/components/core_components.ex @@ -1366,6 +1366,7 @@ defmodule AlgoraWeb.CoreComponents do defdelegate menu_separator(assigns), to: Menu defdelegate menu_shortcut(assigns), to: Menu defdelegate menu(assigns), to: Menu + defdelegate multiline(assigns), to: AlgoraWeb.Components.UI.Multiline defdelegate popover_content(assigns), to: Popover defdelegate popover_trigger(assigns), to: Popover defdelegate popover(assigns), to: Popover diff --git a/lib/algora_web/components/footer.ex b/lib/algora_web/components/footer.ex index 080ac336c..05c99dac6 100644 --- a/lib/algora_web/components/footer.ex +++ b/lib/algora_web/components/footer.ex @@ -118,14 +118,6 @@ defmodule AlgoraWeb.Components.Footer do SDK -
  • - <.link - class="text-base font-medium leading-6 text-gray-400 hover:text-white" - navigate={~p"/pricing"} - > - Pricing - -
  • @@ -173,9 +165,9 @@ defmodule AlgoraWeb.Components.Footer do # "mt-16 sm:mt-20 lg:mt-24" ]) }> -
    +
    <.link - class="rounded-xl border-2 border-gray-500 p-3 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" + class="rounded-xl border-2 border-gray-500 p-2 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" href={Constants.get(:discord_url)} rel="noopener" target="_blank" @@ -184,7 +176,7 @@ defmodule AlgoraWeb.Components.Footer do <.icon name="tabler-brand-discord-filled" class="h-6 w-6 sm:h-8 sm:w-8" /> <.link - class="rounded-xl border-2 border-gray-500 p-3 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" + class="rounded-xl border-2 border-gray-500 p-2 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" href={Constants.get(:twitter_url)} rel="noopener" target="_blank" @@ -193,7 +185,7 @@ defmodule AlgoraWeb.Components.Footer do <.icon name="tabler-brand-x" class="h-6 w-6 sm:h-8 sm:w-8" /> <.link - class="rounded-xl border-2 border-gray-500 p-3 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" + class="rounded-xl border-2 border-gray-500 p-2 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" href={Constants.get(:github_url)} rel="noopener" target="_blank" @@ -202,7 +194,16 @@ defmodule AlgoraWeb.Components.Footer do <.link - class="rounded-xl border-2 border-gray-500 p-3 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" + class="rounded-xl border-2 border-gray-500 p-2 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" + href={Constants.get(:linkedin_url)} + rel="noopener" + target="_blank" + > + LinkedIn + <.icon name="tabler-brand-linkedin" class="h-6 w-6 sm:h-8 sm:w-8" /> + + <.link + class="rounded-xl border-2 border-gray-500 p-2 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" href={Constants.get(:youtube_url)} rel="noopener" target="_blank" @@ -211,7 +212,7 @@ defmodule AlgoraWeb.Components.Footer do <.icon name="tabler-brand-youtube-filled" class="h-6 w-6 sm:h-8 sm:w-8" /> <.link - class="rounded-xl border-2 border-gray-500 p-3 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" + class="rounded-xl border-2 border-gray-500 p-2 text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-300 sm:p-3" href={"mailto:" <> Constants.get(:email)} > Email diff --git a/lib/algora_web/components/header.ex b/lib/algora_web/components/header.ex index 22afde77c..fdcadda56 100644 --- a/lib/algora_web/components/header.ex +++ b/lib/algora_web/components/header.ex @@ -9,8 +9,7 @@ defmodule AlgoraWeb.Components.Header do [ %{name: "Bounties", path: ~p"/bounties"}, %{name: "Jobs", path: ~p"/jobs"}, - %{name: "Docs", path: ~p"/docs"}, - %{name: "Pricing", path: ~p"/pricing"} + %{name: "Docs", path: ~p"/docs"} ] end diff --git a/lib/algora_web/components/tech_badge.ex b/lib/algora_web/components/tech_badge.ex index 1e3abbcf2..b02460036 100644 --- a/lib/algora_web/components/tech_badge.ex +++ b/lib/algora_web/components/tech_badge.ex @@ -35,6 +35,11 @@ defmodule AlgoraWeb.Components.TechBadge do defp icon_url("huggingface"), do: "/images/logos/huggingface.png" defp icon_url("youtube"), do: "/images/logos/youtube.png" defp icon_url("tiktok"), do: "/images/logos/tiktok.png" + defp icon_url("openai"), do: "https://avatars.githubusercontent.com/u/14957082?s=200&v=4" + defp icon_url("claude"), do: "https://avatars.githubusercontent.com/u/76263028?s=200&v=4" + defp icon_url("gemini"), do: "https://avatars.githubusercontent.com/u/161781182?s=200&v=4" + defp icon_url("grok"), do: "https://avatars.githubusercontent.com/u/130314967?s=200&v=4" + defp icon_url("clickhouse"), do: "https://avatars.githubusercontent.com/u/54801242?s=200&v=4" defp icon_url(tech), do: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/#{icon_path(tech)}" @@ -62,6 +67,7 @@ defmodule AlgoraWeb.Components.TechBadge do defp icon_class("apachekafka"), do: "bg-white invert saturate-0" defp icon_class("emacs"), do: "bg-white invert saturate-0" defp icon_class("flask"), do: "bg-white invert saturate-0" + defp icon_class("prisma"), do: "bg-white invert saturate-0" defp icon_class(_tech), do: "bg-transparent" defp normalize(tech) do @@ -123,6 +129,9 @@ defmodule AlgoraWeb.Components.TechBadge do "emacs lisp" -> "emacs" + "tailwind" -> + "tailwindcss" + t -> t |> String.replace("+", "plus") @@ -212,7 +221,17 @@ defmodule AlgoraWeb.Components.TechBadge do "Flutter", "Vim", "Emacs", - "Flask" + "Flask", + "OpenAI", + "Claude", + "Gemini", + "Grok", + "Solidity", + "Zig", + "Prisma", + "TailwindCSS", + "tRPC", + "Clickhouse" ] end end diff --git a/lib/algora_web/components/ui/multiline.ex b/lib/algora_web/components/ui/multiline.ex new file mode 100644 index 000000000..23c78de1b --- /dev/null +++ b/lib/algora_web/components/ui/multiline.ex @@ -0,0 +1,13 @@ +defmodule AlgoraWeb.Components.UI.Multiline do + @moduledoc false + use AlgoraWeb.Component + + attr :value, :string + attr :class, :string, default: nil + + def multiline(assigns) do + ~H""" +
    {@value}
    + """ + end +end diff --git a/lib/algora_web/constants.ex b/lib/algora_web/constants.ex index 74700e0fa..a2932829e 100644 --- a/lib/algora_web/constants.ex +++ b/lib/algora_web/constants.ex @@ -12,6 +12,7 @@ defmodule AlgoraWeb.Constants do youtube_url: "https://www.youtube.com/@algora-io", discord_url: ~p"/discord", github_url: "https://github.com/algora-io", + linkedin_url: "https://linkedin.com/company/algorapbc", calendar_url: "https://cal.com/ioannisflo", github_repo_url: "https://github.com/algora-io/algora", github_repo_api_url: "https://api.github.com/repos/algora-io/algora", diff --git a/lib/algora_web/controllers/og_image_controller.ex b/lib/algora_web/controllers/og_image_controller.ex index f6d61665c..abf98cf07 100644 --- a/lib/algora_web/controllers/og_image_controller.ex +++ b/lib/algora_web/controllers/og_image_controller.ex @@ -149,28 +149,39 @@ defmodule AlgoraWeb.OGImageController do url = Path.join([AlgoraWeb.Endpoint.url() | path]) <> params object_path = Path.join(["og"] ++ path ++ ["og.png"]) + caller_pid = Keyword.get(opts, :notify_pid) case ScreenshotQueue.generate_image(url, @opts |> Keyword.put(:path, filepath) |> Keyword.merge(opts)) do {:ok, _log} -> case File.read(filepath) do {:ok, body} -> Task.start(fn -> - Algora.S3.upload(body, object_path, + result = Algora.S3.upload(body, object_path, content_type: "image/png", cache_control: "public, max-age=#{max_age(path)}" ) + if caller_pid do + send(caller_pid, {:screenshot_upload_complete, object_path, result}) + end + File.rm(filepath) end) {:ok, body} error -> + if caller_pid do + send(caller_pid, {:screenshot_upload_complete, object_path, error}) + end File.rm(filepath) error end error -> + if caller_pid do + send(caller_pid, {:screenshot_upload_complete, object_path, error}) + end error end end diff --git a/lib/algora_web/data/home_cache.ex b/lib/algora_web/data/home_cache.ex new file mode 100644 index 000000000..b580c30e5 --- /dev/null +++ b/lib/algora_web/data/home_cache.ex @@ -0,0 +1,199 @@ +defmodule AlgoraWeb.Data.HomeCache do + @moduledoc """ + ETS-based cache for homepage data to reduce database load. + """ + use GenServer + + # Cache keys + @platform_stats_key :platform_stats + @jobs_key :jobs_by_user + @orgs_with_stats_key :orgs_with_stats + + # Cache TTL in milliseconds (10 minutes) + @cache_ttl 10 * 60 * 1000 + + def start_link(_opts) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + def init(_) do + table = :ets.new(__MODULE__, [:named_table, :public, read_concurrency: true]) + {:ok, %{table: table}} + end + + @doc """ + Get platform stats with caching + """ + def get_platform_stats do + get_or_compute(@platform_stats_key, &compute_platform_stats/0) + end + + @doc """ + Get jobs by user with caching + """ + def get_jobs_by_user do + get_or_compute(@jobs_key, &compute_jobs_by_user/0) + end + + @doc """ + Get organizations with stats with caching + """ + def get_orgs_with_stats do + get_or_compute(@orgs_with_stats_key, &compute_orgs_with_stats/0) + end + + @doc """ + Clear all cache entries + """ + def clear_cache do + :ets.delete_all_objects(__MODULE__) + end + + @doc """ + Clear specific cache entry + """ + def clear_cache(key) do + :ets.delete(__MODULE__, key) + end + + @doc """ + Invalidate platform stats cache when transactions change + """ + def invalidate_platform_stats do + clear_cache(@platform_stats_key) + end + + @doc """ + Invalidate jobs cache when jobs change + """ + def invalidate_jobs do + clear_cache(@jobs_key) + end + + @doc """ + Invalidate orgs cache when organizations or bounties change + """ + def invalidate_orgs do + clear_cache(@orgs_with_stats_key) + end + + # Private functions + + defp get_or_compute(key, compute_fn) do + case :ets.lookup(__MODULE__, key) do + [{^key, value, expires_at}] -> + if System.monotonic_time(:millisecond) < expires_at do + value + else + # Cache expired, recompute + :ets.delete(__MODULE__, key) + compute_and_cache(key, compute_fn) + end + + [] -> + # Cache miss, compute and cache + compute_and_cache(key, compute_fn) + end + end + + defp compute_and_cache(key, compute_fn) do + value = compute_fn.() + expires_at = System.monotonic_time(:millisecond) + @cache_ttl + :ets.insert(__MODULE__, {key, value, expires_at}) + value + end + + defp compute_platform_stats do + import Ecto.Query + alias Algora.Accounts.User + alias Algora.Payments.Transaction + alias Algora.Repo + alias AlgoraWeb.Data.PlatformStats + + total_contributors = + Repo.one( + from(t in Transaction, + where: t.type == :credit, + where: t.status == :succeeded, + where: not is_nil(t.linked_transaction_id), + select: count(fragment("DISTINCT ?", t.user_id)) + ) + ) || 0 + + total_contributors = total_contributors + PlatformStats.get().extra_contributors + + total_countries = + Repo.one( + from(u in User, + join: t in Transaction, + on: t.user_id == u.id, + where: t.type == :credit, + where: t.status == :succeeded, + where: not is_nil(t.linked_transaction_id), + where: not is_nil(u.country) and u.country != "", + select: count(fragment("DISTINCT ?", u.country)) + ) + ) || 0 + + total_paid_out_subtotal = + Repo.one( + from(t in Transaction, + where: t.type == :credit, + where: t.status == :succeeded, + where: not is_nil(t.linked_transaction_id), + select: sum(t.net_amount) + ) + ) || Money.new(0, :USD) + + total_paid_out = + total_paid_out_subtotal + |> Money.add!(PlatformStats.get().extra_paid_out) + |> Money.round(currency_digits: 0) + + completed_bounties_subtotal = + Repo.one( + from(t in Transaction, + where: t.type == :credit, + where: t.status == :succeeded, + where: not is_nil(t.linked_transaction_id), + where: not is_nil(t.bounty_id), + select: count(fragment("DISTINCT (?, ?)", t.bounty_id, t.user_id)) + ) + ) || 0 + + completed_tips_subtotal = + Repo.one( + from(t in Transaction, + where: t.type == :credit, + where: t.status == :succeeded, + where: not is_nil(t.linked_transaction_id), + where: not is_nil(t.tip_id), + select: count(fragment("DISTINCT (?, ?)", t.tip_id, t.user_id)) + ) + ) || 0 + + completed_bounties_count = + completed_bounties_subtotal + completed_tips_subtotal + PlatformStats.get().extra_completed_bounties + + %{ + total_contributors: total_contributors, + total_countries: total_countries, + total_paid_out: total_paid_out, + completed_bounties_count: completed_bounties_count + } + end + + defp compute_jobs_by_user do + Algora.Jobs.list_jobs() + |> Enum.group_by(& &1.user) + end + + defp compute_orgs_with_stats do + orgs = Algora.Organizations.list_orgs(limit: 6) + + Enum.map(orgs, fn org -> + stats = Algora.Bounties.fetch_stats(org_id: org.id) + Map.put(org, :bounty_stats, stats) + end) + end +end \ No newline at end of file diff --git a/lib/algora_web/forms/challenge_form.ex b/lib/algora_web/forms/challenge_form.ex new file mode 100644 index 000000000..d43dbfc98 --- /dev/null +++ b/lib/algora_web/forms/challenge_form.ex @@ -0,0 +1,20 @@ +defmodule AlgoraWeb.Forms.ChallengeForm do + @moduledoc false + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field :email, :string + field :description, :string + end + + def changeset(form \\ %__MODULE__{}, attrs) do + form + |> cast(attrs, [:email, :description]) + |> validate_required([:email, :description]) + |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must be a valid email") + |> validate_length(:description, min: 1) + end +end diff --git a/lib/algora_web/live/challenges/activepieces_live.ex b/lib/algora_web/live/challenges/activepieces_live.ex new file mode 100644 index 000000000..78a822b82 --- /dev/null +++ b/lib/algora_web/live/challenges/activepieces_live.ex @@ -0,0 +1,749 @@ +defmodule AlgoraWeb.Challenges.ActivepiecesLive do + @moduledoc false + use AlgoraWeb, :live_view + + alias AlgoraWeb.Components.Header + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page_title, "Activepieces MCP Challenge") + |> assign( + :page_description, + "Build MCPs for Activepieces and earn $200 per integration!" + ) + |> assign(:page_image, "#{AlgoraWeb.Endpoint.url()}/images/challenges/activepieces/og.png")} + end + + @impl true + def render(assigns) do + ~H""" +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + Challenge by + Activepieces + +

    + Expand Activepieces with MCPs
    + + Earn $200 per integration + +

    +

    + Activepieces is revolutionizing workflow automation with an open-source, AI-first platform that already supports 280+ integrations. With 60% of pieces contributed by the community, we're building the most extensible automation ecosystem. +

    + Now we're expanding into the MCP (Model Context Protocol) ecosystem, enabling LLMs like Claude to directly interact with your favorite tools. Build MCPs that bridge the gap between AI and automation, and earn + $200 + for each accepted MCP integration. Join us in making AI more capable and connected. +

    +
    +
    + + + +
    +
    +
    +
    +
    + Background +
    +
    +
    +
    +

    + How to participate +

    +

    + Got questions? + + Join us on Discord! + +

    +
      +
    • + <.icon name="tabler-square-rounded-number-1" class="size-8 mr-2 shrink-0" /> + + + Fork the Activepieces repository + + and set up your local development environment + +
    • +
    • + <.icon name="tabler-square-rounded-number-2" class="size-8 mr-2 shrink-0" /> + + Learn about + + Model Context Protocol (MCP) + + and explore existing + + Activepieces MCPs + + +
    • +
    • + <.icon name="tabler-square-rounded-number-3" class="size-8 mr-2 shrink-0" /> + + Choose a service to integrate from the list below and build an MCP that enables LLMs to interact with that service through Activepieces + +
    • +
    • + <.icon name="tabler-square-rounded-number-4" class="size-8 mr-2 shrink-0" /> + + Submit a PR with your MCP implementation including comprehensive documentation and examples + +
    • +
    • + <.icon name="tabler-square-rounded-number-5" class="size-8 mr-2 shrink-0" /> + + Once your MCP is reviewed and merged, receive your bounty reward up to + $200 + +
    • +
    +
    +
    +
    +

    + Available MCP Bounties +

    +

    + 10 active bounties available totaling $1,250 in rewards. Select a bounty to start building. Each bounty includes detailed requirements and compensation. +

    +
    + +
    + Canva +
    +

    Canva MCP

    +

    Design creation

    +
    +
    +

    + Build MCP for uploading assets, creating/importing/exporting designs, managing folders, and finding designs. 8 specific actions required including Upload Asset, Create Design, Export Design. +

    +
    + + $100 + + + Active + +
    +
    + +
    + Gmail +
    +

    Gmail MCP

    +

    Email management

    +
    +
    +

    + Extend Gmail integration with new triggers (starred emails, conversations, attachments, labels) and actions (reply, drafts, labels, archive/delete, find). Comprehensive email automation. +

    +
    + + $200 + + + Active + +
    +
    + +
    + DIMO +
    +

    DIMO MCP

    +

    Vehicle data

    +
    +
    +

    + Blockchain-based vehicle protocol with multiple APIs: Attestation, Device Definition, Token Exchange, Identity, Telemetry, and Webhooks. Requires developer license and vehicle data permissions. +

    +
    + + $200 + + + Active + +
    +
    + +
    + Smartsheet +
    +

    Smartsheet MCP

    +

    Project management

    +
    +
    +

    + Spreadsheet and project management platform with triggers (New Row Added, Updated Row, New Attachment/Comment) and actions (Add/Update Row, Attach File, Find Rows/Attachments/Sheets). +

    +
    + + $100 + + + Active + +
    +
    + +
    + PandaDoc +
    +

    PandaDoc MCP

    +

    Document automation

    +
    +
    +

    + Document automation platform with triggers (Document Completed, State Changed, Updated), write actions (Create Document from Template, Create Attachment, Create/Update Contact), and read actions. +

    +
    + + $100 + + + Active + +
    +
    + +
    + HeyGen +
    +

    HeyGen MCP

    +

    AI video generation

    +
    +
    +

    + AI video generation platform with triggers (Video Generation Completed/Failed), write actions (Create Avatar Video, Translate Video, Upload Asset, Create Video From Template), and read actions. +

    +
    + + $100 + + + Active + +
    +
    + +
    + Cognito Forms +
    +

    Cognito Forms MCP

    +

    Form builder

    +
    +
    +

    + Form builder platform with triggers (New Entry Submitted, Entry Updated), write actions (Create/Update/Delete Entry), and read actions (Get Entry Details). +

    +
    + + $50 + + + Active + +
    +
    + +
    + pCloud +
    +

    pCloud MCP

    +

    Cloud storage

    +
    +
    +

    + Cloud storage platform with triggers (New File Uploaded, Folder Created), write actions (Upload File, Create Folder, Download File Content, Copy File), and search actions (Find File/Folder). +

    +
    + + $100 + + + Active + +
    +
    + +
    + QuickBooks +
    +

    QuickBooks MCP

    +

    Accounting platform

    +
    +
    +

    + Accounting and business management platform with triggers (New Customer, New Invoice, Payment Received), write actions (Create Customer/Invoice/Payment), and read actions (Get Customer/Invoice details). +

    +
    + + $100 + + + Active + +
    +
    + +
    + Huly +
    +

    Huly MCP

    +

    Project management

    +
    +
    +

    + All-in-one project management platform with triggers (New Issue, Status Changed, Comment Added), write actions (Create/Update Issue, Add Comment, Update Status), and read actions (Get Issue/Project details). +

    +
    + + $100 + + + Active + +
    +
    +
    +
    +

    + Don't see your favorite tool? Propose a new MCP integration in our + + Discord community + +

    +
    +
    +
    +

    + Media +

    + +
    +
    +
    +

    + Discussions +

    +
    +
    + + Hacker News discussion + +
    + <.link + href="https://news.ycombinator.com/item?id=34723989" + target="_blank" + rel="noopener" + > +
    +

    + Huge TAM so there's lots of room for a healthy ecosystem of competitors in no code low code... UX is important. Spend the resources as you scale to understand how your users are leveraging your product for their workflows; it should be magical to them. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    toomuchtodo
    +
    +
    + +
    +
    +
    +
    +
    + <.link + href="https://news.ycombinator.com/item?id=34723989" + target="_blank" + rel="noopener" + > +
    +

    + Ah man this is fantastic... Love what you are doing with authentication/connections to other services. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + WatchDog +
    +
    +
    + +
    +
    + <.link + href="https://news.ycombinator.com/item?id=34723989" + target="_blank" + rel="noopener" + > +
    +

    + Excited about this open source alternative! +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + edotrajan +
    +
    +
    + +
    +
    +
    +
    +
    +
    + <.link + href="https://news.ycombinator.com/item?id=34723989" + target="_blank" + rel="noopener" + > +
    +

    + Having a large Open Source ecosystem increases the size of the pie. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + ensignavenger +
    +
    +
    + +
    +
    + <.link + href="https://news.ycombinator.com/item?id=34723989" + target="_blank" + rel="noopener" + > +
    +

    + MIT license builds the trust to use it and contribute to it. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + vishalchandra +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Start contributing +

    +
    + <.link + class="rounded-xl border-2 border-[#843ee5] p-3 text-[#843ee5] transition-colors hover:border-[#9f5aea] hover:text-[#9f5aea] sm:p-5" + href="https://discord.gg/2jUXBKDdP8" + rel="noopener" + target="_blank" + > + Discord + <.icon name="tabler-brand-discord-filled" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#843ee5] p-3 text-[#843ee5] transition-colors hover:border-[#9f5aea] hover:text-[#9f5aea] sm:p-5" + href="https://x.com/activepieces" + rel="noopener" + target="_blank" + > + X (formerly Twitter) + <.icon name="tabler-brand-x" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#843ee5] p-3 text-[#843ee5] transition-colors hover:border-[#9f5aea] hover:text-[#9f5aea] sm:p-5" + href="https://github.com/activepieces/activepieces" + rel="noopener" + target="_blank" + > + GitHub + <.icon name="github" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#843ee5] p-3 text-[#843ee5] transition-colors hover:border-[#9f5aea] hover:text-[#9f5aea] sm:p-5" + href="https://www.youtube.com/@activepiecesco" + rel="noopener" + target="_blank" + > + YouTube + <.icon name="tabler-brand-youtube-filled" class="h-6 w-6 sm:h-12 sm:w-12" /> + +
    +
    + + activepieces + +
    +
    +
    +
    +
    +
    +
    + +
    + """ + end +end diff --git a/lib/algora_web/live/challenges/atopile_live.ex b/lib/algora_web/live/challenges/atopile_live.ex new file mode 100644 index 000000000..22b977e97 --- /dev/null +++ b/lib/algora_web/live/challenges/atopile_live.ex @@ -0,0 +1,582 @@ +defmodule AlgoraWeb.Challenges.AtopileLive do + @moduledoc false + use AlgoraWeb, :live_view + + alias AlgoraWeb.Components.Header + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page_title, "Atopile Challenge") + |> assign( + :page_description, + "Expand the atopile ecosystem - build packages, modules, utilities, and tools to win $1,000!" + ) + |> assign(:page_image, "#{AlgoraWeb.Endpoint.url()}/images/challenges/atopile/og.png")} + end + + @impl true + def handle_event("subscribe_challenge", %{"email" => email}, socket) do + Algora.Activities.alert("New challenge subscription [atopile] from #{email}", :critical) + + {:noreply, put_flash(socket, :info, "Thank you for subscribing! We'll notify you when the challenge goes live.")} + end + + @impl true + def render(assigns) do + ~H""" +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + Challenge brought to you by + atopile + +

    + Build the Future of Hardware Design
    + + Expand atopile to win $1,000 + +

    +

    + Atopile is revolutionizing how we build electronics by bringing modern software development practices to hardware design. We need passionate builders to expand our ecosystem with packages, modules, utilities, and tools.

    + Publish packages, modules, utilities, or tools that expand the atopile ecosystem. Every accepted published contribution will go in the atopile directory, you'll receive credits, and earn a + $1,000 + reward. To participate, you need electrical engineering knowledge (college students welcome!) and passion for building tools for builders. +

    +
    + <.form + for={%{}} + phx-submit="subscribe_challenge" + class="flex flex-col sm:flex-row gap-3" + > + + + +
    +
    +
    + + + +
    +
    +
    +
    +
    + Background +
    +
    +
    +
    +

    + How to participate +

    +

    + Got questions? + + Reach out on X! + +

    +
      +
    • + <.icon name="tabler-square-rounded-number-1" class="size-8 mr-2 shrink-0" /> + + + Set up your development environment + + and familiarize yourself with atopile's syntax and workflow + +
    • +
    • + <.icon name="tabler-square-rounded-number-2" class="size-8 mr-2 shrink-0" /> + + + Explore packages + + to see examples and identify gaps + +
    • +
    • + <.icon name="tabler-square-rounded-number-3" class="size-8 mr-2 shrink-0" /> + + Create a new package, module, utility, or tool that extends atopile's capabilities. This could be: +
        +
      • Hardware modules (power supplies, motor drivers, sensors)
      • +
      • Development tools and utilities
      • +
      • Testing frameworks for hardware
      • +
      • Integration tools with other EDA software
      • +
      • Educational examples and tutorials
      • +
      +
      +
    • +
    • + <.icon name="tabler-square-rounded-number-4" class="size-8 mr-2 shrink-0" /> + + + Publish your package + + to the atopile registry. Include comprehensive documentation and examples + +
    • +
    • + <.icon name="tabler-square-rounded-number-5" class="size-8 mr-2 shrink-0" /> + + If your contribution is accepted and published, you'll receive + $1,000 + and recognition in the atopile community + +
    • +
    +
    +
    +
    +
    +

    + Example packages +

    +

    + Get inspired by existing packages in the + + atopile registry + +

    +
    +
    +
    +

    + + atopile/buttons + +

    + + v0.1.7 + +
    +

    + A collection of buttons for convenience +

    + + <.icon name="github" class="size-4" /> + Repository + +
    +
    +
    +

    + + atopile/addressable-leds + +

    + + v0.2.2 + +
    +

    + SK6805 addressable RGB LEDs with integrated controller for creating colorful LED effects and... +

    + + <.icon name="github" class="size-4" /> + Repository + +
    +
    +
    +

    + + atopile/indicator-leds + +

    + + v0.1.1 + +
    +

    Indicator LEDs for convenience

    + + <.icon name="github" class="size-4" /> + Repository + +
    +
    +
    +
    +
    +

    + Media +

    + +
    +
    +
    +

    + Discussions +

    +
    +
    + + Hacker News discussion + +
    + <.link + href="https://news.ycombinator.com/item?id=39263854" + target="_blank" + rel="noopener" + > +
    +

    + 99% of people who put a regulator into their schematic will want an appropriate input and output capacitor... It'll be very exciting if we can move towards a more modular world, where designs can be composed. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    Michael T
    +
    +
    + +
    +
    +
    +
    +
    + <.link + href="https://news.ycombinator.com/item?id=39263854" + target="_blank" + rel="noopener" + > +
    +

    + Looks really useful! As a hardware designer I've had plenty of copy pasting bits of schematics to duplicate common functionality. Seems like this could be really helpful in preventing mistakes and increasing quality. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + Liftyee +
    +
    +
    + +
    +
    + <.link + href="https://news.ycombinator.com/item?id=39263854" + target="_blank" + rel="noopener" + > +
    +

    + LOVE IT LOVE IT LOVE IT!!! I'm doing a lot of home automation work, and I absolutely hate that I need to use breadboards, hunt for pre-assembled components, or to spend days designing a PCB. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + Cyberax +
    +
    +
    + +
    +
    +
    +
    +
    +
    + <.link + href="https://news.ycombinator.com/item?id=39263854" + target="_blank" + rel="noopener" + > +
    +

    + This has sooo much promise... Imagine optimizing for cost, removing redundancy, simplifying footprints, and prioritizing in-stock inventory over new order components. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + Mikeortman +
    +
    +
    + +
    +
    + <.link + href="https://news.ycombinator.com/item?id=39263854" + target="_blank" + rel="noopener" + > +
    +

    + We are hoping that ato modules can become a convenient language for the community to share modules with each other, in a similar fashion to python and pypi. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + atopile team +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Start contributing +

    +
    + <.link + class="rounded-xl border-2 border-[#f0551c] p-3 text-[#f0551c] transition-colors hover:border-[#ff6b33] hover:text-[#ff6b33] sm:p-5" + href="https://x.com/atopile_io" + rel="noopener" + target="_blank" + > + X (formerly Twitter) + <.icon name="tabler-brand-x" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#f0551c] p-3 text-[#f0551c] transition-colors hover:border-[#ff6b33] hover:text-[#ff6b33] sm:p-5" + href="https://linkedin.com/company/atopile" + rel="noopener" + target="_blank" + > + LinkedIn + + + + + <.link + class="rounded-xl border-2 border-[#f0551c] p-3 text-[#f0551c] transition-colors hover:border-[#ff6b33] hover:text-[#ff6b33] sm:p-5" + href="https://github.com/atopile/atopile" + rel="noopener" + target="_blank" + > + GitHub + <.icon name="github" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#f0551c] p-3 text-[#f0551c] transition-colors hover:border-[#ff6b33] hover:text-[#ff6b33] sm:p-5" + href="https://www.youtube.com/@atopile_io" + rel="noopener" + target="_blank" + > + YouTube + <.icon name="tabler-brand-youtube-filled" class="h-6 w-6 sm:h-12 sm:w-12" /> + +
    +
    + + atopile + +
    +
    +
    +
    +
    +
    +
    + +
    + """ + end +end diff --git a/lib/algora_web/live/challenges/electric_live.ex b/lib/algora_web/live/challenges/electric_live.ex new file mode 100644 index 000000000..86401806d --- /dev/null +++ b/lib/algora_web/live/challenges/electric_live.ex @@ -0,0 +1,608 @@ +defmodule AlgoraWeb.Challenges.ElectricLive do + @moduledoc false + use AlgoraWeb, :live_view + + alias AlgoraWeb.Components.Header + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page_title, "Electric SQL Challenge") + |> assign(:page_description, "Build something cool with Electric SQL's real-time sync engine - win $500!") + |> assign(:page_image, "#{AlgoraWeb.Endpoint.url()}/images/challenges/electric/og.png")} + end + + @impl true + def render(assigns) do + ~H""" +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + Challenge brought to you by + Electric SQL + +

    + Sync AI with Electric SQL
    + + Win $500 + +

    +

    + Electric SQL is a Postgres sync engine that makes real-time data synchronization simple. Build collaborative apps, AI agents with live data, reactive dashboards, or offline-first experiences. +

    + We're looking for creative applications that showcase the power of real-time sync - whether it's for agentic systems, human-in-the-loop AI workflows, reactive data substrates for AI apps, or something else entirely. Show us what's possible when sync just works - from stress testing the engine to building the next generation of collaborative tools. The most innovative projects win $500. +

    +
    +
    + + + +
    +
    +
    +
    +
    + Background +
    +
    +
    +
    +

    + How to participate +

    +

    + Got questions? + + Join us on Discord! + +

    +
      +
    • + <.icon name="tabler-square-rounded-number-1" class="size-8 mr-2 shrink-0" /> + + + Explore Electric SQL + + and set up your development environment using the quickstart guide + +
    • +
    • + <.icon name="tabler-square-rounded-number-2" class="size-8 mr-2 shrink-0" /> + + Build something creative! Ideas include: AI agents with live data sync, collaborative tools, offline-first apps, reactive dashboards, human-in-the-loop agentic systems, or stress testing Electric's sync engine + +
    • +
    • + <.icon name="tabler-square-rounded-number-3" class="size-8 mr-2 shrink-0" /> + + Document your project with a clear README explaining what you built, how it works, and why it showcases Electric SQL's capabilities + +
    • +
    • + <.icon name="tabler-square-rounded-number-4" class="size-8 mr-2 shrink-0" /> + + Publish your project on GitHub and share it with the community on X. Tag + + @ElectricSQL + + and include a demo video or live link + +
    • +
    • + <.icon name="tabler-square-rounded-number-5" class="size-8 mr-2 shrink-0" /> + + The most innovative uses of Electric SQL's real-time sync capabilities win + $500 + +
    • +
    +
    +
    +
    + +
    +
    +
    +

    + Use Your Stack +

    +

    + Electric SQL works with your favorite frameworks and tools +

    + +
    +
    +
    +

    + Media +

    + +
    +
    +
    +

    + Community Highlights +

    +
    +
    + + Hacker News discussion + +
    + <.link + href="https://news.ycombinator.com/item?id=37584049" + target="_blank" + rel="noopener" + > +
    +

    + We're building an offline-first, mobile-first app and have high hopes for this project! The combination of SQLite's convenience on the client-side and PostgreSQL's flexibility on the server-side is a potent foundation. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    gregzo & thanikkal
    +
    +
    + +
    +
    +
    +
    +
    + <.link + href="https://news.ycombinator.com/item?id=37584049" + target="_blank" + rel="noopener" + > +
    +

    + Synchronizing data with multiple users updating data in a distributed manner then reconciling it is not trivial. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + brigadier132 +
    +
    +
    + +
    +
    + <.link + href="https://news.ycombinator.com/item?id=37584049" + target="_blank" + rel="noopener" + > +
    +

    + Reducing network reliance solves so many problems and corner-cases in my web app. Having access to local data makes everything very snappy too. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + mjadobson +
    +
    +
    + +
    +
    +
    +
    +
    +
    + <.link + href="https://news.ycombinator.com/item?id=37584049" + target="_blank" + rel="noopener" + > +
    +

    + This looks amazing. Real-time sync has been such a pain point for our collaborative apps. Can't wait to try this out. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + devbuilder42 +
    +
    +
    + +
    +
    + <.link + href="https://news.ycombinator.com/item?id=37584049" + target="_blank" + rel="noopener" + > +
    +

    + The local-first approach is the future. Finally something that makes it easy to build reactive apps with live data. +

    +
    +
    + <.icon name="tabler-brand-ycombinator" class="size-6" /> +
    +
    + reactivedev +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Start contributing +

    +
    + <.link + class="rounded-xl border-2 border-[#d1b9fe] p-3 text-[#d1b9fe] transition-colors hover:border-[#d1b9fe] hover:text-[#d1b9fe] sm:p-5" + href="https://discord.electric-sql.com" + rel="noopener" + target="_blank" + > + Discord + <.icon name="tabler-brand-discord-filled" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#d1b9fe] p-3 text-[#d1b9fe] transition-colors hover:border-[#d1b9fe] hover:text-[#d1b9fe] sm:p-5" + href="https://x.com/ElectricSQL" + rel="noopener" + target="_blank" + > + X (formerly Twitter) + <.icon name="tabler-brand-x" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#d1b9fe] p-3 text-[#d1b9fe] transition-colors hover:border-[#d1b9fe] hover:text-[#d1b9fe] sm:p-5" + href="https://github.com/electric-sql/electric" + rel="noopener" + target="_blank" + > + GitHub + <.icon name="github" class="h-6 w-6 sm:h-12 sm:w-12" /> + + <.link + class="rounded-xl border-2 border-[#d1b9fe] p-3 text-[#d1b9fe] transition-colors hover:border-[#d1b9fe] hover:text-[#d1b9fe] sm:p-5" + href="https://www.youtube.com/@ElectricSQL" + rel="noopener" + target="_blank" + > + YouTube + <.icon name="tabler-brand-youtube-filled" class="h-6 w-6 sm:h-12 sm:w-12" /> + +
    +
    + + electric + +
    +
    +
    +
    +
    +
    +
    + +
    + """ + end +end diff --git a/lib/algora_web/live/challenges/limbo_live.ex b/lib/algora_web/live/challenges/limbo_live.ex index 772deb819..30d0cc52a 100644 --- a/lib/algora_web/live/challenges/limbo_live.ex +++ b/lib/algora_web/live/challenges/limbo_live.ex @@ -8,7 +8,7 @@ defmodule AlgoraWeb.Challenges.LimboLive do def mount(_params, _session, socket) do {:ok, socket - |> assign(:page_title, "Limbo Challenge") + |> assign(:page_title, "Turso Challenge") |> assign(:page_description, "Turso rewrote SQLite in Rust - find a bug to win $1,000!") |> assign(:page_image, "#{AlgoraWeb.Endpoint.url()}/images/challenges/limbo/og.png")} end @@ -52,9 +52,10 @@ defmodule AlgoraWeb.Challenges.LimboLive do SQLite alternative in Rust - one that's open source and community-driven. Our goal isn't just features, but rock-solid reliability through Deterministic Simulation Testing.

    - We are so confident in the long-term ability of the DST to find the rarest bugs that even at the current early stage, we are offering cash bounties for those who can find cases where a bug survived this testing. In this initial phase of the project, we still expect many bugs to exist, and we'll offer + We are so confident in the long-term ability of the DST to find the rarest bugs that even at the current early stage, we are offering cash bounties for those who can find cases where a bug survived this testing. During the alpha phase of the project, we still expect some bugs to exist, and we'll offer $1,000 for any bugs that lead to data loss or data corruption. As our releases progress, we will continuously expand the scope of bugs and size of the bounty.

    @@ -102,11 +103,11 @@ defmodule AlgoraWeb.Challenges.LimboLive do rel="noopener" target="_blank" class="font-semibold text-white underline inline-flex" - href="https://github.com/tursodatabase/limbo/blob/main/CONTRIBUTING.md" + href="https://github.com/tursodatabase/turso/blob/main/CONTRIBUTING.md" > Set up your development environment - and build the Limbo CLI locally + and build the Turso CLI locally
  • @@ -117,7 +118,7 @@ defmodule AlgoraWeb.Challenges.LimboLive do rel="noopener" target="_blank" class="font-semibold text-white underline inline-flex" - href="https://github.com/tursodatabase/limbo/tree/main/simulator" + href="https://github.com/tursodatabase/turso/tree/main/simulator" > simulator @@ -386,7 +387,7 @@ defmodule AlgoraWeb.Challenges.LimboLive do <.link class="rounded-xl border-2 border-[#1ebba2] p-3 text-[#4ff7d3] transition-colors hover:border-[#4ff7d3] hover:text-[#75ffe1] sm:p-5" - href="https://github.com/tursodatabase/limbo" + href="https://github.com/tursodatabase/turso" rel="noopener" target="_blank" > @@ -421,12 +422,12 @@ defmodule AlgoraWeb.Challenges.LimboLive do limbo diff --git a/lib/algora_web/live/home_live.ex b/lib/algora_web/live/home_live.ex index 168d431c9..135a664fe 100644 --- a/lib/algora_web/live/home_live.ex +++ b/lib/algora_web/live/home_live.ex @@ -4,84 +4,36 @@ defmodule AlgoraWeb.HomeLive do use LiveSvelte.Components import AlgoraWeb.Components.ModalVideo - import Ecto.Changeset - import Ecto.Query alias Algora.Accounts - alias Algora.Accounts.User - alias Algora.Bounties alias Algora.Jobs - alias Algora.Jobs.JobPosting - alias Algora.Payments - alias Algora.Payments.Transaction - alias Algora.Repo alias AlgoraWeb.Components.Footer alias AlgoraWeb.Components.Header - alias AlgoraWeb.Data.PlatformStats - alias AlgoraWeb.Forms.BountyForm - alias AlgoraWeb.Forms.TipForm - alias Phoenix.LiveView.AsyncResult + alias AlgoraWeb.Data.HomeCache + alias AlgoraWeb.Forms.ChallengeForm require Logger - defmodule Form do - @moduledoc false - use Ecto.Schema - - @primary_key false - @derive {Jason.Encoder, only: [:email, :job_description, :candidate_description]} - embedded_schema do - field :email, :string - field :job_description, :string - field :candidate_description, :string - end - - def changeset(form, attrs \\ %{}) do - form - |> Ecto.Changeset.cast(attrs, [:email, :job_description, :candidate_description]) - |> Ecto.Changeset.validate_required([:email, :job_description]) - |> Ecto.Changeset.validate_format(:email, ~r/@/) - end - end - - defp placeholder_text do - """ - - 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 - @impl true def mount(params, _session, socket) do - total_contributors = get_contributors_count() - total_countries = get_countries_count() + # Get cached platform stats + platform_stats = HomeCache.get_platform_stats() stats = [ - %{label: "Paid Out", value: format_money(get_total_paid_out())}, - %{label: "Completed Bounties", value: format_number(get_completed_bounties_count())}, - %{label: "Contributors", value: format_number(total_contributors)}, - %{label: "Countries", value: format_number(total_countries)} + %{label: "Full-time SWEs Hired", value: "30+"}, + %{label: "Happy Customers", value: "100+"}, + %{label: "Rewarded Contributors", value: format_number(platform_stats.total_contributors)}, + %{label: "Countries", value: format_number(platform_stats.total_countries)}, + %{label: "Paid Out", value: format_money(platform_stats.total_paid_out)}, + %{label: "Completed Bounties", value: format_number(platform_stats.completed_bounties_count)} ] - featured_devs = Accounts.list_featured_developers() - - jobs_by_user = Enum.group_by(Jobs.list_jobs(), & &1.user) - - bounties0 = - Bounties.list_bounties( - status: :open, - owner_handles: Algora.Settings.get_featured_orgs() - ) + # Get company and people avatars for the section + company_people_examples = get_company_people_examples() - bounties1 = - Bounties.list_bounties( - status: :open, - limit: 3, - amount_gt: Money.new(:USD, 500) - ) - - bounties = bounties0 ++ bounties1 + # Get cached jobs and orgs data + jobs_by_user = HomeCache.get_jobs_by_user() + orgs_with_stats = HomeCache.get_orgs_with_stats() case socket.assigns[:current_user] do %{handle: handle} = user when is_binary(handle) -> @@ -94,39 +46,16 @@ defmodule AlgoraWeb.HomeLive do |> assign(:page_title_suffix, "") |> assign(:page_image, "#{AlgoraWeb.Endpoint.url()}/images/og/home.png") |> assign(:screenshot?, not is_nil(params["screenshot"])) - |> assign(:bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{}))) - |> assign(:tip_form, to_form(TipForm.changeset(%TipForm{}, %{}))) - |> assign(:featured_devs, featured_devs) |> assign(:stats, stats) - |> assign(:form, to_form(Form.changeset(%Form{}, %{}))) |> assign(:jobs_by_user, jobs_by_user) - |> assign(:user_metadata, AsyncResult.loading()) - |> assign(:transactions, Payments.list_featured_transactions()) - |> assign(:bounties, bounties) - |> assign_events() + |> assign(:orgs_with_stats, orgs_with_stats) + |> assign(:company_people_examples, company_people_examples) + |> assign(:show_challenge_drawer, false) + |> assign(:challenge_form, to_form(ChallengeForm.changeset(%ChallengeForm{}, %{}))) |> assign_user_applications()} end end - defp assign_events(socket) do - events = - (socket.assigns.transactions || []) - |> Enum.map(fn tx -> %{item: tx, type: :transaction, timestamp: tx.succeeded_at} end) - |> Enum.concat( - (socket.assigns.jobs_by_user || []) - |> Enum.flat_map(fn {_user, jobs} -> jobs end) - |> Enum.map(fn job -> %{item: job, type: :job, timestamp: job.inserted_at} end) - ) - |> Enum.concat( - Enum.map(socket.assigns.bounties || [], fn bounty -> - %{item: bounty, type: :bounty, timestamp: bounty.inserted_at} - end) - ) - |> Enum.sort_by(& &1.timestamp, {:desc, DateTime}) - - assign(socket, :events, events) - end - @impl true def render(assigns) do ~H""" @@ -138,104 +67,87 @@ defmodule AlgoraWeb.HomeLive do <% end %>
    -
    -
    -
    -
    -
    -

    - Meet your
    - new hire today -

    -

    - Access a network of top 1% engineers,
    - pre-vetted through OSS contributions.
    - Only pay when you hire. -

    -
      -
    • - <.icon - name="tabler-square-rounded-number-1" - class="size-6 mr-2 shrink-0 text-foreground/80" - /> - - Submit JD - -
    • -
    • - <.icon - name="tabler-square-rounded-number-2" - class="size-6 mr-2 shrink-0 text-foreground/80" - /> - - Receive matches within hours - -
    • -
    • - <.icon - name="tabler-square-rounded-number-3" - class="size-6 mr-2 shrink-0 text-foreground/80" - /> - - Interview within days - -
    • -
    - Job candidates +
    +
    +

    + Open source
    + hiring platform +

    +

    + Algora connects companies with + OSS + engineers
    for full-time jobs and paid + OSS + contributions. +

    +
    + <.button + navigate={~p"/onboarding/org"} + class="h-12 sm:h-14 rounded-md px-6 sm:px-12 text-base sm:text-lg" + > + Hire with Algora + + <.button + href="https://www.youtube.com/watch?v=Jne9mVas9i0" + target="_blank" + rel="noopener" + class="h-12 sm:h-14 rounded-md px-6 sm:px-12 text-base sm:text-lg" + variant="secondary" + > + Watch demo + +
    +
    +
    + +
    +
    +
    + <%= for stat <- @stats do %> +
    +
    + {stat.value} +
    +
    + {stat.label} +
    -
    -
    -
    -

    - View your candidates -

    -

    - Share JD to receive matches within hours -

    + <% end %> +
    +
    +
    - <.form - for={@form} - phx-submit="submit" - class="mt-4 2xl:mt-6 flex flex-col gap-4 2xl:gap-6" - > - <.input - field={@form[:job_description]} - type="textarea" - label="Job description / careers URL" - rows="3" - placeholder="Tell us about the role and your requirements..." - class="resize-none" - /> - <.input - field={@form[:candidate_description]} - type="textarea" - label="Describe your ideal candidate, heuristics, green/red flags etc." - rows="3" - placeholder={placeholder_text()} - class="resize-none" - /> - <.input - field={@form[:email]} - type="email" - label="Work email" - placeholder="you@company.com" - /> -
    - <.button class="w-full" type="submit">Receive your candidates -
    - No credit card required - only pay when you hire -
    -
    - +
    +
    +
    + <%= for example <- @company_people_examples do %> +
    + {example.person_name} + <.icon name="tabler-arrow-right" class="size-4 text-muted-foreground shrink-0" /> + {example.company_name} +
    +
    + {example.person_name} + <.icon name="tabler-arrow-right" class="size-3 text-foreground" /> {example.company_name}
    +
    {example.person_title}
    + <.badge + variant="secondary" + class="absolute -top-2 -left-2 text-xs px-2 py-1 text-emerald-400 bg-emerald-950" + > + Full-time hire! +
    -
    + <% end %>
    @@ -248,7 +160,7 @@ defmodule AlgoraWeb.HomeLive do alt="Y Combinator Logo" loading="lazy" /> -

    +

    Trusted by
    open source YC founders

    @@ -326,17 +238,323 @@ defmodule AlgoraWeb.HomeLive do
    -

    - Community highlights +

    + Open positions +

    +

    + Discover jobs at top startups +

    + + <%= if Enum.empty?(@jobs_by_user) do %> +
    + <.icon name="tabler-briefcase" class="h-12 w-12 text-muted-foreground mx-auto mb-4" /> +

    No open positions at the moment

    +
    + <% else %> +
    + <%= for {user, jobs} <- Enum.take(@jobs_by_user, 3) do %> + <%= for job <- Enum.take(jobs, 1) do %> +
    +
    +
    + <.avatar class="size-12"> + <.avatar_image src={user.avatar_url} /> + <.avatar_fallback> + {Algora.Util.initials(user.name)} + + +
    +
    {job.title}
    +
    {user.name}
    +
    +
    +
    + <%= for tech <- Enum.take(job.tech_stack || [], 3) do %> + <.tech_badge tech={tech} size="sm" /> + <% end %> +
    +
    +
    + <%= if MapSet.member?(@user_applications, job.id) do %> + <.button size="sm" disabled class="opacity-50 w-full sm:w-auto"> + Applied + + <% else %> + <.button + size="sm" + phx-click="apply_job" + phx-value-job-id={job.id} + class="w-full sm:w-auto" + > + Apply + + <% end %> +
    +
    + <% end %> + <% end %> +
    + +
    + <.button navigate={~p"/jobs"} variant="outline"> + View all positions + +
    + <% end %> +
    +
    + +
    +
    +

    + Challenges +

    +

    + +
    +
    + <.link + class="group relative flex aspect-[1200/630] flex-1 rounded-2xl border-2 border-solid border-border bg-cover hover:no-underline hover:scale-[1.02] transition-all duration-200" + style="background-image:url(/images/challenges/limbo/og.png)" + navigate={~p"/challenges/limbo"} + > + +
    + Sponsored by + Turso +
    +
    +
    +
    + <.link + class="group relative flex aspect-[1200/630] flex-1 rounded-2xl border-2 border-solid border-border bg-cover hover:no-underline hover:scale-[1.02] transition-all duration-200 opacity-75 hover:opacity-100" + style="background-image:url(/images/challenges/atopile/og.png)" + navigate={~p"/challenges/atopile"} + > + +
    + Coming Soon +
    +
    +
    + Sponsored by + Atopile +
    +
    +
    +
    + <.icon + name="tabler-plus" + class="size-12 text-muted-foreground group-hover:text-emerald-400 transition-colors mb-4" + /> +

    + Create a Challenge +

    +

    +

    +
    +
    +
    +
    +
    + +
    +
    +

    + Active bounty programs +

    +

    + Contribute to open source and get paid by top companies when your PRs are merged +

    + + <%= if Enum.empty?(@orgs_with_stats) do %> +
    + <.icon name="tabler-trophy" class="h-12 w-12 text-muted-foreground mx-auto mb-4" /> +

    No active bounty programs at the moment

    +
    + <% else %> +
    + <%= for org <- @orgs_with_stats do %> + <.link navigate={~p"/#{org.handle}/home"} class="group"> +
    +
    + {org.name} +
    +

    + {org.name} +

    +

    + {org.bio} +

    +
    +
    + +
    +
    +
    + {org.bounty_stats.open_bounties_count || 0} +
    +
    Open bounties
    +
    +
    +
    + {org.bounty_stats.rewarded_bounties_count || 0} +
    +
    Rewarded bounties
    +
    +
    +
    + {org.bounty_stats.solvers_count || 0} +
    +
    Developers rewarded
    +
    +
    + + <%= if org.bounty_stats.total_awarded_amount do %> +
    +
    + + {format_money(org.bounty_stats.total_awarded_amount)} + + paid out +
    +
    + <% end %> + +
    + View bounties + <.icon name="tabler-arrow-right" class="size-3" /> +
    +
    + + <% end %> +
    + +
    + <.button navigate={~p"/bounties"} variant="outline"> + View all bounties + +
    + <% end %> +
    +
    + +
    +
    +

    + Our journey

    - See what's poppin' on Algora + From coding marketplace to the largest open source hiring platform

    -
    - <%= for event <- events() do %> - <.event_card event={event} /> - <% end %> +
    +
    + +
    +
    + +
    + +
    +
    + 2022 +
    +
    +

    + "Uber for Coding" introduced +

    +

    + Launched an on-demand marketplace where companies would match with a developer for contract work with a click of a button. The top HN comment branded us "delusional entrepreneurs", and we struggled to make it work for the next 6 months. +

    + <.link + href="https://news.ycombinator.com/item?id=32129089" + target="_blank" + class="inline-flex items-center gap-2 text-sm text-foreground/90 hover:text-foreground transition-colors group" + > + <.icon + name="tabler-brand-ycombinator" + class="size-5 text-orange-500 group-hover:text-orange-400 transition-colors" + /> View HN discussion + +
    +
    + + +
    +
    + 2023 +
    +
    +

    + Open source bounties launched +

    +

    + Launched a new platform focused on open source projects with bounties and payments integrated on GitHub. We successfully reduced friction and increased trust for paid open source contributions, and fulfilled our vision of "press a button and get work done". +

    + <.link + href="https://news.ycombinator.com/item?id=35412226" + target="_blank" + class="inline-flex items-center gap-2 text-sm text-foreground/90 hover:text-foreground transition-colors group" + > + <.icon + name="tabler-brand-ycombinator" + class="size-5 text-orange-500 group-hover:text-orange-400 transition-colors" + /> View HN discussion + +
    +
    + + +
    +
    + 2024 +
    +
    +

    + Sustainable & profitable +

    +

    + Algora Public Benefit Corporation became a bootstrapped, profitable business. Dozens of customers hired full-time the engineers they met with Algora. We unlocked a bigger adjacent problem to solve: full-time hiring. +

    + <.link + href="https://news.ycombinator.com/item?id=37769595" + target="_blank" + class="inline-flex items-center gap-2 text-sm text-foreground/90 hover:text-foreground transition-colors group" + > + <.icon + name="tabler-brand-ycombinator" + class="size-5 text-orange-500 group-hover:text-orange-400 transition-colors" + /> View HN discussion + +
    +
    + + +
    +
    + 2025 +
    +
    +

    + The future: Uber for hiring +

    +

    + Today, companies simply share their job description and receive qualified candidates within hours. We've transformed from a coding marketplace into the most efficient hiring platform for technical talent globally. +

    +
    + <.icon name="tabler-rocket" class="size-4 text-emerald-400" /> + + New HN launch coming in July + +
    +
    +
    +
    +
    @@ -348,8 +566,7 @@ defmodule AlgoraWeb.HomeLive do
    <.button - id="start-hiring-button" - phx-hook="ScrollToTop" + navigate={~p"/onboarding/org"} class="h-10 sm:h-14 rounded-md px-8 sm:px-12 text-sm sm:text-xl" > Start hiring @@ -362,14 +579,6 @@ defmodule AlgoraWeb.HomeLive do Explore platform
    - <%!--
    - <.button navigate={~p"/auth/signup"}> - Get started - - <.button href={AlgoraWeb.Constants.get(:github_repo_url)} variant="secondary"> - <.icon name="github" class="size-4 mr-2 -ml-1" /> View source code - -
    --%>
  • @@ -380,62 +589,13 @@ defmodule AlgoraWeb.HomeLive do
    <.modal_video_dialog /> + <.challenge_drawer + show_challenge_drawer={@show_challenge_drawer} + challenge_form={@challenge_form} + /> """ end - @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.")} - - {:error, changeset} -> - {:noreply, assign(socket, form: to_form(changeset))} - end - end - - @impl true - def handle_event("create_bounty", _params, socket) do - {:noreply, redirect(socket, to: ~p"/auth/signup")} - end - - @impl true - def handle_event("create_tip", _params, socket) do - {:noreply, redirect(socket, to: ~p"/auth/signup")} - end - - @impl true - def handle_event("email_changed", %{"job_posting" => %{"email" => email}}, socket) do - if String.match?(email, ~r/^[^\s]+@[^\s]+$/i) do - {:noreply, start_async(socket, :fetch_metadata, fn -> Algora.Crawler.fetch_user_metadata(email) end)} - else - {:noreply, socket} - 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 - with {:ok, user} <- - Accounts.get_or_register_user(params["email"], %{type: :organization, display_name: params["company_name"]}), - {:ok, job} <- params |> Map.put("user_id", user.id) |> Jobs.create_job_posting() do - Algora.Activities.alert("Job posting initialized: #{job.company_name}", :critical) - {:noreply, redirect(socket, external: AlgoraWeb.Constants.get(:calendar_url))} - else - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, :form, to_form(changeset))} - - {:error, reason} -> - Logger.error("Failed to create job posting: #{inspect(reason)}") - {:noreply, put_flash(socket, :error, "Something went wrong. Please try again.")} - end - end - @impl true def handle_event("apply_job", %{"job-id" => job_id}, socket) do if socket.assigns[:current_user] do @@ -456,84 +616,31 @@ defmodule AlgoraWeb.HomeLive do end @impl true - def handle_async(:fetch_metadata, {:ok, metadata}, socket) do - {: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]))))} + def handle_event("show_challenge_drawer", _, socket) do + {:noreply, assign(socket, :show_challenge_drawer, true)} end @impl true - def handle_async(:fetch_metadata, {:exit, reason}, socket) do - {:noreply, assign(socket, :user_metadata, AsyncResult.failed(socket.assigns.user_metadata, reason))} + def handle_event("close_challenge_drawer", _, socket) do + {:noreply, assign(socket, :show_challenge_drawer, false)} end - defp get_total_paid_out do - subtotal = - Repo.one( - from(t in Transaction, - where: t.type == :credit, - where: t.status == :succeeded, - where: not is_nil(t.linked_transaction_id), - select: sum(t.net_amount) - ) - ) || Money.new(0, :USD) - - subtotal |> Money.add!(PlatformStats.get().extra_paid_out) |> Money.round(currency_digits: 0) - end - - defp get_completed_bounties_count do - bounties_subtotal = - Repo.one( - from(t in Transaction, - where: t.type == :credit, - where: t.status == :succeeded, - where: not is_nil(t.linked_transaction_id), - where: not is_nil(t.bounty_id), - select: count(fragment("DISTINCT (?, ?)", t.bounty_id, t.user_id)) - ) - ) || 0 - - tips_subtotal = - Repo.one( - from(t in Transaction, - where: t.type == :credit, - where: t.status == :succeeded, - where: not is_nil(t.linked_transaction_id), - where: not is_nil(t.tip_id), - select: count(fragment("DISTINCT (?, ?)", t.tip_id, t.user_id)) - ) - ) || 0 - - bounties_subtotal + tips_subtotal + PlatformStats.get().extra_completed_bounties - end + @impl true + def handle_event("submit_challenge", %{"challenge_form" => params}, socket) do + case ChallengeForm.changeset(%ChallengeForm{}, params) do + %{valid?: true} = changeset -> + data = Ecto.Changeset.apply_changes(changeset) + Algora.Activities.alert("New challenge submission from #{data.email}: #{data.description}", :critical) - defp get_contributors_count do - subtotal = - Repo.one( - from(t in Transaction, - where: t.type == :credit, - where: t.status == :succeeded, - where: not is_nil(t.linked_transaction_id), - select: count(fragment("DISTINCT ?", t.user_id)) - ) - ) || 0 - - subtotal + PlatformStats.get().extra_contributors - end + {:noreply, + socket + |> assign(:show_challenge_drawer, false) + |> put_flash(:info, "Thank you for your submission! We'll be in touch soon.")} - defp get_countries_count do - Repo.one( - from(u in User, - join: t in Transaction, - on: t.user_id == u.id, - where: t.type == :credit, - where: t.status == :succeeded, - where: not is_nil(t.linked_transaction_id), - where: not is_nil(u.country) and u.country != "", - select: count(fragment("DISTINCT ?", u.country)) - ) - ) || 0 + %{valid?: false} = changeset -> + dbg(changeset) + {:noreply, assign(socket, :challenge_form, to_form(changeset))} + end end defp format_money(money), do: money |> Money.round(currency_digits: 0) |> Money.to_string!(no_fraction_if_integer: true) @@ -780,56 +887,92 @@ defmodule AlgoraWeb.HomeLive do """ end - defp events do + defp get_company_people_examples do [ %{ - title: ".txt is hiring with Algora!", - date: "Jun 2, 2025", - logo: "https://avatars.githubusercontent.com/u/142257755?s=200&v=4", - alt: "DotTxt", - theme_color: "#ffffff", - link: "/candidates/dottxt-ai" + company_name: "Golem Cloud", + company_avatar: "https://github.com/golemcloud.png", + person_name: "Maxim", + person_avatar: "https://github.com/mschuwalow.png", + person_title: "Lead Engineer" }, %{ - title: "Prequel launches bounty program!", - date: "May 31, 2025", - logo: "https://avatars.githubusercontent.com/u/129894407?v=4", - alt: "Prequel", - theme_color: "#ffffff", - link: "/prequel-dev/home" + company_name: "Firecrawl", + company_avatar: "https://github.com/mendableai.png", + person_name: "Gergő", + person_avatar: "https://github.com/mogery.png", + person_title: "Software Engineer" }, %{ - title: "Notes hired with Algora!", - date: "May 28, 2025", - logo: "https://notes.fm/images/favicon.png", - alt: "Notes", - theme_color: "#0029ff", - link: "/notes/home" + company_name: "Cal.com", + company_avatar: "https://github.com/calcom.png", + person_name: "Efraín", + person_avatar: "https://github.com/roae.png", + person_title: "Software Engineer" }, %{ - title: "Outspeed is hiring with Algora!", - date: "May 26, 2025", - logo: "https://avatars.githubusercontent.com/u/181807673?s=200&v=4", - alt: "Outspeed", - theme_color: "#00ffff", - link: "/candidates/outspeed" + company_name: "Hanko", + company_avatar: "https://avatars.githubusercontent.com/u/20222142?v=4", + person_name: "Ashutosh", + person_avatar: "https://avatars.githubusercontent.com/u/62984427?v=4", + person_title: "Developer Advocate" }, %{ - title: "Unsiloed launches bounty program!", - date: "May 11, 2025", - logo: "https://avatars.githubusercontent.com/u/194294730?s=200&v=4", - alt: "Unsiloed", - theme_color: "#ffffff", - link: "/unsiloed-ai/home" + company_name: "Trigger.dev", + company_avatar: "https://github.com/triggerdotdev.png", + person_name: "Nick", + person_avatar: "https://github.com/nicktrn.png", + person_title: "Founding Engineer" }, %{ - title: "Notes hired with Algora!", - date: "May 1, 2025", - logo: "https://notes.fm/images/favicon.png", - alt: "Notes", - theme_color: "#0029ff", - link: "/notes/home" + company_name: "Tailcall", + company_avatar: + "https://algora.io/asset/storage/v1/object/public/images/org/cli0b0kdt0000mh0fngt4r4bk-1741007407053", + person_name: "Kiryl", + person_avatar: "https://algora.io/asset/storage/v1/object/public/images/user/clg4rtl2n0002jv0fg30lto6l", + person_title: "Founding Engineer" } ] end + + defp challenge_drawer(assigns) do + ~H""" + <.drawer show={@show_challenge_drawer} on_cancel="close_challenge_drawer" direction="right"> + <.drawer_header> + <.drawer_title>Submit Challenge + <.drawer_description> + Tell us about your coding challenge and we'll help you set it up. + + + <.drawer_content class="mt-4"> + <.form for={@challenge_form} phx-submit="submit_challenge"> +
    + <.input + field={@challenge_form[:email]} + label="Email" + type="email" + required + placeholder="your@email.com" + /> + <.input + field={@challenge_form[:description]} + label="Challenge Description" + type="textarea" + required + placeholder="Describe your coding challenge..." + /> +
    + <.button variant="outline" type="button" phx-click="close_challenge_drawer"> + Cancel + + <.button type="submit"> + Submit + +
    +
    + + + + """ + end end diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index 4bc49e89d..a594d1258 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -400,7 +400,7 @@ defmodule AlgoraWeb.Org.DashboardLive do
    - <.section + <%!-- <.section :if={@matches != []} title="Algora Matches" subtitle="Top 1% Algora developers in your tech stack available to hire now" @@ -432,7 +432,7 @@ defmodule AlgoraWeb.Org.DashboardLive do /> <% end %> - + --%> <.section :if={@experts != []} diff --git a/lib/algora_web/live/org/job_live.ex b/lib/algora_web/live/org/job_live.ex index b7572dde3..01c60243b 100644 --- a/lib/algora_web/live/org/job_live.ex +++ b/lib/algora_web/live/org/job_live.ex @@ -166,8 +166,13 @@ defmodule AlgoraWeb.Org.JobLive do
    -
    - {@job.title} +
    +
    + {@job.title} +
    + <%= if @job.id in ["b4sFSeJvb2rteUEX", "M9yTwVXFjvQM2WJf"] do %> + <.badge variant="success">Contract to Hire + <% end %>
    <%= if @current_org.hiring_subscription != :active && length(@truncated_matches) > 0 do %>
    - +
    + <%= for {match, index} <- @next_matches |> Enum.with_index() do %> +
    0, do: "hidden lg:block"}"}> + <.match_card + current_user={@current_user} + user={match.user} + tech_stack={@job.tech_stack |> Enum.take(1)} + job={@job} + contributions={Map.get(@contributions_map, match.user.id, [])} + contract_type="bring_your_own" + anonymized={@current_org.hiring_subscription != :active} + heatmap_data={Map.get(@heatmaps_map, match.user.id)} + /> +
    + <% end %> +
    + {length(@matches) - length(@truncated_matches)} more matches
    @@ -588,16 +605,14 @@ defmodule AlgoraWeb.Org.JobLive do # Sort matches by total contributions (0 if no heatmap) and take top 6 sorted_matches = - all_matches - |> Enum.sort_by(fn match -> + Enum.sort_by(all_matches, fn match -> heatmap_data = Map.get(heatmaps_map, match.user.id) total_contributions = if heatmap_data, do: get_in(heatmap_data, ["totalContributions"]) || 0, else: 0 # negative for descending sort -total_contributions end) - |> Enum.take(6) - truncated_matches = Algora.Cloud.truncate_matches(socket.assigns.current_org, sorted_matches) + truncated_matches = Algora.Cloud.truncate_matches(socket.assigns.current_org, Enum.take(sorted_matches, 6)) # Create a fake matches list with the right count for UI compatibility fake_matches = List.duplicate(%{}, total_matches_count) @@ -606,6 +621,7 @@ defmodule AlgoraWeb.Org.JobLive do |> assign(:developers, developers) |> assign(:applicants, sort_by_contributions(socket.assigns.job, all_applicants, contributions_map)) |> assign(:matches, fake_matches) + |> assign(:next_matches, sorted_matches |> Enum.drop(length(truncated_matches)) |> Enum.take(3)) |> assign(:truncated_matches, truncated_matches) |> assign(:contributions_map, contributions_map) |> assign(:heatmaps_map, heatmaps_map) @@ -885,10 +901,10 @@ defmodule AlgoraWeb.Org.JobLive do {@user.provider_meta["twitter_username"]} --%> -
    +
    <.icon name="tabler-map-pin" class="shrink-0 h-4 w-4" /> - {@user.provider_meta["location"]} + {@user.location}
    @@ -1304,10 +1320,10 @@ defmodule AlgoraWeb.Org.JobLive do {@selected_developer.provider_meta["twitter_handle"]} -
    +
    <.icon name="tabler-map-pin" class="h-4 w-4" /> - {@selected_developer.provider_meta["location"]} + {@selected_developer.location}
    diff --git a/lib/algora_web/live/org/jobs_live.ex b/lib/algora_web/live/org/jobs_live.ex index 9f7810730..1c53e8c7c 100644 --- a/lib/algora_web/live/org/jobs_live.ex +++ b/lib/algora_web/live/org/jobs_live.ex @@ -129,7 +129,7 @@ defmodule AlgoraWeb.Org.JobsLive do <%= for job <- @jobs do %>
    -
    +
    <%= if @current_user_role in [:admin, :mod] do %> <.link navigate={~p"/#{@current_org.handle}/jobs/#{job.id}"} @@ -142,6 +142,9 @@ defmodule AlgoraWeb.Org.JobsLive do {job.title}
    <% end %> + <%= if job.id in ["b4sFSeJvb2rteUEX", "M9yTwVXFjvQM2WJf"] do %> + <.badge variant="success">Contract to Hire + <% end %>
    - <%= if @screenshot? do %> -
    - <% else %> - - <% end %> - -
    - -
    -
    -
    -
    -
    -

    - Pricing for every team -

    -

    - For individuals, OSS communities, and open/closed source companies -

    -
    -
    -
    - -
    -
    -
    -

    -
    - <.icon name="tabler-wallet" class="h-6 w-6 text-emerald-400" /> Payments -
    -

    -

    - Fund GitHub issues with USD rewards and pay when work is merged. Set up contracts for ongoing development work. Simple, outcome-based payments. -

    -
    -
    -
    -
    - <%= for plan <- @plans1 do %> - <.pricing_card1 plan={plan} plans={@plans1} /> - <% end %> -
    - -
    -
    -
    -

    -
    - <.icon name="tabler-users" class="h-6 w-6 text-purple-400" /> Matching & Hiring -
    -

    -

    - Find and hire top 1% OSS engineers with confidence -

    -
    -
    -
    -
    - <.link - href={AlgoraWeb.Constants.get(:calendar_url)} - class="group border ring-1 ring-transparent hover:ring-purple-400 rounded-xl overflow-hidden" - > -
    -
    -
    -

    - Talent Matching -

    - <.button variant="purple" size="lg" class="font-display text-lg mr-auto sm:mr-0"> - Contact us - -
    -

    - Connect with top engineers for your project -

    -
    -
    -
      -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Smart matching based on: - -
      -
        -
      • -
        - <.icon name="tabler-circle-filled" class="size-1.5 text-purple-300" /> -
        - - Tech stack and skills - -
      • -
      • -
        - <.icon name="tabler-circle-filled" class="size-1.5 text-purple-300" /> -
        - - Location and budget - -
      • -
      • -
        - <.icon name="tabler-circle-filled" class="size-1.5 text-purple-300" /> -
        - - Granular GitHub - contribution data - -
      • -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Open source: GitHub issue hypermatch - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Closed source: Project spec hypermatch - -
      -
    • -
    - -
    -
    -
    -

    - "I've used Algora in the past for bounties, and recently used them to hire a contract engineer. Every time the process has yield fantastic results, with high quality code and fast turn arounds. I'm a big fan." -

    -
    -
    - Drew Baker -
    -
    Drew Baker
    -
    Technical Partner
    -
    Funkhaus
    -
    -
    -
    -
    -
    -
    - - - <.link - href={AlgoraWeb.Constants.get(:calendar_url)} - class="group border ring-1 ring-transparent hover:ring-purple-400 rounded-xl overflow-hidden" - > -
    -
    -
    -

    - Hiring Platform -

    - <.button variant="purple" size="lg" class="font-display text-lg mr-auto sm:mr-0"> - Contact us - -
    -

    - End-to-end hiring -

    -
    -
    -
      -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Job board that highlights applicant's OSS contributions - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Embed 1-click apply on careers page - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Automatically screen & rank candidates - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Schedule interviews and chat in-app - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Trial with bounties and contract-to-hire - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Access your matches and stand out - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Publish jobs on Algora platform - -
      -
    • -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-purple-400" /> -
      - - Recruiting partner option available - -
      -
    • -
    -
    -
    - -
    - -
    -
    -

    - Why Choose Algora for Hiring? -

    -
    -
    -
    - <.icon name="tabler-filter" class="h-8 w-8 text-purple-400" /> -
    -

    High Signal

    -

    - Access pre-vetted developers with proven OSS track records -

    -
    -
    -
    - <.icon name="tabler-clock" class="h-8 w-8 text-purple-400" /> -
    -

    Save Time & Money

    -

    - Match with top developers efficiently -

    -
    -
    -
    - <.icon name="tabler-shield-check" class="h-8 w-8 text-purple-400" /> -
    -

    Reduce Risk

    -

    - Trial with bounties before committing to full-time hires -

    -
    -
    -
    -
    -
    -
    - -
    -
    -

    - Frequently asked questions -

    -
    - <%= for item <- @faq_items do %> -
    - - -
    - <% end %> -
    -
    -
    - -
    -
    -

    - Join the open source economy -

    -
    - <.button - navigate={~p"/onboarding/org"} - class="h-10 sm:h-14 rounded-md px-8 sm:px-12 text-sm sm:text-xl" - > - Companies - - <.button - navigate={~p"/onboarding/dev"} - variant="secondary" - class="h-10 sm:h-14 rounded-md px-8 sm:px-12 text-sm sm:text-xl" - > - Developers - -
    -
    -
    - -
    - -
    -
    - """ - end - - @impl true - def mount(params, _session, socket) do - socket = - assign(socket, - screenshot?: not is_nil(params["screenshot"]), - page_title: "Pricing", - plans1: get_plans1(), - faq_items: get_faq_items(), - active_faq: nil - ) - - {:ok, socket} - end - - def pricing_card1(assigns) do - ~H""" - <.link - href={@plan.cta_url} - class="group border ring-1 ring-transparent hover:ring-emerald-400 rounded-xl overflow-hidden" - > -
    -
    -
    -
    -

    - {@plan.name} -

    - <%= if @plan.popular do %> - - Most Popular - - <% end %> -
    -
    -

    - {@plan.description} -

    -
    -
    -
    - <%= case @plan.id do %> - <% "receive-payments" -> %> -
    -
    -

    - 100% -

    -

    - received -

    -
    -
    - <% "pay-developers" -> %> -
    -
    - <.button - variant="default" - size="lg" - class="font-display text-lg mr-auto sm:mr-0" - > - Contact us - -
    -
    - <% end %> -
    -
    -
    -
    -
    -
      - <%= for feature <- @plan.features do %> -
    • -
      -
      - <.icon name="tabler-check" class="size-5 text-emerald-400" /> -
      - - {Phoenix.HTML.raw(feature.name)} - -
      - <%= if feature.detail do %> -

      {feature.detail}

      - <% end %> -
    • - <% end %> -
    -
    -
    - - """ - end - - def get_plans1 do - [ - %Plan{ - id: "receive-payments", - name: "Receive payments", - description: "Get paid for your open source work", - price: "100%", - cta_text: "Start earning", - cta_url: "/onboarding/dev", - popular: false, - features: [ - %Feature{name: "Keep 100% of your earnings"}, - %Feature{name: "Available in #{ConnectCountries.count()} countries"}, - %Feature{name: "Fast payouts in 2-7 days"} - ] - }, - %Plan{ - id: "pay-developers", - name: "Pay developers", - description: "Reward contributors with Algora", - price: nil, - cta_text: "Create bounties", - cta_url: AlgoraWeb.Constants.get(:calendar_url), - popular: false, - features: [ - %Feature{name: "GitHub bounties and tips"}, - %Feature{name: "Contract work (fixed/hourly)"}, - %Feature{name: "Invoices, payouts, compliance, 1099s"} - ] - } - ] - end - - defp get_faq_items do - [ - %FaqItem{ - id: "payment-methods", - question: "What payment methods do you support?", - answer: - ~s(We support payments via Stripe for funding bounties. Contributors can receive payments directly to their bank accounts in #{ConnectCountries.count()} countries/regions worldwide.) - }, - %FaqItem{ - id: "payment-process", - question: "How does the payment process work?", - answer: - "Organizations can either pay manually after merging pull requests, or save their card with Stripe to enable auto-pay on merge. Manual payments are processed through a secure Stripe hosted checkout page." - }, - %FaqItem{ - id: "invoices-receipts", - question: "Do you provide invoices and receipts?", - answer: - "Yes, users receive an invoice and receipt after each bounty payment. These documents are automatically generated and delivered to your email." - }, - %FaqItem{ - id: "tax-forms", - question: "How are tax forms handled?", - answer: - "We partner with Stripe to file and deliver 1099 forms for your US-based freelancers, simplifying tax compliance for organizations working with US contributors." - }, - %FaqItem{ - id: "payout-time", - question: "How long do payouts take?", - answer: - "Payout timing varies by country, typically ranging from 2-7 business days after a bounty is awarded. Initial payouts for new accounts may take 7-14 days. The exact timing depends on your location, banking system, and account history with Stripe, our payment processor." - }, - %FaqItem{ - id: "minimum-bounty", - question: "Is there a minimum bounty amount?", - answer: - "There's no strict minimum bounty amount. However, bounties with higher values tend to attract more attention and faster solutions from contributors." - }, - %FaqItem{ - id: "enterprise-options", - question: "Do you offer custom enterprise plans?", - answer: - ~s(Yes, for larger organizations with specific needs, we offer custom enterprise plans with additional features, dedicated support, and volume-based pricing. Please schedule a call with a founder to discuss your requirements.) - }, - %FaqItem{ - id: "supported-countries", - question: "Which countries are supported for contributors?", - answer: - ~s(We support contributors from #{ConnectCountries.count()} countries/regions worldwide. You can receive payments regardless of your location as long as you have a bank account in one of our supported countries. See the full list of supported countries.) - } - ] - end -end diff --git a/lib/algora_web/live/user/profile_live.ex b/lib/algora_web/live/user/profile_live.ex index 45f0e6f82..67ebc4707 100644 --- a/lib/algora_web/live/user/profile_live.ex +++ b/lib/algora_web/live/user/profile_live.ex @@ -14,7 +14,7 @@ defmodule AlgoraWeb.User.ProfileLive do {:ok, user} -> transactions = Payments.list_received_transactions(user.id, limit: page_size()) - contributions = Algora.Workspace.list_user_contributions([user.id], limit: 20) + contributions = Algora.Workspace.list_user_contributions([user.id], limit: 20, display_all: true) {:ok, socket diff --git a/lib/algora_web/router.ex b/lib/algora_web/router.ex index a7a02db40..3a42f8a2a 100644 --- a/lib/algora_web/router.ex +++ b/lib/algora_web/router.ex @@ -107,9 +107,11 @@ defmodule AlgoraWeb.Router do live "/community/:tech", CommunityLive, :index live "/platform", PlatformLive, :index live "/crowdfund", CrowdfundLive, :index - live "/pricing", PricingLive live "/challenges", ChallengesLive live "/challenges/turso", Challenges.LimboLive + live "/challenges/activepieces", Challenges.ActivepiecesLive + live "/challenges/atopile", Challenges.AtopileLive + live "/challenges/electric", Challenges.ElectricLive live "/challenges/prettier", Challenges.PrettierLive live "/challenges/golem", Challenges.GolemLive live "/challenges/tsperf", Challenges.TsperfLive diff --git a/priv/repo/migrations/20250701191252_create_talents.exs b/priv/repo/migrations/20250701191252_create_talents.exs new file mode 100644 index 000000000..6b940186e --- /dev/null +++ b/priv/repo/migrations/20250701191252_create_talents.exs @@ -0,0 +1,17 @@ +defmodule Algora.Repo.Migrations.CreateTalents do + use Ecto.Migration + + def change do + create table(:talents) do + add :user_id, references(:users, on_delete: :delete_all), null: false + add :status, :string, null: false + add :source, :string + add :metadata, :map + + timestamps() + end + + create unique_index(:talents, [:user_id]) + create index(:talents, [:status]) + end +end diff --git a/priv/repo/migrations/20250702141654_add_preferences_to_users.exs b/priv/repo/migrations/20250702141654_add_preferences_to_users.exs new file mode 100644 index 000000000..22ae8c28c --- /dev/null +++ b/priv/repo/migrations/20250702141654_add_preferences_to_users.exs @@ -0,0 +1,9 @@ +defmodule Algora.Repo.Migrations.AddPreferencesToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :preferences, :text + end + end +end diff --git a/priv/repo/migrations/20250704125710_add_internal_email_to_users.exs b/priv/repo/migrations/20250704125710_add_internal_email_to_users.exs new file mode 100644 index 000000000..b137e2ef7 --- /dev/null +++ b/priv/repo/migrations/20250704125710_add_internal_email_to_users.exs @@ -0,0 +1,9 @@ +defmodule Algora.Repo.Migrations.AddInternalEmailToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :internal_email, :string + end + end +end diff --git a/priv/repo/migrations/20250704131526_add_job_invitation_fields_to_users.exs b/priv/repo/migrations/20250704131526_add_job_invitation_fields_to_users.exs new file mode 100644 index 000000000..ccd7c9460 --- /dev/null +++ b/priv/repo/migrations/20250704131526_add_job_invitation_fields_to_users.exs @@ -0,0 +1,13 @@ +defmodule Algora.Repo.Migrations.AddJobInvitationFieldsToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :refer_to_company, :boolean, default: false + add :company_domain, :string + add :friends_recommendations, :boolean, default: false + add :friends_github_handles, :string + add :opt_out_algora, :boolean, default: false + end + end +end diff --git a/priv/repo/migrations/20250704140049_add_internal_notes_to_users.exs b/priv/repo/migrations/20250704140049_add_internal_notes_to_users.exs new file mode 100644 index 000000000..f9123c930 --- /dev/null +++ b/priv/repo/migrations/20250704140049_add_internal_notes_to_users.exs @@ -0,0 +1,9 @@ +defmodule Algora.Repo.Migrations.AddInternalNotesToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :internal_notes, :text + end + end +end diff --git a/priv/static/images/challenges/activepieces/bg.png b/priv/static/images/challenges/activepieces/bg.png new file mode 100644 index 000000000..6c8942b73 Binary files /dev/null and b/priv/static/images/challenges/activepieces/bg.png differ diff --git a/priv/static/images/challenges/activepieces/bg.webp b/priv/static/images/challenges/activepieces/bg.webp new file mode 100644 index 000000000..e5dde2209 Binary files /dev/null and b/priv/static/images/challenges/activepieces/bg.webp differ diff --git a/priv/static/images/challenges/activepieces/hn-post.png b/priv/static/images/challenges/activepieces/hn-post.png new file mode 100644 index 000000000..3c5c4c813 Binary files /dev/null and b/priv/static/images/challenges/activepieces/hn-post.png differ diff --git a/priv/static/images/challenges/activepieces/og.png b/priv/static/images/challenges/activepieces/og.png new file mode 100644 index 000000000..fd23f78b4 Binary files /dev/null and b/priv/static/images/challenges/activepieces/og.png differ diff --git a/priv/static/images/challenges/atopile/atopile-fullscroll.png b/priv/static/images/challenges/atopile/atopile-fullscroll.png new file mode 100644 index 000000000..5289a371b Binary files /dev/null and b/priv/static/images/challenges/atopile/atopile-fullscroll.png differ diff --git a/priv/static/images/challenges/atopile/bg.webp b/priv/static/images/challenges/atopile/bg.webp new file mode 100644 index 000000000..07ba39b04 Binary files /dev/null and b/priv/static/images/challenges/atopile/bg.webp differ diff --git a/priv/static/images/challenges/atopile/github.com_atopile_atopile_blob_main_examples_ch2_7_pcb_layout.py(HD (1366x668)).png b/priv/static/images/challenges/atopile/github.com_atopile_atopile_blob_main_examples_ch2_7_pcb_layout.py(HD (1366x668)).png new file mode 100644 index 000000000..ab144bebd Binary files /dev/null and b/priv/static/images/challenges/atopile/github.com_atopile_atopile_blob_main_examples_ch2_7_pcb_layout.py(HD (1366x668)).png differ diff --git a/priv/static/images/challenges/atopile/hn-post.webp b/priv/static/images/challenges/atopile/hn-post.webp new file mode 100644 index 000000000..c547bfe54 Binary files /dev/null and b/priv/static/images/challenges/atopile/hn-post.webp differ diff --git a/priv/static/images/challenges/atopile/og.png b/priv/static/images/challenges/atopile/og.png new file mode 100644 index 000000000..9dab661b6 Binary files /dev/null and b/priv/static/images/challenges/atopile/og.png differ diff --git a/priv/static/images/challenges/atopile/thumbnail1.png b/priv/static/images/challenges/atopile/thumbnail1.png new file mode 100644 index 000000000..fc9ce2377 Binary files /dev/null and b/priv/static/images/challenges/atopile/thumbnail1.png differ diff --git a/priv/static/images/challenges/atopile/tmphl82ba03.png b/priv/static/images/challenges/atopile/tmphl82ba03.png new file mode 100644 index 000000000..d7a9f4d15 Binary files /dev/null and b/priv/static/images/challenges/atopile/tmphl82ba03.png differ diff --git a/priv/static/images/challenges/atopile/tmps18ne6vg.png b/priv/static/images/challenges/atopile/tmps18ne6vg.png new file mode 100644 index 000000000..7a6542956 Binary files /dev/null and b/priv/static/images/challenges/atopile/tmps18ne6vg.png differ diff --git a/priv/static/images/challenges/electric/bg.png b/priv/static/images/challenges/electric/bg.png new file mode 100644 index 000000000..31cedc72f Binary files /dev/null and b/priv/static/images/challenges/electric/bg.png differ diff --git a/priv/static/images/challenges/electric/hn-post.png b/priv/static/images/challenges/electric/hn-post.png new file mode 100644 index 000000000..7045bcf6b Binary files /dev/null and b/priv/static/images/challenges/electric/hn-post.png differ diff --git a/priv/static/images/challenges/electric/og.png b/priv/static/images/challenges/electric/og.png new file mode 100644 index 000000000..b1f1a3cb8 Binary files /dev/null and b/priv/static/images/challenges/electric/og.png differ diff --git a/priv/static/images/challenges/electric/thumbnail1.png b/priv/static/images/challenges/electric/thumbnail1.png new file mode 100644 index 000000000..21440861f Binary files /dev/null and b/priv/static/images/challenges/electric/thumbnail1.png differ diff --git a/priv/static/images/logos/airparser.jpeg b/priv/static/images/logos/airparser.jpeg new file mode 100644 index 000000000..454f209d2 Binary files /dev/null and b/priv/static/images/logos/airparser.jpeg differ diff --git a/priv/static/images/logos/chatbase.jpeg b/priv/static/images/logos/chatbase.jpeg new file mode 100644 index 000000000..26a6d4d9a Binary files /dev/null and b/priv/static/images/logos/chatbase.jpeg differ diff --git a/priv/static/images/logos/memdotai.jpeg b/priv/static/images/logos/memdotai.jpeg new file mode 100644 index 000000000..d03e09b97 Binary files /dev/null and b/priv/static/images/logos/memdotai.jpeg differ diff --git a/priv/static/images/wordmarks/activepieces-dark.png b/priv/static/images/wordmarks/activepieces-dark.png new file mode 100644 index 000000000..d28f9e6e2 Binary files /dev/null and b/priv/static/images/wordmarks/activepieces-dark.png differ diff --git a/priv/static/images/wordmarks/activepieces-dark.svg b/priv/static/images/wordmarks/activepieces-dark.svg new file mode 100644 index 000000000..c17841804 --- /dev/null +++ b/priv/static/images/wordmarks/activepieces-dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/priv/static/images/wordmarks/atopile.png b/priv/static/images/wordmarks/atopile.png new file mode 100644 index 000000000..6a23bbfdd Binary files /dev/null and b/priv/static/images/wordmarks/atopile.png differ diff --git a/priv/static/images/wordmarks/atopile.svg b/priv/static/images/wordmarks/atopile.svg new file mode 100644 index 000000000..8b2ea7238 --- /dev/null +++ b/priv/static/images/wordmarks/atopile.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/priv/static/images/wordmarks/electric.svg b/priv/static/images/wordmarks/electric.svg new file mode 100644 index 000000000..70dbd56af --- /dev/null +++ b/priv/static/images/wordmarks/electric.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + +