|
| 1 | +--- |
| 2 | +layout: default |
| 3 | +--- |
| 4 | + |
| 5 | +<style> |
| 6 | + /* minimal styling — adapt to your site CSS / tailwind as needed */ |
| 7 | + .communities-wrap { max-width: 1100px; margin: 0 auto; padding: 24px; padding-top: 90px; } |
| 8 | + .controls { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:18px; } |
| 9 | + .controls input, .controls select { padding:8px 10px; font-size:14px; } |
| 10 | + #speakers-grid { display:grid; grid-template-columns: repeat(auto-fill,minmax(180px,1fr)); gap:12px; } |
| 11 | + .speaker-card { |
| 12 | + position:relative; |
| 13 | + display:flex; flex-direction:column; align-items:center; |
| 14 | + text-decoration:none; color:inherit; |
| 15 | + border:1px solid #e6e6e6; border-radius:12px; padding:14px; background:#fff; |
| 16 | + box-shadow:0 6px 18px rgba(10,10,10,0.03); transition:transform .12s ease; |
| 17 | + } |
| 18 | + .speaker-card:hover { transform:translateY(-4px); } |
| 19 | + .speaker-card img { width:80px; height:80px; border-radius:50%; object-fit:cover; margin-bottom:8px; } |
| 20 | + .speaker-name { font-weight:600; text-align:center; font-size:0.95rem; line-height:1.2; } |
| 21 | + .speaker-badge { |
| 22 | + position:absolute; top:10px; right:10px; |
| 23 | + background:#0073b1; color:#fff; font-weight:700; font-size:12px; |
| 24 | + padding:4px 8px; border-radius:999px; |
| 25 | + } |
| 26 | +</style> |
| 27 | + |
| 28 | +<div class="communities-wrap pt-44 pb-16 px-6 max-w-4xl mx-auto"> |
| 29 | + <h1>{{ page.title | default: "Communities" }}</h1> |
| 30 | + |
| 31 | + <div class="controls"> |
| 32 | + <input id="search" type="search" placeholder="Search speakers (name / topic / linkedin)..." /> |
| 33 | + <select id="sort"> |
| 34 | + <option value="az">Sort A → Z</option> |
| 35 | + <option value="za">Sort Z → A</option> |
| 36 | + <option value="most">Most Appearances</option> |
| 37 | + </select> |
| 38 | + </div> |
| 39 | + |
| 40 | + {%- comment -%} |
| 41 | + Build an array `speakers` where each item is: |
| 42 | + key|||display_url|||name|||count |
| 43 | + key is normalized url used for dedupe |
| 44 | + {%- endcomment -%} |
| 45 | + |
| 46 | + {% assign speakers = "" | split: "|" %} |
| 47 | + |
| 48 | + {% for event in site.events %} |
| 49 | + {% if event.speakers %} |
| 50 | + {% for sp in event.speakers %} |
| 51 | + {% comment %} original display url (may include https) or fallback to name {% endcomment %} |
| 52 | + {% assign display_url = sp.url | default: sp.name %} |
| 53 | + {% comment %} create a normalized key for dedupe: |
| 54 | + strip scheme, www, remove slashes, lower-case {% endcomment %} |
| 55 | + {% assign key = display_url | remove: "https://" | remove: "http://" | remove: "www." | replace: "/", "" | downcase %} |
| 56 | + |
| 57 | + {% assign updated = false %} |
| 58 | + {% assign new_speakers = "" | split: "|" %} |
| 59 | + |
| 60 | + {% for item in speakers %} |
| 61 | + {% if item != "" %} |
| 62 | + {% assign parts = item | split: "|||" %} |
| 63 | + {% assign ex_key = parts[0] %} |
| 64 | + {% assign ex_display = parts[1] %} |
| 65 | + {% assign ex_name = parts[2] %} |
| 66 | + {% assign ex_count = parts[3] %} |
| 67 | + {% if ex_key == key %} |
| 68 | + {% comment %} choose longest name and increment count {% endcomment %} |
| 69 | + {% assign cur_name = sp.name %} |
| 70 | + {% if cur_name.size > ex_name.size %} |
| 71 | + {% assign chosen_name = cur_name %} |
| 72 | + {% else %} |
| 73 | + {% assign chosen_name = ex_name %} |
| 74 | + {% endif %} |
| 75 | + {% assign new_count = ex_count | plus: 1 %} |
| 76 | + {% assign updated_item = ex_key | append: "|||" | append: ex_display | append: "|||" | append: chosen_name | append: "|||" | append: new_count %} |
| 77 | + {% assign new_speakers = new_speakers | push: updated_item %} |
| 78 | + {% assign updated = true %} |
| 79 | + {% else %} |
| 80 | + {% assign new_speakers = new_speakers | push: item %} |
| 81 | + {% endif %} |
| 82 | + {% endif %} |
| 83 | + {% endfor %} |
| 84 | + |
| 85 | + {% if updated == false %} |
| 86 | + {% assign new_item = key | append: "|||" | append: display_url | append: "|||" | append: sp.name | append: "|||" | append: "1" %} |
| 87 | + {% assign new_speakers = new_speakers | push: new_item %} |
| 88 | + {% endif %} |
| 89 | + |
| 90 | + {% assign speakers = new_speakers %} |
| 91 | + {% endfor %} |
| 92 | + {% endif %} |
| 93 | + {% endfor %} |
| 94 | + |
| 95 | + <!-- Render the grid (render order is the insertion order; client JS will sort) --> |
| 96 | + <div id="speakers-grid"> |
| 97 | + {% for item in speakers %} |
| 98 | + {% if item != "" %} |
| 99 | + {% assign parts = item | split: "|||" %} |
| 100 | + {% assign key = parts[0] %} |
| 101 | + {% assign display_url = parts[1] %} |
| 102 | + {% assign name = parts[2] %} |
| 103 | + {% assign count = parts[3] %} |
| 104 | + <!-- anchor to open linkedin in new tab; data attributes used for JS sorting/filtering --> |
| 105 | + <a class="speaker-card" href="{{ display_url }}" target="_blank" rel="noopener noreferrer" |
| 106 | + data-name="{{ name | escape }}" data-url="{{ display_url | escape }}" data-count="{{ count }}"> |
| 107 | + <!-- avatar via unavatar.io; pass URL-encoded display_url --> |
| 108 | + <img src="https://unavatar.io/{{ display_url | url_encode }}" alt="{{ name | escape }}" |
| 109 | + onerror="this.onerror=null;this.src='/assets/img/avatar-placeholder.png'"> |
| 110 | + <div class="speaker-name">{{ name }}</div> |
| 111 | + <div class="speaker-badge">{{ count }}x</div> |
| 112 | + </a> |
| 113 | + {% endif %} |
| 114 | + {% endfor %} |
| 115 | + </div> |
| 116 | +</div> |
| 117 | + |
| 118 | +<script> |
| 119 | + (function () { |
| 120 | + const searchInput = document.getElementById('search'); |
| 121 | + const sortSelect = document.getElementById('sort'); |
| 122 | + const grid = document.getElementById('speakers-grid'); |
| 123 | + |
| 124 | + function getCards() { |
| 125 | + return Array.from(grid.querySelectorAll('.speaker-card')); |
| 126 | + } |
| 127 | + |
| 128 | + function filterCards() { |
| 129 | + const term = (searchInput.value || '').trim().toLowerCase(); |
| 130 | + getCards().forEach(card => { |
| 131 | + // searchable text: name + url |
| 132 | + const name = card.dataset.name.toLowerCase(); |
| 133 | + const url = card.dataset.url.toLowerCase(); |
| 134 | + const visible = name.includes(term) || url.includes(term); |
| 135 | + card.style.display = visible ? '' : 'none'; |
| 136 | + }); |
| 137 | + } |
| 138 | + |
| 139 | + function sortCards(order) { |
| 140 | + const cards = getCards().slice(); // copy |
| 141 | + cards.sort((a,b) => { |
| 142 | + const nameA = a.dataset.name.toLowerCase(); |
| 143 | + const nameB = b.dataset.name.toLowerCase(); |
| 144 | + if (order === 'az') return nameA.localeCompare(nameB); |
| 145 | + if (order === 'za') return nameB.localeCompare(nameA); |
| 146 | + if (order === 'most') { |
| 147 | + const ca = parseInt(a.dataset.count || '0', 10); |
| 148 | + const cb = parseInt(b.dataset.count || '0', 10); |
| 149 | + // if same count, tie-break by name A→Z |
| 150 | + if (cb === ca) return nameA.localeCompare(nameB); |
| 151 | + return cb - ca; |
| 152 | + } |
| 153 | + return 0; |
| 154 | + }); |
| 155 | + // append in sorted order (preserves only visible display settings) |
| 156 | + cards.forEach(c => grid.appendChild(c)); |
| 157 | + } |
| 158 | + |
| 159 | + searchInput.addEventListener('input', filterCards); |
| 160 | + sortSelect.addEventListener('change', e => sortCards(e.target.value)); |
| 161 | + |
| 162 | + // initial sort |
| 163 | + sortCards('az'); |
| 164 | + })(); |
| 165 | +</script> |
0 commit comments