Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
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>