chore: add i18n
This commit is contained in:
@@ -3,31 +3,31 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<i class="mdi mdi-shield-account text-2xl text-warning"></i>
|
||||
Admin Panel
|
||||
{{ t("components.adminPanel.title") }}
|
||||
</h2>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats stats-vertical lg:stats-horizontal shadow mb-6">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Submissions</div>
|
||||
<div class="stat-title">{{ t("components.adminPanel.totalSubmissions") }}</div>
|
||||
<div class="stat-value text-primary">
|
||||
{{ stats.total_submissions }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Responses</div>
|
||||
<div class="stat-title">{{ t("components.adminPanel.totalResponses") }}</div>
|
||||
<div class="stat-value text-secondary">
|
||||
{{ stats.total_responses }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Need Validation</div>
|
||||
<div class="stat-title">{{ t("components.adminPanel.needValidation") }}</div>
|
||||
<div class="stat-value text-warning">
|
||||
{{ stats.needs_validation }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Validation Rate</div>
|
||||
<div class="stat-title">{{ t("components.adminPanel.validationRate") }}</div>
|
||||
<div class="stat-value text-success">
|
||||
{{ Math.round(stats.validation_rate * 100) }}%
|
||||
</div>
|
||||
@@ -36,21 +36,21 @@
|
||||
|
||||
<button class="btn btn-sm btn-primary" @click="autoValidationResponse">
|
||||
<i class="mdi mdi-check-circle mr-1"></i>
|
||||
Auto validation for all responses
|
||||
{{ t("components.adminPanel.autoValidateAll") }}
|
||||
</button>
|
||||
|
||||
<!-- Responses Needing Validation -->
|
||||
<div v-if="responsesNeedingValidation.length > 0">
|
||||
<h3 class="text-lg font-bold mb-4">Responses Needing Validation</h3>
|
||||
<h3 class="text-lg font-bold mb-4">{{ t("components.adminPanel.responsesNeedingValidation") }}</h3>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Puzzle</th>
|
||||
<th>OCR Data</th>
|
||||
<th>Confidence</th>
|
||||
<th>Actions</th>
|
||||
<th>{{ t("common.puzzle") }}</th>
|
||||
<th>{{ t("submissions.ocrData") }}</th>
|
||||
<th>{{ t("components.adminPanel.confidence") }}</th>
|
||||
<th>{{ t("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -60,12 +60,12 @@
|
||||
>
|
||||
<td>
|
||||
<div class="font-bold">{{ response.puzzle_name }}</div>
|
||||
<div class="text-sm opacity-50">ID: {{ response.id }}</div>
|
||||
<div class="text-sm opacity-50">{{ t("common.idLabel", { id: response.id }) }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm space-y-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Cost: {{ response.cost || "-" }}</span>
|
||||
<span>{{ t("components.adminPanel.costLabel", { value: response.cost || "-" }) }}</span>
|
||||
<span
|
||||
v-if="response.ocr_confidence_cost"
|
||||
class="badge badge-xs"
|
||||
@@ -77,7 +77,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Cycles: {{ response.cycles || "-" }}</span>
|
||||
<span>{{ t("components.adminPanel.cyclesLabel", { value: response.cycles || "-" }) }}</span>
|
||||
<span
|
||||
v-if="response.ocr_confidence_cycles"
|
||||
class="badge badge-xs"
|
||||
@@ -91,7 +91,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Area: {{ response.area || "-" }}</span>
|
||||
<span>{{ t("components.adminPanel.areaLabel", { value: response.area || "-" }) }}</span>
|
||||
<span
|
||||
v-if="response.ocr_confidence_area"
|
||||
class="badge badge-xs"
|
||||
@@ -115,7 +115,7 @@
|
||||
class="btn btn-sm btn-primary mr-2"
|
||||
>
|
||||
<i class="mdi mdi-check-circle mr-1"></i>
|
||||
Validate
|
||||
{{ t("components.adminPanel.validate") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="response.id"
|
||||
@@ -123,7 +123,7 @@
|
||||
class="btn btn-sm btn-warning"
|
||||
>
|
||||
<i class="mdi mdi-check-circle mr-1"></i>
|
||||
Auto Validation
|
||||
{{ t("components.adminPanel.autoValidation") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -134,9 +134,9 @@
|
||||
|
||||
<div v-else class="text-center py-8">
|
||||
<i class="mdi mdi-check-all text-6xl text-success opacity-50"></i>
|
||||
<p class="text-lg font-medium mt-2">All responses validated!</p>
|
||||
<p class="text-lg font-medium mt-2">{{ t("components.adminPanel.allValidated") }}</p>
|
||||
<p class="text-sm opacity-70">
|
||||
No responses currently need manual validation.
|
||||
{{ t("components.adminPanel.noValidationNeeded") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@
|
||||
<!-- Validation Modal -->
|
||||
<div v-if="validationModal.show" class="modal modal-open">
|
||||
<div class="modal-box w-11/12 max-w-5xl">
|
||||
<h3 class="font-bold text-lg mb-4">Validate Response</h3>
|
||||
<h3 class="font-bold text-lg mb-4">{{ t("components.adminPanel.validateResponseTitle") }}</h3>
|
||||
|
||||
<div v-for="file in validationModal.response?.files ?? []">
|
||||
<img :src="file.file_url" />
|
||||
@@ -158,20 +158,20 @@
|
||||
<div class="font-bold">
|
||||
{{ validationModal.response.puzzle_name }}
|
||||
</div>
|
||||
<div class="text-sm">Review and correct the OCR data below</div>
|
||||
<div class="text-sm">{{ t("components.adminPanel.reviewHint") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Puzzle</span>
|
||||
<span class="label-text">{{ t("common.puzzle") }}</span>
|
||||
</label>
|
||||
<select
|
||||
v-model="validationModal.data.puzzle"
|
||||
class="select select-bordered select-sm w-full"
|
||||
>
|
||||
<option value="">Select puzzle...</option>
|
||||
<option value="">{{ t("common.selectPuzzlePlaceholder") }}</option>
|
||||
<option
|
||||
v-for="puzzle in puzzlesStore.puzzles"
|
||||
:key="puzzle.id"
|
||||
@@ -184,42 +184,42 @@
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Cost</span>
|
||||
<span class="label-text">{{ t("common.cost") }}</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="validationModal.data.validated_cost"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
:placeholder="
|
||||
validationModal.response.cost?.toString() || 'Enter cost'
|
||||
validationModal.response.cost?.toString() || t('components.adminPanel.enterCost')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Cycles</span>
|
||||
<span class="label-text">{{ t("common.cycles") }}</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="validationModal.data.validated_cycles"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
:placeholder="
|
||||
validationModal.response.cycles?.toString() || 'Enter cycles'
|
||||
validationModal.response.cycles?.toString() || t('components.adminPanel.enterCycles')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Area</span>
|
||||
<span class="label-text">{{ t("common.area") }}</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="validationModal.data.validated_area"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
:placeholder="
|
||||
validationModal.response.area?.toString() || 'Enter area'
|
||||
validationModal.response.area?.toString() || t('components.adminPanel.enterArea')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@@ -227,7 +227,7 @@
|
||||
|
||||
<div class="modal-action">
|
||||
<button @click="closeValidationModal" class="btn btn-ghost">
|
||||
Cancel
|
||||
{{ t("common.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
@click="submitValidation"
|
||||
@@ -238,7 +238,7 @@
|
||||
v-if="isValidating"
|
||||
class="loading loading-spinner loading-sm"
|
||||
></span>
|
||||
{{ isValidating ? "Validating..." : "Validate" }}
|
||||
{{ isValidating ? t("components.adminPanel.validating") : t("components.adminPanel.validate") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -253,10 +253,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { apiService } from "@/services/apiService";
|
||||
import type { PuzzleResponse } from "@/types";
|
||||
import { usePuzzlesStore } from "@/stores/puzzles";
|
||||
const puzzlesStore = usePuzzlesStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Reactive data
|
||||
const stats = ref({
|
||||
@@ -384,7 +386,7 @@ const submitValidation = async () => {
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
alert(`Validation failed: ${response.error}`);
|
||||
alert(t("components.adminPanel.validationFailed", { error: response.error }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -402,7 +404,7 @@ const submitValidation = async () => {
|
||||
closeValidationModal();
|
||||
} catch (error) {
|
||||
console.error("Validation error:", error);
|
||||
alert("Validation failed");
|
||||
alert(t("components.adminPanel.validationFailedGeneric"));
|
||||
} finally {
|
||||
isValidating.value = false;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Upload Solution Files</span>
|
||||
<span class="label-text-alt text-xs">Images or GIFs only</span>
|
||||
<span class="label-text font-medium">{{ t("components.fileUpload.title") }}</span>
|
||||
<span class="label-text-alt text-xs">{{ t("components.fileUpload.hint") }}</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
@@ -29,17 +29,17 @@
|
||||
<i class="mdi mdi-cloud-upload text-5xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-base-content/70 mb-2">Drop your files here or</p>
|
||||
<p class="text-base-content/70 mb-2">{{ t("components.fileUpload.dropHint") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="fileInput?.click()"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Choose Files
|
||||
{{ t("common.chooseFiles") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-base-content/50">
|
||||
Supported formats: JPG, PNG, GIF (max 256MB each)
|
||||
{{ t("components.fileUpload.supportedFormats") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
class="mt-1 flex items-center gap-1"
|
||||
>
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
<span class="text-xs text-info">Extracting puzzle data...</span>
|
||||
<span class="text-xs text-info">{{ t("components.fileUpload.extracting") }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="file.ocrError" class="mt-1">
|
||||
@@ -92,14 +92,14 @@
|
||||
<div v-else-if="file.ocrData" class="mt-1 space-y-1">
|
||||
<div class="text-xs flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-success">✓ OCR Complete</span>
|
||||
<span class="font-medium text-success">{{ t("components.fileUpload.ocrComplete") }}</span>
|
||||
<span
|
||||
v-if="file.ocrData.confidence"
|
||||
class="badge badge-xs"
|
||||
:class="
|
||||
getConfidenceBadgeClass(file.ocrData.confidence.overall)
|
||||
"
|
||||
:title="`Overall confidence: ${Math.round(file.ocrData.confidence.overall * 100)}%`"
|
||||
:title="t('components.fileUpload.overallConfidence', { pct: Math.round(file.ocrData.confidence.overall * 100) })"
|
||||
>
|
||||
{{ Math.round(file.ocrData.confidence.overall * 100) }}%
|
||||
</span>
|
||||
@@ -107,48 +107,48 @@
|
||||
<button
|
||||
@click="processOCR(file)"
|
||||
class="btn btn-xs btn-ghost"
|
||||
title="Retry OCR"
|
||||
:title="t('components.fileUpload.retryOcr')"
|
||||
>
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs space-y-1 bg-base-200 p-2 rounded">
|
||||
<div v-if="file.ocrData.puzzle">
|
||||
<strong>Puzzle:</strong> {{ file.ocrData.puzzle }}
|
||||
<strong>{{ t("components.fileUpload.puzzleLabel") }}</strong> {{ file.ocrData.puzzle }}
|
||||
<span
|
||||
v-if="file.ocrData.confidence?.puzzle"
|
||||
class="ml-2 opacity-60"
|
||||
:title="`Puzzle confidence: ${Math.round(file.ocrData.confidence.puzzle * 100)}%`"
|
||||
:title="t('components.fileUpload.puzzleConfidence', { pct: Math.round(file.ocrData.confidence.puzzle * 100) })"
|
||||
>
|
||||
({{ Math.round(file.ocrData.confidence.puzzle * 100) }}%)
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="file.ocrData.cost">
|
||||
<strong>Cost:</strong> {{ file.ocrData.cost }}
|
||||
<strong>{{ t("components.fileUpload.costLabel") }}</strong> {{ file.ocrData.cost }}
|
||||
<span
|
||||
v-if="file.ocrData.confidence?.cost"
|
||||
class="ml-2 opacity-60"
|
||||
:title="`Cost confidence: ${Math.round(file.ocrData.confidence.cost * 100)}%`"
|
||||
:title="t('components.fileUpload.costConfidence', { pct: Math.round(file.ocrData.confidence.cost * 100) })"
|
||||
>
|
||||
({{ Math.round(file.ocrData.confidence.cost * 100) }}%)
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="file.ocrData.cycles">
|
||||
<strong>Cycles:</strong> {{ file.ocrData.cycles }}
|
||||
<strong>{{ t("components.fileUpload.cyclesLabel") }}</strong> {{ file.ocrData.cycles }}
|
||||
<span
|
||||
v-if="file.ocrData.confidence?.cycles"
|
||||
class="ml-2 opacity-60"
|
||||
:title="`Cycles confidence: ${Math.round(file.ocrData.confidence.cycles * 100)}%`"
|
||||
:title="t('components.fileUpload.cyclesConfidence', { pct: Math.round(file.ocrData.confidence.cycles * 100) })"
|
||||
>
|
||||
({{ Math.round(file.ocrData.confidence.cycles * 100) }}%)
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="file.ocrData.area">
|
||||
<strong>Area:</strong> {{ file.ocrData.area }}
|
||||
<strong>{{ t("components.fileUpload.areaLabel") }}</strong> {{ file.ocrData.area }}
|
||||
<span
|
||||
v-if="file.ocrData.confidence?.area"
|
||||
class="ml-2 opacity-60"
|
||||
:title="`Area confidence: ${Math.round(file.ocrData.confidence.area * 100)}%`"
|
||||
:title="t('components.fileUpload.areaConfidence', { pct: Math.round(file.ocrData.confidence.area * 100) })"
|
||||
>
|
||||
({{ Math.round(file.ocrData.confidence.area * 100) }}%)
|
||||
</span>
|
||||
@@ -161,9 +161,9 @@
|
||||
<div class="alert alert-warning alert-sm">
|
||||
<i class="mdi mdi-alert-circle text-lg"></i>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Low OCR Confidence</div>
|
||||
<div class="font-medium">{{ t("components.fileUpload.lowConfidence") }}</div>
|
||||
<div class="text-xs">
|
||||
Please select the correct puzzle manually
|
||||
{{ t("components.fileUpload.selectManually") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,7 +173,7 @@
|
||||
class="select select-bordered select-sm w-full"
|
||||
@change="onManualPuzzleSelection(file)"
|
||||
>
|
||||
<option value="">Select puzzle...</option>
|
||||
<option value="">{{ t("common.selectPuzzlePlaceholder") }}</option>
|
||||
<option
|
||||
v-for="puzzle in puzzlesStore.puzzles"
|
||||
:key="puzzle.id"
|
||||
@@ -197,7 +197,7 @@
|
||||
class="btn btn-xs btn-outline"
|
||||
>
|
||||
<i class="mdi mdi-text-recognition"></i>
|
||||
Extract Puzzle Data
|
||||
{{ t("components.fileUpload.extractPuzzleData") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +210,7 @@
|
||||
@click="fileInput?.click()"
|
||||
class="btn btn-outline btn-sm"
|
||||
>
|
||||
Add More Files
|
||||
{{ t("components.fileUpload.addMoreFiles") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,11 +224,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ocrService } from "@/services/ocrService";
|
||||
import { usePuzzlesStore } from "@/stores/puzzles";
|
||||
import { useUploadsStore } from "@/stores/uploads";
|
||||
import type { SubmissionFile } from "@/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Pinia store
|
||||
const puzzlesStore = usePuzzlesStore();
|
||||
const { submissionFiles, processOCR } = useUploadsStore();
|
||||
@@ -295,7 +298,7 @@ const processFiles = async (newFiles: File[]) => {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = `Failed to process ${file.name}`;
|
||||
error.value = t("components.fileUpload.processFailed", { name: file.name });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -303,13 +306,13 @@ const processFiles = async (newFiles: File[]) => {
|
||||
const isValidFile = (file: File): boolean => {
|
||||
// Check file type
|
||||
if (!file.type.startsWith("image/")) {
|
||||
error.value = `${file.name} is not a valid image file`;
|
||||
error.value = t("components.fileUpload.invalidFileType", { name: file.name });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file size (256MB limit)
|
||||
if (file.size > 256 * 1024 * 1024) {
|
||||
error.value = `${file.name} is too large (max 256MB)`;
|
||||
error.value = t("components.fileUpload.fileTooLarge", { name: file.name });
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { marketApiCreateBet, marketApiCloseMarket, marketApiResolveMarket } from "../api";
|
||||
import { useMarketStore } from "../stores/market";
|
||||
import { useEventPath } from "../composables/useEventPath";
|
||||
@@ -15,6 +16,7 @@ const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const eventPath = useEventPath();
|
||||
const marketStore = useMarketStore();
|
||||
const { userInfo, userBets } = storeToRefs(marketStore);
|
||||
@@ -33,7 +35,7 @@ const timeRemaining = computed(() => {
|
||||
const now = new Date().getTime();
|
||||
const diff = endDate - now;
|
||||
|
||||
if (diff <= 0) return "Ended";
|
||||
if (diff <= 0) return t("components.marketCard.ended");
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
@@ -88,7 +90,7 @@ const closeMarket = async () => {
|
||||
});
|
||||
emit("refresh");
|
||||
} catch (e) {
|
||||
error.value = "Error closing market";
|
||||
error.value = t("components.marketCard.errorClosing");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -96,7 +98,7 @@ const closeMarket = async () => {
|
||||
|
||||
const resolveMarket = async () => {
|
||||
if (!selectedWinningOption.value) {
|
||||
error.value = "Please select a winning option";
|
||||
error.value = t("components.marketCard.selectWinnerRequired");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +120,7 @@ const resolveMarket = async () => {
|
||||
} else if (typeof err === 'string') {
|
||||
error.value = err;
|
||||
} else {
|
||||
error.value = "Error resolving market";
|
||||
error.value = t("components.marketCard.errorResolving");
|
||||
}
|
||||
} finally {
|
||||
resolveLoading.value = false;
|
||||
@@ -155,11 +157,11 @@ const placeBet = async () => {
|
||||
} else if (typeof err === 'string') {
|
||||
error.value = err;
|
||||
} else {
|
||||
error.value = "Failed to place bet";
|
||||
error.value = t("components.marketCard.failedToPlaceBet");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = "Error placing bet";
|
||||
error.value = t("components.marketCard.errorPlacingBet");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -193,7 +195,7 @@ const updateExistingBet = () => {
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div :class="['badge', statusColor, 'text-white']">
|
||||
{{ market.status }}
|
||||
{{ t(`common.marketStatus.${market.status}`) }}
|
||||
</div>
|
||||
<div v-if="market.multiplier > 1" class="badge badge-accent text-white font-bold">
|
||||
{{ market.multiplier }}x
|
||||
@@ -201,7 +203,7 @@ const updateExistingBet = () => {
|
||||
</div>
|
||||
<div v-if="market.status === 'open' || market.status === 'closed'" class="text-sm text-base-content/60 text-right">
|
||||
<div>{{ timeRemaining }}</div>
|
||||
<div class="text-xs">until close</div>
|
||||
<div class="text-xs">{{ t("components.marketCard.untilClose") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,10 +216,10 @@ const updateExistingBet = () => {
|
||||
<div class="flex gap-2">
|
||||
<button v-if="market.status === 'open'" @click="closeMarket" :disabled="loading" class="btn btn-sm btn-warning">
|
||||
<span v-if="loading" class="loading loading-spinner loading-sm"></span>
|
||||
<span v-else>Close Market</span>
|
||||
<span v-else>{{ t("components.marketCard.closeMarket") }}</span>
|
||||
</button>
|
||||
<button v-if="market.status === 'closed'" @click="showResolveModal = true" class="btn btn-sm btn-success">
|
||||
Resolve Market
|
||||
{{ t("components.marketCard.resolveMarketButton") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,7 +227,7 @@ const updateExistingBet = () => {
|
||||
<!-- Resolve Modal -->
|
||||
<dialog v-if="showResolveModal" class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Resolve Market - Select Winner</h3>
|
||||
<h3 class="font-bold text-lg mb-4">{{ t("components.marketCard.resolveModalTitle") }}</h3>
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
<div v-for="option in market.options" :key="option.uuid" class="form-control">
|
||||
@@ -243,11 +245,11 @@ const updateExistingBet = () => {
|
||||
|
||||
<div class="modal-action">
|
||||
<button @click="showResolveModal = false" class="btn" :disabled="resolveLoading">
|
||||
Cancel
|
||||
{{ t("common.cancel") }}
|
||||
</button>
|
||||
<button @click="resolveMarket" :disabled="!selectedWinningOption || resolveLoading" class="btn btn-primary">
|
||||
<span v-if="resolveLoading" class="loading loading-spinner loading-sm"></span>
|
||||
<span v-else>Resolve & Distribute Points</span>
|
||||
<span v-else>{{ t("components.marketCard.resolveAndDistribute") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,15 +267,15 @@ const updateExistingBet = () => {
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<span class="label-text font-medium">{{ option.text }}</span>
|
||||
<div class="text-xs text-base-content/60">
|
||||
<div>Pool: {{ option.total_bets }} pts</div>
|
||||
<div v-if="option.total_bets > 0">Multiplier: {{ getMultiplier(option).toFixed(2) }}x</div>
|
||||
<div>{{ t("components.marketCard.pool", { n: option.total_bets }) }}</div>
|
||||
<div v-if="option.total_bets > 0">{{ t("components.marketCard.multiplierLabel", { x: getMultiplier(option).toFixed(2) }) }}</div>
|
||||
</div>
|
||||
<div v-if="existingBet && existingBet.option.uuid === option.uuid" class="flex flex-col gap-1">
|
||||
<span class="badge badge-primary badge-sm w-fit">
|
||||
Your bet: {{ existingBet.amount }} pts
|
||||
{{ t("components.marketCard.yourBet", { n: existingBet.amount }) }}
|
||||
</span>
|
||||
<span v-if="option.total_bets > 0" class="badge badge-success badge-sm w-fit">
|
||||
Potential: {{ getPotentialGain(option) }} pts
|
||||
{{ t("components.marketCard.potential", { n: getPotentialGain(option) }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,12 +291,12 @@ const updateExistingBet = () => {
|
||||
<div class="form-control gap-3">
|
||||
<label class="label">
|
||||
<span class="label-text">
|
||||
<span v-if="existingBet">Increase bet (current: {{ existingBet.amount }} pts)</span>
|
||||
<span v-else>Points to bet</span>
|
||||
<span v-if="existingBet">{{ t("components.marketCard.increaseBetLabel", { n: existingBet.amount }) }}</span>
|
||||
<span v-else>{{ t("components.marketCard.pointsToBet") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<input v-model.number="betAmount" type="number"
|
||||
:placeholder="existingBet ? `Enter amount to increase by` : 'Enter points'" class="input input-bordered"
|
||||
:placeholder="existingBet ? t('components.marketCard.increaseByPlaceholder') : t('components.marketCard.enterPointsPlaceholder')" class="input input-bordered"
|
||||
min="1" :disabled="loading" />
|
||||
|
||||
<div v-if="error" class="alert alert-error">
|
||||
@@ -304,8 +306,8 @@ const updateExistingBet = () => {
|
||||
|
||||
<button @click="placeBet" :disabled="!canBet || loading" class="btn btn-primary">
|
||||
<span v-if="loading" class="loading loading-spinner loading-sm"></span>
|
||||
<span v-else-if="existingBet">Increase Bet</span>
|
||||
<span v-else>Place Bet</span>
|
||||
<span v-else-if="existingBet">{{ t("components.marketCard.increaseBet") }}</span>
|
||||
<span v-else>{{ t("components.marketCard.placeBet") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,12 +315,12 @@ const updateExistingBet = () => {
|
||||
<!-- Show existing bet (market closed/resolved) -->
|
||||
<div v-else-if="market.status !== 'open' && existingBet" class="card-body py-4 bg-base-200">
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold">Your Bet</div>
|
||||
<div class="font-semibold">{{ t("components.marketCard.yourBetTitle") }}</div>
|
||||
<div class="text-base-content/70 mt-1">
|
||||
Option: <span class="font-semibold">{{ existingBet.option.text }}</span>
|
||||
{{ t("components.marketCard.optionLabel", { text: existingBet.option.text }) }}
|
||||
</div>
|
||||
<div class="text-base-content/70">
|
||||
Amount: <span class="font-semibold">{{ existingBet.amount }} pts</span>
|
||||
{{ t("components.marketCard.amountLabel", { n: existingBet.amount }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,12 +330,12 @@ const updateExistingBet = () => {
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold"
|
||||
:class="existingBet && existingBet.option.uuid === market.winning_option.uuid ? 'text-success' : 'text-error'">
|
||||
<span v-if="existingBet && existingBet.option.uuid === market.winning_option.uuid">✓ You Won!</span>
|
||||
<span v-else-if="existingBet">✗ You Lost</span>
|
||||
<span v-else>Resolved</span>
|
||||
<span v-if="existingBet && existingBet.option.uuid === market.winning_option.uuid">{{ t("components.marketCard.won") }}</span>
|
||||
<span v-else-if="existingBet">{{ t("components.marketCard.lost") }}</span>
|
||||
<span v-else>{{ t("components.marketCard.resolved") }}</span>
|
||||
</div>
|
||||
<div class="text-base-content/70 mt-1">
|
||||
Winner: <span class="font-semibold">{{ market.winning_option.text }}</span>
|
||||
{{ t("components.marketCard.winnerLabel", { text: market.winning_option.text }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
{{ puzzle.title }}
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70 mb-2">
|
||||
by {{ puzzle.author_name }}
|
||||
{{ t("components.puzzleCard.byAuthor", { author: puzzle.author_name }) }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="badge badge-primary badge-sm">
|
||||
{{ puzzle.steam_item_id }}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-sm">ID: {{ puzzle.id }}</div>
|
||||
<div class="badge badge-ghost badge-sm">{{ t("common.idLabel", { id: puzzle.id }) }}</div>
|
||||
</div>
|
||||
|
||||
<p v-if="puzzle.description" class="text-sm text-base-content/80 mb-4">
|
||||
@@ -24,19 +24,19 @@
|
||||
|
||||
<!-- Points Factor Coefficients -->
|
||||
<div v-if="puzzle.points_factor" class="bg-base-200 p-3 rounded-lg mb-4">
|
||||
<p class="text-xs text-base-content/70 font-semibold mb-2">Points Coefficients</p>
|
||||
<p class="text-xs text-base-content/70 font-semibold mb-2">{{ t("common.pointsCoefficients") }}</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cost }}</span>
|
||||
<p class="text-xs text-base-content/70">Cost</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.cost") }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cycles }}</span>
|
||||
<p class="text-xs text-base-content/70">Cycles</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.cycles") }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.area }}</span>
|
||||
<p class="text-xs text-base-content/70">Area</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.area") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,13 +46,13 @@
|
||||
{{ tag }}
|
||||
</span>
|
||||
<span v-if="puzzle.tags.length > 3" class="badge badge-outline badge-xs">
|
||||
+{{ puzzle.tags.length - 3 }} more
|
||||
{{ t("components.puzzleCard.moreTags", { n: puzzle.tags.length - 3 }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<div class="tooltip" data-tip="View on Steam Workshop">
|
||||
<div class="tooltip" :data-tip="t('components.puzzleCard.viewOnSteam')">
|
||||
<a :href="`https://steamcommunity.com/workshop/filedetails/?id=${puzzle.steam_item_id}`" target="_blank"
|
||||
class="btn btn-ghost btn-sm btn-square">
|
||||
<i class="mdi mdi-steam text-lg"></i>
|
||||
@@ -64,17 +64,17 @@
|
||||
<!-- Responses Table -->
|
||||
<div v-if="responses && responses.length > 0" class="mt-1">
|
||||
<div class="divider">
|
||||
<span class="text-sm font-medium">Solutions ({{ responses.length }})</span>
|
||||
<span class="text-sm font-medium">{{ t("components.puzzleCard.solutionsCount", { n: responses.length }) }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cost</th>
|
||||
<th>Cycles</th>
|
||||
<th>Area</th>
|
||||
<th>Files</th>
|
||||
<th>{{ t("common.cost") }}</th>
|
||||
<th>{{ t("common.cycles") }}</th>
|
||||
<th>{{ t("common.area") }}</th>
|
||||
<th>{{ t("common.files") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -108,7 +108,7 @@
|
||||
">
|
||||
<i class="mdi mdi-information-outline text-xs"></i>
|
||||
</div>
|
||||
<div v-if="response.needs_manual_validation" class="tooltip" data-tip="Needs manual validation">
|
||||
<div v-if="response.needs_manual_validation" class="tooltip" :data-tip="t('components.puzzleCard.needsManualValidation')">
|
||||
<i class="mdi mdi-alert-circle text-xs text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,9 +124,9 @@
|
||||
class="mt-6 text-center py-4 border-2 border-dashed border-base-300 rounded-lg hover:border-primary transition-colors duration-300 cursor-pointer"
|
||||
@click="openSubmissionModal">
|
||||
<i class="mdi mdi-upload text-2xl text-base-content/40"></i>
|
||||
<p class="text-sm text-base-content/60 mt-2">No solutions yet</p>
|
||||
<p class="text-sm text-base-content/60 mt-2">{{ t("components.puzzleCard.noSolutionsYet") }}</p>
|
||||
<p class="text-xs text-base-content/40">
|
||||
Upload solutions using the submit button
|
||||
{{ t("components.puzzleCard.uploadHint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,6 +134,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { SteamCollectionItem, PuzzleResponse } from "@/types";
|
||||
import { useSubmissionsStore } from "@/stores/submissions";
|
||||
|
||||
@@ -144,5 +145,6 @@ interface Props {
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openSubmissionModal } = useSubmissionsStore();
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { apiService } from "@/services/apiService";
|
||||
import type { TournamentPuzzleResults } from "@/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
const isLoading = ref(true);
|
||||
const resultsData = ref<TournamentPuzzleResults | null>(null);
|
||||
const error = ref<string>("");
|
||||
@@ -24,7 +26,7 @@ const fetchResults = async () => {
|
||||
console.error("Error fetching results:", response.error);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Failed to fetch results";
|
||||
error.value = err instanceof Error ? err.message : t("common.fetchResultsError");
|
||||
console.error("Error fetching results:", err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
@@ -68,7 +70,7 @@ onMounted(() => {
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl flex items-center gap-2">
|
||||
<i class="mdi mdi-table text-blue-500 text-3xl"></i>
|
||||
Results by Puzzle
|
||||
{{ t("components.puzzleResults.title") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center py-12">
|
||||
@@ -81,7 +83,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-else-if="!resultsData || resultsData.results.length === 0" class="text-center py-8">
|
||||
<p class="text-base-content/70">No results available yet.</p>
|
||||
<p class="text-base-content/70">{{ t("common.noResultsYet") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-8">
|
||||
@@ -90,19 +92,19 @@ onMounted(() => {
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-bold mb-3">{{ puzzle.puzzle_title }}</h3>
|
||||
<div class="bg-base-200 p-3 rounded-lg mb-4" v-if="puzzle.points_factor">
|
||||
<p class="text-xs text-base-content/70 font-semibold mb-2">Points Coefficients</p>
|
||||
<p class="text-xs text-base-content/70 font-semibold mb-2">{{ t("common.pointsCoefficients") }}</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cost }}</span>
|
||||
<p class="text-xs text-base-content/70">Cost</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.cost") }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cycles }}</span>
|
||||
<p class="text-xs text-base-content/70">Cycles</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.cycles") }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.area }}</span>
|
||||
<p class="text-xs text-base-content/70">Area</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.area") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +112,7 @@ onMounted(() => {
|
||||
<!-- Winners GIF Display (all with same top points) -->
|
||||
<div v-if="puzzle.submissions.length > 0 && getWinnersForPuzzle(puzzle).length > 0"
|
||||
class="bg-base-200 p-4 rounded-lg">
|
||||
<p class="text-xs text-base-content/70 font-semibold mb-3 text-center">🏆 Winning Solutions</p>
|
||||
<p class="text-xs text-base-content/70 font-semibold mb-3 text-center">{{ t("components.puzzleResults.winningSolutions") }}</p>
|
||||
<div class="flex flex-wrap justify-center gap-6">
|
||||
<template v-for="submission in getWinnersForPuzzle(puzzle)"
|
||||
:key="`${puzzle.puzzle_id}-${submission.user_id}`">
|
||||
@@ -133,14 +135,14 @@ onMounted(() => {
|
||||
<table class="table table-sm table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12 text-center">Pos</th>
|
||||
<th>User</th>
|
||||
<th class="text-right">Cost</th>
|
||||
<th class="text-right">Cycles</th>
|
||||
<th class="text-right">Area</th>
|
||||
<th class="text-right font-bold">Total Pts</th>
|
||||
<th class="text-right font-bold">Total Coef</th>
|
||||
<th class="text-center">GIF</th>
|
||||
<th class="w-12 text-center">{{ t("components.puzzleResults.columns.pos") }}</th>
|
||||
<th>{{ t("components.puzzleResults.columns.user") }}</th>
|
||||
<th class="text-right">{{ t("common.cost") }}</th>
|
||||
<th class="text-right">{{ t("common.cycles") }}</th>
|
||||
<th class="text-right">{{ t("common.area") }}</th>
|
||||
<th class="text-right font-bold">{{ t("components.puzzleResults.columns.totalPts") }}</th>
|
||||
<th class="text-right font-bold">{{ t("components.puzzleResults.columns.totalCoef") }}</th>
|
||||
<th class="text-center">{{ t("components.puzzleResults.columns.gif") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -167,7 +169,7 @@ onMounted(() => {
|
||||
@click="openImageModal(submission.files[0].file_url, submission.files[0].original_filename)"
|
||||
class="btn btn-xs btn-primary gap-1">
|
||||
<i class="mdi mdi-image"></i>
|
||||
View
|
||||
{{ t("common.view") }}
|
||||
</button>
|
||||
<span v-else class="text-base-content/50">—</span>
|
||||
</td>
|
||||
@@ -177,7 +179,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-else class="p-4 bg-base-200 rounded-lg text-center text-base-content/70">
|
||||
No submissions yet
|
||||
{{ t("common.noSubmissionsYet") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,9 +203,9 @@ onMounted(() => {
|
||||
<div class="modal-action mt-4">
|
||||
<a :href="selectedImageUrl" target="_blank" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download
|
||||
{{ t("common.download") }}
|
||||
</a>
|
||||
<button @click="closeImageModal" class="btn btn-sm">Close</button>
|
||||
<button @click="closeImageModal" class="btn btn-sm">{{ t("common.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" @click="closeImageModal"></div>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
defineProps<{
|
||||
rank: number | null;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -20,6 +24,6 @@ defineProps<{
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-2xl text-base-content/50">
|
||||
No rank yet
|
||||
{{ t("common.noRankYet") }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RankBadge from "./RankBadge.vue";
|
||||
import { animationsApiResults, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo } from "@/api";
|
||||
import { useEventPath } from "@/composables/useEventPath";
|
||||
|
||||
const { t } = useI18n();
|
||||
const eventPath = useEventPath();
|
||||
|
||||
interface User {
|
||||
@@ -126,15 +128,15 @@ const clearCache = async () => {
|
||||
});
|
||||
|
||||
if (!response.error) {
|
||||
alert("Cache cleared successfully!");
|
||||
alert(t("common.cacheClearedSuccess"));
|
||||
await fetchResults();
|
||||
} else {
|
||||
const detail = (response.error as { detail?: string })?.detail;
|
||||
alert(`Error clearing cache: ${detail || "Unknown error"}`);
|
||||
alert(t("components.results.clearCacheError", { detail: detail || t("common.unknownError") }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache:", error);
|
||||
alert("Error clearing cache. Please try again.");
|
||||
alert(t("common.cacheClearError"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,12 +180,12 @@ onMounted(() => {
|
||||
<div class="bg-gradient-to-br from-purple-600 to-purple-400 p-6 text-white rounded-t-2xl">
|
||||
<h3 class="text-3xl font-bold">
|
||||
<i class="mdi mdi-trophy text-3xl"></i>
|
||||
Your Ranking
|
||||
{{ t("common.yourRanking") }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-6">
|
||||
<p class="text-sm text-base-content/70">Player</p>
|
||||
<p class="text-sm text-base-content/70">{{ t("common.player") }}</p>
|
||||
<p class="text-3xl font-bold">{{ userInfo.username }}</p>
|
||||
</div>
|
||||
|
||||
@@ -195,26 +197,26 @@ onMounted(() => {
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-base-content/70 mb-1">Current Rank</p>
|
||||
<p class="text-sm text-base-content/70 mb-1">{{ t("common.currentRank") }}</p>
|
||||
<p v-if="userInfo.rank !== null" class="text-4xl font-bold text-primary">
|
||||
<RankBadge :rank="userInfo.rank" />
|
||||
</p>
|
||||
<p v-else class="text-2xl text-base-content/50">No rank yet</p>
|
||||
<p v-else class="text-2xl text-base-content/50">{{ t("common.noRankYet") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-base-content/70 mb-1">Total Points</p>
|
||||
<p class="text-sm text-base-content/70 mb-1">{{ t("components.results.totalPoints") }}</p>
|
||||
<p class="text-2xl font-bold">{{ userInfo.totalPoints.toLocaleString() }}</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-base-content/70 mb-1">Puzzles Solved</p>
|
||||
<p class="text-sm text-base-content/70 mb-1">{{ t("components.results.puzzlesSolved") }}</p>
|
||||
<p class="text-2xl font-bold">{{ userInfo.puzzlesSolved }}</p>
|
||||
</div>
|
||||
|
||||
<button v-if="userInfo.isStaff" @click="clearCache" class="btn btn-error btn-sm w-full mt-6">
|
||||
<i class="mdi mdi-cache-clear mr-1"></i>
|
||||
Clear Cache
|
||||
{{ t("common.clearCache") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,7 +229,7 @@ onMounted(() => {
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-6">
|
||||
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
||||
General Results
|
||||
{{ t("components.results.generalResults") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center py-8">
|
||||
@@ -235,7 +237,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-else-if="!resultsData" class="text-center py-8">
|
||||
<p class="text-base-content/70">No results available yet</p>
|
||||
<p class="text-base-content/70">{{ t("common.noResultsYet") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
@@ -246,31 +248,31 @@ onMounted(() => {
|
||||
selectedTab === 'overall' ? 'tab-active' : '',
|
||||
]">
|
||||
<i class="mdi mdi-chart-line mr-2"></i>
|
||||
Overall Ranking
|
||||
{{ t("components.results.tabs.overall") }}
|
||||
</button>
|
||||
<button @click="selectedTab = 'byPuzzle'" :class="[
|
||||
'tab',
|
||||
selectedTab === 'byPuzzle' ? 'tab-active' : '',
|
||||
]">
|
||||
<i class="mdi mdi-puzzle mr-2"></i>
|
||||
By Puzzle
|
||||
{{ t("components.results.tabs.byPuzzle") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Overall Ranking -->
|
||||
<div v-show="selectedTab === 'overall'" class="space-y-4">
|
||||
<div v-if="getOverallRanking().length === 0" class="text-center py-8">
|
||||
<p class="text-base-content/70">No submissions yet</p>
|
||||
<p class="text-base-content/70">{{ t("common.noSubmissionsYet") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Player</th>
|
||||
<th class="text-right">Puzzles Solved</th>
|
||||
<th class="text-right">Total Points</th>
|
||||
<th>{{ t("common.rank") }}</th>
|
||||
<th>{{ t("common.player") }}</th>
|
||||
<th class="text-right">{{ t("components.results.puzzlesSolved") }}</th>
|
||||
<th class="text-right">{{ t("components.results.totalPoints") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -281,7 +283,7 @@ onMounted(() => {
|
||||
<td class="font-medium">
|
||||
{{ ranking.user.username }}
|
||||
<span v-if="ranking.user.is_staff" class="badge badge-warning">
|
||||
admin
|
||||
{{ t("common.staffBadge") }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">{{ ranking.puzzlesSolved }}</td>
|
||||
@@ -302,35 +304,35 @@ onMounted(() => {
|
||||
:class="['mdi mr-2', expandedPuzzleId === puzzle.id ? 'mdi-chevron-down' : 'mdi-chevron-right']"></i>
|
||||
{{ puzzle.title }}
|
||||
<span class="ml-auto badge badge-sm">
|
||||
{{ getPuzzleRanking(puzzle.id).length }} submissions
|
||||
{{ t("components.results.submissionsCount", { n: getPuzzleRanking(puzzle.id).length }) }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Expanded Details -->
|
||||
<div v-if="expandedPuzzleId === puzzle.id" class="card-body">
|
||||
<div v-if="getPuzzleRanking(puzzle.id).length === 0" class="text-center py-8">
|
||||
<p class="text-base-content/70 text-lg">No submissions yet</p>
|
||||
<p class="text-base-content/70 text-lg">{{ t("common.noSubmissionsYet") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Points Factor Info -->
|
||||
<div v-if="puzzle.points_factor" class="bg-base-200 p-4 rounded-lg">
|
||||
<p class="text-sm text-base-content/70 mb-3 font-semibold">Points Coefficients</p>
|
||||
<p class="text-sm text-base-content/70 mb-3 font-semibold">{{ t("common.pointsCoefficients") }}</p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="text-center">
|
||||
<span class="text-2xl font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cost
|
||||
}}</span>
|
||||
<p class="text-xs text-base-content/70">Cost</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.cost") }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="text-2xl font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cycles
|
||||
}}</span>
|
||||
<p class="text-xs text-base-content/70">Cycles</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.cycles") }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="text-2xl font-bold text-primary"><small>x</small>{{ puzzle.points_factor.area
|
||||
}}</span>
|
||||
<p class="text-xs text-base-content/70">Area</p>
|
||||
<p class="text-xs text-base-content/70">{{ t("common.area") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,41 +343,41 @@ onMounted(() => {
|
||||
class="card bg-base-200">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-xs text-base-content/70 font-bold">
|
||||
{{ index === 0 ? '🏆 1st Place' : index === 1 ? '🥈 2nd Place' : '🥉 3rd Place' }}
|
||||
{{ index === 0 ? t("components.results.podium.first") : index === 1 ? t("components.results.podium.second") : t("components.results.podium.third") }}
|
||||
</div>
|
||||
<h4 class="font-bold text-lg">
|
||||
{{ response.user.username }}
|
||||
<span v-if="response.user.is_staff" class="badge badge-warning">
|
||||
admin
|
||||
{{ t("common.staffBadge") }}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span>Cost<span v-if="puzzle.points_factor" class="text-xs text-base-content/60">
|
||||
<span>{{ t("common.cost") }}<span v-if="puzzle.points_factor" class="text-xs text-base-content/60">
|
||||
(x{{ puzzle.points_factor.cost }})
|
||||
</span></span>
|
||||
<span class="badge badge-sm">{{ response.cost || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Cycles<span v-if="puzzle.points_factor" class="text-xs text-base-content/60">
|
||||
<span>{{ t("common.cycles") }}<span v-if="puzzle.points_factor" class="text-xs text-base-content/60">
|
||||
(x{{ puzzle.points_factor.cycles }})
|
||||
</span></span>
|
||||
<span class="badge badge-sm">{{ response.cycles || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Area<span v-if="puzzle.points_factor" class="text-xs text-base-content/60">
|
||||
<span>{{ t("common.area") }}<span v-if="puzzle.points_factor" class="text-xs text-base-content/60">
|
||||
(x{{ puzzle.points_factor.area }})
|
||||
</span></span>
|
||||
<span class="badge badge-sm">{{ response.area || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between pt-2 border-t">
|
||||
<span>Total (with coef.)</span>
|
||||
<span>{{ t("components.results.totalWithCoef") }}</span>
|
||||
<span class="badge badge-sm">{{ response.points || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between pt-2 border-t">
|
||||
<span class="font-bold">Points</span>
|
||||
<span class="badge badge-primary">{{ response.rank_points }} pts</span>
|
||||
<span class="font-bold">{{ t("common.points") }}</span>
|
||||
<span class="badge badge-primary">{{ response.rank_points }} {{ t("common.pts") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,25 +389,25 @@ onMounted(() => {
|
||||
<table class="table table-zebra w-full table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12">Rank</th>
|
||||
<th>Player</th>
|
||||
<th class="w-12">{{ t("common.rank") }}</th>
|
||||
<th>{{ t("common.player") }}</th>
|
||||
<th class="text-center">
|
||||
Cost
|
||||
{{ t("common.cost") }}
|
||||
<span v-if="puzzle.points_factor" class="text-xs text-base-content/60 block">(x{{
|
||||
puzzle.points_factor.cost }})</span>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Cycles
|
||||
{{ t("common.cycles") }}
|
||||
<span v-if="puzzle.points_factor" class="text-xs text-base-content/60 block">(x{{
|
||||
puzzle.points_factor.cycles }})</span>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Area
|
||||
{{ t("common.area") }}
|
||||
<span v-if="puzzle.points_factor" class="text-xs text-base-content/60 block">(x{{
|
||||
puzzle.points_factor.area }})</span>
|
||||
</th>
|
||||
<th class="text-center">Total (with coef.)</th>
|
||||
<th class="text-right">Points</th>
|
||||
<th class="text-center">{{ t("components.results.totalWithCoef") }}</th>
|
||||
<th class="text-right">{{ t("common.points") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-xl mb-6">
|
||||
<i class="mdi mdi-check-circle text-2xl text-primary"></i>
|
||||
Submit Solution
|
||||
{{ t("common.submitSolution") }}
|
||||
</h2>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
@@ -15,7 +15,7 @@
|
||||
<i class="mdi mdi-information-outline text-xl"></i>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold">
|
||||
Detected Puzzles ({{ Object.keys(responsesByPuzzle).length }})
|
||||
{{ t("components.submissionForm.detectedPuzzles", { n: Object.keys(responsesByPuzzle).length }) }}
|
||||
</h4>
|
||||
<div class="text-sm space-y-1 mt-1">
|
||||
<div
|
||||
@@ -25,7 +25,7 @@
|
||||
>
|
||||
<span>{{ puzzleName }}</span>
|
||||
<span class="badge badge-ghost badge-sm ml-2"
|
||||
>{{ data.files.length }} file(s)</span
|
||||
>{{ t("common.fileCount", data.files.length) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,11 +42,9 @@
|
||||
>
|
||||
<i class="mdi mdi-alert-circle text-xl"></i>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold">Manual Puzzle Selection Required</div>
|
||||
<div class="font-bold">{{ t("components.submissionForm.manualSelectionRequired") }}</div>
|
||||
<div class="text-sm">
|
||||
{{ submissionFilesNeedingManualSelection.length }} file(s) have
|
||||
low OCR confidence for puzzle names. Please select the correct
|
||||
puzzle for each file before submitting.
|
||||
{{ t("components.submissionForm.manualSelectionBody", { n: submissionFilesNeedingManualSelection.length }) }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -54,7 +52,7 @@
|
||||
@click="processLowConfidenceOCRFiles"
|
||||
>
|
||||
<span class="mdi mdi-reload text-2xl"></span>
|
||||
Retry OCR on low confidence puzzle
|
||||
{{ t("components.submissionForm.retryOcr") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,13 +61,13 @@
|
||||
<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">{{ notesLength }}/500</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="notes"
|
||||
class="flex textarea textarea-bordered h-24 w-full resize-none"
|
||||
placeholder="Add any notes about your solution, approach, or interesting findings..."
|
||||
:placeholder="t('components.submissionForm.notesPlaceholder')"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
@@ -86,16 +84,12 @@
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium"
|
||||
>Request manual validation</span
|
||||
>{{ t("components.submissionForm.requestManualValidation") }}</span
|
||||
>
|
||||
<div class="label-text-alt text-xs opacity-70 mt-1">
|
||||
Check this if you want an admin to manually review your
|
||||
submission, even if OCR confidence is high.
|
||||
{{ t("components.submissionForm.manualValidationHint") }}
|
||||
<br />
|
||||
<em
|
||||
>Note: This will be automatically checked if any OCR
|
||||
confidence is below 80%.</em
|
||||
>
|
||||
<em>{{ t("components.submissionForm.manualValidationNote") }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
@@ -108,12 +102,11 @@
|
||||
v-if="isSubmitting"
|
||||
class="loading loading-spinner loading-sm"
|
||||
></span>
|
||||
<span v-if="isSubmitting">Submitting...</span>
|
||||
<span v-if="isSubmitting">{{ t("common.submitting") }}</span>
|
||||
<span v-else-if="submissionFilesNeedingManualSelection.length > 0">
|
||||
Select Puzzles ({{ submissionFilesNeedingManualSelection.length }}
|
||||
remaining)
|
||||
{{ t("components.submissionForm.selectPuzzlesRemaining", { n: submissionFilesNeedingManualSelection.length }) }}
|
||||
</span>
|
||||
<span v-else>Submit Solution</span>
|
||||
<span v-else>{{ t("common.submitSolution") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -123,6 +116,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import FileUpload from "@/components/FileUpload.vue";
|
||||
import type { SteamCollectionItem, SubmissionFile } from "@/types";
|
||||
import { useUploadsStore } from "@/stores/uploads";
|
||||
@@ -136,6 +130,7 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const uploadsStore = useUploadsStore();
|
||||
const {
|
||||
submissionFiles,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { apiService } from "@/services/apiService";
|
||||
import type { TournamentPuzzleResults } from "@/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
const isLoading = ref(true);
|
||||
const resultsData = ref<TournamentPuzzleResults | null>(null);
|
||||
const error = ref<string>("");
|
||||
@@ -19,7 +21,7 @@ const fetchResults = async () => {
|
||||
console.error("Error fetching results:", response.error);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Failed to fetch results";
|
||||
error.value = err instanceof Error ? err.message : t("common.fetchResultsError");
|
||||
console.error("Error fetching results:", err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
@@ -60,7 +62,7 @@ onMounted(() => {
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl flex items-center gap-2">
|
||||
<i class="mdi mdi-podium text-yellow-500 text-3xl"></i>
|
||||
Top 10 Users
|
||||
{{ t("components.topUsersLeaderboard.title") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center py-12">
|
||||
@@ -73,7 +75,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-else-if="getTop10Users().length === 0" class="text-center py-8">
|
||||
<p class="text-base-content/70">No results available yet.</p>
|
||||
<p class="text-base-content/70">{{ t("common.noResultsYet") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3">
|
||||
@@ -87,7 +89,7 @@ onMounted(() => {
|
||||
{{ index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}` }}
|
||||
</div>
|
||||
<div class="font-semibold text-sm mb-2 truncate">{{ user.username }}</div>
|
||||
<div class="text-2xl font-bold text-primary">{{ user.total_points }} pts</div>
|
||||
<div class="text-2xl font-bold text-primary">{{ user.total_points }} {{ t("common.pts") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMarketStore } from "../stores/market";
|
||||
|
||||
defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const marketStore = useMarketStore();
|
||||
const { userBets } = storeToRefs(marketStore);
|
||||
const loading = computed(() => marketStore.isLoading);
|
||||
@@ -49,7 +51,7 @@ const totalWinnings = computed(() => {
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="userBets.length === 0" class="alert">
|
||||
<i class="mdi mdi-information mr-2"></i>
|
||||
<span>You haven't placed any bets yet</span>
|
||||
<span>{{ t("components.userBets.empty") }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
@@ -57,27 +59,27 @@ const totalWinnings = computed(() => {
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="stat bg-base-100 rounded-lg shadow">
|
||||
<div class="stat-title text-sm">Total Bets</div>
|
||||
<div class="stat-title text-sm">{{ t("components.userBets.totalBets") }}</div>
|
||||
<div class="stat-value text-2xl">{{ userBets.length }}</div>
|
||||
<div class="stat-desc text-xs">{{ totalBetAmount }} points</div>
|
||||
<div class="stat-desc text-xs">{{ totalBetAmount }} {{ t("common.pointsUnit") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 rounded-lg shadow">
|
||||
<div class="stat-title text-sm">Active</div>
|
||||
<div class="stat-title text-sm">{{ t("components.userBets.active") }}</div>
|
||||
<div class="stat-value text-2xl text-info">{{ openBets.length }}</div>
|
||||
<div class="stat-desc text-xs">Waiting for result</div>
|
||||
<div class="stat-desc text-xs">{{ t("components.userBets.waitingForResult") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 rounded-lg shadow">
|
||||
<div class="stat-title text-sm">Won</div>
|
||||
<div class="stat-title text-sm">{{ t("components.userBets.won") }}</div>
|
||||
<div class="stat-value text-2xl text-success">{{ winningBets.length }}</div>
|
||||
<div class="stat-desc text-xs text-success">+{{ totalWinnings }} points</div>
|
||||
<div class="stat-desc text-xs text-success">{{ t("components.userBets.wonPointsLabel", { n: totalWinnings }) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 rounded-lg shadow">
|
||||
<div class="stat-title text-sm">Lost</div>
|
||||
<div class="stat-title text-sm">{{ t("components.userBets.lost") }}</div>
|
||||
<div class="stat-value text-2xl text-error">{{ losingBets.length }}</div>
|
||||
<div class="stat-desc text-xs">Better luck next time</div>
|
||||
<div class="stat-desc text-xs">{{ t("components.userBets.betterLuck") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +89,7 @@ const totalWinnings = computed(() => {
|
||||
<div v-if="openBets.length > 0">
|
||||
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
|
||||
<i class="mdi mdi-progress-clock text-info"></i>
|
||||
Active Bets ({{ openBets.length }})
|
||||
{{ t("components.userBets.activeBets", { n: openBets.length }) }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
@@ -100,13 +102,13 @@ const totalWinnings = computed(() => {
|
||||
<div class="flex-1">
|
||||
<h4 class="font-semibold text-lg">{{ bet.market?.title }}</h4>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Bet on: <span class="font-medium">{{ bet.option.text }}</span>
|
||||
{{ t("components.userBets.betOn", { option: bet.option.text }) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-bold">{{ bet.amount }} pts</div>
|
||||
<div class="text-lg font-bold">{{ bet.amount }} {{ t("common.pts") }}</div>
|
||||
<div class="text-xs text-base-content/60 mt-1">
|
||||
Status: <span class="badge badge-info badge-sm">{{ bet.market?.status }}</span>
|
||||
{{ t("components.userBets.statusLabel") }} <span class="badge badge-info badge-sm">{{ bet.market?.status ? t(`common.marketStatus.${bet.market.status}`) : '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,7 +122,7 @@ const totalWinnings = computed(() => {
|
||||
<button @click="isWonBetsExpanded = !isWonBetsExpanded" class="text-xl font-bold mb-4 flex items-center gap-2 cursor-pointer hover:opacity-70 transition-opacity">
|
||||
<i :class="['mdi', isWonBetsExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right']"></i>
|
||||
<i class="mdi mdi-check-circle text-success"></i>
|
||||
Won Bets ({{ winningBets.length }})
|
||||
{{ t("components.userBets.wonBets", { n: winningBets.length }) }}
|
||||
</button>
|
||||
<div v-if="isWonBetsExpanded" class="space-y-4">
|
||||
<div
|
||||
@@ -133,11 +135,11 @@ const totalWinnings = computed(() => {
|
||||
<div class="flex-1">
|
||||
<h4 class="font-semibold text-lg">{{ bet.market?.title }}</h4>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Correct! You bet on: <span class="font-medium text-success">{{ bet.option.text }}</span>
|
||||
{{ t("components.userBets.correctBetOn", { option: bet.option.text }) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-bold text-success">+{{ bet.amount }} pts</div>
|
||||
<div class="text-lg font-bold text-success">{{ t("components.userBets.wonPointsLabel", { n: bet.amount }) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,7 +151,7 @@ const totalWinnings = computed(() => {
|
||||
<div v-if="losingBets.length > 0">
|
||||
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
|
||||
<i class="mdi mdi-close-circle text-error"></i>
|
||||
Lost Bets ({{ losingBets.length }})
|
||||
{{ t("components.userBets.lostBets", { n: losingBets.length }) }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
@@ -162,14 +164,14 @@ const totalWinnings = computed(() => {
|
||||
<div class="flex-1">
|
||||
<h4 class="font-semibold text-lg">{{ bet.market?.title }}</h4>
|
||||
<p class="text-sm text-base-content/70">
|
||||
You bet on: <span class="font-medium">{{ bet.option.text }}</span>
|
||||
{{ t("components.userBets.youBetOn", { option: bet.option.text }) }}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/60 mt-1">
|
||||
Winner: <span class="font-medium">{{ bet.market?.winning_option?.text }}</span>
|
||||
{{ t("components.userBets.winnerLabel", { option: bet.market?.winning_option?.text }) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-bold text-error">-{{ bet.amount }} pts</div>
|
||||
<div class="text-lg font-bold text-error">-{{ bet.amount }} {{ t("common.pts") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { apiService } from "@/services/apiService";
|
||||
import type { TournamentSubmissions, WinnerResponse, PuzzleSubmissions } from "@/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
const isLoading = ref(true);
|
||||
const submissionsData = ref<TournamentSubmissions | null>(null);
|
||||
const error = ref<string>("");
|
||||
@@ -24,7 +26,7 @@ const fetchSubmissions = async () => {
|
||||
console.error("Error fetching submissions:", response.error);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Failed to fetch submissions";
|
||||
error.value = err instanceof Error ? err.message : t("components.winners.fetchError");
|
||||
console.error("Error fetching submissions:", err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
@@ -90,7 +92,7 @@ onMounted(() => {
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl flex items-center gap-2">
|
||||
<i class="mdi mdi-trophy text-yellow-500 text-3xl"></i>
|
||||
Top Submissions
|
||||
{{ t("components.winners.title") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center py-12">
|
||||
@@ -103,21 +105,21 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-else-if="flattenedRows.length === 0" class="text-center py-8">
|
||||
<p class="text-base-content/70">No results available yet.</p>
|
||||
<p class="text-base-content/70">{{ t("common.noResultsYet") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Puzzle</th>
|
||||
<th>User</th>
|
||||
<th class="text-right">Cost</th>
|
||||
<th class="text-right">Cycles</th>
|
||||
<th class="text-right">Area</th>
|
||||
<th class="text-right">Total Pts</th>
|
||||
<th class="text-right">Total Coef</th>
|
||||
<th class="text-center">GIF</th>
|
||||
<th>{{ t("components.winners.columns.puzzle") }}</th>
|
||||
<th>{{ t("components.winners.columns.user") }}</th>
|
||||
<th class="text-right">{{ t("common.cost") }}</th>
|
||||
<th class="text-right">{{ t("common.cycles") }}</th>
|
||||
<th class="text-right">{{ t("common.area") }}</th>
|
||||
<th class="text-right">{{ t("components.winners.columns.totalPts") }}</th>
|
||||
<th class="text-right">{{ t("components.winners.columns.totalCoef") }}</th>
|
||||
<th class="text-center">{{ t("components.winners.columns.gif") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -133,7 +135,7 @@ onMounted(() => {
|
||||
<button v-if="row.files.length > 0" @click="openImageModal(row.files[0].url, row.files[0].name)"
|
||||
class="btn btn-xs btn-primary gap-1">
|
||||
<i class="mdi mdi-image"></i>
|
||||
View
|
||||
{{ t("common.view") }}
|
||||
</button>
|
||||
<span v-else class="text-base-content/50">—</span>
|
||||
</td>
|
||||
@@ -161,9 +163,9 @@ onMounted(() => {
|
||||
<div class="modal-action mt-4">
|
||||
<a :href="selectedImageUrl" target="_blank" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-download"></i>
|
||||
Download
|
||||
{{ t("common.download") }}
|
||||
</a>
|
||||
<button @click="closeImageModal" class="btn btn-sm">Close</button>
|
||||
<button @click="closeImageModal" class="btn btn-sm">{{ t("common.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" @click="closeImageModal"></div>
|
||||
|
||||
Reference in New Issue
Block a user