aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-28 23:55:47 -0800
committerFuwn <[email protected]>2026-01-28 23:55:47 -0800
commit079290f881e422b03e504da27f89d4e997e63a25 (patch)
tree33f36945d6ec78f8ae5caa88c5527a2e21e54151 /src
parentfeat: Add hero for landing and welcome page (diff)
downloaddue.moe-079290f881e422b03e504da27f89d4e997e63a25.tar.xz
due.moe-079290f881e422b03e504da27f89d4e997e63a25.zip
feat(Landing): Update to match LandingHero
Diffstat (limited to 'src')
-rw-r--r--src/lib/Landing.svelte553
-rw-r--r--src/lib/List/Anime/AnimeListTemplate.svelte3
-rw-r--r--src/lib/List/Anime/CleanAnimeList.svelte2
-rw-r--r--src/lib/List/Anime/CompletedAnimeList.svelte58
-rw-r--r--src/lib/List/CleanGrid.svelte4
-rw-r--r--src/lib/List/Manga/CleanMangaList.svelte3
-rw-r--r--src/lib/List/Manga/MangaListTemplate.svelte44
-rw-r--r--src/routes/+page.svelte6
8 files changed, 499 insertions, 174 deletions
diff --git a/src/lib/Landing.svelte b/src/lib/Landing.svelte
index 8fb8f72b..4c824a8d 100644
--- a/src/lib/Landing.svelte
+++ b/src/lib/Landing.svelte
@@ -1,154 +1,481 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
import root from './Utility/root';
import { env } from '$env/dynamic/public';
- import tooltip from './Tooltip/tooltip';
import CompletedAnimeList from './List/Anime/CompletedAnimeList.svelte';
import MangaListTemplate from './List/Manga/MangaListTemplate.svelte';
import localforage from 'localforage';
+ import { onMount } from 'svelte';
+
+ let sectionsVisible = $state<boolean[]>([false, false, false]);
+ let mangaContainer: HTMLElement;
+ let animeContainer: HTMLElement;
+ let gridLimit = $state<number | undefined>(undefined);
+ let demoFocused = $state(false);
+ const COVER_WIDTH = 100;
+ const COVER_GAP = 12;
+ const dummyCount = 20;
+
+ const calculateLimit = () => {
+ const container = mangaContainer || animeContainer;
+
+ if (!container) return;
+
+ const containerWidth = container.clientWidth;
+ const itemWidth = COVER_WIDTH + COVER_GAP;
+ const padding = containerWidth < 500 ? 48 : 36;
+
+ gridLimit = Math.max(2, Math.floor((containerWidth - padding) / itemWidth));
+ };
+
+ onMount(() => {
+ const intersectionObserver = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ const index = Number(entry.target.getAttribute('data-section'));
+
+ sectionsVisible[index] = true;
+ }
+ });
+ },
+ { threshold: 0.1 }
+ );
+
+ document.querySelectorAll('.landing-section').forEach((el) => intersectionObserver.observe(el));
+
+ calculateLimit();
+
+ const resizeObserver = new ResizeObserver(calculateLimit);
+
+ if (mangaContainer) resizeObserver.observe(mangaContainer);
+ if (animeContainer) resizeObserver.observe(animeContainer);
+
+ const handleKeydown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && demoFocused) demoFocused = false;
+ };
+
+ document.addEventListener('keydown', handleKeydown);
+
+ return () => {
+ intersectionObserver.disconnect();
+ resizeObserver.disconnect();
+ document.removeEventListener('keydown', handleKeydown);
+ };
+ });
</script>
-<div class="example-item card">
- <div class="item-content">
- <details class="list" open>
- <MangaListTemplate due={true} dummy displayUnresolved={false} disableFilter />
- </details>
- </div>
+<div class="landing">
+ <section class="landing-section" class:visible={sectionsVisible[0]} data-section="0">
+ <div class="section-row">
+ <div class="section-demo card" bind:this={mangaContainer}>
+ <details class="list" open>
+ <MangaListTemplate
+ due={true}
+ dummy
+ displayUnresolved={false}
+ disableFilter
+ {dummyCount}
+ limit={gridLimit}
+ />
+ </details>
+ </div>
- <div class="card item-description">
- <span class="big-text">Manga, Without the Guesswork</span>
+ <div class="section-info card">
+ <p class="section-label">Manga Tracking</p>
- <p>
- <a href={root('/')}>due.moe</a> automatically keeps your manga and light novel lists up to date—checking
- for new chapters, notifying you of new releases, and reminding you to update your volume count if
- you fall behind. Completed and ongoing series stay neatly organized so you can instantly see what
- needs your attention. Staying on top of your reading has never been easier.
- </p>
+ <h2 class="section-title">Without the Guesswork</h2>
- <small class="bottom">
- This demo view contains simulated data which may include concluded manga.
- </small>
- </div>
-</div>
+ <p class="section-description">
+ Automatically track new chapters, get notified of releases, and stay organised. Completed
+ and ongoing series stay neatly arranged so you can instantly see what needs your
+ attention.
+ </p>
+ <p class="demo-note">Simulated data shown</p>
+ </div>
+ </div>
+ </section>
-<Spacer />
+ <section class="landing-section" class:visible={sectionsVisible[1]} data-section="1">
+ <div class="section-row reverse">
+ <div class="section-info card">
+ <p class="section-label">Anime Tracking</p>
-<div class="example-item card">
- <div class="card item-description">
- <span class="big-text">Anime, Made Smarter</span>
+ <h2 class="section-title">Made Smarter</h2>
- <p>
- Track your anime effortlessly. <a href={root('/')}>due.moe</a> shows which episodes you still need
- to watch, keeps you updated on upcoming releases, and even counts down to the next subtitled episode—so
- you're always on time.
- </p>
+ <p class="section-description">
+ See what you need to watch, track upcoming releases, and count down to the next subtitled
+ episode. Never fall behind on seasonal anime again.
+ </p>
+ <p class="demo-note">Simulated data shown</p>
+ </div>
- <small class="bottom">
- This demo view contains simulated data which may include concluded anime.
- </small>
- </div>
- <div class="item-content">
- <details class="list" open>
- <CompletedAnimeList dummy disableFilter />
- </details>
- </div>
+ <div class="section-demo card" bind:this={animeContainer}>
+ <details class="list" open>
+ <CompletedAnimeList dummy disableFilter {dummyCount} limit={gridLimit} />
+ </details>
+ </div>
+ </div>
+ </section>
+
+ <section
+ class="landing-section tools-section"
+ class:visible={sectionsVisible[2]}
+ data-section="2"
+ >
+ <div class="section-row">
+ <div class="section-info card">
+ <p class="section-label">Beyond Tracking</p>
+
+ <h2 class="section-title">A Suite of Tools</h2>
+
+ <p class="section-description">
+ Everything you need to enhance your AniList experience. From year-in-review stats to
+ subtitle schedules, it's all here.
+ </p>
+
+ <a
+ href={`https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`}
+ class="cta"
+ onclick={async () => {
+ await localforage.setItem(
+ 'redirect',
+ window.location.origin + window.location.pathname + window.location.search
+ );
+ }}
+ >
+ Connect with AniList
+ </a>
+ </div>
+
+ <div class="tools-and-demo">
+ <div class="tools-grid">
+ <a href={root('/wrapped')} class="tool-card card">
+ <h3 class="tool-title">AniList Wrapped</h3>
+
+ <p class="tool-description">Your year on AniList, visualised</p>
+ </a>
+
+ <div class="tool-card card">
+ <h3 class="tool-title">Badge Wall</h3>
+
+ <p class="tool-description">All your badges in one place, including AWC</p>
+ </div>
+
+ <a href={root('/schedule')} class="tool-card card">
+ <h3 class="tool-title">Subtitle Schedule</h3>
+
+ <p class="tool-description">Know when subtitles drop for simulcasts</p>
+ </a>
+
+ <a href={root('/birthdays')} class="tool-card card">
+ <h3 class="tool-title">Character Birthdays</h3>
+
+ <p class="tool-description">Never miss your favourites' special day</p>
+ </a>
+
+ <a href={root('/tools/sequel_spy')} class="tool-card card">
+ <h3 class="tool-title">Sequel Spy</h3>
+
+ <p class="tool-description">Find prequels you might have missed</p>
+ </a>
+ </div>
+
+ <div
+ class="demo-card card"
+ onclick={() => (demoFocused = true)}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => e.key === 'Enter' && (demoFocused = true)}
+ >
+ <img src="https://i.imgur.com/j5vfKbx.gif" alt="Demo" title="Click to expand" />
+ </div>
+ </div>
+ </div>
+ </section>
</div>
-<Spacer />
-
-<div class="example-item card">
- <div class="item-content">
- <span class="big-text">Smarter Tools, Better Experience</span>
-
- <p>
- <a href={root('/')}>due.moe</a> isn't just tracking—it's a full suite of tools designed to enhance
- your AniList experience. From Wrapped to Sequel Spy, everything you need is right here.
- </p>
-
- <ul>
- <li><a href={root('/wrapped')}>AniList Wrapped</a> — Your Year on AniList</li>
- <li>
- Badge Wall — A unified badge collection experience for AniList
- <blockquote style="margin: 0 0 0 1.5rem;">
- Easily display all of your earned badges in a single place, with your Anime Watching Club
- (AWC) badges automatically included!
- </blockquote>
- </li>
- <li>
- <a href={root('/schedule')}>Subtitle Schedule</a> — A release calendar which displaying the
- scheduled <b>subtitle release times</b> for simulcast anime!
- </li>
- <li>
- <a href={root('/birthdays')}>Today's Character Birthdays</a> — A calendar to help you stay up
- to date with your favourite characters' birthdays, featuring an even bigger character database
- than AniList!
- </li>
- <li>
- <a href={root('/tools/sequel_spy')}>Sequel Spy</a> — Find media with prequels you haven't seen
- for any simulcast season
- </li>
- </ul>
-
- <br /><br />
-
- <span class="medium-text">
- <a
- href={`https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`}
- onclick={async () => {
- await localforage.setItem(
- 'redirect',
- window.location.origin + window.location.pathname + window.location.search
- );
- }}>Log in</a
- >
- with AniList, and <a href={root('/')}>due.moe</a> does the rest.
- </span>
- </div>
+{#if demoFocused}
+ <div
+ class="demo-overlay"
+ onclick={() => (demoFocused = false)}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => e.key === 'Escape' && (demoFocused = false)}
+ >
+ <div class="demo-focused">
+ <img src="https://i.imgur.com/j5vfKbx.gif" alt="Demo" />
- <div class="item-description demo">
- <a href="https://imgur.com/j5vfKbx.mp4" target="_blank">
- <img src="https://imgur.com/j5vfKbx.gif" alt="Demo" title="Demo" use:tooltip />
- </a>
+ <p class="demo-hint">Click anywhere to close</p>
+ </div>
</div>
-</div>
+{/if}
<style>
- .example-item {
+ .landing {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ }
+
+ .landing-section {
+ opacity: 0;
+ transform: translateY(30px);
+ transition:
+ opacity 0.6s ease,
+ transform 0.6s ease;
+ }
+
+ .landing-section.visible {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ .section-row {
display: flex;
- flex-wrap: wrap;
+ gap: 1rem;
}
- .demo {
+ .section-row.reverse {
+ flex-direction: row-reverse;
+ }
+
+ .section-demo {
+ flex: 1 1 60%;
+ min-width: 0;
+ }
+
+ .section-info {
+ flex: 1 1 40%;
display: flex;
flex-direction: column;
- justify-content: center;
- padding: 1rem;
}
- .demo img {
- border-radius: 8px;
- margin: 0.15rem;
+ .section-label {
+ font-size: 0.85rem;
+ letter-spacing: 0.15em;
+ color: var(--base04);
+ margin: 0 0 0.75rem 0;
+ font-weight: 500;
+ }
+
+ .section-title {
+ font-size: clamp(1.5rem, 4vw, 2rem);
+ font-weight: 700;
+ margin: 0 0 1rem 0;
+ color: var(--base06);
+ line-height: 1.2;
+ letter-spacing: -0.02em;
+ }
+
+ .section-description {
+ font-size: 1rem;
+ color: var(--base04);
+ margin: 0 0 1.5rem 0;
+ line-height: 1.6;
+ }
+
+ .demo-note {
+ font-size: 0.8rem;
+ color: var(--base03);
+ margin: auto 0 0 0;
+ }
+
+ .tools-section .section-info {
+ flex: 0 0 420px;
+ }
+
+ .tools-and-demo {
+ flex: 1;
+ display: grid;
+ grid-template-columns: 1fr 1fr 2fr;
+ grid-template-rows: auto auto auto;
+ gap: 1rem;
+ min-width: 0;
+ }
+
+ .tools-grid {
+ display: contents;
+ }
+
+ .tool-card {
+ display: flex;
+ flex-direction: column;
+ padding: 1.25rem;
+ text-decoration: none;
+ color: inherit;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
+ }
+
+ .tool-card:hover {
+ transform: translateY(-3px);
+ text-decoration: none;
+ }
+
+ .tool-title {
+ font-size: 1rem;
+ font-weight: 600;
+ margin: 0 0 0.35rem 0;
+ color: var(--base06);
+ }
+
+ .tool-description {
+ font-size: 0.85rem;
+ color: var(--base04);
+ margin: 0;
+ line-height: 1.4;
+ }
+
+ .demo-card {
+ grid-column: 3;
+ grid-row: 1 / 4;
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ aspect-ratio: 230 / 123;
+ cursor: pointer;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
+ }
+
+ .demo-card:hover {
+ transform: scale(1.02);
+ }
+
+ .demo-card img {
width: 100%;
+ height: 100%;
+ object-fit: fill;
+ border-radius: 8px;
+ }
+
+ .cta {
+ display: inline-block;
+ padding: 0.75rem 1.5rem;
+ background-color: var(--base06);
+ color: var(--base00);
+ font-weight: 600;
+ font-size: 0.9rem;
+ border-radius: 6px;
+ text-decoration: none;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
+ align-self: flex-start;
+ }
+
+ .cta:hover {
+ text-decoration: none;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ }
+
+ .demo-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.85);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ cursor: pointer;
+ animation: fadeIn 0.2s ease;
+ }
+
+ .demo-focused {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ animation: scaleIn 0.2s ease;
}
- .item-content {
- flex: 1 1 50%;
+ .demo-focused img {
+ max-width: 90vw;
+ max-height: 80vh;
+ border-radius: 12px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
- .item-description {
- flex: 1 1 50%;
+ .demo-hint {
+ color: var(--base03);
+ font-size: 0.85rem;
+ margin: 0;
}
- .medium-text {
- font-size: 1.125rem;
+ @keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
}
- .big-text {
- font-size: 1.25rem;
+ @keyframes scaleIn {
+ from {
+ transform: scale(0.9);
+ opacity: 0;
+ }
+
+ to {
+ transform: scale(1);
+ opacity: 1;
+ }
}
- .bottom {
- position: absolute;
- bottom: 1em;
+ @media (max-width: 1000px) {
+ .tools-and-demo {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .demo-card {
+ grid-column: 2;
+ grid-row: 1 / 4;
+ }
+ }
+
+ @media (max-width: 800px) {
+ .section-row,
+ .section-row.reverse {
+ flex-direction: column;
+ }
+
+ .section-demo,
+ .section-info {
+ flex: 1 1 100%;
+ }
+
+ .section-info {
+ order: -1;
+ }
+
+ .tools-section .section-info {
+ flex: 1 1 100%;
+ }
+
+ .tools-and-demo {
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: auto auto auto;
+ }
+
+ .demo-card {
+ grid-column: 1 / 3;
+ grid-row: auto;
+ max-height: 250px;
+ }
+ }
+
+ @media (max-width: 500px) {
+ .tools-and-demo {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-card {
+ grid-column: 1;
+ }
}
</style>
diff --git a/src/lib/List/Anime/AnimeListTemplate.svelte b/src/lib/List/Anime/AnimeListTemplate.svelte
index 6e8d6d6c..bcd4e806 100644
--- a/src/lib/List/Anime/AnimeListTemplate.svelte
+++ b/src/lib/List/Anime/AnimeListTemplate.svelte
@@ -31,6 +31,7 @@
export let notYetReleased = false;
export let dummy = false;
export let disableFilter = false;
+ export let limit: number | undefined = undefined;
let lastUpdatedMedia = -1;
let previousAnimeList: Media[];
@@ -69,6 +70,7 @@
bind:pendingUpdate
{dummy}
{disableFilter}
+ {limit}
/>
{:else}
<PlaceholderList count={lastListSize} {title} />
@@ -91,6 +93,7 @@
bind:pendingUpdate
{dummy}
{disableFilter}
+ {limit}
/>
{/if}
{:catch}
diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte
index fef76c03..04b1d03c 100644
--- a/src/lib/List/Anime/CleanAnimeList.svelte
+++ b/src/lib/List/Anime/CleanAnimeList.svelte
@@ -33,6 +33,7 @@
export let notYetReleased = false;
export let dummy = false;
export let disableFilter = false;
+ export let limit: number | undefined = undefined;
let showRoulette = false;
let keyCacher: NodeJS.Timeout;
@@ -183,6 +184,7 @@
{upcoming}
{notYetReleased}
reverseSort={$settings.displayReverseSort}
+ {limit}
>
<div slot="title" let:title={anime} let:progress>
{#if !upcoming && !notYetReleased}
diff --git a/src/lib/List/Anime/CompletedAnimeList.svelte b/src/lib/List/Anime/CompletedAnimeList.svelte
index 8e914ef6..b9e86ba0 100644
--- a/src/lib/List/Anime/CompletedAnimeList.svelte
+++ b/src/lib/List/Anime/CompletedAnimeList.svelte
@@ -18,7 +18,9 @@
tokenType: ''
};
export let dummy = false;
+ export let dummyCount = 7;
export let disableFilter = false;
+ export let limit: number | undefined = undefined;
let animeLists: Promise<Media[]>;
let startTime: number;
let endTime: number;
@@ -27,40 +29,29 @@
startTime = performance.now();
if (dummy) {
+ // Use deterministic selection for consistent display
+ const filtered = sampleAnime.filter(
+ (anime) =>
+ anime.episodes &&
+ !anime.tags.some((tag) => tag.name === 'Nudity') &&
+ !anime.tags.some((tag) => tag.name === 'Rape') &&
+ !anime.tags.some((tag) => tag.name === 'Tragedy') &&
+ !anime.tags.some((tag) => tag.name === 'Bondage') &&
+ !anime.genres.some((genre) => genre === 'Hentai') &&
+ anime.genres.some((genre) => genre === 'Comedy') &&
+ anime.status !== 'NOT_YET_RELEASED' &&
+ anime.episodes > 1
+ );
animeLists = Promise.resolve(
- sampleAnime
- .filter(
- (anime) =>
- anime.episodes &&
- !anime.tags.some((tag) => tag.name === 'Nudity') &&
- !anime.tags.some((tag) => tag.name === 'Rape') &&
- !anime.tags.some((tag) => tag.name === 'Tragedy') &&
- !anime.tags.some((tag) => tag.name === 'Bondage') &&
- !anime.genres.some((genre) => genre === 'Hentai') &&
- anime.genres.some((genre) => genre === 'Comedy') &&
- anime.status !== 'NOT_YET_RELEASED' &&
- anime.episodes > 1
- )
- .sort(() => 0.5 - Math.random())
- .map((anime) => {
- const nextEpisode = Math.floor(Math.random() * (anime.episodes || 0)) + 1 || 1;
-
- anime.status = 'FINISHED';
- anime.nextAiringEpisode = {
- airingAt:
- Math.floor(Date.now() / 1000) +
- Math.floor(Math.random() * 7 * 24 * 60 * 60) +
- 60 * 60,
- episode: Math.floor(Math.random() * (anime.episodes || 0)) + 1 || 1
- };
- anime.mediaListEntry.progress = Math.floor(Math.random() * nextEpisode) || 1;
-
- return anime;
- })
- .sort(
- (a, b) => (a.nextAiringEpisode?.airingAt || 0) - (b.nextAiringEpisode?.airingAt || 0)
- )
- .slice(0, 7) as unknown as Media[]
+ filtered.slice(0, dummyCount).map((anime, index) => {
+ anime.status = 'FINISHED';
+ anime.nextAiringEpisode = {
+ airingAt: Math.floor(Date.now() / 1000) + (index + 1) * 24 * 60 * 60,
+ episode: Math.floor((anime.episodes || 12) * 0.8)
+ };
+ anime.mediaListEntry.progress = Math.floor((anime.nextAiringEpisode.episode || 5) * 0.6);
+ return anime;
+ }) as unknown as Media[]
);
} else {
animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, {
@@ -127,4 +118,5 @@
completed
{dummy}
{disableFilter}
+ {limit}
/>
diff --git a/src/lib/List/CleanGrid.svelte b/src/lib/List/CleanGrid.svelte
index 119e4d71..ed4a9733 100644
--- a/src/lib/List/CleanGrid.svelte
+++ b/src/lib/List/CleanGrid.svelte
@@ -13,10 +13,12 @@
export let upcoming = false;
export let notYetReleased = false;
export let reverseSort = false;
+ export let limit: number | undefined = undefined;
let uniqueID = new Date().getTime();
- $: processedMedia = reverseSort ? media.reverse() : media;
+ $: sortedMedia = reverseSort ? media.reverse() : media;
+ $: processedMedia = limit !== undefined ? sortedMedia.slice(0, limit) : sortedMedia;
</script>
<div
diff --git a/src/lib/List/Manga/CleanMangaList.svelte b/src/lib/List/Manga/CleanMangaList.svelte
index c337a053..f5af25fc 100644
--- a/src/lib/List/Manga/CleanMangaList.svelte
+++ b/src/lib/List/Manga/CleanMangaList.svelte
@@ -33,6 +33,7 @@
export let authorised: boolean;
export let dummy = false;
export let disableFilter = false;
+ export let limit: number | undefined = undefined;
let showRoulette = false;
let serviceStatusResponse: Promise<Response>;
@@ -165,7 +166,7 @@
{/if}
{#if $settings.displayCoverModeManga || dummy}
- <CleanGrid media={filteredMedia} {dummy} type="manga">
+ <CleanGrid media={filteredMedia} {dummy} type="manga" {limit}>
<div slot="title" let:title={manga} let:progress>
{pendingUpdate === manga.id ? progress + 1 : progress}{#if !due}
<span class="opaque">/{manga.chapters || '?'}</span>
diff --git a/src/lib/List/Manga/MangaListTemplate.svelte b/src/lib/List/Manga/MangaListTemplate.svelte
index 16665d56..c2fc0513 100644
--- a/src/lib/List/Manga/MangaListTemplate.svelte
+++ b/src/lib/List/Manga/MangaListTemplate.svelte
@@ -31,7 +31,9 @@
export let displayUnresolved: boolean;
export let due: boolean;
export let dummy = $settings.debugDummyLists || false;
+ export let dummyCount = 7;
export let disableFilter = false;
+ export let limit: number | undefined = undefined;
const authorised = privilegedUser($identity.id);
let mangaLists: Promise<Media[]>;
let startTime: number;
@@ -67,28 +69,25 @@
startTime = performance.now();
if (dummy) {
+ // Use deterministic selection for consistent display
+ const filtered = sampleManga.filter(
+ (manga) =>
+ manga.chapters &&
+ !manga.tags.some((tag) => tag.name === 'Nudity') &&
+ !manga.tags.some((tag) => tag.name === 'Rape') &&
+ !manga.tags.some((tag) => tag.name === 'Tragedy') &&
+ !manga.tags.some((tag) => tag.name === 'Bondage') &&
+ !manga.genres.some((genre) => genre === 'Hentai') &&
+ manga.genres.some((genre) => genre === 'Comedy') &&
+ manga.status !== 'NOT_YET_RELEASED'
+ );
mangaLists = Promise.resolve(
- sampleManga
- .filter(
- (manga) =>
- manga.chapters &&
- !manga.tags.some((tag) => tag.name === 'Nudity') &&
- !manga.tags.some((tag) => tag.name === 'Rape') &&
- !manga.tags.some((tag) => tag.name === 'Tragedy') &&
- !manga.tags.some((tag) => tag.name === 'Bondage') &&
- !manga.genres.some((genre) => genre === 'Hentai') &&
- manga.genres.some((genre) => genre === 'Comedy') &&
- manga.status !== 'NOT_YET_RELEASED'
- )
- .sort(() => 0.5 - Math.random())
- .map((manga) => {
- manga.status = 'FINISHED';
- manga.episodes = Math.floor(Math.random() * (manga.chapters || 0)) as unknown as null;
- manga.mediaListEntry.progress = Math.floor(Math.random() * (manga.episodes || 0)) + 1;
-
- return manga;
- })
- .slice(0, 7) as unknown as Media[]
+ filtered.slice(0, dummyCount).map((manga) => {
+ manga.status = 'FINISHED';
+ manga.episodes = Math.floor((manga.chapters || 10) * 0.7) as unknown as null;
+ manga.mediaListEntry.progress = Math.floor((manga.episodes || 5) * 0.5) + 1;
+ return manga;
+ }) as unknown as Media[]
);
} else {
mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, {
@@ -262,6 +261,7 @@
{authorised}
{dummy}
{disableFilter}
+ {limit}
/>
{:else}
{#if !authorised}
@@ -310,6 +310,7 @@
{authorised}
{dummy}
{disableFilter}
+ {limit}
/>
{:else}
{#if !authorised}
@@ -378,6 +379,7 @@
{authorised}
{dummy}
{disableFilter}
+ {limit}
/>
{:catch}
{#if authorised}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index a66d10c2..ca356724 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -36,11 +36,7 @@
{#if data.user === undefined}
<LandingHero />
- <Spacer />
-
- <div class="card">Please log in to view due media.</div>
-
- <Spacer />
+ <Spacer size="lg" />
<Landing />
{:else}