diff --git a/Gemfile.lock b/Gemfile.lock index 34249d0..5b9e90b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,31 +1,31 @@ GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.17) + action_text-trix (2.1.18) railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,38 +33,38 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) + actiontext (8.1.3) action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) timeout (>= 0.4.0) activerecord-import (2.2.0) activerecord (>= 4.2) - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -82,7 +82,7 @@ GEM ast (2.4.3) autotuner (1.1.0) aws-eventstream (1.4.0) - aws-partitions (1.1229.0) + aws-partitions (1.1231.0) aws-sdk-core (3.244.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -109,7 +109,7 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (4.0.1) + bigdecimal (4.1.0) bindex (0.8.1) bootsnap (1.23.0) msgpack (~> 1.2) @@ -189,22 +189,22 @@ GEM faraday-net_http_persistent (2.3.1) faraday (~> 2.5) net-http-persistent (>= 4.0.4, < 5) - ffi (1.17.3-aarch64-linux-gnu) - ffi (1.17.3-aarch64-linux-musl) - ffi (1.17.3-arm-linux-gnu) - ffi (1.17.3-arm-linux-musl) - ffi (1.17.3-arm64-darwin) - ffi (1.17.3-x86_64-linux-gnu) - ffi (1.17.3-x86_64-linux-musl) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) flamegraph (0.9.5) - flipper (1.4.0) + flipper (1.4.1) concurrent-ruby (< 2) - flipper-active_record (1.4.0) + flipper-active_record (1.4.1) activerecord (>= 4.2, < 9) - flipper (~> 1.4.0) - flipper-ui (1.4.0) + flipper (~> 1.4.1) + flipper-ui (1.4.1) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 1.4.0) + flipper (~> 1.4.1) rack (>= 1.4, < 4) rack-protection (>= 1.5.3, < 5.0.0) rack-session (>= 1.0.2, < 3.0.0) @@ -245,7 +245,7 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - inertia_rails (3.18.0) + inertia_rails (3.19.0) railties (>= 6) io-console (0.8.2) irb (1.17.0) @@ -257,7 +257,7 @@ GEM actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) - json (2.19.2) + json (2.19.3) json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) @@ -300,8 +300,6 @@ GEM activesupport (>= 7.1) marcel (1.1.0) matrix (0.4.3) - mcp (0.9.0) - json-schema (>= 4.1) memory_profiler (1.1.0) mini_magick (5.3.1) logger @@ -357,7 +355,7 @@ GEM activerecord (>= 7.1) request_store (~> 1.4) parallel (1.27.0) - parser (3.3.10.2) + parser (3.3.11.1) ast (~> 2.4.1) racc pg (1.6.3) @@ -366,7 +364,7 @@ GEM pg (1.6.3-arm64-darwin) pg (1.6.3-x86_64-linux) pg (1.6.3-x86_64-linux-musl) - posthog-ruby (3.6.0) + posthog-ruby (3.6.1) concurrent-ruby (~> 1) pp (0.6.3) prettyprint @@ -416,20 +414,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.1.3) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -437,9 +435,9 @@ GEM rails-html-sanitizer (1.7.0) loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -487,11 +485,10 @@ GEM rswag-ui (2.17.0) actionpack (>= 5.2, < 8.2) railties (>= 5.2, < 8.2) - rubocop (1.85.1) + rubocop (1.86.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - mcp (~> 0.6) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) @@ -587,10 +584,10 @@ GEM tailwindcss-ruby (4.2.1-x86_64-linux-gnu) tailwindcss-ruby (4.2.1-x86_64-linux-musl) thor (1.5.0) - thruster (0.1.19) - thruster (0.1.19-aarch64-linux) - thruster (0.1.19-arm64-darwin) - thruster (0.1.19-x86_64-linux) + thruster (0.1.20) + thruster (0.1.20-aarch64-linux) + thruster (0.1.20-arm64-darwin) + thruster (0.1.20-x86_64-linux) timeout (0.6.1) tsort (0.2.0) turbo-rails (2.0.23) @@ -608,7 +605,7 @@ GEM vite_rails (3.10.0) railties (>= 5.1, < 9) vite_ruby (~> 3.0, >= 3.2.2) - vite_ruby (3.10.0) + vite_ruby (3.10.1) dry-cli (>= 0.7, < 2) logger (~> 1.6) mutex_m diff --git a/app/controllers/inertia_controller.rb b/app/controllers/inertia_controller.rb index 6f44b88..10c85ce 100644 --- a/app/controllers/inertia_controller.rb +++ b/app/controllers/inertia_controller.rb @@ -65,7 +65,7 @@ class InertiaController < ApplicationController def inertia_primary_links links = [] links << inertia_link("Home", root_path, active: helpers.current_page?(root_path), inertia: true) - links << inertia_link("Leaderboards", leaderboards_path, active: helpers.current_page?(leaderboards_path)) + links << inertia_link("Leaderboards", leaderboards_path, active: helpers.current_page?(leaderboards_path), inertia: true) if current_user links << inertia_link("Projects", my_projects_path, active: request.path.start_with?("/my/projects"), inertia: true) diff --git a/app/controllers/leaderboards_controller.rb b/app/controllers/leaderboards_controller.rb index de613d4..5228994 100644 --- a/app/controllers/leaderboards_controller.rb +++ b/app/controllers/leaderboards_controller.rb @@ -1,29 +1,24 @@ -class LeaderboardsController < ApplicationController - PER_PAGE = 100 - LEADERBOARD_SCOPES = %w[global country].freeze +class LeaderboardsController < InertiaController + layout "inertia" def index - @period_type = validated_period_type - load_country_context - @leaderboard_scope = validated_leaderboard_scope - @leaderboard = LeaderboardService.get(period: @period_type, date: start_date) - @leaderboard.nil? ? flash.now[:notice] = "Leaderboard is being updated..." : load_metadata - end + period_type = validated_period_type + country = load_country_context + leaderboard_scope = validated_leaderboard_scope(country) - def entries - @period_type = validated_period_type - load_country_context - @leaderboard_scope = validated_leaderboard_scope - @leaderboard = LeaderboardService.get(period: @period_type, date: start_date) - return head :no_content unless @leaderboard&.persisted? + leaderboard = LeaderboardService.get(period: period_type, date: Date.current) - page = [ (params[:page] || 1).to_i, 1 ].max - @entries = leaderboard_entries_scope.includes(:user).order(total_seconds: :desc) - .offset((page - 1) * PER_PAGE).limit(PER_PAGE) - @active_projects = Cache::ActiveProjectsJob.perform_now - @offset = (page - 1) * PER_PAGE - - render partial: "entries", locals: { entries: @entries, active_projects: @active_projects, offset: @offset } + render inertia: "Leaderboards/Index", props: { + period_type: period_type.to_s, + scope: leaderboard_scope.to_s, + country: country, + leaderboard: leaderboard_metadata(leaderboard), + is_logged_in: current_user.present?, + github_uid_blank: current_user&.github_uid.blank? || false, + github_auth_path: "/auth/github", + settings_path: my_settings_path, + entries: InertiaRails.defer { entries_payload(leaderboard, leaderboard_scope, country) } + } end private @@ -33,42 +28,68 @@ class LeaderboardsController < ApplicationController %w[daily last_7_days].include?(p) ? p.to_sym : :daily end - def validated_leaderboard_scope - requested_scope = params[:scope].to_s - requested_scope = "global" unless LEADERBOARD_SCOPES.include?(requested_scope) - requested_scope = "global" if requested_scope == "country" && !@country_scope_available - requested_scope.to_sym + def validated_leaderboard_scope(country) + requested = params[:scope].to_s + requested = "global" unless %w[global country].include?(requested) + requested = "global" if requested == "country" && !country[:available] + requested.to_sym end def load_country_context - country = ISO3166::Country.new(current_user&.country_code) - @country_code = country&.alpha2 - @country_name = country&.common_name - @country_scope_available = @country_code.present? && @country_name.present? + c = ISO3166::Country.new(current_user&.country_code) + { + code: c&.alpha2, + name: c&.common_name, + available: c&.alpha2.present? && c&.common_name.present? + } end - def country_scope? - @leaderboard_scope == :country && @country_scope_available + def leaderboard_metadata(leaderboard) + return nil unless leaderboard&.persisted? + + { + date_range_text: leaderboard.date_range_text, + updated_at: leaderboard.updated_at&.iso8601, + finished_generating: leaderboard.finished_generating?, + generation_duration_seconds: leaderboard.generation_duration_seconds + } end - def leaderboard_entries_scope - entries_scope = @leaderboard.entries - return entries_scope unless country_scope? + def entries_payload(leaderboard, scope, country) + return { entries: [], total: 0 } unless leaderboard&.persisted? - entries_scope.joins(:user).where(users: { country_code: @country_code }) - end + country_code = (scope == :country && country[:available]) ? country[:code] : nil + payload = LeaderboardPageCache.fetch( + leaderboard: leaderboard, + scope: scope, + country_code: country_code + ) - def start_date - @start_date ||= Date.current - end + active_projects = Cache::ActiveProjectsJob.perform_now - def load_metadata - return unless @leaderboard.persisted? + entries = payload[:entries].map do |e| + user = e[:user] + proj = active_projects&.dig(e[:user_id]) + { + user_id: e[:user_id], + total_seconds: e[:total_seconds], + streak_count: e[:streak_count], + is_current_user: e[:user_id] == current_user&.id, + user: { + display_name: user[:display_name], + avatar_url: user[:avatar_url], + profile_path: user[:profile_path], + verified: user[:verified], + country_code: user[:country_code], + red: user[:red] + }, + active_project: proj ? { name: proj.project_name, repo_url: proj.repo_url } : nil + } + end - entries_scope = leaderboard_entries_scope - ids = entries_scope.distinct.pluck(:user_id) - @user_on_leaderboard = current_user && ids.include?(current_user.id) - @untracked_entries = 0 - @total_entries = entries_scope.count + { + entries: entries, + total: payload[:total_entries] + } end end diff --git a/app/javascript/layouts/AppLayout.svelte b/app/javascript/layouts/AppLayout.svelte index a38722b..fdba2c7 100644 --- a/app/javascript/layouts/AppLayout.svelte +++ b/app/javascript/layouts/AppLayout.svelte @@ -6,6 +6,7 @@ import type { Snippet } from "svelte"; import { onMount, onDestroy } from "svelte"; import plur from "plur"; + import { streakTheme, streakLabel } from "../utils"; type NavLink = { label: string; @@ -154,42 +155,6 @@ const footerStatsText = () => `${layout.footer.heartbeat_recent_count} ${plur("heartbeat", layout.footer.heartbeat_recent_count)} (${layout.footer.heartbeat_recent_imported_count} imported) in the past 24 hours. (DB: ${layout.footer.query_count} ${plur("query", layout.footer.query_count)}, ${layout.footer.query_cache_count} cached) (CACHE: ${layout.footer.cache_hits} hits, ${layout.footer.cache_misses} misses) (${layout.footer.requests_per_second})`; - const streakThemeClasses = (streakDays: number) => { - if (streakDays >= 30) { - return { - bg: "from-blue/20 to-purple/20", - hbg: "hover:from-blue/30 hover:to-purple/30", - bc: "border-blue", - ic: "text-blue group-hover:text-blue", - tc: "text-blue group-hover:text-blue", - tm: "text-blue", - }; - } - - if (streakDays >= 7) { - return { - bg: "from-red/20 to-orange/20", - hbg: "hover:from-red/30 hover:to-orange/30", - bc: "border-red", - ic: "text-red group-hover:text-red", - tc: "text-red group-hover:text-red", - tm: "text-red", - }; - } - - return { - bg: "from-orange/20 to-yellow/20", - hbg: "hover:from-orange/30 hover:to-yellow/30", - bc: "border-orange", - ic: "text-orange group-hover:text-orange", - tc: "text-orange group-hover:text-orange", - tm: "text-orange", - }; - }; - - const streakLabel = (streakDays: number) => - streakDays > 30 ? "30+" : `${streakDays}`; - const adminLevelLabel = (adminLevel?: AdminLevel | null) => { if (adminLevel === "superadmin") return "Superadmin"; if (adminLevel === "admin") return "Admin"; @@ -361,11 +326,9 @@ {#if layout.nav.current_user.streak_days && layout.nav.current_user.streak_days > 0} - {@const streakTheme = streakThemeClasses( - layout.nav.current_user.streak_days, - )} + {@const streak = streakTheme(layout.nav.current_user.streak_days)}
+ Set your country in + settings + to unlock regional leaderboards. +
+ {/if} + + {#if github_uid_blank} ++ Check back later for {period_type === "last_7_days" + ? "last 7 days" + : "last 24 hours"} results! +
++ Check back in a moment for {scope === "country" && country.name + ? `${country.name} ` + : ""}{period_type === "last_7_days" + ? "last 7 days" + : "last 24 hours"} results! +
+