PageSourceSearch

https://hssrsheep-5jtg5a4u.manus.space/directory-widget.js

js hssrsheep-5jtg5a4u.manus.space collected 2026-10-03 05:02:59 UTC 24,117 bytes, 487 lines download raw bytes

1
2(function() {
3  'use strict';
4
5  var ORIGIN = 'https://harlequinsheepsociety.net';
6  var US_TOPO_URL = 'https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json';
7  var CA_TOPO_URL = 'https://cdn.jsdelivr.net/npm/[email protected]/canada.json';
8  var CURRENT_YEAR = String(new Date().getFullYear());
9
10  var US_FIPS_TO_ABBR = {
11    "01":"AL","02":"AK","04":"AZ","05":"AR","06":"CA","08":"CO","09":"CT",
12    "10":"DE","11":"DC","12":"FL","13":"GA","15":"HI","16":"ID","17":"IL",
13    "18":"IN","19":"IA","20":"KS","21":"KY","22":"LA","23":"ME","24":"MD",
14    "25":"MA","26":"MI","27":"MN","28":"MS","29":"MO","30":"MT","31":"NE",
15    "32":"NV","33":"NH","34":"NJ","35":"NM","36":"NY","37":"NC","38":"ND",
16    "39":"OH","40":"OK","41":"OR","42":"PA","44":"RI","45":"SC","46":"SD",
17    "47":"TN","48":"TX","49":"UT","50":"VT","51":"VA","53":"WA","54":"WV",
18    "55":"WI","56":"WY"
19  };
20
21  // Canada province abbreviations (ISO 3166-2:CA codes)
22  var CA_PROVINCES = ["AB","BC","MB","NB","NL","NS","NT","NU","ON","PE","QC","SK","YT"];
23
24  var SAGE = '#6b7c5c';
25  var SAGE_DARK = '#4a5940';
26  var CREAM = '#f5f2eb';
27  var BORDER = '#d4dbc8';
28  var TEXT = '#2c3a1e';
29  var TEXT_MID = '#4a5940';
30  var TEXT_LIGHT = '#6b7c5c';
31  var GOLD = '#b8860b';
32  var WHITE = '#ffffff';
33  var RAINBOW = 'linear-gradient(90deg,#e63946,#f4a261,#e9c46a,#2a9d8f,#457b9d,#6a4c93)';
34
35  var CSS = [
36    '#hssr-directory * { box-sizing: border-box; }',
37    '#hssr-directory { font-family: Georgia,"Times New Roman",serif; color:' + TEXT + '; background:' + CREAM + '; border-radius:12px; padding:0 0 24px 0; max-width:100%; }',
38    '.hssr-rainbow-bar { height:4px; background:' + RAINBOW + '; border-radius:12px 12px 0 0; }',
39    '.hssr-header { background:' + SAGE + '; border-radius:0; padding:20px 24px 16px; text-align:center; }',
40    '.hssr-header h2 { color:' + WHITE + '; font-size:1.5rem; margin:0 0 4px 0; font-weight:700; }',
41    '.hssr-header p { color:rgba(255,255,255,0.85); font-size:0.9rem; margin:0; }',
42    '.hssr-map-wrap { padding:12px 16px 4px; }',
43    '.hssr-map-wrap svg { width:100%; height:auto; display:block; cursor:default; }',
44    '.hssr-map-wrap svg path { transition:fill 0.15s; }',
45    '.hssr-map-tabs { display:flex; justify-content:center; gap:8px; padding:0 16px 8px; }',
46    '.hssr-map-tab { font-size:0.8rem; padding:4px 14px; border-radius:999px; border:1.5px solid ' + BORDER + '; background:' + WHITE + '; color:' + TEXT_MID + '; cursor:pointer; font-family:inherit; }',
47    '.hssr-map-tab.active { background:' + SAGE + '; color:' + WHITE + '; border-color:' + SAGE + '; }',
48    '.hssr-map-legend { display:flex; align-items:center; gap:8px; justify-content:center; padding:0 16px 8px; font-size:0.75rem; color:' + TEXT_LIGHT + '; flex-wrap:wrap; }',
49    '.hssr-legend-swatch { display:inline-block; width:12px; height:12px; border-radius:2px; }',
50    '.hssr-new-dot { display:inline-block; width:8px; height:8px; border-radius:50%; background:#16a34a; margin-right:4px; animation:hssr-pulse 1.5s infinite; }',
51    '@keyframes hssr-pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(1.4)} }',
52    '.hssr-search-wrap { padding:8px 24px 4px; }',
53    '.hssr-search { width:100%; max-width:420px; display:block; margin:0 auto; padding:9px 14px; border:1.5px solid ' + BORDER + '; border-radius:8px; font-size:0.9rem; background:' + WHITE + '; color:' + TEXT + '; outline:none; font-family:inherit; }',
54    '.hssr-search:focus { border-color:' + SAGE + '; }',
55    '.hssr-stats { text-align:center; font-size:0.8rem; color:' + TEXT_LIGHT + '; padding:4px 24px 10px; }',
56    '.hssr-state-section { padding:0 16px 8px; }',
57    '.hssr-state-header { display:flex; align-items:center; gap:10px; padding:8px 8px 4px; }',
58    '.hssr-state-name { font-size:1rem; font-weight:700; color:' + SAGE_DARK + '; flex:1; }',
59    '.hssr-state-count { background:' + SAGE + '; color:' + WHITE + '; border-radius:999px; font-size:0.72rem; font-weight:600; padding:1px 8px; }',
60    '.hssr-divider { height:2px; background:' + RAINBOW + '; border-radius:2px; margin:0 8px 8px; opacity:0.5; }',
61    '.hssr-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:10px; padding:0 8px; }',
62    '.hssr-card { background:' + WHITE + '; border:1.5px solid ' + BORDER + '; border-radius:10px; padding:12px 14px; transition:border-color 0.15s; }',
63    '.hssr-card:hover { border-color:' + SAGE + '; }',
64    '.hssr-card-farm { font-weight:700; font-size:0.95rem; color:' + SAGE_DARK + '; margin-bottom:2px; }',
65    '.hssr-card-name { font-size:0.82rem; color:' + TEXT_MID + '; margin-bottom:4px; }',
66    '.hssr-card-location { font-size:0.78rem; color:' + TEXT_LIGHT + '; margin-bottom:5px; }',
67    '.hssr-card-links { display:flex; flex-wrap:wrap; gap:5px; }',
68    '.hssr-card-link { font-size:0.75rem; color:' + SAGE + '; text-decoration:none; border:1px solid ' + BORDER + '; border-radius:4px; padding:2px 7px; }',
69    '.hssr-card-link:hover { background:' + CREAM + '; }',
70    '.hssr-lifetime-badge { display:inline-block; background:' + GOLD + '; color:' + WHITE + '; font-size:0.68rem; font-weight:700; border-radius:4px; padding:1px 6px; margin-bottom:3px; letter-spacing:0.03em; }',
71    '.hssr-new-badge { display:inline-block; background:#16a34a; color:' + WHITE + '; font-size:0.65rem; font-weight:700; border-radius:4px; padding:1px 6px; margin-bottom:3px; margin-left:4px; letter-spacing:0.03em; }',
72    '.hssr-footer { text-align:center; padding:14px 24px 4px; font-size:0.78rem; color:' + TEXT_LIGHT + '; }',
73    '.hssr-footer a { color:' + SAGE + '; text-decoration:none; font-weight:600; }',
74    '.hssr-footer a:hover { text-decoration:underline; }',
75    '.hssr-loading { text-align:center; padding:36px; color:' + TEXT_LIGHT + '; font-size:0.9rem; }',
76    '.hssr-error { text-align:center; padding:20px; color:#c0392b; font-size:0.88rem; }',
77    '@media (max-width:520px) { .hssr-cards { grid-template-columns:1fr; } .hssr-header h2 { font-size:1.2rem; } }'
78  ].join('\n');
79
80  // ── Helpers ──────────────────────────────────────────────────────────────────
81
82  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
83
84  function isLifetime(m) {
85    var t = (m.memberType || '').toLowerCase();
86    return t.indexOf('lifetime') !== -1;
87  }
88
89  function isNewMember(m) {
90    return m.membershipYear && String(m.membershipYear) === CURRENT_YEAR;
91  }
92
93  function formatName(m) {
94    var parts = [];
95    if (m.firstName || m.lastName) parts.push(((m.firstName||'') + ' ' + (m.lastName||'')).trim());
96    if (m.member2First || m.member2Last) parts.push(((m.member2First||'') + ' ' + (m.member2Last||'')).trim());
97    return parts.join(' & ');
98  }
99
100  function formatLocation(m) {
101    var parts = [];
102    if (m.city) parts.push(m.city);
103    if (m.state) parts.push(m.state);
104    if (m.country && m.country.toUpperCase() !== 'US' && m.country.toUpperCase() !== 'USA') parts.push(m.country);
105    return parts.join(', ');
106  }
107
108  function isCanadian(m) {
109    var c = (m.country || '').toUpperCase();
110    return c === 'CA' || c === 'CAN' || c === 'CANADA' || CA_PROVINCES.indexOf(m.state || '') !== -1;
111  }
112
113  function regionLabel(m) {
114    return m.state || (isCanadian(m) ? 'Canada' : 'Unknown');
115  }
116
117  function renderCard(m) {
118    var farm = m.farmName || formatName(m) || 'HSSR Member';
119    var name = formatName(m);
120    var loc = formatLocation(m);
121    var lifetime = isLifetime(m);
122    var newMem = isNewMember(m);
123    var links = '';
124    if (m.websiteUrl) {
125      var url = m.websiteUrl.startsWith('http') ? m.websiteUrl : 'https://' + m.websiteUrl;
126      links += '<a class="hssr-card-link" href="' + url + '" target="_blank" rel="noopener">Website</a>';
127    }
128    if (m.email && (m.listingPreference||'').toLowerCase().indexOf('email') !== -1) {
129      links += '<a class="hssr-card-link" href="mailto:' + esc(m.email) + '">Email</a>';
130    }
131    return '<div class="hssr-card">' +
132      '<div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-bottom:2px;">' +
133      (lifetime ? '<span class="hssr-lifetime-badge">&#9733; Lifetime Member</span>' : '') +
134      (newMem ? '<span class="hssr-new-badge">&#10022; New ' + CURRENT_YEAR + '</span>' : '') +
135      '</div>' +
136      '<div class="hssr-card-farm">' + esc(farm) + '</div>' +
137      (name ? '<div class="hssr-card-name">' + esc(name) + '</div>' : '') +
138      (loc ? '<div class="hssr-card-location">&#128205; ' + esc(loc) + '</div>' : '') +
139      (links ? '<div class="hssr-card-links">' + links + '</div>' : '') +
140      '</div>';
141  }
142
143  function groupByState(members) {
144    var groups = {};
145    members.forEach(function(m) {
146      var r = regionLabel(m);
147      if (!groups[r]) groups[r] = [];
148      groups[r].push(m);
149    });
150    Object.keys(groups).forEach(function(r) {
151      groups[r].sort(function(a, b) {
152        var aL = isLifetime(a) ? 0 : 1, bL = isLifetime(b) ? 0 : 1;
153        if (aL !== bL) return aL - bL;
154        var aF = (a.farmName || a.lastName || '').toLowerCase();
155        var bF = (b.farmName || b.lastName || '').toLowerCase();
156        return aF < bF ? -1 : aF > bF ? 1 : 0;
157      });
158    });
159    return groups;
160  }
161
162  function renderDirectory(members, query, filterState) {
163    var filtered = members;
164    if (filterState) {
165      filtered = filtered.filter(function(m) { return (m.state || '') === filterState; });
166    }
167    if (query) {
168      var q = query.toLowerCase();
169      filtered = filtered.filter(function(m) {
170        return (m.farmName||'').toLowerCase().indexOf(q) !== -1 ||
171          (m.firstName||'').toLowerCase().indexOf(q) !== -1 ||
172          (m.lastName||'').toLowerCase().indexOf(q) !== -1 ||
173          (m.state||'').toLowerCase().indexOf(q) !== -1 ||
174          (m.city||'').toLowerCase().indexOf(q) !== -1;
175      });
176    }
177    var groups = groupByState(filtered);
178    var states = Object.keys(groups).sort();
179    var usStates = states.filter(function(s) { return s.length === 2 && s === s.toUpperCase() && CA_PROVINCES.indexOf(s) === -1; });
180    var caProvinces = states.filter(function(s) { return CA_PROVINCES.indexOf(s) !== -1; });
181    var others = states.filter(function(s) { return usStates.indexOf(s) === -1 && caProvinces.indexOf(s) === -1; });
182    var sorted = usStates.sort().concat(caProvinces.sort()).concat(others.sort());
183
184    var newCount = filtered.filter(isNewMember).length;
185    var html = '<div class="hssr-stats">' + filtered.length + ' breeder' + (filtered.length !== 1 ? 's' : '') +
186      (newCount > 0 ? ' &nbsp;<span class="hssr-new-dot"></span>' + newCount + ' new in ' + CURRENT_YEAR : '') +
187      (filterState ? ' in ' + esc(filterState) + ' <button id="hssr-clear-filter" style="background:none;border:none;color:' + SAGE + ';cursor:pointer;font-size:0.78rem;text-decoration:underline;padding:0;font-family:inherit;">clear filter</button>' : '') +
188      '</div>';
189    sorted.forEach(function(region) {
190      var rm = groups[region];
191      var hasNew = rm.some(isNewMember);
192      html += '<div class="hssr-state-section">' +
193        '<div class="hssr-state-header">' +
194        (hasNew ? '<span class="hssr-new-dot" title="New member(s) in ' + CURRENT_YEAR + '"></span>' : '') +
195        '<span class="hssr-state-name">' + esc(region) + '</span>' +
196        '<span class="hssr-state-count">' + rm.length + '</span></div>' +
197        '<div class="hssr-divider"></div><div class="hssr-cards">';
198      rm.forEach(function(m) { html += renderCard(m); });
199      html += '</div></div>';
200    });
201    if (sorted.length === 0) {
202      html += '<div class="hssr-loading">No breeders found' + (filterState ? ' in ' + esc(filterState) : '') + '.</div>';
203    }
204    return html;
205  }
206
207  // ── Map rendering with d3-geo + TopoJSON ────────────────────────────────────
208
209  function loadScript(src, cb) {
210    var s = document.createElement('script');
211    s.src = src;
212    s.onload = cb;
213    s.onerror = function() { cb(new Error('Failed to load ' + src)); };
214    document.head.appendChild(s);
215  }
216
217  function fillColor(count, max) {
218    if (count === 0) return '#e5e7eb';
219    var t = Math.min(count / Math.max(max, 1), 1);
220    var r = Math.round(187 + (21 - 187) * t);
221    var g = Math.round(247 + (128 - 247) * t);
222    var b = Math.round(208 + (61 - 208) * t);
223    return 'rgb(' + r + ',' + g + ',' + b + ')';
224  }
225
226  function renderMap(svgEl, topoData, counts, newRegions, onRegionClick, viewBox, objectKey, idToAbbr) {
227    var d3geo = window.d3 || {};
228    var topojson = window.topojson;
229    if (!d3geo.geoPath || !topojson) return;
230
231    var vb = viewBox || '0 0 960 600';
232    svgEl.setAttribute('viewBox', vb);
233
234    var w = parseInt(vb.split(' ')[2]) || 960;
235    var h = parseInt(vb.split(' ')[3]) || 600;
236
237    var projection = d3geo.geoAlbersUsa
238      ? d3geo.geoAlbersUsa().scale(900).translate([480, 300])
239      : d3geo.geoMercator().fitSize([w, h], topojson.feature(topoData, topoData.objects[objectKey]));
240
241    var pathGen = d3geo.geoPath().projection(projection);
242    var obj = topoData.objects[objectKey];
243    if (!obj) return;
244    var collection = topojson.feature(topoData, obj);
245    var features = collection.features;
246    var max = Math.max.apply(null, Object.values(counts).concat([1]));
247
248    var svgNS = 'http://www.w3.org/2000/svg';
249
250    features.forEach(function(f) {
251      var d = pathGen(f);
252      if (!d) return;
253      var abbr = idToAbbr(f) || '';
254      var count = counts[abbr] || 0;
255      var hasNew = newRegions[abbr] || false;
256
257      var path = document.createElementNS(svgNS, 'path');
258      path.setAttribute('d', d);
259      path.setAttribute('fill', fillColor(count, max));
260      path.setAttribute('stroke', '#9ca3af');
261      path.setAttribute('stroke-width', '0.6');
262      if (count > 0) {
263        path.style.cursor = 'pointer';
264        path.addEventListener('mouseenter', function() {
265          path.setAttribute('fill', '#f97316');
266          path.setAttribute('stroke', '#ea580c');
267          path.setAttribute('stroke-width', '1.5');
268        });
269        path.addEventListener('mouseleave', function() {
270          path.setAttribute('fill', fillColor(count, max));
271          path.setAttribute('stroke', '#9ca3af');
272          path.setAttribute('stroke-width', '0.6');
273        });
274        path.addEventListener('click', function() { onRegionClick(abbr); });
275        var title = document.createElementNS(svgNS, 'title');
276        title.textContent = abbr + ': ' + count + ' breeder' + (count !== 1 ? 's' : '') + (hasNew ? ' (' + CURRENT_YEAR + ' new member)' : '') + ' — click to filter';
277        path.appendChild(title);
278      }
279      svgEl.appendChild(path);
280
281      // Pulsing dot for new members
282      if (hasNew && count > 0) {
283        var centroid = pathGen.centroid(f);
284        if (centroid && isFinite(centroid[0]) && isFinite(centroid[1])) {
285          var circle = document.createElementNS(svgNS, 'circle');
286          circle.setAttribute('cx', centroid[0]);
287          circle.setAttribute('cy', centroid[1]);
288          circle.setAttribute('r', '5');
289          circle.setAttribute('fill', '#16a34a');
290          circle.setAttribute('opacity', '0.85');
291          circle.setAttribute('pointer-events', 'none');
292          // CSS animation via style attribute
293          circle.style.animation = 'hssr-pulse 1.5s infinite';
294          svgEl.appendChild(circle);
295        }
296      }
297
298      // State abbreviation label
299      var centroid2 = pathGen.centroid(f);
300      if (abbr && centroid2 && isFinite(centroid2[0]) && isFinite(centroid2[1])) {
301        var text = document.createElementNS(svgNS, 'text');
302        text.setAttribute('x', centroid2[0]);
303        text.setAttribute('y', centroid2[1]);
304        text.setAttribute('text-anchor', 'middle');
305        text.setAttribute('dominant-baseline', 'central');
306        text.setAttribute('font-size', '9');
307        text.setAttribute('font-weight', '600');
308        text.setAttribute('fill', count > 0 ? '#166534' : '#9ca3af');
309        text.setAttribute('pointer-events', 'none');
310        text.textContent = abbr;
311        svgEl.appendChild(text);
312      }
313    });
314  }
315
316  // ── Main init ────────────────────────────────────────────────────────────────
317
318  function init() {
319    var container = document.getElementById('hssr-directory');
320    if (!container) return;
321
322    // Inject styles
323    var style = document.createElement('style');
324    style.textContent = CSS;
325    document.head.appendChild(style);
326
327    container.innerHTML = '<div class="hssr-rainbow-bar"></div>' +
328      '<div class="hssr-header"><h2>Find a Harlequin Sheep Breeder</h2>' +
329      '<p>Active HSSR members across the United States and Canada</p></div>' +
330      '<div class="hssr-loading">Loading member directory&hellip;</div>';
331
332    var apiUrl = ORIGIN + '/api/trpc/members.directory?batch=1&input=%7B%220%22%3A%7B%22json%22%3Anull%7D%7D';
333
334    var membersData = null, usTopoData = null, caTopoData = null;
335    var query = '', filterState = '', activeTab = 'us'; // 'us' | 'ca'
336
337    function buildCounts(tab) {
338      var counts = {}, newRegions = {};
339      if (!membersData) return { counts: counts, newRegions: newRegions };
340      membersData.forEach(function(m) {
341        var s = m.state || '';
342        if (!s) return;
343        var isCa = isCanadian(m);
344        if (tab === 'us' && isCa) return;
345        if (tab === 'ca' && !isCa) return;
346        counts[s] = (counts[s] || 0) + 1;
347        if (isNewMember(m)) newRegions[s] = true;
348      });
349      return { counts: counts, newRegions: newRegions };
350    }
351
352    function tryRender() {
353      if (!membersData) return;
354      renderFull();
355    }
356
357    function renderFull() {
358      var members = membersData;
359      var usMembers = members.filter(function(m) { return !isCanadian(m); });
360      var caMembers = members.filter(isCanadian);
361      var hasCanada = caMembers.length > 0;
362
363      var tabsHtml = '';
364      if (hasCanada) {
365        tabsHtml = '<div class="hssr-map-tabs">' +
366          '<button class="hssr-map-tab' + (activeTab === 'us' ? ' active' : '') + '" id="hssr-tab-us">United States</button>' +
367          '<button class="hssr-map-tab' + (activeTab === 'ca' ? ' active' : '') + '" id="hssr-tab-ca">Canada</button>' +
368          '</div>';
369      }
370
371      var mapHtml = '<div class="hssr-map-wrap"><svg id="hssr-map-svg" style="max-height:320px"></svg></div>' +
372        tabsHtml +
373        '<div class="hssr-map-legend">' +
374        '<span class="hssr-legend-swatch" style="background:#e5e7eb"></span> No breeders &nbsp;&nbsp;' +
375        '<span class="hssr-legend-swatch" style="background:rgb(187,247,208)"></span> 1–2 &nbsp;&nbsp;' +
376        '<span class="hssr-legend-swatch" style="background:rgb(104,188,135)"></span> 3–5 &nbsp;&nbsp;' +
377        '<span class="hssr-legend-swatch" style="background:rgb(21,128,61)"></span> 6+ &nbsp;&nbsp;' +
378        '<span class="hssr-new-dot"></span> New in ' + CURRENT_YEAR + ' &nbsp;&nbsp;' +
379        '<span style="font-size:0.72rem">Click a state to filter</span></div>';
380
381      // Filter directory to current tab
382      var tabMembers = activeTab === 'ca' ? caMembers : usMembers;
383
384      container.innerHTML = '<div class="hssr-rainbow-bar"></div>' +
385        '<div class="hssr-header"><h2>Find a Harlequin Sheep Breeder</h2>' +
386        '<p>Active HSSR members across the United States and Canada</p></div>' +
387        mapHtml +
388        '<div class="hssr-search-wrap"><input id="hssr-search-input" class="hssr-search" type="text" placeholder="Search by farm, name, or state&hellip;" value="' + esc(query) + '"></div>' +
389        '<div id="hssr-dir-body"></div>' +
390        '<div class="hssr-footer"><a href="' + ORIGIN + '/directory" target="_blank">View full directory on HSSR website &rarr;</a></div>';
391
392      // Tab click handlers
393      var tabUs = document.getElementById('hssr-tab-us');
394      var tabCa = document.getElementById('hssr-tab-ca');
395      if (tabUs) tabUs.addEventListener('click', function() { activeTab = 'us'; filterState = ''; renderFull(); });
396      if (tabCa) tabCa.addEventListener('click', function() { activeTab = 'ca'; filterState = ''; renderFull(); });
397
398      // Render map
399      var svgEl = document.getElementById('hssr-map-svg');
400      var built = buildCounts(activeTab);
401      if (svgEl && window.topojson && window.d3) {
402        if (activeTab === 'us' && usTopoData) {
403          renderMap(svgEl, usTopoData, built.counts, built.newRegions, function(abbr) {
404            filterState = (filterState === abbr) ? '' : abbr;
405            updateDir();
406          }, '0 0 960 600', 'states', function(f) {
407            var fips = String(f.id || '').padStart(2, '0');
408            return US_FIPS_TO_ABBR[fips] || '';
409          });
410        } else if (activeTab === 'ca' && caTopoData) {
411          // Try common TopoJSON object key names for Canada
412          var caKey = caTopoData.objects ? Object.keys(caTopoData.objects)[0] : null;
413          if (caKey) {
414            renderMap(svgEl, caTopoData, built.counts, built.newRegions, function(abbr) {
415              filterState = (filterState === abbr) ? '' : abbr;
416              updateDir();
417            }, '0 0 1000 800', caKey, function(f) {
418              // Try common property names for province abbreviation
419              var props = f.properties || {};
420              return props.PRUID && CA_PROVINCES[parseInt(props.PRUID) - 10]
421                ? CA_PROVINCES[parseInt(props.PRUID) - 10]
422                : props.PREABBR || props.abbr || props.code || props.PROV || '';
423            });
424          }
425        }
426      }
427
428      // Render directory body
429      function updateDir() {
430        var body = document.getElementById('hssr-dir-body');
431        if (body) body.innerHTML = renderDirectory(tabMembers, query, filterState);
432        var clearBtn = document.getElementById('hssr-clear-filter');
433        if (clearBtn) clearBtn.addEventListener('click', function() { filterState = ''; updateDir(); });
434      }
435      updateDir();
436
437      // Search input
438      var input = document.getElementById('hssr-search-input');
439      if (input) {
440        input.addEventListener('input', function() { query = this.value; updateDir(); });
441      }
442    }
443
444    // Fetch member data
445    fetch(apiUrl)
446      .then(function(r) { return r.json(); })
447      .then(function(data) {
448        membersData = (data[0] && data[0].result && data[0].result.data && data[0].result.data.json) || [];
449        tryRender();
450      })
451      .catch(function() {
452        container.innerHTML = '<div class="hssr-rainbow-bar"></div>' +
453          '<div class="hssr-header"><h2>HSSR Member Directory</h2></div>' +
454          '<div class="hssr-error">Unable to load the member directory. Please visit <a href="' + ORIGIN + '/directory">harlequinsheepsociety.net/directory</a> directly.</div>';
455      });
456
457    // Load d3-geo and topojson from CDN, then fetch both TopoJSON files
458    loadScript('https://cdn.jsdelivr.net/npm/d3-geo@3/dist/d3-geo.umd.min.js', function(err) {
459      if (err) return;
460      loadScript('https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js', function(err2) {
461        if (err2) return;
462        // Fetch US topo
463        fetch(US_TOPO_URL)
464          .then(function(r) { return r.json(); })
465          .then(function(topo) {
466            usTopoData = topo;
467            if (membersData) renderFull();
468          })
469          .catch(function() { /* map won't show */ });
470        // Fetch Canada topo
471        fetch(CA_TOPO_URL)
472          .then(function(r) { return r.json(); })
473          .then(function(topo) {
474            caTopoData = topo;
475            if (membersData && activeTab === 'ca') renderFull();
476          })
477          .catch(function() { /* Canada map won't show, list still works */ });
478      });
479    });
480  }
481
482  if (document.readyState === 'loading') {
483    document.addEventListener('DOMContentLoaded', init);
484  } else {
485    init();
486  }
487})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.