mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 16:38:23 +00:00
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
This commit is contained in:
parent
49ea53128d
commit
8d00418059
29 changed files with 574 additions and 421 deletions
8
.claude/settings.json
Normal file
8
.claude/settings.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"includeCoAuthoredBy": false,
|
||||
"gitAttribution": false,
|
||||
"attribution": {
|
||||
"commits": false,
|
||||
"pullRequests": false
|
||||
}
|
||||
}
|
||||
1
CLAUDE.md
Normal file
1
CLAUDE.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Read @AGENTS.md
|
||||
22
Gemfile.lock
22
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" ],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,13 +42,26 @@ 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: {
|
||||
|
||||
errors: {
|
||||
full_messages: @user.errors.full_messages,
|
||||
username: @user.errors[:username]
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
|
@ -108,9 +83,12 @@ class Settings::BaseController < InertiaController
|
|||
destroy_path: my_settings_goal_destroy_path(goal)
|
||||
)
|
||||
}
|
||||
},
|
||||
paths: {
|
||||
settings_path: settings_update_path,
|
||||
}
|
||||
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,
|
||||
|
|
@ -122,66 +100,62 @@ class Settings::BaseController < InertiaController
|
|||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
{ label: key.humanize, value: key }
|
||||
},
|
||||
themes: User.theme_options,
|
||||
badge_themes: GithubReadmeStats.themes,
|
||||
goals: {
|
||||
periods: Goal::PERIODS.map { |period|
|
||||
{
|
||||
label: period.humanize,
|
||||
value: period
|
||||
}
|
||||
{ label: period.humanize, value: period }
|
||||
},
|
||||
preset_target_seconds: Goal::PRESET_TARGET_SECONDS,
|
||||
selectable_languages: @goal_selectable_languages
|
||||
selectable_languages: goal_languages.uniq.sort
|
||||
.map { |language| { label: language, value: language } },
|
||||
selectable_projects: @goal_selectable_projects
|
||||
selectable_projects: goal_projects.uniq.sort
|
||||
.map { |project| { label: project, value: project } }
|
||||
}
|
||||
},
|
||||
slack: {
|
||||
can_enable_status: @can_enable_slack_status,
|
||||
notification_channels: channel_ids.map { |channel_id|
|
||||
}
|
||||
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
|
||||
|
||||
{
|
||||
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,
|
||||
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",
|
||||
|
|
@ -190,31 +164,6 @@ class Settings::BaseController < InertiaController
|
|||
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]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -125,9 +125,7 @@
|
|||
{ssp_users_recent}
|
||||
{ssp_users_size}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if github_uid_blank}
|
||||
{:else if github_uid_blank}
|
||||
<GitHubLinkBanner {github_auth_path} />
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,6 @@
|
|||
</script>
|
||||
|
||||
<div class="text-left mt-2 mb-4 flex flex-col">
|
||||
<p class="mb-4 text-xl text-primary">
|
||||
Hello friend! Looks like you are new around here, let's get you set up so
|
||||
you can start tracking your coding time.
|
||||
</p>
|
||||
<div
|
||||
class="mb-4 rounded-xl border border-primary/40 bg-surface-100/70 p-4 md:p-5"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
|
||||
<div
|
||||
class={`
|
||||
relative flex flex-col justify-start p-4 rounded-xl border transition-all duration-200 h-full
|
||||
${subtitle ? "pb-6" : ""}
|
||||
flex flex-col justify-start p-4 rounded-xl border transition-all duration-200 h-full
|
||||
${
|
||||
highlight
|
||||
? "bg-primary/10 border-primary/30"
|
||||
|
|
@ -34,8 +33,7 @@
|
|||
{value || "—"}
|
||||
</div>
|
||||
{#if subtitle}
|
||||
<span
|
||||
class="absolute bottom-2 left-4 text-xs text-secondary font-normal opacity-70"
|
||||
<span class="mt-1 text-sm text-secondary font-normal opacity-70"
|
||||
>{subtitle}</span
|
||||
>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
<script module lang="ts">
|
||||
export const layout = false;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Form from "./Form.svelte";
|
||||
import type { OAuthApplicationFormProps } from "./types";
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
themes.includes("darcula") ? "darcula" : themes[0] || "default";
|
||||
|
||||
let selectedTheme = $state("default");
|
||||
let selectedProject = $state("");
|
||||
let selectedProject = $state<string>("");
|
||||
|
||||
$effect(() => {
|
||||
if (
|
||||
|
|
@ -36,8 +36,11 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (!badges.projects.includes(selectedProject)) {
|
||||
selectedProject = badges.projects[0];
|
||||
const projectRepoPath = badges.projects.find(
|
||||
(p) => p.repo_path === selectedProject,
|
||||
);
|
||||
if (!projectRepoPath) {
|
||||
selectedProject = badges.projects[0]?.repo_path || "";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -49,7 +52,8 @@
|
|||
|
||||
const projectBadgeUrl = () => {
|
||||
if (!badges.project_badge_base_url || !selectedProject) return "";
|
||||
return `${badges.project_badge_base_url}${encodeURIComponent(selectedProject)}`;
|
||||
// selectedProject is already in owner/repo format
|
||||
return `${badges.project_badge_base_url}${selectedProject}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -108,8 +112,8 @@
|
|||
id="badge_project"
|
||||
bind:value={selectedProject}
|
||||
items={badges.projects.map((project) => ({
|
||||
value: project,
|
||||
label: project,
|
||||
value: project.repo_path,
|
||||
label: project.display_name,
|
||||
}))}
|
||||
/>
|
||||
<div class="mt-4 rounded-md border border-surface-200 bg-darker p-4">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Form, usePoll } from "@inertiajs/svelte";
|
||||
import { Deferred, Form, usePoll } from "@inertiajs/svelte";
|
||||
import Button from "../../../components/Button.svelte";
|
||||
import SectionCard from "./components/SectionCard.svelte";
|
||||
import SettingsShell from "./Shell.svelte";
|
||||
|
|
@ -61,17 +61,13 @@
|
|||
return Date.now() - overlayStartTime > OVERLAY_TIMEOUT_MS;
|
||||
});
|
||||
|
||||
// Clear overlay when server confirms the import or overlay times out
|
||||
const effectiveImport = $derived(() => {
|
||||
// If overlay is stale, discard it
|
||||
if (isOverlayStale()) {
|
||||
return latest_heartbeat_import;
|
||||
}
|
||||
// If server has caught up to terminal state, prefer server state
|
||||
if (isServerTerminal && importOverlay) {
|
||||
return latest_heartbeat_import;
|
||||
}
|
||||
// Otherwise prefer overlay if it exists
|
||||
return importOverlay ?? latest_heartbeat_import;
|
||||
});
|
||||
|
||||
|
|
@ -224,14 +220,14 @@
|
|||
<div class="space-y-3">
|
||||
{#each PROVIDERS as provider}
|
||||
<label
|
||||
class="flex cursor-pointer items-start gap-3 rounded-md border border-surface-200 bg-darker px-3 py-3 text-sm text-surface-content"
|
||||
class="flex cursor-pointer items-start gap-3 rounded-md border border-surface-200 bg-surface-100 px-3 py-3 text-sm text-surface-content hover:border-surface-300"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="heartbeat_import[provider]"
|
||||
value={provider.value}
|
||||
bind:group={remoteProvider}
|
||||
class="mt-1 h-4 w-4 border-surface-200 bg-surface text-primary"
|
||||
class="mt-1 h-4 w-4 shrink-0 cursor-pointer border-2 border-surface-300 text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
disabled={importInProgress || processing}
|
||||
/>
|
||||
<span class="space-y-1">
|
||||
|
|
@ -297,9 +293,9 @@
|
|||
<span class="text-muted">·</span>
|
||||
<span
|
||||
class={importState === "failed"
|
||||
? "text-red-300"
|
||||
? "text-red-500 dark:text-red-300"
|
||||
: importState === "completed"
|
||||
? "text-green-300"
|
||||
? "text-primary"
|
||||
: "text-muted"}
|
||||
>
|
||||
{prettyStatus(importState)}
|
||||
|
|
@ -364,7 +360,26 @@
|
|||
description="Download your coding history as JSON for backups or analysis."
|
||||
wide
|
||||
>
|
||||
{#if data_export.is_restricted}
|
||||
<Deferred data="data_export">
|
||||
{#snippet fallback()}
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
{#each Array(3) as _}
|
||||
<div
|
||||
class="rounded-md border border-surface-200 bg-darker px-3 py-3"
|
||||
>
|
||||
<div class="h-3 w-24 animate-pulse rounded bg-surface-200"></div>
|
||||
<div
|
||||
class="mt-3 h-5 w-16 animate-pulse rounded bg-surface-200"
|
||||
></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-3 h-4 w-64 animate-pulse rounded bg-surface-200"></div>
|
||||
{/snippet}
|
||||
|
||||
{#if !data_export}
|
||||
<!-- waiting for deferred data -->
|
||||
{:else if data_export.is_restricted}
|
||||
<p
|
||||
class="rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-red-200"
|
||||
>
|
||||
|
|
@ -389,7 +404,9 @@
|
|||
</p>
|
||||
</div>
|
||||
<div class="rounded-md border border-surface-200 bg-darker px-3 py-3">
|
||||
<p class="text-xs uppercase tracking-wide text-muted">Last 7 days</p>
|
||||
<p class="text-xs uppercase tracking-wide text-muted">
|
||||
Last 7 days
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-semibold text-surface-content">
|
||||
{data_export.heartbeats_last_7_days}
|
||||
</p>
|
||||
|
|
@ -530,7 +547,9 @@
|
|||
{prettyStatus(importState)}
|
||||
</span>
|
||||
{#if activeImport?.error_message}
|
||||
<span class="text-red-300">{activeImport.error_message}</span>
|
||||
<span class="text-red-300"
|
||||
>{activeImport.error_message}</span
|
||||
>
|
||||
{/if}
|
||||
{#if importState === "completed"}
|
||||
<span class="text-muted">
|
||||
|
|
@ -545,6 +564,7 @@
|
|||
</Form>
|
||||
{/if}
|
||||
{/if}
|
||||
</Deferred>
|
||||
</SectionCard>
|
||||
|
||||
{#if user.can_request_deletion}
|
||||
|
|
|
|||
|
|
@ -60,11 +60,9 @@
|
|||
{#if footer}
|
||||
<div
|
||||
data-settings-footer
|
||||
class="border-t border-surface-200 bg-surface-100/60 px-5 py-3.5 sm:px-6 sm:py-4"
|
||||
class={`border-t border-surface-200 bg-surface-100/60 px-5 py-3.5 sm:px-6 sm:py-4 ${footerClass}`}
|
||||
>
|
||||
<div class={`${contentWidth} ${footerClass}`}>
|
||||
{@render footer()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export type BadgesProps = {
|
|||
general_badge_url: string;
|
||||
project_badge_url?: string | null;
|
||||
project_badge_base_url?: string | null;
|
||||
projects: string[];
|
||||
projects: Array<{ display_name: string; repo_path: string }>;
|
||||
profile_url?: string | null;
|
||||
markscribe_template: string;
|
||||
markscribe_reference_url: string;
|
||||
|
|
@ -250,7 +250,7 @@ export type BadgesPageProps = SettingsCommonProps & {
|
|||
export type DataPageProps = SettingsCommonProps & {
|
||||
user: UserProps;
|
||||
paths: PathsProps;
|
||||
data_export: DataExportProps;
|
||||
data_export?: DataExportProps;
|
||||
imports_enabled: boolean;
|
||||
remote_import_cooldown_until?: string | null;
|
||||
latest_heartbeat_import?: HeartbeatImportStatusProps | null;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ class Repository < ApplicationRecord
|
|||
last_synced_at.nil? || last_synced_at < 1.day.ago
|
||||
end
|
||||
|
||||
# Get owner/repo format (e.g., "hackclub/hackatime")
|
||||
def full_path
|
||||
"#{owner}/#{name}"
|
||||
end
|
||||
|
||||
# Get formatted languages list
|
||||
def formatted_languages
|
||||
return nil if languages.blank?
|
||||
|
|
|
|||
12
docs/editors/antigravity.md
Normal file
12
docs/editors/antigravity.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Antigravity
|
||||
|
||||
Antigravity is a VSCode fork from Google with built-in AI features for code assistance and generation.
|
||||
|
||||
## Setup
|
||||
|
||||
Antigravity uses the WakaTime extension for time tracking. Follow these steps to track your coding time with Hackatime:
|
||||
|
||||
1. Configure your editor by using our [simple installer](https://hackatime.hackclub.com/my/wakatime_setup)
|
||||
2. Restart Antigravity and start coding!
|
||||
|
||||
The WakaTime extension will automatically track your time spent coding in Antigravity.
|
||||
|
|
@ -8,7 +8,7 @@ Follow these steps to start tracking your game development in Godot with Hackati
|
|||
|
||||
**[hackatime.hackclub.com](https://hackatime.hackclub.com)**
|
||||
|
||||
Sign in with your Hack Club Slack account. If you haven't created one yet, head to **[shiba.hackclub.com](https://shiba.hackclub.com)** and enter your email address to get a link to the Slack :)
|
||||
Sign in with your Hackatime account (or create one if you don't have one yet)!
|
||||
|
||||
## 2: Set up Hackatime
|
||||
|
||||
|
|
@ -22,9 +22,7 @@ This will set some environment variables and tell Hackatime where to "phone home
|
|||
|
||||
**[godotengine.org/download](https://godotengine.org/download)**
|
||||
|
||||
Download the Godot binary! Make sure you get the **regular version** and not the .NET version - web exports (required for Shiba) aren't supported with .NET.
|
||||
|
||||
If you're on Linux and know what you're doing, make sure to check your distro's package manager for any special builds.
|
||||
Download the Godot binary! If you're on Linux and know what you're doing, make sure to check your distro's package manager for any special builds.
|
||||
|
||||
## 4: Create a New Godot Project
|
||||
|
||||
|
|
@ -65,20 +63,12 @@ If you'd like to be able to run your game in browser, choose the **Mobile** rend
|
|||
|
||||
**Important:** You need to install Godot Super WakaTime for every project (it's a Godot limitation)
|
||||
|
||||
## ✅ All Done!
|
||||
## All Done!
|
||||
|
||||
After you're finished, make sure to check **[hackatime.hackclub.com](https://hackatime.hackclub.com)** after a little while and ensure you're logging progress!
|
||||
|
||||
You can also try some **[wakatime.com/plugins](https://wakatime.com/plugins)** if you'd like to log time spent editing your project in other programs.
|
||||
|
||||
## Features
|
||||
|
||||
This Hack Club-approved plugin provides:
|
||||
- **Accurate tracking** - Differentiates between script editing and scene building
|
||||
- **Detailed metrics** - Counts keystrokes as coding, mouse clicks as scene work
|
||||
- **Smart detection** - Tracks scene structure changes and file modifications
|
||||
- **Seamless integration** - Works with your existing Hackatime setup
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
|
||||

|
||||
|
||||
## Automatic Setup
|
||||
|
||||
1. Configure your editor by using our [simple installer](https://hackatime.hackclub.com/my/wakatime_setup)
|
||||
2. Restart VS Code and start coding!
|
||||
|
||||
---
|
||||
|
||||
Let's set up VS Code to count how much time you spend coding!
|
||||
|
||||
## Step 1: Make a Hackatime Account
|
||||
|
|
@ -10,7 +17,7 @@ Go to [Hackatime](https://hackatime.hackclub.com) and make an account. Then log
|
|||
|
||||
## Step 2: Get Your Settings Ready
|
||||
|
||||
Click this link to the [setup page](https://hackatime.hackclub.com/my/wakatime_setup). It will set up your account so it works with VS Code.
|
||||
Click this link to the [setup page](https://hackatime.hackclub.com/my/wakatime_setup). It will set up your device so it works with Hackatime.
|
||||
|
||||
## Step 3: Add the Plugin to VS Code
|
||||
|
||||
|
|
@ -18,7 +25,7 @@ Click this link to the [setup page](https://hackatime.hackclub.com/my/wakatime_s
|
|||
2. Click the squares icon on the left (Extensions)
|
||||
3. Type "WakaTime" in the search box
|
||||
4. Click the **Install** button next to WakaTime
|
||||
5. VS Code might ask you to restart - click **Reload** if it does
|
||||
5. VS Code might ask you to reload your extensions to get it working - click **Reload Extensions** if it does
|
||||
|
||||
That's it! The plugin will use your settings from Step 2.
|
||||
|
||||
|
|
|
|||
BIN
public/images/editor-icons/antigravity-128.png
Normal file
BIN
public/images/editor-icons/antigravity-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -90,7 +90,17 @@ RSpec.describe 'Api::Hackatime::V1::Compatibility', type: :request do
|
|||
type: :object,
|
||||
properties: {
|
||||
total_seconds: { type: :number, example: 7200.0 },
|
||||
text: { type: :string, example: '2 hrs' }
|
||||
text: { type: :string, example: '2h 30m / 4h today' }
|
||||
}
|
||||
},
|
||||
goal: {
|
||||
type: :object,
|
||||
nullable: true,
|
||||
properties: {
|
||||
target_seconds: { type: :number, example: 14400 },
|
||||
tracked_seconds: { type: :number, example: 9000 },
|
||||
completion_percent: { type: :number, example: 62 },
|
||||
complete: { type: :boolean, example: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1695,7 +1695,23 @@ paths:
|
|||
example: 7200.0
|
||||
text:
|
||||
type: string
|
||||
example: 2 hrs
|
||||
example: 2h 30m / 4h today
|
||||
goal:
|
||||
type: object
|
||||
nullable: true
|
||||
properties:
|
||||
target_seconds:
|
||||
type: number
|
||||
example: 14400
|
||||
tracked_seconds:
|
||||
type: number
|
||||
example: 9000
|
||||
completion_percent:
|
||||
type: number
|
||||
example: 62
|
||||
complete:
|
||||
type: boolean
|
||||
example: false
|
||||
'401':
|
||||
description: unauthorized
|
||||
"/api/hackatime/v1/users/current/stats/last_7_days":
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue