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.
This commit is contained in:
@@ -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 (
|
||||||
|
<div style={{ position: 'relative', width: '100%' }}>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} style={{ overflow: 'visible' }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="viewsGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="var(--accent)" stopOpacity="0.25" />
|
||||||
|
<stop offset="100%" stopColor="var(--accent)" stopOpacity="0.0" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="downloadsGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="var(--success)" stopOpacity="0.25" />
|
||||||
|
<stop offset="100%" stopColor="var(--success)" stopOpacity="0.0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Grid lines */}
|
||||||
|
<line x1={padding} y1={padding} x2={width - padding} y2={padding} stroke="var(--border-subtle)" strokeDasharray="3,3" />
|
||||||
|
<line x1={padding} y1={padding + chartHeight / 2} x2={width - padding} y2={padding + chartHeight / 2} stroke="var(--border-subtle)" strokeDasharray="3,3" />
|
||||||
|
<line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} stroke="var(--border)" />
|
||||||
|
|
||||||
|
{/* Area under curves */}
|
||||||
|
{viewsArea && <path d={viewsArea} fill="url(#viewsGrad)" />}
|
||||||
|
{downloadsArea && <path d={downloadsArea} fill="url(#downloadsGrad)" />}
|
||||||
|
|
||||||
|
{/* Lines */}
|
||||||
|
{viewsLine && <path d={viewsLine} fill="none" stroke="var(--accent)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />}
|
||||||
|
{downloadsLine && <path d={downloadsLine} fill="none" stroke="var(--success)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />}
|
||||||
|
|
||||||
|
{/* Interactive vertical hover guide */}
|
||||||
|
{hoveredIdx !== null && (
|
||||||
|
<line
|
||||||
|
x1={points[hoveredIdx].x}
|
||||||
|
y1={padding}
|
||||||
|
x2={points[hoveredIdx].x}
|
||||||
|
y2={height - padding}
|
||||||
|
stroke="var(--accent-border)"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="2,2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Points & Interactive Hover Area */}
|
||||||
|
{points.map((p, i) => (
|
||||||
|
<g key={i} onMouseEnter={() => setHoveredIdx(i)} onMouseLeave={() => setHoveredIdx(null)}>
|
||||||
|
{/* Highlight circles */}
|
||||||
|
{hoveredIdx === i && (
|
||||||
|
<>
|
||||||
|
<circle cx={p.x} cy={p.yViews} r="5" fill="var(--bg-elevated)" stroke="var(--accent)" strokeWidth="2" />
|
||||||
|
<circle cx={p.x} cy={p.yDownloads} r="5" fill="var(--bg-elevated)" stroke="var(--success)" strokeWidth="2" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Invisible touch/hover target */}
|
||||||
|
<rect
|
||||||
|
x={p.x - 15}
|
||||||
|
y={0}
|
||||||
|
width={30}
|
||||||
|
height={height}
|
||||||
|
fill="transparent"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Hover Tooltip */}
|
||||||
|
{hoveredIdx !== null && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${(points[hoveredIdx].x / width) * 100}%`,
|
||||||
|
top: '-50px',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
background: 'var(--bg-surface)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
padding: '6px 10px',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 10,
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '2px',
|
||||||
|
animation: 'fadeIn 0.15s ease',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: '0.625rem', color: 'var(--text-muted)', fontWeight: 600 }}>{points[hoveredIdx].day}</span>
|
||||||
|
<span style={{ fontSize: '0.6875rem', color: 'var(--accent)', fontWeight: 500 }}>Views: <strong>{points[hoveredIdx].views}</strong></span>
|
||||||
|
<span style={{ fontSize: '0.6875rem', color: 'var(--success)', fontWeight: 500 }}>Downloads: <strong>{points[hoveredIdx].downloads}</strong></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend & X Axis */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: `0 ${padding}px`, marginTop: '8px' }}>
|
||||||
|
{data.map((d, i) => (
|
||||||
|
<span key={i} style={{ fontSize: '0.625rem', color: 'var(--text-muted)', fontFamily: 'var(--font-heading)' }}>
|
||||||
|
{d.day}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '16px', marginTop: '12px', justifyContent: 'center' }}>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.6875rem', color: 'var(--text-muted)' }}>
|
||||||
|
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: 'var(--accent)' }} /> Views
|
||||||
|
</span>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.6875rem', color: 'var(--text-muted)' }}>
|
||||||
|
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: 'var(--success)' }} /> Downloads
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{ position: 'fixed', inset: 0, zIndex: 200, display: 'flex', justifyContent: 'flex-end' }}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', inset: 0,
|
||||||
|
background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)',
|
||||||
|
animation: 'fadeIn 0.2s ease',
|
||||||
|
}} />
|
||||||
|
|
||||||
|
{/* Panel */}
|
||||||
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
className="card file-detail-panel"
|
||||||
|
style={{
|
||||||
|
position: 'relative', zIndex: 1,
|
||||||
|
width: '100%', maxWidth: '440px',
|
||||||
|
borderRadius: 0,
|
||||||
|
borderLeft: '1px solid var(--border)',
|
||||||
|
borderRight: 'none', borderTop: 'none', borderBottom: 'none',
|
||||||
|
background: 'var(--bg-elevated)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
animation: 'slideInRight 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Top accent */}
|
||||||
|
<div style={{
|
||||||
|
position: 'sticky', top: 0, zIndex: 2,
|
||||||
|
background: 'var(--bg-elevated)',
|
||||||
|
borderBottom: '1px solid var(--border-subtle)',
|
||||||
|
}}>
|
||||||
|
<div style={{ height: '3px', background: 'linear-gradient(90deg, var(--accent), var(--accent-hover))' }} />
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px' }}>
|
||||||
|
<h3 className="heading-sm" style={{
|
||||||
|
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||||
|
maxWidth: 'calc(100% - 40px)',
|
||||||
|
}}>
|
||||||
|
{file.name}
|
||||||
|
</h3>
|
||||||
|
<button className="btn btn-ghost btn-icon btn-sm" onClick={onClose}>
|
||||||
|
<XIcon size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ padding: '24px', display: 'flex', flexDirection: 'column', gap: '24px', flex: 1 }}>
|
||||||
|
{/* File type badge */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '48px', height: '48px', borderRadius: 'var(--radius-md)',
|
||||||
|
background: `${getFileIconColor(file.type)}14`,
|
||||||
|
border: `1px solid ${getFileIconColor(file.type)}30`,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '0.75rem',
|
||||||
|
color: getFileIconColor(file.type),
|
||||||
|
}}>
|
||||||
|
{getFileIcon(file.type)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 600, fontSize: '0.9375rem' }}>
|
||||||
|
{file.type.split('/')[1]?.toUpperCase() || 'FILE'} File
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.8125rem', color: 'var(--text-muted)' }}>
|
||||||
|
{formatFileSize(file.size)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expiry badge */}
|
||||||
|
<div style={{
|
||||||
|
padding: '12px 16px', borderRadius: 'var(--radius-md)',
|
||||||
|
background: isExpiringSoon ? 'var(--danger-muted)' : 'var(--accent-muted)',
|
||||||
|
border: `1px solid ${isExpiringSoon ? 'rgba(232,72,72,0.25)' : 'var(--accent-border)'}`,
|
||||||
|
display: 'flex', alignItems: 'center', gap: '10px',
|
||||||
|
}}>
|
||||||
|
<ClockIcon size={16} style={{ color: isExpiringSoon ? 'var(--danger)' : 'var(--accent)' }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '2px' }}>Expires</div>
|
||||||
|
<div style={{
|
||||||
|
fontFamily: 'var(--font-heading)', fontWeight: 600, fontSize: '0.875rem',
|
||||||
|
color: isExpiringSoon ? 'var(--danger)' : 'var(--accent)',
|
||||||
|
}}>
|
||||||
|
{formatDateRelative(file.expiresAt)} — {formatDate(file.expiresAt)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats row */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||||
|
<div className="card" style={{ padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', marginBottom: '8px' }}>
|
||||||
|
<EyeIcon size={14} style={{ color: 'var(--accent)' }} />
|
||||||
|
<span style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Views</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '1.5rem', letterSpacing: '-0.03em' }}>{file.views}</div>
|
||||||
|
</div>
|
||||||
|
<div className="card" style={{ padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', marginBottom: '8px' }}>
|
||||||
|
<DownloadIcon size={14} style={{ color: 'var(--success)' }} />
|
||||||
|
<span style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Downloads</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '1.5rem', letterSpacing: '-0.03em' }}>{file.downloads}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mini chart */}
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.06em', fontFamily: 'var(--font-heading)', fontWeight: 600,
|
||||||
|
marginBottom: '12px',
|
||||||
|
}}>
|
||||||
|
Last 7 Days
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '16px', background: 'var(--bg-surface)', borderRadius: 'var(--radius-md)',
|
||||||
|
border: '1px solid var(--border-subtle)',
|
||||||
|
}}>
|
||||||
|
<MiniChart data={chartData} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Share link */}
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.06em', fontFamily: 'var(--font-heading)', fontWeight: 600,
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}>
|
||||||
|
Share Link
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', gap: '8px', alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
flex: 1, padding: '10px 14px', background: 'var(--bg-surface)',
|
||||||
|
borderRadius: 'var(--radius-md)', border: '1px solid var(--border-subtle)',
|
||||||
|
fontSize: '0.8125rem', color: 'var(--accent)',
|
||||||
|
fontFamily: 'var(--font-mono, monospace)',
|
||||||
|
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
{file.link}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={copyLink}
|
||||||
|
style={{ flexShrink: 0, minWidth: '90px' }}
|
||||||
|
>
|
||||||
|
{copied ? <><CheckIcon size={14} /> Copied!</> : <><CopyIcon size={14} /> Copy</>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details grid */}
|
||||||
|
<div style={{
|
||||||
|
padding: '16px', background: 'var(--bg-surface)', borderRadius: 'var(--radius-md)',
|
||||||
|
border: '1px solid var(--border-subtle)',
|
||||||
|
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px',
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', marginBottom: '4px' }}>Uploaded</div>
|
||||||
|
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 500, fontSize: '0.8125rem' }}>{formatDate(file.uploadedAt)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', marginBottom: '4px' }}>By</div>
|
||||||
|
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 500, fontSize: '0.8125rem' }}>{file.uploader}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', marginBottom: '4px' }}>Type</div>
|
||||||
|
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 500, fontSize: '0.8125rem' }}>{file.type.split('/')[1]?.toUpperCase() || 'FILE'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', marginBottom: '4px' }}>Size</div>
|
||||||
|
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 500, fontSize: '0.8125rem' }}>{formatFileSize(file.size)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer actions */}
|
||||||
|
<div style={{
|
||||||
|
position: 'sticky', bottom: 0, padding: '16px 24px',
|
||||||
|
borderTop: '1px solid var(--border-subtle)', background: 'var(--bg-elevated)',
|
||||||
|
display: 'flex', gap: '8px',
|
||||||
|
}}>
|
||||||
|
<button className="btn btn-secondary" onClick={copyLink} style={{ flex: 1 }}>
|
||||||
|
<CopyIcon size={14} /> Copy Link
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-danger" onClick={() => onDelete(file.id)}>
|
||||||
|
<TrashIcon size={14} /> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user