Init Repo
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Submissions History (Top) -->
|
||||
<div class="flex-1 overflow-y-auto p-2">
|
||||
<div v-if="submissions.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<Upload class="h-10 w-10 mb-2 opacity-50" />
|
||||
<p class="text-sm">No submissions yet. Submit your work below.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<SubmissionCard
|
||||
v-for="submission in submissions"
|
||||
:key="submission.id"
|
||||
:submission="submission"
|
||||
@view="handleView"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Work Form (Bottom) -->
|
||||
<div class="flex-shrink-0 border-t bg-background p-2">
|
||||
<div class="space-y-2">
|
||||
<div class="border-2 border-dashed rounded-lg p-3 text-center hover:border-primary/50 transition-colors">
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<Upload class="h-6 w-6 mx-auto mb-1 text-muted-foreground" />
|
||||
<p class="text-xs font-medium mb-1">Submit Work</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="fileInput?.click()"
|
||||
:disabled="uploading"
|
||||
class="h-7 text-xs"
|
||||
>
|
||||
<Upload class="h-3 w-3 mr-1" />
|
||||
{{ uploading ? 'Uploading...' : 'Choose File' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
v-model="submissionNotes"
|
||||
placeholder="Add notes about this submission..."
|
||||
rows="2"
|
||||
class="resize-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Viewer Dialog -->
|
||||
<Dialog v-model:open="viewerOpen">
|
||||
<DialogContent class="max-w-6xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{{ selectedSubmission?.file_name }} (v{{ selectedSubmission?.version_number }})
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Submission preview and details
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="flex items-center justify-center overflow-auto">
|
||||
<!-- Video Player -->
|
||||
<video
|
||||
v-if="selectedSubmission?.stream_url && mediaBlobUrl"
|
||||
:src="mediaBlobUrl"
|
||||
controls
|
||||
class="max-w-full max-h-[70vh]"
|
||||
/>
|
||||
<!-- Image Viewer -->
|
||||
<img
|
||||
v-else-if="selectedSubmission?.thumbnail_url && mediaBlobUrl"
|
||||
:src="mediaBlobUrl"
|
||||
:alt="selectedSubmission.file_name"
|
||||
class="max-w-full max-h-[70vh] object-contain"
|
||||
/>
|
||||
<!-- Fallback for other file types -->
|
||||
<div v-else class="text-center p-8">
|
||||
<p class="text-muted-foreground mb-4">Preview not available for this file type</p>
|
||||
<Button @click="handleDownload(selectedSubmission)" variant="outline">
|
||||
Download File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedSubmission?.notes" class="mt-4 p-4 bg-muted rounded-lg">
|
||||
<p class="text-sm font-semibold mb-1">Submission Notes:</p>
|
||||
<p class="text-sm whitespace-pre-wrap">{{ selectedSubmission.notes }}</p>
|
||||
</div>
|
||||
<div v-if="selectedSubmission?.latest_review" class="mt-4 p-4 border rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-sm font-semibold">Review</p>
|
||||
<Badge :variant="selectedSubmission.latest_review.decision === 'approved' ? 'default' : 'destructive'">
|
||||
{{ selectedSubmission.latest_review.decision }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mb-1">
|
||||
By {{ selectedSubmission.latest_review.reviewer_first_name }} {{ selectedSubmission.latest_review.reviewer_last_name }}
|
||||
</p>
|
||||
<p v-if="selectedSubmission.latest_review.feedback" class="text-sm whitespace-pre-wrap">
|
||||
{{ selectedSubmission.latest_review.feedback }}
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Upload } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog'
|
||||
import SubmissionCard from './SubmissionCard.vue'
|
||||
import { taskService, type Submission } from '@/services/task'
|
||||
import { useToast } from '@/components/ui/toast/use-toast'
|
||||
import { apiClient } from '@/services/api'
|
||||
|
||||
const props = defineProps<{
|
||||
taskId: number
|
||||
submissions: Submission[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submissionsUpdated: []
|
||||
}>()
|
||||
|
||||
const { toast } = useToast()
|
||||
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const uploading = ref(false)
|
||||
const submissionNotes = ref('')
|
||||
const viewerOpen = ref(false)
|
||||
const selectedSubmission = ref<Submission | null>(null)
|
||||
const mediaBlobUrl = ref<string | null>(null)
|
||||
|
||||
async function handleFileSelect(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
uploading.value = true
|
||||
try {
|
||||
await taskService.submitWork(
|
||||
props.taskId,
|
||||
file,
|
||||
submissionNotes.value || undefined
|
||||
)
|
||||
submissionNotes.value = ''
|
||||
emit('submissionsUpdated')
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Work submitted successfully'
|
||||
})
|
||||
// Reset input
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = ''
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error submitting work:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.response?.data?.detail || 'Failed to submit work',
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleView(submission: Submission) {
|
||||
selectedSubmission.value = submission
|
||||
viewerOpen.value = true
|
||||
|
||||
// Load media for viewer
|
||||
if (submission.thumbnail_url || submission.stream_url) {
|
||||
await loadMediaForViewer(submission)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMediaForViewer(submission: Submission) {
|
||||
try {
|
||||
if (submission.stream_url) {
|
||||
// Load video
|
||||
const response = await apiClient.get(submission.stream_url, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
if (mediaBlobUrl.value) {
|
||||
URL.revokeObjectURL(mediaBlobUrl.value)
|
||||
}
|
||||
mediaBlobUrl.value = URL.createObjectURL(response.data)
|
||||
} else if (submission.download_url) {
|
||||
// Load image
|
||||
const response = await apiClient.get(submission.download_url, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
if (mediaBlobUrl.value) {
|
||||
URL.revokeObjectURL(mediaBlobUrl.value)
|
||||
}
|
||||
mediaBlobUrl.value = URL.createObjectURL(response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load media:', error)
|
||||
mediaBlobUrl.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function getFileUrl(url: string | null | undefined) {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
// Remove any leading slashes and backend prefix
|
||||
const cleanUrl = url.replace(/^backend[\/\\]/, '').replace(/\\/g, '/').replace(/^\/+/, '')
|
||||
return `http://localhost:8000/${cleanUrl}`
|
||||
}
|
||||
|
||||
async function handleDownload(submission: Submission | null) {
|
||||
if (!submission?.download_url) return
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(submission.download_url, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// Create download link
|
||||
const url = URL.createObjectURL(response.data)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = submission.file_name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('Failed to download file:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user