Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Migrate to svelte-infinite-loading #96

Merged
merged 11 commits into from
Sep 17, 2024
16 changes: 7 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"mode-watcher": "^0.4.0",
"paneforge": "^0.0.5",
"posthog-js": "^1.160.3",
"svelte-infinite": "^0.3.2",
"svelte-infinite-loading": "^1.4.0",
"svelte-sonner": "^0.3.26",
"tailwind-merge": "^2.4.0",
"tailwind-variants": "^0.2.1",
Expand Down
25 changes: 25 additions & 0 deletions src/lib/components/DefaultInfiniteLoader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import Separator from '$lib/components/ui/separator/separator.svelte';
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
import LoaderCircle from 'virtual:icons/lucide/loader-circle';

type PropsType = {
loadMore: (event: InfiniteEvent) => Promise<void>;
identifier: unknown;
entityPlural: string;
};

let { loadMore, identifier, entityPlural }: PropsType = $props();
</script>

<InfiniteLoading on:infinite={loadMore} {identifier}>
<div class="flex items-center justify-center gap-2 py-2 text-muted-foreground" slot="noMore">
<Separator class="w-20" />
That's all
<Separator class="w-20" />
</div>
<div class="muted-text-box text-left" slot="noResults">No {entityPlural} found</div>
<div slot="spinner">
<LoaderCircle class="mx-auto my-2 animate-spin" />
</div>
</InfiniteLoading>
11 changes: 0 additions & 11 deletions src/routes/exercise-splits/+page.server.ts

This file was deleted.

101 changes: 38 additions & 63 deletions src/routes/exercise-splits/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,52 +1,50 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { Badge } from '$lib/components/ui/badge/index.js';
import Button from '$lib/components/ui/button/button.svelte';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { Input } from '$lib/components/ui/input';
import H2 from '$lib/components/ui/typography/H2.svelte';
import { trpc } from '$lib/trpc/client';
import type { RouterOutputs } from '$lib/trpc/router.js';
import type { InfiniteEvent } from 'svelte-infinite-loading';
import AddIcon from 'virtual:icons/lucide/plus';
import SearchIcon from 'virtual:icons/lucide/search';
import LoaderCircle from 'virtual:icons/lucide/loader-circle';
import { afterNavigate, goto } from '$app/navigation';
import { trpc } from '$lib/trpc/client';
import { InfiniteLoader, loaderState } from 'svelte-infinite';
import { page } from '$app/stores';
import type { ExerciseSplit, ExerciseSplitDay } from '@prisma/client';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
import Separator from '$lib/components/ui/separator/separator.svelte';
import DefaultInfiniteLoader from '../../lib/components/DefaultInfiniteLoader.svelte';
import { exerciseSplitRunes } from './manage/exerciseSplitRunes.svelte.js';

type ExerciseSplitsWithSplitDays = (ExerciseSplit & { exerciseSplitDays: ExerciseSplitDay[] })[];

let { data } = $props();
let exerciseSplits: ExerciseSplitsWithSplitDays | 'loading' = $state('loading');
let exerciseSplits: RouterOutputs['exerciseSplits']['load'] = $state([]);
let searchString = $state($page.url.searchParams.get('search') ?? '');

afterNavigate(async () => {
loaderState.reset();
exerciseSplits = await data.exerciseSplits;
if (exerciseSplits.length !== 10) loaderState.complete();
});

function updateSearchParam(e: Event) {
e.preventDefault();
const url = new URL($page.url);
if (searchString === (url.searchParams.get('search') ?? '')) return;

if (searchString) url.searchParams.set('search', searchString);
else url.searchParams.delete('search');

exerciseSplits = 'loading';
exerciseSplits = [];
goto(url);
}

