diff --git a/client/src/components/ArtefactDetailPanel.jsx b/client/src/components/ArtefactDetailPanel.jsx
index 648797e..5c65623 100644
--- a/client/src/components/ArtefactDetailPanel.jsx
+++ b/client/src/components/ArtefactDetailPanel.jsx
@@ -73,10 +73,10 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
// File upload (for design/video)
const [uploading, setUploading] = useState(false)
- // Video modal (for video type with Drive link)
- const [showVideoModal, setShowVideoModal] = useState(false)
- const [videoMode, setVideoMode] = useState('upload') // 'upload' or 'drive'
+ // Video inline (Drive link input)
const [driveUrl, setDriveUrl] = useState('')
+ const [dragOver, setDragOver] = useState(false)
+ const [uploadProgress, setUploadProgress] = useState(0)
// Comments
const [comments, setComments] = useState([])
@@ -190,15 +190,20 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
}
}
- const handleFileUpload = async (e) => {
- const file = e.target.files?.[0]
+ const handleFileUpload = async (fileOrEvent) => {
+ const file = fileOrEvent instanceof File ? fileOrEvent : fileOrEvent.target?.files?.[0]
if (!file) return
setUploading(true)
+ setUploadProgress(0)
try {
const formData = new FormData()
formData.append('file', file)
- await api.upload(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/attachments`, formData)
+ await api.upload(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/attachments`, formData, {
+ onUploadProgress: (e) => {
+ if (e.total) setUploadProgress(Math.round((e.loaded / e.total) * 100))
+ }
+ })
toast.success(t('artefacts.fileUploaded'))
loadVersionData(selectedVersion.Id)
} catch (err) {
@@ -206,6 +211,16 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
toast.error(t('artefacts.uploadFailed'))
} finally {
setUploading(false)
+ setUploadProgress(0)
+ }
+ }
+
+ const handleVideoDrop = (e) => {
+ e.preventDefault()
+ setDragOver(false)
+ const file = e.dataTransfer.files?.[0]
+ if (file && file.type.startsWith('video/')) {
+ handleFileUpload(file)
}
}
@@ -221,7 +236,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
drive_url: driveUrl,
})
toast.success(t('artefacts.videoLinkAdded'))
- setShowVideoModal(false)
setDriveUrl('')
loadVersionData(selectedVersion.Id)
} catch (err) {
@@ -608,68 +622,87 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
)}
- {/* VIDEO TYPE: Files and Drive links */}
+ {/* VIDEO TYPE: Files and Drive links — all inline */}
{artefact.type === 'video' && (
-
-
{t('artefacts.videosLabel')}
-
-
+
{t('artefacts.videosLabel')}
- {versionData.attachments && versionData.attachments.length > 0 ? (
-
+ {/* Existing attachments */}
+ {versionData.attachments && versionData.attachments.length > 0 && (
+
{versionData.attachments.map(att => (
{att.drive_url ? (
{t('artefacts.googleDriveVideo')}
-
-
+
) : (
{att.original_name}
- setConfirmDeleteAttId(att.Id)}
- className="text-red-600 hover:text-red-700"
- >
+ setConfirmDeleteAttId(att.Id)} className="text-red-600 hover:text-red-700">
-
+
)}
))}
- ) : (
-
-
-
{t('artefacts.noVideos')}
-
)}
+
+ {/* Drag-and-drop / click-to-upload zone */}
+
+
+ {/* Google Drive URL inline input */}
+
+
+ setDriveUrl(e.target.value)}
+ placeholder="https://drive.google.com/file/d/..."
+ className="flex-1 px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
+ onKeyDown={e => { if (e.key === 'Enter' && driveUrl.trim()) handleAddDriveVideo() }}
+ />
+
+ {t('artefacts.addLink')}
+
+
)}
@@ -896,84 +929,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
- {/* Video Modal */}
- setShowVideoModal(false)} title={t('artefacts.addVideoTitle')} size="md">
-
-
- setVideoMode('upload')}
- className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
- videoMode === 'upload'
- ? 'bg-brand-primary text-white'
- : 'bg-surface-secondary text-text-secondary hover:bg-surface-tertiary'
- }`}
- >
- {t('artefacts.uploadFile')}
-
- setVideoMode('drive')}
- className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
- videoMode === 'drive'
- ? 'bg-brand-primary text-white'
- : 'bg-surface-secondary text-text-secondary hover:bg-surface-tertiary'
- }`}
- >
- {t('artefacts.googleDriveLink')}
-
-
-
- {videoMode === 'upload' ? (
-
-
-
- ) : (
-
-
-
setDriveUrl(e.target.value)}
- placeholder="https://drive.google.com/file/d/..."
- className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
- />
-
- {t('artefacts.publiclyAccessible')}
-
-
- setShowVideoModal(false)}
- className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
- >
- {t('common.cancel')}
-
-
- {uploading ? t('artefacts.adding') : t('artefacts.addLink')}
-
-
-
- )}
-
-
-
{/* Delete Language Confirmation */}
handleResponse(r, `DELETE ${path}`)),
- upload: (path, formData) => fetch(`${API}${path}`, {
- method: 'POST',
- credentials: 'include',
- body: formData,
- }).then(r => handleResponse(r, `UPLOAD ${path}`)),
+ upload: (path, formData, opts = {}) => {
+ if (opts.onUploadProgress) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest()
+ xhr.open('POST', `${API}${path}`)
+ xhr.withCredentials = true
+ xhr.upload.onprogress = (e) => {
+ if (e.lengthComputable) opts.onUploadProgress({ loaded: e.loaded, total: e.total })
+ }
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ try { resolve(normalize(JSON.parse(xhr.responseText))) } catch { resolve(xhr.responseText) }
+ } else {
+ reject(new Error(`Upload failed: ${xhr.status}`))
+ }
+ }
+ xhr.onerror = () => reject(new Error('Upload failed'))
+ xhr.send(formData)
+ })
+ }
+ return fetch(`${API}${path}`, {
+ method: 'POST',
+ credentials: 'include',
+ body: formData,
+ }).then(r => handleResponse(r, `UPLOAD ${path}`))
+ },
};
// Brand color palette — dynamically assigned from a rotating palette