Explorar el Código

feat(ui): add per-link analytics to dashboard

Fszontagh hace 2 días
padre
commit
7ca10a6c2e
Se han modificado 1 ficheros con 80 adiciones y 3 borrados
  1. 80 3
      src/components/AnalyticsDashboard.tsx

+ 80 - 3
src/components/AnalyticsDashboard.tsx

@@ -1,6 +1,7 @@
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import { useBaaS } from '@picobaas/client/react';
 import { useBaaS } from '@picobaas/client/react';
-import type { ImageAnalytics, ViewLog } from '../types';
+import type { ImageAnalytics, ViewLog, ShareLink } from '../types';
+import { listShareLinks } from '../api/shareLinks';
 import LoadingSpinner from './LoadingSpinner';
 import LoadingSpinner from './LoadingSpinner';
 
 
 interface ImageInfo {
 interface ImageInfo {
@@ -18,6 +19,7 @@ export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDash
   const { client } = useBaaS();
   const { client } = useBaaS();
   const [analytics, setAnalytics] = useState<ImageAnalytics | null>(null);
   const [analytics, setAnalytics] = useState<ImageAnalytics | null>(null);
   const [imageInfo, setImageInfo] = useState<ImageInfo | null>(null);
   const [imageInfo, setImageInfo] = useState<ImageInfo | null>(null);
+  const [shareLinks, setShareLinks] = useState<ShareLink[]>([]);
   const [isLoading, setIsLoading] = useState(true);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
 
 
@@ -35,10 +37,11 @@ export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDash
         'Authorization': token ? `Bearer ${token}` : '',
         'Authorization': token ? `Bearer ${token}` : '',
       };
       };
 
 
-      // Fetch image info and analytics in parallel
-      const [imageResponse, analyticsResponse] = await Promise.all([
+      // Fetch image info, analytics, and share links in parallel
+      const [imageResponse, analyticsResponse, links] = await Promise.all([
         fetch(`/api/images/${shortCode}`, { headers }),
         fetch(`/api/images/${shortCode}`, { headers }),
         fetch(`/api/images/${shortCode}/analytics`, { headers }),
         fetch(`/api/images/${shortCode}/analytics`, { headers }),
+        listShareLinks(shortCode, token || undefined),
       ]);
       ]);
 
 
       // Handle image info
       // Handle image info
@@ -51,6 +54,9 @@ export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDash
         });
         });
       }
       }
 
 
+      // Handle share links
+      setShareLinks(links);
+
       // Handle analytics
       // Handle analytics
       if (!analyticsResponse.ok) {
       if (!analyticsResponse.ok) {
         const errorData = await analyticsResponse.json().catch(() => ({}));
         const errorData = await analyticsResponse.json().catch(() => ({}));
@@ -239,6 +245,77 @@ export default function AnalyticsDashboard({ shortCode, onClose }: AnalyticsDash
         </div>
         </div>
       )}
       )}
 
 
+      {/* Share Links Section */}
+      {shareLinks.length > 0 && (
+        <div>
+          <h3
+            className="text-sm font-medium mb-3"
+            style={{ color: 'var(--text-secondary)' }}
+          >
+            Share Links ({shareLinks.length})
+          </h3>
+          <div
+            className="rounded-lg divide-y"
+            style={{
+              backgroundColor: 'var(--bg-tertiary)',
+              borderColor: 'var(--border)',
+            }}
+          >
+            {shareLinks
+              .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
+              .map((link) => (
+                <div
+                  key={link.id}
+                  className="px-4 py-3"
+                  style={{ borderColor: 'var(--border)' }}
+                >
+                  <div className="flex justify-between items-start mb-2">
+                    <div className="flex items-center gap-2">
+                      <code
+                        className="text-sm font-mono px-2 py-0.5 rounded"
+                        style={{ backgroundColor: 'var(--bg-secondary)' }}
+                      >
+                        {link.linkCode}
+                      </code>
+                      {link.isRevoked ? (
+                        <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
+                          Revoked
+                        </span>
+                      ) : !link.isValid ? (
+                        <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400">
+                          Expired
+                        </span>
+                      ) : (
+                        <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
+                          Active
+                        </span>
+                      )}
+                    </div>
+                    <div
+                      className="text-sm font-medium"
+                      style={{ color: 'var(--text-primary)' }}
+                    >
+                      {link.viewCount} {link.viewCount === 1 ? 'view' : 'views'}
+                    </div>
+                  </div>
+                  <div
+                    className="text-xs flex flex-wrap gap-x-4 gap-y-1"
+                    style={{ color: 'var(--text-muted)' }}
+                  >
+                    <span>Created: {formatDate(link.createdAt)}</span>
+                    <span>
+                      Expires: {formatDate(link.expiresAt)}
+                    </span>
+                    {link.lastViewedAt && (
+                      <span>Last viewed: {formatDate(link.lastViewedAt)}</span>
+                    )}
+                  </div>
+                </div>
+              ))}
+          </div>
+        </div>
+      )}
+
       {/* Recent Views Table */}
       {/* Recent Views Table */}
       <div>
       <div>
         <h3
         <h3