From d4e9a1a5aaae35bab5ff93c9c943804ed0486946 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Wed, 16 Apr 2025 09:30:15 -0400 Subject: [PATCH 01/28] Add leaderboard emoji to extension text --- app/jobs/leaderboard_update_job.rb | 7 +++--- app/models/user.rb | 36 +++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/jobs/leaderboard_update_job.rb b/app/jobs/leaderboard_update_job.rb index 52606ce0..45d8db4d 100644 --- a/app/jobs/leaderboard_update_job.rb +++ b/app/jobs/leaderboard_update_job.rb @@ -39,16 +39,17 @@ def perform(period_type = :daily, date = Date.current) .group(:user_id) .duration_seconds - entries_data = entries_data.filter { |_, total_seconds| total_seconds > 60 } + entries_data = entries_data.filter { |_, total_seconds| total_seconds > 60 }.sort_by { |_, total_seconds| -total_seconds } streaks = Heartbeat.daily_streaks_for_users(entries_data.map { |user_id, _| user_id }) - entries_data = entries_data.map do |user_id, total_seconds| + entries_data = entries_data.map.with_index(1) do |(user_id, total_seconds), index| { leaderboard_id: leaderboard.id, user_id: user_id, total_seconds: total_seconds, - streak_count: streaks[user_id] || 0 + streak_count: streaks[user_id] || 0, + rank: index } end diff --git a/app/models/user.rb b/app/models/user.rb index bb30e1a2..1eab65a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,11 +46,41 @@ def data_migration_jobs ).order(created_at: :desc).limit(10).all end - def format_extension_text(duration) - case hackatime_extension_text_type + def leaderboard_rank_text + @leaderboard_rank_text ||= begin + entry = LeaderboardEntry.joins(:leaderboard) + .where(user: self) + .where(leaderboards: { period_type: [ :daily, :weekly, :last_7_days ] }) + .where(rank: 1..3) + .order("leaderboards.period_type ASC") + .first + + return nil unless entry + + rank_emoji = { + 1 => "๐Ÿฅ‡", + 2 => "๐Ÿฅˆ", + 3 => "๐Ÿฅ‰" + }[entry.rank] + + leaderboard_type = case entry.leaderboard.period_type + when "daily" then "daily" + when "weekly" then "weekly" + when "last_7_days" then "7-day" + end + + "#{rank_emoji} on #{leaderboard_type} ldbrd" + end + end + + def format_extension_text(duration, text_type = hackatime_extension_text_type) + case text_type when "simple_text" return "Start coding to track your time" if duration.zero? - ::ApplicationController.helpers.short_time_simple(duration) + + msg = ::ApplicationController.helpers.short_time_simple(duration) + msg += " (#{leaderboard_rank_text})" if leaderboard_rank_text.present? + msg when "clock_emoji" ::ApplicationController.helpers.time_in_emoji(duration) when "compliment_text" From 2ffaa636d988f7e7b7210ac3af4cd017d869772a Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Sat, 19 Apr 2025 14:10:12 -0400 Subject: [PATCH 02/28] Add membership pages --- .env.example | 4 ++- Gemfile | 5 +++ Gemfile.lock | 16 ++++++++++ app/controllers/memberships_controller.rb | 37 +++++++++++++++++++++++ app/models/airtable/approved_project.rb | 26 ++++++++++++++++ app/views/memberships/index.html.erb | 37 +++++++++++++++++++++++ app/views/memberships/show.html.erb | 29 ++++++++++++++++++ app/views/shared/_nav.html.erb | 11 +++++++ config/routes.rb | 5 +++ 9 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 app/controllers/memberships_controller.rb create mode 100644 app/models/airtable/approved_project.rb create mode 100644 app/views/memberships/index.html.erb create mode 100644 app/views/memberships/show.html.erb diff --git a/.env.example b/.env.example index 62c9f8ea..99dacaee 100644 --- a/.env.example +++ b/.env.example @@ -47,4 +47,6 @@ WILDCARD_HOST=your_wildcard_host_here GITHUB_CLIENT_ID=your_github_client_id_here GITHUB_CLIENT_SECRET=your_github_client_secret_here -SKYLIGHT_AUTHENTICATION=replace_me \ No newline at end of file +SKYLIGHT_AUTHENTICATION=replace_me + +YSWS_AIRTABLE_API_KEY=your_airtable_api_key_here \ No newline at end of file diff --git a/Gemfile b/Gemfile index cff1eb00..d4ca8eee 100644 --- a/Gemfile +++ b/Gemfile @@ -80,6 +80,11 @@ gem "flamegraph" gem "skylight" +gem "airrecord" + +# For activity logging +gem "public_activity" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" diff --git a/Gemfile.lock b/Gemfile.lock index 9ed3b1fa..5d2251a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,6 +79,10 @@ GEM uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + airrecord (1.0.12) + faraday (>= 1.0, < 3.0) + faraday-net_http_persistent + net-http-persistent ast (2.4.2) avo (3.19.3) actionview (>= 6.1) @@ -151,6 +155,9 @@ GEM multipart-post (~> 2.0) faraday-net_http (3.4.0) net-http (>= 0.5.0) + faraday-net_http_persistent (2.3.0) + faraday (~> 2.5) + net-http-persistent (>= 4.0.4, < 5) ffi (1.17.1-aarch64-linux-gnu) ffi (1.17.1-aarch64-linux-musl) ffi (1.17.1-arm-linux-gnu) @@ -252,6 +259,8 @@ GEM multipart-post (2.4.1) net-http (0.6.0) uri + net-http-persistent (4.0.5) + connection_pool (~> 2.2) net-imap (0.5.6) date net-protocol @@ -304,6 +313,11 @@ GEM psych (5.2.3) date stringio + public_activity (3.0.1) + actionpack (>= 6.1.0) + activerecord (>= 6.1) + i18n (>= 0.5.0) + railties (>= 6.1.0) public_suffix (6.0.1) puma (6.6.0) nio4r (~> 2.0) @@ -491,6 +505,7 @@ PLATFORMS DEPENDENCIES activerecord-import + airrecord avo (>= 3.2.1) avo-record_link_field (~> 0.0.2) bootsnap @@ -511,6 +526,7 @@ DEPENDENCIES paper_trail pg propshaft + public_activity puma (>= 5.0) query_count rack-cors diff --git a/app/controllers/memberships_controller.rb b/app/controllers/memberships_controller.rb new file mode 100644 index 00000000..b640d2c4 --- /dev/null +++ b/app/controllers/memberships_controller.rb @@ -0,0 +1,37 @@ +class MembershipsController < ApplicationController + before_action :authenticate_user!, only: [ :my_membership ] + + def index + # Public page about membership perks + end + + def my_membership + @user = current_user + render :show + end + + def show + # Public view of a specific user's membership + @user = User.find_by!(slack_uid: params[:slack_uid]) + @ysws_projects = get_ysws_projects + end + + private + + def set_user + @user = begin + if params[:slack_uid] == "my" + current_user + else + User.find_by!(slack_uid: params[:slack_uid]) + end + rescue + nil + end + end + + def get_ysws_projects + return [] if @user.nil? + Airtable::ApprovedProject.find_by_user(@user) + end +end diff --git a/app/models/airtable/approved_project.rb b/app/models/airtable/approved_project.rb new file mode 100644 index 00000000..9bb8cb69 --- /dev/null +++ b/app/models/airtable/approved_project.rb @@ -0,0 +1,26 @@ +Airrecord.api_key = ENV["YSWS_AIRTABLE_API_KEY"] +class Airtable::ApprovedProject < Airrecord::Table + self.base_key = "app3A5kJwYqxMLOgh" + self.table_name = "Approved Projects" + + def self.find_by_slack_uid(slack_uid) + user = User.find_by(slack_uid: slack_uid) + return [] if user.nil? + find_by_user(user) + end + + def self.find_by_user(user) + emails = EmailAddress.where(user: user).pluck(:email) + puts "emails: #{emails}" + if emails.none? + [] + elsif emails.length == 1 + puts "searching for #{emails.first}" + result = self.all(filter: "{Email} = \"#{emails.first}\"") + puts "result: #{result}" + result + else + self.all(filter: "OR(" + emails.map { |email| "Email = \"#{email}\"" }.join(",") + ")") + end + end +end diff --git a/app/views/memberships/index.html.erb b/app/views/memberships/index.html.erb new file mode 100644 index 00000000..ca886d6e --- /dev/null +++ b/app/views/memberships/index.html.erb @@ -0,0 +1,37 @@ +<%# app/views/memberships/index.html.erb %> +
+

