Files
TheEvilList/js/pages/List.js
2026-04-17 12:29:41 -04:00

679 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { store } from '../main.js';
import { embed, getFontColour, getLevelThumbnail } from '../util.js';
import { score } from '../score.js';
import { fetchEditors, fetchList } from '../content.js';
import Spinner from '../components/Spinner.js';
import LevelAuthors from '../components/List/LevelAuthors.js';
const roleIconMap = {
owner: 'crown',
member: 'cube',
admin: 'user-gear',
kreo: 'kreo',
seniormod: 'user-shield',
mod: 'user-lock',
trial: 'user-check',
dev: 'code'
};
// Extract a YouTube video ID from any YT URL and return the thumbnail URL
function ytThumb(url) {
if (!url) return null;
const m = url.match(/(?:youtu\.be\/|v=|\/embed\/)([A-Za-z0-9_-]{11})/);
return m ? `https://img.youtube.com/vi/${m[1]}/mqdefault.jpg` : null;
}
export default {
components: { Spinner, LevelAuthors },
template: `
<main v-if="loading" class="surface">
<Spinner></Spinner>
</main>
<!-- ══════════════════════════════════════════
POINTERCRATE LAYOUT (store.pointercrateLayout = false)
Identical to the original layout
══════════════════════════════════════════ -->
<main v-else-if="!store.pointercrateLayout" class="page-list">
<div class="list-container surface">
<div class="list-toolbar">
<input
id="levelSearch"
v-model="searchQuery"
type="search"
placeholder="Search levels..."
aria-label="Search levels"
autocomplete="off"
style="flex:1; padding:10px 12px; border-radius:8px; border:none; background:#2a2a2a; color:#fff;"
/>
<div style="position:relative;">
<button @click="menuOpen = !menuOpen" style="padding:8px 14px; border-radius:8px; display:flex; align-items:center; gap:6px; background:#2a2a2a; color:#fff; border:none; cursor:pointer;">
<span style="font-size:14px;">Sort & Filter</span>
<span style="font-size:10px;">{{ menuOpen ? '▲' : '▼' }}</span>
</button>
<div v-if="menuOpen" style="position:absolute; right:0; top:calc(100% + 6px); width:280px; background:#1e1e1e; border:1px solid #444; border-radius:10px; padding:1rem; z-index:100;">
<p style="font-size:12px; color:#888; margin:0 0 8px; text-transform:uppercase;">Sort by</p>
<div style="display:flex; flex-direction:column; gap:6px; margin-bottom:1rem;">
<label style="display:flex; align-items:center; gap:8px; font-size:14px; color:#fff; cursor:pointer;">
<input type="radio" v-model="pendingSortBy" value="rank" /> Rank
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:14px; color:#fff; cursor:pointer;">
<input type="radio" v-model="pendingSortBy" value="enjoyment" /> Enjoyment
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:14px; color:#fff; cursor:pointer;">
<input type="radio" v-model="pendingSortBy" value="name" /> Name (AZ)
</label>
</div>
<div style="border-top:1px solid #333; padding-top:1rem; margin-bottom:1rem;">
<p style="font-size:12px; color:#888; margin:0 0 8px; text-transform:uppercase;">Filter</p>
<div style="display:flex; flex-direction:column; gap:8px;">
<label style="display:flex; align-items:center; gap:8px; font-size:14px; color:#fff; cursor:pointer;">
<input type="checkbox" v-model="pendingRatedOnly" /> Rated levels only
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:14px; color:#fff; cursor:pointer;">
<input type="checkbox" v-model="pendingUnratedOnly" /> Unrated levels only
</label>
<div style="margin-top:4px;">
<label style="font-size:13px; color:#888; display:block; margin-bottom:4px;">Min enjoyment: {{ pendingMinEnjoyment }}</label>
<input type="range" min="0" max="100" v-model.number="pendingMinEnjoyment" style="width:100%;" />
</div>
</div>
</div>
<button @click="applyFilters" style="width:100%; padding:9px; border-radius:8px; background:#3a6ee8; color:#fff; border:none; font-size:14px; font-weight:500; cursor:pointer;">
Apply
</button>
</div>
</div>
</div>
<h2 class="list-separator"><i>The List</i></h2>
<table class="list" v-if="list">
<tr v-for="([err, rank, level], i) in filteredList">
<td class="rank">
<p v-if="rank != null && rank <= 150" class="type-label-lg">#{{ rank }}</p>
<p v-else-if="rank == null && showUnverified" class="type-label-lg">&mdash;</p>
</td>
<td class="rank-image">
<img v-if="rank == 1" class="rank-trophy" src="assets/Top1Trophy.png" />
<img v-if="rank == 2" class="rank-trophy" src="assets/Top2Trophy.png" />
<img v-if="rank == 3" class="rank-trophy" src="assets/Top3Trophy.png" />
<img v-if="rank >= 4" class="rank-trophy" src="assets/NoTrophy.png" />
</td>
<td class="level" :class="{ 'active': level?.id === this.level?.id, 'error': !level }">
<button
@click="selectLevel(level)"
@mouseenter="previewBackground(level)"
@mouseleave="previewBackground(this.level)"
v-if="rank != null && rank <= 150"
:style="getLevelThumbnail(level.name)"
>
<span class="type-label-lg" v-html="highlight(level?.name || '')"></span>
<span class="type-label-sm" v-html="highlight(level?.author || '')"></span>
<span v-if="aredlRanks[level.id]" class="type-label-sm">Aredl #{{ aredlRanks[level.id] ?? "Loading..." }}</span>
</button>
</td>
</tr>
</table>
<h2 class="list-separator" v-if="list[150] != null"><i>Legacy List</i></h2>
<table class="list" v-if="list">
<tr v-for="([err, rank, level], i) in filteredList">
<td class="rank">
<p v-if="rank != null && rank > 150" class="type-label-lg">#{{ rank }}</p>
</td>
<td class="rank-image">
<img v-if="rank >= 151" class="rank-trophy" src="assets/NoTrophy.png" />
</td>
<td class="level" :class="{ 'active': level?.id === this.level?.id, 'error': !level }">
<button
@click="selectLevel(level)"
@mouseenter="previewBackground(level)"
v-if="rank != null && rank > 150"
:style="getLevelThumbnail(level.name)"
>
<span class="type-label-lg" v-html="highlight(level?.name || '')"></span>
<span class="type-label-sm" v-html="highlight(level?.verifier || '')"></span>
<span v-if="aredlRanks[level.id]" class="type-label-sm">Aredl #{{ aredlRanks[level.id] ?? "Loading..." }}</span>
</button>
</td>
</tr>
</table>
</div>
<div class="level-container surface">
<div class="level" v-if="level">
<h1>{{ level.name }}</h1>
<p><i>"{{level.gdleveldescription || "(No description provided)"}}"</i></p>
<LevelAuthors :author="level.author" :creators="level.creators" :verifier="level.verifier"></LevelAuthors>
<p>{{level.description || "No Description Provided."}}</p>
<div class="packs" v-if="level.packs.length > 0">
<div v-for="pack in level.packs" class="tag" :style="{background:pack.colour}">
<p :style="{color:getFontColour(pack.colour)}">{{pack.name}}</p>
</div>
</div>
<div v-if="level.showcase" class="tabs">
<button class="tab type-label-lg" :class="{selected: !toggledShowcase}" @click="toggledShowcase = false">
<span class="type-label-lg">Verification</span>
</button>
<button class="tab" :class="{selected: toggledShowcase}" @click="toggledShowcase = true">
<span class="type-label-lg">Showcase</span>
</button>
</div>
<iframe class="video" id="videoframe" :src="video" frameborder="0"></iframe>
<ul class="stats">
<li>
<div class="type-title-sm">Points when completed</div>
<p>{{ score(level.rank, 100, level.percentToQualify) }}</p>
</li>
<li>
<div class="type-title-sm">ID</div>
<p>{{ level.id }}</p>
</li>
<li class="enjoyment-stat" :style="{ '--enjoyment': level.enjoyment ?? 0 }">
<div class="type-title-sm">Enjoyment</div>
<p>{{ level.enjoyment ?? '—' }}/100</p>
<div class="enjoyment-bar-track">
<div
class="enjoyment-bar-fill"
:style="{ width: (level.enjoyment ?? 0) + '%' }"
></div>
</div>
</li>
</ul>
</div>
<div v-else class="level" style="height: 100%; justify-content: center; align-items: center;">
<p>(ノಠ益ಠ)ノ彡┻━┻</p>
</div>
</div>
<div class="meta-container surface">
<div class="meta">
<div class="errors" v-show="errors.length > 0">
<p class="error" v-for="error of errors">{{ error }}</p>
</div>
<h2>Records</h2>
<p v-if="level.rank == null"><strong>{{ level.percentToQualify }}%</strong> or better to qualify</p>
<p v-else-if="level.rank <= 75"><strong>{{ level.percentToQualify }}%</strong> or better to qualify</p>
<p v-else><strong>100%</strong> to qualify</p>
<table class="records">
<tr v-for="record in level.records" class="record">
<td class="percent">
<p>{{ record.percent }}%</p>
</td>
<td class="userIcon">
<img class="ico" :src="\`/assets/icons/\${record.user}.png\`" :alt="record.user">
</td>
<td class="user">
<a :href="record.link" target="_blank" class="type-label-lg">{{ record.user }}</a>
</td>
<td class="mobile">
<img v-if="record.mobile" :src="\`/assets/phone-landscape\${store?.dark ? '-dark' : ''}.svg\`" alt="Mobile">
</td>
</tr>
</table>
</div>
</div>
</main>
<!-- ══════════════════════════════════════════
CARD LAYOUT (store.pointercrateLayout = true)
Centered list of large level cards + slide-in detail panel
══════════════════════════════════════════ -->
<main v-else class="page-list-cards">
<!-- Detail panel overlay -->
<transition name="cl-panel-slide">
<div v-if="cardSelected" class="cl-panel-backdrop" @click.self="cardSelected = null">
<div class="cl-panel surface">
<button class="cl-panel-close" @click="cardSelected = null" aria-label="Close">&#x2715;</button>
<div class="cl-panel-hero" :style="cardSelected.name ? { backgroundImage: \`url('/assets/levels/\${cardSelected.name}.png')\` } : {}">
<div class="cl-panel-hero-tint"></div>
<div class="cl-panel-hero-info">
<span class="cl-panel-rank" v-if="cardSelected.rank">#{{ cardSelected.rank }}</span>
<h1 class="cl-panel-title">{{ cardSelected.name }}</h1>
<LevelAuthors :author="cardSelected.author" :creators="cardSelected.creators" :verifier="cardSelected.verifier"></LevelAuthors>
</div>
</div>
<div class="cl-panel-body">
<p v-if="cardSelected.gdleveldescription"><i>"{{ cardSelected.gdleveldescription }}"</i></p>
<p v-else><i>(No description provided)</i></p>
<p v-if="cardSelected.description">{{ cardSelected.description }}</p>
<div class="packs" v-if="cardSelected.packs && cardSelected.packs.length > 0">
<div v-for="pack in cardSelected.packs" class="tag" :style="{background:pack.colour}">
<p :style="{color:getFontColour(pack.colour)}">{{pack.name}}</p>
</div>
</div>
<div v-if="cardSelected.showcase" class="tabs">
<button class="tab type-label-lg" :class="{selected: !toggledShowcase}" @click="toggledShowcase = false">
<span class="type-label-lg">Verification</span>
</button>
<button class="tab" :class="{selected: toggledShowcase}" @click="toggledShowcase = true">
<span class="type-label-lg">Showcase</span>
</button>
</div>
<iframe class="video" :src="cardVideo" frameborder="0"></iframe>
<ul class="stats">
<li>
<div class="type-title-sm">Points when completed</div>
<p>{{ score(cardSelected.rank, 100, cardSelected.percentToQualify) }}</p>
</li>
<li>
<div class="type-title-sm">ID</div>
<p>{{ cardSelected.id }}</p>
</li>
<li class="enjoyment-stat" :style="{ '--enjoyment': cardSelected.enjoyment ?? 0 }">
<div class="type-title-sm">Enjoyment</div>
<p>{{ cardSelected.enjoyment ?? '—' }}/100</p>
<div class="enjoyment-bar-track">
<div
class="enjoyment-bar-fill"
:style="{ width: (cardSelected.enjoyment ?? 0) + '%' }"
></div>
</div>
</li>
</ul>
<h2>Records</h2>
<p v-if="cardSelected.rank == null"><strong>{{ cardSelected.percentToQualify }}%</strong> or better to qualify</p>
<p v-else-if="cardSelected.rank <= 75"><strong>{{ cardSelected.percentToQualify }}%</strong> or better to qualify</p>
<p v-else><strong>100%</strong> to qualify</p>
<table class="records">
<tr v-for="record in cardSelected.records" class="record">
<td class="percent"><p>{{ record.percent }}%</p></td>
<td class="userIcon">
<img class="ico" :src="\`/assets/icons/\${record.user}.png\`" :alt="record.user">
</td>
<td class="user">
<a :href="record.link" target="_blank" class="type-label-lg">{{ record.user }}</a>
</td>
<td class="mobile">
<img v-if="record.mobile" :src="\`/assets/phone-landscape\${store?.dark ? '-dark' : ''}.svg\`" alt="Mobile">
</td>
</tr>
</table>
</div>
</div>
</div>
</transition>
<!-- Search + filter bar -->
<div class="cl-toolbar">
<input
v-model="searchQuery"
type="search"
placeholder="Search levels..."
aria-label="Search levels"
autocomplete="off"
class="cl-search"
/>
<button ref="filterBtn" @click="toggleMenu" class="cl-filter-btn">
<span>Sort & Filter</span>
<span style="font-size:10px;">{{ menuOpen ? '▲' : '▼' }}</span>
</button>
</div>
<!-- Dropdown teleported to body so no parent overflow/stacking context can trap it -->
<Teleport to="body">
<div
v-if="menuOpen"
class="cl-filter-dropdown"
:style="dropdownStyle"
>
<p class="cl-filter-heading">Sort by</p>
<div style="display:flex; flex-direction:column; gap:6px; margin-bottom:1rem;">
<label class="cl-filter-label"><input type="radio" v-model="pendingSortBy" value="rank" /> Rank</label>
<label class="cl-filter-label"><input type="radio" v-model="pendingSortBy" value="enjoyment" /> Enjoyment</label>
<label class="cl-filter-label"><input type="radio" v-model="pendingSortBy" value="name" /> Name (AZ)</label>
</div>
<div style="border-top:1px solid #333; padding-top:1rem; margin-bottom:1rem;">
<p class="cl-filter-heading">Filter</p>
<div style="display:flex; flex-direction:column; gap:8px;">
<label class="cl-filter-label"><input type="checkbox" v-model="pendingRatedOnly" /> Rated levels only</label>
<label class="cl-filter-label"><input type="checkbox" v-model="pendingUnratedOnly" /> Unrated levels only</label>
<div style="margin-top:4px;">
<label style="font-size:13px; color:#888; display:block; margin-bottom:4px;">Min enjoyment: {{ pendingMinEnjoyment }}</label>
<input type="range" min="0" max="100" v-model.number="pendingMinEnjoyment" style="width:100%;" />
</div>
</div>
</div>
<button @click="applyFilters" class="cl-filter-apply">Apply</button>
</div>
<!-- Click-outside backdrop to close menu -->
<div v-if="menuOpen" class="cl-menu-backdrop" @click="menuOpen = false"></div>
</Teleport>
<!-- Section label -->
<div class="cl-section-label">
<span>{{ filteredList.filter(([,r]) => r != null && r <= 150).length }} levels</span>
<span v-if="filteredList.some(([,r]) => r > 150)"> · including legacy</span>
</div>
<!-- Card list -->
<div class="cl-list">
<!-- Main list separator -->
<div class="cl-separator" v-if="filteredList.some(([,r]) => r != null && r <= 150)">
<span>The List</span>
</div>
<template v-for="([err, rank, level]) in filteredList" :key="level?.id">
<button
v-if="rank != null && rank <= 150 && level"
class="cl-card"
@click="openCard(level)"
>
<!-- Blurred level thumbnail background -->
<div
class="cl-card-bg"
:style="{ backgroundImage: \`url('/assets/levels/\${level.name}.png')\` }"
></div>
<div class="cl-card-tint"></div>
<!-- YouTube thumbnail -->
<div class="cl-card-yt" v-if="ytThumb(level.verification)">
<img
:src="ytThumb(level.verification)"
:alt="level.name + ' verification'"
class="cl-card-yt-img"
/>
<div class="cl-card-yt-play">&#9654;</div>
</div>
<div class="cl-card-yt cl-card-yt-placeholder" v-else></div>
<!-- Text info -->
<div class="cl-card-info">
<div class="cl-card-rank">#{{ rank }}</div>
<div class="cl-card-name" v-html="highlight(level.name)"></div>
<div class="cl-card-meta">
<span class="cl-card-pts">{{ score(rank, 100, level.percentToQualify) }} pts</span>
<span class="cl-card-dot">·</span>
<span class="cl-card-author" v-html="highlight(level.author || '')"></span>
<span class="cl-card-dot">·</span>
<span class="cl-card-verifier">verified by <span v-html="highlight(level.verifier || '')"></span></span>
</div>
<div class="cl-card-tags">
<span v-if="aredlRanks[level.id]" class="cl-card-tag">AREDL #{{ aredlRanks[level.id] }}</span>
<span v-for="pack in (level.packs || [])" :key="pack.name" class="cl-card-tag" :style="{background: pack.colour, color: getFontColour(pack.colour)}">{{ pack.name }}</span>
</div>
</div>
<div class="cl-card-arrow">&#8250;</div>
</button>
</template>
<!-- Legacy separator -->
<div class="cl-separator" v-if="filteredList.some(([,r]) => r != null && r > 150)">
<span>Legacy List</span>
</div>
<template v-for="([err, rank, level]) in filteredList" :key="'leg-' + level?.id">
<button
v-if="rank != null && rank > 150 && level"
class="cl-card cl-card-legacy"
@click="openCard(level)"
>
<div
class="cl-card-bg"
:style="{ backgroundImage: \`url('/assets/levels/\${level.name}.png')\` }"
></div>
<div class="cl-card-tint"></div>
<div class="cl-card-yt" v-if="ytThumb(level.verification)">
<img
:src="ytThumb(level.verification)"
:alt="level.name + ' verification'"
class="cl-card-yt-img"
/>
<div class="cl-card-yt-play">&#9654;</div>
</div>
<div class="cl-card-yt cl-card-yt-placeholder" v-else></div>
<div class="cl-card-info">
<div class="cl-card-rank">#{{ rank }} <span class="cl-legacy-badge">Legacy</span></div>
<div class="cl-card-name" v-html="highlight(level.name)"></div>
<div class="cl-card-meta">
<span class="cl-card-pts">{{ score(rank, 100, level.percentToQualify) }} pts</span>
<span class="cl-card-dot">·</span>
<span class="cl-card-author" v-html="highlight(level.author || '')"></span>
<span class="cl-card-dot">·</span>
<span class="cl-card-verifier">verified by <span v-html="highlight(level.verifier || '')"></span></span>
</div>
<div class="cl-card-tags">
<span v-if="aredlRanks[level.id]" class="cl-card-tag">AREDL #{{ aredlRanks[level.id] }}</span>
<span v-for="pack in (level.packs || [])" :key="pack.name" class="cl-card-tag" :style="{background: pack.colour, color: getFontColour(pack.colour)}">{{ pack.name }}</span>
</div>
</div>
<div class="cl-card-arrow">&#8250;</div>
</button>
</template>
</div>
</main>
`,
data: () => ({
list: [],
editors: [],
loading: true,
selected: 0,
errors: [],
roleIconMap,
store,
toggledShowcase: false,
showUnverified: false,
aredlRanks: {},
searchQuery: '',
menuOpen: false,
pendingSortBy: 'rank',
pendingRatedOnly: false,
pendingUnratedOnly: false,
pendingMinEnjoyment: 0,
sortBy: 'rank',
filterRatedOnly: false,
filterUnratedOnly: false,
filterMinEnjoyment: 0,
// Card layout state
cardSelected: null,
}),
computed: {
level() {
return this.list && this.list[this.selected] && this.list[this.selected][2];
},
video() {
if (!this.level.showcase) {
return embed(this.level.verification);
}
return embed(
this.toggledShowcase ? this.level.showcase : this.level.verification,
);
},
dropdownStyle() {
// Position dropdown anchored to the filter button via fixed coords
// so it escapes every overflow/stacking context in the page
if (!this.menuOpen || !this.$refs.filterBtn) return { display: 'none' };
const r = this.$refs.filterBtn.getBoundingClientRect();
return {
position: 'fixed',
top: (r.bottom + 6) + 'px',
right: (window.innerWidth - r.right) + 'px',
zIndex: 99999,
};
},
// Video for the card panel
cardVideo() {
if (!this.cardSelected) return '';
if (!this.cardSelected.showcase) return embed(this.cardSelected.verification);
return embed(this.toggledShowcase ? this.cardSelected.showcase : this.cardSelected.verification);
},
filteredList() {
const q = (this.searchQuery || '').toLowerCase().trim();
let filtered = this.list.filter(([err, rank, level]) => {
if (!level || !level.name) return false;
if (q && !(
level.name.toLowerCase().includes(q) ||
(level.author || '').toLowerCase().includes(q) ||
(level.creators || []).some(c => c.toLowerCase().includes(q))
)) return false;
const hasAredlRank = !!this.aredlRanks[level.id];
if (this.filterRatedOnly && !hasAredlRank) return false;
if (this.filterUnratedOnly && hasAredlRank) return false;
if ((level.enjoyment ?? 0) < this.filterMinEnjoyment) return false;
return true;
});
return filtered.sort(([, , a], [, , b]) => {
if (!a || !b) return 0;
switch (this.sortBy) {
case 'rank': return (a.rank ?? Infinity) - (b.rank ?? Infinity);
case 'enjoyment': return (b.enjoyment ?? 0) - (a.enjoyment ?? 0);
case 'name': return a.name.localeCompare(b.name);
default: return 0;
}
});
},
},
async mounted() {
store.list = this;
await resetList();
const levelName = getLevelFromURL();
if (levelName) {
const index = this.list.findIndex(([, , l]) => l?.name === levelName);
if (index !== -1) {
this.selected = index;
this.selectLevel(this.list[index][2]);
}
}
for (const [, , level] of this.list) {
if (level?.id) this.getAredlRank(level.id);
}
},
methods: {
embed,
score,
getFontColour,
getLevelThumbnail,
ytThumb,
selectLevel(level) {
const index = this.list.findIndex(([, , l]) => l?.id === level?.id);
if (index !== -1) {
this.selected = index;
document.querySelector('.dark.root').style.background =
`url("/assets/levels/${level.name}.png") #000000e3`;
this.scrollToSelected();
}
},
toggleMenu() {
this.menuOpen = !this.menuOpen;
// Re-compute position on next tick after button is in DOM
this.$nextTick(() => { /* dropdownStyle computed auto-updates */ });
},
openCard(level) {
this.toggledShowcase = false;
this.cardSelected = level;
document.querySelector('.dark.root').style.background =
`url("/assets/levels/${level.name}.png") #000000e3`;
},
async getAredlRank(id) {
if (this.aredlRanks[id]) return;
let response = await fetch(`https://api.aredl.net/v2/api/aredl/levels/${id}`);
if(response.status == 404)
{
response = await fetch(`https://api.aredl.net/v2/api/arepl/levels/${id}`);
}
const levelInfo = await response.json();
this.aredlRanks[id] = levelInfo.position;
},
highlight(text) {
const q = (this.searchQuery || '').trim();
if (!q) return text;
const safe = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${safe})`, 'ig');
return text.replace(regex, `<span class="search-highlight">$1</span>`);
},
previewBackground(level) {
if (!level) return;
document.querySelector('.dark.root').style.background =
`url("/assets/levels/${level.name}.png") #000000e3`;
},
applyFilters() {
this.sortBy = this.pendingSortBy;
this.filterRatedOnly = this.pendingRatedOnly;
this.filterUnratedOnly = this.pendingUnratedOnly;
this.filterMinEnjoyment = this.pendingMinEnjoyment;
this.menuOpen = false;
this.scrollToSelected();
},
scrollToSelected() {
this.$nextTick(() => {
const activeBtn = document.querySelector('.level.active button');
if (activeBtn) {
activeBtn.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
});
},
},
watch: {
filteredList(newList) {
if (!newList.length) return;
// reserved for future use
},
// Reset showcase toggle when switching cards
cardSelected() {
this.toggledShowcase = false;
},
}
};
export async function resetList() {
console.log("resetting");
store.list.loading = true;
store.list.aredlRanks = {};
store.list.list = await fetchList();
store.list.editors = await fetchEditors();
if (!store.list.list) {
store.list.errors = [
"Failed to load list. Retry in a few minutes or notify list staff.",
];
} else {
store.list.errors.push(
...store.list.list
.filter(([err, _, __]) => err)
.map(([err, _, __]) => `Failed to load level. (${err}.json)`)
);
if (!store.list.editors) {
store.list.errors.push("Failed to load list editors.");
}
}
store.list.showUnverified = false;
for (var i = 0; i < store.list.list.length; i++) {
if (store.list.list[i][1] != null) {
store.list.selected = i;
store.list.selectLevel(store.list.list[i][2]);
break;
}
}
store.list.loading = false;
for (const [, , level] of store.list.list) {
if (level?.id) store.list.getAredlRank(level.id);
}
}
function getLevelFromURL() {
const hash = window.location.hash.split('?')[1] || '';
const params = new URLSearchParams(hash);
return params.get('level');
}