diff --git a/app/assets/stylesheets/leaderboard.css b/app/assets/stylesheets/leaderboard.css index 9be195d..186015e 100644 --- a/app/assets/stylesheets/leaderboard.css +++ b/app/assets/stylesheets/leaderboard.css @@ -52,4 +52,31 @@ margin: -4px 0; font-weight: bold; letter-spacing: 2px; -} \ No newline at end of file +} + +.period-toggle { + display: inline-flex; + background-color: #f1f1f1; + border-radius: 999px; + padding: 4px; + margin-bottom: 16px; +} + +.period-toggle-btn { + padding: 8px 16px; + border-radius: 999px; + text-decoration: none; + color: #444; + font-weight: 500; + transition: all 0.2s ease; +} + +.period-toggle-btn.active { + background-color: var(--primary-color); + color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.period-toggle-btn:hover:not(.active) { + background-color: #e0e0e0; +} diff --git a/app/controllers/leaderboards_controller.rb b/app/controllers/leaderboards_controller.rb index 71c86df..d82d4dc 100644 --- a/app/controllers/leaderboards_controller.rb +++ b/app/controllers/leaderboards_controller.rb @@ -1,9 +1,22 @@ class LeaderboardsController < ApplicationController def index - @leaderboard = Leaderboard.find_by(start_date: Date.current, deleted_at: nil) + @period_type = (params[:period_type] || 'daily').to_sym + @period_type = :daily unless [:daily, :weekly].include?(@period_type) + + start_date = if @period_type == :weekly + Date.current.beginning_of_week + else + Date.current + end + + @leaderboard = Leaderboard.find_by( + start_date: start_date, + period_type: @period_type, + deleted_at: nil + ) if @leaderboard.nil? - LeaderboardUpdateJob.perform_later + LeaderboardUpdateJob.perform_later(start_date, @period_type) flash.now[:notice] = "Leaderboard is being updated..." else @entries = @leaderboard.entries @@ -14,9 +27,14 @@ class LeaderboardsController < ApplicationController @user_on_leaderboard = current_user && tracked_user_ids.include?(current_user.id) unless @user_on_leaderboard - today = Time.current + time_range = if @period_type == :weekly + (start_date.beginning_of_day...(start_date + 7.days).beginning_of_day) + else + Time.current + end + @untracked_entries = Hackatime::Heartbeat - .where(time: today.beginning_of_day..today.end_of_day) + .where(time: time_range) .distinct .pluck(:user_id) .count { |user_id| !tracked_user_ids.include?(user_id) } diff --git a/app/jobs/leaderboard_update_job.rb b/app/jobs/leaderboard_update_job.rb index 51745b9..3214666 100644 --- a/app/jobs/leaderboard_update_job.rb +++ b/app/jobs/leaderboard_update_job.rb @@ -6,25 +6,40 @@ class LeaderboardUpdateJob < ApplicationJob # Limits concurrency to 1 job per date good_job_control_concurrency_with( - key: -> { arguments.first || Date.current.to_s }, + key: -> { "#{arguments[0] || Date.current.to_s}_#{arguments[1] || 'daily'}" }, total: 1, drop: true ) - def perform(date = Date.current) + def perform(date = Date.current, period_type = :daily) parsed_date = date.is_a?(Date) ? date : Date.parse(date.to_s) - leaderboard = Leaderboard.create!(start_date: parsed_date) + period_type = period_type.to_sym + + if period_type == :weekly + parsed_date = parsed_date.beginning_of_week + end + + leaderboard = Leaderboard.create!( + start_date: parsed_date, + period_type: period_type + ) # Get list of valid user IDs from our database valid_user_ids = User.pluck(:id) return if valid_user_ids.empty? + date_range = if period_type == :weekly + (parsed_date.beginning_of_day...(parsed_date + 7.days).beginning_of_day) + else + parsed_date.all_day + end + ActiveRecord::Base.transaction do valid_user_ids.each_slice(BATCH_SIZE) do |batch_user_ids| entries_data = Heartbeat.where(user_id: batch_user_ids) - .where(time: parsed_date.all_day) - .group(:user_id) - .duration_seconds + .where(time: date_range) + .group(:user_id) + .duration_seconds entries_data = entries_data.filter { |_, total_seconds| total_seconds > 60 } @@ -45,8 +60,10 @@ class LeaderboardUpdateJob < ApplicationJob leaderboard.finished_generating_at = Time.current leaderboard.save! - # Delete previous leaderboard entries from today - Leaderboard.where.not(id: leaderboard.id).where(start_date: parsed_date).where(deleted_at: nil).update_all(deleted_at: Time.current) + Leaderboard.where.not(id: leaderboard.id) + .where(start_date: parsed_date, period_type: period_type) + .where(deleted_at: nil) + .update_all(deleted_at: Time.current) rescue => e Rails.logger.error "Failed to update current leaderboard: #{e.message}" raise diff --git a/app/models/leaderboard.rb b/app/models/leaderboard.rb index 185c9ad..581ec1e 100644 --- a/app/models/leaderboard.rb +++ b/app/models/leaderboard.rb @@ -4,8 +4,29 @@ class Leaderboard < ApplicationRecord dependent: :destroy validates :start_date, presence: true + + enum :period_type, { + daily: 0, + weekly: 1 + } def finished_generating? finished_generating_at.present? end -end + + def period_end_date + if weekly? + start_date + 6.days + else + start_date + end + end + + def date_range_text + if weekly? + "#{start_date.strftime('%b %d')} - #{period_end_date.strftime('%b %d, %Y')}" + else + start_date.strftime("%B %d, %Y") + end + end +end \ No newline at end of file diff --git a/db/migrate/20250312110220_add_period_type_to_leaderboards.rb b/db/migrate/20250312110220_add_period_type_to_leaderboards.rb new file mode 100644 index 0000000..b9b1d7f --- /dev/null +++ b/db/migrate/20250312110220_add_period_type_to_leaderboards.rb @@ -0,0 +1,5 @@ +class AddPeriodTypeToLeaderboards < ActiveRecord::Migration[8.0] + def change + add_column :leaderboards, :period_type, :integer, default: 0, null: false + end +end