Skip to content

Commit 0e80369

Browse files
authored
add challenges, update landing pages (#155)
1 parent 98ae54d commit 0e80369

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3152
-1037
lines changed

assets/js/app.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,167 @@ const Hooks = {
635635
});
636636
},
637637
},
638+
CompensationStrengthIndicator: {
639+
mounted() {
640+
const input = this.el.querySelector("input[type='text']");
641+
const strengthBar = this.el.querySelector("[data-strength-bar]");
642+
const strengthLabel = this.el.querySelector("[data-strength-label]");
643+
644+
if (!input || !strengthBar || !strengthLabel) return;
645+
646+
const minAmount = 50000;
647+
648+
const expandShorthand = (value: string): string => {
649+
const trimmed = value.trim().toLowerCase();
650+
651+
// Handle 'k' for thousands (e.g., "100k" -> "100000")
652+
if (trimmed.endsWith("k")) {
653+
const number = parseFloat(trimmed.slice(0, -1));
654+
if (!isNaN(number)) {
655+
return Math.floor(number * 1000).toString();
656+
}
657+
}
658+
659+
// Handle 'm' for millions (e.g., "1m" -> "1000000")
660+
if (trimmed.endsWith("m")) {
661+
const number = parseFloat(trimmed.slice(0, -1));
662+
if (!isNaN(number)) {
663+
return Math.floor(number * 1000000).toString();
664+
}
665+
}
666+
667+
// Return just the digits if no shorthand
668+
return value.replace(/[^0-9]/g, "");
669+
};
670+
671+
const formatWithCommas = (value: string): string => {
672+
// First expand any shorthand notation
673+
const expanded = expandShorthand(value);
674+
// Add commas for thousands separators
675+
return expanded.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
676+
};
677+
678+
const updateStrength = () => {
679+
const value = expandShorthand(input.value);
680+
const amount = parseInt(value) || 0;
681+
682+
let strength = 0;
683+
let label = "";
684+
let color = "bg-gray-200";
685+
686+
if (amount >= 500000) {
687+
strength = 99;
688+
label = "Big D Energy";
689+
color = "bg-emerald-500";
690+
} else if (amount >= 400000) {
691+
strength = 90;
692+
label = "Baller Status";
693+
color = "bg-emerald-500";
694+
} else if (amount >= 300000) {
695+
strength = 80;
696+
label = "High Roller";
697+
color = "bg-emerald-500";
698+
} else if (amount >= 200000) {
699+
strength = 70;
700+
label = "Big League";
701+
color = "bg-emerald-500";
702+
} else if (amount >= 150000) {
703+
strength = 60;
704+
label = "Major League";
705+
color = "bg-emerald-500";
706+
} else if (amount >= 100000) {
707+
strength = 50;
708+
label = "Six Figures";
709+
color = "bg-emerald-500";
710+
} else if (amount >= 75000) {
711+
strength = 40;
712+
label = "Solid Pay";
713+
color = "bg-emerald-500";
714+
} else if (amount >= minAmount) {
715+
strength = 30;
716+
label = "Decent";
717+
color = "bg-emerald-500";
718+
}
719+
720+
// Update strength bar
721+
strengthBar.style.width = `${strength}%`;
722+
strengthBar.className = `h-2 rounded-full transition-all duration-300 ${color}`;
723+
724+
// Show/hide the entire indicator section
725+
const indicatorSection = strengthBar.closest(".mt-2");
726+
if (amount >= minAmount) {
727+
indicatorSection.style.display = "block";
728+
} else {
729+
indicatorSection.style.display = "none";
730+
}
731+
732+
// Update label
733+
strengthLabel.textContent = label;
734+
strengthLabel.className = `text-sm font-medium transition-colors duration-300 ${
735+
strength >= 80
736+
? "text-emerald-500"
737+
: strength >= 60
738+
? "text-emerald-500"
739+
: strength >= 40
740+
? "text-emerald-500"
741+
: strength >= 20
742+
? "text-emerald-500"
743+
: "text-gray-600"
744+
}`;
745+
};
746+
747+
const handleInput = (e: Event) => {
748+
const target = e.target as HTMLInputElement;
749+
const cursorPosition = target.selectionStart || 0;
750+
const oldValue = target.value;
751+
752+
// Check if user just typed 'k' or 'm' to trigger expansion
753+
const shouldExpand =
754+
oldValue.toLowerCase().endsWith("k") ||
755+
oldValue.toLowerCase().endsWith("m");
756+
757+
let formattedValue: string;
758+
let newCursorPosition = cursorPosition;
759+
760+
if (shouldExpand) {
761+
// Expand shorthand and format with commas
762+
formattedValue = formatWithCommas(oldValue);
763+
// Place cursor at the end after expansion
764+
newCursorPosition = formattedValue.length;
765+
} else {
766+
// Just format with commas, preserving user input
767+
const digitsOnly = oldValue.replace(/[^0-9]/g, "");
768+
formattedValue = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
769+
770+
// Adjust cursor position to account for added/removed commas
771+
const oldCommas = (oldValue.match(/,/g) || []).length;
772+
const newCommas = (formattedValue.match(/,/g) || []).length;
773+
newCursorPosition = cursorPosition + (newCommas - oldCommas);
774+
}
775+
776+
// Only update if the value changed to prevent cursor jumping
777+
if (oldValue !== formattedValue) {
778+
target.value = formattedValue;
779+
780+
// Set cursor position after the DOM updates
781+
setTimeout(() => {
782+
target.setSelectionRange(newCursorPosition, newCursorPosition);
783+
}, 0);
784+
}
785+
786+
updateStrength();
787+
};
788+
789+
input.addEventListener("input", handleInput);
790+
input.addEventListener("keyup", updateStrength);
791+
792+
// Initial formatting and update
793+
if (input.value) {
794+
input.value = formatWithCommas(input.value);
795+
}
796+
updateStrength();
797+
},
798+
},
638799
} satisfies Record<string, Partial<ViewHook> & Record<string, unknown>>;
639800

