diff --git a/app/assets/stylesheets/filterable_dashboard.css b/app/assets/stylesheets/filterable_dashboard.css new file mode 100644 index 0000000..12eaa15 --- /dev/null +++ b/app/assets/stylesheets/filterable_dashboard.css @@ -0,0 +1,517 @@ +.filter .options-container { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: rgba(30, 33, 37, 0.95); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + margin-top: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + z-index: 1000; + padding: 0; +} + +.filter .search-input { + width: 100%; + border: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding: 8px; + margin: 0; + background: transparent; + color: rgba(255, 255, 255, 0.9); + font-size: 13px; + border-radius: 0; + height: auto; +} + +.filter .search-input::placeholder { + color: rgba(255, 255, 255, 0.4); +} + +.filter .search-input:focus { + outline: none; + box-shadow: none; + border-color: rgba(255, 255, 255, 0.2); +} + +.filter .options-list { + max-height: 200px; + overflow-y: auto; + padding: 0; + margin: 0; +} + +.filter .option { + display: flex; + align-items: center; + padding: 6px 8px; + cursor: pointer; + font-size: 13px; + color: rgba(255, 255, 255, 0.9); + margin: 0; + background: transparent; +} + +.filter .option:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.filter .option input[type="checkbox"] { + margin-right: 8px; + margin-bottom: 0; + height: 14px; + width: 14px; + min-width: 14px; + appearance: none; + -webkit-appearance: none; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 3px; + background: rgba(255, 255, 255, 0.1); + position: relative; + cursor: pointer; + padding: 0; +} + +.filter .option input[type="checkbox"]:checked { + background: #3291ff; + border-color: #3291ff; +} + +.filter .option input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.filter .option input[type="checkbox"]:hover { + border-color: rgba(255, 255, 255, 0.5); +} + +.filter .select-header-container { + display: flex; + align-items: center; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.05); + margin: 0; + padding: 0; +} + +.filter .select-header { + flex: 1; + padding: 6px 8px; + font-size: 13px; + cursor: pointer; + user-select: none; + color: rgba(255, 255, 255, 0.7); + margin: 0; + background: transparent; +} + +.filter .clear-button { + padding: 6px 8px; + font-size: 16px; + line-height: 1; + color: rgba(255, 255, 255, 0.4); + background: none; + border: none; + border-left: 1px solid rgba(255, 255, 255, 0.1); + cursor: pointer; + margin: 0; +} + +.filter .clear-button:hover { + color: #ff4444; + background-color: rgba(255, 255, 255, 0.05); +} + +.filter .option.hidden { + display: none; +} + +@media (prefers-color-scheme: light) { + .filter .options-container { + background: white; + border: 1px solid #ddd; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .filter .search-input { + border-bottom: 1px solid #eee; + color: var(--text-color); + } + + .filter .search-input::placeholder { + color: #999; + } + + .filter .option { + color: var(--text-color); + } + + .filter .option:hover { + background-color: #f5f5f5; + } + + .filter .select-header-container { + border: 1px solid #ddd; + background-color: white; + } + + .filter .select-header { + color: #666; + } + + .filter .clear-button { + color: #666; + border-left: 1px solid #ddd; + } + + .filter .clear-button:hover { + color: #ff4444; + background-color: rgba(0, 0, 0, 0.05); + } + + .filter .option input[type="checkbox"] { + border: 1px solid rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, 0.05); + } + + .filter .option input[type="checkbox"]:checked { + background: #3291ff; + border-color: #3291ff; + } + + .filter .option input[type="checkbox"]:hover { + border-color: rgba(0, 0, 0, 0.4); + } +} + +.dashboard-wrapper { + display: flex; + flex-direction: column; + gap: 1.5rem; + width: 100%; +} + +.dashboard-wrapper .stats-section { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; +} + +.dashboard-wrapper .dashboard-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + width: 100%; +} + +.dashboard-wrapper .stat-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 6px; + padding: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: transform 0.2s ease; +} + +.dashboard-wrapper .stat-label { + color: rgba(255, 255, 255, 0.5); + font-size: 0.5rem; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.dashboard-wrapper .stat-value { + font-size: 1rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); +} + +.dashboard-wrapper .card { + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 1.5rem; + display: flex; + flex-direction: column; +} + +.dashboard-wrapper .card h2 { + margin-bottom: 1rem; + font-size: 1.1rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); +} + +.dashboard-wrapper .chart-container { + flex: 1; + position: relative; + height: 140px; +} + +.dashboard-wrapper .chart-container canvas { + max-height: 100%; + width: 100%; +} + +.dashboard-wrapper .bar-graph { + margin-top: 1rem; +} + +.dashboard-wrapper .bar-row { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.dashboard-wrapper .bar-label { + width: 150px; + text-align: right; + padding-right: 1rem; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.dashboard-wrapper .bar-container { + flex: 1; + height: 24px; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + overflow: hidden; +} + +.dashboard-wrapper .bar { + height: 100%; + background: #85e394; + border-radius: 4px; + position: relative; + transition: width 0.3s ease; +} + +.dashboard-wrapper .bar-value { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + font-size: 0.8rem; + color: rgba(0, 0, 0, 0.8); +} + +.dashboard-wrapper .no-data { + text-align: center; + padding: 2rem; + color: rgba(255, 255, 255, 0.5); + font-style: italic; +} + +@media (prefers-color-scheme: light) { + .dashboard-wrapper .stat-card { + background: var(--smoke); + border: 1px solid rgba(0, 0, 0, 0.1); + } + + .dashboard-wrapper .stat-label { + color: #666; + } + + .dashboard-wrapper .stat-value { + color: var(--text-color); + } + + .dashboard-wrapper .card { + background: white; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .dashboard-wrapper .card h2 { + color: var(--text-color); + } + + .dashboard-wrapper .bar-label { + color: var(--text-color); + } + + .dashboard-wrapper .bar-container { + background: var(--smoke); + } + + .dashboard-wrapper .bar-value { + color: var(--text-inverse); + } + + .dashboard-wrapper .no-data { + color: #666; + } +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; +} + +.content { + max-width: 1200px; + margin: 0 auto; +} + +h1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: 1.5rem; +} + +.filters-section { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.filters { + margin-bottom: 2rem; + background: #f5f5f5; + padding: 1rem; + border-radius: 4px; +} + +.filter-group { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + align-items: flex-start; +} + +.filter { + flex: 1; + min-width: 150px; + position: relative; +} + +.filter-label { + display: block; + font-size: 0.9rem; + margin-bottom: 0.25rem; + color: #666; +} + +.custom-select { + position: relative; + width: 100%; +} + +.select-header-container { + display: flex; + align-items: center; + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; +} + +.select-header { + flex: 1; + padding: 0.4rem 0.5rem; + font-size: 0.8rem; + cursor: pointer; + user-select: none; +} + +.clear-button { + padding: 0.4rem 0.5rem; + font-size: 1rem; + line-height: 1; + color: #666; + background: none; + border: none; + border-left: 1px solid #ddd; + cursor: pointer; + display: none; +} + +.clear-button:hover { + color: #ff4444; + background-color: rgba(0, 0, 0, 0.05); +} + +.options-container { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 4px; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.custom-select.active .options-container { + display: block; +} + +.option { + display: flex; + align-items: center; + padding: 0.35rem 0.5rem; + cursor: pointer; + transition: background-color 0.2s; + font-size: 0.8rem; +} + +.option:hover { + background-color: #f5f5f5; +} + +.option input[type="checkbox"] { + margin-right: 0.5rem; + transform: scale(0.9); +} + +.time-filter { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + background: white; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + white-space: nowrap; +} + +.calendar-icon { + font-size: 1.1rem; +} + +.dropdown-arrow { + font-size: 0.8rem; + color: #666; +} + +#dashboard-content { + min-height: 200px; + width: 100%; +} + +#dashboard-content.loading .dashboard-wrapper { + filter: grayscale(1) opacity(0.7); + pointer-events: none; + transition: filter 0.2s ease; +} \ No newline at end of file diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index cc8efe0..503288e 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -48,6 +48,11 @@ class StaticPagesController < ApplicationController @todays_languages = language_counts.map(&:first) @todays_editors = editor_counts.map(&:first) @show_logged_time_sentence = @todays_languages.any? || @todays_editors.any? + + cached_data = filterable_dashboard_data + cached_data.entries.each do |key, value| + instance_variable_set("@#{key}", value) + end else @social_proof ||= begin # Only run queries as needed, starting with the smallest time range @@ -145,6 +150,24 @@ class StaticPagesController < ApplicationController render partial: "currently_hacking", locals: locals end + def filterable_dashboard + cached_data = filterable_dashboard_data + cached_data.entries.each do |key, value| + instance_variable_set("@#{key}", value) + end + + render partial: "filterable_dashboard" + end + + def filterable_dashboard_content + cached_data = filterable_dashboard_data + cached_data.entries.each do |key, value| + instance_variable_set("@#{key}", value) + end + + render partial: "filterable_dashboard_content" + end + def 🃏 redirect_to root_path unless current_user && current_user.slack_uid.present? @@ -220,4 +243,95 @@ class StaticPagesController < ApplicationController "#{count_unique.to_s + ' Hack Clubber'.pluralize(count_unique)} set up Hackatime #{humanized_time_period}" end + + def filterable_dashboard_data + filters = %i[project language operating_system editor] + + # Cache key based on user and filter parameters + cache_key = [] + cache_key << current_user + filters.each do |filter| + cache_key << params[filter] + end + + filtered_heartbeats = current_user.heartbeats + # Load filter options and apply filters with caching + Rails.cache.fetch(cache_key, expires_in: 5.minutes) do + result = {} + # Load filter options + filters.each do |filter| + group_by_time = current_user.heartbeats.group(filter).duration_seconds + result[filter] = group_by_time.sort_by { |k, v| v } + .reverse.map(&:first) + .compact_blank + + if params[filter].present? + filter_arr = params[filter].split(",") + filtered_heartbeats = filtered_heartbeats.where(filter => filter_arr) + + result["singular_#{filter}"] = filter_arr.length == 1 + end + end + + result[:filtered_heartbeats] = filtered_heartbeats + + # Calculate stats for filtered data + result[:total_time] = filtered_heartbeats.duration_seconds + result[:total_heartbeats] = filtered_heartbeats.count + + filters.each do |filter| + result["top_#{filter}"] = filtered_heartbeats.group(filter) + .duration_seconds + .max_by { |_, v| v } + &.first + end + + # Prepare project durations data + result[:project_durations] = filtered_heartbeats + .group(:project) + .duration_seconds + .sort_by { |_, duration| -duration } + .first(10) + .to_h unless result["singular_project"] + + # Prepare pie chart data + result[:language_stats] = filtered_heartbeats + .group(:language) + .duration_seconds + .sort_by { |_, duration| -duration } + .first(10) + .map { |k, v| [ k.presence || "Unknown", v ] } + .to_h unless result["singular_language"] + + result[:editor_stats] = filtered_heartbeats + .group(:editor) + .duration_seconds + .sort_by { |_, duration| -duration } + .map { |k, v| [ k.presence || "Unknown", v ] } + .to_h unless result["singular_editor"] + + result[:operating_system_stats] = filtered_heartbeats + .group(:operating_system) + .duration_seconds + .sort_by { |_, duration| -duration } + .map { |k, v| [ k.presence || "Unknown", v ] } + .to_h unless result["singular_operating_system"] + + # Calculate weekly project stats for the last 6 months + result[:weekly_project_stats] = {} + (0..25).each do |week_offset| # 26 weeks = 6 months + week_start = week_offset.weeks.ago.beginning_of_week + week_end = week_offset.weeks.ago.end_of_week + + week_stats = filtered_heartbeats + .where(time: week_start.to_f..week_end.to_f) + .group(:project) + .duration_seconds + + result[:weekly_project_stats][week_start.to_date.iso8601] = week_stats + end + + result + end + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8f2ec26..b897278 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -57,134 +57,6 @@ class UsersController < ApplicationController ].sample end - def show - # Use current_user for /my/home route, otherwise find by id - @user = if params[:id].present? - User.find(params[:id]) - else - current_user - end - - # Cache key based on user and filter parameters - cache_key = [ - @user, - params[:projects], - params[:language], - params[:os], - params[:editor] - ] - - # Load filter options and apply filters with caching - cached_data = Rails.cache.fetch(cache_key, expires_in: 5.minutes) do - result = {} - # Load filter options - result[:projects] = @user.heartbeats.select(:project).distinct.order(:project).pluck(:project) - result[:languages] = @user.heartbeats.select(:language).distinct.order(:language).pluck(:language) - result[:operating_systems] = @user.heartbeats.select(:operating_system).distinct.order(:operating_system).pluck(:operating_system) - result[:editors] = @user.heartbeats.select(:editor).distinct.order(:editor).pluck(:editor) - - # Apply filters to heartbeats - filtered_heartbeats = @user.heartbeats - filtered_heartbeats = filtered_heartbeats.where(project: params[:projects].split(",")) if params[:projects].present? - filtered_heartbeats = filtered_heartbeats.where(language: params[:language].split(",")) if params[:language].present? - filtered_heartbeats = filtered_heartbeats.where(operating_system: params[:os].split(",")) if params[:os].present? - filtered_heartbeats = filtered_heartbeats.where(editor: params[:editor].split(",")) if params[:editor].present? - - result[:filtered_heartbeats] = filtered_heartbeats - - - # Calculate stats for filtered data - result[:total_time] = filtered_heartbeats.duration_seconds - result[:total_heartbeats] = filtered_heartbeats.count - result[:top_project] = filtered_heartbeats.group(:project).duration_seconds.max_by { |_, v| v }&.first - result[:top_language] = filtered_heartbeats.group(:language).duration_seconds.max_by { |_, v| v }&.first - result[:top_os] = filtered_heartbeats.group(:operating_system).duration_seconds.max_by { |_, v| v }&.first - result[:top_editor] = filtered_heartbeats.group(:editor).duration_seconds.max_by { |_, v| v }&.first - - # Prepare project durations data - result[:project_durations] = filtered_heartbeats - .group(:project) - .duration_seconds - .sort_by { |_, duration| -duration } - .first(10) - .to_h - - # Prepare pie chart data - result[:language_stats] = filtered_heartbeats - .group(:language) - .duration_seconds - .sort_by { |_, duration| -duration } - .first(10) - .map { |k, v| [ k.presence || "Unknown", v ] } - .to_h - - result[:editor_stats] = filtered_heartbeats - .group(:editor) - .duration_seconds - .sort_by { |_, duration| -duration } - .map { |k, v| [ k.presence || "Unknown", v ] } - .to_h - - result[:os_stats] = filtered_heartbeats - .group(:operating_system) - .duration_seconds - .sort_by { |_, duration| -duration } - .map { |k, v| [ k.presence || "Unknown", v ] } - .to_h - - # Calculate weekly project stats for the last 6 months - result[:weekly_project_stats] = {} - (0..25).each do |week_offset| # 26 weeks = 6 months - week_start = week_offset.weeks.ago.beginning_of_week - week_end = week_offset.weeks.ago.end_of_week - - week_stats = filtered_heartbeats - .where(time: week_start.to_f..week_end.to_f) - .group(:project) - .duration_seconds - - result[:weekly_project_stats][week_start.to_date.iso8601] = week_stats - end - - result - end - - cached_data.entries.each do |key, value| - instance_variable_set("@#{key}", value) - end - - respond_to do |format| - format.html do - if request.xhr? - render partial: "filterable_dashboard_content" - end - end - - format.json do - render json: { - stats: { - total_time: ApplicationController.helpers.short_time_simple(@total_time), - total_heartbeats: number_with_delimiter(@total_heartbeats), - top_project: @top_project || "None", - top_language: @top_language || "Unknown", - top_os: @top_os || "Unknown", - top_editor: @top_editor || "Unknown" - }, - project_durations: @project_durations.transform_values { |v| - { - seconds: v, - formatted: ApplicationController.helpers.short_time_simple(v) - } - }, - language_stats: @language_stats, - editor_stats: @editor_stats, - os_stats: @os_stats, - weekly_project_stats: @weekly_project_stats - } - end - end - end - private def require_admin diff --git a/app/views/shared/_multi_select.html.erb b/app/views/shared/_multi_select.html.erb index 3b497a1..a534e3d 100644 --- a/app/views/shared/_multi_select.html.erb +++ b/app/views/shared/_multi_select.html.erb @@ -1,6 +1,6 @@
-
+
Filter by <%= label.downcase %>... @@ -23,298 +23,4 @@
-
- - - - \ No newline at end of file +
\ No newline at end of file diff --git a/app/views/shared/_nav.html.erb b/app/views/shared/_nav.html.erb index 64cb9f4..f52efbd 100644 --- a/app/views/shared/_nav.html.erb +++ b/app/views/shared/_nav.html.erb @@ -50,11 +50,6 @@ Settings <% end %> -
  • - <%= link_to my_home_path, class: "nav-item #{current_page?(my_home_path) ? 'active' : ''}", data: { turbo: false } do %> - Dashboard - <% end %> -
  • <%= link_to signout_path, class: "nav-item", data: { turbo_method: :delete } do %> Logout diff --git a/app/views/static_pages/_filterable_dashboard.html.erb b/app/views/static_pages/_filterable_dashboard.html.erb new file mode 100644 index 0000000..d5a6afa --- /dev/null +++ b/app/views/static_pages/_filterable_dashboard.html.erb @@ -0,0 +1,405 @@ +<%= turbo_frame_tag "filterable_dashboard" do %> +
    +
    +
    + <%= render partial: 'shared/multi_select', locals: { + label: 'Project', + param: 'project', + values: @project, + selected: params[:project] + } %> + + <%= render partial: 'shared/multi_select', locals: { + label: 'Language', + param: 'language', + values: @language, + selected: params[:language] + } %> + + <%= render partial: 'shared/multi_select', locals: { + label: 'OS', + param: 'operating_system', + values: @operating_system, + selected: params[:operating_system] + } %> + + <%= render partial: 'shared/multi_select', locals: { + label: 'Editor', + param: 'editor', + values: @editor, + selected: params[:editor] + } %> +
    +
    +
    + +
    + <%= render partial: 'filterable_dashboard_content' %> +
    + + + + + + +<% end %> \ No newline at end of file diff --git a/app/views/static_pages/_filterable_dashboard_content.html.erb b/app/views/static_pages/_filterable_dashboard_content.html.erb new file mode 100644 index 0000000..6b141e9 --- /dev/null +++ b/app/views/static_pages/_filterable_dashboard_content.html.erb @@ -0,0 +1,140 @@ +
    +
    +
    +
    TOTAL TIME
    +
    <%= ApplicationController.helpers.short_time_simple(@total_time) %>
    +
    + +
    +
    TOTAL HEARTBEATS
    +
    + <%= number_with_delimiter(@total_heartbeats) %> +
    +
    + +
    +
    TOP PROJECT
    +
    + <%= @top_project || "None" %> + <% if @singular_project %> + <%= FlavorText.obvious.sample %> + <% end %> +
    +
    + +
    +
    TOP LANGUAGE
    +
    + <%= @top_language || "Unknown" %> + <% if @singular_language %> + <%= FlavorText.obvious.sample %> + <% end %> +
    +
    + +
    +
    TOP OS
    +
    + <%= @top_operating_system || "Unknown" %> + <% if @singular_operating_system %> + <%= FlavorText.obvious.sample %> + <% end %> +
    +
    + +
    +
    TOP EDITOR
    +
    + <%= @top_editor || "Unknown" %> + <% if @singular_editor %> + <%= FlavorText.obvious.sample %> + <% end %> +
    +
    +
    + +
    + <% if @project_durations&.size&.> 1 %> +
    +

    Project Durations

    +
    + <% + max_duration = @project_durations.values.max + min_duration = @project_durations.values.min + + # Use logarithmic scale for better visibility of smaller values + # Add 1 to avoid log(0), scale to 15-100 range + def log_scale(value, max_val) + return 0 if value == 0 + min_percent = 5 # Minimum bar width percentage + max_percent = 100 # Maximum bar width percentage + + # Mix linear and logarithmic scaling + # 80% linear, 20% logarithmic + linear_ratio = value.to_f / max_val + log_ratio = Math.log(value + 1) / Math.log(max_val + 1) + + linear_weight = 0.8 + log_weight = 0.2 + + scaled = min_percent + (linear_weight * linear_ratio + log_weight * log_ratio) * (max_percent - min_percent) + [scaled, max_percent].min.round + end + %> + + <% @project_durations.each do |project, duration| %> +
    +
    <%= project %>
    +
    +
    + <%= ApplicationController.helpers.short_time_simple(duration) %> +
    +
    +
    + <% end %> +
    +
    + <% end %> + + <%# Language distribution %> + <% if @language_stats.present? %> +
    +

    Languages

    +
    + +
    +
    + <% end %> + + <%# Editor distribution %> + <% if @editor_stats.present? %> +
    +

    Editors

    +
    + +
    +
    + <% end %> + + <%# OS distribution %> + <% if @operating_system_stats.present? %> +
    +

    Operating Systems

    +
    + +
    +
    + <% end %> + +
    +
    +
    +

    Project Timeline

    +
    +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/views/static_pages/index.html.erb b/app/views/static_pages/index.html.erb index 35dfd50..b064eee 100644 --- a/app/views/static_pages/index.html.erb +++ b/app/views/static_pages/index.html.erb @@ -105,6 +105,10 @@ Loading project durations... <% end %> + + <%= turbo_frame_tag "filterable_dashboard", src: filterable_dashboard_static_pages_path do %> + Loading... + <% end %> <% else %> <% if @leaderboard %>

    Today's Top Hack Clubbers

    diff --git a/app/views/users/_filterable_dashboard_content.html.erb b/app/views/users/_filterable_dashboard_content.html.erb deleted file mode 100644 index 9bf1ed6..0000000 --- a/app/views/users/_filterable_dashboard_content.html.erb +++ /dev/null @@ -1,495 +0,0 @@ -<%# This partial will be loaded asynchronously when filters change %> -
    - <%# Stats cards %> -
    -
    -
    TOTAL TIME
    -
    <%= ApplicationController.helpers.short_time_simple(@total_time) %>
    -
    - -
    -
    TOTAL HEARTBEATS
    -
    <%= number_with_delimiter(@total_heartbeats) %>
    -
    - -
    -
    TOP PROJECT
    -
    <%= @top_project || "None" %>
    -
    - -
    -
    TOP LANGUAGE
    -
    <%= @top_language || "Unknown" %>
    -
    - -
    -
    TOP OS
    -
    <%= @top_os || "Unknown" %>
    -
    - -
    -
    TOP EDITOR
    -
    <%= @top_editor || "Unknown" %>
    -
    -
    - -
    - <% if @project_durations.size > 1 %> -
    -

    Project Durations

    -
    - <% - max_duration = @project_durations.values.max - min_duration = @project_durations.values.min - - # Use logarithmic scale for better visibility of smaller values - # Add 1 to avoid log(0), scale to 15-100 range - def log_scale(value, max_val) - return 0 if value == 0 - min_percent = 5 # Minimum bar width percentage - max_percent = 100 # Maximum bar width percentage - - # Mix linear and logarithmic scaling - # 80% linear, 20% logarithmic - linear_ratio = value.to_f / max_val - log_ratio = Math.log(value + 1) / Math.log(max_val + 1) - - linear_weight = 0.8 - log_weight = 0.2 - - scaled = min_percent + (linear_weight * linear_ratio + log_weight * log_ratio) * (max_percent - min_percent) - [scaled, max_percent].min.round - end - %> - - <% @project_durations.each do |project, duration| %> -
    -
    <%= project %>
    -
    -
    - <%= ApplicationController.helpers.short_time_simple(duration) %> -
    -
    -
    - <% end %> -
    -
    - <% end %> - - <%# Language distribution %> -
    -

    Languages

    -
    - -
    -
    - - <%# Editor distribution %> -
    -

    Editors

    -
    - -
    -
    - - <%# OS distribution %> -
    -

    Operating Systems

    -
    - -
    -
    - -
    -
    -
    -

    Project Timeline

    -
    -
    - -
    -
    -
    -
    -
    - - - - - \ No newline at end of file diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 5047d77..3befaf8 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -5,439 +5,9 @@

    Welcome, <%= @user.display_name %>

    - -
    - <%= render partial: 'shared/multi_select', locals: { - label: 'Project', - param: 'projects', - values: @projects, - selected: params[:projects] - } %> - - <%= render partial: 'shared/multi_select', locals: { - label: 'Language', - param: 'language', - values: @languages, - selected: params[:language] - } %> - - <%= render partial: 'shared/multi_select', locals: { - label: 'OS', - param: 'os', - values: @operating_systems, - selected: params[:os] - } %> - - <%= render partial: 'shared/multi_select', locals: { - label: 'Editor', - param: 'editor', - values: @editors, - selected: params[:editor] - } %> -
    -<%# Async dashboard content %> -
    - <%= render partial: 'filterable_dashboard_content' %> -
    - - - - \ No newline at end of file +<%= turbo_frame_tag "filterable_dashboard", src: filterable_dashboard_static_pages_path do %> + Loading... +<% end %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index d1a788f..7a0d690 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,8 @@ Rails.application.routes.draw do get :project_durations get :activity_graph get :currently_hacking + get :filterable_dashboard_content + get :filterable_dashboard get "🃏", to: "static_pages#🃏", as: :wildcard end end @@ -56,7 +58,6 @@ Rails.application.routes.draw do end # Namespace for current user actions - get "my/home", to: "users#show", as: :my_home get "my/settings", to: "users#edit", as: :my_settings patch "my/settings", to: "users#update" post "my/settings/migrate_heartbeats", to: "users#migrate_heartbeats", as: :my_settings_migrate_heartbeats diff --git a/lib/flavor_text.rb b/lib/flavor_text.rb index 61c9ed2..44522ed 100644 --- a/lib/flavor_text.rb +++ b/lib/flavor_text.rb @@ -166,6 +166,14 @@ class FlavorText ] end + def self.obvious + [ + "obv.", + "duh", + "clearly" + ] + end + def self.motto [ "track your time before it tracks you!",