async function loadMore() {
async function loadMore(infiniteEvent: InfiniteEvent) {
const lastExerciseSplit = exerciseSplits.at(-1);
if (typeof lastExerciseSplit === 'string' || lastExerciseSplit === undefined) return;

const newExerciseSplits = await trpc().exerciseSplits.load.query({
cursorId: lastExerciseSplit.id
cursorId: lastExerciseSplit?.id,
searchString
});
if (exerciseSplits !== 'loading') exerciseSplits.push(...newExerciseSplits);
if (newExerciseSplits.length !== 10) loaderState.complete();

if (newExerciseSplits.length === 0) {
infiniteEvent.detail.complete();
return;
}

infiniteEvent.detail.loaded();
exerciseSplits.push(...newExerciseSplits);
if (newExerciseSplits.length < 10) infiniteEvent.detail.complete();
}

function createNewExerciseSplit() {
Expand Down Expand Up @@ -78,43 +76,20 @@
</DropdownMenu.Root>
</div>
<div class="flex h-px grow flex-col gap-1 overflow-y-auto">
{#if exerciseSplits === 'loading'}
{#each Array(10) as _}
<div class="flex h-12 items-center justify-between rounded-md border bg-card p-2">
<Skeleton class="text-lg-skeleton" />
<Skeleton class="badge-skeleton" />
</div>
{/each}
{:else}
<InfiniteLoader triggerLoad={loadMore}>
{#each exerciseSplits as exerciseSplit}
<Button
class="mb-1 flex h-12 items-center justify-between rounded-md border bg-card p-2"
href="/exercise-splits/{exerciseSplit.id}"
variant="outline"
>
<span class="truncate text-lg font-semibold">{exerciseSplit.name}</span>
<Badge>{exerciseSplit.exerciseSplitDays.length} days / cycle</Badge>
</Button>
{:else}
<div class="muted-text-box">No exercise splits found</div>
{/each}
{#snippet loading()}
<LoaderCircle class="animate-spin" />
{/snippet}
{#snippet error(load)}
<Button onclick={load} variant="outline">An error occurred. Retry?</Button>
{/snippet}
{#snippet noData()}
{#if exerciseSplits.length > 0}
<div class="flex items-center justify-start gap-2 font-semibold text-muted-foreground">
<Separator class="h-0.5 w-20" />
<span class="whitespace-nowrap">That's all!</span>
<Separator class="h-0.5 w-20" />
</div>
{/if}
{/snippet}
</InfiniteLoader>
{/if}
{#each exerciseSplits as exerciseSplit}
<Button
class="flex h-12 items-center justify-between rounded-md border bg-card p-2"
href="/exercise-splits/{exerciseSplit.id}"
variant="outline"
>
<span class="truncate text-lg font-semibold">{exerciseSplit.name}</span>
<Badge>{exerciseSplit.exerciseSplitDays.length} days / cycle</Badge>
</Button>
{/each}
<DefaultInfiniteLoader
{loadMore}
identifier={$page.url.searchParams.get('search')}
entityPlural="exercise splits"
/>
</div>
</div>
7 changes: 1 addition & 6 deletions src/routes/mesocycles/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import { createContext } from '$lib/trpc/context';
import { createCaller } from '$lib/trpc/router';

export const load = async (event) => {
event.depends('mesocycles:all');
event.depends('mesocycles:active');
const trpc = createCaller(await createContext(event));
const searchString = event.url.searchParams.get('search') ?? undefined;

return {
mesocycles: trpc.mesocycles.load({ searchString }),
activeMesocycle: trpc.mesocycles.findActiveMesocycle()
};
return { activeMesocycle: trpc.mesocycles.findActiveMesocycle() };
};
103 changes: 41 additions & 62 deletions src/routes/mesocycles/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,48 +1,54 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import DefaultInfiniteLoader from '$lib/components/DefaultInfiniteLoader.svelte';
import { Badge } from '$lib/components/ui/badge/index.js';
import Button from '$lib/components/ui/button/button.svelte';
import { Input } from '$lib/components/ui/input';
import H2 from '$lib/components/ui/typography/H2.svelte';
import LoaderCircle from 'virtual:icons/lucide/loader-circle';
import AddIcon from 'virtual:icons/lucide/plus';
import SearchIcon from 'virtual:icons/lucide/search';
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import Separator from '$lib/components/ui/separator/separator.svelte';
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
import H2 from '$lib/components/ui/typography/H2.svelte';
import { trpc } from '$lib/trpc/client.js';
import type { Mesocycle } from '@prisma/client';
import { InfiniteLoader, loaderState } from 'svelte-infinite';
import { Badge } from '$lib/components/ui/badge/index.js';
import { onMount } from 'svelte';
import type { InfiniteEvent } from 'svelte-infinite-loading';
import AddIcon from 'virtual:icons/lucide/plus';
import SearchIcon from 'virtual:icons/lucide/search';
import { mesocycleRunes } from './manage/mesocycleRunes.svelte.js';

let { data } = $props();
let activeMesocycle: Pick<Mesocycle, 'id' | 'name'> | null | 'loading' = $state('loading');
let mesocycles: Mesocycle[] | 'loading' = $state('loading');
let mesocycles: Mesocycle[] = $state([]);
let searchString = $state($page.url.searchParams.get('search') ?? '');

afterNavigate(async () => {
loaderState.reset();
[activeMesocycle, mesocycles] = await Promise.all([data.activeMesocycle, data.mesocycles]);
if (mesocycles.length !== 10) loaderState.complete();
onMount(async () => {
activeMesocycle = await data.activeMesocycle;
});

function updateSearchParam(e: Event) {
e.preventDefault();
const url = new URL($page.url);
if (searchString === (url.searchParams.get('search') ?? '')) return;

if (searchString) url.searchParams.set('search', searchString);
else url.searchParams.delete('search');

mesocycles = 'loading';
mesocycles = [];
goto(url);
}

async function loadMore() {
async function loadMore(infiniteEvent: InfiniteEvent) {
const lastMesocycle = mesocycles.at(-1);
if (typeof lastMesocycle === 'string' || lastMesocycle === undefined) return;
const newMesocycles = await trpc().mesocycles.load.query({ cursorId: lastMesocycle?.id, searchString });

if (newMesocycles.length === 0) {
infiniteEvent.detail.complete();
return;
}

const newMesocycles = await trpc().mesocycles.load.query({ cursorId: lastMesocycle.id });
if (mesocycles !== 'loading') mesocycles.push(...newMesocycles);
if (newMesocycles.length !== 10) loaderState.complete();
infiniteEvent.detail.loaded();
mesocycles.push(...newMesocycles);
if (newMesocycles.length < 10) infiniteEvent.detail.complete();
}

function createNewMesocycle() {
Expand Down Expand Up @@ -92,49 +98,22 @@
<Separator class="w-px grow" />
</div>
<div class="flex h-px grow flex-col gap-1 overflow-y-auto">
{#if mesocycles === 'loading'}
{#each Array(10) as _}
<div class="flex h-12 items-center justify-between rounded-md border bg-card p-2">
<Skeleton class="text-lg-skeleton" />
<Skeleton class="badge-skeleton" />
</div>
{/each}
{:else}
<InfiniteLoader triggerLoad={loadMore}>
{#each mesocycles as mesocycle}
<Button
class="mb-1 flex h-12 items-center justify-between rounded-md border bg-card p-2"
href="/mesocycles/{mesocycle.id}"
variant="outline"
>
<span class="text-lg font-semibold">{mesocycle.name}</span>
{#if !mesocycle.startDate}
<Badge variant="secondary">Unused</Badge>
{:else if !mesocycle.endDate}
<Badge>Active</Badge>
{:else}
<Badge variant="outline">Completed</Badge>
{/if}
</Button>
{#each mesocycles as mesocycle}
<Button
class="flex h-12 items-center justify-between rounded-md border bg-card p-2"
href="/mesocycles/{mesocycle.id}"
variant="outline"
>
<span class="text-lg font-semibold">{mesocycle.name}</span>
{#if !mesocycle.startDate}
<Badge variant="secondary">Unused</Badge>
{:else if !mesocycle.endDate}
<Badge>Active</Badge>
{:else}
<div class="muted-text-box">No mesocycles found</div>
{/each}
{#snippet loading()}
<LoaderCircle class="animate-spin" />
{/snippet}
{#snippet error(load)}
<Button onclick={load} variant="outline">An error occurred. Retry?</Button>
{/snippet}
{#snippet noData()}
{#if mesocycles.length > 0}
<div class="flex items-center justify-start gap-2 font-semibold text-muted-foreground">
<Separator class="h-0.5 w-20" />
<span class="whitespace-nowrap">That's all!</span>
<Separator class="h-0.5 w-20" />
</div>
{/if}
{/snippet}
</InfiniteLoader>
{/if}
<Badge variant="outline">Completed</Badge>
{/if}
</Button>
{/each}
<DefaultInfiniteLoader {loadMore} identifier={$page.url.searchParams.get('search')} entityPlural="mesocycles" />
</div>
</div>
Loading
Loading