Hack Club Membership

+ +
+

Membership Perks

+ +
+

๐Ÿ”ฅ Streak Tracking

+

Track your coding streak and see how many days in a row you've been coding!

+
+ +
+

๐Ÿ“Š Activity Dashboard

+

View your coding activity and see your progress over time.

+
+ +
+

๐Ÿ† Leaderboards

+

Compete with other members on the leaderboards and see where you rank!

+
+ +
+

๐Ÿค Community Access

+

Connect with other Hack Club members and share your projects.

+
+
+ +
+

Ready to Join?

+ <% if current_user %> +

You're already a member! Check out your <%= link_to "membership status", my_membership_path %>.

+ <% else %> +

<%= link_to "Sign in with Slack", slack_auth_path, class: "button" %> to become a member!

+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/memberships/show.html.erb b/app/views/memberships/show.html.erb new file mode 100644 index 00000000..e666e0db --- /dev/null +++ b/app/views/memberships/show.html.erb @@ -0,0 +1,29 @@ +<%# app/views/memberships/show.html.erb %> +
+ <% if @user %> +

Membership for <%= render "shared/user_mention", user: @user %>

+ +
+

Slack UID: <%= @user.slack_uid %>

+

Member since: <%= @user.created_at.strftime("%B %d, %Y") %>

+ <% if @user.streak_days_formatted %> +

Current streak: <%= @user.streak_days_formatted %> days ๐Ÿ”ฅ

