|
| 1 | +<script lang="ts"> |
| 2 | + import { draggable, droppable } from '$lib/index.js'; |
| 3 | + import { dndState } from '$lib/stores/dnd.svelte.js'; |
| 4 | + import type { DragDropState } from '$lib/types/index.js'; |
| 5 | +
|
| 6 | + interface Fruit { |
| 7 | + id: string; |
| 8 | + name: string; |
| 9 | + color: string; |
| 10 | + } |
| 11 | +
|
| 12 | + let sourceFruits = $state([ |
| 13 | + { id: '1', name: 'Apple', color: 'Red' }, |
| 14 | + { id: '2', name: 'Banana', color: 'Yellow' }, |
| 15 | + { id: '3', name: 'Grapes', color: 'Green' }, |
| 16 | + { id: '4', name: 'Orange', color: 'Orange' }, |
| 17 | + { id: '5', name: 'Pineapple', color: 'Yellow' }, |
| 18 | + { id: '6', name: 'Strawberry', color: 'Red' }, |
| 19 | + { id: '7', name: 'Watermelon', color: 'Green' } |
| 20 | + ]); |
| 21 | +
|
| 22 | + let targetFruits = $state([]); |
| 23 | +
|
| 24 | + // Add a derived state for empty states |
| 25 | + let isTargetEmpty = $derived(targetFruits.length === 0); |
| 26 | + let isSourceEmpty = $derived(sourceFruits.length === 0); |
| 27 | +
|
| 28 | + // Validation function that sets invalidDrop state |
| 29 | + function validateDrop(state: DragDropState<Fruit>) { |
| 30 | + const fruit = state.draggedItem; |
| 31 | + if (!fruit) return; |
| 32 | +
|
| 33 | + // Set invalidDrop based on the color condition |
| 34 | + dndState.invalidDrop = fruit.color !== 'Red'; |
| 35 | + } |
| 36 | +
|
| 37 | + const dragDropCallbacks = { |
| 38 | + onDragOver: (state: DragDropState<Fruit>) => { |
| 39 | + validateDrop(state); |
| 40 | + }, |
| 41 | + onDrop: async (state: DragDropState<Fruit>) => { |
| 42 | + if (dndState.invalidDrop || !state.draggedItem) { |
| 43 | + return; // Prevent invalid drops |
| 44 | + } |
| 45 | +
|
| 46 | + // Move fruit to target container |
| 47 | + sourceFruits = sourceFruits.filter((fruit) => fruit.id !== state.draggedItem.id); |
| 48 | + targetFruits = [...targetFruits, state.draggedItem]; |
| 49 | + }, |
| 50 | + onDragEnd: () => { |
| 51 | + // Reset invalidDrop state when drag ends |
| 52 | + dndState.invalidDrop = false; |
| 53 | + } |
| 54 | + }; |
| 55 | +</script> |
| 56 | + |
| 57 | +<div class="container mx-auto p-8"> |
| 58 | + <div class="mb-12 space-y-2"> |
| 59 | + <h1 class="text-3xl font-bold tracking-tight">Fruit Sorter</h1> |
| 60 | + <p class="text-muted-foreground"> |
| 61 | + Drop the red fruits in the target zone. Other colors will be rejected. |
| 62 | + </p> |
| 63 | + </div> |
| 64 | + |
| 65 | + <div class="grid gap-8 md:grid-cols-2"> |
| 66 | + <!-- Source Container --> |
| 67 | + <div class="space-y-4"> |
| 68 | + <h2 class="text-sm font-medium uppercase tracking-wide text-muted-foreground">Available Fruits</h2> |
| 69 | + <div |
| 70 | + class="min-h-[400px] rounded-lg border bg-white p-4 shadow-sm transition-all" |
| 71 | + use:droppable={{ container: 'source' }} |
| 72 | + > |
| 73 | + {#if isSourceEmpty} |
| 74 | + <div class="flex h-full items-center justify-center"> |
| 75 | + <p class="text-muted-foreground">All fruits have been sorted</p> |
| 76 | + </div> |
| 77 | + {:else} |
| 78 | + <div class="grid gap-2"> |
| 79 | + {#each sourceFruits as fruit} |
| 80 | + <div |
| 81 | + use:draggable={{ container: 'source', dragData: fruit }} |
| 82 | + class={`group flex items-center justify-between rounded-md border p-3 shadow-sm transition-all hover:shadow |
| 83 | + ${fruit.color === 'Red' ? 'border-red-200 bg-red-50/50' : 'border-muted bg-muted/5'}`} |
| 84 | + > |
| 85 | + <span class="font-medium">{fruit.name}</span> |
| 86 | + <span |
| 87 | + class={`rounded px-2 py-1 text-xs |
| 88 | + ${fruit.color === 'Red' ? 'bg-red-100 text-red-700' : 'bg-muted/10 text-muted-foreground'}`} |
| 89 | + > |
| 90 | + {fruit.color} |
| 91 | + </span> |
| 92 | + </div> |
| 93 | + {/each} |
| 94 | + </div> |
| 95 | + {/if} |
| 96 | + </div> |
| 97 | + </div> |
| 98 | + |
| 99 | + <!-- Target Container --> |
| 100 | + <div class="space-y-4"> |
| 101 | + <h2 class="text-sm font-medium uppercase tracking-wide text-muted-foreground">Red Fruits Only</h2> |
| 102 | + <div |
| 103 | + class={`min-h-[400px] rounded-lg border bg-white p-4 shadow-sm transition-all |
| 104 | + ${ |
| 105 | + dndState.isDragging |
| 106 | + ? dndState.invalidDrop |
| 107 | + ? 'border-red-500/50 bg-red-50/5' |
| 108 | + : 'border-blue-500/50 bg-blue-50/5' |
| 109 | + : '' |
| 110 | + }`} |
| 111 | + |
| 112 | + use:droppable={{ |
| 113 | + container: 'target', |
| 114 | + callbacks: dragDropCallbacks, |
| 115 | + attributes: { |
| 116 | + dragOverClass: dndState.invalidDrop ? 'invalid-drop' : 'valid-drop' |
| 117 | + } |
| 118 | + }} |
| 119 | + > |
| 120 | + {#if isTargetEmpty} |
| 121 | + <div class="flex h-full items-center justify-center"> |
| 122 | + <p class="text-muted-foreground">Drop red fruits here</p> |
| 123 | + </div> |
| 124 | + {:else} |
| 125 | + <div class="grid gap-2"> |
| 126 | + {#each targetFruits as fruit} |
| 127 | + <div class="flex items-center justify-between rounded-md border-red-200 bg-red-50/50 p-3 shadow-sm"> |
| 128 | + <span class="font-medium">{fruit.name}</span> |
| 129 | + <span class="rounded bg-red-100 px-2 py-1 text-xs text-red-700">{fruit.color}</span> |
| 130 | + </div> |
| 131 | + {/each} |
| 132 | + </div> |
| 133 | + {/if} |
| 134 | + </div> |
| 135 | + </div> |
| 136 | + </div> |
| 137 | +</div> |
| 138 | + |
| 139 | +<style> |
| 140 | + .valid-drop { |
| 141 | + @apply border-blue-500/50 bg-blue-50/5 ring-2 ring-blue-500/20 ring-offset-2; |
| 142 | + } |
| 143 | + .invalid-drop { |
| 144 | + @apply border-red-500/50 bg-red-50/5 ring-2 ring-red-500/20 ring-offset-2; |
| 145 | + } |
| 146 | +</style> |
0 commit comments