feat(opus): add coef subcards
This commit is contained in:
parent
779393106d
commit
f774ff3340
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="card bg-base-100 shadow-lg hover:shadow-2xl transition-shadow duration-300"
|
||||||
class="card bg-base-100 shadow-lg hover:shadow-2xl transition-shadow duration-300"
|
:class="responses?.length == 0 ? 'shadow-red-900' : 'shadow-primary-300'">
|
||||||
:class="responses?.length == 0 ? 'shadow-red-900' : 'shadow-primary-300'"
|
|
||||||
>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="card-title text-lg font-bold">{{ puzzle.title }}</h3>
|
<h3 class="card-title text-lg font-bold" :class="responses?.length == 0 ? 'text-error' : 'text-primary'">
|
||||||
|
{{ puzzle.title }}
|
||||||
|
</h3>
|
||||||
<p class="text-sm text-base-content/70 mb-2">
|
<p class="text-sm text-base-content/70 mb-2">
|
||||||
by {{ puzzle.author_name }}
|
by {{ puzzle.author_name }}
|
||||||
</p>
|
</p>
|
||||||
@ -18,28 +18,34 @@
|
|||||||
<div class="badge badge-ghost badge-sm">ID: {{ puzzle.id }}</div>
|
<div class="badge badge-ghost badge-sm">ID: {{ puzzle.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p v-if="puzzle.description" class="text-sm text-base-content/80 mb-4">
|
||||||
v-if="puzzle.description"
|
|
||||||
class="text-sm text-base-content/80 mb-4"
|
|
||||||
>
|
|
||||||
{{ puzzle.description }}
|
{{ puzzle.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<!-- Points Factor Coefficients -->
|
||||||
v-if="puzzle.tags && puzzle.tags.length > 0"
|
<div v-if="puzzle.points_factor" class="bg-base-200 p-3 rounded-lg mb-4">
|
||||||
class="flex flex-wrap gap-1 mb-4"
|
<p class="text-xs text-base-content/70 font-semibold mb-2">Points Coefficients</p>
|
||||||
>
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<span
|
<div class="text-center">
|
||||||
v-for="tag in puzzle.tags.slice(0, 3)"
|
<span class="font-bold text-primary"><small>x</small>{{ puzzle.points_factor.cost }}</span>
|
||||||
:key="tag"
|
<p class="text-xs text-base-content/70">Cost</p>
|
||||||
class="badge badge-outline badge-xs"
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="puzzle.tags && puzzle.tags.length > 0" class="flex flex-wrap gap-1 mb-4">
|
||||||
|
<span v-for="tag in puzzle.tags.slice(0, 3)" :key="tag" class="badge badge-outline badge-xs">
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span v-if="puzzle.tags.length > 3" class="badge badge-outline badge-xs">
|
||||||
v-if="puzzle.tags.length > 3"
|
|
||||||
class="badge badge-outline badge-xs"
|
|
||||||
>
|
|
||||||
+{{ puzzle.tags.length - 3 }} more
|
+{{ puzzle.tags.length - 3 }} more
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -47,11 +53,8 @@
|
|||||||
|
|
||||||
<div class="flex flex-col items-end gap-2">
|
<div class="flex flex-col items-end gap-2">
|
||||||
<div class="tooltip" data-tip="View on Steam Workshop">
|
<div class="tooltip" data-tip="View on Steam Workshop">
|
||||||
<a
|
<a :href="`https://steamcommunity.com/workshop/filedetails/?id=${puzzle.steam_item_id}`" target="_blank"
|
||||||
:href="`https://steamcommunity.com/workshop/filedetails/?id=${puzzle.steam_item_id}`"
|
class="btn btn-ghost btn-sm btn-square">
|
||||||
target="_blank"
|
|
||||||
class="btn btn-ghost btn-sm btn-square"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-steam text-lg"></i>
|
<i class="mdi mdi-steam text-lg"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -59,11 +62,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responses Table -->
|
<!-- Responses Table -->
|
||||||
<div v-if="responses && responses.length > 0" class="mt-6">
|
<div v-if="responses && responses.length > 0" class="mt-1">
|
||||||
<div class="divider">
|
<div class="divider">
|
||||||
<span class="text-sm font-medium"
|
<span class="text-sm font-medium">Solutions ({{ responses.length }})</span>
|
||||||
>Solutions ({{ responses.length }})</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -77,34 +78,21 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr v-for="response in responses" :key="response.id" class="hover">
|
||||||
v-for="response in responses"
|
|
||||||
:key="response.id"
|
|
||||||
class="hover"
|
|
||||||
>
|
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span v-if="response.final_cost || response.cost" class="badge badge-success badge-xs">
|
||||||
v-if="response.final_cost || response.cost"
|
|
||||||
class="badge badge-success badge-xs"
|
|
||||||
>
|
|
||||||
{{ response.final_cost || response.cost }}
|
{{ response.final_cost || response.cost }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-base-content/50">-</span>
|
<span v-else class="text-base-content/50">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span v-if="response.final_cycles || response.cycles" class="badge badge-info badge-xs">
|
||||||
v-if="response.final_cycles || response.cycles"
|
|
||||||
class="badge badge-info badge-xs"
|
|
||||||
>
|
|
||||||
{{ response.final_cycles || response.cycles }}
|
{{ response.final_cycles || response.cycles }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-base-content/50">-</span>
|
<span v-else class="text-base-content/50">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span v-if="response.final_area || response.area" class="badge badge-warning badge-xs">
|
||||||
v-if="response.final_area || response.area"
|
|
||||||
class="badge badge-warning badge-xs"
|
|
||||||
>
|
|
||||||
{{ response.final_area || response.area }}
|
{{ response.final_area || response.area }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-base-content/50">-</span>
|
<span v-else class="text-base-content/50">-</span>
|
||||||
@ -113,23 +101,14 @@
|
|||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span class="badge badge-ghost badge-xs">{{
|
<span class="badge badge-ghost badge-xs">{{
|
||||||
response.files?.length || 0
|
response.files?.length || 0
|
||||||
}}</span>
|
}}</span>
|
||||||
<div
|
<div v-if="response.files?.length" class="tooltip" :data-tip="response.files
|
||||||
v-if="response.files?.length"
|
.map((f) => f.original_filename || f.file?.name)
|
||||||
class="tooltip"
|
.join(', ')
|
||||||
:data-tip="
|
">
|
||||||
response.files
|
|
||||||
.map((f) => f.original_filename || f.file?.name)
|
|
||||||
.join(', ')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-information-outline text-xs"></i>
|
<i class="mdi mdi-information-outline text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="response.needs_manual_validation" class="tooltip" data-tip="Needs manual validation">
|
||||||
v-if="response.needs_manual_validation"
|
|
||||||
class="tooltip"
|
|
||||||
data-tip="Needs manual validation"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-alert-circle text-xs text-warning"></i>
|
<i class="mdi mdi-alert-circle text-xs text-warning"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,11 +120,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No responses state -->
|
<!-- No responses state -->
|
||||||
<div
|
<div v-else
|
||||||
v-else
|
|
||||||
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"
|
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"
|
@click="openSubmissionModal">
|
||||||
>
|
|
||||||
<i class="mdi mdi-upload text-2xl text-base-content/40"></i>
|
<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">No solutions yet</p>
|
||||||
<p class="text-xs text-base-content/40">
|
<p class="text-xs text-base-content/40">
|
||||||
|
|||||||
@ -9,9 +9,16 @@ interface User {
|
|||||||
last_name?: string;
|
last_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PointsFactor {
|
||||||
|
cost: number;
|
||||||
|
cycles: number;
|
||||||
|
area: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface Puzzle {
|
interface Puzzle {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
points_factor?: PointsFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PuzzleResponse {
|
interface PuzzleResponse {
|
||||||
@ -91,7 +98,6 @@ const getPuzzleRanking = (puzzleId: number) => {
|
|||||||
|
|
||||||
const ranking = resultsData.value.ranking_by_puzzle[puzzleId] || [];
|
const ranking = resultsData.value.ranking_by_puzzle[puzzleId] || [];
|
||||||
return ranking.map((response) => {
|
return ranking.map((response) => {
|
||||||
console.log(response)
|
|
||||||
const user = resultsData.value!.users.find((u) => u.id === response.user_id);
|
const user = resultsData.value!.users.find((u) => u.id === response.user_id);
|
||||||
return {
|
return {
|
||||||
username: user?.username || "Unknown",
|
username: user?.username || "Unknown",
|
||||||
@ -297,6 +303,28 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-6">
|
<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>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Top 3 Podium -->
|
<!-- Top 3 Podium -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div v-for="(response, index) in getPuzzleRanking(puzzle.id).slice(0, 3)" :key="index"
|
<div v-for="(response, index) in getPuzzleRanking(puzzle.id).slice(0, 3)" :key="index"
|
||||||
@ -309,15 +337,21 @@ onMounted(() => {
|
|||||||
<div class="divider my-2"></div>
|
<div class="divider my-2"></div>
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>Cost</span>
|
<span>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>
|
<span class="badge badge-sm">{{ response.cost || 'N/A' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>Cycles</span>
|
<span>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>
|
<span class="badge badge-sm">{{ response.cycles || 'N/A' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>Area</span>
|
<span>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>
|
<span class="badge badge-sm">{{ response.area || 'N/A' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between pt-2 border-t">
|
<div class="flex justify-between pt-2 border-t">
|
||||||
@ -340,9 +374,21 @@ onMounted(() => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="w-12">Rank</th>
|
<th class="w-12">Rank</th>
|
||||||
<th>Player</th>
|
<th>Player</th>
|
||||||
<th class="text-center">Cost</th>
|
<th class="text-center">
|
||||||
<th class="text-center">Cycles</th>
|
Cost
|
||||||
<th class="text-center">Area</th>
|
<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
|
||||||
|
<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
|
||||||
|
<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-center">Total (with coef.)</th>
|
||||||
<th class="text-right">Points</th>
|
<th class="text-right">Points</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -11,6 +11,12 @@ export interface SteamCollection {
|
|||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PointsFactor {
|
||||||
|
cost: number
|
||||||
|
cycles: number
|
||||||
|
area: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface SteamCollectionItem {
|
export interface SteamCollectionItem {
|
||||||
id: number
|
id: number
|
||||||
steam_item_id: string
|
steam_item_id: string
|
||||||
@ -20,6 +26,7 @@ export interface SteamCollectionItem {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
order_index: number
|
order_index: number
|
||||||
collection: number
|
collection: number
|
||||||
|
points_factor?: PointsFactor
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -132,10 +132,19 @@ class ValidationIn(Schema):
|
|||||||
|
|
||||||
|
|
||||||
# Collection Schemas
|
# Collection Schemas
|
||||||
|
class PuzzlePointsFactorOut(Schema):
|
||||||
|
"""Schema for puzzle points factor output"""
|
||||||
|
|
||||||
|
cost: int
|
||||||
|
cycles: int
|
||||||
|
area: int
|
||||||
|
|
||||||
|
|
||||||
class SteamCollectionItemOut(ModelSchema):
|
class SteamCollectionItemOut(ModelSchema):
|
||||||
"""Schema for Steam collection item output"""
|
"""Schema for Steam collection item output"""
|
||||||
|
|
||||||
steam_url: str
|
steam_url: str
|
||||||
|
points_factor: Optional[PuzzlePointsFactorOut] = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SteamCollectionItem
|
model = SteamCollectionItem
|
||||||
@ -151,6 +160,16 @@ class SteamCollectionItemOut(ModelSchema):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_points_factor(obj) -> Optional[PuzzlePointsFactorOut]:
|
||||||
|
if obj.points_factor:
|
||||||
|
return PuzzlePointsFactorOut(
|
||||||
|
cost=obj.points_factor.cost,
|
||||||
|
cycles=obj.points_factor.cycles,
|
||||||
|
area=obj.points_factor.area,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Error Schemas
|
# Error Schemas
|
||||||
class ErrorOut(Schema):
|
class ErrorOut(Schema):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user