mirror of
https://github.com/System-End/hackatime-desktop.git
synced 2026-04-19 22:05:10 +00:00
fix: migrate api to new version
This commit is contained in:
parent
d69129d43e
commit
4183bff4f6
3 changed files with 288 additions and 198 deletions
|
|
@ -12,6 +12,7 @@ use sha2::{Sha256, Digest};
|
|||
use base64::{Engine as _, engine::general_purpose};
|
||||
use rand::{Rng, thread_rng};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use chrono::Datelike;
|
||||
|
||||
mod database;
|
||||
mod discord_rpc;
|
||||
|
|
@ -217,6 +218,12 @@ async fn get_api_key(
|
|||
return Err("Not authenticated".to_string());
|
||||
}
|
||||
|
||||
let base_url = if api_config.base_url.is_empty() {
|
||||
"https://hackatime.hackclub.com"
|
||||
} else {
|
||||
&api_config.base_url
|
||||
};
|
||||
|
||||
let access_token = auth_state
|
||||
.access_token
|
||||
.as_ref()
|
||||
|
|
@ -224,7 +231,7 @@ async fn get_api_key(
|
|||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&format!("{}/api/v1/authenticated/api_key", api_config.base_url))
|
||||
.get(&format!("{}/api/v1/authenticated/api_keys", base_url))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await
|
||||
|
|
@ -243,9 +250,9 @@ async fn get_api_key(
|
|||
.await
|
||||
.map_err(|e| format!("Failed to parse API key response: {}", e))?;
|
||||
|
||||
let api_key = api_key_response["api_key"]
|
||||
let api_key = api_key_response["token"]
|
||||
.as_str()
|
||||
.ok_or("No API key in response")?;
|
||||
.ok_or("No token in response")?;
|
||||
|
||||
Ok(api_key.to_string())
|
||||
}
|
||||
|
|
@ -793,40 +800,6 @@ struct SessionData {
|
|||
heartbeat_count: u32,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn register_presence_connection(
|
||||
api_config: ApiConfig,
|
||||
state: State<'_, Arc<tauri::async_runtime::Mutex<AuthState>>>,
|
||||
) -> Result<(), String> {
|
||||
let auth_state = state.lock().await;
|
||||
|
||||
if !auth_state.is_authenticated {
|
||||
return Err("Not authenticated".to_string());
|
||||
}
|
||||
|
||||
let access_token = auth_state
|
||||
.access_token
|
||||
.as_ref()
|
||||
.ok_or("No access token available")?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&format!("{}/api/v1/presence/register", api_config.base_url))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to register presence connection: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(format!("Presence registration failed: {}", error_text));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_latest_heartbeat(
|
||||
|
|
@ -841,6 +814,12 @@ async fn get_latest_heartbeat(
|
|||
return Err("Not authenticated".to_string());
|
||||
}
|
||||
|
||||
let base_url = if api_config.base_url.is_empty() {
|
||||
"https://hackatime.hackclub.com"
|
||||
} else {
|
||||
&api_config.base_url
|
||||
};
|
||||
|
||||
let access_token = auth_state
|
||||
.access_token
|
||||
.as_ref()
|
||||
|
|
@ -849,8 +828,8 @@ async fn get_latest_heartbeat(
|
|||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&format!(
|
||||
"{}/api/v1/presence/latest_heartbeat",
|
||||
api_config.base_url
|
||||
"{}/api/v1/authenticated/heartbeats/latest",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
|
|
@ -993,40 +972,6 @@ async fn get_latest_heartbeat(
|
|||
Ok(heartbeat_response)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn ping_presence_connection(
|
||||
api_config: ApiConfig,
|
||||
state: State<'_, Arc<tauri::async_runtime::Mutex<AuthState>>>,
|
||||
) -> Result<(), String> {
|
||||
let auth_state = state.lock().await;
|
||||
|
||||
if !auth_state.is_authenticated {
|
||||
return Err("Not authenticated".to_string());
|
||||
}
|
||||
|
||||
let access_token = auth_state
|
||||
.access_token
|
||||
.as_ref()
|
||||
.ok_or("No access token available")?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&format!("{}/api/v1/presence/ping", api_config.base_url))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to ping presence connection: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(format!("Presence ping failed: {}", error_text));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get_config_dir() -> Result<std::path::PathBuf, String> {
|
||||
|
|
@ -1190,6 +1135,12 @@ async fn get_projects(
|
|||
return Err("Not authenticated".to_string());
|
||||
}
|
||||
|
||||
let base_url = if api_config.base_url.is_empty() {
|
||||
"https://hackatime.hackclub.com"
|
||||
} else {
|
||||
&api_config.base_url
|
||||
};
|
||||
|
||||
let access_token = auth_state
|
||||
.access_token
|
||||
.as_ref()
|
||||
|
|
@ -1199,7 +1150,7 @@ async fn get_projects(
|
|||
let response = client
|
||||
.get(&format!(
|
||||
"{}/api/v1/authenticated/projects",
|
||||
api_config.base_url
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
|
|
@ -1234,6 +1185,12 @@ async fn get_project_details(
|
|||
return Err("Not authenticated".to_string());
|
||||
}
|
||||
|
||||
let base_url = if api_config.base_url.is_empty() {
|
||||
"https://hackatime.hackclub.com"
|
||||
} else {
|
||||
&api_config.base_url
|
||||
};
|
||||
|
||||
let access_token = auth_state
|
||||
.access_token
|
||||
.as_ref()
|
||||
|
|
@ -1243,7 +1200,7 @@ async fn get_project_details(
|
|||
let response = client
|
||||
.get(&format!(
|
||||
"{}/api/v1/authenticated/projects/{}",
|
||||
api_config.base_url,
|
||||
base_url,
|
||||
urlencoding::encode(&project_name)
|
||||
))
|
||||
.bearer_auth(access_token)
|
||||
|
|
@ -1351,6 +1308,12 @@ async fn get_statistics_data(
|
|||
return Err("Not authenticated".to_string());
|
||||
}
|
||||
|
||||
let base_url = if api_config.base_url.is_empty() {
|
||||
"https://hackatime.hackclub.com"
|
||||
} else {
|
||||
&api_config.base_url
|
||||
};
|
||||
|
||||
let access_token = auth_state
|
||||
.access_token
|
||||
.as_ref()
|
||||
|
|
@ -1358,37 +1321,140 @@ async fn get_statistics_data(
|
|||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Get dashboard stats from Ruby API
|
||||
let response = client
|
||||
let end_date = chrono::Utc::now().date_naive();
|
||||
|
||||
let mut daily_hours = serde_json::Map::new();
|
||||
let mut total_seconds = 0u64;
|
||||
|
||||
for days_ago in 0..7 {
|
||||
let date = end_date - chrono::Duration::days(days_ago);
|
||||
let date_str = date.format("%Y-%m-%d").to_string();
|
||||
|
||||
let day_response = client
|
||||
.get(&format!(
|
||||
"{}/api/v1/authenticated/hours?start_date={}&end_date={}",
|
||||
base_url,
|
||||
date_str,
|
||||
date_str
|
||||
))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match day_response {
|
||||
Ok(response) if response.status().is_success() => {
|
||||
if let Ok(day_data) = response.json::<serde_json::Value>().await {
|
||||
let seconds = day_data["total_seconds"].as_u64().unwrap_or(0);
|
||||
total_seconds += seconds;
|
||||
|
||||
let day_name = match date.weekday() {
|
||||
chrono::Weekday::Mon => "Mon",
|
||||
chrono::Weekday::Tue => "Tue",
|
||||
chrono::Weekday::Wed => "Wed",
|
||||
chrono::Weekday::Thu => "Thu",
|
||||
chrono::Weekday::Fri => "Fri",
|
||||
chrono::Weekday::Sat => "Sat",
|
||||
chrono::Weekday::Sun => "Sun",
|
||||
};
|
||||
|
||||
daily_hours.insert(date_str.clone(), serde_json::json!({
|
||||
"date": date_str,
|
||||
"day_name": day_name,
|
||||
"hours": seconds as f64 / 3600.0,
|
||||
"seconds": seconds
|
||||
}));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let day_name = match date.weekday() {
|
||||
chrono::Weekday::Mon => "Mon",
|
||||
chrono::Weekday::Tue => "Tue",
|
||||
chrono::Weekday::Wed => "Wed",
|
||||
chrono::Weekday::Thu => "Thu",
|
||||
chrono::Weekday::Fri => "Fri",
|
||||
chrono::Weekday::Sat => "Sat",
|
||||
chrono::Weekday::Sun => "Sun",
|
||||
};
|
||||
|
||||
daily_hours.insert(date_str.clone(), serde_json::json!({
|
||||
"date": date_str,
|
||||
"day_name": day_name,
|
||||
"hours": 0.0,
|
||||
"seconds": 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let all_time_start = end_date - chrono::Duration::days(365);
|
||||
let all_time_response = client
|
||||
.get(&format!(
|
||||
"{}/api/v1/authenticated/dashboard_stats",
|
||||
api_config.base_url
|
||||
"{}/api/v1/authenticated/hours?start_date={}&end_date={}",
|
||||
base_url,
|
||||
all_time_start.format("%Y-%m-%d"),
|
||||
end_date.format("%Y-%m-%d")
|
||||
))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let all_time_seconds = match all_time_response {
|
||||
Ok(response) if response.status().is_success() => {
|
||||
if let Ok(data) = response.json::<serde_json::Value>().await {
|
||||
data["total_seconds"].as_u64().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
_ => 0
|
||||
};
|
||||
|
||||
let hours_data = serde_json::json!({
|
||||
"weekly_stats": {
|
||||
"time_coded_seconds": total_seconds,
|
||||
"daily_hours": daily_hours
|
||||
},
|
||||
"all_time_stats": {
|
||||
"time_coded_seconds": all_time_seconds
|
||||
}
|
||||
});
|
||||
|
||||
let streak_response = client
|
||||
.get(&format!(
|
||||
"{}/api/v1/authenticated/streak",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(access_token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch dashboard stats: {}", e))?;
|
||||
.map_err(|e| format!("Failed to fetch streak: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response
|
||||
if !streak_response.status().is_success() {
|
||||
let error_text = streak_response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(format!("Dashboard stats request failed: {}", error_text));
|
||||
return Err(format!("Streak request failed: {}", error_text));
|
||||
}
|
||||
|
||||
let dashboard_stats: serde_json::Value = response
|
||||
let streak_data: serde_json::Value = streak_response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse dashboard stats: {}", e))?;
|
||||
.map_err(|e| format!("Failed to parse streak data: {}", e))?;
|
||||
|
||||
let mut dashboard_stats = hours_data;
|
||||
if let Some(streak) = streak_data.get("current_streak") {
|
||||
dashboard_stats["current_streak"] = streak.clone();
|
||||
}
|
||||
if let Some(longest) = streak_data.get("longest_streak") {
|
||||
dashboard_stats["longest_streak"] = longest.clone();
|
||||
}
|
||||
|
||||
// Process the data in Rust for heavy computations
|
||||
let statistics = process_statistics_data(dashboard_stats).await?;
|
||||
|
||||
Ok(statistics)
|
||||
}
|
||||
|
||||
// Tray-related commands
|
||||
#[tauri::command]
|
||||
async fn show_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
|
|
@ -1620,36 +1686,43 @@ async fn generate_chart_data(
|
|||
) -> Result<Vec<ChartData>, String> {
|
||||
let mut charts = Vec::new();
|
||||
|
||||
// Daily hours chart
|
||||
let mut chart_data = Vec::new();
|
||||
let mut labels = Vec::new();
|
||||
|
||||
if let Some(daily_hours) = dashboard_stats["weekly_stats"]["daily_hours"].as_object() {
|
||||
let mut chart_data = Vec::new();
|
||||
let mut labels = Vec::new();
|
||||
|
||||
for (_date, day_data) in daily_hours {
|
||||
if let Some(hours) = day_data["hours"].as_f64() {
|
||||
labels.push(day_data["day_name"].as_str().unwrap_or("").to_string());
|
||||
chart_data.push(hours);
|
||||
}
|
||||
}
|
||||
|
||||
charts.push(ChartData {
|
||||
id: "daily_hours".to_string(),
|
||||
title: "Daily Coding Hours".to_string(),
|
||||
chart_type: "bar".to_string(),
|
||||
data: serde_json::json!({
|
||||
"labels": labels,
|
||||
"datasets": [{
|
||||
"label": "Hours",
|
||||
"data": chart_data,
|
||||
"backgroundColor": "#FB4B20",
|
||||
"borderColor": "#FB4B20",
|
||||
"borderWidth": 1
|
||||
}]
|
||||
}),
|
||||
period: "Last 7 days".to_string(),
|
||||
color_scheme: "orange".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if chart_data.is_empty() {
|
||||
let day_names = vec!["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
for day in day_names {
|
||||
labels.push(day.to_string());
|
||||
chart_data.push(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
charts.push(ChartData {
|
||||
id: "daily_hours".to_string(),
|
||||
title: "Daily Coding Hours".to_string(),
|
||||
chart_type: "bar".to_string(),
|
||||
data: serde_json::json!({
|
||||
"labels": labels,
|
||||
"datasets": [{
|
||||
"label": "Hours",
|
||||
"data": chart_data,
|
||||
"backgroundColor": "#FB4B20",
|
||||
"borderColor": "#FB4B20",
|
||||
"borderWidth": 1
|
||||
}]
|
||||
}),
|
||||
period: "Last 7 days".to_string(),
|
||||
color_scheme: "orange".to_string(),
|
||||
});
|
||||
|
||||
// Language distribution pie chart
|
||||
if let Some(top_language) = dashboard_stats["weekly_stats"]["top_language"].as_object() {
|
||||
|
|
@ -1677,24 +1750,22 @@ async fn generate_chart_data(
|
|||
});
|
||||
}
|
||||
|
||||
// Weekly trend line chart
|
||||
let mut trend_data = Vec::new();
|
||||
let mut trend_labels = Vec::new();
|
||||
|
||||
let current_week_seconds = dashboard_stats["weekly_stats"]["time_coded_seconds"]
|
||||
.as_u64()
|
||||
.unwrap_or(0);
|
||||
|
||||
// Simulate 4 weeks of data
|
||||
for week in 0..4 {
|
||||
let week_hours = if week == 3 {
|
||||
dashboard_stats["weekly_stats"]["time_coded_seconds"]
|
||||
.as_u64()
|
||||
.unwrap_or(0) as f64
|
||||
/ 3600.0
|
||||
current_week_seconds as f64 / 3600.0
|
||||
} else if current_week_seconds == 0 {
|
||||
0.0
|
||||
} else {
|
||||
// Simulate previous weeks
|
||||
(dashboard_stats["weekly_stats"]["time_coded_seconds"]
|
||||
.as_u64()
|
||||
.unwrap_or(0) as f64
|
||||
/ 3600.0)
|
||||
* (0.8 + (week as f64 * 0.1))
|
||||
(current_week_seconds as f64 / 3600.0) * (0.8 + (week as f64 * 0.1))
|
||||
};
|
||||
|
||||
trend_data.push(week_hours);
|
||||
|
|
@ -2077,8 +2148,8 @@ pub fn run() {
|
|||
handle_deep_link_callback,
|
||||
logout,
|
||||
test_auth_callback,
|
||||
get_api_key,
|
||||
authenticate_with_direct_oauth,
|
||||
get_api_key,
|
||||
setup_hackatime_macos_linux,
|
||||
setup_hackatime_windows,
|
||||
test_hackatime_heartbeat,
|
||||
|
|
@ -2086,9 +2157,7 @@ pub fn run() {
|
|||
save_auth_state,
|
||||
load_auth_state,
|
||||
clear_auth_state,
|
||||
register_presence_connection,
|
||||
get_latest_heartbeat,
|
||||
ping_presence_connection,
|
||||
get_hackatime_directories,
|
||||
cleanup_old_sessions,
|
||||
get_session_stats,
|
||||
|
|
|
|||
32
src/App.vue
32
src/App.vue
|
|
@ -56,7 +56,20 @@ const { currentTheme, toggleTheme } = useTheme();
|
|||
|
||||
// Computed property for weekly chart data
|
||||
const weeklyChartData = computed(() => {
|
||||
if (!userStats.value?.weekly_stats?.daily_hours) return [];
|
||||
if (!userStats.value?.weekly_stats?.daily_hours) {
|
||||
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
const today = new Date();
|
||||
return dayNames.map((dayName, index) => {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() - (6 - index));
|
||||
return {
|
||||
date: date.toISOString().split('T')[0],
|
||||
day_name: dayName,
|
||||
hours: 0,
|
||||
percentage: 0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const dailyHours = userStats.value.weekly_stats.daily_hours;
|
||||
const maxHours = Math.max(...Object.values(dailyHours).map((day: any) => day.hours), 1);
|
||||
|
|
@ -173,10 +186,7 @@ async function loadAuthState() {
|
|||
authState.value = savedAuthState as AuthState;
|
||||
console.log("Loaded saved authentication state:", authState.value);
|
||||
|
||||
// If authenticated, load user data, stats, and API keys
|
||||
await loadUserData();
|
||||
await loadApiKey();
|
||||
await registerPresenceConnection();
|
||||
} else {
|
||||
// No saved state or not authenticated, get current state
|
||||
console.log("No saved auth state found, getting current state");
|
||||
|
|
@ -216,6 +226,8 @@ async function loadUserData() {
|
|||
console.error("Failed to load user dashboard stats:", error);
|
||||
}
|
||||
|
||||
await loadApiKey();
|
||||
|
||||
// Load presence data and start refresh
|
||||
await loadPresenceData();
|
||||
startPresenceRefresh();
|
||||
|
|
@ -232,16 +244,6 @@ async function loadApiKey() {
|
|||
}
|
||||
}
|
||||
|
||||
async function registerPresenceConnection() {
|
||||
try {
|
||||
await invoke("register_presence_connection", { apiConfig: apiConfig.value });
|
||||
console.log("Presence connection registered successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to register presence connection:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function loadApiConfig() {
|
||||
try {
|
||||
const config = await invoke("get_api_config") as ApiConfig;
|
||||
|
|
@ -565,7 +567,7 @@ function getPageTitle(): string {
|
|||
</div>
|
||||
<div class="p-6 flex-1 overflow-y-auto">
|
||||
<Projects v-if="currentPage === 'projects'" :currentTheme="currentTheme" :toggleTheme="toggleTheme" :apiConfig="apiConfig" />
|
||||
<Settings v-if="currentPage === 'settings'" :currentTheme="currentTheme" :toggleTheme="toggleTheme" :apiKey="apiKey" :showApiKey="showApiKey" @copyApiKey="copyApiKey" />
|
||||
<Settings v-if="currentPage === 'settings'" :currentTheme="currentTheme" :toggleTheme="toggleTheme" :apiKey="apiKey" v-model:showApiKey="showApiKey" @copyApiKey="copyApiKey" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
141
src/api.ts
141
src/api.ts
|
|
@ -18,12 +18,17 @@ export class KubeTimeApi {
|
|||
async initialize() {
|
||||
try {
|
||||
const config: ApiConfig = await invoke("get_api_config");
|
||||
this.baseUrl = config.base_url;
|
||||
if (config.base_url && config.base_url.trim()) {
|
||||
this.baseUrl = config.base_url;
|
||||
}
|
||||
|
||||
const authState: AuthState = await invoke("get_auth_state");
|
||||
this.accessToken = authState.access_token;
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize API:", error);
|
||||
if (!this.baseUrl || !this.baseUrl.trim()) {
|
||||
this.baseUrl = "https://hackatime.hackclub.com";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,78 +56,92 @@ export class KubeTimeApi {
|
|||
}
|
||||
}
|
||||
|
||||
async getHours(startDate?: string, endDate?: string) {
|
||||
if (!this.accessToken) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
|
||||
const url = `${this.baseUrl}/api/v1/authenticated/hours${params.toString() ? `?${params.toString()}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch hours:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getWeeklyHours() {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(endDate.getDate() - 7);
|
||||
|
||||
const formatDate = (date: Date) => date.toISOString().split('T')[0];
|
||||
|
||||
return this.getHours(formatDate(startDate), formatDate(endDate));
|
||||
}
|
||||
|
||||
async getStreak() {
|
||||
if (!this.accessToken) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/authenticated/streak`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch streak:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
if (!this.accessToken) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the current user's dashboard stats endpoint
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/authenticated/dashboard_stats`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const [hoursData, streakData] = await Promise.all([
|
||||
this.getWeeklyHours(),
|
||||
this.getStreak()
|
||||
]);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
return {
|
||||
...hoursData,
|
||||
current_streak: streakData.current_streak,
|
||||
longest_streak: streakData.longest_streak
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch stats:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMyHeartbeats() {
|
||||
if (!this.accessToken) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/my/heartbeats`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch heartbeats:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserDashboardStats(username: string) {
|
||||
if (!this.accessToken) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/users/${username}/dashboard_stats`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user dashboard stats:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentPresence() {
|
||||
if (!this.accessToken) {
|
||||
|
|
@ -136,7 +155,7 @@ export class KubeTimeApi {
|
|||
return this.latestPresenceCache.data;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/presence/latest_heartbeat`, {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/authenticated/heartbeats/latest`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue