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

320 lines
14 KiB
JavaScript
Raw Permalink 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 { fetchEditors, fetchList, fetchLeaderboard, fetchPacks } from '../content.js';
import { store } from '../main.js';
export default {
name: 'HomePage',
template: `
<main v-if="loading" class="surface" style="display:flex;align-items:center;justify-content:center;min-height:100vh;">
<p style="font-family:monospace;color:#6b7a8d;letter-spacing:0.2em;">LOADING...</p>
</main>
<main v-else class="home">
<div class="home-noise"></div>
<!-- ── Hero ── -->
<section class="home-hero">
<!--<div class="home-hero-grid"></div>-->
<div class="home-hero-scroll">
<div
class="home-hero-track"
:style="{ width: backgroundLevels.length * 300 + 'px' }"
>
<div
v-for="(bg, i) in backgroundLevels"
:key="i"
class="home-hero-tile"
:style="{ backgroundImage: \`url('\${bg}')\` }"
></div>
<!-- duplicate for seamless loop -->
<div
v-for="(bg, i) in backgroundLevels"
:key="'dup-' + i"
class="home-hero-tile"
:style="{ backgroundImage: \`url('\${bg}')\` }"
></div>
</div>
</div>
<div class="home-hero-content">
<!--<div class="home-eyebrow">
<span class="home-dot home-dot-pulse"></span>
<span>GEOMETRY DASH</span>
</div>-->
<h1 class="home-title">
<!--<span class="home-title-line">john evil</span>
<span class="home-title-line home-title-accent">EVIL DEMONLIST</span>
<span class="home-title-line">by [JE] john evil</span>-->
<img src="./tsl_logo_wName.png">
</h1>
<p class="home-sub">geometry dash; evil dash</p>
<div class="home-actions">
<router-link to="/list" class="home-btn home-btn-primary">Browse the List</router-link>
<router-link to="/leaderboard" class="home-btn home-btn-ghost">Leaderboard</router-link>
</div>
</div>
<!--<div class="home-deco">
<div class="home-ring home-ring-1"></div>
<div class="home-ring home-ring-2"></div>
<div class="home-ring home-ring-3"></div>
</div>-->
</section>
<!-- ── Stats Bar ── -->
<section class="home-stats-bar">
<div class="home-stats-inner">
<div class="home-stat-item" v-for="(stat, i) in stats" :key="i">
<span class="home-stat-value">{{ stat.value }}</span>
<span class="home-stat-label">{{ stat.label }}</span>
</div>
</div>
</section>
<!-- ── Members Spotlight ── -->
<section class="home-members-section">
<div class="home-section-header">
<h2 class="home-section-title">john evil</h2>
</div>
<div class="home-spotlight" v-if="enrichedEditors.length">
<div class="home-spotlight-bg" :style="spotlightBgStyle"></div>
<div class="home-spotlight-tint"></div>
<!-- Pill nav -->
<div class="home-pills">
<button
v-for="(editor, i) in enrichedEditors"
:key="editor.name"
class="home-pill"
:class="{ 'home-pill-active': i === activeIdx }"
@click="setActive(i)"
>
<img
class="home-pill-icon"
:src="'/assets/icons/' + editor.name + '.png'"
:alt="editor.name"
@error="onIconError"
/>
<span>{{ editor.name }}</span>
</button>
</div>
<!-- Card -->
<transition name="home-card-fade" mode="out-in">
<div class="home-card" :key="activeIdx" v-if="activeEditor">
<div class="home-card-left">
<a
v-if="activeEditor.link"
:href="activeEditor.link"
target="_blank"
rel="noopener noreferrer"
class="home-yt-link"
>
<img
class="home-yt-avatar"
:src="'/assets/icons/' + activeEditor.name + '.png'"
:alt="activeEditor.name"
@error="onIconError"
/>
<span class="home-yt-badge">
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14">
<path d="M23.5 6.2a3 3 0 0 0-2.1-2.1C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.4.6a3 3 0 0 0-2.1 2.1C0 8.1 0 12 0 12s0 3.9.5 5.8a3 3 0 0 0 2.1 2.1c1.9.6 9.4.6 9.4.6s7.5 0 9.4-.6a3 3 0 0 0 2.1-2.1C24 15.9 24 12 24 12s0-3.9-.5-5.8zM9.8 15.5V8.5l6.3 3.5-6.3 3.5z"/>
</svg>
YouTube
</span>
</a>
<div v-else class="home-icon-wrap">
<img
:src="'/assets/icons/' + activeEditor.name + '.png'"
:alt="activeEditor.name"
@error="onIconError"
/>
</div>
<h3 class="home-card-name">{{ activeEditor.name }}</h3>
<p class="home-card-role">{{ activeEditor.role || 'List Member' }}</p>
</div>
<div class="home-card-right">
<div class="home-cstats">
<div class="home-cstat">
<span class="home-cstat-val">{{ activeEditor.completions + activeEditor.verifications ?? '—' }}</span>
<span class="home-cstat-key">Total Completions</span>
</div>
<div class="home-cstat">
<span class="home-cstat-val">{{ activeEditor.verifications ?? '—' }}</span>
<span class="home-cstat-key">First Completions</span>
</div>
<div class="home-cstat">
<span class="home-cstat-val">{{ activeEditor.rank != null ? '#' + activeEditor.rank : '—' }}</span>
<span class="home-cstat-key">Leaderboard Rank</span>
</div>
<div class="home-cstat" v-if="activeEditor.score != null">
<span class="home-cstat-val">{{ activeEditor.score }}</span>
<span class="home-cstat-key">List Score</span>
</div>
</div>
<div class="home-hardest" v-if="activeEditor.hardest">
<span class="home-hardest-label">HARDEST DEMON</span>
<span class="home-hardest-name">{{ activeEditor.hardest.level }}</span>
<span class="home-hardest-rank" v-if="activeEditor.hardest.rank">#{{ activeEditor.hardest.rank }} on the list</span>
</div>
</div>
</div>
</transition>
<button class="home-nav home-nav-left" @click="prev" aria-label="Previous">&#8592;</button>
<button class="home-nav home-nav-right" @click="next" aria-label="Next">&#8594;</button>
</div>
</section>
<!-- ── Fun Facts ── -->
<section class="home-facts">
<div class="home-facts-grid">
<div class="home-fact-card" v-for="(fact, i) in funFacts" :key="i">
<span class="home-fact-icon">{{ fact.icon }}</span>
<p class="home-fact-text">{{ fact.text }}</p>
</div>
</div>
</section>
<!-- ── Footer CTA ── -->
<section class="home-footer-cta">
<h2 class="home-footer-heading">↓ view the list ↓</h2>
<router-link to="/list" class="home-btn home-btn-primary home-btn-lg">yeah View the List</router-link>
</section>
</main>
`,
data: () => ({
store,
rawEditors: [],
leaderboardMap: {},
enrichedEditors: [],
loading: true,
activeIdx: 0,
stats: [
{ value: '—', label: 'Demons Ranked' },
{ value: '—', label: 'Total Completions' },
{ value: '—', label: 'Active Members' },
{ value: '—', label: 'Level Packs' },
],
funFacts: [
{ icon: '🔥', text: 'i got molested when i was 7' },
{ icon: '🤤', text: 'zorpikgmd is our one and only goongod and we are all his bellyslaves' },
{ icon: '💀', text: 'we are evil. john ai vs jew bot incident 3/23/2026 never forget ✊' },
],
backgroundLevels: [],
}),
watch: {
'store.listType'() {
resetHome(this);
}
},
computed: {
activeEditor() {
return this.enrichedEditors[this.activeIdx] || null;
},
spotlightBgStyle() {
const ed = this.activeEditor;
if (!ed || !ed.hardest) return {};
const p = ed.hardest.path
? `/assets/levels/${ed.hardest.path}.png`
: '/assets/levels/default.png';
return { backgroundImage: `url('${p}')` };
},
},
async mounted() {
await resetHome(this);
},
methods: {
setActive(i) { this.activeIdx = i; },
next() { this.activeIdx = (this.activeIdx + 1) % this.enrichedEditors.length; },
prev() { this.activeIdx = (this.activeIdx - 1 + this.enrichedEditors.length) % this.enrichedEditors.length; },
onIconError(e) { e.target.src = '/assets/icons/default.png'; },
},
};
export async function resetHome(ctx) {
console.log("evil");
ctx.loading = true;
try {
const [rawEditors, listData, [leaderboard]] = await Promise.all([
fetchEditors(),
fetchList(),
fetchLeaderboard(),
]);
ctx.rawEditors = (rawEditors || []).map((e) =>
typeof e === 'string' ? { name: e } : e
);
// Build leaderboard lookup keyed by lowercase name
const leaderboardMap = {};
if (leaderboard && Array.isArray(leaderboard)) {
leaderboard.forEach((entry, idx) => {
leaderboardMap[entry.user.toLowerCase()] = { ...entry, rank: idx + 1 };
});
}
ctx.leaderboardMap = leaderboardMap;
// Stats from list
if (listData) {
const valid = listData.filter(([, rank, level]) => rank !== null && level !== null);
const totalRecords = valid.reduce((s, [,, lv]) => s + (lv?.records?.length ?? 0), 0);
ctx.stats[0].value = valid.length;
ctx.stats[1].value = totalRecords + valid.length;
ctx.stats[2].value = ctx.rawEditors.length;
}
if (listData) {
const valid = listData.filter(([, rank, level]) => rank !== null && level !== null);
// pick random levels (like 2030 so it looks full)
const shuffled = valid.sort(() => 0.5 - Math.random());
ctx.backgroundLevels = shuffled.slice(0, 25).map(([, , lv]) => {
return `/assets/levels/${lv.path || lv.name}.png`;
});
}
// Enrich each editor from leaderboard
ctx.enrichedEditors = ctx.rawEditors.map((editor) => {
const lb = leaderboardMap[editor.name.toLowerCase()];
if (!lb) return { ...editor };
const completions = lb.completed?.length ?? 0;
const verifications = lb.verified?.length ?? 0;
const allFinished = [...(lb.verified || []), ...(lb.completed || [])];
let hardest = null;
if (allFinished.length > 0) {
const best = allFinished.reduce((p, c) => c.rank < p.rank ? c : p);
hardest = { level: best.level, rank: best.rank, path: best.path, link: best.link };
}
return { ...editor, completions, verifications, rank: lb.rank, score: lb.total, hardest };
});
// Pack count
try {
const packs = await fetchPacks();
if (packs) ctx.stats[3].value = packs.length;
} catch { /* non-critical */ }
} catch (err) {
console.error('HomePage: failed to load data:', err);
} finally {
ctx.loading = false;
}
}