Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
at main 20 kB view raw
1<!-- Enhanced Filter Events Results with Card Layout --> 2{% if events %} 3<div class="level"> 4 <div class="level-left"> 5 <div class="level-item"> 6 <p class="subtitle is-6 has-text-light"> 7 <span class="icon"> 8 <i class="fas fa-calendar-alt"></i> 9 </span> 10 {{ t("filter-showing-results", start=result_range.start, end=result_range.end, total=total_count) }} 11 </p> 12 </div> 13 </div> 14 <div class="level-right"> 15 <div class="level-item"> 16 <div class="field has-addons"> 17 <div class="control"> 18 <button class="button is-small" id="grid-view-btn" onclick="toggleView('grid')"> 19 <span class="icon"> 20 <i class="fas fa-th"></i> 21 </span> 22 <span>{{ t("view-grid") }}</span> 23 </button> 24 </div> 25 <div class="control"> 26 <button class="button is-small" id="list-view-btn" onclick="toggleView('list')"> 27 <span class="icon"> 28 <i class="fas fa-list"></i> 29 </span> 30 <span>{{ t("view-list") }}</span> 31 </button> 32 </div> 33 </div> 34 </div> 35 </div> 36</div> 37 38<!-- Active Filters Display --> 39{% if active_filters and active_filters.has_active_filters %} 40<div class="message is-info mb-4"> 41 <div class="message-header"> 42 <p> 43 <span class="icon"> 44 <i class="fas fa-filter"></i> 45 </span> 46 {{ t("filter-active-filters") }} 47 </p> 48 <a href="/events" class="button is-small is-white"> 49 <span class="icon"> 50 <i class="fas fa-times"></i> 51 </span> 52 <span>{{ t("filter-clear-all") }}</span> 53 </a> 54 </div> 55 <div class="message-body"> 56 <div class="tags"> 57 {% for filter in active_filters.filters %} 58 <span class="tag is-info is-medium"> 59 <span class="icon is-small"> 60 <i class="fas fa-tag"></i> 61 </span> 62 <!-- Display the filter with translated label --> 63 {% if filter.display_params %} 64 {{ t(filter.display_key, **filter.display_params) }} 65 {% else %} 66 {{ t(filter.display_key, term=filter.display_value) }} 67 {% endif %} 68 69 <!-- Remove filter button --> 70 <button type="button" 71 class="delete is-small" 72 title="{{ t('filter-remove-filter') }}" 73 data-filter-key="{{ filter.key }}" 74 data-remove-params='{{ filter.remove_params | tojson }}' 75 onclick="removeFilter(this)"> 76 </button> 77 </span> 78 {% endfor %} 79 </div> 80 </div> 81</div> 82{% endif %} 83 84<!-- Event List with Enhanced Card Layout --> 85<div id="events-container" class="events-grid"> 86 {% for event in events %} 87 <div class="event-card" data-event-id="{{ event.id }}"> 88 <div class="event-card-header"> 89 <div class="event-date-badge"> 90 <div class="event-month">{{ event.month_abbr | default(event.start_time | date('%b')) }}</div> 91 <div class="event-day">{{ event.start_time | date('%d') }}</div> 92 </div> 93 94 <div class="event-status-tags"> 95 {% if event.status %} 96 <span class="tag event-status-{{ event.status | lower }}"> 97 {% if event.status == "Active" %} 98 <span class="icon"><i class="fas fa-check-circle"></i></span> 99 {% elif event.status == "Cancelled" %} 100 <span class="icon"><i class="fas fa-times-circle"></i></span> 101 {% elif event.status == "Postponed" %} 102 <span class="icon"><i class="fas fa-pause-circle"></i></span> 103 {% endif %} 104 <span>{{ t("event-status-" + event.status | lower) }}</span> 105 </span> 106 {% endif %} 107 108 {% if event.mode %} 109 <span class="tag event-mode-{{ event.mode | lower }}"> 110 {% if event.mode == "InPerson" %} 111 <span class="icon"><i class="fas fa-users"></i></span> 112 {% elif event.mode == "Online" %} 113 <span class="icon"><i class="fas fa-wifi"></i></span> 114 {% elif event.mode == "Hybrid" %} 115 <span class="icon"><i class="fas fa-globe"></i></span> 116 {% endif %} 117 <span>{{ t("event-mode-" + event.mode | lower) }}</span> 118 </span> 119 {% endif %} 120 </div> 121 </div> 122 123 <div class="event-card-body"> 124 <h3 class="event-title"> 125 <a href="{{ event.url | default('/event/' + event.id) }}" class="has-text-light"> 126 {{ event.title }} 127 </a> 128 </h3> 129 130 <p class="event-description"> 131 {{ event.description | truncate(120) | nl2br | safe }} 132 </p> 133 134 <div class="event-meta"> 135 <div class="event-time"> 136 <span class="icon"> 137 <i class="fas fa-clock"></i> 138 </span> 139 <span> 140 {{ event.start_time | date('%H:%M') }} 141 {% if event.end_time %} - {{ event.end_time | date('%H:%M') }}{% endif %} 142 </span> 143 </div> 144 145 {% if event.location %} 146 <div class="event-location"> 147 <span class="icon"> 148 <i class="fas fa-map-marker-alt"></i> 149 </span> 150 <span>{{ event.location | truncate(40) }}</span> 151 </div> 152 {% endif %} 153 154 {% if event.organizer %} 155 <div class="event-organizer"> 156 <span class="icon"> 157 <i class="fas fa-user"></i> 158 </span> 159 <span>{{ event.organizer.display_name | default(event.organizer.handle) }}</span> 160 </div> 161 {% endif %} 162 </div> 163 </div> 164 165 <!-- Event Statistics --> 166 <div class="event-stats"> 167 <div class="stat" title="{{ t('event-count-going', count=event.count_going) }}"> 168 <i class="fas fa-star"></i> 169 <span>{{ event.count_going }}</span> 170 </div> 171 <div class="stat" title="{{ t('event-count-interested', count=event.count_interested) }}"> 172 <i class="fas fa-eye"></i> 173 <span>{{ event.count_interested }}</span> 174 </div> 175 <div class="stat" title="{{ t('event-count-not-going', count=event.count_not_going) }}"> 176 <i class="fas fa-ban"></i> 177 <span>{{ event.count_not_going }}</span> 178 </div> 179 </div> 180 181 <div class="event-actions"> 182 <a href="{{ event.url | default('/event/' + event.id) }}" 183 class="button is-primary is-small"> 184 <span class="icon"> 185 <i class="fas fa-eye"></i> 186 </span> 187 <span>{{ t("event-view") }}</span> 188 </a> 189 190 {% if event.can_rsvp %} 191 <button class="button is-success is-small" 192 onclick="toggleRSVP('{{ event.id }}', this)"> 193 <span class="icon"> 194 <i class="fas fa-check"></i> 195 </span> 196 <span>{{ t("event-rsvp") }}</span> 197 </button> 198 {% endif %} 199 </div> 200 </div> 201 </div> 202 {% endfor %} 203</div> 204 205<!-- Pagination --> 206{% if total_pages > 1 %} 207<nav class="pagination is-centered mt-5" role="navigation" aria-label="pagination"> 208 {% if pagination.has_prev %} 209 <a class="pagination-previous" 210 hx-get="/events" 211 hx-target="#results-content" 212 hx-include="#filter-form" 213 hx-vals='{"page": "{{ pagination.prev_page }}"}' 214 hx-headers='{"Accept-Language": "{{ current_locale }}"}' 215 style="cursor: pointer;"> 216 <span class="icon"> 217 <i class="fas fa-chevron-left"></i> 218 </span> 219 <span>{{ t("pagination-previous") }}</span> 220 </a> 221 {% else %} 222 <a class="pagination-previous" disabled> 223 <span class="icon"> 224 <i class="fas fa-chevron-left"></i> 225 </span> 226 <span>{{ t("pagination-previous") }}</span> 227 </a> 228 {% endif %} 229 230 {% if pagination.has_next %} 231 <a class="pagination-next" 232 hx-get="/events" 233 hx-target="#results-content" 234 hx-include="#filter-form" 235 hx-vals='{"page": "{{ pagination.next_page }}"}' 236 hx-headers='{"Accept-Language": "{{ current_locale }}"}' 237 style="cursor: pointer;"> 238 <span>{{ t("pagination-next") }}</span> 239 <span class="icon"> 240 <i class="fas fa-chevron-right"></i> 241 </span> 242 </a> 243 {% else %} 244 <a class="pagination-next" disabled> 245 <span>{{ t("pagination-next") }}</span> 246 <span class="icon"> 247 <i class="fas fa-chevron-right"></i> 248 </span> 249 </a> 250 {% endif %} 251 252 <ul class="pagination-list"> 253 {% for page_num in pagination.page_range %} 254 {% if page_num == pagination.current_page %} 255 <li> 256 <a class="pagination-link is-current" aria-label="Page {{ page_num }}" aria-current="page">{{ page_num }}</a> 257 </li> 258 {% else %} 259 <li> 260 <a class="pagination-link" 261 aria-label="Goto page {{ page_num }}" 262 hx-get="/events" 263 hx-target="#results-content" 264 hx-include="#filter-form" 265 hx-vals='{"page": "{{ page_num }}"}' 266 hx-headers='{"Accept-Language": "{{ current_locale }}"}' 267 style="cursor: pointer;"> 268 {{ page_num }} 269 </a> 270 </li> 271 {% endif %} 272 {% endfor %} 273 </ul> 274</nav> 275{% endif %} 276 277{% else %} 278<!-- Enhanced No Results State --> 279<div class="has-text-centered py-6"> 280 <div class="empty-state"> 281 <div class="empty-state-icon"> 282 <span class="icon is-large"> 283 <i class="fas fa-calendar-times fa-4x has-text-grey-light"></i> 284 </span> 285 </div> 286 <h3 class="title is-4 has-text-grey-light">{{ t("filter-no-results-title") }}</h3> 287 <p class="subtitle is-6 has-text-grey">{{ t("filter-no-results-subtitle") }}</p> 288 289 <div class="empty-state-actions"> 290 <a href="/events" class="button is-primary is-medium"> 291 <span class="icon"> 292 <i class="fas fa-refresh"></i> 293 </span> 294 <span>{{ t("filter-clear-all") }}</span> 295 </a> 296 <a href="/create-event" class="button is-success is-medium"> 297 <span class="icon"> 298 <i class="fas fa-plus"></i> 299 </span> 300 <span>{{ t("create-event") }}</span> 301 </a> 302 </div> 303 </div> 304</div> 305{% endif %} 306 307<style> 308/* Enhanced Event Cards Grid Layout */ 309.events-grid { 310 display: grid; 311 grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); 312 gap: 1.5rem; 313 margin-top: 1rem; 314} 315 316.events-list { 317 display: flex; 318 flex-direction: column; 319 gap: 1rem; 320} 321 322.events-list .event-card { 323 display: flex; 324 flex-direction: row; 325 max-width: none; 326 min-height: auto; 327} 328 329.events-list .event-card-header { 330 flex-shrink: 0; 331 margin-right: 1rem; 332} 333 334.events-list .event-card-body { 335 flex: 1; 336} 337 338.event-card { 339 background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); 340 border-radius: 16px; 341 padding: 1.5rem; 342 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 343 border: 1px solid rgba(255, 255, 255, 0.1); 344 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 345 position: relative; 346 overflow: hidden; 347 min-height: 300px; 348 display: flex; 349 flex-direction: column; 350} 351 352.event-card::before { 353 content: ''; 354 position: absolute; 355 top: 0; 356 left: 0; 357 right: 0; 358 height: 4px; 359 background: linear-gradient(90deg, #3498db, #e74c3c, #f39c12, #2ecc71); 360 opacity: 0; 361 transition: opacity 0.3s ease; 362} 363 364.event-card:hover::before { 365 opacity: 1; 366} 367 368.event-card:hover { 369 transform: translateY(-8px) scale(1.02); 370 box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); 371 border-color: rgba(52, 152, 219, 0.3); 372} 373 374.event-card-header { 375 display: flex; 376 justify-content: space-between; 377 align-items: flex-start; 378 margin-bottom: 1rem; 379} 380 381.event-date-badge { 382 background: linear-gradient(135deg, #3498db, #2980b9); 383 border-radius: 12px; 384 padding: 0.75rem; 385 text-align: center; 386 color: white; 387 box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); 388 min-width: 60px; 389} 390 391.event-month { 392 font-size: 0.75rem; 393 font-weight: 600; 394 text-transform: uppercase; 395 letter-spacing: 0.5px; 396 opacity: 0.9; 397} 398 399.event-day { 400 font-size: 1.5rem; 401 font-weight: 700; 402 line-height: 1; 403 margin-top: 0.25rem; 404} 405 406.event-status-tags { 407 display: flex; 408 flex-direction: column; 409 gap: 0.5rem; 410 align-items: flex-end; 411} 412 413.event-status-active { 414 background-color: rgba(46, 204, 113, 0.9); 415 color: white; 416} 417 418.event-status-cancelled { 419 background-color: rgba(231, 76, 60, 0.9); 420 color: white; 421} 422 423.event-status-postponed { 424 background-color: rgba(243, 156, 18, 0.9); 425 color: white; 426} 427 428.event-mode-inperson { 429 background-color: rgba(52, 152, 219, 0.9); 430 color: white; 431} 432 433.event-mode-online { 434 background-color: rgba(155, 89, 182, 0.9); 435 color: white; 436} 437 438.event-mode-hybrid { 439 background-color: rgba(26, 188, 156, 0.9); 440 color: white; 441} 442 443.event-card-body { 444 flex: 1; 445 display: flex; 446 flex-direction: column; 447} 448 449.event-title { 450 font-size: 1.25rem; 451 font-weight: 700; 452 margin-bottom: 0.75rem; 453 line-height: 1.3; 454} 455 456.event-title a { 457 color: #ecf0f1; 458 text-decoration: none; 459 transition: color 0.3s ease; 460} 461 462.event-title a:hover { 463 color: #3498db; 464} 465 466.event-description { 467 color: #bdc3c7; 468 font-size: 0.9rem; 469 line-height: 1.5; 470 margin-bottom: 1rem; 471 flex: 1; 472} 473 474.event-meta { 475 display: flex; 476 flex-direction: column; 477 gap: 0.5rem; 478 margin-bottom: 1rem; 479} 480 481.event-time, 482.event-location, 483.event-organizer { 484 display: flex; 485 align-items: center; 486 color: #95a5a6; 487 font-size: 0.85rem; 488} 489 490.event-time .icon, 491.event-location .icon, 492.event-organizer .icon { 493 margin-right: 0.5rem; 494 color: #7f8c8d; 495} 496 497.event-card-footer { 498 display: flex; 499 justify-content: space-between; 500 align-items: center; 501 margin-top: auto; 502 padding-top: 1rem; 503 border-top: 1px solid rgba(255, 255, 255, 0.1); 504} 505 506.event-stats { 507 display: flex; 508 gap: 1rem; 509} 510 511.stat-item { 512 display: flex; 513 align-items: center; 514 color: #7f8c8d; 515 font-size: 0.8rem; 516} 517 518.stat-item .icon { 519 margin-right: 0.25rem; 520} 521 522.event-actions { 523 display: flex; 524 gap: 0.5rem; 525} 526 527.event-actions .button { 528 border-radius: 8px; 529 transition: all 0.3s ease; 530} 531 532.event-actions .button:hover { 533 transform: translateY(-2px); 534} 535 536/* Enhanced Empty State */ 537.empty-state { 538 padding: 3rem 1rem; 539} 540 541.empty-state-icon { 542 margin-bottom: 2rem; 543} 544 545.empty-state-actions { 546 margin-top: 2rem; 547 display: flex; 548 gap: 1rem; 549 justify-content: center; 550 flex-wrap: wrap; 551} 552 553/* View Toggle Buttons */ 554.level .button.is-small { 555 border-radius: 8px; 556 transition: all 0.3s ease; 557} 558 559.level .button.is-small.is-active { 560 background-color: #3498db; 561 color: white; 562} 563 564/* Active Filter Tags */ 565.message.is-info { 566 background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(52, 152, 219, 0.05)); 567 border: 1px solid rgba(52, 152, 219, 0.2); 568} 569 570.message-header { 571 background: linear-gradient(135deg, #3498db, #2980b9); 572} 573 574/* Responsive Design */ 575@media screen and (max-width: 768px) { 576 .events-grid { 577 grid-template-columns: 1fr; 578 gap: 1rem; 579 } 580 581 .event-card { 582 min-height: 250px; 583 padding: 1rem; 584 } 585 586 .event-card-header { 587 flex-direction: column; 588 align-items: flex-start; 589 gap: 0.5rem; 590 } 591 592 .event-status-tags { 593 flex-direction: row; 594 align-items: flex-start; 595 } 596 597 .level { 598 flex-direction: column; 599 gap: 1rem; 600 } 601 602 .level-left, 603 .level-right { 604 width: 100%; 605 justify-content: center; 606 } 607 608 .empty-state-actions { 609 flex-direction: column; 610 align-items: center; 611 } 612 613 .empty-state-actions .button { 614 width: 100%; 615 max-width: 300px; 616 } 617} 618 619@media screen and (max-width: 480px) { 620 .event-card-footer { 621 flex-direction: column; 622 gap: 1rem; 623 align-items: stretch; 624 } 625 626 .event-stats { 627 justify-content: center; 628 } 629 630 .event-actions { 631 justify-content: center; 632 } 633} 634</style> 635 636<script> 637// View Toggle Functionality 638function toggleView(viewType) { 639 const container = document.getElementById('events-container'); 640 const gridBtn = document.getElementById('grid-view-btn'); 641 const listBtn = document.getElementById('list-view-btn'); 642 643 if (viewType === 'grid') { 644 container.className = 'events-grid'; 645 gridBtn.classList.add('is-active'); 646 listBtn.classList.remove('is-active'); 647 localStorage.setItem('eventsView', 'grid'); 648 } else { 649 container.className = 'events-list'; 650 listBtn.classList.add('is-active'); 651 gridBtn.classList.remove('is-active'); 652 localStorage.setItem('eventsView', 'list'); 653 } 654} 655 656// RSVP Toggle Functionality 657function toggleRSVP(eventId, button) { 658 const isRSVPed = button.classList.contains('is-success'); 659 660 // Toggle button state 661 if (isRSVPed) { 662 button.classList.remove('is-success'); 663 button.classList.add('is-outlined'); 664 button.querySelector('span:last-child').textContent = '{{ t("event-rsvp") }}'; 665 button.querySelector('i').className = 'fas fa-plus'; 666 } else { 667 button.classList.add('is-success'); 668 button.classList.remove('is-outlined'); 669 button.querySelector('span:last-child').textContent = '{{ t("event-rsvp-confirmed") }}'; 670 button.querySelector('i').className = 'fas fa-check'; 671 } 672 673 // Here you would typically make an API call to update RSVP status 674 // fetch(`/api/events/${eventId}/rsvp`, { method: 'POST' })... 675} 676 677// Initialize view on page load 678document.addEventListener('DOMContentLoaded', function() { 679 const savedView = localStorage.getItem('eventsView') || 'grid'; 680 toggleView(savedView); 681 682 // Add smooth scroll for pagination 683 const paginationLinks = document.querySelectorAll('.pagination-link, .pagination-previous, .pagination-next'); 684 paginationLinks.forEach(link => { 685 link.addEventListener('click', function() { 686 if (!this.disabled) { 687 document.getElementById('results-content').scrollIntoView({ 688 behavior: 'smooth', 689 block: 'start' 690 }); 691 } 692 }); 693 }); 694 695 // Add loading states for HTMX requests 696 document.body.addEventListener('htmx:beforeRequest', function(evt) { 697 const target = evt.detail.target; 698 if (target.id === 'results-content') { 699 target.classList.add('is-loading'); 700 } 701 }); 702 703 document.body.addEventListener('htmx:afterRequest', function(evt) { 704 const target = evt.detail.target; 705 if (target.id === 'results-content') { 706 target.classList.remove('is-loading'); 707 } 708 }); 709}); 710</script>