add pick button and link mappool map to osu map page
This commit is contained in:
@@ -59,13 +59,13 @@
|
|||||||
// Group mappool slots by category
|
// Group mappool slots by category
|
||||||
const groupedSlots = $derived(() => {
|
const groupedSlots = $derived(() => {
|
||||||
if (!m.mappool?.slots) return {};
|
if (!m.mappool?.slots) return {};
|
||||||
const groups: Record<string, any[]> = {};
|
const groups: Record<string, unknown[]> = {};
|
||||||
for (const slot of m.mappool.slots) {
|
for (const slot of m.mappool.slots) {
|
||||||
if (!groups[slot.category]) groups[slot.category] = [];
|
if (!groups[slot.category]) groups[slot.category] = [];
|
||||||
groups[slot.category].push(slot);
|
groups[slot.category].push(slot);
|
||||||
}
|
}
|
||||||
for (const cat of Object.keys(groups)) {
|
for (const cat of Object.keys(groups)) {
|
||||||
groups[cat].sort((a: any, b: any) => a.orderInCategory - b.orderInCategory);
|
groups[cat].sort((a: unknown, b: unknown) => a.orderInCategory - b.orderInCategory);
|
||||||
}
|
}
|
||||||
// Return entries sorted by canonical mod order
|
// Return entries sorted by canonical mod order
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
const mappoolAvgSR = $derived(() => {
|
const mappoolAvgSR = $derived(() => {
|
||||||
const slots = m.mappool?.slots;
|
const slots = m.mappool?.slots;
|
||||||
if (!slots?.length) return null;
|
if (!slots?.length) return null;
|
||||||
const avg = slots.reduce((sum: number, s: any) => sum + (s.starRating ?? 0), 0) / slots.length;
|
const avg = slots.reduce((sum: number, s: unknown) => sum + (s.starRating ?? 0), 0) / slots.length;
|
||||||
return avg.toFixed(2);
|
return avg.toFixed(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -412,26 +412,58 @@
|
|||||||
|
|
||||||
{#if !mappoolCollapsed}
|
{#if !mappoolCollapsed}
|
||||||
<div class="mt-3 flex flex-col gap-3">
|
<div class="mt-3 flex flex-col gap-3">
|
||||||
{#each Object.entries(groupedSlots()) as [category, slots]}
|
{#each Object.entries(groupedSlots()) as [category, slots] (category)}
|
||||||
|
<!-- Grouped slots -->
|
||||||
<div>
|
<div>
|
||||||
<span class="mb-1.5 inline-block rounded border px-2 py-0.5 text-xs font-600 {catColors[category] ?? 'border-border'}">
|
<span class="mb-1.5 inline-block rounded border px-2 py-0.5 text-xs font-600 {catColors[category] ?? 'border-border'}">
|
||||||
{category}
|
{category}
|
||||||
</span>
|
</span>
|
||||||
<div class="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
|
||||||
{#each slots as slot}
|
{#each slots as slot (slot.id)}
|
||||||
|
<!-- Slot -->
|
||||||
{@const bm = data.beatmapCache[slot.beatmapId]}
|
{@const bm = data.beatmapCache[slot.beatmapId]}
|
||||||
{@const isPlayed = playedSlotIds.has(slot.id)}
|
{@const isPlayed = playedSlotIds.has(slot.id)}
|
||||||
{@const isTB = slot.category === 'TB'}
|
{@const isTB = slot.category === 'TB'}
|
||||||
{@const tbLocked = isTB && !isTiebreakerAllowed}
|
{@const tbLocked = isTB && !isTiebreakerAllowed}
|
||||||
{@const cantPick = !isMyTurnToPick || !pickingPhase}
|
{@const cantPick = !isMyTurnToPick || !pickingPhase}
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 rounded-lg border p-2 transition-all {isPlayed
|
||||||
|
? 'border-border/50 bg-surface-900 opacity-30'
|
||||||
|
: tbLocked
|
||||||
|
? 'border-border/50 bg-surface-900 opacity-30'
|
||||||
|
: 'border-border bg-surface-800'}">
|
||||||
|
<a
|
||||||
|
href="https://osu.ppy.sh/beatmaps/{slot.beatmapId}"
|
||||||
|
target="_blank"
|
||||||
|
class="flex min-w-0 flex-1 items-center gap-3"
|
||||||
|
>
|
||||||
|
{#if bm?.listCoverUrl}
|
||||||
|
<img src={bm.listCoverUrl} alt="" class="h-10 w-20 shrink-0 rounded object-cover" />
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-10 w-20 shrink-0 items-center justify-center rounded bg-surface-600 text-xs text-text-secondary">?</div>
|
||||||
|
{/if}
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
{#if bm}
|
||||||
|
<p class="truncate text-sm font-500">{bm.artist} - {bm.title}</p>
|
||||||
|
<p class="text-xs text-text-secondary">[{bm.version}] · {bm.starRating.toFixed(2)}★ · {bm.bpm}bpm</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-xs text-text-secondary">#{slot.beatmapId}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<span class="text-xs font-600 text-text-secondary">{category}{slot.orderInCategory}</span>
|
||||||
|
{#if isPlayed}
|
||||||
|
<span class="text-xs text-text-secondary">✓</span>
|
||||||
|
{:else if tbLocked}
|
||||||
|
<span class="text-[10px] text-pink-400/60" title="Match point only">🔒</span>
|
||||||
|
{:else if pickingPhase}
|
||||||
<form method="post" action="?/pick" use:enhance={() => {
|
<form method="post" action="?/pick" use:enhance={() => {
|
||||||
picking = true;
|
picking = true;
|
||||||
pickError = '';
|
pickError = '';
|
||||||
return async ({ result, update }) => {
|
return async ({ result, update }) => {
|
||||||
picking = false;
|
picking = false;
|
||||||
if (result.type === 'failure' || (result.type === 'success' && (result.data as any)?.error)) {
|
if (result.type === 'failure' || (result.type === 'success' && (result.data as Record<string, unknown>)?.error)) {
|
||||||
pickError = (result.data as any)?.error ?? 'Pick failed';
|
pickError = (result.data as Record<string, unknown>)?.error as string ?? 'Pick failed';
|
||||||
} else {
|
} else {
|
||||||
await update();
|
await update();
|
||||||
}
|
}
|
||||||
@@ -440,36 +472,16 @@
|
|||||||
<input type="hidden" name="slotId" value={slot.id} />
|
<input type="hidden" name="slotId" value={slot.id} />
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isPlayed || picking || tbLocked || cantPick}
|
disabled={picking || cantPick}
|
||||||
class="flex w-full items-center gap-3 rounded-lg border p-2 text-left transition-all {isPlayed
|
class="rounded px-2 py-1 text-[10px] font-700 transition-colors {cantPick
|
||||||
? 'border-border/50 bg-surface-900 opacity-30 cursor-not-allowed'
|
? 'bg-surface-600 text-text-secondary/40 cursor-default'
|
||||||
: tbLocked
|
: 'bg-white text-black hover:bg-white/80 cursor-pointer'}"
|
||||||
? 'border-border/50 bg-surface-900 opacity-30 cursor-not-allowed'
|
|
||||||
: cantPick
|
|
||||||
? 'border-border bg-surface-800 opacity-60 cursor-default'
|
|
||||||
: 'border-border bg-surface-700 hover:border-accent hover:bg-surface-600 cursor-pointer'}"
|
|
||||||
>
|
>
|
||||||
{#if bm?.listCoverUrl}
|
PICK
|
||||||
<img src={bm.listCoverUrl} alt="" class="h-10 w-20 rounded object-cover" />
|
|
||||||
{:else}
|
|
||||||
<div class="flex h-10 w-20 items-center justify-center rounded bg-surface-600 text-xs text-text-secondary">?</div>
|
|
||||||
{/if}
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
{#if bm}
|
|
||||||
<p class="truncate text-xs font-500">{bm.artist} - {bm.title}</p>
|
|
||||||
<p class="text-xs text-text-secondary">[{bm.version}] · {bm.starRating.toFixed(2)}★ · {bm.bpm}bpm</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-xs text-text-secondary">#{slot.beatmapId}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<span class="text-xs font-600 text-text-secondary">{category}{slot.orderInCategory}</span>
|
|
||||||
{#if isPlayed}
|
|
||||||
<span class="text-xs text-text-secondary">✓</span>
|
|
||||||
{:else if tbLocked}
|
|
||||||
<span class="text-[10px] text-pink-400/60" title="Match point only">🔒</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user