From 8d00418059499567df582ea55bb0470201ad813f Mon Sep 17 00:00:00 2001
From: Mahad Kalam <55807755+skyfallwastaken@users.noreply.github.com>
Date: Sun, 15 Mar 2026 15:26:32 +0000
Subject: [PATCH] Spring cleaning (#1074)
* fix: use owner/repo format for project badges
Updates badge URLs to use GitHub-style owner/repo format (e.g., "hackclub/hackatime")
instead of project names. This ensures compatibility with external badge services that
expect repository paths.
Changes:
- Add Repository#full_path method to get owner/repo format
- Update settings controller to pass both display names and repo paths
- Update Badges component to display project names but use repo paths in URLs
* fix: improve user lookup in API v1 stats endpoint
Use the robust lookup_user method for username parameter in the /api/v1/stats endpoint
to ensure consistent user lookup across all API endpoints. This properly handles Slack
UIDs (HCA IDs), numeric user IDs, and usernames in the correct priority order.
* fix: reduce clutter on new user homepage
Simplify the new user experience by:
- Removing redundant "Hello friend" text from setup notice (header already provides context)
- Hiding GitHub link banner when setup notice is shown to focus user on primary action
This reduces visual clutter and helps new users focus on completing setup first.
* fix: enable full app layout for new OAuth application page
Remove layout=false directive that was preventing the app header and navigation
from appearing on the new OAuth application creation page.
* fix: add antigravity editor to docs
Add documentation for Antigravity, a VSCode fork from Google with built-in AI
features. Includes setup instructions for tracking time with Hackatime using the
WakaTime extension.
* fix: improve stat card subtitle positioning
Remove absolute positioning from subtitle text to allow it to flow naturally after
the main value. This prevents the subtitle from being pushed to the bottom when
other cards have longer content.
* fix: align settings action buttons to card end on larger screens
Remove width constraint from footer to allow action buttons to align to the right
edge of the full card width instead of being constrained to a narrower container.
* fix: improve heartbeat importer visibility on light themes
Update import provider cards and radio buttons to have better contrast on light themes:
- Use bg-surface-100 instead of bg-darker for better card visibility
- Increase radio button border thickness and use darker border color
- Add hover and focus states for better interactivity
* Split up settings controller + perf + goal display
* Make stat card subtitles larger
* Fix AG + VS Code
* Remove Shiba refs
* Bundle update
---
.claude/settings.json | 8 +
CLAUDE.md | 1 +
Gemfile.lock | 22 +-
.../api/hackatime/v1/hackatime_controller.rb | 29 +-
app/controllers/api/v1/stats_controller.rb | 2 +-
app/controllers/docs_controller.rb | 4 +-
app/controllers/settings/access_controller.rb | 18 +
app/controllers/settings/badges_controller.rb | 9 +
app/controllers/settings/base_controller.rb | 299 ++++++--------
app/controllers/settings/data_controller.rb | 28 ++
app/controllers/settings/goals_controller.rb | 17 +-
.../settings/integrations_controller.rb | 37 ++
.../settings/notifications_controller.rb | 7 +
.../settings/profile_controller.rb | 10 +
app/javascript/pages/Home/SignedIn.svelte | 4 +-
.../pages/Home/signedIn/SetupNotice.svelte | 4 -
.../pages/Home/signedIn/StatCard.svelte | 6 +-
.../pages/OAuthApplications/New.svelte | 4 -
.../pages/Users/Settings/Badges.svelte | 16 +-
.../pages/Users/Settings/Data.svelte | 386 +++++++++---------
.../Settings/components/SectionCard.svelte | 6 +-
app/javascript/pages/Users/Settings/types.ts | 4 +-
app/models/repository.rb | 5 +
docs/editors/antigravity.md | 12 +
docs/editors/godot.md | 16 +-
docs/editors/vs-code.md | 11 +-
.../images/editor-icons/antigravity-128.png | Bin 0 -> 20190 bytes
.../api/hackatime/v1/compatibility_spec.rb | 12 +-
swagger/v1/swagger.yaml | 18 +-
29 files changed, 574 insertions(+), 421 deletions(-)
create mode 100644 .claude/settings.json
create mode 100644 CLAUDE.md
create mode 100644 docs/editors/antigravity.md
create mode 100644 public/images/editor-icons/antigravity-128.png
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..d0bc87b
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,8 @@
+{
+ "includeCoAuthoredBy": false,
+ "gitAttribution": false,
+ "attribution": {
+ "commits": false,
+ "pullRequests": false
+ }
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..8d770e2
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+Read @AGENTS.md
diff --git a/Gemfile.lock b/Gemfile.lock
index d314c66..669bba5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -82,7 +82,7 @@ GEM
ast (2.4.3)
autotuner (1.1.0)
aws-eventstream (1.4.0)
- aws-partitions (1.1225.0)
+ aws-partitions (1.1226.0)
aws-sdk-core (3.243.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -337,7 +337,7 @@ GEM
net-ssh (>= 5.0.0, < 8.0.0)
net-smtp (0.5.1)
net-protocol
- net-ssh (7.3.0)
+ net-ssh (7.3.1)
nio4r (2.7.5)
nokogiri (1.19.1-aarch64-linux-gnu)
racc (~> 1.4)
@@ -357,7 +357,7 @@ GEM
faraday (>= 1.0, < 3.0)
faraday-net_http_persistent
net-http-persistent
- oj (3.16.15)
+ oj (3.16.16)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
ostruct (0.6.3)
@@ -548,8 +548,8 @@ GEM
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
logger
- skylight (7.0.0)
- activesupport (>= 7.1.0)
+ skylight (7.1.0)
+ activesupport (>= 7.2.0)
slack-ruby-client (3.1.0)
faraday (>= 2.0.1)
faraday-mashify
@@ -588,12 +588,12 @@ GEM
tailwindcss-rails (4.4.0)
railties (>= 7.0.0)
tailwindcss-ruby (~> 4.0)
- tailwindcss-ruby (4.2.0)
- tailwindcss-ruby (4.2.0-aarch64-linux-gnu)
- tailwindcss-ruby (4.2.0-aarch64-linux-musl)
- tailwindcss-ruby (4.2.0-arm64-darwin)
- tailwindcss-ruby (4.2.0-x86_64-linux-gnu)
- tailwindcss-ruby (4.2.0-x86_64-linux-musl)
+ tailwindcss-ruby (4.2.1)
+ tailwindcss-ruby (4.2.1-aarch64-linux-gnu)
+ tailwindcss-ruby (4.2.1-aarch64-linux-musl)
+ tailwindcss-ruby (4.2.1-arm64-darwin)
+ 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)
diff --git a/app/controllers/api/hackatime/v1/hackatime_controller.rb b/app/controllers/api/hackatime/v1/hackatime_controller.rb
index e138f55..6070219 100644
--- a/app/controllers/api/hackatime/v1/hackatime_controller.rb
+++ b/app/controllers/api/hackatime/v1/hackatime_controller.rb
@@ -46,14 +46,39 @@ class Api::Hackatime::V1::HackatimeController < ApplicationController
def status_bar_today
Time.use_zone(@user.timezone) do
hbt = @user.heartbeats.today
+ total_seconds = hbt.duration_seconds
+
+ # Check if user has a daily goal
+ daily_goal = @user.goals.find_by(period: "day")
+
result = {
data: {
grand_total: {
- text: @user.format_extension_text(hbt.duration_seconds),
- total_seconds: hbt.duration_seconds
+ text: @user.format_extension_text(total_seconds),
+ total_seconds: total_seconds
}
}
}
+
+ # Include goal information if daily goal exists
+ if daily_goal
+ goal_progress = ProgrammingGoalsProgressService.new(user: @user, goals: [ daily_goal ]).call.first
+
+ if goal_progress
+ # Append goal progress to the user's preferred text format
+ user_text = result[:data][:grand_total][:text]
+ goal_text = ApplicationController.helpers.short_time_simple(daily_goal.target_seconds)
+
+ result[:data][:grand_total][:text] = "#{user_text} / #{goal_text} today"
+ result[:data][:goal] = {
+ target_seconds: daily_goal.target_seconds,
+ tracked_seconds: goal_progress[:tracked_seconds],
+ completion_percent: goal_progress[:completion_percent],
+ complete: goal_progress[:complete]
+ }
+ end
+ end
+
render json: result
end
end
diff --git a/app/controllers/api/v1/stats_controller.rb b/app/controllers/api/v1/stats_controller.rb
index 5d870b3..531216b 100644
--- a/app/controllers/api/v1/stats_controller.rb
+++ b/app/controllers/api/v1/stats_controller.rb
@@ -12,7 +12,7 @@ class Api::V1::StatsController < ApplicationController
query = Heartbeat.where(time: start_date..end_date)
if params[:username].present?
- user = User.find_by(username: params[:username]) || User.find_by(slack_uid: params[:username])
+ user = lookup_user(params[:username])
return render json: { error: "User not found" }, status: :not_found unless user
query = query.where(user_id: user.id)
diff --git a/app/controllers/docs_controller.rb b/app/controllers/docs_controller.rb
index 36399d8..3ade01b 100644
--- a/app/controllers/docs_controller.rb
+++ b/app/controllers/docs_controller.rb
@@ -11,8 +11,8 @@ class DocsController < InertiaController
].freeze
ALL_EDITORS = [
- [ "Android Studio", "android-studio" ], [ "AppCode", "appcode" ], [ "Aptana", "aptana" ],
- [ "Arduino IDE", "arduino-ide" ], [ "Azure Data Studio", "azure-data-studio" ],
+ [ "Android Studio", "android-studio" ], [ "Antigravity", "antigravity" ], [ "AppCode", "appcode" ],
+ [ "Aptana", "aptana" ], [ "Arduino IDE", "arduino-ide" ], [ "Azure Data Studio", "azure-data-studio" ],
[ "Brackets", "brackets" ],
[ "C++ Builder", "c++-builder" ],
[ "CLion", "clion" ], [ "Cloud9", "cloud9" ], [ "Coda", "coda" ],
diff --git a/app/controllers/settings/access_controller.rb b/app/controllers/settings/access_controller.rb
index c1d4945..22c6ab4 100644
--- a/app/controllers/settings/access_controller.rb
+++ b/app/controllers/settings/access_controller.rb
@@ -37,6 +37,24 @@ class Settings::AccessController < Settings::BaseController
)
end
+ def section_props
+ api_key_token = @user.api_keys.last&.token
+
+ {
+ settings_update_path: my_settings_access_path,
+ user: user_props,
+ options: options_props,
+ paths: paths_props,
+ config_file: {
+ content: generated_wakatime_config(api_key_token),
+ has_api_key: api_key_token.present?,
+ empty_message: "No API key is available yet. Rotate your API key to generate one.",
+ api_key: api_key_token,
+ api_url: "https://#{request.host_with_port}/api/hackatime/v1"
+ }
+ }
+ end
+
def access_params
params.require(:user).permit(:hackatime_extension_text_type)
end
diff --git a/app/controllers/settings/badges_controller.rb b/app/controllers/settings/badges_controller.rb
index 74e5c2f..4dca419 100644
--- a/app/controllers/settings/badges_controller.rb
+++ b/app/controllers/settings/badges_controller.rb
@@ -5,4 +5,13 @@ class Settings::BadgesController < Settings::BaseController
settings_update_path: my_settings_profile_path
)
end
+
+ private
+
+ def section_props
+ {
+ options: options_props,
+ badges: badges_props
+ }
+ end
end
diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb
index b66211e..0348b35 100644
--- a/app/controllers/settings/base_controller.rb
+++ b/app/controllers/settings/base_controller.rb
@@ -6,15 +6,13 @@ class Settings::BaseController < InertiaController
before_action :set_user
before_action :require_current_user
- before_action :prepare_settings_page
private
- def render_settings_page(active_section:, settings_update_path:, status: :ok)
- render inertia: settings_component_for(active_section), props: settings_page_props(
- active_section: active_section,
- settings_update_path: settings_update_path
- ), status: status
+ def render_settings_page(active_section:, settings_update_path:, status: :ok, extra_props: {})
+ render inertia: settings_component_for(active_section), props: common_props(
+ active_section: active_section
+ ).merge(section_props).merge(extra_props), status: status
end
def settings_component_for(active_section)
@@ -29,45 +27,9 @@ class Settings::BaseController < InertiaController
}.fetch(active_section.to_s, "Users/Settings/Profile")
end
- def prepare_settings_page
- @is_own_settings = is_own_settings?
- @can_enable_slack_status = @user.slack_access_token.present? && @user.slack_scopes.include?("users.profile:write")
- @imports_enabled = Flipper.enabled?(:imports, @user)
- @enabled_sailors_logs = SailorsLogNotificationPreference.where(
- slack_uid: @user.slack_uid,
- enabled: true,
- ).where.not(slack_channel_id: SailorsLog::DEFAULT_CHANNELS)
-
- @projects = @user.project_repo_mappings.distinct.pluck(:project_name)
- heartbeat_language_and_projects = @user.heartbeats.distinct.pluck(:language, :project)
- goal_languages = []
- goal_projects = @projects.dup
-
- heartbeat_language_and_projects.each do |language, project|
- categorized_language = language&.categorize_language
- goal_languages << categorized_language if categorized_language.present?
- goal_projects << project if project.present?
- end
-
- @goal_selectable_languages = goal_languages.uniq.sort
- @goal_selectable_projects = goal_projects.uniq.sort
- @work_time_stats_base_url = @user.slack_uid.present? ? "https://hackatime-badge.hackclub.com/#{@user.slack_uid}/" : nil
- @work_time_stats_url = if @work_time_stats_base_url.present?
- "#{@work_time_stats_base_url}#{@projects.first || 'example'}"
- end
-
- @general_badge_url = GithubReadmeStats.new(@user.id, "darcula").generate_badge_url
- @latest_api_key_token = @user.api_keys.last&.token
- @latest_heartbeat_import = @user.heartbeat_import_runs.latest_first.first
- end
-
- def settings_page_props(active_section:, settings_update_path:)
- if active_section.to_s == "data" && @latest_heartbeat_import.present?
- @latest_heartbeat_import = HeartbeatImportRunner.refresh_remote_run!(@latest_heartbeat_import)
- end
-
- heartbeats_last_7_days = @user.heartbeats.where("time >= ?", 7.days.ago.to_f).count
- channel_ids = @enabled_sailors_logs.pluck(:slack_channel_id)
+ # Lightweight props shared by every settings page
+ def common_props(active_section:)
+ is_own = is_own_settings?
{
active_section: active_section,
@@ -80,137 +42,10 @@ class Settings::BaseController < InertiaController
badges: my_settings_badges_path,
data: my_settings_data_path
},
- page_title: (@is_own_settings ? "My Settings" : "Settings | #{@user.display_name}"),
- heading: (@is_own_settings ? "Settings" : "Settings for #{@user.display_name}"),
+ page_title: (is_own ? "My Settings" : "Settings | #{@user.display_name}"),
+ heading: (is_own ? "Settings" : "Settings for #{@user.display_name}"),
subheading: "Manage your profile, integrations, notifications, access, goals, and data tools.",
- settings_update_path: settings_update_path,
- create_goal_path: my_settings_goals_create_path,
- username_max_length: User::USERNAME_MAX_LENGTH,
- user: {
- id: @user.id,
- display_name: @user.display_name,
- timezone: @user.timezone,
- country_code: @user.country_code,
- username: @user.username,
- theme: @user.theme,
- uses_slack_status: @user.uses_slack_status,
- weekly_summary_email_enabled: @user.subscribed?("weekly_summary"),
- hackatime_extension_text_type: @user.hackatime_extension_text_type,
- allow_public_stats_lookup: @user.allow_public_stats_lookup,
- trust_level: @user.trust_level,
- can_request_deletion: @user.can_request_deletion?,
- github_uid: @user.github_uid,
- github_username: @user.github_username,
- slack_uid: @user.slack_uid,
- programming_goals: @user.goals.order(:created_at).map { |goal|
- goal.as_programming_goal_payload.merge(
- update_path: my_settings_goal_update_path(goal),
- destroy_path: my_settings_goal_destroy_path(goal)
- )
- }
- },
- paths: {
- settings_path: settings_update_path,
- wakatime_setup_path: my_wakatime_setup_path,
- slack_auth_path: slack_auth_path,
- github_auth_path: github_auth_path,
- github_unlink_path: github_unlink_path,
- add_email_path: add_email_auth_path,
- unlink_email_path: unlink_email_auth_path,
- rotate_api_key_path: my_settings_rotate_api_key_path,
- export_all_heartbeats_path: export_my_heartbeats_path(all_data: "true"),
- export_range_heartbeats_path: export_my_heartbeats_path,
- create_heartbeat_import_path: my_heartbeat_imports_path,
- create_deletion_path: create_deletion_path
- },
- options: {
- countries: ISO3166::Country.all.map { |country|
- {
- label: country.common_name,
- value: country.alpha2
- }
- }.sort_by { |country| country[:label] },
- timezones: TZInfo::Timezone.all_identifiers.sort.map { |timezone|
- { label: timezone, value: timezone }
- },
- extension_text_types: User.hackatime_extension_text_types.keys.map { |key|
- {
- label: key.humanize,
- value: key
- }
- },
- themes: User.theme_options,
- badge_themes: GithubReadmeStats.themes,
- goals: {
- periods: Goal::PERIODS.map { |period|
- {
- label: period.humanize,
- value: period
- }
- },
- preset_target_seconds: Goal::PRESET_TARGET_SECONDS,
- selectable_languages: @goal_selectable_languages
- .map { |language| { label: language, value: language } },
- selectable_projects: @goal_selectable_projects
- .map { |project| { label: project, value: project } }
- }
- },
- slack: {
- can_enable_status: @can_enable_slack_status,
- notification_channels: channel_ids.map { |channel_id|
- {
- id: channel_id,
- label: "##{channel_id}",
- url: "https://hackclub.slack.com/archives/#{channel_id}"
- }
- }
- },
- github: {
- connected: @user.github_uid.present?,
- username: @user.github_username,
- profile_url: (@user.github_username.present? ? "https://github.com/#{@user.github_username}" : nil)
- },
- emails: @user.email_addresses.map { |email|
- {
- email: email.email,
- source: email.source&.humanize || "Unknown",
- can_unlink: @user.can_delete_email_address?(email)
- }
- },
- badges: {
- general_badge_url: @general_badge_url,
- project_badge_url: @work_time_stats_url,
- project_badge_base_url: @work_time_stats_base_url,
- projects: @projects,
- profile_url: (@user.username.present? ? "https://hackati.me/#{@user.username}" : nil),
- markscribe_template: '{{ wakatimeDoubleCategoryBar "Languages:" wakatimeData.Languages "Projects:" wakatimeData.Projects 5 }}',
- markscribe_reference_url: "https://github.com/taciturnaxolotl/markscribe#your-wakatime-languages-formated-as-a-bar",
- markscribe_preview_image_url: "https://cdn.fluff.pw/slackcdn/524e293aa09bc5f9115c0c29c18fb4bc.png",
- heatmap_badge_url: "https://heatmap.shymike.dev/?id=#{@user.id}&timezone=#{@user.timezone}",
- heatmap_config_url: "https://hackatime-heatmap.shymike.dev/?id=#{@user.id}&timezone=#{@user.timezone}",
- hackabox_repo_url: "https://github.com/quackclub/hacka-box",
- hackabox_preview_image_url: "https://user-cdn.hackclub-assets.com/019cef81-366a-7543-ad7c-21b738310f23/image.png"
- },
- config_file: {
- content: generated_wakatime_config(@latest_api_key_token),
- has_api_key: @latest_api_key_token.present?,
- empty_message: "No API key is available yet. Rotate your API key to generate one.",
- api_key: @latest_api_key_token,
- api_url: "https://#{request.host_with_port}/api/hackatime/v1"
- },
- data_export: {
- total_heartbeats: number_with_delimiter(@user.heartbeats.count),
- total_coding_time: @user.heartbeats.duration_simple,
- heartbeats_last_7_days: number_with_delimiter(heartbeats_last_7_days),
- is_restricted: (@user.trust_level == "red")
- },
- imports_enabled: @imports_enabled,
- remote_import_cooldown_until: HeartbeatImportRunner.remote_import_cooldown_until(user: @user)&.iso8601,
- latest_heartbeat_import: HeartbeatImportRunner.serialize(@latest_heartbeat_import),
- ui: {
- show_dev_import: Rails.env.development?,
- show_imports: @imports_enabled
- },
+
errors: {
full_messages: @user.errors.full_messages,
username: @user.errors[:username]
@@ -218,6 +53,120 @@ class Settings::BaseController < InertiaController
}
end
+ # Subclasses override this to provide section-specific props
+ def section_props
+ {}
+ end
+
+ # Shared helpers used by multiple section controllers
+
+ def user_props
+ {
+ id: @user.id,
+ display_name: @user.display_name,
+ timezone: @user.timezone,
+ country_code: @user.country_code,
+ username: @user.username,
+ theme: @user.theme,
+ uses_slack_status: @user.uses_slack_status,
+ weekly_summary_email_enabled: @user.subscribed?("weekly_summary"),
+ hackatime_extension_text_type: @user.hackatime_extension_text_type,
+ allow_public_stats_lookup: @user.allow_public_stats_lookup,
+ trust_level: @user.trust_level,
+ can_request_deletion: @user.can_request_deletion?,
+ github_uid: @user.github_uid,
+ github_username: @user.github_username,
+ slack_uid: @user.slack_uid,
+ programming_goals: @user.goals.order(:created_at).map { |goal|
+ goal.as_programming_goal_payload.merge(
+ update_path: my_settings_goal_update_path(goal),
+ destroy_path: my_settings_goal_destroy_path(goal)
+ )
+ }
+ }
+ end
+
+ def paths_props
+ {
+ settings_path: my_settings_profile_path,
+ wakatime_setup_path: my_wakatime_setup_path,
+ slack_auth_path: slack_auth_path,
+ github_auth_path: github_auth_path,
+ github_unlink_path: github_unlink_path,
+ add_email_path: add_email_auth_path,
+ unlink_email_path: unlink_email_auth_path,
+ rotate_api_key_path: my_settings_rotate_api_key_path,
+ export_all_heartbeats_path: export_my_heartbeats_path(all_data: "true"),
+ export_range_heartbeats_path: export_my_heartbeats_path,
+ create_heartbeat_import_path: my_heartbeat_imports_path,
+ create_deletion_path: create_deletion_path
+ }
+ end
+
+ def project_list
+ @project_list ||= @user.project_repo_mappings.includes(:repository).distinct.map do |mapping|
+ { display_name: mapping.project_name, repo_path: mapping.repository&.full_path || mapping.project_name }
+ end
+ end
+
+ def options_props
+ heartbeat_language_and_projects = @user.heartbeats.distinct.pluck(:language, :project)
+ goal_languages = []
+ goal_projects = project_list.map { |p| p[:display_name] }
+
+ heartbeat_language_and_projects.each do |language, project|
+ categorized_language = language&.categorize_language
+ goal_languages << categorized_language if categorized_language.present?
+ goal_projects << project if project.present?
+ end
+
+ {
+ countries: ISO3166::Country.all.map { |country|
+ { label: country.common_name, value: country.alpha2 }
+ }.sort_by { |country| country[:label] },
+ timezones: TZInfo::Timezone.all_identifiers.sort.map { |timezone|
+ { label: timezone, value: timezone }
+ },
+ extension_text_types: User.hackatime_extension_text_types.keys.map { |key|
+ { label: key.humanize, value: key }
+ },
+ themes: User.theme_options,
+ badge_themes: GithubReadmeStats.themes,
+ goals: {
+ periods: Goal::PERIODS.map { |period|
+ { label: period.humanize, value: period }
+ },
+ preset_target_seconds: Goal::PRESET_TARGET_SECONDS,
+ selectable_languages: goal_languages.uniq.sort
+ .map { |language| { label: language, value: language } },
+ selectable_projects: goal_projects.uniq.sort
+ .map { |project| { label: project, value: project } }
+ }
+ }
+ end
+
+ def badges_props
+ work_time_stats_base_url = @user.slack_uid.present? ? "https://hackatime-badge.hackclub.com/#{@user.slack_uid}/" : nil
+ work_time_stats_url = if work_time_stats_base_url.present? && project_list.first.present?
+ "#{work_time_stats_base_url}#{project_list.first[:repo_path]}"
+ end
+
+ {
+ general_badge_url: GithubReadmeStats.new(@user.id, "darcula").generate_badge_url,
+ project_badge_url: work_time_stats_url,
+ project_badge_base_url: work_time_stats_base_url,
+ projects: project_list,
+ profile_url: (@user.username.present? ? "https://hackati.me/#{@user.username}" : nil),
+ markscribe_template: '{{ wakatimeDoubleCategoryBar "Languages:" wakatimeData.Languages "Projects:" wakatimeData.Projects 5 }}',
+ markscribe_reference_url: "https://github.com/taciturnaxolotl/markscribe#your-wakatime-languages-formated-as-a-bar",
+ markscribe_preview_image_url: "https://cdn.fluff.pw/slackcdn/524e293aa09bc5f9115c0c29c18fb4bc.png",
+ heatmap_badge_url: "https://heatmap.shymike.dev/?id=#{@user.id}&timezone=#{@user.timezone}",
+ heatmap_config_url: "https://hackatime-heatmap.shymike.dev/?id=#{@user.id}&timezone=#{@user.timezone}",
+ hackabox_repo_url: "https://github.com/quackclub/hacka-box",
+ hackabox_preview_image_url: "https://user-cdn.hackclub-assets.com/019cef81-366a-7543-ad7c-21b738310f23/image.png"
+ }
+ end
+
def generated_wakatime_config(api_key)
return nil if api_key.blank?
diff --git a/app/controllers/settings/data_controller.rb b/app/controllers/settings/data_controller.rb
index b5ded78..0fe5909 100644
--- a/app/controllers/settings/data_controller.rb
+++ b/app/controllers/settings/data_controller.rb
@@ -12,4 +12,32 @@ class Settings::DataController < Settings::BaseController
status: status
)
end
+
+ def section_props
+ imports_enabled = Flipper.enabled?(:imports, @user)
+ latest_import = @user.heartbeat_import_runs.latest_first.first
+ if latest_import.present?
+ latest_import = HeartbeatImportRunner.refresh_remote_run!(latest_import)
+ end
+
+ {
+ user: user_props,
+ paths: paths_props,
+ data_export: InertiaRails.defer {
+ {
+ total_heartbeats: number_with_delimiter(@user.heartbeats.count),
+ total_coding_time: @user.heartbeats.duration_simple,
+ heartbeats_last_7_days: number_with_delimiter(@user.heartbeats.where("time >= ?", 7.days.ago.to_f).count),
+ is_restricted: (@user.trust_level == "red")
+ }
+ },
+ imports_enabled: imports_enabled,
+ remote_import_cooldown_until: HeartbeatImportRunner.remote_import_cooldown_until(user: @user)&.iso8601,
+ latest_heartbeat_import: HeartbeatImportRunner.serialize(latest_import),
+ ui: {
+ show_dev_import: Rails.env.development?,
+ show_imports: imports_enabled
+ }
+ }
+ end
end
diff --git a/app/controllers/settings/goals_controller.rb b/app/controllers/settings/goals_controller.rb
index 3850fcd..c79df87 100644
--- a/app/controllers/settings/goals_controller.rb
+++ b/app/controllers/settings/goals_controller.rb
@@ -40,10 +40,21 @@ class Settings::GoalsController < Settings::BaseController
extra_props = {}
extra_props[:goal_form] = goal_form if goal_form
- render inertia: settings_component_for("goals"), props: settings_page_props(
+ render_settings_page(
active_section: "goals",
- settings_update_path: my_settings_goals_path
- ).merge(extra_props), status: status
+ settings_update_path: my_settings_goals_path,
+ status: status,
+ extra_props: extra_props
+ )
+ end
+
+ def section_props
+ {
+ settings_update_path: my_settings_goals_path,
+ create_goal_path: my_settings_goals_create_path,
+ user: user_props,
+ options: options_props
+ }
end
def goal_params
diff --git a/app/controllers/settings/integrations_controller.rb b/app/controllers/settings/integrations_controller.rb
index b4d890f..5d128a5 100644
--- a/app/controllers/settings/integrations_controller.rb
+++ b/app/controllers/settings/integrations_controller.rb
@@ -24,6 +24,43 @@ class Settings::IntegrationsController < Settings::BaseController
)
end
+ def section_props
+ can_enable_slack_status = @user.slack_access_token.present? && @user.slack_scopes.include?("users.profile:write")
+ enabled_sailors_logs = SailorsLogNotificationPreference.where(
+ slack_uid: @user.slack_uid,
+ enabled: true,
+ ).where.not(slack_channel_id: SailorsLog::DEFAULT_CHANNELS)
+ channel_ids = enabled_sailors_logs.pluck(:slack_channel_id)
+
+ {
+ settings_update_path: my_settings_integrations_path,
+ user: user_props,
+ slack: {
+ can_enable_status: can_enable_slack_status,
+ notification_channels: channel_ids.map { |channel_id|
+ {
+ id: channel_id,
+ label: "##{channel_id}",
+ url: "https://hackclub.slack.com/archives/#{channel_id}"
+ }
+ }
+ },
+ github: {
+ connected: @user.github_uid.present?,
+ username: @user.github_username,
+ profile_url: (@user.github_username.present? ? "https://github.com/#{@user.github_username}" : nil)
+ },
+ emails: @user.email_addresses.map { |email|
+ {
+ email: email.email,
+ source: email.source&.humanize || "Unknown",
+ can_unlink: @user.can_delete_email_address?(email)
+ }
+ },
+ paths: paths_props
+ }
+ end
+
def integrations_params
params.require(:user).permit(:uses_slack_status)
end
diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb
index 8b23797..57d89a9 100644
--- a/app/controllers/settings/notifications_controller.rb
+++ b/app/controllers/settings/notifications_controller.rb
@@ -32,4 +32,11 @@ class Settings::NotificationsController < Settings::BaseController
status: status
)
end
+
+ def section_props
+ {
+ settings_update_path: my_settings_notifications_path,
+ user: user_props
+ }
+ end
end
diff --git a/app/controllers/settings/profile_controller.rb b/app/controllers/settings/profile_controller.rb
index c981d6a..6d0955a 100644
--- a/app/controllers/settings/profile_controller.rb
+++ b/app/controllers/settings/profile_controller.rb
@@ -23,6 +23,16 @@ class Settings::ProfileController < Settings::BaseController
)
end
+ def section_props
+ {
+ settings_update_path: my_settings_profile_path,
+ username_max_length: User::USERNAME_MAX_LENGTH,
+ user: user_props,
+ options: options_props,
+ badges: badges_props
+ }
+ end
+
def profile_params
permitted = params.require(:user).permit(
:timezone,
diff --git a/app/javascript/pages/Home/SignedIn.svelte b/app/javascript/pages/Home/SignedIn.svelte
index 5310b9b..2312b4b 100644
--- a/app/javascript/pages/Home/SignedIn.svelte
+++ b/app/javascript/pages/Home/SignedIn.svelte
@@ -125,9 +125,7 @@
{ssp_users_recent}
{ssp_users_size}
/>
- {/if}
-
- {#if github_uid_blank}
+ {:else if github_uid_blank}
- Hello friend! Looks like you are new around here, let's get you set up so - you can start tracking your coding time. -