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

update pictuary

Browse files
src/lib/components/Pages/Pictuary.svelte CHANGED
@@ -1,26 +1,81 @@
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
  import { getAllMonsters } from '$lib/db/monsters';
4
- import type { Monster } from '$lib/db/schema';
 
 
 
 
5
 
6
- let monsters: Monster[] = $state([]);
 
 
7
  let isLoading = $state(true);
8
 
 
 
 
 
 
 
 
 
 
 
 
9
  onMount(async () => {
10
  try {
11
- monsters = await getAllMonsters();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  } catch (err) {
13
- console.error('Failed to load monsters:', err);
14
  } finally {
15
  isLoading = false;
16
  }
17
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  </script>
19
 
20
  <div class="pictuary-page">
21
  <header class="page-header">
22
- <h2>Your Pictuary</h2>
23
- <p class="monster-count">{monsters.length} Piclets collected</p>
 
 
24
  </header>
25
 
26
  {#if isLoading}
@@ -28,31 +83,79 @@
28
  <div class="spinner"></div>
29
  <p>Loading collection...</p>
30
  </div>
31
- {:else if monsters.length === 0}
32
  <div class="empty-state">
33
- <img
34
- src="https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/pictuary_logo.png"
35
- alt="Pictuary"
36
- class="empty-icon"
37
- />
38
  <h3>No Piclets Yet</h3>
39
- <p>Start scanning photos to build your collection!</p>
40
  </div>
41
  {:else}
42
- <div class="monster-grid">
43
- {#each monsters as monster}
44
- <div class="monster-card">
45
- <img
46
- src={monster.imageData || monster.imageUrl}
47
- alt={monster.name}
48
- class="monster-image"
49
- />
50
- <h4 class="monster-name">{monster.name}</h4>
51
- <p class="monster-date">
52
- {new Date(monster.createdAt).toLocaleDateString()}
53
- </p>
 
 
 
 
 
 
 
54
  </div>
55
- {/each}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  </div>
57
  {/if}
58
  </div>
@@ -62,24 +165,26 @@
62
  height: 100%;
63
  overflow-y: auto;
64
  -webkit-overflow-scrolling: touch;
 
65
  }
66
 
67
  .page-header {
68
- padding: 1.5rem 1rem;
 
69
  background: white;
70
- border-bottom: 1px solid #eee;
71
  position: sticky;
72
  top: 0;
73
  z-index: 10;
74
  }
75
 
76
- .page-header h2 {
77
  margin: 0;
78
- font-size: 1.5rem;
 
79
  color: #333;
80
  }
81
 
82
- .monster-count {
83
  margin: 0.25rem 0 0;
84
  color: #666;
85
  font-size: 0.9rem;
@@ -106,14 +211,15 @@
106
  margin-bottom: 1rem;
107
  }
108
 
109
- .empty-icon {
110
- width: 80px;
111
- opacity: 0.5;
112
  margin-bottom: 1rem;
113
  }
114
 
115
  .empty-state h3 {
116
  margin: 0 0 0.5rem;
 
 
117
  color: #333;
118
  }
119
 
@@ -122,45 +228,68 @@
122
  color: #666;
123
  }
124
 
125
- .monster-grid {
126
- display: grid;
127
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
128
- gap: 1rem;
129
- padding: 1rem;
130
  }
131
 
132
- .monster-card {
133
- background: white;
134
- border-radius: 12px;
135
- overflow: hidden;
136
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
137
- transition: transform 0.2s;
138
  }
139
 
140
- .monster-card:active {
141
- transform: scale(0.95);
 
 
 
142
  }
143
 
144
- .monster-image {
145
- width: 100%;
146
- aspect-ratio: 1;
147
- object-fit: contain;
148
- background: #f0f0f0;
149
  }
150
 
151
- .monster-name {
152
  margin: 0;
153
- padding: 0.75rem;
154
- font-size: 0.9rem;
155
- font-weight: 600;
156
- color: #333;
157
  }
158
 
159
- .monster-date {
160
- margin: 0;
161
- padding: 0 0.75rem 0.75rem;
162
- font-size: 0.75rem;
163
- color: #999;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  }
165
 
166
  @keyframes spin {
 
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(() => {
17
+ const map = new Map<number, PicletInstance>();
18
+ rosterPiclets.forEach(piclet => {
19
+ if (piclet.rosterPosition !== undefined) {
20
+ map.set(piclet.rosterPosition, piclet);
21
+ }
22
+ });
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, '-'))
43
+ );
44
  } catch (err) {
45
+ console.error('Failed to load piclets:', err);
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">
74
  <header class="page-header">
75
+ <h1>Pictuary</h1>
76
+ <p class="piclet-count">
77
+ {rosterPiclets.length + storagePiclets.length} Piclets collected
78
+ </p>
79
  </header>
80
 
81
  {#if isLoading}
 
83
  <div class="spinner"></div>
84
  <p>Loading collection...</p>
85
  </div>
86
+ {:else if rosterPiclets.length === 0 && storagePiclets.length === 0 && discoveredMonsters.length === 0}
87
  <div class="empty-state">
88
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
89
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
90
+ <circle cx="12" cy="13" r="4"></circle>
91
+ </svg>
 
92
  <h3>No Piclets Yet</h3>
93
+ <p>Take photos to discover new Piclets!</p>
94
  </div>
95
  {:else}
96
+ <div class="content">
97
+ <!-- Roster Section -->
98
+ <section class="roster-section">
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>
117
+
118
+ <!-- Storage Section -->
119
+ {#if storagePiclets.length > 0}
120
+ <section class="storage-section">
121
+ <div class="section-header">
122
+ <h2>Storage ({storagePiclets.length})</h2>
123
+ {#if storagePiclets.length > 10}
124
+ <button class="view-all-btn">View All</button>
125
+ {/if}
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>
136
+ </section>
137
+ {/if}
138
+
139
+ <!-- Discovered Section -->
140
+ {#if discoveredMonsters.length > 0}
141
+ <section class="discovered-section">
142
+ <div class="section-header">
143
+ <h2>Discovered ({discoveredMonsters.length})</h2>
144
+ {#if discoveredMonsters.length > 10}
145
+ <button class="view-all-btn">View All</button>
146
+ {/if}
147
+ </div>
148
+ <div class="horizontal-scroll">
149
+ {#each discoveredMonsters.slice(0, 10) as monster}
150
+ <DiscoveredCard
151
+ {monster}
152
+ size={100}
153
+ onClick={() => handleDiscoveredClick(monster)}
154
+ />
155
+ {/each}
156
+ </div>
157
+ </section>
158
+ {/if}
159
  </div>
160
  {/if}
161
  </div>
 
165
  height: 100%;
166
  overflow-y: auto;
167
  -webkit-overflow-scrolling: touch;
168
+ background: white;
169
  }
170
 
171
  .page-header {
172
+ padding: 1rem;
173
+ padding-top: 0.5rem;
174
  background: white;
 
175
  position: sticky;
176
  top: 0;
177
  z-index: 10;
178
  }
179
 
180
+ .page-header h1 {
181
  margin: 0;
182
+ font-size: 1.75rem;
183
+ font-weight: bold;
184
  color: #333;
185
  }
186
 
187
+ .piclet-count {
188
  margin: 0.25rem 0 0;
189
  color: #666;
190
  font-size: 0.9rem;
 
211
  margin-bottom: 1rem;
212
  }
213
 
214
+ .empty-state svg {
215
+ color: #8e8e93;
 
216
  margin-bottom: 1rem;
217
  }
218
 
219
  .empty-state h3 {
220
  margin: 0 0 0.5rem;
221
+ font-size: 1.25rem;
222
+ font-weight: 600;
223
  color: #333;
224
  }
225
 
 
228
  color: #666;
229
  }
230
 
231
+ .content {
232
+ padding: 0 1rem 100px;
 
 
 
233
  }
234
 
235
+ section {
236
+ margin-bottom: 2rem;
 
 
 
 
237
  }
238
 
239
+ section h2 {
240
+ font-size: 1.5rem;
241
+ font-weight: bold;
242
+ color: #8e8e93;
243
+ margin: 0 0 0.75rem;
244
  }
245
 
246
+ .section-header {
247
+ display: flex;
248
+ justify-content: space-between;
249
+ align-items: center;
250
+ margin-bottom: 0.75rem;
251
  }
252
 
253
+ .section-header h2 {
254
  margin: 0;
 
 
 
 
255
  }
256
 
257
+ .view-all-btn {
258
+ background: none;
259
+ border: none;
260
+ color: #007bff;
261
+ font-size: 1rem;
262
+ cursor: pointer;
263
+ padding: 0;
264
+ }
265
+
266
+ .roster-grid {
267
+ display: grid;
268
+ grid-template-columns: repeat(3, 1fr);
269
+ grid-template-rows: repeat(2, 1fr);
270
+ gap: 12px;
271
+ }
272
+
273
+ .horizontal-scroll {
274
+ display: flex;
275
+ gap: 8px;
276
+ overflow-x: auto;
277
+ -webkit-overflow-scrolling: touch;
278
+ padding-bottom: 8px;
279
+ }
280
+
281
+ .horizontal-scroll::-webkit-scrollbar {
282
+ height: 4px;
283
+ }
284
+
285
+ .horizontal-scroll::-webkit-scrollbar-track {
286
+ background: #f1f1f1;
287
+ border-radius: 2px;
288
+ }
289
+
290
+ .horizontal-scroll::-webkit-scrollbar-thumb {
291
+ background: #888;
292
+ border-radius: 2px;
293
  }
294
 
295
  @keyframes spin {
src/lib/components/Piclets/DiscoveredCard.svelte ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Monster } from '$lib/db/schema';
3
+
4
+ interface Props {
5
+ monster: Monster;
6
+ size?: number;
7
+ onClick?: () => void;
8
+ }
9
+
10
+ let { monster, size = 100, onClick }: Props = $props();
11
+
12
+ // Default type color
13
+ const typeColor = '#007bff';
14
+ </script>
15
+
16
+ <button
17
+ class="discovered-card"
18
+ style="width: {size}px; height: {size + 30}px;"
19
+ onclick={onClick}
20
+ type="button"
21
+ >
22
+ <div class="image-container" style="background-color: {typeColor}10;">
23
+ <img
24
+ src={monster.imageData || monster.imageUrl}
25
+ alt={monster.name}
26
+ class="piclet-image"
27
+ style="width: {size * 0.9}px; height: {size * 0.9}px;"
28
+ />
29
+ </div>
30
+
31
+ <div class="name-section">
32
+ <p class="name">{monster.name}</p>
33
+ </div>
34
+ </button>
35
+
36
+ <style>
37
+ .discovered-card {
38
+ display: flex;
39
+ flex-direction: column;
40
+ background: white;
41
+ border-radius: 12px;
42
+ border: 2px solid;
43
+ border-color: color-mix(in srgb, var(--type-color, #007bff) 30%, transparent);
44
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
45
+ padding: 0;
46
+ cursor: pointer;
47
+ transition: transform 0.2s;
48
+ opacity: 0.8;
49
+ }
50
+
51
+ .discovered-card:active {
52
+ transform: scale(0.95);
53
+ }
54
+
55
+ .image-container {
56
+ flex: 1;
57
+ position: relative;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ border-radius: 10px 10px 0 0;
62
+ overflow: hidden;
63
+ }
64
+
65
+ .piclet-image {
66
+ object-fit: contain;
67
+ filter: brightness(0.9);
68
+ }
69
+
70
+ .name-section {
71
+ height: 30px;
72
+ padding: 4px 6px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ }
77
+
78
+ .name {
79
+ margin: 0;
80
+ font-size: 11px;
81
+ font-weight: 600;
82
+ text-align: center;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ white-space: nowrap;
86
+ }
87
+ </style>
src/lib/components/Piclets/EmptySlotCard.svelte ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ interface Props {
3
+ size?: number;
4
+ isHighlighted?: boolean;
5
+ onClick?: () => void;
6
+ }
7
+
8
+ let { size = 100, isHighlighted = false, onClick }: Props = $props();
9
+ </script>
10
+
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
+ >
18
+ <svg
19
+ width="32"
20
+ height="32"
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ stroke-width="2"
25
+ >
26
+ {#if isHighlighted}
27
+ <circle cx="12" cy="12" r="10" />
28
+ <path d="M12 8v8m-4-4h8" />
29
+ {:else}
30
+ <path d="M12 5v14m-7-7h14" />
31
+ {/if}
32
+ </svg>
33
+ </button>
34
+
35
+ <style>
36
+ .empty-slot {
37
+ background: #f5f5f5;
38
+ border: 2px dashed #d1d1d6;
39
+ border-radius: 12px;
40
+ cursor: pointer;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ color: #8e8e93;
45
+ transition: all 0.2s;
46
+ }
47
+
48
+ .empty-slot:hover {
49
+ background: #e5e5ea;
50
+ }
51
+
52
+ .empty-slot:active {
53
+ transform: scale(0.95);
54
+ }
55
+
56
+ .empty-slot.highlighted {
57
+ background: rgba(0, 123, 255, 0.1);
58
+ border-color: #007bff;
59
+ color: #007bff;
60
+ }
61
+ </style>
src/lib/components/Piclets/PicletCard.svelte ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { PicletInstance } from '$lib/db/schema';
3
+
4
+ interface Props {
5
+ instance: PicletInstance;
6
+ size?: number;
7
+ showDetails?: boolean;
8
+ onClick?: () => void;
9
+ }
10
+
11
+ let { instance, size = 100, showDetails = true, onClick }: Props = $props();
12
+
13
+ const hpPercentage = $derived(instance.currentHp / instance.maxHp);
14
+ const hpColor = $derived(
15
+ hpPercentage > 0.5 ? '#34c759' :
16
+ hpPercentage > 0.25 ? '#ffcc00' :
17
+ '#ff3b30'
18
+ );
19
+
20
+ // Default type color - we'll enhance this later with a proper type system
21
+ const typeColor = '#007bff';
22
+ </script>
23
+
24
+ <button
25
+ class="piclet-card"
26
+ style="width: {size}px; height: {size + (showDetails ? 40 : 0)}px;"
27
+ onclick={onClick}
28
+ type="button"
29
+ >
30
+ <div class="image-container" style="background-color: {typeColor}10;">
31
+ <img
32
+ src={instance.imageData || instance.imageUrl}
33
+ alt={instance.nickname || 'Piclet'}
34
+ class="piclet-image"
35
+ style="width: {size * 0.9}px; height: {size * 0.9}px;"
36
+ />
37
+ <div class="level-badge">
38
+ Lv.{instance.level}
39
+ </div>
40
+ </div>
41
+
42
+ {#if showDetails}
43
+ <div class="details-section">
44
+ <p class="nickname">{instance.nickname || 'Unknown'}</p>
45
+ <div class="hp-bar">
46
+ <div
47
+ class="hp-fill"
48
+ style="width: {hpPercentage * 100}%; background-color: {hpColor};"
49
+ ></div>
50
+ </div>
51
+ </div>
52
+ {/if}
53
+ </button>
54
+
55
+ <style>
56
+ .piclet-card {
57
+ display: flex;
58
+ flex-direction: column;
59
+ background: white;
60
+ border-radius: 12px;
61
+ border: 2px solid;
62
+ border-color: color-mix(in srgb, var(--type-color, #007bff) 30%, transparent);
63
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
64
+ padding: 0;
65
+ cursor: pointer;
66
+ transition: transform 0.2s;
67
+ }
68
+
69
+ .piclet-card:active {
70
+ transform: scale(0.95);
71
+ }
72
+
73
+ .image-container {
74
+ flex: 1;
75
+ position: relative;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ border-radius: 10px 10px 0 0;
80
+ overflow: hidden;
81
+ }
82
+
83
+ .piclet-image {
84
+ object-fit: contain;
85
+ }
86
+
87
+ .level-badge {
88
+ position: absolute;
89
+ top: 4px;
90
+ right: 4px;
91
+ background: rgba(255, 255, 255, 0.9);
92
+ padding: 2px 6px;
93
+ border-radius: 8px;
94
+ font-size: 10px;
95
+ font-weight: bold;
96
+ }
97
+
98
+ .details-section {
99
+ height: 40px;
100
+ padding: 4px 6px;
101
+ display: flex;
102
+ flex-direction: column;
103
+ justify-content: center;
104
+ }
105
+
106
+ .nickname {
107
+ margin: 0;
108
+ font-size: 11px;
109
+ font-weight: 600;
110
+ text-align: center;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ white-space: nowrap;
114
+ }
115
+
116
+ .hp-bar {
117
+ height: 3px;
118
+ background: #f0f0f0;
119
+ border-radius: 1.5px;
120
+ margin-top: 2px;
121
+ overflow: hidden;
122
+ }
123
+
124
+ .hp-fill {
125
+ height: 100%;
126
+ border-radius: 1.5px;
127
+ transition: width 0.3s ease;
128
+ }
129
+ </style>