Add more visual hints for drag and drop, remove unneeded loading button hackery
This commit is contained in:
parent
9c2664d0da
commit
3c0b3df3b2
2 changed files with 97 additions and 74 deletions
|
@ -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,38 +16,37 @@
|
||||||
function handleDndFinalize(e: CustomEvent<any>) {
|
function handleDndFinalize(e: CustomEvent<any>) {
|
||||||
items = e.detail.items;
|
items = e.detail.items;
|
||||||
}
|
}
|
||||||
</script>
|
function transformDraggedElement(draggedEl: HTMLElement | undefined) {
|
||||||
|
if (!draggedEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const card = draggedEl.querySelector('.bg-card') as HTMLElement;
|
||||||
|
|
||||||
<section
|
if (!card) {
|
||||||
use:dndzone={{
|
return;
|
||||||
items,
|
}
|
||||||
flipDurationMs,
|
|
||||||
type: type,
|
card.classList.add('shadow-foreground/25');
|
||||||
dropTargetStyle: {}
|
}
|
||||||
}}
|
|
||||||
onconsider={handleDndConsider}
|
let cardClass = $derived(
|
||||||
onfinalize={handleDndFinalize}
|
`hover select-none overflow-hidden rounded-xl border bg-card text-card-foreground shadow shadow-foreground/15 ${
|
||||||
class="grid grid-cols-3 items-center gap-4 sm:gap-8 md:gap-10 lg:gap-14"
|
type === 'names'
|
||||||
>
|
|
||||||
{#each items as item, i (item.id)}
|
|
||||||
<div animate:flip={{ duration: flipDurationMs, easing: expoOut }}>
|
|
||||||
<Card.Root
|
|
||||||
class="select-none overflow-hidden rounded-xl border bg-card text-card-foreground shadow shadow-foreground/15 transition-shadow hover:shadow-foreground/25 {type ===
|
|
||||||
'names'
|
|
||||||
? 'border-red-400 '
|
? 'border-red-400 '
|
||||||
: type === 'artists'
|
: type === 'artists'
|
||||||
? 'border-purple-400 '
|
? 'border-purple-400 '
|
||||||
: 'border-blue-400'}"
|
: 'border-blue-400'
|
||||||
>
|
}`
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet card(item, i)}
|
||||||
{#if image}
|
{#if image}
|
||||||
<Card.Content class="p-0">
|
|
||||||
<img class="aspect-square w-full object-cover" alt="Album Art" src={item.value} />
|
<img class="aspect-square w-full object-cover" alt="Album Art" src={item.value} />
|
||||||
<input type="hidden" name="{type}_{i}" value={item.value} />
|
<input type="hidden" name="{type}_{i}" value={item.value} />
|
||||||
</Card.Content>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Content>
|
|
||||||
<p
|
<p
|
||||||
class="text-center {type === 'names'
|
class="p-6 text-center {type === 'names'
|
||||||
? 'text-red-900 dark:text-red-200'
|
? 'text-red-900 dark:text-red-200'
|
||||||
: type === 'artists'
|
: type === 'artists'
|
||||||
? 'text-purple-900 dark:text-purple-200'
|
? 'text-purple-900 dark:text-purple-200'
|
||||||
|
@ -59,9 +59,38 @@
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
<input type="hidden" name="{type}_{i}" value={item.value} />
|
<input type="hidden" name="{type}_{i}" value={item.value} />
|
||||||
</Card.Content>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<section
|
||||||
|
use:dndzone={{
|
||||||
|
items,
|
||||||
|
flipDurationMs,
|
||||||
|
type: type,
|
||||||
|
dropTargetStyle: {},
|
||||||
|
dropTargetClasses: ['bg-muted/50', 'dark:bg-muted/25', 'ring-2', 'ring-muted'],
|
||||||
|
transformDraggedElement: transformDraggedElement
|
||||||
|
}}
|
||||||
|
onconsider={handleDndConsider}
|
||||||
|
onfinalize={handleDndFinalize}
|
||||||
|
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)}
|
||||||
|
<div animate:flip={{ duration: flipDurationMs, easing: expoOut }} class="relative">
|
||||||
|
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||||
|
<div
|
||||||
|
in:fade={{ duration: 200, easing: expoOut }}
|
||||||
|
class="{cardClass} visible border-transparent bg-transparent opacity-50 shadow-transparent"
|
||||||
|
>
|
||||||
|
{@render card(item, i)}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Card.Root class={cardClass}>
|
||||||
|
<Card.Content class="p-0">
|
||||||
|
{@render card(item, i)}
|
||||||
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue