Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
66daae8
new homepage
zcesur Jun 21, 2025
70e3a9c
updates
zcesur Jun 21, 2025
6568bc4
updates
zcesur Jun 21, 2025
4b5a72c
remove feed
zcesur Jun 21, 2025
f01e9af
updates
zcesur Jun 21, 2025
646cbf1
refactor: remove obsolete socket assigns from home_live.ex
zcesur Jun 22, 2025
977e04f
add linkedin on footer
zcesur Jun 22, 2025
b18b3e6
feat: add ETS-based cache for homepage data to reduce database load
zcesur Jun 22, 2025
18eb081
remove public pricing
zcesur Jun 22, 2025
81352d6
add atopile challenge
zcesur Jun 23, 2025
1f48c99
update background
zcesur Jun 23, 2025
b5c55d8
add asset
zcesur Jun 23, 2025
3b10aca
add example packages
zcesur Jun 23, 2025
64e8b04
add og image
zcesur Jun 23, 2025
505af50
add activepieces challenge
zcesur Jun 24, 2025
f61e91e
fix logos
zcesur Jun 24, 2025
e9fc4f2
optimize bg image
zcesur Jun 24, 2025
ecdfdf4
use upscaled bg img
zcesur Jun 24, 2025
ca832bb
update badge styling
zcesur Jun 24, 2025
7e52cfa
updates
zcesur Jun 24, 2025
f7537fc
remove challenges from home for now
zcesur Jun 24, 2025
834c5ef
add meta tags
zcesur Jun 24, 2025
64fc174
update bounties list
zcesur Jun 24, 2025
440d69b
update mcps
zcesur Jun 24, 2025
44c7e02
fix typo
zcesur Jun 24, 2025
0214a7a
init electric challenge
zcesur Jun 24, 2025
9f61bc6
updates
zcesur Jun 24, 2025
d98e097
updates
zcesur Jun 24, 2025
2d75a46
optimize bg image
zcesur Jun 24, 2025
b551e50
updates
zcesur Jun 24, 2025
752deb7
updates
zcesur Jun 24, 2025
1524c02
update og image
zcesur Jun 24, 2025
751e523
misc
zcesur Jun 29, 2025
2cfb3ba
Add ChallengeForm module and integrate challenge submission feature i…
zcesur Jul 1, 2025
4429c24
Refactor mailer preheader handling, add preferences field to user sch…
zcesur Jul 3, 2025
e27da78
Comment out Algora Matches section in DashboardLive for conditional r…
zcesur Jul 3, 2025
212c342
feat: add realtime compensation strength indicator
zcesur Jul 3, 2025
3475cd9
fix: render nothing instead of 'Enter amount' when compensation field…
zcesur Jul 3, 2025
e04a3a3
feat: hide compensation strength indicator when amount is empty
zcesur Jul 3, 2025
044e6e0
feat: enhance compensation input handling and strength indicator
zcesur Jul 3, 2025
7435571
refactor: update strength labels and colors for compensation tiers
zcesur Jul 3, 2025
1f6af9c
feat: enhance screenshot upload notifications in OGImageController
zcesur Jul 3, 2025
74edc5e
fix: improve tech stack query logic in workspace module
zcesur Jul 3, 2025
18bb196
feat: add abbreviation lookup for country codes
zcesur Jul 3, 2025
2d8f55f
update examples
zcesur Jul 4, 2025
bb0ba26
feat: add internal_email field to User schema and admin interface
zcesur Jul 4, 2025
517c2ba
feat: add job invitation fields to User schema and migration
zcesur Jul 4, 2025
fedf663
feat: add internal_notes field to User schema and migration
zcesur Jul 4, 2025
994d09d
refactor: remove checkbox normalization from job_preferences_changeset
zcesur Jul 4, 2025
f66fa9a
add multiline component
zcesur Jul 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions assets/js/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Partial<ViewHook> & Record<string, unknown>>;

// Accessible focus handling
Expand Down
23 changes: 22 additions & 1 deletion lib/algora/accounts/schemas/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/algora/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 13 additions & 6 deletions lib/algora/mailer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,7 @@ defmodule Algora.Mailer do
</xml><![endif]-->
</head>
<body style="margin: 0; padding: 0; min-width: 100%; background-color: #ffffff;">
<div class="preheader" style="display: none; max-width: 0; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: #fff; opacity: 0;">
#{opts[:preheader]}
</div>
<div style="display: none; max-height: 0px; overflow: hidden;">
&#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy;
</div>
#{preheader_section(opts[:preheader])}
<div style="background-color: #ffffff; box-sizing: border-box; display: block; padding: 0;">
<table cellpadding="0" cellspacing="0" width="100%">
<tr>
Expand Down Expand Up @@ -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: """
<div class="preheader" style="display: none; max-width: 0; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: #fff; opacity: 0;">
#{preheader}
</div>
<div style="display: none; max-height: 0px; overflow: hidden;">
&#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy; &#847; &zwnj; &nbsp; &#8199; &shy;
</div>
"""
end
9 changes: 9 additions & 0 deletions lib/algora/psp/connect_countries.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
81 changes: 55 additions & 26 deletions lib/algora/settings/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading