Add more visual hints for drag and drop, remove unneeded loading button hackery

This commit is contained in:
Mihkel Martin Kasterpalu 2025-01-21 19:52:19 +02:00
parent 9c2664d0da
commit 3c0b3df3b2
2 changed files with 97 additions and 74 deletions

View file

@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$lib/components/ui/card/index.js'; import * as Card from '$lib/components/ui/card/index.js';
import { dndzone } from 'svelte-dnd-action'; import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { expoOut } from 'svelte/easing'; import { expoOut } from 'svelte/easing';
import { truncate } from '$lib/utils'; import { truncate } from '$lib/utils';
import { fade } from 'svelte/transition';
let { items = $bindable(), image = false, type = 'default' } = $props(); let { items = $bindable(), image = false, type = 'default' } = $props();
@ -15,53 +16,81 @@
function handleDndFinalize(e: CustomEvent<any>) { function handleDndFinalize(e: CustomEvent<any>) {
items = e.detail.items; items = e.detail.items;
} }
function transformDraggedElement(draggedEl: HTMLElement | undefined) {
if (!draggedEl) {
return;
}
const card = draggedEl.querySelector('.bg-card') as HTMLElement;
if (!card) {
return;
}
card.classList.add('shadow-foreground/25');
}
let cardClass = $derived(
`hover select-none overflow-hidden rounded-xl border bg-card text-card-foreground shadow shadow-foreground/15 ${
type === 'names'
? 'border-red-400 '
: type === 'artists'
? 'border-purple-400 '
: 'border-blue-400'
}`
);
</script> </script>
{#snippet card(item, i)}
{#if image}
<img class="aspect-square w-full object-cover" alt="Album Art" src={item.value} />
<input type="hidden" name="{type}_{i}" value={item.value} />
{:else}
<p
class="p-6 text-center {type === 'names'
? 'text-red-900 dark:text-red-200'
: type === 'artists'
? 'text-purple-900 dark:text-purple-200'
: ''}"
>
{#if type === 'artists'}
{truncate(item.value, 30)}
{:else}
{truncate(item.value, 45)}
{/if}
</p>
<input type="hidden" name="{type}_{i}" value={item.value} />
{/if}
{/snippet}
<section <section
use:dndzone={{ use:dndzone={{
items, items,
flipDurationMs, flipDurationMs,
type: type, type: type,
dropTargetStyle: {} dropTargetStyle: {},
dropTargetClasses: ['bg-muted/50', 'dark:bg-muted/25', 'ring-2', 'ring-muted'],
transformDraggedElement: transformDraggedElement
}} }}
onconsider={handleDndConsider} onconsider={handleDndConsider}
onfinalize={handleDndFinalize} onfinalize={handleDndFinalize}
class="grid grid-cols-3 items-center gap-4 sm:gap-8 md:gap-10 lg:gap-14" class="grid grid-cols-3 items-center gap-2 rounded-xl p-3 transition-colors sm:gap-6 md:gap-8 lg:gap-12 xl:gap-14"
> >
{#each items as item, i (item.id)} {#each items as item, i (item.id)}
<div animate:flip={{ duration: flipDurationMs, easing: expoOut }}> <div animate:flip={{ duration: flipDurationMs, easing: expoOut }} class="relative">
<Card.Root {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
class="select-none overflow-hidden rounded-xl border bg-card text-card-foreground shadow shadow-foreground/15 transition-shadow hover:shadow-foreground/25 {type === <div
'names' in:fade={{ duration: 200, easing: expoOut }}
? 'border-red-400 ' class="{cardClass} visible border-transparent bg-transparent opacity-50 shadow-transparent"
: type === 'artists' >
? 'border-purple-400 ' {@render card(item, i)}
: 'border-blue-400'}" </div>
> {:else}
{#if image} <Card.Root class={cardClass}>
<Card.Content class="p-0"> <Card.Content class="p-0">
<img class="aspect-square w-full object-cover" alt="Album Art" src={item.value} /> {@render card(item, i)}
<input type="hidden" name="{type}_{i}" value={item.value} />
</Card.Content> </Card.Content>
{:else} </Card.Root>
<Card.Content> {/if}
<p
class="text-center {type === 'names'
? 'text-red-900 dark:text-red-200'
: type === 'artists'
? 'text-purple-900 dark:text-purple-200'
: ''}"
>
{#if type === 'artists'}
{truncate(item.value, 30)}
{:else}
{truncate(item.value, 45)}
{/if}
</p>
<input type="hidden" name="{type}_{i}" value={item.value} />
</Card.Content>
{/if}
</Card.Root>
</div> </div>
{/each} {/each}
</section> </section>

View file

@ -8,17 +8,11 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
let { data, form }: { data: PageData; form: FormData } = $props(); let { data }: { data: PageData; form: FormData } = $props();
let loading = $state(true); let loading = $state(false);
let oldAlbums: SpotifyApi.AlbumObjectSimplified[] = $state([]); let oldAlbums: SpotifyApi.AlbumObjectSimplified[] = $state([]);
$effect(() => {
// this is a hack to disable grayscale, please ignore
// eslint-disable-next-line no-constant-binary-expression
loading = false && form?.success;
});
// Used when user answers wrong and no new data comes in // Used when user answers wrong and no new data comes in
$effect(() => { $effect(() => {
if (data.streamed?.albums) { if (data.streamed?.albums) {
@ -44,7 +38,34 @@
</div> </div>
{/snippet} {/snippet}
<AlertDialog.Root open={form?.solved === false}> {#snippet playArea(albums, placeholder = false)}
{#if placeholder}
{#each { length: 2 } as _}
<section class="grid grid-cols-3 items-center gap-14">
{#each { length: 3 } as _}
<Skeleton class="h-[5.25rem] w-full rounded-xl " />
{/each}
</section>
<Separator />
{/each}
<section class="grid grid-cols-3 items-center gap-14">
{#each { length: 3 } as _}
<Skeleton class="aspect-square h-auto max-w-full rounded-xl object-cover" />
{/each}
</section>
{:else}
<DndGroup items={albums.names} type="names"></DndGroup>
<Separator />
<DndGroup items={albums.artists} type="artists"></DndGroup>
<Separator />
<DndGroup items={albums.images} image type="images"></DndGroup>
{/if}
{@render footer(placeholder)}
{/snippet}
<AlertDialog.Root open={data.playing === false}>
<AlertDialog.Content> <AlertDialog.Content>
<AlertDialog.Header> <AlertDialog.Header>
<AlertDialog.Title> <AlertDialog.Title>
@ -83,43 +104,16 @@
action="?/submit" action="?/submit"
method="POST" method="POST"
use:enhance use:enhance
class="grid w-full gap-6 transition-all {loading || data?.playing === false ? 'grayscale' : ''}" class="grid w-full gap-4 transition-all {loading || data?.playing === false ? 'grayscale' : ''}"
> >
{#if data?.streamed?.albums} {#if data?.streamed?.albums}
{#await data.streamed.albums} {#await data.streamed.albums}
{#each { length: 2 } as _} {@render playArea({}, true)}
<section class="grid grid-cols-3 items-center gap-14">
{#each { length: 3 } as _}
<Skeleton class="h-[5.25rem] w-full rounded-xl " />
{/each}
</section>
<Separator />
{/each}
<section class="grid grid-cols-3 items-center gap-14">
{#each { length: 3 } as _}
<Skeleton class="aspect-square h-auto max-w-full rounded-xl object-cover" />
{/each}
</section>
{@render footer(true)}
{:then albums} {:then albums}
<DndGroup items={albums.names} type="names"></DndGroup> {@render playArea(albums)}
<Separator />
<DndGroup items={albums.artists} type="artists"></DndGroup>
<Separator />
<DndGroup items={albums.images} image type="images"></DndGroup>
{@render footer(false)}
{/await} {/await}
{:else} {:else}
<DndGroup items={oldAlbums.names} type="names"></DndGroup> {@render playArea(oldAlbums)}
<Separator />
<DndGroup items={oldAlbums.artists} type="artists"></DndGroup>
<Separator />
<DndGroup items={oldAlbums.images} image type="images"></DndGroup>
{@render footer(false)}
{/if} {/if}
</form> </form>
</main> </main>