Add limits for content + fix video playback

This commit is contained in:
2025-12-16 02:01:03 +07:00
parent 574140e67d
commit d96f8de568
5 changed files with 166 additions and 44 deletions

View File

@@ -32,11 +32,16 @@ export const assignmentsApi = {
return response.data
},
// Get proof image as blob URL
getProofImageUrl: async (assignmentId: number): Promise<string> => {
const response = await client.get(`/assignments/${assignmentId}/proof-image`, {
// Get proof media as blob URL (supports both images and videos)
getProofMediaUrl: async (assignmentId: number): Promise<{ url: string; type: 'image' | 'video' }> => {
const response = await client.get(`/assignments/${assignmentId}/proof-media`, {
responseType: 'blob',
})
return URL.createObjectURL(response.data)
const contentType = response.headers['content-type'] || ''
const isVideo = contentType.startsWith('video/')
return {
url: URL.createObjectURL(response.data),
type: isVideo ? 'video' : 'image',
}
},
}

View File

@@ -20,7 +20,8 @@ export function AssignmentDetailPage() {
const [assignment, setAssignment] = useState<AssignmentDetail | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [proofImageBlobUrl, setProofImageBlobUrl] = useState<string | null>(null)
const [proofMediaBlobUrl, setProofMediaBlobUrl] = useState<string | null>(null)
const [proofMediaType, setProofMediaType] = useState<'image' | 'video' | null>(null)
// Dispute creation
const [showDisputeForm, setShowDisputeForm] = useState(false)
@@ -38,8 +39,8 @@ export function AssignmentDetailPage() {
loadAssignment()
return () => {
// Cleanup blob URL on unmount
if (proofImageBlobUrl) {
URL.revokeObjectURL(proofImageBlobUrl)
if (proofMediaBlobUrl) {
URL.revokeObjectURL(proofMediaBlobUrl)
}
}
}, [id])
@@ -52,13 +53,14 @@ export function AssignmentDetailPage() {
const data = await assignmentsApi.getDetail(parseInt(id))
setAssignment(data)
// Load proof image if exists
// Load proof media if exists
if (data.proof_image_url) {
try {
const blobUrl = await assignmentsApi.getProofImageUrl(parseInt(id))
setProofImageBlobUrl(blobUrl)
const { url, type } = await assignmentsApi.getProofMediaUrl(parseInt(id))
setProofMediaBlobUrl(url)
setProofMediaType(type)
} catch {
// Ignore error, image just won't show
// Ignore error, media just won't show
}
}
} catch (err: unknown) {
@@ -251,15 +253,24 @@ export function AssignmentDetailPage() {
Доказательство
</h3>
{/* Proof image */}
{/* Proof media (image or video) */}
{assignment.proof_image_url && (
<div className="mb-4">
{proofImageBlobUrl ? (
<img
src={proofImageBlobUrl}
alt="Proof"
className="w-full rounded-lg max-h-96 object-contain bg-gray-900"
/>
{proofMediaBlobUrl ? (
proofMediaType === 'video' ? (
<video
src={proofMediaBlobUrl}
controls
className="w-full rounded-lg max-h-96 bg-gray-900"
preload="metadata"
/>
) : (
<img
src={proofMediaBlobUrl}
alt="Proof"
className="w-full rounded-lg max-h-96 object-contain bg-gray-900"
/>
)
) : (
<div className="w-full h-48 bg-gray-900 rounded-lg flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-gray-500" />

View File

@@ -9,6 +9,12 @@ import { Loader2, Upload, X, Gamepad2, ArrowLeftRight, Check, XCircle, Clock, Se
import { useToast } from '@/store/toast'
import { useConfirm } from '@/store/confirm'
// File size limits
const MAX_IMAGE_SIZE = 15 * 1024 * 1024 // 15 MB
const MAX_VIDEO_SIZE = 30 * 1024 * 1024 // 30 MB
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov']
export function PlayPage() {
const { id } = useParams<{ id: string }>()
const toast = useToast()
@@ -142,6 +148,38 @@ export function PlayPage() {
}
}
const validateAndSetFile = (
file: File | null,
setFile: (file: File | null) => void,
inputRef: React.RefObject<HTMLInputElement>
) => {
if (!file) {
setFile(null)
return
}
const ext = file.name.split('.').pop()?.toLowerCase() || ''
const isImage = IMAGE_EXTENSIONS.includes(ext)
const isVideo = VIDEO_EXTENSIONS.includes(ext)
if (!isImage && !isVideo) {
toast.error('Неподдерживаемый формат файла')
if (inputRef.current) inputRef.current.value = ''
return
}
const maxSize = isImage ? MAX_IMAGE_SIZE : MAX_VIDEO_SIZE
const maxSizeMB = isImage ? 15 : 30
if (file.size > maxSize) {
toast.error(`Файл слишком большой. Максимум ${maxSizeMB} МБ для ${isImage ? 'изображений' : 'видео'}`)
if (inputRef.current) inputRef.current.value = ''
return
}
setFile(file)
}
const loadData = async () => {
if (!id) return
try {
@@ -651,7 +689,7 @@ export function PlayPage() {
type="file"
accept="image/*,video/*"
className="hidden"
onChange={(e) => setEventProofFile(e.target.files?.[0] || null)}
onChange={(e) => validateAndSetFile(e.target.files?.[0] || null, setEventProofFile, eventFileInputRef)}
/>
{eventProofFile ? (
@@ -666,14 +704,19 @@ export function PlayPage() {
</Button>
</div>
) : (
<Button
variant="secondary"
className="w-full"
onClick={() => eventFileInputRef.current?.click()}
>
<Upload className="w-4 h-4 mr-2" />
Выбрать файл
</Button>
<div>
<Button
variant="secondary"
className="w-full"
onClick={() => eventFileInputRef.current?.click()}
>
<Upload className="w-4 h-4 mr-2" />
Выбрать файл
</Button>
<p className="text-xs text-gray-500 mt-1 text-center">
Макс. 15 МБ для изображений, 30 МБ для видео
</p>
</div>
)}
</div>
@@ -983,7 +1026,7 @@ export function PlayPage() {
type="file"
accept="image/*,video/*"
className="hidden"
onChange={(e) => setProofFile(e.target.files?.[0] || null)}
onChange={(e) => validateAndSetFile(e.target.files?.[0] || null, setProofFile, fileInputRef)}
/>
{proofFile ? (
@@ -998,14 +1041,19 @@ export function PlayPage() {
</Button>
</div>
) : (
<Button
variant="secondary"
className="w-full"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="w-4 h-4 mr-2" />
Выбрать файл
</Button>
<div>
<Button
variant="secondary"
className="w-full"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="w-4 h-4 mr-2" />
Выбрать файл
</Button>
<p className="text-xs text-gray-500 mt-1 text-center">
Макс. 15 МБ для изображений, 30 МБ для видео
</p>
</div>
)}
</div>