diff --git a/frontend/src/routes/[tierlistId]/+page.svelte b/frontend/src/routes/[tierlistId]/+page.svelte index b3a0dd7..b117a9b 100644 --- a/frontend/src/routes/[tierlistId]/+page.svelte +++ b/frontend/src/routes/[tierlistId]/+page.svelte @@ -15,37 +15,44 @@ let uploadedImages: TierImage[] = $state([]); let tiers: Tier[] = $state([]); - let draggedImage: TierImage | null = null; - let draggedFrom: SourceType | null = null; + let draggedImage: TierImage | null = $state(null); + let draggedFrom: SourceType | null = $state(null); + let draggedIndex: number | null = $state(null); + let activeDropTarget: { tier: SourceType; index: number } | null = $state(null); onMount(async () => { tierlistId = data.tierlistId; - - let response = await fetch(`${PUBLIC_API_URL}/tierlist/${tierlistId}`); - let tierlist = await response.json(); - tierlistName = tierlist['Name']; - uploadedImages = tierlist['UnassignedEntries'].map((x) => ({ - id: x['id'], - src: `${PUBLIC_API_URL}/images/${x['file_key']}` - })); - tiers = tierlist['Tiers'].map((x) => ({ - id: x['id'], - name: x['name'], - entries: x['entries'].map((y) => ({ - id: y['id'], - src: `${PUBLIC_API_URL}/images/${y['file_key']}` - })) - })); + try { + let response = await fetch(`${PUBLIC_API_URL}/tierlist/${tierlistId}`); + if (response.ok) { + let tierlist = await response.json(); + tierlistName = tierlist['Name']; + uploadedImages = tierlist['UnassignedEntries'].map((x: any) => ({ + id: x['id'], + src: `${PUBLIC_API_URL}/images/${x['file_key']}` + })); + tiers = tierlist['Tiers'].map((x: any) => ({ + id: x['id'], + name: x['name'], + entries: x['entries'].map((y: any) => ({ + id: y['id'], + src: `${PUBLIC_API_URL}/images/${y['file_key']}` + })) + })); + } + } catch (error) { + console.error("Failed to load tierlist:", error); + } }); async function handleUpload(event: Event) { const input = event.target as HTMLInputElement; const files = Array.from(input.files ?? []); let newImages = []; + for (const file of files) { const formData = new FormData(); formData.append('image', file); - try { const response = await fetch(`${PUBLIC_API_URL}/tierlist/${tierlistId}/upload`, { method: 'POST', @@ -60,12 +67,30 @@ uploadedImages = [...uploadedImages, ...newImages]; } - async function handleDragStart(image: TierImage, from: SourceType) { + function handleDragStart(image: TierImage, from: SourceType, index: number) { draggedImage = image; draggedFrom = from; + draggedIndex = index; + } + + function handleDragEnd() { + draggedImage = null; + draggedFrom = null; + draggedIndex = null; + activeDropTarget = null; + } + + function handleEnter(tier: SourceType, index: number) { + activeDropTarget = { tier, index }; } - async function handleDrop(target: SourceType) { + function handleLeave(tier: SourceType, index: number) { + if (activeDropTarget?.tier === tier && activeDropTarget?.index === index) { + activeDropTarget = null; + } + } + + async function handleDrop(target: SourceType, insertIndex: number) { if (!draggedImage || draggedFrom === null) return; if (draggedFrom === 'uploaded') { @@ -77,12 +102,16 @@ } if (target === 'uploaded') { - uploadedImages = [...uploadedImages, draggedImage]; + const items = [...uploadedImages]; + items.splice(insertIndex, 0, draggedImage); + uploadedImages = items; } else { - tiers[target].entries = [...tiers[target].entries, draggedImage]; + const items = [...tiers[target].entries]; + items.splice(insertIndex, 0, draggedImage); + tiers[target].entries = items; } - const response = await fetch(`${PUBLIC_API_URL}/tierlist/${tierlistId}/move`, { + await fetch(`${PUBLIC_API_URL}/tierlist/${tierlistId}/move`, { method: 'POST', body: JSON.stringify({ TierID: target === 'uploaded' ? null : tiers[target].id, @@ -90,8 +119,7 @@ }) }); - draggedImage = null; - draggedFrom = null; + handleDragEnd(); } function allowDrop(event: DragEvent) { @@ -106,7 +134,6 @@

Tierlist Creator

-
{#each tiers as tier, i}
@@ -116,24 +143,87 @@ > {tier.name}
+
handleDrop(i)} > - {#each tier.entries as image} - tier item handleDragStart(image, i)} - /> + {#if tier.entries.length === 0} +
handleEnter(i, 0)} + ondragleave={() => handleLeave(i, 0)} + ondragover={allowDrop} + ondrop={() => handleDrop(i, 0)} + role="listitem" + > + {#if activeDropTarget?.tier === i && activeDropTarget?.index === 0 && draggedImage} + ghost preview + {:else} +

Drop items here...

+ {/if} +
{:else} -

Drop items here...

- {/each} + {#each tier.entries as image, index (image.id.toString())} +
handleEnter(i, index)} + ondragleave={() => handleLeave(i, index)} + ondragover={allowDrop} + ondrop={() => handleDrop(i, index)} + role="listitem" + > + {#if activeDropTarget?.tier === i && activeDropTarget?.index === index && draggedImage && (draggedFrom !== i || (draggedIndex !== index && draggedIndex !== index - 1))} + ghost preview + {/if} +
+ + tier item handleDragStart(image, i, index)} + ondragend={handleDragEnd} + /> + {/each} + +
handleEnter(i, tier.entries.length)} + ondragleave={() => handleLeave(i, tier.entries.length)} + ondragover={allowDrop} + ondrop={() => handleDrop(i, tier.entries.length)} + role="listitem" + > + {#if activeDropTarget?.tier === i && activeDropTarget?.index === tier.entries.length && draggedImage && (draggedFrom !== i || (draggedIndex !== tier.entries.length && draggedIndex !== tier.entries.length - 1))} + ghost preview + {/if} +
+ {/if}
{/each} @@ -144,25 +234,66 @@ aria-label="Uploaded items drop zone" class="mx-4 mb-6 mt-8 min-h-[120px] rounded-lg border-2 border-dashed border-gray-400 bg-gray-50 p-4" ondragover={allowDrop} - ondrop={() => handleDrop('uploaded')} + ondrop={() => handleDrop('uploaded', uploadedImages.length)} >

Uploaded Items

- {#each uploadedImages as image} + {#each uploadedImages as image, index (image.id.toString())} +
handleEnter('uploaded', index)} + ondragleave={() => handleLeave('uploaded', index)} + ondragover={allowDrop} + ondrop={() => handleDrop('uploaded', index)} + role="listitem" + > + {#if activeDropTarget?.tier === 'uploaded' && activeDropTarget?.index === index && draggedImage && (draggedFrom !== 'uploaded' || (draggedIndex !== index && draggedIndex !== index - 1))} + ghost preview + {/if} +
+ uploaded item handleDragStart(image, 'uploaded')} + ondragstart={() => handleDragStart(image, 'uploaded', index)} + ondragend={handleDragEnd} /> - {:else} -

No images uploaded yet.

{/each} + +
handleEnter('uploaded', uploadedImages.length)} + ondragleave={() => handleLeave('uploaded', uploadedImages.length)} + ondragover={allowDrop} + ondrop={() => handleDrop('uploaded', uploadedImages.length)} + role="listitem" + > + {#if activeDropTarget?.tier === 'uploaded' && activeDropTarget?.index === uploadedImages.length && draggedImage && (draggedFrom !== 'uploaded' || (draggedIndex !== uploadedImages.length && draggedIndex !== uploadedImages.length - 1))} + ghost preview + {/if} +
-
+ + \ No newline at end of file