Import source: cshare-app/src/pages/UploadPage

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:15:12 +03:30
parent 77d457cb77
commit 5d20e7d099
+453
View File
@@ -0,0 +1,453 @@
import { useState, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { UploadIcon, CopyIcon, CheckIcon, XIcon, TrashIcon } from '../components/Icons';
import { formatFileSize, getFileIcon, getFileIconColor } from '../data/mockData';
import { useToast } from '../components/ToastContext';
import { uploadFile } from '../lib/api';
export default function UploadPage() {
const navigate = useNavigate();
const [files, setFiles] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const [sharedLinks, setSharedLinks] = useState({});
const [copiedId, setCopiedId] = useState(null);
const dropRef = useRef(null);
const addToast = useToast();
const addFiles = useCallback((fileList) => {
const newFiles = fileList.map((f) => ({
id: 'f' + Date.now() + Math.random().toString(36).slice(2, 6),
rawFile: f,
name: f.name,
size: f.size,
type: f.type || 'application/octet-stream',
progress: 0,
status: 'uploading',
}));
setFiles((prev) => [...prev, ...newFiles]);
newFiles.forEach((file) => {
uploadFile(file.rawFile, {
onProgress: (progress) => {
setFiles((prev) =>
prev.map((f) =>
f.id === file.id ? { ...f, progress: Math.min(progress, 99) } : f
)
);
},
})
.then((payload) => {
setFiles((prev) =>
prev.map((f) =>
f.id === file.id ? { ...f, progress: 100, status: 'done', apiFile: payload.data } : f
)
);
setSharedLinks((prev) => ({ ...prev, [file.id]: payload.data.link }));
addToast(`${file.name} uploaded`, 'success');
})
.catch((error) => {
setFiles((prev) =>
prev.map((f) =>
f.id === file.id ? { ...f, progress: 0, status: 'error', error: error.message } : f
)
);
addToast(error.message, 'error');
});
});
}, [addToast]);
const handleDrop = useCallback((e) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
addFiles(droppedFiles);
}, [addFiles]);
const generateShareLink = (fileId) => {
const file = files.find((item) => item.id === fileId);
if (file?.apiFile?.link) {
setSharedLinks((prev) => ({ ...prev, [fileId]: file.apiFile.link }));
addToast('Link ready', 'success');
}
};
const copyLink = (fileId) => {
const link = sharedLinks[fileId];
if (!link) return;
navigator.clipboard?.writeText(link);
setCopiedId(fileId);
addToast('Link copied to clipboard', 'success');
setTimeout(() => setCopiedId(null), 2000);
};
const removeFile = (fileId) => {
setFiles((prev) => prev.filter((f) => f.id !== fileId));
setSharedLinks((prev) => {
const next = { ...prev };
delete next[fileId];
return next;
});
};
const handleFileInput = (e) => {
const fileList = Array.from(e.target.files || []);
if (fileList.length) addFiles(fileList);
e.target.value = '';
};
return (
<div className="page-enter" style={{ maxWidth: '760px', margin: '0 auto', padding: '48px 24px' }}>
<div style={{ marginBottom: '40px' }}>
<h1 className="heading-lg" style={{ marginBottom: '12px' }}>Upload files</h1>
<p className="text-secondary">Drop your files below and share them with expiring links.</p>
</div>
{/* Drop Zone — animated dashed border */}
<div
ref={dropRef}
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
onClick={() => document.getElementById('file-input')?.click()}
className={`drop-zone-animated ${isDragging ? 'dragging' : ''}`}
style={{
border: 'none',
borderRadius: 'var(--radius-xl)',
background: isDragging ? 'var(--accent-muted)' : 'var(--bg-elevated)',
padding: 'clamp(48px, 6vh, 80px) 24px',
textAlign: 'center',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden',
}}
>
<input
id="file-input"
type="file"
multiple
style={{ display: 'none' }}
onChange={handleFileInput}
/>
{/* Ambient glow behind upload icon when dragging */}
{isDragging && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
width: '200px',
height: '200px',
transform: 'translate(-50%, -50%)',
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(232,168,56,0.15) 0%, transparent 70%)',
pointerEvents: 'none',
animation: 'pulseGlow 1.5s ease infinite',
}} />
)}
<div style={{
width: '64px',
height: '64px',
margin: '0 auto 20px',
borderRadius: '50%',
background: 'var(--accent-muted)',
border: '1px solid var(--accent-border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--accent)',
transition: 'all var(--transition-base)',
transform: isDragging ? 'scale(1.15)' : 'scale(1)',
position: 'relative',
zIndex: 1,
}}>
<UploadIcon size={28} style={{
transition: 'transform var(--transition-base)',
transform: isDragging ? 'translateY(-4px)' : 'translateY(0)',
}} />
</div>
<p style={{
fontFamily: 'var(--font-heading)',
fontWeight: 600,
fontSize: '1.125rem',
marginBottom: '8px',
color: isDragging ? 'var(--accent)' : 'var(--text-primary)',
transition: 'color var(--transition-fast)',
position: 'relative',
zIndex: 1,
}}>
{isDragging ? 'Drop your files here' : 'Drag files here to upload'}
</p>
<p className="text-muted" style={{ fontSize: '0.875rem', position: 'relative', zIndex: 1 }}>
or click to browse — up to 500MB per file
</p>
</div>
{/* File List */}
{files.length > 0 && (
<div style={{ marginTop: '32px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}>
<h3 className="heading-sm">
{files.filter((f) => f.status === 'done').length} of {files.length} uploaded
</h3>
{files.every((f) => f.status === 'done') && (
<button
className="btn btn-primary btn-sm"
onClick={() => {
files.forEach((f) => {
if (!sharedLinks[f.id]) generateShareLink(f.id);
});
}}
style={{ animation: 'scaleIn 0.3s ease' }}
>
Share All
</button>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{files.map((file, index) => (
<div
key={file.id}
style={{
background: 'var(--bg-elevated)',
border: `1px solid ${file.status === 'done' && sharedLinks[file.id] ? 'var(--accent-border)' : 'var(--border-subtle)'}`,
borderLeft: `3px solid ${getFileIconColor(file.type)}`,
borderRadius: 'var(--radius-lg)',
padding: '16px 20px',
animation: `fadeInUp 0.4s ${index * 0.05}s ease both`,
transition: 'border-color var(--transition-fast), box-shadow var(--transition-fast)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.boxShadow = 'var(--shadow-md)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.boxShadow = 'none';
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* File icon */}
<div style={{
width: '40px',
height: '40px',
borderRadius: 'var(--radius-sm)',
background: 'var(--bg-surface)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'var(--font-heading)',
fontWeight: 700,
fontSize: '0.625rem',
letterSpacing: '0.05em',
color: getFileIconColor(file.type),
flexShrink: 0,
border: '1px solid var(--border-subtle)',
}}>
{getFileIcon(file.type)}
</div>
{/* File info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontFamily: 'var(--font-heading)',
fontWeight: 600,
fontSize: '0.9375rem',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
{file.name}
</div>
<div style={{ fontSize: '0.8125rem', color: 'var(--text-muted)', marginTop: '2px', display: 'flex', alignItems: 'center', gap: '8px' }}>
{formatFileSize(file.size)}
{file.status === 'done' && (
<span style={{
color: 'var(--success)',
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
animation: 'scaleIn 0.3s ease',
}}>
<CheckIcon size={12} />
Complete
</span>
)}
{file.status === 'error' && (
<span style={{ color: 'var(--danger)' }}>
{file.error || 'Upload failed'}
</span>
)}
</div>
</div>
{/* Actions */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
{file.status === 'done' && sharedLinks[file.id] && (
<button
className="btn btn-secondary btn-sm"
onClick={() => copyLink(file.id)}
style={{ fontFamily: 'var(--font-body)', fontSize: '0.75rem' }}
>
{copiedId === file.id ? <CheckIcon size={14} style={{ color: 'var(--success)' }} /> : <CopyIcon size={14} />}
{copiedId === file.id ? ' Copied' : ' Copy Link'}
</button>
)}
{file.status === 'done' && !sharedLinks[file.id] && (
<button
className="btn btn-primary btn-sm"
onClick={() => generateShareLink(file.id)}
style={{ animation: 'scaleIn 0.3s ease' }}
>
Share
</button>
)}
<button
className="btn btn-ghost btn-icon btn-sm"
onClick={() => removeFile(file.id)}
style={{
color: 'var(--danger)',
background: 'rgba(232, 72, 72, 0.08)',
border: '1px solid rgba(232, 72, 72, 0.15)',
borderRadius: 'var(--radius-sm)',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(232, 72, 72, 0.15)';
e.currentTarget.style.borderColor = 'rgba(232, 72, 72, 0.35)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(232, 72, 72, 0.08)';
e.currentTarget.style.borderColor = 'rgba(232, 72, 72, 0.15)';
}}
title="Remove from queue"
>
<TrashIcon size={14} />
</button>
</div>
</div>
{/* Progress bar — with shimmer */}
{file.status === 'uploading' && (
<div style={{ marginTop: '12px' }}>
<div style={{
height: '4px',
borderRadius: '2px',
background: 'var(--bg-surface)',
overflow: 'hidden',
}}>
<div
className="shimmer-progress"
style={{
height: '100%',
width: `${file.progress}%`,
borderRadius: '2px',
background: 'linear-gradient(90deg, var(--accent), var(--accent-hover))',
transition: 'width 0.3s ease',
boxShadow: '0 0 8px rgba(74, 144, 255, 0.3)',
}}
/>
</div>
<div style={{
fontSize: '0.75rem',
color: 'var(--text-muted)',
marginTop: '4px',
textAlign: 'right',
}}>
{Math.round(file.progress)}%
</div>
</div>
)}
{/* Share link display */}
{sharedLinks[file.id] && (
<div style={{ animation: 'fadeIn 0.35s ease' }}>
<div style={{
marginTop: '12px',
padding: '10px 14px',
background: 'var(--bg-surface)',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border-subtle)',
fontFamily: 'var(--font-mono, monospace)',
fontSize: '0.8125rem',
color: 'var(--accent)',
wordBreak: 'break-all',
}}>
{sharedLinks[file.id]}
</div>
{/* One-Click Quick Share Buttons */}
<div style={{
display: 'flex',
gap: '6px',
marginTop: '8px',
alignItems: 'center',
flexWrap: 'wrap',
}}>
<span style={{ fontSize: '0.625rem', color: 'var(--text-muted)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', marginRight: '4px' }}>
Quick Share:
</span>
{/* Slack */}
<a
href={`slack://open?message=${encodeURIComponent('Check out this shared file on CShare: ' + sharedLinks[file.id])}`}
className="btn btn-ghost btn-sm"
style={{ padding: '3px 8px', fontSize: '0.6875rem', height: '24px', borderRadius: '100px', background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)' }}
>
Slack
</a>
{/* WhatsApp */}
<a
href={`https://api.whatsapp.com/send?text=${encodeURIComponent('Check out this shared file on CShare: ' + sharedLinks[file.id])}`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost btn-sm"
style={{ padding: '3px 8px', fontSize: '0.6875rem', height: '24px', borderRadius: '100px', background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)' }}
>
WhatsApp
</a>
{/* Telegram */}
<a
href={`https://t.me/share/url?url=${encodeURIComponent(sharedLinks[file.id])}&text=${encodeURIComponent('Check out this shared file on CShare!')}`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost btn-sm"
style={{ padding: '3px 8px', fontSize: '0.6875rem', height: '24px', borderRadius: '100px', background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)' }}
>
Telegram
</a>
{/* Email */}
<a
href={`mailto:?subject=${encodeURIComponent('File shared via CShare')}&body=${encodeURIComponent('Here is the link to the shared file on CShare: ' + sharedLinks[file.id])}`}
className="btn btn-ghost btn-sm"
style={{ padding: '3px 8px', fontSize: '0.6875rem', height: '24px', borderRadius: '100px', background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)' }}
>
Email
</a>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Preview link — go to file view */}
<div style={{ marginTop: '48px', textAlign: 'center' }}>
<p className="text-muted" style={{ fontSize: '0.875rem', marginBottom: '12px' }}>
Want to see what recipients see?
</p>
<button
className="btn btn-ghost"
onClick={() => navigate('/preview')}
>
View sample shared file
</button>
</div>
</div>
);
}