diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index b5b39f7..b338de0 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -63,15 +63,26 @@ export const deleteLink = async (id: number) => { }; export const getLinkClickStats = async (id: number) => { - const response = await api.get(`/links/${id}/clicks`); - return response.data; + try { + const response = await api.get(`/links/${id}/clicks`); + return response.data; + } catch (error) { + console.error('Error fetching click stats:', error); + throw error; + } }; export const getLinkSourceStats = async (id: number) => { - const response = await api.get(`/links/${id}/sources`); - return response.data; + try { + const response = await api.get(`/links/${id}/sources`); + return response.data; + } catch (error) { + console.error('Error fetching source stats:', error); + throw error; + } }; + export const checkFirstUser = async () => { const response = await api.get<{ isFirstUser: boolean }>('/auth/check-first-user'); return response.data.isFirstUser; diff --git a/frontend/src/components/StatisticsModal.tsx b/frontend/src/components/StatisticsModal.tsx index eb495f7..1df2bc4 100644 --- a/frontend/src/components/StatisticsModal.tsx +++ b/frontend/src/components/StatisticsModal.tsx @@ -9,6 +9,7 @@ import { ResponsiveContainer, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { toast } from "@/hooks/use-toast" import { useState, useEffect } from "react"; import { getLinkClickStats, getLinkSourceStats } from '../api/client'; @@ -36,13 +37,18 @@ export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProp ]); setClicksOverTime(clicksData); setSourcesData(sourcesData); - } catch (error) { + } catch (error: any) { console.error("Failed to fetch statistics:", error); + toast({ + variant: "destructive", + title: "Error", + description: error.response?.data || "Failed to load statistics", + }); } finally { setLoading(false); } }; - + fetchData(); } }, [isOpen, linkId]); diff --git a/src/handlers.rs b/src/handlers.rs index fdecc7d..dca0bea 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -537,31 +537,21 @@ pub async fn get_link_clicks( ) -> Result { let link_id = path.into_inner(); - // Verify the link belongs to the user + // First verify the link belongs to the user let link = match &state.db { DatabasePool::Postgres(pool) => { - let mut tx = pool.begin().await?; - let link = sqlx::query_as::( - "SELECT id FROM links WHERE id = $1 AND user_id = $2", - ) - .bind(link_id) - .bind(user.user_id) - .fetch_optional(&mut *tx) - .await?; - tx.commit().await?; - link + sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = $1 AND user_id = $2") + .bind(link_id) + .bind(user.user_id) + .fetch_optional(pool) + .await? } DatabasePool::Sqlite(pool) => { - let mut tx = pool.begin().await?; - let link = sqlx::query_as::( - "SELECT id FROM links WHERE id = ? AND user_id = ?", - ) - .bind(link_id) - .bind(user.user_id) - .fetch_optional(&mut *tx) - .await?; - tx.commit().await?; - link + sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = ? AND user_id = ?") + .bind(link_id) + .bind(user.user_id) + .fetch_optional(pool) + .await? } }; @@ -571,11 +561,11 @@ pub async fn get_link_clicks( let clicks = match &state.db { DatabasePool::Postgres(pool) => { - sqlx::query_as::( + sqlx::query_as::<_, ClickStats>( r#" SELECT - DATE(created_at)::date as "date!", - COUNT(*)::bigint as "clicks!" + DATE(created_at) as date, + COUNT(*) as clicks FROM clicks WHERE link_id = $1 GROUP BY DATE(created_at) @@ -588,11 +578,11 @@ pub async fn get_link_clicks( .await? } DatabasePool::Sqlite(pool) => { - sqlx::query_as::( + sqlx::query_as::<_, ClickStats>( r#" SELECT - DATE(created_at) as "date!", - COUNT(*) as "clicks!" + DATE(created_at) as date, + COUNT(*) as clicks FROM clicks WHERE link_id = ? GROUP BY DATE(created_at) @@ -650,11 +640,11 @@ pub async fn get_link_sources( let sources = match &state.db { DatabasePool::Postgres(pool) => { - sqlx::query_as::( + sqlx::query_as::<_, SourceStats>( r#" SELECT - query_source as "source!", - COUNT(*)::bigint as "count!" + query_source as source, // Remove the ! mark + COUNT(*)::bigint as count // Remove the ! mark FROM clicks WHERE link_id = $1 AND query_source IS NOT NULL @@ -669,11 +659,11 @@ pub async fn get_link_sources( .await? } DatabasePool::Sqlite(pool) => { - sqlx::query_as::( + sqlx::query_as::<_, SourceStats>( r#" SELECT - query_source as "source!", - COUNT(*) as "count!" + query_source as source, + COUNT(*) as count FROM clicks WHERE link_id = ? AND query_source IS NOT NULL diff --git a/src/models.rs b/src/models.rs index 1705dff..9e97dcf 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use chrono::NaiveDate; use futures::future::BoxFuture; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgRow; @@ -145,7 +144,7 @@ pub struct User { #[derive(sqlx::FromRow, Serialize)] pub struct ClickStats { - pub date: NaiveDate, + pub date: String, pub clicks: i64, }