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