Skip to content

Recent Activity Tab #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
110 changes: 110 additions & 0 deletions app/assets/stylesheets/activity.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
.recent-activity {
margin-top: 1rem;
}

.recent-activity h3 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
color: var(--muted-color);
}

.activity-list {
max-height: 600px;
overflow-y: auto;
}

.activity-item {
padding: 0.75rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
font-size: 0.9rem;
}

.activity-item:last-child {
border-bottom: none;
}

.activity-content {
margin: 0.5rem 0;
}

.activity-content p {
margin: 0;
}

.activity-time {
color: var(--muted-color);
font-size: 0.75rem;
}

.activity-actions {
display: flex;
margin-top: 0.5rem;
}

.give-kudos-btn {
display: inline-flex;
align-items: center;
background-color: rgba(var(--primary-color-rgb), 0.1);
color: var(--primary-color);
padding: 0.25rem 0.5rem;
border-radius: 4px;
text-decoration: none;
font-size: 0.75rem;
transition: background-color 0.2s;
}

.give-kudos-btn:hover {
background-color: rgba(var(--primary-color-rgb), 0.2);
}

.kudos-icon {
margin-right: 0.25rem;
font-style: normal;
}

.kudos-count {
margin-left: 0.25rem;
background: rgba(var(--primary-color-rgb), 0.2);
border-radius: 10px;
padding: 0.1rem 0.4rem;
font-size: 0.7rem;
}

.kudos-given,
.kudos-received {
display: inline-flex;
align-items: center;
color: var(--primary-color);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
background-color: rgba(var(--primary-color-rgb), 0.1);
}

/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
.activity-item {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.activity-time {
color: rgba(255, 255, 255, 0.5);
}

.give-kudos-btn {
background-color: rgba(var(--primary-color-rgb), 0.2);
}

.give-kudos-btn:hover {
background-color: rgba(var(--primary-color-rgb), 0.3);
}

.kudos-count {
background: rgba(var(--primary-color-rgb), 0.3);
}

.kudos-given,
.kudos-received {
background-color: rgba(var(--primary-color-rgb), 0.2);
}
}
31 changes: 31 additions & 0 deletions app/controllers/project_milestones_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class ProjectMilestonesController < ApplicationController
before_action :authenticate_user!

def give_kudos
milestone = ProjectMilestone.find(params[:id])

# Don't allow users to give kudos to themselves
if milestone.user_id == current_user.id
return render json: { error: "You cannot give kudos to yourself" }, status: :unprocessable_entity
end

# Check if user already gave kudos
if milestone.kudos_from?(current_user.id)
return render json: { error: "You already gave kudos for this milestone" }, status: :unprocessable_entity
end

kudos = ProjectMilestoneKudos.new(
project_milestone: milestone,
user_id: current_user.id
)

if kudos.save
render json: {
success: true,
kudos_count: milestone.reload.kudos_count
}
else
render json: { error: kudos.errors.full_messages.join(", ") }, status: :unprocessable_entity
end
end
end
43 changes: 43 additions & 0 deletions app/javascript/controllers/kudos_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["button"]

connect() {
console.log("Kudos controller connected")
}

giveKudos(event) {
event.preventDefault()

const button = event.currentTarget
const url = button.getAttribute("href")

fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']").content,
"Accept": "application/json"
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Replace the button with the kudos count
const container = button.closest(".activity-actions")

const kudosEl = document.createElement("span")
kudosEl.classList.add("kudos-given")
kudosEl.innerHTML = `<i class="kudos-icon">👏</i> ${data.kudos_count}`

container.innerHTML = ""
container.appendChild(kudosEl)
} else {
console.error("Error giving kudos:", data.error)
}
})
.catch(error => {
console.error("Error giving kudos:", error)
})
}
}
50 changes: 50 additions & 0 deletions app/jobs/project_milestone_check_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
class ProjectMilestoneCheckJob < ApplicationJob
queue_as :default

def perform
# Get all users with heartbeats in the last hour
active_users = Heartbeat.where("created_at > ?", 1.hour.ago)
.distinct.pluck(:user_id)

active_users.each do |user_id|
check_hourly_milestones(user_id)
end
end

private

def check_hourly_milestones(user_id)
user = User.find_by(id: user_id)
return unless user

# Get projects with significant time in the last period
project_durations = user.heartbeats.today.group(:project).duration_seconds

project_durations.each do |project, duration|
next if project.blank?

# Convert to hours
hours = (duration / 3600.0).floor
next if hours < 1

# Check if we already have a milestone for this hour count
existing = ProjectMilestone.where(
user_id: user_id,
project_name: project,
milestone_type: :hourly,
milestone_value: hours
).where("created_at > ?", 1.day.ago).exists?

