Init Repo
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<Card class="overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div class="flex gap-4 p-4">
|
||||
<!-- Thumbnail -->
|
||||
<div class="w-32 h-24 bg-muted rounded flex items-center justify-center flex-shrink-0 relative group cursor-pointer" @click="emit('view', submission)">
|
||||
<img
|
||||
v-if="thumbnailBlobUrl"
|
||||
:src="thumbnailBlobUrl"
|
||||
:alt="submission.file_name"
|
||||
class="w-full h-full object-cover rounded"
|
||||
/>
|
||||
<div v-else class="flex flex-col items-center gap-1">
|
||||
<FileIcon class="h-8 w-8 text-muted-foreground" />
|
||||
<span class="text-xs text-muted-foreground">{{ getFileExtension(submission.file_name) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Play icon for videos -->
|
||||
<div v-if="submission.stream_url" class="absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/50 transition-colors">
|
||||
<Play class="h-8 w-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="font-semibold">Version {{ submission.version_number }}</h4>
|
||||
<Badge v-if="submission.latest_review" :variant="getReviewVariant(submission.latest_review.decision)">
|
||||
{{ submission.latest_review.decision }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{{ submission.file_name }}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="handleDownload"
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-muted-foreground space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar class="h-5 w-5">
|
||||
<AvatarImage
|
||||
:src="`https://api.dicebear.com/7.x/initials/svg?seed=${submission.user_first_name} ${submission.user_last_name}`"
|
||||
/>
|
||||
<AvatarFallback class="text-[10px]">{{ getUserInitials(submission.user_first_name, submission.user_last_name) }}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>Submitted by {{ submission.user_first_name }} {{ submission.user_last_name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ formatDateTime(submission.submitted_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="submission.notes" class="text-sm bg-muted p-2 rounded">
|
||||
<p class="font-semibold text-xs mb-1">Notes:</p>
|
||||
<p class="line-clamp-2">{{ submission.notes }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="submission.latest_review?.feedback" class="text-sm border-l-2 pl-2" :class="getReviewBorderClass(submission.latest_review.decision)">
|
||||
<p class="font-semibold text-xs mb-1">Review Feedback:</p>
|
||||
<p class="line-clamp-2">{{ submission.latest_review.feedback }}</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
By {{ submission.latest_review.reviewer_first_name }} {{ submission.latest_review.reviewer_last_name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="emit('view', submission)"
|
||||
>
|
||||
<Eye class="h-4 w-4 mr-2" />
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { FileIcon, Download, Eye, Play } from 'lucide-vue-next'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import type { Submission } from '@/services/task'
|
||||
import { apiClient } from '@/services/api'
|
||||
|
||||
const props = defineProps<{
|
||||
submission: Submission
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
view: [submission: Submission]
|
||||
}>()
|
||||
|
||||
const thumbnailBlobUrl = ref<string | null>(null)
|
||||
|
||||
function getFileExtension(filename: string): string {
|
||||
const parts = filename.split('.')
|
||||
return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : 'FILE'
|
||||
}
|
||||
|
||||
function formatDateTime(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
function getReviewVariant(decision: string): 'default' | 'destructive' {
|
||||
return decision === 'approved' ? 'default' : 'destructive'
|
||||
}
|
||||
|
||||
function getReviewBorderClass(decision: string): string {
|
||||
return decision === 'approved' ? 'border-green-500' : 'border-destructive'
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
if (props.submission.download_url) {
|
||||
const url = getThumbnailUrl(props.submission.download_url)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
function getUserInitials(firstName: string, lastName: string) {
|
||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase()
|
||||
}
|
||||
|
||||
async function loadThumbnail() {
|
||||
if (!props.submission.thumbnail_url) {
|
||||
thumbnailBlobUrl.value = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(props.submission.thumbnail_url, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// Revoke old blob URL if it exists
|
||||
if (thumbnailBlobUrl.value) {
|
||||
URL.revokeObjectURL(thumbnailBlobUrl.value)
|
||||
}
|
||||
|
||||
// Create new blob URL
|
||||
thumbnailBlobUrl.value = URL.createObjectURL(response.data)
|
||||
} catch (error) {
|
||||
console.error('Failed to load thumbnail:', error)
|
||||
thumbnailBlobUrl.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadThumbnail()
|
||||
})
|
||||
|
||||
watch(() => props.submission.id, () => {
|
||||
loadThumbnail()
|
||||
})
|
||||
|
||||
function getThumbnailUrl(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}`
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user