COI gate + admin review, mobile file viewer fixes for iOS
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m12s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m12s
- Integrate COI declaration dialog into jury evaluate page (blocks evaluation until declared) - Add COI review section to admin round page with clear/reassign/note actions - Fix mobile: remove inline preview (viewport too small), add labeled buttons - Fix iOS: open-in-new-tab uses synchronous window.open to avoid popup blocker - Fix iOS: download falls back to direct link if fetch+blob fails (CORS/Safari) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -333,36 +333,16 @@ function FileItem({ file }: { file: ProjectFile }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile action buttons — visible only on small screens */}
|
||||
{/* Mobile action buttons — no inline preview on mobile, just open + download */}
|
||||
<div className="flex md:hidden items-center gap-2 mt-3 pt-3 border-t">
|
||||
{canPreview && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
>
|
||||
{showPreview ? (
|
||||
<>
|
||||
<X className="mr-1.5 h-4 w-4" />
|
||||
Close
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="mr-1.5 h-4 w-4" />
|
||||
Preview
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<FileOpenButton file={file} />
|
||||
<FileDownloadButton file={file} />
|
||||
<FileOpenButton file={file} className="flex-1" label="Open in New Tab" />
|
||||
<FileDownloadButton file={file} className="flex-1" label="Download" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview area */}
|
||||
{/* Preview area — desktop only */}
|
||||
{showPreview && (
|
||||
<div className="rounded-lg border bg-muted/50 overflow-hidden">
|
||||
<div className="hidden md:block rounded-lg border bg-muted/50 overflow-hidden">
|
||||
{isLoadingUrl ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
@@ -561,7 +541,7 @@ function BulkDownloadButton({ projectId, fileIds }: { projectId: string; fileIds
|
||||
)
|
||||
}
|
||||
|
||||
function FileOpenButton({ file }: { file: ProjectFile }) {
|
||||
function FileOpenButton({ file, className, label }: { file: ProjectFile; className?: string; label?: string }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const { refetch } = trpc.file.getDownloadUrl.useQuery(
|
||||
@@ -571,13 +551,25 @@ function FileOpenButton({ file }: { file: ProjectFile }) {
|
||||
|
||||
const handleOpen = async () => {
|
||||
setLoading(true)
|
||||
// Open the blank window immediately in the click handler (synchronous)
|
||||
// so iOS Safari doesn't block it as a popup
|
||||
const newWindow = window.open('about:blank', '_blank')
|
||||
try {
|
||||
const result = await refetch()
|
||||
if (result.data?.url) {
|
||||
window.open(result.data.url, '_blank')
|
||||
if (newWindow) {
|
||||
newWindow.location.href = result.data.url
|
||||
} else {
|
||||
// Fallback: if popup was still blocked, navigate current tab
|
||||
window.location.href = result.data.url
|
||||
}
|
||||
} else {
|
||||
newWindow?.close()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get URL:', error)
|
||||
newWindow?.close()
|
||||
toast.error('Failed to open file')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -590,17 +582,19 @@ function FileOpenButton({ file }: { file: ProjectFile }) {
|
||||
onClick={handleOpen}
|
||||
disabled={loading}
|
||||
aria-label="Open file in new tab"
|
||||
className={className}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className={cn('h-4 w-4 animate-spin', label && 'mr-1.5')} />
|
||||
) : (
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<ExternalLink className={cn('h-4 w-4', label && 'mr-1.5')} />
|
||||
)}
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function FileDownloadButton({ file }: { file: ProjectFile }) {
|
||||
function FileDownloadButton({ file, className, label }: { file: ProjectFile; className?: string; label?: string }) {
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
|
||||
const { refetch } = trpc.file.getDownloadUrl.useQuery(
|
||||
@@ -613,17 +607,36 @@ function FileDownloadButton({ file }: { file: ProjectFile }) {
|
||||
try {
|
||||
const result = await refetch()
|
||||
if (result.data?.url) {
|
||||
// Fetch as blob to force download (cross-origin URLs ignore <a download>)
|
||||
const response = await fetch(result.data.url)
|
||||
const blob = await response.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = file.fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
// Try fetch+blob first (works on desktop, some mobile browsers)
|
||||
try {
|
||||
const response = await fetch(result.data.url)
|
||||
if (!response.ok) throw new Error('fetch failed')
|
||||
const blob = await response.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = file.fileName
|
||||
// iOS Safari needs the link in the DOM
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
// Delay cleanup for iOS
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}, 100)
|
||||
} catch {
|
||||
// Fallback: open in new tab (iOS Safari often blocks blob downloads)
|
||||
// Open synchronously via a link click to avoid popup blockers
|
||||
const link = document.createElement('a')
|
||||
link.href = result.data.url
|
||||
link.target = '_blank'
|
||||
link.rel = 'noopener noreferrer'
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
setTimeout(() => document.body.removeChild(link), 100)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to download file:', error)
|
||||
@@ -640,12 +653,14 @@ function FileDownloadButton({ file }: { file: ProjectFile }) {
|
||||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
aria-label="Download file"
|
||||
className={className}
|
||||
>
|
||||
{downloading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className={cn('h-4 w-4 animate-spin', label && 'mr-1.5')} />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
<Download className={cn('h-4 w-4', label && 'mr-1.5')} />
|
||||
)}
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user