diff --git a/packages/ui/src/components/CreationWizardPopup.tsx b/packages/ui/src/components/CreationWizardPopup.tsx index 968860ab..30cc53e0 100644 --- a/packages/ui/src/components/CreationWizardPopup.tsx +++ b/packages/ui/src/components/CreationWizardPopup.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { T, translate } from '@/i18n'; -import { Image, FilePlus, Zap, Mic, Loader2, Upload, Video, Layers, BookPlus, Plus } from 'lucide-react'; +import { Image, FilePlus, Zap, Mic, Loader2, Upload, Video, Layers, BookPlus, Plus, HardDrive } from 'lucide-react'; import { usePageGenerator } from '@/hooks/usePageGenerator'; import VoiceRecordingPopup from './VoiceRecordingPopup'; @@ -20,6 +20,7 @@ import { createPicture } from '@/modules/posts/client-pictures'; import { fetchPostById } from '@/modules/posts/client-posts'; import { toast } from 'sonner'; import { supabase } from '@/integrations/supabase/client'; +import { serverUrl } from '@/lib/db'; interface CreationWizardPopupProps { isOpen: boolean; @@ -45,6 +46,8 @@ export const CreationWizardPopup: React.FC = ({ const [isUploadingImage, setIsUploadingImage] = useState(false); const [isUploadingVideo, setIsUploadingVideo] = useState(false); const [videoUploadProgress, setVideoUploadProgress] = useState(0); + const [isUploadingFiles, setIsUploadingFiles] = useState(false); + const [fileUploadProgress, setFileUploadProgress] = useState(0); const imageInputRef = useRef(null); const videoInputRef = useRef(null); @@ -283,6 +286,66 @@ export const CreationWizardPopup: React.FC = ({ imageInputRef.current?.click(); }; + const handleFileUpload = async () => { + const filesToUpload = preloadedImages.filter(img => img.file); + if (filesToUpload.length === 0) { + toast.error(translate('No files to upload')); + return; + } + + const { data: { session: authSession } } = await supabase.auth.getSession(); + if (!authSession?.access_token) { + toast.error(translate('Please sign in to upload files')); + return; + } + + setIsUploadingFiles(true); + setFileUploadProgress(0); + const mount = 'home'; + let done = 0; + + try { + await Promise.all(filesToUpload.map(img => new Promise((resolve, reject) => { + const file = img.file as File; + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${serverUrl}/api/vfs/upload/${mount}/${encodeURIComponent(file.name)}`, true); + xhr.setRequestHeader('Authorization', `Bearer ${authSession.access_token}`); + + xhr.upload.onprogress = (evt) => { + if (evt.lengthComputable) { + const filePct = (evt.loaded / evt.total) * 100; + setFileUploadProgress(Math.round(((done * 100) + filePct) / filesToUpload.length)); + } + }; + + xhr.onload = () => { + done++; + setFileUploadProgress(Math.round((done / filesToUpload.length) * 100)); + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + let msg = `Error ${xhr.status}`; + try { const b = JSON.parse(xhr.responseText); msg = b.message || b.error || msg; } catch {} + reject(new Error(msg)); + } + }; + xhr.onerror = () => reject(new Error('Network error')); + + const formData = new FormData(); + formData.append('file', file); + xhr.send(formData); + }))); + + toast.success(translate(`${filesToUpload.length} file(s) uploaded to storage`)); + onClose(); + } catch (error: any) { + toast.error(error.message || translate('Upload failed')); + } finally { + setIsUploadingFiles(false); + setFileUploadProgress(0); + } + }; + // Helper function to upload internal video const uploadInternalVideo = async (file: File): Promise => { // Get auth token before creating the XHR @@ -526,6 +589,25 @@ export const CreationWizardPopup: React.FC = ({ Append to Existing Post )} + {preloadedImages.length > 0 && ( + + )} diff --git a/packages/ui/src/components/GlobalDragDrop.tsx b/packages/ui/src/components/GlobalDragDrop.tsx index 41756f29..f25a88f7 100644 --- a/packages/ui/src/components/GlobalDragDrop.tsx +++ b/packages/ui/src/components/GlobalDragDrop.tsx @@ -28,18 +28,19 @@ const GlobalDragDrop = () => { // Check if it's a valid URL (simplistic check) const isUrl = url && (url.startsWith('http://') || url.startsWith('https://')); - if (supportedFiles.length === 0 && !isUrl) { - if (files.length > 0) { - toast.error("Unsupported file type. Please drop images or videos."); - } + // Any file type is acceptable — wizard has "Upload as Files" for non-media + if (files.length === 0 && !isUrl) { return; } + // Use all files if no media-specific ones found (for VFS file upload) + const filesToPass = supportedFiles.length > 0 ? supportedFiles : files; + try { - if (supportedFiles.length > 0) { - // Normal file workflow + if (files.length > 0) { + // File workflow — wizard shows options incl. "Upload as Files" await set('share-target', { - files: supportedFiles, + files: filesToPass, title: '', text: '', url: isUrl ? url : '', @@ -142,7 +143,7 @@ const GlobalDragDrop = () => { Drop files to upload

- Drag images or videos anywhere to start + Drag files anywhere to start

);