diff --git a/.gitattributes b/.gitattributes
index 8dc4323..65a3dd3 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -7,3 +7,8 @@ db/schema.rb linguist-generated
vendor/* linguist-vendored
config/credentials/*.yml.enc diff=rails_credentials
config/credentials.yml.enc diff=rails_credentials
+
+# TWO HOURS. This took 2 HOURS to figure out I hate windows now
+# Just makes stuff easy to run on Windows without it yelling at you!
+*.sh text eol=lf
+bin/* text eol=lf
diff --git a/app/controllers/my/heartbeats_controller.rb b/app/controllers/my/heartbeats_controller.rb
new file mode 100644
index 0000000..61811f6
--- /dev/null
+++ b/app/controllers/my/heartbeats_controller.rb
@@ -0,0 +1,85 @@
+module My
+ class HeartbeatsController < ApplicationController
+ before_action :ensure_current_user
+
+
+ def export
+ all_data = params[:all_data] == "true"
+ if all_data
+ heartbeats = current_user.heartbeats.order(time: :asc)
+ if heartbeats.any?
+ start_date = Time.at(heartbeats.first.time).to_date
+ end_date = Time.at(heartbeats.last.time).to_date
+ else
+ start_date = Date.current
+ end_date = Date.current
+ end
+ else
+ start_date = params[:start_date].present? ? Date.parse(params[:start_date]) : 30.days.ago.to_date
+ end_date = params[:end_date].present? ? Date.parse(params[:end_date]) : Date.current
+ start_time = start_date.beginning_of_day.to_f
+ end_time = end_date.end_of_day.to_f
+
+ heartbeats = current_user.heartbeats
+ .where("time >= ? AND time <= ?", start_time, end_time)
+ .order(time: :asc)
+ end
+
+
+ export_data = {
+ export_info: {
+ exported_at: Time.current.iso8601,
+ date_range: {
+ start_date: start_date.iso8601,
+ end_date: end_date.iso8601
+ },
+ total_heartbeats: heartbeats.count,
+ total_duration_seconds: heartbeats.duration_seconds
+ },
+ heartbeats: heartbeats.map do |heartbeat|
+ {
+ id: heartbeat.id,
+ time: Time.at(heartbeat.time).iso8601,
+ entity: heartbeat.entity,
+ type: heartbeat.type,
+ category: heartbeat.category,
+ project: heartbeat.project,
+ language: heartbeat.language,
+ editor: heartbeat.editor,
+ operating_system: heartbeat.operating_system,
+ machine: heartbeat.machine,
+ branch: heartbeat.branch,
+ user_agent: heartbeat.user_agent,
+ is_write: heartbeat.is_write,
+ line_additions: heartbeat.line_additions,
+ line_deletions: heartbeat.line_deletions,
+ lineno: heartbeat.lineno,
+ lines: heartbeat.lines,
+ cursorpos: heartbeat.cursorpos,
+ dependencies: heartbeat.dependencies,
+ source_type: heartbeat.source_type,
+ created_at: heartbeat.created_at.iso8601,
+ updated_at: heartbeat.updated_at.iso8601
+ }
+ end
+ }
+
+ filename = "heartbeats_#{current_user.slack_uid}_#{start_date.strftime('%Y%m%d')}_#{end_date.strftime('%Y%m%d')}.json"
+
+ respond_to do |format|
+ format.json {
+ send_data export_data.to_json,
+ filename: filename,
+ type: "application/json",
+ disposition: "attachment"
+ }
+ end
+ end
+
+ private
+
+ def ensure_current_user
+ redirect_to root_path, alert: "You must be logged in to view this page!!" unless current_user
+ end
+ end
+end
diff --git a/app/javascript/controllers/heartbeat_export_controller.js b/app/javascript/controllers/heartbeat_export_controller.js
new file mode 100644
index 0000000..4952281
--- /dev/null
+++ b/app/javascript/controllers/heartbeat_export_controller.js
@@ -0,0 +1,120 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+// The Calender thing is mostly vibe coded, pls check
+
+ handleExport(event) {
+ event.preventDefault()
+
+ this.showDateThing()
+ }
+
+ showDateThing() {
+ const modalHTML = `
+
+ `
+
+ document.body.insertAdjacentHTML("beforeend", modalHTML)
+
+ document.getElementById("cancel-export").addEventListener("click", this.closeme)
+ document.getElementById("export-form").addEventListener("submit", this.exportIT.bind(this))
+
+ document.getElementById("export-modal").addEventListener("click", (event) => {
+ if (event.target.id === "export-modal") {
+ this.closeme()
+ }
+ })
+ }
+ getDefaultStartDate() {
+ const date = new Date()
+ date.setDate(date.getDate() - 30)
+ return date.toISOString().split("T")[0]
+ }
+
+ getDefaultEndDate() {
+ return new Date().toISOString().split("T")[0]
+ }
+
+ closeme() {
+ const modal = document.getElementById("export-modal")
+ if (modal) {
+ modal.remove()
+ }
+ }
+
+ async exportIT(event) {
+ event.preventDefault()
+
+ const startDate = document.getElementById("start-date").value
+ const endDate = document.getElementById("end-date").value
+
+ if (!startDate || !endDate) {
+ alert("Please select both start and end dates")
+ return
+ }
+
+ if (new Date(startDate) > new Date(endDate)) {
+ alert("Start date must be before end date")
+ return
+ }
+
+ const submitButton = document.getElementById("confirm-export")
+ const originalText = submitButton.textContent
+ submitButton.textContent = "Exporting..."
+ submitButton.disabled = true
+
+ try {
+ const exportUrl = `/my/heartbeats/export.json?start_date=${startDate}&end_date=${endDate}`
+
+ const link = document.createElement("a")
+ link.href = exportUrl
+ link.download = `heartbeats_${startDate}_${endDate}.json`
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+
+ setTimeout(() => {
+ this.closeme()
+ }, 1000)
+
+ } catch (error) {
+ console.error("Export failed:", error)
+ alert("Export failed. Please try again. :(")
+
+ submitButton.textContent = originalText
+ submitButton.disabled = false
+ }
+ }
+
+
+}
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index 6e2349a..7e6c571 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -344,6 +344,87 @@
+ <%# This is copied from the github thingie blog, Im not good at UI so I copied :) %>
+
+
+
+
+ 💾
+
+
Download Your Data
+
+
+
+
+
Your Data Overview
+
+
+
+
<%= number_with_delimiter(@user.heartbeats.count) %>
+
Total Heartbeats
+
+
+
+
+
<%= @user.heartbeats.duration_simple %>
+
Total Coding Time
+
+
+
+
+
<%= @user.heartbeats.where("time >= ?", 7.days.ago.to_f).count %>
+
Heartbeats in the Last 7 Days
+
+
+
+
+
+
+
Export Options
+
+
+
+
Export your coding activity as JSON with detailed information about each coding session.
+
+
+ <%= link_to export_my_heartbeats_path(format: :json, all_data: "true"),
+ class: "w-full bg-primary hover:bg-red text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2",
+ method: :get do %>
+
+ Export All Heartbeats
+ <% end %>
+
+ <%= link_to "#",
+ class: "w-full bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2",
+ data: {
+ controller: "heartbeat-export",
+ action: "click->heartbeat-export#handleExport"
+ } do %>
+
+ Export Date Range
+ <% end %>
+
+
+
+
All Heartbeats: Downloads your complete coding history, from the very start to your last heartbeat
+
Date Range: Choose specific dates to export
+
+
+
+
+
+
+
+
<% admin_tool do %>
diff --git a/config/routes.rb b/config/routes.rb
index 924dc7d..67aa8eb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -107,9 +107,14 @@ Rails.application.routes.draw do
post "my/settings/migrate_heartbeats", to: "users#migrate_heartbeats", as: :my_settings_migrate_heartbeats
namespace :my do
- resources :project_repo_mappings, param: :project_name, only: [ :edit, :update ], constraints: { project_name: /.+/ }
+ resources :project_repo_mappings, param: :project_name, only: [ :edit, :update ], constraints: { project_name: /.+/ }
resource :mailing_address, only: [ :show, :edit ]
get "mailroom", to: "mailroom#index"
+ resources :heartbeats, only: [] do
+ collection do
+ get :export
+ end
+ end
end
get "my/wakatime_setup", to: "users#wakatime_setup"