# If no milestone exists, create one
unless existing
ProjectMilestone.create!(
user_id: user_id,
project_name: project,
milestone_type: :hourly,
milestone_value: hours
)
Rails.logger.info "Created hourly milestone for user #{user_id} on project #{project}: #{hours} hours"
end
end
end
end
38 changes: 38 additions & 0 deletions app/models/project_milestone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class ProjectMilestone < ApplicationRecord
belongs_to :user, foreign_key: :user_id

has_many :project_milestone_kudos, class_name: "ProjectMilestoneKudos"

validates :project_name, presence: true
validates :milestone_type, presence: true
validates :milestone_value, presence: true

# We keep this because I don't want to change the database schema
enum :milestone_type, {
hourly: 0,
daily: 1,
weekly: 2
}

# Get milestones for display in the sidebar
def self.recent_for_display(limit = 20)
where(milestone_type: :hourly)
.order(created_at: :desc)
.includes(:user, :project_milestone_kudos)
.limit(limit)
end

# Check if the current user has given kudos to this milestone
def kudos_from?(user_id)
project_milestone_kudos.where(user_id: user_id).exists?
end

# Get the kudos count
def kudos_count
project_milestone_kudos.count
end

def formatted_message
"completed #{milestone_value} hour#{'s' if milestone_value > 1} on #{project_name}"
end
end
6 changes: 6 additions & 0 deletions app/models/project_milestone_kudos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ProjectMilestoneKudos < ApplicationRecord
belongs_to :project_milestone
belongs_to :user

validates :project_milestone_id, uniqueness: { scope: :user_id }
end
40 changes: 40 additions & 0 deletions app/views/shared/_nav.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,44 @@
<% end %>
<% end %>
</ul>

<% if current_user %>
<hr />

<div class="recent-activity">
<h3>Recent activity</h3>

<div class="activity-list">
<% ProjectMilestone.recent_for_display(10).each do |milestone| %>
<div class="activity-item">
<%= render "shared/user_mention", user: milestone.user %>
<div class="activity-content">
<p><%= milestone.formatted_message %></p>
<div class="activity-actions">
<% if current_user.id != milestone.user_id %>
<% if milestone.kudos_from?(current_user.id) %>
<span class="kudos-given">
<i class="kudos-icon">👏</i> <%= milestone.kudos_count %>
</span>
<% else %>
<%= link_to give_kudos_project_milestone_path(milestone),
data: { turbo_method: :post, controller: "kudos", action: "click->kudos#giveKudos" },
class: "give-kudos-btn" do %>
<i class="kudos-icon">👏</i> Give kudos
<% if milestone.kudos_count > 0 %><span class="kudos-count"><%= milestone.kudos_count %></span><% end %>
<% end %>
<% end %>
<% elsif milestone.kudos_count > 0 %>
<span class="kudos-received">
<i class="kudos-icon">👏</i> <%= milestone.kudos_count %>
</span>
<% end %>
</div>
</div>
<span class="activity-time"><%= time_ago_in_words(milestone.created_at) %> ago</span>
</div>
<% end %>
</div>
</div>
<% end %>
</aside>
4 changes: 4 additions & 0 deletions config/initializers/good_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
scan_github_repos: {
cron: "0 10 * * *",
class: "ScanGithubReposJob"
},
project_milestone_check: {
cron: "*/5 * * * *",
class: "ProjectMilestoneCheckJob"
}
}
end
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,10 @@ def self.matches?(request)
end

resources :scrapyard_leaderboards, only: [ :index, :show ]

resources :project_milestones, only: [] do
member do
post :give_kudos
end
end
end
17 changes: 17 additions & 0 deletions db/migrate/20250320052532_create_project_milestones.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class CreateProjectMilestones < ActiveRecord::Migration[8.0]
def change
create_table :project_milestones do |t|
t.bigint :user_id, null: false
t.string :project_name, null: false
t.integer :milestone_type, null: false, default: 0
t.integer :milestone_value, null: false
t.boolean :notified, default: false

t.timestamps
end

add_index :project_milestones, :user_id
add_index :project_milestones, [ :user_id, :project_name, :milestone_type ]
add_index :project_milestones, :created_at
end
end
12 changes: 12 additions & 0 deletions db/migrate/20250320052612_create_project_milestone_kudos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateProjectMilestoneKudos < ActiveRecord::Migration[8.0]
def change
create_table :project_milestone_kudos do |t|
t.references :project_milestone, null: false, foreign_key: true
t.bigint :user_id, null: false

t.timestamps
end

add_index :project_milestone_kudos, [ :project_milestone_id, :user_id ], unique: true
end
end
Loading