diff --git a/Gemfile b/Gemfile index 0f3b34e..1f83dd0 100644 --- a/Gemfile +++ b/Gemfile @@ -61,6 +61,9 @@ gem "appsignal", "~> 4.8", ">= 4.8.4" # honeybadger gem "honeybadger", "~> 6.6" +# rack-cors +gem "rack-cors", ">= 3.0" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" diff --git a/Gemfile.lock b/Gemfile.lock index bd9e366..e8c42f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -247,6 +247,9 @@ GEM raabro (1.4.0) racc (1.8.1) rack (3.2.3) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -463,6 +466,7 @@ DEPENDENCIES propshaft pry-rails puma (>= 5.0) + rack-cors (>= 3.0) rails (~> 8.0.3) rspec-rails rubocop-rails-omakase diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 980cbce..daf0402 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -230,30 +230,7 @@ th a:hover { @media (min-width: 1024px) { .dashboard-kpi-cards { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - - .dashboard-kpi-cards .funnel-card:nth-child(-n+2)::after { - content: ""; - position: absolute; - top: 50%; - right: -0.45rem; - width: 0.9rem; - border-top: 2px dashed var(--border-color); - transform: translateY(-50%); - z-index: 2; - } - - .dashboard-kpi-cards .funnel-card:nth-child(-n+2)::before { - content: ""; - position: absolute; - top: 50%; - right: -0.62rem; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 7px solid var(--border-color); - transform: translateY(-50%); - z-index: 2; + grid-template-columns: repeat(3, minmax(0, 1fr)); } } diff --git a/app/controllers/api/v1/opportunities_controller.rb b/app/controllers/api/v1/opportunities_controller.rb new file mode 100644 index 0000000..3c63bdc --- /dev/null +++ b/app/controllers/api/v1/opportunities_controller.rb @@ -0,0 +1,29 @@ +class Api::V1::OpportunitiesController < ApplicationController + def index + limit = params.fetch(:limit, 20).to_i + limit = [ [ limit, 1 ].max, 100 ].min + + scope = current_or_demo_user + .opportunities + .includes(:company) + .order(id: :desc) + + if params[:cursor].present? + scope = scope.where("id < ?", params[:cursor].to_i) + end + + rows = scope.limit(limit + 1).to_a + has_more = rows.length > limit + data = has_more ? rows.first(limit) : rows + next_cursor = has_more ? data.last.id : nil + + render json: { + data: data, + meta: { + limit: limit, + has_more: has_more, + next_cursor: next_cursor + } + } + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 96aa74f..d901352 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -5,7 +5,6 @@ def index @total_resumes = submitted_opportunities.count @total_open_applications = opportunities.where(status: %w[applied interviewing]).count - @total_assessed = opportunities.count # Count interview processes, not interview rounds: 1 per submitted opportunity that has interviews. @total_interviews = submitted_opportunities.joins(:interview_sessions).distinct.count @interview_conversion_rate = if @total_resumes.positive? diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index a94ddfc..f12f95b 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -1,2 +1,76 @@ module DashboardHelper + def pie_chart_svg(data, size: 200, colors: nil) + # data should be a hash of {label => value} or array of {name, value} + values = data.is_a?(Hash) ? data.values : data.map { |d| d[:value] || d[:count] } + total = values.sum.to_f + + # Enhanced color palette with yellow, green, and light blue for better contrast + default_colors = [ "#6366f1", "#22c55e", "#eab308", "#60a5fa", "#f43f5e", "#ec4899", "#f97316" ] + colors ||= default_colors + + svg_data = [] + current_angle = -90 + + data_array = data.is_a?(Hash) ? data.to_a : data + + data_array.each_with_index do |item, index| + label = data.is_a?(Hash) ? item[0] : item[:name] + value = data.is_a?(Hash) ? item[1] : (item[:value] || item[:count]) + percentage = (value.to_f / total * 100).round(1) + + # Calculate angle + angle = percentage * 3.6 + start_angle = current_angle + end_angle = current_angle + angle + + # Convert to radians + start_rad = (start_angle * Math::PI / 180) + end_rad = (end_angle * Math::PI / 180) + + # Calculate coordinates + radius = size / 2 - 10 + cx = size / 2 + cy = size / 2 + + x1 = cx + radius * Math.cos(start_rad) + y1 = cy + radius * Math.sin(start_rad) + x2 = cx + radius * Math.cos(end_rad) + y2 = cy + radius * Math.sin(end_rad) + + large_arc = angle > 180 ? 1 : 0 + + color = colors[index % colors.length] + + # SVG path for pie slice + path_data = "M #{cx},#{cy} L #{x1},#{y1} A #{radius},#{radius} 0 #{large_arc},1 #{x2},#{y2} Z" + svg_data << { + path: path_data, + color: color, + label: label, + value: value, + percentage: percentage + } + + current_angle = end_angle + end + + # Generate SVG + svg = "" + svg_data.each do |slice| + svg += "" + end + svg += "" + + # Return SVG and legend + legend_html = "
" + svg_data.each do |slice| + legend_html += "
" + legend_html += "" + legend_html += "#{slice[:label].to_s.humanize}: #{slice[:value]} (#{slice[:percentage]}%)" + legend_html += "
" + end + legend_html += "
" + + "
#{svg}#{legend_html}
".html_safe + end end diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 1fd4860..9fa9318 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -4,16 +4,6 @@
-
-
-

