Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,12 @@ div[style*="color: red"],
box-sizing: border-box;
}

.opportunity-form-page .form-group textarea.technology-picker-input {
max-width: none !important;
height: auto !important;
padding: 0.5rem !important;
}

/* Skill Badges */
.skill-badge {
display: inline-block;
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/opportunities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def index
@opportunities = @opportunities.order("#{sort_column} #{sort_direction}")
elsif sort_column == "company"
@opportunities = @opportunities.joins(:company).order("companies.name #{sort_direction}")
elsif sort_column == "industry"
@opportunities = @opportunities.joins(:company).order("companies.industry #{sort_direction}")
end
else
@opportunities = @opportunities.order(:application_date).reverse_order
Expand Down
42 changes: 42 additions & 0 deletions app/javascript/controllers/technology_picker_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = [ "input", "checkbox", "result" ]

apply() {
const pastedTechnologies = this.inputTarget.value
.split(/[;,\n]+/)
.map((technology) => technology.trim())
.filter(Boolean)

const checkboxesByName = new Map(
this.checkboxTargets.map((checkbox) => [
this.normalize(checkbox.dataset.technologyName),
checkbox
])
)
const unmatched = []
let selected = 0

pastedTechnologies.forEach((technology) => {
const checkbox = checkboxesByName.get(this.normalize(technology))

if (checkbox) {
checkbox.checked = true
selected += 1
} else {
unmatched.push(technology)
}
})

const messages = []
if (selected > 0) messages.push(`${selected} ${selected === 1 ? "technology" : "technologies"} selected`)
if (unmatched.length) messages.push(`Not found: ${unmatched.join(", ")}`)

this.resultTarget.textContent = messages.join(". ") || "Paste one or more technologies to match the catalog."
}

normalize(technology) {
return technology.toLowerCase().replace(/\s+/g, " ").trim()
}
}
17 changes: 1 addition & 16 deletions app/models/company.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Company < ApplicationRecord
has_many :resource_sheets, dependent: :nullify

before_validation :normalize_name
before_save :shorten_urls, :sanitize_size
before_save :sanitize_size

# Validations
validates :name, presence: true
Expand Down Expand Up @@ -77,21 +77,6 @@ def normalize_name
self.name = name.to_s.squish.presence
end

def shorten_urls
if website_changed? && website.present? && !website.include?("is.gd")
shortened = UrlShortenerService.shorten(website)
self.website = shortened if shortened.present?
end

if linkedin_changed? && linkedin.present? && !linkedin.include?("is.gd")
shortened = UrlShortenerService.shorten(linkedin)
self.linkedin = shortened if shortened.present?
end
rescue StandardError => e
Rails.logger.error("URL shortening failed in Company#shorten_urls: #{e.message}")
# Continue with save even if shortening fails
end

def sanitize_size
self.size = size&.gsub(",", "") if size.present?
end
Expand Down
3 changes: 3 additions & 0 deletions app/models/technology.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class Technology < ApplicationRecord
"Testing",
"DevOps/Infrastructure",
"API/Integration",
"AI/LLM",
"Observability",
"Event/Messaging",
"Other"
].freeze
end
15 changes: 4 additions & 11 deletions app/services/opportunities_csv_exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,13 @@ def generate
csv << headers

@opportunities.each do |opportunity|
# Combine structured technologies with other_tech_stack
tech_list = opportunity.technologies.order(:category, :name).pluck(:name).join(", ")
tech_list += ", #{opportunity.other_tech_stack}" if opportunity.other_tech_stack.present?
tech_list = tech_list.presence || opportunity.tech_stack # Fallback to old field if no structured data

