Fraser commited on
Commit
657d1cf
·
1 Parent(s): 192de08

pull to refresh

Browse files
src/lib/components/Pages/Encounters.svelte CHANGED
@@ -8,6 +8,7 @@
8
  import { db } from '$lib/db';
9
  import { uiStore } from '$lib/stores/ui';
10
  import Battle from './Battle.svelte';
 
11
 
12
  let encounters: Encounter[] = [];
13
  let isLoading = true;
@@ -45,6 +46,24 @@
45
  isLoading = false;
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  async function handleEncounterTap(encounter: Encounter) {
49
  if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
50
  if (encounter.title === 'Your First Piclet!') {
@@ -329,24 +348,24 @@
329
  />
330
  {:else}
331
  <div class="encounters-page">
332
-
333
- {#if isLoading}
334
- <div class="loading">
335
- <div class="spinner"></div>
336
- <p>Loading encounters...</p>
337
- </div>
338
- {:else if encounters.length === 0}
339
- <div class="empty-state">
340
- <div class="empty-icon">📸</div>
341
- <h2>No Piclets Discovered</h2>
342
- <p>To start your adventure, select the Snap logo image:</p>
343
- <div class="logo-instruction">
344
- <img src="/assets/snap_logo.png" alt="Snap Logo" class="snap-logo-preview" />
345
- <p class="instruction-text">↑ Select this image in the scanner</p>
346
  </div>
347
- </div>
348
- {:else}
349
- <div class="encounters-list">
 
 
 
 
 
 
 
 
 
350
  {#each encounters as encounter, index (encounter.id)}
351
  <button
352
  class="encounter-card"
@@ -396,16 +415,14 @@
396
  {/each}
397
  </div>
398
  {/if}
 
399
  </div>
400
  {/if}
401
 
402
  <style>
403
  .encounters-page {
404
  height: 100%;
405
- overflow-y: auto;
406
- -webkit-overflow-scrolling: touch;
407
- padding: 1rem;
408
- padding-bottom: 5rem;
409
  }
410
 
411
  .loading, .empty-state {
@@ -415,6 +432,7 @@
415
  justify-content: center;
416
  height: 60vh;
417
  text-align: center;
 
418
  }
419
 
420
  .spinner {
@@ -451,6 +469,8 @@
451
  display: flex;
452
  flex-direction: column;
453
  gap: 1rem;
 
 
454
  }
455
 
456
  .encounter-card {
 
8
  import { db } from '$lib/db';
9
  import { uiStore } from '$lib/stores/ui';
10
  import Battle from './Battle.svelte';
11
+ import PullToRefresh from '../UI/PullToRefresh.svelte';
12
 
13
  let encounters: Encounter[] = [];
14
  let isLoading = true;
 
46
  isLoading = false;
47
  }
48
 
49
+ async function handleRefresh() {
50
+ isRefreshing = true;
51
+ try {
52
+ // Force refresh encounters
53
+ console.log('Force refreshing encounters...');
54
+ encounters = await EncounterService.generateEncounters();
55
+
56
+ // Update game state with new refresh time
57
+ const gameState = await getOrCreateGameState();
58
+ await db.gameState.update(gameState.id!, {
59
+ lastEncounterRefresh: new Date()
60
+ });
61
+ } catch (error) {
62
+ console.error('Error refreshing encounters:', error);
63
+ }
64
+ isRefreshing = false;
65
+ }
66
+
67
  async function handleEncounterTap(encounter: Encounter) {
68
  if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
69
  if (encounter.title === 'Your First Piclet!') {
 
348
  />
349
  {:else}
350
  <div class="encounters-page">
351
+ <PullToRefresh onRefresh={handleRefresh}>
352
+ {#if isLoading}
353
+ <div class="loading">
354
+ <div class="spinner"></div>
355
+ <p>Loading encounters...</p>
 
 
 
 
 
 
 
 
 
356
  </div>
357
+ {:else if encounters.length === 0}
358
+ <div class="empty-state">
359
+ <div class="empty-icon">📸</div>
360
+ <h2>No Piclets Discovered</h2>
361
+ <p>To start your adventure, select the Snap logo image:</p>
362
+ <div class="logo-instruction">
363
+ <img src="/assets/snap_logo.png" alt="Snap Logo" class="snap-logo-preview" />
364
+ <p class="instruction-text">↑ Select this image in the scanner</p>
365
+ </div>
366
+ </div>
367
+ {:else}
368
+ <div class="encounters-list">
369
  {#each encounters as encounter, index (encounter.id)}
370
  <button
371
  class="encounter-card"
 
415
  {/each}
416
  </div>
417
  {/if}
418
+ </PullToRefresh>
419
  </div>
420
  {/if}
421
 
422
  <style>
423
  .encounters-page {
424
  height: 100%;
425
+ overflow: hidden; /* PullToRefresh handles scrolling */
 
 
 
426
  }
427
 
428
  .loading, .empty-state {
 
432
  justify-content: center;
433
  height: 60vh;
434
  text-align: center;
435
+ padding: 1rem;
436
  }
437
 
438
  .spinner {
 
469
  display: flex;
470
  flex-direction: column;
471
  gap: 1rem;
472
+ padding: 1rem;
473
+ padding-bottom: 5rem;
474
  }
475
 
476
  .encounter-card {
src/lib/components/UI/PullToRefresh.svelte ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ interface Props {
5
+ onRefresh: () => Promise<void>;
6
+ threshold?: number;
7
+ children?: any;
8
+ }
9
+
10
+ let { onRefresh, threshold = 80, children }: Props = $props();
11
+
12
+ let container: HTMLDivElement;
13
+ let startY = 0;
14
+ let currentY = 0;
15
+ let pulling = $state(false);
16
+ let releasing = $state(false);
17
+ let refreshing = $state(false);
18
+ let pullDistance = $state(0);
19
+
20
+ let pullProgress = $derived(Math.min(pullDistance / threshold, 1));
21
+ let canRefresh = $derived(pullProgress >= 1 && !refreshing);
22
+
23
+ function handleTouchStart(e: TouchEvent) {
24
+ if (container.scrollTop !== 0 || refreshing) return;
25
+
26
+ startY = e.touches[0].clientY;
27
+ pulling = true;
28
+ }
29
+
30
+ function handleTouchMove(e: TouchEvent) {
31
+ if (!pulling || refreshing) return;
32
+
33
+ currentY = e.touches[0].clientY;
34
+ const diff = currentY - startY;
35
+
36
+ if (diff > 0 && container.scrollTop === 0) {
37
+ e.preventDefault();
38
+ pullDistance = diff * 0.5; // Add resistance
39
+ }
40
+ }
41
+
42
+ async function handleTouchEnd() {
43
+ if (!pulling) return;
44
+
45
+ pulling = false;
46
+
47
+ if (canRefresh) {
48
+ releasing = true;
49
+ refreshing = true;
50
+
51
+ try {
52
+ await onRefresh();
53
+ } finally {
54
+ refreshing = false;
55
+ setTimeout(() => {
56
+ releasing = false;
57
+ pullDistance = 0;
58
+ }, 300);
59
+ }
60
+ } else {
61
+ releasing = true;
62
+ setTimeout(() => {
63
+ releasing = false;
64
+ pullDistance = 0;
65
+ }, 300);
66
+ }
67
+ }
68
+
69
+ onMount(() => {
70
+ // Also handle mouse events for desktop testing
71
+ function handleMouseDown(e: MouseEvent) {
72
+ if (container.scrollTop !== 0 || refreshing) return;
73
+ startY = e.clientY;
74
+ pulling = true;
75
+ }
76
+
77
+ function handleMouseMove(e: MouseEvent) {
78
+ if (!pulling || refreshing) return;
79
+
80
+ currentY = e.clientY;
81
+ const diff = currentY - startY;
82
+
83
+ if (diff > 0 && container.scrollTop === 0) {
84
+ e.preventDefault();
85
+ pullDistance = diff * 0.5;
86
+ }
87
+ }
88
+
89
+ function handleMouseUp() {
90
+ handleTouchEnd();
91
+ }
92
+
93
+ container.addEventListener('mousedown', handleMouseDown);
94
+ window.addEventListener('mousemove', handleMouseMove);
95
+ window.addEventListener('mouseup', handleMouseUp);
96
+
97
+ return () => {
98
+ container?.removeEventListener('mousedown', handleMouseDown);
99
+ window.removeEventListener('mousemove', handleMouseMove);
100
+ window.removeEventListener('mouseup', handleMouseUp);
101
+ };
102
+ });
103
+ </script>
104
+
105
+ <div class="pull-to-refresh-container">
106
+ <div
107
+ class="pull-indicator"
108
+ class:pulling
109
+ class:releasing
110
+ class:refreshing
111
+ style="transform: translateY({refreshing ? threshold : pullDistance}px)"
112
+ >
113
+ <div class="spinner-container" style="transform: rotate({pullProgress * 180}deg)">
114
+ {#if refreshing}
115
+ <div class="spinner"></div>
116
+ {:else}
117
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
118
+ <path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"
119
+ opacity={pullProgress} />
120
+ </svg>
121
+ {/if}
122
+ </div>
123
+ <div class="pull-text">
124
+ {#if refreshing}
125
+ Refreshing...
126
+ {:else if canRefresh}
127
+ Release to refresh
128
+ {:else}
129
+ Pull to refresh
130
+ {/if}
131
+ </div>
132
+ </div>
133
+
134
+ <div
135
+ bind:this={container}
136
+ class="content-container"
137
+ class:pulling
138
+ on:touchstart={handleTouchStart}
139
+ on:touchmove={handleTouchMove}
140
+ on:touchend={handleTouchEnd}
141
+ style="transform: translateY({refreshing ? threshold : pullDistance}px)"
142
+ >
143
+ {@render children?.()}
144
+ </div>
145
+ </div>
146
+
147
+ <style>
148
+ .pull-to-refresh-container {
149
+ position: relative;
150
+ height: 100%;
151
+ overflow: hidden;
152
+ }
153
+
154
+ .content-container {
155
+ height: 100%;
156
+ overflow-y: auto;
157
+ -webkit-overflow-scrolling: touch;
158
+ transition: transform 0.3s ease;
159
+ }
160
+
161
+ .content-container.pulling {
162
+ transition: none;
163
+ }
164
+
165
+ .pull-indicator {
166
+ position: absolute;
167
+ top: -60px;
168
+ left: 0;
169
+ right: 0;
170
+ height: 60px;
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ gap: 12px;
175
+ transition: transform 0.3s ease;
176
+ z-index: 10;
177
+ }
178
+
179
+ .pull-indicator.pulling {
180
+ transition: none;
181
+ }
182
+
183
+ .spinner-container {
184
+ transition: transform 0.2s ease;
185
+ }
186
+
187
+ .spinner {
188
+ width: 24px;
189
+ height: 24px;
190
+ border: 2px solid #e0e0e0;
191
+ border-top-color: #007bff;
192
+ border-radius: 50%;
193
+ animation: spin 0.8s linear infinite;
194
+ }
195
+
196
+ @keyframes spin {
197
+ to { transform: rotate(360deg); }
198
+ }
199
+
200
+ .pull-text {
201
+ font-size: 14px;
202
+ color: #666;
203
+ white-space: nowrap;
204
+ }
205
+ </style>