chore: add i18n
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTitle } from "@vueuse/core";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
|
||||
import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket";
|
||||
@@ -26,6 +28,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
provideEventPath(props.eventPath);
|
||||
|
||||
const { t } = useI18n();
|
||||
useTitle(computed(() => t("submissions.pageTitle", { game: props.gameName, scope: props.scopeName })));
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { userInfo } = storeToRefs(userStore);
|
||||
|
||||
@@ -138,12 +143,12 @@ const copyOCRText = async () => {
|
||||
if (selectedSubmissionFile.value?.ocr_raw_data) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(selectedSubmissionFile.value.ocr_raw_data);
|
||||
copyFeedback.value = "Copied!";
|
||||
copyFeedback.value = t("common.copied");
|
||||
setTimeout(() => {
|
||||
copyFeedback.value = "";
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
copyFeedback.value = "Failed to copy";
|
||||
copyFeedback.value = t("submissions.copyFailed");
|
||||
setTimeout(() => {
|
||||
copyFeedback.value = "";
|
||||
}, 2000);
|
||||
@@ -266,14 +271,14 @@ const submitCrop = async () => {
|
||||
redrawCropCanvas();
|
||||
} catch (error) {
|
||||
console.error("Error creating crop:", error);
|
||||
alert(`Error creating crop: ${error instanceof Error ? error.message : "Please try again."}`);
|
||||
alert(t("submissions.createCropError", { message: error instanceof Error ? error.message : t("common.pleaseTryAgain") }));
|
||||
} finally {
|
||||
isCreatingCrop.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCrop = async (cropId: string) => {
|
||||
if (!confirm("Delete this crop?")) return;
|
||||
if (!confirm(t("submissions.deleteCropConfirm"))) return;
|
||||
|
||||
try {
|
||||
await submissionsApiDeleteImageCrop({
|
||||
@@ -285,7 +290,7 @@ const deleteCrop = async (cropId: string) => {
|
||||
redrawCropCanvas();
|
||||
} catch (error) {
|
||||
console.error("Error deleting crop:", error);
|
||||
alert(`Error deleting crop: ${error instanceof Error ? error.message : "Please try again."}`);
|
||||
alert(t("submissions.deleteCropError", { message: error instanceof Error ? error.message : t("common.pleaseTryAgain") }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -305,7 +310,7 @@ const processFileOcr = async () => {
|
||||
selectedSubmissionFile.value.ocr_status = "processing";
|
||||
} catch (error) {
|
||||
console.error("Error processing OCR:", error);
|
||||
alert(`Error processing OCR: ${error instanceof Error ? error.message : "Please try again."}`);
|
||||
alert(t("submissions.processOcrError", { message: error instanceof Error ? error.message : t("common.pleaseTryAgain") }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -323,7 +328,7 @@ const processCropOcr = async (cropId: string) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing crop OCR:", error);
|
||||
alert(`Error processing OCR: ${error instanceof Error ? error.message : "Please try again."}`);
|
||||
alert(t("submissions.processOcrError", { message: error instanceof Error ? error.message : t("common.pleaseTryAgain") }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -352,7 +357,7 @@ const addFiles = (filesToAdd: File[]) => {
|
||||
|
||||
if (totalFiles > MAX_FILES) {
|
||||
const canAdd = MAX_FILES - uploadedFiles.value.length;
|
||||
uploadLimitExceeded.value = `Maximum ${MAX_FILES} files allowed. You can add ${canAdd} more file${canAdd !== 1 ? 's' : ''}.`;
|
||||
uploadLimitExceeded.value = t("submissions.uploadLimitExceeded", { max: MAX_FILES, count: canAdd, unit: t("common.fileWord", canAdd) });
|
||||
setTimeout(() => {
|
||||
uploadLimitExceeded.value = "";
|
||||
}, 5000);
|
||||
@@ -415,11 +420,11 @@ const submitSubmission = async () => {
|
||||
|
||||
uploadedFiles.value = [];
|
||||
notes.value = "";
|
||||
alert("Submission successful!");
|
||||
alert(t("submissions.submitSuccess"));
|
||||
await loadUserSubmissions();
|
||||
} catch (error) {
|
||||
console.error("Error submitting:", error);
|
||||
alert(`Error submitting: ${error instanceof Error ? error.message : "Please try again."}`);
|
||||
alert(t("submissions.submitError", { message: error instanceof Error ? error.message : t("common.pleaseTryAgain") }));
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
@@ -521,21 +526,21 @@ onUnmounted(() => {
|
||||
<div class="container min-w-3/4 mx-auto w-full flex items-center gap-4">
|
||||
<button @click="goHome" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-arrow-left"></i>
|
||||
Back
|
||||
{{ t("common.back") }}
|
||||
</button>
|
||||
<h1 class="text-xl font-bold">{{ gameName }} Submissions — {{ scopeName }}</h1>
|
||||
<h1 class="text-xl font-bold">{{ t("submissions.pageTitle", { game: gameName, scope: scopeName }) }}</h1>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- WebSocket Status Indicator -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div :class="['badge', wsConnected ? 'badge-success' : 'badge-warning']">
|
||||
<i :class="['mdi mr-1', wsConnected ? 'mdi-wifi' : 'mdi-wifi-off']"></i>
|
||||
{{ wsConnected ? 'Live' : 'Offline' }}
|
||||
{{ wsConnected ? t("submissions.live") : t("submissions.offline") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/api/docs" class="btn btn-xs">API docs</a>
|
||||
<a v-if="userInfo.isStaff" href="/admin" class="btn btn-xs btn-warning">Admin panel</a>
|
||||
<a href="/api/docs" class="btn btn-xs">{{ t("common.apiDocs") }}</a>
|
||||
<a v-if="userInfo.isStaff" href="/admin" class="btn btn-xs btn-warning">{{ t("common.adminPanel") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -556,7 +561,7 @@ onUnmounted(() => {
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl">{{ gameName }}</h2>
|
||||
<p class="text-base-content/70">Submitting for match: <span class="font-semibold">{{ scopeName }}</span>
|
||||
<p class="text-base-content/70">{{ t("submissions.submittingForMatch") }} <span class="font-semibold">{{ scopeName }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -566,7 +571,7 @@ onUnmounted(() => {
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-6">
|
||||
<i class="mdi mdi-cloud-upload text-primary mr-2"></i>
|
||||
Submit Results
|
||||
{{ t("submissions.submitResultsTitle") }}
|
||||
</h2>
|
||||
|
||||
<!-- Upload Area -->
|
||||
@@ -582,8 +587,8 @@ onUnmounted(() => {
|
||||
isDragover ? 'mdi-cloud-check text-primary' : 'mdi-file-upload text-base-content/50'
|
||||
]"></i>
|
||||
<div>
|
||||
<p class="font-semibold">Click to upload, drag and drop, or paste</p>
|
||||
<p class="text-sm text-base-content/70">Any file type supported • Images from clipboard welcome</p>
|
||||
<p class="font-semibold">{{ t("submissions.uploadPrompt") }}</p>
|
||||
<p class="text-sm text-base-content/70">{{ t("submissions.uploadHint") }}</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -596,12 +601,12 @@ onUnmounted(() => {
|
||||
|
||||
<!-- File Count -->
|
||||
<div v-if="uploadedFiles.length > 0" class="mb-3 text-sm text-base-content/70">
|
||||
<span class="font-medium">{{ uploadedFiles.length }}/{{ MAX_FILES }}</span> files selected
|
||||
<span class="font-medium">{{ uploadedFiles.length }}/{{ MAX_FILES }}</span> {{ t("submissions.filesSelectedSuffix") }}
|
||||
</div>
|
||||
|
||||
<!-- Uploaded Files List -->
|
||||
<div v-if="uploadedFiles.length > 0" class="mb-6">
|
||||
<p class="font-semibold mb-3">Selected Files:</p>
|
||||
<p class="font-semibold mb-3">{{ t("common.selectedFiles") }}</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(file, index) in uploadedFiles" :key="index"
|
||||
class="flex items-center gap-3 bg-base-200 p-3 rounded-lg cursor-pointer hover:bg-base-300 transition-colors group"
|
||||
@@ -624,12 +629,12 @@ onUnmounted(() => {
|
||||
<div class="form-control">
|
||||
<div class="flex-1">
|
||||
<label class="flex label">
|
||||
<span class="label-text font-medium">Notes (Optional)</span>
|
||||
<span class="label-text font-medium">{{ t("common.notesOptional") }}</span>
|
||||
<span class="label-text-alt">0/500</span>
|
||||
</label>
|
||||
|
||||
<textarea v-model="notes" class="flex textarea textarea-bordered h-24 w-full resize-none"
|
||||
placeholder="Add any notes special for this upload" maxlength="500"></textarea>
|
||||
:placeholder="t('submissions.notesPlaceholder')" maxlength="500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -638,17 +643,17 @@ onUnmounted(() => {
|
||||
<div class="flex gap-3">
|
||||
<label for="file-upload" class="btn btn-outline flex-1">
|
||||
<i class="mdi mdi-folder-open mr-2"></i>
|
||||
Choose Files
|
||||
{{ t("common.chooseFiles") }}
|
||||
</label>
|
||||
<button @click="submitSubmission" :disabled="uploadedFiles.length === 0 || isUploading"
|
||||
:class="['btn btn-primary flex-1', { 'loading': isUploading }]">
|
||||
<i v-if="!isUploading" class="mdi mdi-send mr-2"></i>
|
||||
{{ isUploading ? 'Submitting...' : 'Submit' }}
|
||||
{{ isUploading ? t("common.submitting") : t("common.submit") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-base-content/70 text-center mt-4">
|
||||
Maximum file size: 256 MB per file
|
||||
{{ t("common.maxFileSize") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -661,7 +666,7 @@ onUnmounted(() => {
|
||||
<div class="bg-gradient-to-br from-blue-600 to-blue-400 p-8 text-white rounded-t-2xl">
|
||||
<h2 class="text-3xl font-bold">
|
||||
<i class="mdi mdi-information-outline text-3xl"></i>
|
||||
Submission Guidelines
|
||||
{{ t("submissions.guidelines.title") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body p-8">
|
||||
@@ -669,11 +674,10 @@ onUnmounted(() => {
|
||||
<div>
|
||||
<h3 class="font-bold text-lg mb-2">
|
||||
<i class="mdi mdi-file-upload mr-2 text-primary"></i>
|
||||
File Upload
|
||||
{{ t("submissions.guidelines.fileUpload.title") }}
|
||||
</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Upload your results as screenshots, logs, or any other file format. You can upload multiple
|
||||
files at once.
|
||||
{{ t("submissions.guidelines.fileUpload.body") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -682,11 +686,10 @@ onUnmounted(() => {
|
||||
<div>
|
||||
<h3 class="font-bold text-lg mb-2">
|
||||
<i class="mdi mdi-note-text mr-2 text-primary"></i>
|
||||
Add Notes
|
||||
{{ t("submissions.guidelines.notes.title") }}
|
||||
</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Include any additional context about your submission. This helps reviewers understand your
|
||||
results.
|
||||
{{ t("submissions.guidelines.notes.body") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -695,11 +698,10 @@ onUnmounted(() => {
|
||||
<div>
|
||||
<h3 class="font-bold text-lg mb-2">
|
||||
<i class="mdi mdi-check-circle mr-2 text-primary"></i>
|
||||
Verification
|
||||
{{ t("submissions.guidelines.verification.title") }}
|
||||
</h3>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Your submission will be reviewed and verified. You'll receive confirmation once it's been
|
||||
processed.
|
||||
{{ t("submissions.guidelines.verification.body") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -715,7 +717,7 @@ onUnmounted(() => {
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-6">
|
||||
<i class="mdi mdi-history text-primary mr-2"></i>
|
||||
Your Submissions
|
||||
{{ t("submissions.yourSubmissions") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="isLoadingSubmissions" class="flex justify-center py-8">
|
||||
@@ -723,19 +725,19 @@ onUnmounted(() => {
|
||||
</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>
|
||||
<p class="text-base-content/70 mb-2">{{ t("common.noSubmissionsYet") }}</p>
|
||||
<p class="text-sm text-base-content/50">{{ t("submissions.noSubmissionsHint") }}</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>
|
||||
<th>{{ t("common.date") }}</th>
|
||||
<th>{{ t("common.files") }}</th>
|
||||
<th>{{ t("submissions.fileStatus") }}</th>
|
||||
<th>{{ t("submissions.ocrStatus") }}</th>
|
||||
<th>{{ t("common.notes") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -811,16 +813,16 @@ onUnmounted(() => {
|
||||
<div class="bg-base-200 p-8 rounded-lg text-center">
|
||||
<i class="mdi mdi-file-question text-6xl text-base-content/30 mb-4"></i>
|
||||
<p class="text-base-content/70">
|
||||
Preview not available for {{ selectedFile?.type || 'this file type' }}
|
||||
{{ t("submissions.previewNotAvailableFor", { type: selectedFile?.type || t("submissions.thisFileType") }) }}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/50">
|
||||
File size: {{ selectedFile ? formatFileSize(selectedFile.size) : '' }}
|
||||
{{ selectedFile ? t("submissions.fileSizeLabel", { size: formatFileSize(selectedFile.size) }) : '' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button @click="closeModal" class="btn">Close</button>
|
||||
<button @click="closeModal" class="btn">{{ t("common.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -839,7 +841,7 @@ onUnmounted(() => {
|
||||
<div class="gap-4 h-[80vh] w-full overflow-hidden flex flex-col">
|
||||
<!-- File Display -->
|
||||
<div class="flex flex-col overflow-hidden flex-[1.5]">
|
||||
<p class="text-sm text-base-content/70 mb-2">File Preview</p>
|
||||
<p class="text-sm text-base-content/70 mb-2">{{ t("submissions.filePreview") }}</p>
|
||||
<div class="flex-1 overflow-auto border border-base-300 rounded-lg">
|
||||
<!-- Image Preview with Crop Drawing (Admin Only) -->
|
||||
<div v-if="selectedSubmissionFile?.content_type.startsWith('image/')"
|
||||
@@ -860,7 +862,7 @@ onUnmounted(() => {
|
||||
<!-- Text Preview -->
|
||||
<div v-else-if="selectedSubmissionFile?.content_type.startsWith('text/')" class="p-4">
|
||||
<div class="bg-base-200 p-4 rounded font-mono text-sm whitespace-pre-wrap break-words">
|
||||
<p class="text-base-content/70">Text content preview not available. Download file to view.</p>
|
||||
<p class="text-base-content/70">{{ t("submissions.textPreviewUnavailable") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -868,7 +870,7 @@ onUnmounted(() => {
|
||||
<div v-else class="flex items-center justify-center h-full">
|
||||
<div class="text-center">
|
||||
<i class="mdi mdi-file-question text-6xl text-base-content/30 mb-4"></i>
|
||||
<p class="text-base-content/70">Preview not available</p>
|
||||
<p class="text-base-content/70">{{ t("submissions.previewNotAvailable") }}</p>
|
||||
<p class="text-sm text-base-content/50">{{ selectedSubmissionFile?.content_type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -880,7 +882,7 @@ onUnmounted(() => {
|
||||
<!-- OCR Data -->
|
||||
<div class="flex flex-col overflow-hidden flex-1">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-sm text-base-content/70 mb-2">OCR Data</p>
|
||||
<p class="text-sm text-base-content/70 mb-2">{{ t("submissions.ocrData") }}</p>
|
||||
<span :class="getOCRStatusColor(selectedSubmissionFile?.ocr_status)">
|
||||
{{ selectedSubmissionFile?.ocr_status }}
|
||||
</span>
|
||||
@@ -892,10 +894,10 @@ onUnmounted(() => {
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs font-semibold text-base-content/70">File - Extracted Text:</p>
|
||||
<p class="text-xs font-semibold text-base-content/70">{{ t("submissions.extractedText") }}</p>
|
||||
<button @click="copyOCRText" class="btn btn-xs btn-ghost gap-1">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
{{ copyFeedback || "Copy" }}
|
||||
{{ copyFeedback || t("common.copy") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -909,30 +911,29 @@ onUnmounted(() => {
|
||||
class="flex items-center justify-center py-8">
|
||||
<div class="text-center">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<p class="text-base-content/70 mt-4">OCR is being processed...</p>
|
||||
<p class="text-base-content/70 mt-4">{{ t("submissions.ocrProcessing") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="selectedSubmissionFile?.ocr_status === 'error'"
|
||||
class="flex items-center justify-center py-8">
|
||||
<div class="text-center">
|
||||
<i class="mdi mdi-alert-circle text-4xl text-error mb-2"></i>
|
||||
<p class="text-base-content/70">OCR processing failed</p>
|
||||
<p class="text-base-content/70">{{ t("submissions.ocrFailed") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!selectedSubmissionFile?.ocr_raw_data" class="flex items-center justify-center py-8">
|
||||
<div class="text-center">
|
||||
<i class="mdi mdi-clock-outline text-4xl text-base-content/30 mb-2"></i>
|
||||
<p class="text-base-content/70">Waiting for OCR processing...</p>
|
||||
<p class="text-base-content/70">{{ t("submissions.ocrWaiting") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crops OCR Data -->
|
||||
<div v-if="selectedSubmissionFile?.crops?.length" class="space-y-3 pt-4 border-t border-base-300">
|
||||
<p class="text-xs font-semibold text-base-content/70">Crop OCR Results ({{
|
||||
selectedSubmissionFile.crops.length }}):</p>
|
||||
<p class="text-xs font-semibold text-base-content/70">{{ t("submissions.cropOcrResults", { count: selectedSubmissionFile.crops.length }) }}</p>
|
||||
<div v-for="crop in selectedSubmissionFile.crops" :key="crop.id" class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-medium">{{ crop.label || `Crop (${crop.x1}, ${crop.y1})` }}</p>
|
||||
<p class="text-xs font-medium">{{ crop.label || t("submissions.cropLabelFallback", { x: crop.x1, y: crop.y1 }) }}</p>
|
||||
<span :class="getOCRStatusColor(crop.ocr_status)">{{ crop.ocr_status }}</span>
|
||||
</div>
|
||||
<div v-if="crop.ocr_status === 'processed' && crop.ocr_raw_data"
|
||||
@@ -942,15 +943,15 @@ onUnmounted(() => {
|
||||
<div v-else-if="crop.ocr_status === 'processing'"
|
||||
class="text-xs text-base-content/70 text-center py-2">
|
||||
<span class="loading loading-spinner loading-sm inline-block"></span>
|
||||
Processing...
|
||||
{{ t("common.processing") }}
|
||||
</div>
|
||||
<div v-else-if="crop.ocr_status === 'error'" class="text-xs text-error text-center py-2">
|
||||
<i class="mdi mdi-alert-circle mr-1"></i>
|
||||
Processing failed
|
||||
{{ t("submissions.cropProcessingFailed") }}
|
||||
</div>
|
||||
<div v-else class="text-xs text-base-content/50 text-center py-2">
|
||||
<i class="mdi mdi-clock-outline mr-1"></i>
|
||||
Pending
|
||||
{{ t("common.pending") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -960,13 +961,13 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Crops Data -->
|
||||
<div v-if="userInfo.isStaff" class="flex flex-col overflow-hidden flex-1">
|
||||
<p class="text-sm text-base-content/70 mb-2">Crops Data</p>
|
||||
<p class="text-sm text-base-content/70 mb-2">{{ t("submissions.cropsData") }}</p>
|
||||
<div class="flex-1 overflow-auto border border-base-300 rounded-lg p-4 bg-base-100 flex flex-col gap-4">
|
||||
<!-- Crop Drawing Hints -->
|
||||
<div>
|
||||
<p class="text-xs text-base-content/50 text-center mb-2">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
Drag on image to create crop (blue) or existing crops (green)
|
||||
{{ t("submissions.cropDrawHint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -975,40 +976,40 @@ onUnmounted(() => {
|
||||
<div class="space-y-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text text-sm">Crop Label (optional)</span>
|
||||
<span class="label-text text-sm">{{ t("submissions.cropLabelOptional") }}</span>
|
||||
</label>
|
||||
<input v-model="cropLabel" type="text" placeholder="e.g., Top-left corner"
|
||||
<input v-model="cropLabel" type="text" :placeholder="t('submissions.cropLabelPlaceholder')"
|
||||
class="input input-sm input-bordered" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="submitCrop" :disabled="isCreatingCrop" class="btn btn-sm btn-primary flex-1">
|
||||
<i v-if="!isCreatingCrop" class="mdi mdi-content-save"></i>
|
||||
{{ isCreatingCrop ? "Creating..." : "Save Crop" }}
|
||||
{{ isCreatingCrop ? t("submissions.creatingCrop") : t("submissions.saveCrop") }}
|
||||
</button>
|
||||
<button @click="cancelCrop" class="btn btn-sm btn-ghost flex-1">Cancel</button>
|
||||
<button @click="cancelCrop" class="btn btn-sm btn-ghost flex-1">{{ t("common.cancel") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crops List -->
|
||||
<div v-if="selectedSubmissionFile?.crops?.length" class="flex-1 overflow-y-auto">
|
||||
<p class="text-xs font-semibold text-base-content/70 mb-2">Crops ({{
|
||||
selectedSubmissionFile.crops.length }}):</p>
|
||||
<p class="text-xs font-semibold text-base-content/70 mb-2">{{ t("submissions.cropsCount", { count:
|
||||
selectedSubmissionFile.crops.length }) }}</p>
|
||||
<div class="space-y-1">
|
||||
<div v-for="crop in selectedSubmissionFile.crops" :key="crop.id"
|
||||
class="flex items-center justify-between p-1 rounded border border-green-200 text-xs">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium truncate">{{ crop.label || `Crop (${crop.x1}, ${crop.y1})` }}</p>
|
||||
<p class="font-medium truncate">{{ crop.label || t("submissions.cropLabelFallback", { x: crop.x1, y: crop.y1 }) }}</p>
|
||||
<p class="text-xs text-base-content/50">
|
||||
<span :class="getOCRStatusColor(crop.ocr_status)">{{ crop.ocr_status }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-1 flex-shrink-0">
|
||||
<button @click="processCropOcr(crop.id)" class="btn btn-xs btn-ghost" title="Process OCR">
|
||||
<button @click="processCropOcr(crop.id)" class="btn btn-xs btn-ghost" :title="t('submissions.processOcr')">
|
||||
<i class="mdi mdi-lightning-bolt"></i>
|
||||
</button>
|
||||
<button @click="deleteCrop(crop.id)" class="btn btn-xs btn-ghost text-error"
|
||||
title="Delete crop">
|
||||
:title="t('submissions.deleteCropTooltip')">
|
||||
<i class="mdi mdi-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1016,7 +1017,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-xs text-base-content/50 py-4">
|
||||
No crops yet
|
||||
{{ t("submissions.noCropsYet") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1026,14 +1027,14 @@ onUnmounted(() => {
|
||||
<div class="modal-action mt-4">
|
||||
<button v-if="userInfo.isStaff" @click="processFileOcr" class="btn btn-warning gap-1">
|
||||
<i class="mdi mdi-lightning-bolt"></i>
|
||||
Process OCR
|
||||
{{ t("submissions.processOcr") }}
|
||||
</button>
|
||||
<a v-if="selectedSubmissionFile?.file_url" :href="selectedSubmissionFile.file_url" download
|
||||
class="btn btn-outline">
|
||||
<i class="mdi mdi-download mr-2"></i>
|
||||
Download
|
||||
{{ t("common.download") }}
|
||||
</a>
|
||||
<button @click="closeSubmissionFileModal" class="btn">Close</button>
|
||||
<button @click="closeSubmissionFileModal" class="btn">{{ t("common.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user