add everything
This commit is contained in:
459
js/pages/OLDLISTJSONLYAREDLLAYOUT.js
Normal file
459
js/pages/OLDLISTJSONLYAREDLLAYOUT.js
Normal file
@@ -0,0 +1,459 @@
|
||||
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'
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { Spinner, LevelAuthors },
|
||||
template: `
|
||||
<main v-if="loading" class="surface">
|
||||
<Spinner></Spinner>
|
||||
</main>
|
||||
<main v-else class="page-list">
|
||||
<div class="list-container surface">
|
||||
<!-- removing this thing
|
||||
<input type="checkbox" id="sul-checkbox" name="sul-check" value="showUnverified" @click="showUnverified = !showUnverified">
|
||||
<label for="sul-checkbox" class="sul-label">Show Upcoming/Unverified Levels</label><br>
|
||||
|
||||
<br v-if="!showUnverified">-->
|
||||
<div style="display:flex; gap:10px; margin-bottom:10px; align-items:center;">
|
||||
<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;"
|
||||
/>
|
||||
|
||||
<!--<select v-model="sortBy" style="padding:8px 12px; border-radius:6px; background:#2a2a2a; color:#fff; border:none;" selected="rank">
|
||||
<option value="rank">Rank</option>
|
||||
<option value="enjoyment">Enjoyment</option>
|
||||
<option value="name">Name</option>
|
||||
</select>-->
|
||||
|
||||
<!-- sorting options -->
|
||||
<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 (A–Z)
|
||||
</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">—</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>
|
||||
<div class="type-title-sm">Enjoyment</div>
|
||||
<p>{{ level.enjoyment }}/100</p>
|
||||
</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>
|
||||
<!--<template v-if="editors">
|
||||
<h2>John Evil Members</h2>
|
||||
<ol class="editors">
|
||||
<li v-for="editor in editors">
|
||||
<img :src="\`/assets/\${roleIconMap[editor.role]}\${(store.dark || store.shitty) ? '-dark' : ''}.svg\`" :alt="editor.role">
|
||||
<a v-if="editor.link" class="type-label-lg link" target="_blank" :href="editor.link">{{ editor.name }}</a>
|
||||
<p v-else>{{ editor.name }}</p>
|
||||
<img :src="\`/assets/icons/\${editor.name}.png\`" :alt="editor.name">
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
<h2> List Rules </h2>
|
||||
<p><i>read this now</i></p>
|
||||
<h3> Demonlist Rules </h3>
|
||||
<p>Levels must be verified on NO LOWER than 60 fps, and you can't use physics bypass. CBF is allowed.</p>
|
||||
<p>Levels must be possible on 60 fps.</p>
|
||||
<p>Audible clicks are <b><i>required.</i></b> It does not matter if you have a CPS counter.</p>
|
||||
<p>Your previous attempt/entry of the level, full completion, endscreen, and Cheat Indicator must be shown.</p>
|
||||
<p>No secret ways or bug routes allowed.</p>
|
||||
<p>LDM's are allowed as long as they don't make the level any easier to play or see.</p>
|
||||
<p>Bug fixes are allowed as long as they don't nerf the level.</p>
|
||||
<h3> Challenge List Rules </h3>
|
||||
<p>Levels must be verified on NO LOWER than 60 FPS, and NO HIGHER than 360 FPS. Your FPS must be shown.</p>
|
||||
<p>Challenges that force drastic changes depending on the player's FPS; including different routes; are not eligible for the list!</p>
|
||||
<p>Recordings are REQUIRED for all challenges! PC recordings must include audible clicks and the last death attempt before completion!</p>
|
||||
<p>Challenges that fall out of the top 150 will be moved into the Legacy List.</p>
|
||||
<p>Challenges that require more than 16 CPS spam are not eligible for the list!</p>
|
||||
<p>Levels that are 30 seconds or longer are not eligible for the list! Challenges must be 29 seconds or shorter.</p>
|
||||
<h3> Info </h3>
|
||||
<p>Levels that fall out of the top 150 will be moves into the Legacy List.</p>
|
||||
<p>Levels that are deemed not list-worthy will not be placed at all.</p>
|
||||
<p>Banned members are ineligible for submitting future records.</p>
|
||||
<p>Packs do not give bonus points; they are simply an extra incentive.</p>-->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
data: () => ({
|
||||
list: [],
|
||||
editors: [],
|
||||
loading: true,
|
||||
selected: 0,
|
||||
errors: [],
|
||||
roleIconMap,
|
||||
store,
|
||||
toggledShowcase: false,
|
||||
showUnverified: false,
|
||||
aredlRanks: {},
|
||||
searchQuery: '',
|
||||
|
||||
// ... your existing properties ...
|
||||
menuOpen: false,
|
||||
// "pending" = what's in the menu before hitting Apply
|
||||
pendingSortBy: 'rank',
|
||||
pendingRatedOnly: false,
|
||||
pendingUnratedOnly: false,
|
||||
pendingMinEnjoyment: 0,
|
||||
// "active" = what's actually applied
|
||||
sortBy: 'rank',
|
||||
filterRatedOnly: false,
|
||||
filterUnratedOnly: false,
|
||||
filterMinEnjoyment: 0,
|
||||
}),
|
||||
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,
|
||||
);
|
||||
},
|
||||
filteredList() {
|
||||
const q = (this.searchQuery || '').toLowerCase().trim();
|
||||
|
||||
let filtered = this.list.filter(([err, rank, level]) => {
|
||||
if (!level || !level.name) return false;
|
||||
|
||||
// text search
|
||||
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];
|
||||
// rank filters (these are mutually exclusive in practice)
|
||||
if (this.filterRatedOnly && !hasAredlRank) return false;
|
||||
if (this.filterUnratedOnly && hasAredlRank) return false;
|
||||
|
||||
// enjoyment filter
|
||||
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();
|
||||
|
||||
// Grab the level from URL
|
||||
const levelName = getLevelFromURL();
|
||||
if (levelName) {
|
||||
// find index in the list
|
||||
const index = this.list.findIndex(([, , l]) => l?.name === levelName);
|
||||
if (index !== -1) {
|
||||
this.selected = index;
|
||||
this.selectLevel(this.list[index][2]); // select it
|
||||
}
|
||||
}
|
||||
|
||||
for (const [, , level] of this.list) {
|
||||
this.getAredlRank(level.id);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
embed,
|
||||
score,
|
||||
getFontColour,
|
||||
getLevelThumbnail,
|
||||
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();
|
||||
}
|
||||
},
|
||||
async getAredlRank(id) {
|
||||
if (this.aredlRanks[id]) return;
|
||||
|
||||
const response = await fetch(`https://api.aredl.net/v2/api/aredl/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) {
|
||||
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;
|
||||
|
||||
const level = newList[0][2];
|
||||
const index = this.list.findIndex(([, , l]) => l?.id === level?.id);
|
||||
|
||||
/*if (index !== -1) {
|
||||
this.selectLevel(level);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function resetList() {
|
||||
console.log("resetting");
|
||||
|
||||
store.list.loading = true;
|
||||
|
||||
// Hide loading spinner
|
||||
store.list.list = await fetchList();
|
||||
store.list.editors = await fetchEditors();
|
||||
|
||||
// Error handling
|
||||
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, _, __]) => {
|
||||
return `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(i, store.list.list[i][2]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
store.list.loading = false;
|
||||
}
|
||||
|
||||
function getLevelFromURL() {
|
||||
// window.location.hash looks like: "#/?level=Bloodbath"
|
||||
const hash = window.location.hash.split('?')[1] || '';
|
||||
const params = new URLSearchParams(hash);
|
||||
return params.get('level'); // returns "Bloodbath" or null
|
||||
}
|
||||
Reference in New Issue
Block a user