Просмотр исходного кода

fix: Fix share bucket selection and add image preview to analytics

- ShareModal: Use correct bucket based on image.isPublic flag
  (was always using PUBLIC_BUCKET for signed URLs)
- AnalyticsDashboard: Add image thumbnail and filename to header
  so users know which image's stats they're viewing
Fszontagh 3 дней назад
Родитель
Сommit
1e6886472e
2 измененных файлов с 72 добавлено и 27 удалено
  1. 68 25
      src/components/AnalyticsDashboard.tsx
  2. 4 2
      src/components/ShareModal.tsx

+ 68 - 25
src/components/AnalyticsDashboard.tsx

@@ -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>

+ 4 - 2
src/components/ShareModal.tsx

@@ -1,7 +1,7 @@
 import { useState, useEffect } from 'react';
 import { useBaaS } from '@picobaas/client/react';
 import type { ImageFile } from '../types';
-import { EXPIRY_OPTIONS, PUBLIC_BUCKET } from '../config';
+import { EXPIRY_OPTIONS, PUBLIC_BUCKET, USER_BUCKET } from '../config';
 import LoadingSpinner from './LoadingSpinner';
 
 interface ShareModalProps {
@@ -29,7 +29,9 @@ export default function ShareModal({ isOpen, onClose, image }: ShareModalProps)
     setError(null);
 
     try {
-      const bucket = client.storage.from(PUBLIC_BUCKET);
+      // Use the correct bucket based on where the image is stored
+      const bucketName = image.isPublic ? PUBLIC_BUCKET : USER_BUCKET;
+      const bucket = client.storage.from(bucketName);
 
       if (image.isPublic) {
         // Prefer SEO-friendly short URL if available