640801
// Accessible focus handling

lib/algora/accounts/schemas/user.ex

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ defmodule Algora.Accounts.User do
2525

2626
field :type, Ecto.Enum, values: [:individual, :organization, :bot], default: :individual
2727
field :email, :string
28+
field :internal_email, :string
29+
field :internal_notes, :string
2830
field :name, :string
2931
field :display_name, :string
3032
field :handle, :string
@@ -62,6 +64,13 @@ defmodule Algora.Accounts.User do
6264
field :min_compensation, Money
6365
field :willing_to_relocate, :boolean, default: false
6466
field :us_work_authorization, :boolean, default: false
67+
field :preferences, :string
68+
69+
field :refer_to_company, :boolean, default: false
70+
field :company_domain, :string
71+
field :friends_recommendations, :boolean, default: false
72+
field :friends_github_handles, :string
73+
field :opt_out_algora, :boolean, default: false
6574

6675
field :total_earned, Money, virtual: true
6776
field :transactions_count, :integer, virtual: true
@@ -402,10 +411,22 @@ defmodule Algora.Accounts.User do
402411
:us_work_authorization,
403412
:linkedin_url,
404413
:twitter_url,
405-
:location
414+
:youtube_url,
415+
:website_url,
416+
:location,
417+
:preferences,
418+
:internal_email,
419+
:internal_notes,
420+
:refer_to_company,
421+
:company_domain,
422+
:friends_recommendations,
423+
:friends_github_handles,
424+
:opt_out_algora
406425
])
407426
|> validate_url(:linkedin_url)
408427
|> validate_url(:twitter_url)
428+
|> validate_url(:youtube_url)
429+
|> validate_url(:website_url)
409430
end
410431

411432
defp validate_url(changeset, field) do

lib/algora/application.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ defmodule Algora.Application do
2323
Algora.Github.Poller.RootSupervisor,
2424
Algora.ScreenshotQueue,
2525
Algora.RateLimit,
26+
AlgoraWeb.Data.HomeCache,
2627
# Start to serve requests, typically the last entry
2728
AlgoraWeb.Endpoint,
2829
Algora.Stargazer,

lib/algora/mailer.ex

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,7 @@ defmodule Algora.Mailer do
3737
</xml><![endif]-->
3838
</head>
3939
<body style="margin: 0; padding: 0; min-width: 100%; background-color: #ffffff;">
40-
<div class="preheader" style="display: none; max-width: 0; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: #fff; opacity: 0;">
41-
#{opts[:preheader]}
42-
</div>
43-
<div style="display: none; max-height: 0px; overflow: hidden;">
44-
&#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;
45-
</div>
40+
#{preheader_section(opts[:preheader])}
4641
<div style="background-color: #ffffff; box-sizing: border-box; display: block; padding: 0;">
4742
<table cellpadding="0" cellspacing="0" width="100%">
4843
<tr>
@@ -123,4 +118,16 @@ defmodule Algora.Mailer do
123118
defp html_section_by_type(_, text) do
124119
text
125120
end
121+
122+
defp preheader_section(nil), do: ""
123+
124+
defp preheader_section(preheader),
125+
do: """
126+
<div class="preheader" style="display: none; max-width: 0; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: #fff; opacity: 0;">
127+
#{preheader}
128+
</div>
129+
<div style="display: none; max-height: 0px; overflow: hidden;">
130+
&#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;
131+
</div>
132+
"""
126133
end

