From f89c99901b93ae3bb895f0977a533440144fe68c Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:14:04 +0330 Subject: [PATCH] Import source: cshare-app/src/components/FileDetailSlideOver Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- .../src/components/FileDetailSlideOver.jsx | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 cshare-app/src/components/FileDetailSlideOver.jsx diff --git a/cshare-app/src/components/FileDetailSlideOver.jsx b/cshare-app/src/components/FileDetailSlideOver.jsx new file mode 100644 index 0000000..c04735a --- /dev/null +++ b/cshare-app/src/components/FileDetailSlideOver.jsx @@ -0,0 +1,418 @@ +import { useState, useEffect, useRef, useMemo } from 'react'; +import { CopyIcon, CheckIcon, TrashIcon, EyeIcon, ClockIcon, DownloadIcon, XIcon } from './Icons'; +import { formatFileSize, formatDate, formatDateRelative, getFileIcon, getFileIconColor } from '../data/mockData'; +import { useToast } from './ToastContext'; +import { getFileAnalytics } from '../lib/api'; + +const MOCK_CHART_DATA = [ + { day: 'Mon', views: 12, downloads: 3 }, + { day: 'Tue', views: 18, downloads: 5 }, + { day: 'Wed', views: 8, downloads: 2 }, + { day: 'Thu', views: 22, downloads: 7 }, + { day: 'Fri', views: 15, downloads: 4 }, + { day: 'Sat', views: 6, downloads: 1 }, + { day: 'Sun', views: 10, downloads: 3 }, +]; + +function MiniChart({ data }) { + const [hoveredIdx, setHoveredIdx] = useState(null); + const width = 360; + const height = 100; + const padding = 15; + + if (!data || data.length === 0) return null; + + const maxVal = Math.max(...data.map(d => Math.max(d.views, d.downloads)), 1); + + const chartWidth = width - padding * 2; + const chartHeight = height - padding * 2; + + // Coordinates for points + const points = data.map((d, i) => { + const x = padding + (i / (data.length - 1)) * chartWidth; + const yViews = padding + chartHeight - (d.views / maxVal) * chartHeight; + const yDownloads = padding + chartHeight - (d.downloads / maxVal) * chartHeight; + return { x, yViews, yDownloads, ...d }; + }); + + // Construct smooth lines or simple linear svg paths + const viewsLine = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.yViews}`).join(' '); + const downloadsLine = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.yDownloads}`).join(' '); + + // Construct area paths (fill under the line) + const viewsArea = points.length > 0 + ? `${viewsLine} L ${points[points.length - 1].x} ${height - padding} L ${points[0].x} ${height - padding} Z` + : ''; + const downloadsArea = points.length > 0 + ? `${downloadsLine} L ${points[points.length - 1].x} ${height - padding} L ${points[0].x} ${height - padding} Z` + : ''; + + return ( +
+
+ + + + + + + + + + + + + {/* Grid lines */} + + + + + {/* Area under curves */} + {viewsArea && } + {downloadsArea && } + + {/* Lines */} + {viewsLine && } + {downloadsLine && } + + {/* Interactive vertical hover guide */} + {hoveredIdx !== null && ( + + )} + + {/* Points & Interactive Hover Area */} + {points.map((p, i) => ( + setHoveredIdx(i)} onMouseLeave={() => setHoveredIdx(null)}> + {/* Highlight circles */} + {hoveredIdx === i && ( + <> + + + + )} + {/* Invisible touch/hover target */} + + + ))} + + + {/* Hover Tooltip */} + {hoveredIdx !== null && ( +
+ {points[hoveredIdx].day} + Views: {points[hoveredIdx].views} + Downloads: {points[hoveredIdx].downloads} +
+ )} +
+ + {/* Legend & X Axis */} +
+ {data.map((d, i) => ( + + {d.day} + + ))} +
+
+ +
Views + + +
Downloads + +
+
+ ); +} + +export default function FileDetailSlideOver({ file, onClose, onDelete }) { + const panelRef = useRef(null); + const [copied, setCopied] = useState(false); + const addToast = useToast(); + const [analytics, setAnalytics] = useState(null); + const [analyticsLoading, setAnalyticsLoading] = useState(true); + + useEffect(() => { + if (!file?.id) return; + setAnalyticsLoading(true); + getFileAnalytics(file.id) + .then((data) => { + setAnalytics(data); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setAnalyticsLoading(false); + }); + }, [file?.id]); + + const daysOfWeek = useMemo(() => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], []); + const chartData = useMemo(() => { + if (!analytics?.dailyBreakdown) { + return MOCK_CHART_DATA; + } + return analytics.dailyBreakdown.slice(-7).map((item) => { + const d = new Date(item.date + 'T00:00:00'); + return { + day: daysOfWeek[d.getDay()], + views: item.views, + downloads: item.downloads, + }; + }); + }, [analytics, daysOfWeek]); + + useEffect(() => { + const handleKey = (e) => { if (e.key === 'Escape') onClose(); }; + document.addEventListener('keydown', handleKey); + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', handleKey); + document.body.style.overflow = ''; + }; + }, [onClose]); + + if (!file) return null; + + const copyLink = () => { + navigator.clipboard?.writeText(file.link); + setCopied(true); + addToast('Link copied to clipboard', 'success'); + setTimeout(() => setCopied(false), 2000); + }; + + const daysUntilExpiry = Math.ceil((new Date(file.expiresAt) - new Date()) / (1000 * 60 * 60 * 24)); + const isExpiringSoon = daysUntilExpiry <= 3 && daysUntilExpiry > 0; + + return ( +
+ {/* Backdrop */} +
+ + {/* Panel */} +
e.stopPropagation()} + > + {/* Top accent */} +
+
+
+

+ {file.name} +

+ +
+
+ + {/* Content */} +
+ {/* File type badge */} +
+
+ {getFileIcon(file.type)} +
+
+
+ {file.type.split('/')[1]?.toUpperCase() || 'FILE'} File +
+
+ {formatFileSize(file.size)} +
+
+
+ + {/* Expiry badge */} +
+ +
+
Expires
+
+ {formatDateRelative(file.expiresAt)} — {formatDate(file.expiresAt)} +
+
+
+ + {/* Stats row */} +
+
+
+ + Views +
+
{file.views}
+
+
+
+ + Downloads +
+
{file.downloads}
+
+
+ + {/* Mini chart */} +
+
+ Last 7 Days +
+
+ +
+
+ + {/* Share link */} +
+
+ Share Link +
+
+
+ {file.link} +
+ +
+
+ + {/* Details grid */} +
+
+
Uploaded
+
{formatDate(file.uploadedAt)}
+
+
+
By
+
{file.uploader}
+
+
+
Type
+
{file.type.split('/')[1]?.toUpperCase() || 'FILE'}
+
+
+
Size
+
{formatFileSize(file.size)}
+
+
+
+ + {/* Footer actions */} +
+ + +
+
+
+ ); +}