csv << [
opportunity.role_type,
opportunity.company.name,
opportunity.company.company_type || "",
opportunity.company.size || "",
opportunity.company.industry,
opportunity.position_title,
opportunity.application_date,
opportunity.status,
tech_list,
opportunity.salary_range,
opportunity.chatgpt_match,
opportunity.jobright_match,
Expand All @@ -43,13 +37,12 @@ def generate

def headers
[
"Role Type",
"Company",
"Company Type",
"Company Size",
"Industry",
"Position Title",
"Application Date",
"Status",
"Tech Stack",
"Salary Range",
"ChatGPT Match",
"Jobright Match",
Expand Down
2 changes: 1 addition & 1 deletion app/services/tech_stack_analyzer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def analyze_main_stack_combinations
combination_counts = Hash.new(0)

# Main framework technologies to track
backend_frameworks = [ "Ruby on Rails", "Python", "Django", "Node.js", "Laravel", "Express" ]
backend_frameworks = [ "Ruby on Rails", "Python", "Django", "Node.js", "Express", "FastAPI" ]
frontend_frameworks = [ "React", "Vue", "Angular", "Stimulus", "Hotwire" ]

@opportunities.each do |opp|
Expand Down
13 changes: 10 additions & 3 deletions app/views/companies/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,18 @@
<%= form.url_field :linkedin, placeholder: "https://linkedin.com/...", class: "form-input" %>
</div>

<div class="form-group full-width-field">
<div class="form-group full-width-field" data-controller="technology-picker">
<%= form.label :known_tech_stack, "Known Technologies", class: "form-label" %>
<p class="form-hint">
Technologies this company uses (from research, website, job postings, etc.)
</p>
<label for="company_technology_paste" class="form-label">Paste Tech Stack</label>
<div style="display: flex; gap: 0.5rem; align-items: flex-start; margin-bottom: 0.5rem;">
<textarea id="company_technology_paste" class="form-input" rows="2" placeholder="Ruby on Rails, PostgreSQL, React" data-technology-picker-target="input"></textarea>
<button type="button" class="btn btn-secondary" data-action="technology-picker#apply">Apply</button>
</div>
<p class="form-hint" data-technology-picker-target="result" aria-live="polite"></p>
<% known_techs = company.known_tech_stack.present? ? company.known_tech_stack.split(",").map(&:strip) : [] %>
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; border-radius: 4px; background-color: var(--bg-primary, #fff);">
<% Technology::CATEGORIES.each do |category| %>
<% techs = Technology.where(category: category).order(:name) %>
Expand All @@ -75,9 +82,9 @@
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 5px;">
<% techs.each do |tech| %>
<label style="display: flex; align-items: center; cursor: pointer;">
<% known_techs = company.known_tech_stack.present? ? company.known_tech_stack.split(',').map(&:strip) : [] %>
<input type="checkbox" name="company[known_tech_stack_list][]" value="<%= tech.name %>"
<%= 'checked' if known_techs.include?(tech.name) %> />
data-technology-picker-target="checkbox" data-technology-name="<%= tech.name %>"
<%= 'checked' if known_techs.include?(tech.name) || (company.new_record? && tech.name == "Ruby on Rails") %> />
<span style="margin-left: 5px; font-size: 14px;"><%= tech.name %></span>
</label>
<% end %>
Expand Down
10 changes: 8 additions & 2 deletions app/views/opportunities/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,14 @@
<div id="role-specific-fields">
<!-- Software Engineer Fields -->
<div data-role-type-target="section" data-role="software_engineer" style="<%= opportunity.role_type == 'software_engineer' || opportunity.role_type.blank? ? '' : 'display: none;' %>">
<div class="form-group full-width-field">
<div class="form-group full-width-field" data-controller="technology-picker">
<%= form.label "Technologies" %>
<label for="opportunity_technology_paste" class="form-label">Paste Tech Stack</label>
<div style="display: flex; gap: 0.5rem; align-items: flex-start; margin-bottom: 0.5rem;">
<textarea id="opportunity_technology_paste" class="form-input technology-picker-input" rows="2" placeholder="Ruby on Rails, PostgreSQL, React" data-technology-picker-target="input"></textarea>
<button type="button" class="btn btn-secondary" data-action="technology-picker#apply">Apply</button>
</div>
<p class="form-hint" data-technology-picker-target="result" aria-live="polite"></p>
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; border-radius: 4px; background-color: var(--bg-primary, #fff);">
<% Technology::CATEGORIES.each do |category| %>
<% techs = Technology.where(category: category).order(:name) %>
Expand All @@ -83,7 +89,7 @@
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 5px;">
<% techs.each do |tech| %>
<label style="display: flex; align-items: center; cursor: pointer;">
<%= check_box_tag "opportunity[technology_ids][]", tech.id, opportunity.technologies.include?(tech), id: "opportunity_technology_ids_#{tech.id}" %>
<%= check_box_tag "opportunity[technology_ids][]", tech.id, opportunity.technologies.include?(tech) || (opportunity.new_record? && tech.name == "Ruby on Rails"), id: "opportunity_technology_ids_#{tech.id}", data: { technology_picker_target: "checkbox", technology_name: tech.name } %>
<span style="margin-left: 5px; font-size: 14px;"><%= tech.name %></span>
</label>
<% end %>
Expand Down
3 changes: 1 addition & 2 deletions app/views/opportunities/_opportunity.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
</span>
</td>
<td><%= opportunity.company.name if opportunity.company %></td>
<td><%= opportunity.company&.industry if opportunity.company %></td>
<td><%= opportunity.position_title %></td>
<td><%= opportunity.application_date %></td>
<td><%= opportunity.status.present? && opportunity.status != "select" ? opportunity.status : "" %></td>
<td class="hidden md:table-cell"><%= opportunity.source.present? ? opportunity.source.humanize : "" %></td>
<td class="hidden md:table-cell">
<% tech_sources = opportunity.technologies.map { |t| { name: t.name, category: t.category, source: :opportunity } } %>
<% if opportunity.other_tech_stack.present? %>
Expand Down Expand Up @@ -83,7 +83,6 @@
<% end %>
</td>
<td class="hidden md:table-cell"><%= opportunity.salary_range %></td>
<td class="hidden md:table-cell"><%= opportunity.bus_factor %></td>
<td class="hidden md:table-cell"><%= opportunity.chatgpt_match %></td>
<td class="hidden md:table-cell"><%= opportunity.jobright_match %></td>
<td class="hidden md:table-cell"><%= opportunity.linkedin_match %></td>
Expand Down
Loading
Loading