+ <% end %> +
+ + <%# list of ysws projects in @ysws_projects %> +
    + <% @ysws_projects.each do |project| %> +
  • + <%= project["Name"] %> + <%= project["Hours Spent"] %>hrs + <%= link_to "Play", project["Playable URL"], target: "_blank" %> + <%= link_to "Code", project["Code URL"], target: "_blank" %> +
  • + <% end %> +
+ <% else %> +

User Not Found

+

The requested user could not be found.

+ <% end %> +
\ No newline at end of file diff --git a/app/views/shared/_nav.html.erb b/app/views/shared/_nav.html.erb index 21f131f3..cbbb1b19 100644 --- a/app/views/shared/_nav.html.erb +++ b/app/views/shared/_nav.html.erb @@ -33,6 +33,17 @@ Leaderboards <% end %> +
  • + <% if current_user %> + <%= link_to my_membership_path, class: "nav-item #{current_page?(my_membership_path) ? 'active' : ''}" do %> + Membership + <% end %> + <% else %> + <%= link_to membership_path, class: "nav-item #{current_page?(membership_path) ? 'active' : ''}" do %> + Membership + <% end %> + <% end %> +
  • <% if current_user %>
  • <%= link_to my_settings_path, class: "nav-item #{current_page?(my_settings_path) ? 'active' : ''}" do %> diff --git a/config/routes.rb b/config/routes.rb index 7a0d690b..5cbde7d0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,6 +62,11 @@ def self.matches?(request) patch "my/settings", to: "users#update" post "my/settings/migrate_heartbeats", to: "users#migrate_heartbeats", as: :my_settings_migrate_heartbeats + # Membership routes + get "membership", to: "memberships#index", as: :membership + get "my/membership", to: "memberships#my_membership", as: :my_membership + get ":slack_uid/membership", to: "memberships#show", as: :user_membership + get "my/wakatime_setup", to: "users#wakatime_setup" get "my/wakatime_setup/step-2", to: "users#wakatime_setup_step_2" get "my/wakatime_setup/step-3", to: "users#wakatime_setup_step_3" From 2a85cd1fc041f077a1808a848498937a7f5158a5 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Sat, 19 Apr 2025 14:44:11 -0400 Subject: [PATCH 03/28] Add in "member since" data --- app/controllers/memberships_controller.rb | 3 +++ app/models/airtable/hack_clubber.rb | 23 +++++++++++++++++++++++ app/views/memberships/show.html.erb | 13 ++++++++----- 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 app/models/airtable/hack_clubber.rb diff --git a/app/controllers/memberships_controller.rb b/app/controllers/memberships_controller.rb index b640d2c4..c9dda65c 100644 --- a/app/controllers/memberships_controller.rb +++ b/app/controllers/memberships_controller.rb @@ -7,6 +7,8 @@ def index def my_membership @user = current_user + @ysws_projects = get_ysws_projects + @member_since = Airtable::HackClubber.member_since(@user) render :show end @@ -14,6 +16,7 @@ def show # Public view of a specific user's membership @user = User.find_by!(slack_uid: params[:slack_uid]) @ysws_projects = get_ysws_projects + @member_since = Airtable::HackClubber.member_since(@user) end private diff --git a/app/models/airtable/hack_clubber.rb b/app/models/airtable/hack_clubber.rb new file mode 100644 index 00000000..2b260701 --- /dev/null +++ b/app/models/airtable/hack_clubber.rb @@ -0,0 +1,23 @@ +Airrecord.api_key = ENV["YSWS_AIRTABLE_API_KEY"] + +class Airtable::HackClubber < Airrecord::Table + self.base_key = "app3A5kJwYqxMLOgh" + self.table_name = "Synced - Hack Clubbers" + + def self.member_since(user) + user_emails = EmailAddress.where(user: user).pluck(:email) + return nil if user_emails.empty? + users = self.find_by_email(user_emails) + return nil if users.empty? + start_date = users.map { |user| user["First Engagement At"] }.min + Date.parse(start_date) + end + + def self.find_by_email(emails) + if emails.length == 1 + self.all(filter: "{Email} = \"#{emails.first}\"") + else + self.all(filter: "OR(" + emails.map { |email| "{Email} = \"#{email}\"" }.join(",") + ")") + end + end +end diff --git a/app/views/memberships/show.html.erb b/app/views/memberships/show.html.erb index e666e0db..a7639de0 100644 --- a/app/views/memberships/show.html.erb +++ b/app/views/memberships/show.html.erb @@ -4,19 +4,22 @@

    Membership for <%= render "shared/user_mention", user: @user %>

    -

    Slack UID: <%= @user.slack_uid %>

    -

    Member since: <%= @user.created_at.strftime("%B %d, %Y") %>

    + <% if @member_since %> +

    + Member since: <%= @member_since.strftime("%B %d, %Y") %> + (<%= time_ago_in_words(@member_since) %> ago) +

    + <% end %> <% if @user.streak_days_formatted %>

    Current streak: <%= @user.streak_days_formatted %> days ๐Ÿ”ฅ

    <% end %>
    - <%# list of ysws projects in @ysws_projects %>