Assessed Opportunities

-
-
-
<%= @total_assessed %>
-

Total opportunities tracked

-
-
-

Total Applications Submitted

@@ -70,63 +60,28 @@
-
+
<% if @focus_insights.any? %> -
+

🎯 Top 4 Tech Skills to Focus On

📚
-
- <% @focus_insights.each_with_index do |insight, index| %> -
-
-
- <%= index + 1 %>. <%= insight[:name] %> - <% if index == 0 %> - TOP PRIORITY - <% end %> -
- <% if insight[:percentage].present? %> - <%= insight[:percentage] %>% - <% end %> -
-
- <%= insight[:insight] %> -
- <% if insight[:count].present? %> -
- Appears in <%= insight[:count] %> of your tracked opportunities -
- <% end %> -
- <% end %> -
+ <% insight_data = @focus_insights.map { |i| { name: i[:name], value: i[:percentage] || 10 } } %> + <%= pie_chart_svg(insight_data, size: 220) %>
<% end %> <% if @source_data.any? %> -
+

Source Distribution

🔍
-
- <% max_count_source = @source_data.values.max %> - <% @source_data.first(6).each do |source, count| %> -
-
<%= source&.humanize %>
-
-
- <%= count %> -
-
-
- <% end %> -
+ <%= pie_chart_svg(@source_data, size: 200) %>
<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 715ca78..5a61d25 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -20,8 +20,8 @@ <%# Includes all stylesheet files in app/assets/stylesheets %> - <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> - <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload", "as": "stylesheet" %> + <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload", "as": "stylesheet" %> <%= javascript_importmap_tags %> <%= Sentry.get_trace_propagation_meta.html_safe %> diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..84227b7 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,9 @@ +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins "http://localhost:5173", "http://localhost:3004" + resource "/api/*", + headers: :any, + methods: [ :get, :post, :put, :patch, :delete, :options, :head ], + credentials: false + end +end diff --git a/config/routes.rb b/config/routes.rb index a717f76..b44fa89 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,13 @@ post :generate_from_opportunity end end + + namespace :api, defaults: { format: :json } do + namespace :v1 do + resources :opportunities, only: [ :index ] + end + end + get "resources/guides/behavioral" => "resource_sheets#behavioral_guide", as: :behavioral_guide get "resources/guides/technical" => "resource_sheets#technical_guide", as: :technical_guide get "resources/guides/interviewer-questions" => "resource_sheets#interviewer_questions_guide", as: :interviewer_questions_guide