Fraser commited on
Commit
ce29375
·
1 Parent(s): 00d1c08

drag & drop

Browse files
src/lib/components/Pages/Pictuary.svelte CHANGED
@@ -1,16 +1,19 @@
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
  import { getAllMonsters } from '$lib/db/monsters';
4
- import { getAllPicletInstances, getRosterPiclets } from '$lib/db/piclets';
5
  import type { Monster, PicletInstance } from '$lib/db/schema';
6
  import PicletCard from '../Piclets/PicletCard.svelte';
7
  import EmptySlotCard from '../Piclets/EmptySlotCard.svelte';
8
  import DiscoveredCard from '../Piclets/DiscoveredCard.svelte';
 
 
9
 
10
  let rosterPiclets: PicletInstance[] = $state([]);
11
  let storagePiclets: PicletInstance[] = $state([]);
12
  let discoveredMonsters: Monster[] = $state([]);
13
  let isLoading = $state(true);
 
14
 
15
  // Map roster positions for easy access
16
  let rosterMap = $derived(() => {
@@ -23,20 +26,15 @@
23
  return map;
24
  });
25
 
26
- onMount(async () => {
27
  try {
28
- // Load all piclet instances
29
  const allInstances = await getAllPicletInstances();
30
 
31
- // Separate roster and storage
32
  rosterPiclets = allInstances.filter(p => p.isInRoster);
33
  storagePiclets = allInstances.filter(p => !p.isInRoster);
34
 
35
- // Load all discovered monsters (for now, all generated monsters)
36
- // In a real game, this would track which monsters have been encountered
37
  const allMonsters = await getAllMonsters();
38
 
39
- // Filter out monsters that have been caught (have instances)
40
  const caughtTypeIds = new Set(allInstances.map(p => p.typeId));
41
  discoveredMonsters = allMonsters.filter(m =>
42
  !caughtTypeIds.has(m.name.toLowerCase().replace(/\s+/g, '-'))
@@ -46,28 +44,64 @@
46
  } finally {
47
  isLoading = false;
48
  }
 
 
 
 
49
  });
50
 
51
  function handleRosterClick(position: number) {
52
  const piclet = rosterMap().get(position);
53
  if (piclet) {
54
- // TODO: Navigate to piclet detail page
55
  console.log('View piclet:', piclet);
56
  } else {
57
- // TODO: Show add to roster dialog
58
  console.log('Add piclet to position:', position);
59
  }
60
  }
61
 
62
  function handleStorageClick(piclet: PicletInstance) {
63
- // TODO: Navigate to piclet detail page
64
  console.log('View storage piclet:', piclet);
65
  }
66
 
67
  function handleDiscoveredClick(monster: Monster) {
68
- // TODO: Navigate to discovered piclet detail page
69
  console.log('View discovered monster:', monster);
70
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </script>
72
 
73
  <div class="pictuary-page">
@@ -99,18 +133,16 @@
99
  <h2>Roster</h2>
100
  <div class="roster-grid">
101
  {#each Array(6) as _, position}
102
- {#if rosterMap().has(position)}
103
- <PicletCard
104
- instance={rosterMap().get(position)!}
105
- size={100}
106
- onClick={() => handleRosterClick(position)}
107
- />
108
- {:else}
109
- <EmptySlotCard
110
- size={100}
111
- onClick={() => handleRosterClick(position)}
112
- />
113
- {/if}
114
  {/each}
115
  </div>
116
  </section>
@@ -126,10 +158,13 @@
126
  </div>
127
  <div class="horizontal-scroll">
128
  {#each storagePiclets.slice(0, 10) as piclet}
129
- <PicletCard
130
  instance={piclet}
131
  size={100}
 
132
  onClick={() => handleStorageClick(piclet)}
 
 
133
  />
134
  {/each}
135
  </div>
 
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
  import { getAllMonsters } from '$lib/db/monsters';
4
+ import { getAllPicletInstances, getRosterPiclets, moveToRoster, swapRosterPositions, moveToStorage } from '$lib/db/piclets';
5
  import type { Monster, PicletInstance } from '$lib/db/schema';
6
  import PicletCard from '../Piclets/PicletCard.svelte';
7
  import EmptySlotCard from '../Piclets/EmptySlotCard.svelte';
8
  import DiscoveredCard from '../Piclets/DiscoveredCard.svelte';
9
+ import DraggablePicletCard from '../Piclets/DraggablePicletCard.svelte';
10
+ import RosterSlot from '../Piclets/RosterSlot.svelte';
11
 
12
  let rosterPiclets: PicletInstance[] = $state([]);
13
  let storagePiclets: PicletInstance[] = $state([]);
14
  let discoveredMonsters: Monster[] = $state([]);
15
  let isLoading = $state(true);
16
+ let currentlyDragging: PicletInstance | null = $state(null);
17
 
18
  // Map roster positions for easy access
19
  let rosterMap = $derived(() => {
 
26
  return map;
27
  });
28
 
29
+ async function loadPiclets() {
30
  try {
 
31
  const allInstances = await getAllPicletInstances();
32
 
 
33
  rosterPiclets = allInstances.filter(p => p.isInRoster);
34
  storagePiclets = allInstances.filter(p => !p.isInRoster);
35
 
 
 
36
  const allMonsters = await getAllMonsters();
37
 
 
38
  const caughtTypeIds = new Set(allInstances.map(p => p.typeId));
39
  discoveredMonsters = allMonsters.filter(m =>
40
  !caughtTypeIds.has(m.name.toLowerCase().replace(/\s+/g, '-'))
 
44
  } finally {
45
  isLoading = false;
46
  }
47
+ }
48
+
49
+ onMount(() => {
50
+ loadPiclets();
51
  });
52
 
53
  function handleRosterClick(position: number) {
54
  const piclet = rosterMap().get(position);
55
  if (piclet) {
 
56
  console.log('View piclet:', piclet);
57
  } else {
 
58
  console.log('Add piclet to position:', position);
59
  }
60
  }
61
 
62
  function handleStorageClick(piclet: PicletInstance) {
 
63
  console.log('View storage piclet:', piclet);
64
  }
65
 
66
  function handleDiscoveredClick(monster: Monster) {
 
67
  console.log('View discovered monster:', monster);
68
  }
69
+
70
+ function handleDragStart(instance: PicletInstance) {
71
+ currentlyDragging = instance;
72
+ }
73
+
74
+ function handleDragEnd() {
75
+ currentlyDragging = null;
76
+ }
77
+
78
+ async function handleRosterDrop(position: number, dragData: any) {
79
+ if (!dragData.instanceId) return;
80
+
81
+ try {
82
+ const draggedPiclet = [...rosterPiclets, ...storagePiclets].find(p => p.id === dragData.instanceId);
83
+ if (!draggedPiclet) return;
84
+
85
+ const targetPiclet = rosterMap().get(position);
86
+
87
+ if (dragData.fromRoster && targetPiclet) {
88
+ // Swap two roster positions
89
+ await swapRosterPositions(
90
+ dragData.instanceId,
91
+ dragData.fromPosition,
92
+ targetPiclet.id!,
93
+ position
94
+ );
95
+ } else {
96
+ // Move to roster (possibly replacing existing)
97
+ await moveToRoster(dragData.instanceId, position);
98
+ }
99
+
100
+ await loadPiclets();
101
+ } catch (err) {
102
+ console.error('Failed to handle drop:', err);
103
+ }
104
+ }
105
  </script>
106
 
107
  <div class="pictuary-page">
 
133
  <h2>Roster</h2>
134
  <div class="roster-grid">
135
  {#each Array(6) as _, position}
136
+ <RosterSlot
137
+ {position}
138
+ piclet={rosterMap().get(position)}
139
+ size={100}
140
+ onDrop={handleRosterDrop}
141
+ onPicletClick={(piclet) => handleRosterClick(position)}
142
+ onEmptyClick={handleRosterClick}
143
+ onDragStart={handleDragStart}
144
+ onDragEnd={handleDragEnd}
145
+ />
 
 
146
  {/each}
147
  </div>
148
  </section>
 
158
  </div>
159
  <div class="horizontal-scroll">
160
  {#each storagePiclets.slice(0, 10) as piclet}
161
+ <DraggablePicletCard
162
  instance={piclet}
163
  size={100}
164
+ showDetails={true}
165
  onClick={() => handleStorageClick(piclet)}
166
+ onDragStart={handleDragStart}
167
+ onDragEnd={handleDragEnd}
168
  />
169
  {/each}
170
  </div>
src/lib/components/Piclets/DraggablePicletCard.svelte ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { PicletInstance } from '$lib/db/schema';
3
+ import PicletCard from './PicletCard.svelte';
4
+
5
+ interface Props {
6
+ instance: PicletInstance;
7
+ size?: number;
8
+ showDetails?: boolean;
9
+ onDragStart?: (instance: PicletInstance) => void;
10
+ onDragEnd?: () => void;
11
+ onClick?: () => void;
12
+ }
13
+
14
+ let { instance, size = 100, showDetails = true, onDragStart, onDragEnd, onClick }: Props = $props();
15
+
16
+ let isDragging = $state(false);
17
+
18
+ function handleDragStart(e: DragEvent) {
19
+ isDragging = true;
20
+
21
+ // Set drag data
22
+ e.dataTransfer!.effectAllowed = 'move';
23
+ e.dataTransfer!.setData('application/json', JSON.stringify({
24
+ instanceId: instance.id,
25
+ fromRoster: instance.isInRoster,
26
+ fromPosition: instance.rosterPosition
27
+ }));
28
+
29
+ // Create a custom drag image
30
+ const dragImage = e.currentTarget as HTMLElement;
31
+ const clone = dragImage.cloneNode(true) as HTMLElement;
32
+ clone.style.transform = 'scale(1.1)';
33
+ clone.style.opacity = '0.8';
34
+ document.body.appendChild(clone);
35
+ e.dataTransfer!.setDragImage(clone, size / 2, size / 2);
36
+ setTimeout(() => document.body.removeChild(clone), 0);
37
+
38
+ onDragStart?.(instance);
39
+ }
40
+
41
+ function handleDragEnd() {
42
+ isDragging = false;
43
+ onDragEnd?.();
44
+ }
45
+ </script>
46
+
47
+ <div
48
+ draggable="true"
49
+ ondragstart={handleDragStart}
50
+ ondragend={handleDragEnd}
51
+ class="draggable-wrapper"
52
+ class:dragging={isDragging}
53
+ >
54
+ <PicletCard {instance} {size} {showDetails} {onClick} />
55
+ </div>
56
+
57
+ <style>
58
+ .draggable-wrapper {
59
+ cursor: move;
60
+ }
61
+
62
+ .draggable-wrapper.dragging {
63
+ opacity: 0.5;
64
+ }
65
+
66
+ .draggable-wrapper.dragging :global(.piclet-card) {
67
+ transform: scale(0.9);
68
+ }
69
+ </style>
src/lib/components/Piclets/EmptySlotCard.svelte CHANGED
@@ -11,7 +11,7 @@
11
  <button
12
  class="empty-slot"
13
  class:highlighted={isHighlighted}
14
- style="width: {size}px; height: {size}px;"
15
  onclick={onClick}
16
  type="button"
17
  >
 
11
  <button
12
  class="empty-slot"
13
  class:highlighted={isHighlighted}
14
+ style="width: {size}px; height: {size + 30}px;"
15
  onclick={onClick}
16
  type="button"
17
  >
src/lib/components/Piclets/RosterSlot.svelte ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { PicletInstance } from '$lib/db/schema';
3
+ import DraggablePicletCard from './DraggablePicletCard.svelte';
4
+ import EmptySlotCard from './EmptySlotCard.svelte';
5
+
6
+ interface Props {
7
+ position: number;
8
+ piclet?: PicletInstance;
9
+ size?: number;
10
+ onDrop?: (position: number, dragData: any) => void;
11
+ onPicletClick?: (piclet: PicletInstance) => void;
12
+ onEmptyClick?: (position: number) => void;
13
+ onDragStart?: (instance: PicletInstance) => void;
14
+ onDragEnd?: () => void;
15
+ }
16
+
17
+ let {
18
+ position,
19
+ piclet,
20
+ size = 100,
21
+ onDrop,
22
+ onPicletClick,
23
+ onEmptyClick,
24
+ onDragStart,
25
+ onDragEnd
26
+ }: Props = $props();
27
+
28
+ let isDragOver = $state(false);
29
+ let canAcceptDrop = $state(false);
30
+
31
+ function handleDragOver(e: DragEvent) {
32
+ e.preventDefault();
33
+
34
+ // Check if we can accept the drop
35
+ const data = e.dataTransfer?.getData('application/json');
36
+ if (data) {
37
+ isDragOver = true;
38
+ canAcceptDrop = true;
39
+ e.dataTransfer!.dropEffect = 'move';
40
+ }
41
+ }
42
+
43
+ function handleDragLeave() {
44
+ isDragOver = false;
45
+ canAcceptDrop = false;
46
+ }
47
+
48
+ function handleDrop(e: DragEvent) {
49
+ e.preventDefault();
50
+ isDragOver = false;
51
+ canAcceptDrop = false;
52
+
53
+ const data = e.dataTransfer?.getData('application/json');
54
+ if (data) {
55
+ const dragData = JSON.parse(data);
56
+ onDrop?.(position, dragData);
57
+ }
58
+ }
59
+
60
+ function handleClick() {
61
+ if (piclet) {
62
+ onPicletClick?.(piclet);
63
+ } else {
64
+ onEmptyClick?.(position);
65
+ }
66
+ }
67
+ </script>
68
+
69
+ <div
70
+ class="roster-slot"
71
+ ondragover={handleDragOver}
72
+ ondragleave={handleDragLeave}
73
+ ondrop={handleDrop}
74
+ class:drag-over={isDragOver}
75
+ >
76
+ {#if piclet}
77
+ <DraggablePicletCard
78
+ instance={piclet}
79
+ {size}
80
+ showDetails={false}
81
+ onClick={handleClick}
82
+ {onDragStart}
83
+ {onDragEnd}
84
+ />
85
+ {:else}
86
+ <EmptySlotCard
87
+ {size}
88
+ isHighlighted={isDragOver && canAcceptDrop}
89
+ onClick={handleClick}
90
+ />
91
+ {/if}
92
+ </div>
93
+
94
+ <style>
95
+ .roster-slot {
96
+ position: relative;
97
+ }
98
+
99
+ .roster-slot.drag-over::after {
100
+ content: '';
101
+ position: absolute;
102
+ inset: -4px;
103
+ border: 2px solid #007bff;
104
+ border-radius: 16px;
105
+ background: rgba(0, 123, 255, 0.1);
106
+ pointer-events: none;
107
+ }
108
+ </style>