|
|
|
@@ -66,11 +66,18 @@ function CardTypeSelect({ |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const isVideoUrl = (url: string) => /\.(mp4|webm|mov|m4v|ogv)(\?|$)/i.test(url);
|
|
|
|
const VIDEO_EXTENSIONS = 'mp4|m4v|webm|mov|qt|mkv|avi|divx|wmv|asf|flv|f4v|3gp|3gpp|3g2|mts|m2ts|ts|mpg|mpeg|vob|mxf|ogv|ogg';
|
|
|
|
// Sottoinsieme di formati video davvero riproducibili dai browser moderni
|
|
|
|
const PLAYBACK_SUPPORTED_VIDEO = 'mp4|m4v|webm|mov|qt|ogv|ogg';
|
|
|
|
const PLAYBACK_SUPPORTED_LABEL = 'MP4, M4V, WebM, MOV, OGV';
|
|
|
|
|
|
|
|
const isVideoUrl = (url: string) => new RegExp(`\\.(${VIDEO_EXTENSIONS})(\\?|$)`, 'i').test(url);
|
|
|
|
const isPdfFile = (file: File) =>
|
|
|
|
file.type === 'application/pdf' || /\.pdf$/i.test(file.name);
|
|
|
|
const isVideoFile = (file: File) =>
|
|
|
|
file.type.startsWith('video/') || /\.(mp4|webm|mov|m4v|ogv)$/i.test(file.name);
|
|
|
|
file.type.startsWith('video/') || new RegExp(`\\.(${VIDEO_EXTENSIONS})$`, 'i').test(file.name);
|
|
|
|
const isPlayableVideoFile = (file: File) =>
|
|
|
|
new RegExp(`\\.(${PLAYBACK_SUPPORTED_VIDEO})$`, 'i').test(file.name);
|
|
|
|
|
|
|
|
const extractFileName = (url: string): string => {
|
|
|
|
const match = url.match(/[?&]name=([^&]+)/);
|
|
|
|
@@ -171,7 +178,7 @@ export default function AdminDashboard() { |
|
|
|
const [uploading, setUploading] = useState<{ [key: string]: boolean }>({});
|
|
|
|
|
|
|
|
// NEW UI STATES: Toast and Confirm Dialog
|
|
|
|
const [toast, setToast] = useState<string | null>(null);
|
|
|
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
|
|
|
const [confirmDialog, setConfirmDialog] = useState<{ message: string, onConfirm: () => void } | null>(null);
|
|
|
|
const [pdfProgress, setPdfProgress] = useState<{ name: string; page: number; total: number } | null>(null);
|
|
|
|
|
|
|
|
@@ -179,9 +186,9 @@ export default function AdminDashboard() { |
|
|
|
const externalLinksOn = portal.externalLinkEnabled ?? EXTERNAL_LINK_DEFAULT;
|
|
|
|
|
|
|
|
// Helper to show auto-dismissing toast
|
|
|
|
const showToast = (message: string) => {
|
|
|
|
setToast(message);
|
|
|
|
setTimeout(() => setToast(null), 3000);
|
|
|
|
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
|
|
|
setToast({ message, type });
|
|
|
|
setTimeout(() => setToast(null), type === 'error' ? 6000 : 3000);
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
@@ -217,8 +224,33 @@ export default function AdminDashboard() { |
|
|
|
let pendingCover: string | null = null;
|
|
|
|
const canPromote = () => startedWithoutCover && !pendingCover;
|
|
|
|
|
|
|
|
const uploaded: MediaItem[] = [];
|
|
|
|
// Pre-filtro: scarta video con formati non riproducibili nei browser
|
|
|
|
const rejected: string[] = [];
|
|
|
|
const acceptedFiles: File[] = [];
|
|
|
|
for (const file of Array.from(files)) {
|
|
|
|
if (isVideoFile(file) && !isPlayableVideoFile(file)) {
|
|
|
|
rejected.push(file.name);
|
|
|
|
} else {
|
|
|
|
acceptedFiles.push(file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (rejected.length > 0) {
|
|
|
|
const list = rejected.length <= 3
|
|
|
|
? rejected.join(', ')
|
|
|
|
: `${rejected.slice(0, 3).join(', ')} e altri ${rejected.length - 3}`;
|
|
|
|
showToast(
|
|
|
|
`Formato non supportato! I formati supportati sono: ${PLAYBACK_SUPPORTED_LABEL}. File ignorati: ${list}`,
|
|
|
|
'error'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (acceptedFiles.length === 0) {
|
|
|
|
setUploading(prev => ({ ...prev, extraMedia: false }));
|
|
|
|
e.target.value = '';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const uploaded: MediaItem[] = [];
|
|
|
|
for (const file of acceptedFiles) {
|
|
|
|
try {
|
|
|
|
if (isPdfFile(file)) {
|
|
|
|
const items = await pdfToImageItems(file, (page, total) =>
|
|
|
|
@@ -701,7 +733,7 @@ export default function AdminDashboard() { |
|
|
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 hover:bg-gray-50 transition-colors">
|
|
|
|
<input
|
|
|
|
type="file"
|
|
|
|
accept="image/*,video/*,application/pdf,.pdf"
|
|
|
|
accept="image/*,video/*,application/pdf,.pdf,.mov,.qt,.mkv,.avi,.divx,.wmv,.asf,.flv,.f4v,.3gp,.3gpp,.3g2,.mts,.m2ts,.ts,.mpg,.mpeg,.vob,.mxf,.ogv,.ogg"
|
|
|
|
multiple
|
|
|
|
onChange={handleUploadExtraMedia}
|
|
|
|
className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100 cursor-pointer"
|
|
|
|
@@ -818,11 +850,15 @@ export default function AdminDashboard() { |
|
|
|
|
|
|
|
{/* CUSTOM TOAST NOTIFICATION */}
|
|
|
|
{toast && (
|
|
|
|
<div className="fixed bottom-6 right-6 z-[70] bg-gray-900 text-white px-6 py-4 rounded-lg shadow-2xl flex items-center gap-3 animate-in slide-in-from-bottom-5 fade-in duration-300">
|
|
|
|
<div className="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center text-gray-900 font-bold text-sm">
|
|
|
|
✓
|
|
|
|
<div className={`fixed bottom-6 right-6 z-[70] text-white px-6 py-4 rounded-lg shadow-2xl flex items-start gap-3 animate-in slide-in-from-bottom-5 fade-in duration-300 max-w-md ${
|
|
|
|
toast.type === 'error' ? 'bg-red-700' : 'bg-gray-900'
|
|
|
|
}`}>
|
|
|
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center font-bold text-sm shrink-0 mt-0.5 ${
|
|
|
|
toast.type === 'error' ? 'bg-white text-red-700' : 'bg-green-500 text-gray-900'
|
|
|
|
}`}>
|
|
|
|
{toast.type === 'error' ? '!' : '✓'}
|
|
|
|
</div>
|
|
|
|
<span className="font-medium">{toast}</span>
|
|
|
|
<span className="font-medium leading-snug">{toast.message}</span>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
|