fix(submissions): fix upload + bucket path
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { submissionsApiCreateSubmission, submissionsApiListUserSubmissions } from "@/api";
|
||||
|
||||
interface Props {
|
||||
gamePath?: string;
|
||||
@@ -27,6 +28,8 @@ const notes = ref("");
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const filePreviewUrl = ref<string>("");
|
||||
const isModalOpen = ref(false);
|
||||
const userSubmissions = ref<any[]>([]);
|
||||
const isLoadingSubmissions = ref(false);
|
||||
|
||||
const openFilePreview = async (file: File) => {
|
||||
selectedFile.value = file;
|
||||
@@ -65,13 +68,13 @@ const getGameHeaderImage = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const getFileIcon = (file: File | null) => {
|
||||
const getFileIcon = (file: File | null | { type?: string }) => {
|
||||
if (!file) return "mdi-file";
|
||||
const type = file.type;
|
||||
if (type.startsWith("image/")) return "mdi-image";
|
||||
const type = (file as any).type;
|
||||
if (type?.startsWith("image/")) return "mdi-image";
|
||||
if (type === "application/pdf") return "mdi-file-pdf";
|
||||
if (type.startsWith("text/")) return "mdi-file-document";
|
||||
if (type.includes("video")) return "mdi-video";
|
||||
if (type?.startsWith("text/")) return "mdi-file-document";
|
||||
if (type?.includes("video")) return "mdi-video";
|
||||
return "mdi-file";
|
||||
};
|
||||
|
||||
@@ -113,36 +116,18 @@ const submitSubmission = async () => {
|
||||
|
||||
isUploading.value = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
// Add files
|
||||
uploadedFiles.value.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
await submissionsApiCreateSubmission({
|
||||
query: { game_path: props.gamePath },
|
||||
body: {
|
||||
files: uploadedFiles.value,
|
||||
notes: notes.value || null,
|
||||
},
|
||||
});
|
||||
|
||||
// Add form fields
|
||||
formData.append("notes", notes.value);
|
||||
|
||||
// Build URL with query parameters
|
||||
const url = new URL("/api/submissions/create", window.location.origin);
|
||||
url.searchParams.append("game_path", props.gamePath);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMsg = Array.isArray(errorData.detail)
|
||||
? errorData.detail.map((e: any) => e.msg).join(", ")
|
||||
: errorData.detail || "Failed to submit";
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
uploadedFiles.value = [];
|
||||
notes.value = "";
|
||||
alert("Submission successful!");
|
||||
await loadUserSubmissions();
|
||||
} catch (error) {
|
||||
console.error("Error submitting:", error);
|
||||
alert(`Error submitting: ${error instanceof Error ? error.message : "Please try again."}`);
|
||||
@@ -163,8 +148,46 @@ const formatFileSize = (bytes: number) => {
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const getOCRStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "processed":
|
||||
return "badge badge-success";
|
||||
case "processing":
|
||||
return "badge badge-warning";
|
||||
case "to_process":
|
||||
return "badge badge-info";
|
||||
case "error":
|
||||
return "badge badge-error";
|
||||
default:
|
||||
return "badge";
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserSubmissions = async () => {
|
||||
if (!props.gamePath) return;
|
||||
|
||||
isLoadingSubmissions.value = true;
|
||||
try {
|
||||
const { data } = await submissionsApiListUserSubmissions({
|
||||
query: { game_path: props.gamePath },
|
||||
});
|
||||
if (data && Array.isArray(data)) {
|
||||
userSubmissions.value = data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading submissions:", error);
|
||||
} finally {
|
||||
isLoadingSubmissions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
userStore.loadUserData();
|
||||
loadUserSubmissions();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -188,7 +211,23 @@ onMounted(() => {
|
||||
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Right Column: Upload -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="lg:col-span-1 flex flex-col gap-8 order-1 lg:order-2">
|
||||
<!-- Game Card -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<figure v-if="getGameHeaderImage()" class="relative h-48 bg-base-300 overflow-hidden">
|
||||
<img :src="getGameHeaderImage()!" :alt="gameName" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 bg-black/30"></div>
|
||||
</figure>
|
||||
<figure v-else class="relative h-48 bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center">
|
||||
<i class="mdi mdi-gamepad-variant text-6xl text-white opacity-30"></i>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl">{{ gameName }}</h2>
|
||||
<p class="text-base-content/70">Submit your best results for this game</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Card -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-6">
|
||||
@@ -269,22 +308,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Left Column: Info -->
|
||||
<div class="lg:col-span-1 flex flex-col gap-8">
|
||||
<!-- Game Card -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<figure v-if="getGameHeaderImage()" class="relative h-48 bg-base-300 overflow-hidden">
|
||||
<img :src="getGameHeaderImage()!" :alt="gameName" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 bg-black/30"></div>
|
||||
</figure>
|
||||
<figure v-else class="relative h-48 bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center">
|
||||
<i class="mdi mdi-gamepad-variant text-6xl text-white opacity-30"></i>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl">{{ gameName }}</h2>
|
||||
<p class="text-base-content/70">Submit your best results for this game</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-1 order-2 lg:order-1">
|
||||
<!-- Guidelines Card -->
|
||||
<div class="card bg-base-100 shadow-lg sticky top-8">
|
||||
<div class="bg-gradient-to-br from-blue-600 to-blue-400 p-8 text-white rounded-t-2xl">
|
||||
@@ -347,6 +371,77 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Submissions Section -->
|
||||
<div class="container min-w-3/4 mx-auto px-4 py-8">
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-6">
|
||||
<i class="mdi mdi-history text-primary mr-2"></i>
|
||||
Your Submissions
|
||||
</h2>
|
||||
|
||||
<div v-if="isLoadingSubmissions" class="flex justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="userSubmissions.length === 0" class="text-center py-8">
|
||||
<p class="text-base-content/70 mb-2">No submissions yet</p>
|
||||
<p class="text-sm text-base-content/50">Upload your first submission above to get started</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Files</th>
|
||||
<th>File Status</th>
|
||||
<th>OCR Status</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="submission in userSubmissions" :key="submission.id" class="hover">
|
||||
<td class="text-sm">{{ formatDate(submission.created_at) }}</td>
|
||||
<td class="text-sm">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div v-for="file in submission.files" :key="file.id" class="flex items-center gap-2">
|
||||
<i :class="['mdi text-primary text-sm', getFileIcon({ type: file.content_type })]"></i>
|
||||
<a :href="file.file_url" target="_blank" class="link link-primary truncate max-w-xs">
|
||||
{{ file.original_filename }}
|
||||
</a>
|
||||
<span class="text-xs text-base-content/50">({{ formatFileSize(file.file_size) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div v-for="file in submission.files" :key="file.id" class="text-xs">
|
||||
{{ file.content_type }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div v-for="file in submission.files" :key="file.id">
|
||||
<span :class="getOCRStatusColor(file.ocr_status)">{{ file.ocr_status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm text-base-content/70">
|
||||
<span v-if="submission.notes" :title="submission.notes" class="truncate block max-w-xs">
|
||||
{{ submission.notes }}
|
||||
</span>
|
||||
<span v-else class="text-base-content/50">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Preview Modal -->
|
||||
<div :class="['modal', { 'modal-open': isModalOpen }]">
|
||||
<div class="modal-box w-11/12 max-w-4xl">
|
||||
|
||||
Reference in New Issue
Block a user