lib/algora/psp/connect_countries.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ defmodule Algora.PSP.ConnectCountries do
136136
end
137137
end
138138

139+
def abbr_from_code("US"), do: "US"
140+
141+
def abbr_from_code(code) do
142+
case Enum.find(list(), &(elem(&1, 1) == code)) do
143+
nil -> code
144+
{name, _} -> name
145+
end
146+
end
147+
139148
@spec list_codes() :: [String.t()]
140149
def list_codes, do: Enum.map(list(), &elem(&1, 1))
141150

lib/algora/settings/settings.ex

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -106,44 +106,69 @@ defmodule Algora.Settings do
106106
opts = Keyword.put_new(opts, :limit, 1000)
107107

108108
case get("job_matches:#{job.id}") do
109+
%{"matches_2" => matches} when is_list(matches) ->
110+
matches
111+
|> Enum.map(fn %{"user_id" => id} -> %{user_id: id} end)
112+
|> load_matches_2()
113+
109114
%{"matches" => matches} when is_list(matches) ->
110115
matches
111116
|> load_matches()
112117
|> Enum.take(opts[:limit])
113118

114119
_ ->
115-
[
116-
tech_stack: job.tech_stack,
117-
email_required: false,
118-
sort_by:
119-
case get_job_criteria(job) do
120-
criteria when map_size(criteria) > 0 -> criteria
121-
_ -> [{"solver", true}]
122-
end
123-
]
124-
|> Keyword.merge(opts)
125-
|> Algora.Cloud.list_top_matches()
126-
|> load_matches_2()
120+
matches =
121+
[
122+
tech_stack: job.tech_stack,
123+
email_required: false,
124+
sort_by:
125+
case get_job_criteria(job) do
126+
criteria when map_size(criteria) > 0 -> criteria
127+
_ -> [{"solver", true}]
128+
end
129+
]
130+
|> Keyword.merge(opts)
131+
|> Algora.Cloud.list_top_matches()
132+
133+
# Cache the raw matches for future calls
134+
_count = get_job_matches_count(job, opts)
135+
set_job_matches_2(job.id, matches)
136+
137+
load_matches_2(matches)
127138
end
128139
end
129140

141+
def set_job_matches_count(job_id, count) when is_binary(job_id) and is_integer(count) do
142+
set("job_matches_count:#{job_id}", %{"count" => count})
143+
end
144+
130145
def get_job_matches_count(job, opts \\ []) do
131-
case get("job_matches:#{job.id}") do
132-
%{"matches" => matches} when is_list(matches) ->
133-
length(matches)
146+
case get("job_matches_count:#{job.id}") do
147+
%{"count" => count} when is_integer(count) ->
148+
count
134149

135150
_ ->
136-
[
137-
tech_stack: job.tech_stack,
138-
email_required: false,
139-
sort_by:
140-
case get_job_criteria(job) do
141-
criteria when map_size(criteria) > 0 -> criteria
142-
_ -> [{"solver", true}]
143-
end
144-
]
145-
|> Keyword.merge(opts)
146-
|> Algora.Cloud.count_top_matches()
151+
count =
152+
case get("job_matches:#{job.id}") do
153+
%{"matches" => matches} when is_list(matches) ->
154+
length(matches)
155+
156+
_ ->
157+
[
158+
tech_stack: job.tech_stack,
159+
email_required: false,
160+
sort_by:
161+
case get_job_criteria(job) do
162+
criteria when map_size(criteria) > 0 -> criteria
163+
_ -> [{"solver", true}]
164+
end
165+
]
166+
|> Keyword.merge(opts)
167+
|> Algora.Cloud.count_top_matches()
168+
end
169+
170+
set_job_matches_count(job.id, count)
171+
count
147172
end
148173
end
149174

@@ -182,6 +207,10 @@ defmodule Algora.Settings do
182207
set("job_matches:#{job_id}", %{"matches" => matches})
183208
end
184209

210+
def set_job_matches_2(job_id, matches) when is_binary(job_id) and is_list(matches) do
211+
set("job_matches:#{job_id}", %{"matches_2" => matches})
212+
end
213+
185214
def get_tech_matches(tech) do
186215
case get("tech_matches:#{String.downcase(tech)}") do
187216
%{"matches" => matches} when is_list(matches) -> load_matches(matches)

0 commit comments

Comments
 (0)