import { useState, useEffect } from 'react'; import { useBaaS } from '@picobaas/client/react'; import type { ImageAnalytics, ViewLog, ShareLink } from '../types'; import { listShareLinks } from '../api/shareLinks'; import { getSessionToken } from '../utils/session'; import LoadingSpinner from './LoadingSpinner'; interface ImageInfo { imageUrl: string; shortUrl: string; path: string; } interface AnalyticsDashboardProps { shortCode: string; onClose?: () => void; } export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDashboardProps) { const { client } = useBaaS(); const [analytics, setAnalytics] = useState(null); const [imageInfo, setImageInfo] = useState(null); const [shareLinks, setShareLinks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetchData(); }, [shortCode]); const fetchData = async () => { setIsLoading(true); setError(null); try { const token = client.accessToken; const headers = { 'Authorization': token ? `Bearer ${token}` : '', }; // Fetch image info, analytics, and share links in parallel const sessionToken = getSessionToken(); const [imageResponse, analyticsResponse, links] = await Promise.all([ fetch(`/api/images/${shortCode}`, { headers }), fetch(`/api/images/${shortCode}/analytics`, { headers }), listShareLinks(shortCode, token || undefined, sessionToken), ]); // 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, }); } // Handle share links setShareLinks(links); // Handle analytics if (!analyticsResponse.ok) { const errorData = await analyticsResponse.json().catch(() => ({})); throw new Error(errorData.error || 'Failed to load analytics'); } const data = await analyticsResponse.json(); setAnalytics({ totalViews: data.total_views, uniqueVisitors: data.unique_visitors, proxyViewsCount: data.proxy_views_count, viewsByReferrer: data.views_by_referrer || {}, recentViews: (data.recent_views || []).map((v: Record) => ({ id: v.id as string, viewerIp: v.viewer_ip as string, userAgent: v.user_agent as string, referrer: v.referrer as string | null, country: v.country as string | null, isProxyDetected: v.is_proxy_detected as boolean, viewedAt: new Date(v.viewed_at as string), })), }); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load analytics'); } finally { setIsLoading(false); } }; if (isLoading) { return (
); } if (error) { return (
{error}
); } if (!analytics) { return null; } return (
{/* Header with image preview */}
{imageInfo && ( Image preview )}

Image Analytics

{imageInfo && (

{imageInfo.path.split('/').pop()}

)}
{onClose && ( )}
{/* Stats Grid */}
Total Views
{analytics.totalViews}
Unique Visitors
{analytics.uniqueVisitors}
Proxy/VPN Views
{analytics.proxyViewsCount}
{/* Referrers */} {Object.keys(analytics.viewsByReferrer).length > 0 && (

Top Referrers

{Object.entries(analytics.viewsByReferrer) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([referrer, count]) => (
{referrer} {count}
))}
)} {/* Share Links Section */} {shareLinks.length > 0 && (

Share Links ({shareLinks.length})

{shareLinks .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) .map((link) => (
{link.linkCode} {link.isRevoked ? ( Revoked ) : !link.isValid ? ( Expired ) : ( Active )}
{link.viewCount} {link.viewCount === 1 ? 'view' : 'views'}
Created: {formatDate(link.createdAt)} Expires: {formatDate(link.expiresAt)} {link.lastViewedAt && ( Last viewed: {formatDate(link.lastViewedAt)} )}
))}
)} {/* Recent Views Table */}

Recent Views

{analytics.recentViews.length === 0 ? (

No views recorded yet

) : (
{analytics.recentViews.slice(0, 20).map((view: ViewLog) => ( ))}
Time IP Address Referrer Proxy
{formatDate(view.viewedAt)} {view.viewerIp} {view.referrer || '-'} {view.isProxyDetected ? ( Detected ) : ( - )}
)}
{/* Refresh Button */}
); } function formatDate(date: Date): string { const now = new Date(); const diff = now.getTime() - date.getTime(); if (diff < 60000) { return 'Just now'; } if (diff < 3600000) { const mins = Math.floor(diff / 60000); return `${mins}m ago`; } if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours}h ago`; } return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }