add everything

This commit is contained in:
Koolant
2026-04-17 12:29:41 -04:00
commit 4c1cfe6847
437 changed files with 11939 additions and 0 deletions

679
js/pages/List.js Normal file
View File

@@ -0,0 +1,679 @@
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');
}