|
|
@@ -3,6 +3,12 @@ import { useBaaS } from '@picobaas/client/react';
|
|
|
import type { ImageAnalytics, ViewLog } from '../types';
|
|
|
import LoadingSpinner from './LoadingSpinner';
|
|
|
|
|
|
+interface ImageInfo {
|
|
|
+ imageUrl: string;
|
|
|
+ shortUrl: string;
|
|
|
+ path: string;
|
|
|
+}
|
|
|
+
|
|
|
interface AnalyticsDashboardProps {
|
|
|
shortCode: string;
|
|
|
onClose?: () => void;
|
|
|
@@ -11,33 +17,47 @@ interface AnalyticsDashboardProps {
|
|
|
export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDashboardProps) {
|
|
|
const { client } = useBaaS();
|
|
|
const [analytics, setAnalytics] = useState<ImageAnalytics | null>(null);
|
|
|
+ const [imageInfo, setImageInfo] = useState<ImageInfo | null>(null);
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- fetchAnalytics();
|
|
|
+ fetchData();
|
|
|
}, [shortCode]);
|
|
|
|
|
|
- const fetchAnalytics = async () => {
|
|
|
+ const fetchData = async () => {
|
|
|
setIsLoading(true);
|
|
|
setError(null);
|
|
|
|
|
|
try {
|
|
|
- // Get auth token from client
|
|
|
const token = client.accessToken;
|
|
|
+ const headers = {
|
|
|
+ 'Authorization': token ? `Bearer ${token}` : '',
|
|
|
+ };
|
|
|
|
|
|
- const response = await fetch(`/api/images/${shortCode}/analytics`, {
|
|
|
- headers: {
|
|
|
- 'Authorization': token ? `Bearer ${token}` : '',
|
|
|
- },
|
|
|
- });
|
|
|
+ // Fetch image info and analytics in parallel
|
|
|
+ const [imageResponse, analyticsResponse] = await Promise.all([
|
|
|
+ fetch(`/api/images/${shortCode}`, { headers }),
|
|
|
+ fetch(`/api/images/${shortCode}/analytics`, { headers }),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // Handle image info
|
|
|
+ if (imageResponse.ok) {
|
|
|
+ const imgData = await imageResponse.json();
|
|
|
+ setImageInfo({
|
|
|
+ imageUrl: imgData.image_url,
|
|
|
+ shortUrl: imgData.short_url || `/i/${shortCode}`,
|
|
|
+ path: imgData.path,
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- if (!response.ok) {
|
|
|
- const errorData = await response.json().catch(() => ({}));
|
|
|
+ // Handle analytics
|
|
|
+ if (!analyticsResponse.ok) {
|
|
|
+ const errorData = await analyticsResponse.json().catch(() => ({}));
|
|
|
throw new Error(errorData.error || 'Failed to load analytics');
|
|
|
}
|
|
|
|
|
|
- const data = await response.json();
|
|
|
+ const data = await analyticsResponse.json();
|
|
|
setAnalytics({
|
|
|
totalViews: data.total_views,
|
|
|
uniqueVisitors: data.unique_visitors,
|
|
|
@@ -72,7 +92,7 @@ export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDash
|
|
|
return (
|
|
|
<div className="p-6 text-center">
|
|
|
<div className="text-red-600 dark:text-red-400 mb-4">{error}</div>
|
|
|
- <button onClick={fetchAnalytics} className="btn btn-secondary">
|
|
|
+ <button onClick={fetchData} className="btn btn-secondary">
|
|
|
Try Again
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -85,26 +105,49 @@ export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDash
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-6">
|
|
|
- {/* Header */}
|
|
|
- {onClose && (
|
|
|
- <div className="flex justify-between items-center">
|
|
|
- <h2
|
|
|
- className="text-xl font-semibold"
|
|
|
- style={{ color: 'var(--text-primary)' }}
|
|
|
- >
|
|
|
- Image Analytics
|
|
|
- </h2>
|
|
|
+ {/* Header with image preview */}
|
|
|
+ <div className="flex justify-between items-start gap-4">
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
+ {imageInfo && (
|
|
|
+ <a
|
|
|
+ href={imageInfo.shortUrl}
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="flex-shrink-0"
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ src={imageInfo.imageUrl}
|
|
|
+ alt="Image preview"
|
|
|
+ className="w-16 h-16 object-cover rounded-lg hover:opacity-80 transition-opacity"
|
|
|
+ />
|
|
|
+ </a>
|
|
|
+ )}
|
|
|
+ <div>
|
|
|
+ <h2
|
|
|
+ className="text-xl font-semibold"
|
|
|
+ style={{ color: 'var(--text-primary)' }}
|
|
|
+ >
|
|
|
+ Image Analytics
|
|
|
+ </h2>
|
|
|
+ {imageInfo && (
|
|
|
+ <p className="text-sm" style={{ color: 'var(--text-muted)' }}>
|
|
|
+ {imageInfo.path.split('/').pop()}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {onClose && (
|
|
|
<button
|
|
|
onClick={onClose}
|
|
|
- className="transition-colors"
|
|
|
+ className="transition-colors flex-shrink-0"
|
|
|
style={{ color: 'var(--text-muted)' }}
|
|
|
>
|
|
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
</svg>
|
|
|
</button>
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
|
|
|
{/* Stats Grid */}
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
|
@@ -289,7 +332,7 @@ export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDash
|
|
|
|
|
|
{/* Refresh Button */}
|
|
|
<div className="flex justify-end">
|
|
|
- <button onClick={fetchAnalytics} className="btn btn-secondary text-sm">
|
|
|
+ <button onClick={fetchData} className="btn btn-secondary text-sm">
|
|
|
Refresh
|
|
|
</button>
|
|
|
</div>
|