EventTemplateSelector.svelte raw
1 <script>
2 import { createEventDispatcher } from "svelte";
3 import { eventKinds, kindCategories, createTemplateEvent, searchEventKinds } from "./eventKinds.js";
4
5 export let isOpen = false;
6 export let userPubkey = "";
7
8 const dispatch = createEventDispatcher();
9
10 let searchQuery = "";
11 let selectedCategory = "all";
12 let filteredKinds = eventKinds;
13
14 // Filter kinds based on search and category
15 $: {
16 let kinds = eventKinds;
17
18 // Apply category filter
19 const category = kindCategories.find(c => c.id === selectedCategory);
20 if (category) {
21 kinds = kinds.filter(category.filter);
22 }
23
24 // Apply search filter
25 if (searchQuery.trim()) {
26 const query = searchQuery.toLowerCase();
27 kinds = kinds.filter(k =>
28 k.name.toLowerCase().includes(query) ||
29 k.description.toLowerCase().includes(query) ||
30 k.kind.toString().includes(query) ||
31 (k.nip && k.nip.includes(query))
32 );
33 }
34
35 filteredKinds = kinds;
36 }
37
38 function selectKind(kindInfo) {
39 const template = createTemplateEvent(kindInfo.kind, userPubkey);
40 dispatch("select", {
41 kind: kindInfo,
42 template: template
43 });
44 closeModal();
45 }
46
47 function closeModal() {
48 isOpen = false;
49 searchQuery = "";
50 selectedCategory = "all";
51 dispatch("close");
52 }
53
54 function handleKeydown(event) {
55 if (event.key === "Escape") {
56 closeModal();
57 }
58 }
59
60 function handleBackdropClick(event) {
61 if (event.target === event.currentTarget) {
62 closeModal();
63 }
64 }
65
66 function getKindBadgeClass(kindInfo) {
67 if (kindInfo.isAddressable) return "badge-addressable";
68 if (kindInfo.isReplaceable) return "badge-replaceable";
69 if (kindInfo.kind >= 20000 && kindInfo.kind < 30000) return "badge-ephemeral";
70 return "badge-regular";
71 }
72
73 function getKindBadgeText(kindInfo) {
74 if (kindInfo.isAddressable) return "Addressable";
75 if (kindInfo.isReplaceable) return "Replaceable";
76 if (kindInfo.kind >= 20000 && kindInfo.kind < 30000) return "Ephemeral";
77 return "Regular";
78 }
79 </script>
80
81 <svelte:window on:keydown={handleKeydown} />
82
83 {#if isOpen}
84 <!-- svelte-ignore a11y-click-events-have-key-events -->
85 <!-- svelte-ignore a11y-no-static-element-interactions -->
86 <div class="modal-backdrop" on:click={handleBackdropClick}>
87 <div class="modal-content">
88 <div class="modal-header">
89 <h2>Generate Event Template</h2>
90 <button class="close-btn" on:click={closeModal}>×</button>
91 </div>
92
93 <div class="modal-filters">
94 <div class="search-box">
95 <input
96 type="text"
97 placeholder="Search by name, description, or kind number..."
98 bind:value={searchQuery}
99 class="search-input"
100 />
101 </div>
102
103 <div class="category-tabs">
104 {#each kindCategories as category}
105 <button
106 class="category-tab"
107 class:active={selectedCategory === category.id}
108 on:click={() => selectedCategory = category.id}
109 >
110 {category.name}
111 </button>
112 {/each}
113 </div>
114 </div>
115
116 <div class="modal-body">
117 <div class="kinds-list">
118 {#if filteredKinds.length === 0}
119 <div class="no-results">
120 No event kinds found matching "{searchQuery}"
121 </div>
122 {:else}
123 {#each filteredKinds as kindInfo}
124 <button
125 class="kind-item"
126 on:click={() => selectKind(kindInfo)}
127 >
128 <div class="kind-header">
129 <span class="kind-number">Kind {kindInfo.kind}</span>
130 <span class="kind-badge {getKindBadgeClass(kindInfo)}">
131 {getKindBadgeText(kindInfo)}
132 </span>
133 {#if kindInfo.nip && kindInfo.nip !== "XX"}
134 <span class="nip-badge">NIP-{kindInfo.nip}</span>
135 {/if}
136 </div>
137 <div class="kind-name">{kindInfo.name}</div>
138 <div class="kind-description">{kindInfo.description}</div>
139 </button>
140 {/each}
141 {/if}
142 </div>
143 </div>
144
145 <div class="modal-footer">
146 <span class="result-count">{filteredKinds.length} event type{filteredKinds.length !== 1 ? 's' : ''}</span>
147 <button class="cancel-btn" on:click={closeModal}>Cancel</button>
148 </div>
149 </div>
150 </div>
151 {/if}
152
153 <style>
154 .modal-backdrop {
155 position: fixed;
156 top: 0;
157 left: 0;
158 right: 0;
159 bottom: 0;
160 background: rgba(0, 0, 0, 0.7);
161 display: flex;
162 align-items: center;
163 justify-content: center;
164 z-index: 1000;
165 }
166
167 .modal-content {
168 background: var(--card-bg);
169 border-radius: 0.5rem;
170 width: 90%;
171 max-width: 800px;
172 max-height: 85vh;
173 display: flex;
174 flex-direction: column;
175 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
176 border: 1px solid var(--border-color);
177 }
178
179 .modal-header {
180 display: flex;
181 justify-content: space-between;
182 align-items: center;
183 padding: 1rem 1.5rem;
184 border-bottom: 1px solid var(--border-color);
185 }
186
187 .modal-header h2 {
188 margin: 0;
189 font-size: 1.25rem;
190 color: var(--text-color);
191 }
192
193 .close-btn {
194 background: none;
195 border: none;
196 font-size: 1.5rem;
197 cursor: pointer;
198 color: var(--text-color);
199 padding: 0;
200 width: 2rem;
201 height: 2rem;
202 display: flex;
203 align-items: center;
204 justify-content: center;
205 border-radius: 0.25rem;
206 }
207
208 .close-btn:hover {
209 background: var(--button-hover-bg);
210 }
211
212 .modal-filters {
213 padding: 1rem 1.5rem;
214 border-bottom: 1px solid var(--border-color);
215 }
216
217 .search-box {
218 margin-bottom: 0.75rem;
219 }
220
221 .search-input {
222 width: 100%;
223 padding: 0.75rem;
224 border: 1px solid var(--border-color);
225 border-radius: 0.25rem;
226 background: var(--input-bg);
227 color: var(--input-text-color);
228 font-size: 0.9rem;
229 }
230
231 .search-input:focus {
232 outline: none;
233 border-color: var(--accent-color);
234 }
235
236 .category-tabs {
237 display: flex;
238 flex-wrap: wrap;
239 gap: 0.5rem;
240 }
241
242 .category-tab {
243 padding: 0.4rem 0.75rem;
244 border: 1px solid var(--border-color);
245 border-radius: 1rem;
246 background: transparent;
247 color: var(--text-color);
248 font-size: 0.75rem;
249 cursor: pointer;
250 transition: all 0.2s;
251 }
252
253 .category-tab:hover {
254 background: var(--button-hover-bg);
255 }
256
257 .category-tab.active {
258 background: var(--accent-color);
259 border-color: var(--accent-color);
260 color: white;
261 }
262
263 .modal-body {
264 flex: 1;
265 overflow-y: auto;
266 padding: 1rem 1.5rem;
267 }
268
269 .kinds-list {
270 display: flex;
271 flex-direction: column;
272 gap: 0.5rem;
273 }
274
275 .kind-item {
276 display: block;
277 width: 100%;
278 text-align: left;
279 padding: 0.75rem 1rem;
280 border: 1px solid var(--border-color);
281 border-radius: 0.375rem;
282 background: var(--bg-color);
283 cursor: pointer;
284 transition: all 0.2s;
285 }
286
287 .kind-item:hover {
288 border-color: var(--accent-color);
289 background: var(--button-hover-bg);
290 }
291
292 .kind-header {
293 display: flex;
294 align-items: center;
295 gap: 0.5rem;
296 margin-bottom: 0.25rem;
297 }
298
299 .kind-number {
300 font-family: monospace;
301 font-size: 0.8rem;
302 color: var(--accent-color);
303 font-weight: bold;
304 }
305
306 .kind-badge {
307 font-size: 0.65rem;
308 padding: 0.15rem 0.4rem;
309 border-radius: 0.25rem;
310 font-weight: 500;
311 }
312
313 .badge-regular {
314 background: #6c757d;
315 color: white;
316 }
317
318 .badge-replaceable {
319 background: #17a2b8;
320 color: white;
321 }
322
323 .badge-ephemeral {
324 background: #ffc107;
325 color: black;
326 }
327
328 .badge-addressable {
329 background: var(--success);
330 color: white;
331 }
332
333 .nip-badge {
334 font-size: 0.65rem;
335 padding: 0.15rem 0.4rem;
336 border-radius: 0.25rem;
337 background: var(--primary);
338 color: var(--text-color);
339 }
340
341 .kind-name {
342 font-weight: 600;
343 color: var(--text-color);
344 margin-bottom: 0.25rem;
345 }
346
347 .kind-description {
348 font-size: 0.85rem;
349 color: var(--text-color);
350 opacity: 0.7;
351 }
352
353 .no-results {
354 text-align: center;
355 padding: 2rem;
356 color: var(--text-color);
357 opacity: 0.6;
358 }
359
360 .modal-footer {
361 display: flex;
362 justify-content: space-between;
363 align-items: center;
364 padding: 1rem 1.5rem;
365 border-top: 1px solid var(--border-color);
366 }
367
368 .result-count {
369 font-size: 0.85rem;
370 color: var(--text-color);
371 opacity: 0.7;
372 }
373
374 .cancel-btn {
375 padding: 0.5rem 1rem;
376 border: 1px solid var(--border-color);
377 border-radius: 0.25rem;
378 background: var(--button-bg);
379 color: var(--button-text);
380 cursor: pointer;
381 font-size: 0.9rem;
382 }
383
384 .cancel-btn:hover {
385 background: var(--button-hover-bg);
386 }
387
388 @media (max-width: 640px) {
389 .modal-content {
390 width: 95%;
391 max-height: 90vh;
392 }
393
394 .category-tabs {
395 overflow-x: auto;
396 flex-wrap: nowrap;
397 padding-bottom: 0.5rem;
398 }
399
400 .category-tab {
401 white-space: nowrap;
402 }
403 }
404 </style>
405