33---
44
55< style >
6- /* minimal styling — adapt to your site CSS / tailwind as needed */
76 .communities-wrap { max-width : 1100px ; margin : 0 auto; padding : 24px ; padding-top : 90px ; }
87 .controls { display : flex; gap : 10px ; flex-wrap : wrap; margin-bottom : 18px ; }
98 .controls input , .controls select { padding : 8px 10px ; font-size : 14px ; }
109 # speakers-grid { display : grid; grid-template-columns : repeat (auto-fill, minmax (180px , 1fr )); gap : 12px ; }
10+
1111 .speaker-card {
1212 position : relative;
1313 display : flex; flex-direction : column; align-items : center;
1414 text-decoration : none; color : inherit;
1515 border : 1px solid # e6e6e6 ; border-radius : 12px ; padding : 14px ; background : # fff ;
1616 box-shadow : 0 6px 18px rgba (10 , 10 , 10 , 0.03 ); transition : transform .12s ease;
17+ z-index : 1 ; /* keep base stacking for card itself */
1718 }
1819 .speaker-card : hover { transform : translateY (-4px ); }
1920 .speaker-card img { width : 80px ; height : 80px ; border-radius : 50% ; object-fit : cover; margin-bottom : 8px ; }
2324 background : # 0073b1 ; color : # fff ; font-weight : 700 ; font-size : 12px ;
2425 padding : 4px 8px ; border-radius : 999px ;
2526 }
27+
28+ /* Global tooltip (appended to body via JS) */
29+ .global-tooltip {
30+ position : absolute;
31+ pointer-events : none; /* never block mouse events */
32+ white-space : nowrap; /* do not wrap lines */
33+ z-index : 9999999 ; /* very high, will be above everything */
34+ background : # 222 ;
35+ color : # fff ;
36+ padding : 8px 12px ;
37+ border-radius : 6px ;
38+ font-size : 0.8rem ;
39+ box-shadow : 0 6px 20px rgba (0 , 0 , 0 , 0.25 );
40+ transform-origin : center top;
41+ transition : opacity .12s ease;
42+ opacity : 0 ;
43+ display : block; /* kept in DOM; JS sets opacity/visibility */
44+ }
45+ .global-tooltip .visible { opacity : 1 ; }
46+
47+ .global-tooltip strong { display : block; margin-bottom : 6px ; }
48+ .global-tooltip ol { margin : 0 ; padding-left : 18px ; }
49+ .global-tooltip li { margin : 2px 0 ; }
50+
51+ /* small responsive fallback: if viewport too small, allow wrapping */
52+ @media (max-width : 420px ) {
53+ .global-tooltip { white-space : normal; max-width : calc (100vw - 20px ); }
54+ }
2655</ style >
2756
2857< div class ="communities-wrap pt-44 pb-16 px-6 max-w-4xl mx-auto ">
@@ -37,25 +66,18 @@ <h2>{{ page.title | default: "Communities" }}</h2>
3766 </ select >
3867 </ div >
3968
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-
4669 {% assign speakers = "" | split: "|" %}
4770
4871 {% for event in site.events %}
4972 {% if event.speakers %}
5073 {% for sp in event.speakers %}
5174 {% if sp.url contains "linkedin.com" %}
52- {% assign display_url = sp.url %}
75+ {% assign display_url = sp.url %}
5376 {% else %}
54- {% continue %}
77+ {% continue %}
5578 {% endif %}
5679
5780 {% assign key = display_url | remove: "https://" | remove: "http://" | remove: "www." | replace: "/", "" | downcase %}
58-
5981 {% assign updated = false %}
6082 {% assign new_speakers = "" | split: "|" %}
6183
@@ -66,6 +88,8 @@ <h2>{{ page.title | default: "Communities" }}</h2>
6688 {% assign ex_display = parts[1] %}
6789 {% assign ex_name = parts[2] %}
6890 {% assign ex_count = parts[3] %}
91+ {% assign ex_history = parts[4] | default: "" %}
92+
6993 {% if ex_key == key %}
7094 {% assign cur_name = sp.name %}
7195 {% if cur_name.size > ex_name.size %}
@@ -74,7 +98,11 @@ <h2>{{ page.title | default: "Communities" }}</h2>
7498 {% assign chosen_name = ex_name %}
7599 {% endif %}
76100 {% assign new_count = ex_count | plus: 1 %}
77- {% assign updated_item = ex_key | append: "|||" | append: ex_display | append: "|||" | append: chosen_name | append: "|||" | append: new_count %}
101+
102+ {% assign history_entry = event.title | append: " (" | append: event.event_date | append: ") - " | append: sp.topic %}
103+ {% assign new_history = ex_history | append: ";;;" | append: history_entry %}
104+
105+ {% assign updated_item = ex_key | append: "|||" | append: ex_display | append: "|||" | append: chosen_name | append: "|||" | append: new_count | append: "|||" | append: new_history %}
78106 {% assign new_speakers = new_speakers | push: updated_item %}
79107 {% assign updated = true %}
80108 {% else %}
@@ -84,7 +112,8 @@ <h2>{{ page.title | default: "Communities" }}</h2>
84112 {% endfor %}
85113
86114 {% if updated == false %}
87- {% assign new_item = key | append: "|||" | append: display_url | append: "|||" | append: sp.name | append: "|||" | append: "1" %}
115+ {% assign history_entry = event.title | append: " (" | append: event.event_date | append: ") - " | append: sp.topic %}
116+ {% assign new_item = key | append: "|||" | append: display_url | append: "|||" | append: sp.name | append: "|||" | append: "1" | append: "|||" | append: history_entry %}
88117 {% assign new_speakers = new_speakers | push: new_item %}
89118 {% endif %}
90119
@@ -102,14 +131,8 @@ <h2>{{ page.title | default: "Communities" }}</h2>
102131 {% assign display_url = parts[1] %}
103132 {% assign name = parts[2] %}
104133 {% assign count = parts[3] %}
105-
106- {%- comment -%}
107- Image filename resolution order (priority jpeg > jpg > png):
108- 1. slug-first.jpeg / .jpg / .png
109- 2. full-name.jpeg / .jpg / .png
110- 3. first-word.jpeg / .jpg / .png
111- 4. placeholder.png
112- {%- endcomment -%}
134+ {% assign history_raw = parts[4] %}
135+ {% assign history_list = history_raw | split: ";;;" %}
113136
114137 {% assign slug = display_url | split: "/" | last %}
115138 {% assign slug_first = slug | split: "-" | first %}
@@ -128,8 +151,12 @@ <h2>{{ page.title | default: "Communities" }}</h2>
128151 {% assign file_jpg_first = "../assets/lombokdev/speakers/" | append: first_word | append: ".jpg" %}
129152 {% assign file_png_first = "../assets/lombokdev/speakers/" | append: first_word | append: ".png" %}
130153
131- < a class ="speaker-card " href ="{{ display_url }} " target ="_blank " rel ="noopener noreferrer "
132- data-name ="{{ name | escape }} " data-url ="{{ display_url | escape }} " data-count ="{{ count }} ">
154+ < a class ="speaker-card "
155+ href ="{{ display_url }} " target ="_blank " rel ="noopener noreferrer "
156+ data-name ="{{ name | escape }} "
157+ data-url ="{{ display_url | escape }} "
158+ data-count ="{{ count }} "
159+ data-history ="{{ history_raw | escape }} ">
133160 < img src ="{{ file_jpeg_slug }} " alt ="{{ name | escape }} "
134161 onerror ="this.onerror=null;
135162 this.src='{{ file_jpg_slug }}';
@@ -163,8 +190,8 @@ <h2>{{ page.title | default: "Communities" }}</h2>
163190 function filterCards ( ) {
164191 const term = ( searchInput . value || '' ) . trim ( ) . toLowerCase ( ) ;
165192 getCards ( ) . forEach ( card => {
166- const name = card . dataset . name . toLowerCase ( ) ;
167- const url = card . dataset . url . toLowerCase ( ) ;
193+ const name = ( card . dataset . name || '' ) . toLowerCase ( ) ;
194+ const url = ( card . dataset . url || '' ) . toLowerCase ( ) ;
168195 const visible = name . includes ( term ) || url . includes ( term ) ;
169196 card . style . display = visible ? '' : 'none' ;
170197 } ) ;
@@ -190,8 +217,113 @@ <h2>{{ page.title | default: "Communities" }}</h2>
190217
191218 searchInput . addEventListener ( 'input' , filterCards ) ;
192219 sortSelect . addEventListener ( 'change' , e => sortCards ( e . target . value ) ) ;
193-
194- // initial sort
195220 sortCards ( 'az' ) ;
221+
222+ /* ---------------------------
223+ Global tooltip appended to body
224+ --------------------------- */
225+ const tooltip = document . createElement ( 'div' ) ;
226+ tooltip . className = 'global-tooltip' ;
227+ tooltip . setAttribute ( 'role' , 'tooltip' ) ;
228+ tooltip . style . display = 'none' ;
229+ document . body . appendChild ( tooltip ) ;
230+
231+ let activeCard = null ;
232+ let hideTimeout = null ;
233+
234+ function showTooltipFor ( card ) {
235+ const historyRaw = card . getAttribute ( 'data-history' ) || '' ;
236+ const items = historyRaw . split ( ';;;' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
237+ if ( ! items . length ) {
238+ hideTooltip ( ) ;
239+ return ;
240+ }
241+
242+ // build content via DOM nodes (safe against HTML injection)
243+ tooltip . innerHTML = '' ; // clear
244+ const strong = document . createElement ( 'strong' ) ;
245+ strong . textContent = 'Speaking at:' ;
246+ tooltip . appendChild ( strong ) ;
247+
248+ const ol = document . createElement ( 'ol' ) ;
249+ items . forEach ( it => {
250+ const li = document . createElement ( 'li' ) ;
251+ li . textContent = it ;
252+ ol . appendChild ( li ) ;
253+ } ) ;
254+ tooltip . appendChild ( ol ) ;
255+
256+ // ensure it's visible in DOM to measure
257+ tooltip . style . display = 'block' ;
258+ // allow CSS transition: set visible class next tick
259+ window . requestAnimationFrame ( ( ) => tooltip . classList . add ( 'visible' ) ) ;
260+
261+ // position below the card (absolute to the document)
262+ const rect = card . getBoundingClientRect ( ) ;
263+
264+ // measure tooltip width after it's rendered
265+ const ttRect = tooltip . getBoundingClientRect ( ) ;
266+ const ttWidth = ttRect . width ;
267+ const scrollX = window . scrollX || window . pageXOffset ;
268+ const scrollY = window . scrollY || window . pageYOffset ;
269+
270+ // center horizontally on card, but clamp to viewport
271+ let left = scrollX + rect . left + ( rect . width / 2 ) - ( ttWidth / 2 ) ;
272+ const minLeft = scrollX + 8 ;
273+ const maxLeft = scrollX + window . innerWidth - ttWidth - 8 ;
274+ if ( left < minLeft ) left = minLeft ;
275+ if ( left > maxLeft ) left = maxLeft ;
276+
277+ // position just below the card
278+ const top = scrollY + rect . bottom + 8 ;
279+
280+ tooltip . style . left = left + 'px' ;
281+ tooltip . style . top = top + 'px' ;
282+ tooltip . style . pointerEvents = 'none' ;
283+
284+ activeCard = card ;
285+ }
286+
287+ function hideTooltip ( ) {
288+ if ( ! tooltip ) return ;
289+ tooltip . classList . remove ( 'visible' ) ;
290+ // allow transition; hide completely after short delay
291+ clearTimeout ( hideTimeout ) ;
292+ hideTimeout = setTimeout ( ( ) => {
293+ tooltip . style . display = 'none' ;
294+ tooltip . innerHTML = '' ;
295+ } , 120 ) ;
296+ activeCard = null ;
297+ }
298+
299+ // attach pointer events to cards
300+ getCards ( ) . forEach ( card => {
301+ card . addEventListener ( 'pointerenter' , ( ) => {
302+ // cancel any pending hide
303+ clearTimeout ( hideTimeout ) ;
304+ showTooltipFor ( card ) ;
305+ } ) ;
306+ card . addEventListener ( 'pointerleave' , ( ) => {
307+ hideTooltip ( ) ;
308+ } ) ;
309+ // keyboard accessibility: show tooltip on focus
310+ card . addEventListener ( 'focus' , ( ) => {
311+ clearTimeout ( hideTimeout ) ;
312+ showTooltipFor ( card ) ;
313+ } ) ;
314+ card . addEventListener ( 'blur' , ( ) => hideTooltip ( ) ) ;
315+ } ) ;
316+
317+ // hide tooltip on scroll / resize (reposition would be nicer, but simple hide is robust)
318+ let lastScroll = 0 ;
319+ window . addEventListener ( 'scroll' , ( ) => {
320+ // only hide if user scrolled more than a few px to avoid flicker
321+ if ( Math . abs ( window . scrollY - lastScroll ) > 2 ) {
322+ hideTooltip ( ) ;
323+ }
324+ lastScroll = window . scrollY ;
325+ } , { passive : true } ) ;
326+ window . addEventListener ( 'resize' , hideTooltip ) ;
327+
196328 } ) ( ) ;
197329</ script >
0 commit comments