320 lines
14 KiB
JavaScript
320 lines
14 KiB
JavaScript
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">←</button>
|
||
<button class="home-nav home-nav-right" @click="next" aria-label="Next">→</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 20–30 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;
|
||
}
|
||
} |