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}>&times;</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