Skip to content

Commit

Permalink
WIP: loading class instance
Browse files Browse the repository at this point in the history
  • Loading branch information
gwennlbh committed Nov 11, 2024
1 parent d0e65d8 commit 3b9f650
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 52 deletions.
4 changes: 2 additions & 2 deletions packages/app/src/lib/components/AvatarGroup.houdini.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { fragment, graphql, PendingValue, type AvatarGroup } from '$houdini';
import Avatar from '$lib/components/Avatar.svelte';
import LoadingText from '$lib/components/LoadingText.svelte';
import { loading, mapLoading, type MaybeLoading } from '$lib/loading';
import { Loading, loading, mapLoading, type MaybeLoading } from '$lib/loading';
import { refroute } from '$lib/navigation';
import { isDark } from '$lib/theme';
Expand Down Expand Up @@ -34,7 +34,7 @@
{...$$restProps}
{src}
href=""
alt={mapLoading($data?.name ?? PendingValue, (n) => `Logo de ${n}`)}
alt={new Loading($data?.name).then(name => `Logo de ${name}`)}
help={$data?.name}
/>
<LoadingText value={$data?.name} />
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/lib/components/BookingBeneficiary.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import { fragment, graphql, type BookingBeneficiary } from '$houdini';
import { graphql, type BookingBeneficiary } from '$houdini';
import AvatarUser from '$lib/components/AvatarUser.svelte';
import { LoadingText, allLoaded } from '$lib/loading';
import { LoadingText, allLoaded, loadingFragment } from '$lib/loading';
/** Whether to use the UIDs instead of the full names */
export let shortName = false;
$: nameProperty = (shortName ? 'uid' : 'fullName') as 'uid' | 'fullName';
export let booking: BookingBeneficiary;
$: data = fragment(
$: data = loadingFragment(
booking,
graphql(`
fragment BookingBeneficiary on Registration @loading {
Expand Down
14 changes: 7 additions & 7 deletions packages/app/src/lib/components/BookingPaymentMethod.svelte
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<script lang="ts">
import { fragment, graphql, type BookingPaymentMethod } from '$houdini';
import { graphql, type BookingPaymentMethod } from '$houdini';
import { DISPLAY_PAYMENT_METHODS, ICONS_PAYMENT_METHODS } from '$lib/display';
import { loaded, LoadingText } from '$lib/loading';
import { loadingFragment, LoadingText } from '$lib/loading';
export let booking: BookingPaymentMethod | null;
$: data = fragment(
$: data = loadingFragment(
booking,
graphql(`
fragment BookingPaymentMethod on Registration {
fragment BookingPaymentMethod on Registration @loading {
paymentMethod
}
`),
);
</script>

<div class="payment-method">
{#if $data && loaded($data.paymentMethod)}
<svelte:component this={ICONS_PAYMENT_METHODS[$data.paymentMethod]}></svelte:component>
<span class="payment-method-name">{DISPLAY_PAYMENT_METHODS[$data.paymentMethod]}</span>
{#if $data?.loaded()}
<svelte:component this={ICONS_PAYMENT_METHODS[$data.v.paymentMethod]}></svelte:component>
<span class="payment-method-name">{DISPLAY_PAYMENT_METHODS[$data.v.paymentMethod]}</span>
{:else}
<LoadingText />
{/if}
Expand Down
55 changes: 24 additions & 31 deletions packages/app/src/lib/components/BookingStatus.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<script lang="ts">
import { fragment, graphql, PendingValue, type BookingStatus } from '$houdini';
import { allLoaded, mapAllLoading, LoadingText } from '$lib/loading';
import { graphql, type BookingStatus } from '$houdini';
import LoadingTextNew from '$lib/components/LoadingTextNew.svelte';
import { loadingFragment, LoadingText } from '$lib/loading';
import IconPaid from '~icons/msl/check';
import IconCancelled from '~icons/msl/close';
import IconOpposed from '~icons/msl/do-not-disturb-on-outline';
import IconVerified from '~icons/msl/done-all';
import IconWaitingForPayment from '~icons/msl/more-horiz';
export let booking: BookingStatus | null;
$: data = fragment(
$: data = loadingFragment(
booking,
graphql(`
fragment BookingStatus on Registration {
fragment BookingStatus on Registration @loading {
opposed
verified
cancelled
Expand All @@ -22,45 +23,37 @@
</script>

<div
class="booking-status {$data?.opposed
? 'danger'
: $data?.verified
? 'primary'
: $data?.cancelled
? 'warning'
: $data?.paid
? 'success'
: 'warning'}"
class="booking-status {$data?.then(({ opposed, verified, cancelled, paid }) => {
if (opposed) return 'danger';
if (verified) return 'primary';
if (cancelled) return 'warning';
if (paid) return 'success';
}) ?? 'warning'}"
>
<div class="icon-booking-status">
{#if !allLoaded($data) || !$data}
{#if !$data?.loaded()}
<LoadingText>..</LoadingText>
{:else if $data.opposed}
{:else if $data.v.opposed}
<IconOpposed />
{:else if $data.verified}
{:else if $data.v.verified}
<IconVerified />
{:else if $data.cancelled}
{:else if $data.v.cancelled}
<IconCancelled />
{:else if $data.paid}
{:else if $data.v.paid}
<IconPaid />
{:else}
<IconWaitingForPayment />
{/if}
</div>

<LoadingText
value={$data
? mapAllLoading(
[$data.opposed, $data.verified, $data.cancelled, $data.paid],
(opposed, verified, cancelled, paid) => {
if (opposed) return 'En opposition';
if (verified) return 'Scannée';
if (cancelled) return 'Annulée';
if (paid) return 'Payée';
return 'En attente de paiement';
},
)
: PendingValue}
<LoadingTextNew
value={$data?.map(({ opposed, verified, cancelled, paid }) => {
if (opposed) return 'En opposition';
if (verified) return 'Scannée';
if (cancelled) return 'Annulée';
if (paid) return 'Payée';
return 'En attente de paiement';
})}
/>
</div>

Expand Down
65 changes: 65 additions & 0 deletions packages/app/src/lib/components/LoadingTextNew.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!-- Credits: https://github.com/nolimits4web/skeleton-elements -->
<script lang="ts">
import { Loading, LOREM_IPSUM } from '$lib/loading';
export let tag: string = 'span';
export let lines: number | undefined = undefined;
export let value: Loading<string | number> | null | undefined = new Loading();
let loadingTextSlotContent: HTMLSpanElement | null = null;
// Text to use as skeleton UI is either the text given in the default slot, or lines of lorem ipsum if lines is specified, or a fallback
function loadingTextLines() {
let output: string[] = [];
if (lines) output = LOREM_IPSUM.split('\n').slice(0, lines);
else if (loadingTextSlotContent?.textContent)
output = loadingTextSlotContent.textContent.split('\n');
output = output.filter(Boolean);
if (output.length > 0) return output;
return ['Chargement...'];
}
$: unwrapped = value?.unwrap(undefined);
</script>

{#if unwrapped === undefined || unwrapped === null}
<svelte:element
this={tag === 'code' ? 'span' : tag}
{...$$restProps}
class="skeleton-text skeleton-effect-wave"
>
{#each loadingTextLines() as line}
<span>{line}</span>
<br />
{/each}
<span bind:this={loadingTextSlotContent} style:display="none"><slot></slot></span>
</svelte:element>
{:else}
<slot name="loaded" value={unwrapped}>
<svelte:element this={tag} data-loaded {...$$restProps}>{unwrapped}</svelte:element>
</slot>
{/if}

<style>
.skeleton-text span {
/* no putting in a fallback generic font prevents unsupported skeleton text characters from showing up */
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: skeleton;
user-select: none;
&,
& * {
color: transparent;
letter-spacing: -0.03em;
background-color: var(--skeleton-ui-bg);
border-radius: 1000px;
}
}
[data-loaded] {
overflow: inherit;
text-overflow: inherit;
white-space: inherit;
}
</style>
71 changes: 62 additions & 9 deletions packages/app/src/lib/loading.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { dev } from '$app/environment';
import { PendingValue } from '$houdini';
import { fragment, FragmentStore, PendingValue, type Fragment } from '$houdini';
import { derived, type Readable } from 'svelte/store';

export { default as LoadingChurros } from '$lib/components/LoadingChurros.svelte';
export { default as LoadingSpinner } from '$lib/components/LoadingSpinner.svelte';
Expand Down Expand Up @@ -30,13 +31,16 @@ export function loading<T>(value: MaybeLoading<T>, fallback: T): T {
return value === PendingValue || value === null || value === undefined ? fallback : value;
}

export type AllLoaded<T> = T extends object
? { [K in keyof T]: AllLoaded<T[K]> }
: T extends unknown[]
? AllLoaded<T[number]>[]
: T extends typeof PendingValue
? never
: T;
export type AllLoaded<T> =
T extends Loading<infer U>
? U
: T extends object
? { [K in keyof T]: AllLoaded<T[K]> }
: T extends unknown[]
? AllLoaded<T[number]>[]
: T extends typeof PendingValue
? never
: T;

export type DeepMaybeLoading<T> = T extends object
? { [K in keyof T]: DeepMaybeLoading<T[K]> }
Expand Down Expand Up @@ -73,7 +77,7 @@ export function allLoaded<T>(value: T): value is AllLoaded<T> {
else if (typeof value === 'object' && value !== null)
return Object.values(value).every((item) => allLoaded(item));

return loaded(value);
return value instanceof Loading ? value.loaded() : loaded(value);
}

export function mapLoading<T, O>(
Expand Down Expand Up @@ -106,3 +110,52 @@ accusantium enim et repudiandae omnis cum dolorem nemo id quia facilis.
Et dolorem perferendis et rerum suscipit qui voluptatibus quia et nihil nostrum 33 omnis soluta.
Nam minus minima et perspiciatis velit et eveniet rerum et nihil voluptates aut eaque ipsa et
ratione facere!`;

export function loadingFragment<
Store extends FragmentStore<any, any, any>,
Data = Store extends FragmentStore<infer D, any, any> ? D : any,
>(fragmentRef: Fragment<any> | null, store: Store): Readable<null | Loading<Data>> {
return derived([fragment(fragmentRef, store)], ([$store]) =>
$store ? new Loading($store) : null,
);
}

export class Loading<T> {
v: typeof PendingValue | AllLoaded<T>;

constructor(value?: T) {
if (value instanceof Loading) this.v = value.v;
if (value !== undefined && allLoaded(value)) this.v = value;
this.v = PendingValue;
}

static collect<Value>(values: Array<Loading<Value>>): Loading<Value[]> {
if (values.some((v) => !v.loaded())) return new Loading([PendingValue] as Value[]);
return new Loading(values as Value[]);
}

loading(): this is { v: typeof PendingValue } {
return !this.loaded();
}

loaded(): this is { v: AllLoaded<T> } {
if (simulatingLoadingState()) return false;
return this.v !== PendingValue;
}

map<O>(mapper: (value: AllLoaded<T>) => O): Loading<O> {
return new Loading(this.loaded() ? mapper(this.v) : PendingValue);
}

unwrap<Fallback>(fallback: Fallback): T | Fallback;
unwrap(): T | undefined;
unwrap<Fallback>(fallback?: Fallback) {
return this.loaded() ? this.v : fallback;
}

then<Out, Fallback>(mapper: (value: AllLoaded<T>) => Out, fallback: Fallback): T | Fallback;
then<Out>(mapper: (value: AllLoaded<T>) => Out): T | undefined;
then<Out, Fallback>(mapper: (value: AllLoaded<T>) => Out, fallback?: Fallback) {
return this.map(mapper).unwrap(fallback);
}
}

0 comments on commit 3b9f650

Please sign in to comment.