aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Landing.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Landing.svelte')
-rw-r--r--src/lib/Landing.svelte553
1 files changed, 440 insertions, 113 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>