1var searchInput = document.getElementById('institution-search'); 2var institutionList = document.getElementById('institution-list'); 3var noResults = document.getElementById('no-results'); 4var resultsCount = document.getElementById('results-count'); 5var servicesSection = document.getElementById('services-section'); 6var serviceDetailSection = document.getElementById('service-detail'); 7var nationalResourcesSection = document.getElementById('national-resources'); 8var filtersSidebar = document.getElementById('filters-sidebar'); 9var mobileFilterBtn = document.getElementById('mobile-filter-btn'); 10var clearFiltersBtn = document.getElementById('clear-filters'); 11var verifiedNote = document.getElementById('verified-note'); 12 13var selectedInstitution = null; 14var currentSearch = ''; 15 16var typeLabels = { 17 research: 'Research', 18 applied: 'Applied Sciences', 19 arts: 'Arts & Music', 20 specialized: 'Specialized' 21}; 22 23document.addEventListener('DOMContentLoaded', function() { 24 updateTypeCounts(); 25 renderInstitutionList(); 26 renderNationalResources(); 27 renderVerifiedNote(); 28 setupFilters(); 29 setupSearch(); 30 setupMobileFilter(); 31 setupAlertBanner(); 32 setupTabs(); 33}); 34 35// "2026-08-07" -> "7 August 2026". Parsed by hand rather than through Date so 36// the result does not shift with the reader's time zone. 37function formatDate(iso) { 38 var months = ['January', 'February', 'March', 'April', 'May', 'June', 39 'July', 'August', 'September', 'October', 'November', 'December']; 40 var parts = String(iso).split('-'); 41 if (parts.length !== 3) return iso; 42 return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1] + ' ' + parts[0]; 43} 44 45function renderVerifiedNote() { 46 if (!verifiedNote || typeof directoryMeta === 'undefined') return; 47 verifiedNote.textContent = 'Every link was opened and checked on ' 48 + formatDate(directoryMeta.lastChecked) 49 + '. Institutions change their pages often â always verify with your own institution.'; 50} 51 52function setupTabs() { 53 var tabBtns = document.querySelectorAll('.tab-btn'); 54 var tabContents = document.querySelectorAll('.tab-content'); 55 56 tabBtns.forEach(function(btn) { 57 btn.addEventListener('click', function() { 58 var targetTab = btn.dataset.tab; 59 60 // Update button states 61 tabBtns.forEach(function(b) { b.classList.remove('active'); }); 62 btn.classList.add('active'); 63 64 // Update content visibility 65 tabContents.forEach(function(content) { 66 if (content.id === targetTab) { 67 content.classList.add('active'); 68 } else { 69 content.classList.remove('active'); 70 } 71 }); 72 73 // Scroll to the content 74 var targetElement = document.getElementById(targetTab); 75 if (targetElement) { 76 targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); 77 } 78 }); 79 }); 80} 81 82function setupAlertBanner() { 83 var alertBanner = document.getElementById('alert-banner'); 84 var alertClose = document.getElementById('alert-close'); 85 86 if (!alertBanner || !alertClose) return; 87 88 // Check if banner was previously dismissed 89 if (localStorage.getItem('alertBannerDismissed') === 'true') { 90 alertBanner.classList.add('hidden'); 91 return; 92 } 93 94 alertClose.addEventListener('click', function() { 95 alertBanner.classList.add('hidden'); 96 localStorage.setItem('alertBannerDismissed', 'true'); 97 }); 98} 99 100function updateTypeCounts() { 101 var counts = { research: 0, applied: 0, arts: 0, specialized: 0 }; 102 103 Object.values(universities).forEach(function(uni) { 104 if (counts[uni.type] !== undefined) { 105 counts[uni.type]++; 106 } 107 }); 108 109 var researchCount = document.getElementById('type-count-research'); 110 var appliedCount = document.getElementById('type-count-applied'); 111 var artsCount = document.getElementById('type-count-arts'); 112 var specializedCount = document.getElementById('type-count-specialized'); 113 114 if (researchCount) researchCount.textContent = counts.research; 115 if (appliedCount) appliedCount.textContent = counts.applied; 116 if (artsCount) artsCount.textContent = counts.arts; 117 if (specializedCount) specializedCount.textContent = counts.specialized; 118} 119 120function setupFilters() { 121 var typeCheckboxes = document.querySelectorAll('input[name="type"]'); 122 var serviceCheckboxes = document.querySelectorAll('input[name="service"]'); 123
124 typeCheckboxes.forEach(function(checkbox) { 125 checkbox.addEventListener('change', renderInstitutionList); 126 }); 127 128 serviceCheckboxes.forEach(function(checkbox) { 129 checkbox.addEventListener('change', renderInstitutionList); 130 }); 131 132 if (clearFiltersBtn) { 133 clearFiltersBtn.addEventListener('click', function() { 134 typeCheckboxes.forEach(function(cb) { cb.checked = true; }); 135 serviceCheckboxes.forEach(function(cb) { cb.checked = false; }); 136 searchInput.value = ''; 137 currentSearch = ''; 138 renderInstitutionList(); 139 }); 140 } 141} 142 143function setupSearch() { 144 searchInput.addEventListener('input', function(e) { 145 currentSearch = e.target.value.toLowerCase().trim(); 146 renderInstitutionList(); 147 }); 148} 149 150function setupMobileFilter() { 151 if (mobileFilterBtn) { 152 mobileFilterBtn.addEventListener('click', function() { 153 filtersSidebar.classList.toggle('active'); 154 mobileFilterBtn.textContent = filtersSidebar.classList.contains('active') ? 'Hide filters' : 'Filter options'; 155 }); 156 } 157} 158 159function getSelectedTypes() { 160 var selected = []; 161 document.querySelectorAll('input[name="type"]:checked').forEach(function(cb) { 162 selected.push(cb.value); 163 }); 164 return selected; 165} 166 167function getSelectedServices() { 168 var selected = []; 169 document.querySelectorAll('input[name="service"]:checked').forEach(function(cb) { 170 selected.push(cb.value); 171 }); 172 return selected; 173} 174 175function getFilteredInstitutions() { 176 var selectedTypes = getSelectedTypes(); 177 var selectedServices = getSelectedServices(); 178 var results = []; 179 180 Object.entries(universities).forEach(function(entry) { 181 var id = entry[0]; 182 var uni = entry[1]; 183 184 // Check search 185 var searchStr = (uni.name + ' ' + (uni.shortName || '')).toLowerCase(); 186 var matchesSearch = !currentSearch || searchStr.indexOf(currentSearch) !== -1; 187 if (!matchesSearch) return; 188 189 // Check type filter 190 var matchesType = selectedTypes.length === 0 || selectedTypes.indexOf(uni.type) !== -1; 191 if (!matchesType) return; 192 193 // Check service filter (if any selected, must have that service) 194 if (selectedServices.length > 0) { 195 var hasService = selectedServices.some(function(service) { 196 return uni.services && uni.services[service]; 197 }); 198 if (!hasService) return; 199 } 200 201 results.push({ 202 id: id, 203 name: uni.name, 204 shortName: uni.shortName, 205 type: uni.type 206 }); 207 }); 208 209 // Sort alphabetically by name 210 results.sort(function(a, b) { 211 return a.name.localeCompare(b.name); 212 }); 213 214 return results; 215} 216 217function renderInstitutionList() { 218 var filtered = getFilteredInstitutions(); 219 var html = ''; 220 221 filtered.forEach(function(uni) { 222 html += renderInstitutionItem(uni); 223 }); 224 225 institutionList.innerHTML = html; 226 227 // Update results count 228 var total = Object.keys(universities).length; 229 if (resultsCount) { 230 resultsCount.textContent = filtered.length + ' of ' + total + ' institutions'; 231 } 232 233 // Show/hide no results message 234 if (filtered.length === 0) { 235 noResults.style.display = 'block'; 236 } else { 237 noResults.style.display = 'none'; 238 } 239 240 // Add click handlers 241 institutionList.querySelectorAll('.institution-item').forEach(function(item) { 242 item.addEventListener('click', function() { 243 selectInstitution(item.dataset.id); 244 }); 245 }); 246 247 // Restore selected state 248 if (selectedInstitution) { 249 var selectedItem = institutionList.querySelector('[data-id="' + selectedInstitution.id + '"]'); 250 if (selectedItem) { 251 selectedItem.classList.add('selected'); 252 } 253 } 254} 255 256function renderInstitutionItem(uni) { 257 var selectedClass = selectedInstitution && selectedInstitution.id === uni.id ? ' selected' : ''; 258 var typeClass = ' type-' + uni.type; 259 var typeLabel = typeLabels[uni.type] || uni.type; 260 var badgeText = uni.shortName || uni.name.substring(0, 3).toUpperCase(); 261 262 var html = '<div class="institution-item' + selectedClass + typeClass + '" data-id="' + uni.id + '">'; 263 html += '<div class="inst-info">'; 264 html += '<span class="inst-badge">' + badgeText + '</span>'; 265 html += '<span class="inst-name">' + uni.name + '</span>'; 266 html += '</div>'; 267 html += '<span class="inst-type">' + typeLabel + '</span>'; 268 html += '<span class="inst-arrow">›</span>'; 269 html += '</div>'; 270 return html; 271} 272 273function selectInstitution(uniId) { 274 var prevSelected = institutionList.querySelector('.institution-item.selected'); 275 if (prevSelected) { 276 prevSelected.classList.remove('selected'); 277 } 278 279 selectedInstitution = { id: uniId }; 280 Object.assign(selectedInstitution, universities[uniId]); 281 282 var newSelected = institutionList.querySelector('[data-id="' + uniId + '"]'); 283 if (newSelected) { 284 newSelected.classList.add('selected'); 285 } 286 287 renderServiceCategories(); 288 hideServiceDetail(); 289 290 setTimeout(function() { 291 servicesSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); 292 }, 100); 293} 294
295function renderServiceCategories() { 296 var services = selectedInstitution.services; 297 var uni = selectedInstitution; 298 var typeLabel = typeLabels[uni.type] || uni.type; 299 300 var html = '<div class="services-header">'; 301 html += '<div class="services-info">'; 302 html += '<div class="services-type">' + typeLabel + '</div>'; 303 html += '<h2>' + uni.name + '</h2>'; 304 html += '<p>Select a support service</p>'; 305 if (uni.lastVerified) { 306 html += '<p class="services-verified">Links checked ' + formatDate(uni.lastVerified) + '</p>'; 307 } 308 html += '</div>'; 309 html += '<a href="' + uni.website + '" target="_blank" rel="noopener noreferrer" class="website-link">Visit website</a>'; 310 html += '</div>'; 311 312 // Show contact person if available 313 if (uni.contactPerson) { 314 html += '<div class="contact-person-card">'; 315 html += '<div class="contact-person-header">Contact Person</div>'; 316 html += '<div class="contact-person-name">' + uni.contactPerson.name + '</div>'; 317 if (uni.contactPerson.role) { 318 html += '<div class="contact-person-role">' + uni.contactPerson.role + '</div>'; 319 } 320 if (uni.contactPerson.email) { 321 html += '<div class="contact-person-email"><a href="mailto:' + uni.contactPerson.email + '">' + uni.contactPerson.email + '</a></div>'; 322 } 323 if (uni.contactPerson.phone) { 324 html += '<div class="contact-person-phone"><a href="tel:' + uni.contactPerson.phone + '">' + uni.contactPerson.phone + '</a></div>'; 325 } 326 html += '</div>'; 327 } 328 329 html += '<div class="service-grid">'; 330 331 Object.entries(serviceCategories).forEach(function(entry) { 332 var key = entry[0]; 333 var category = entry[1]; 334 var available = services[key]; 335 var unavailableClass = available ? '' : ' unavailable'; 336 337 html += '<div class="service-card' + unavailableClass + '" data-service="' + key + '"'; 338 if (!available) { 339 html += ' title="Not available"'; 340 } 341 html += '>'; 342 html += '<div class="icon">' + category.label + '</div>'; 343 html += '<h3>' + category.title + '</h3>'; 344 html += '<p>' + (available ? category.description : 'Not available') + '</p>'; 345 html += '</div>'; 346 }); 347 348 html += '</div>'; 349 servicesSection.querySelector('.container').innerHTML = html; 350 servicesSection.classList.add('active'); 351 352 servicesSection.querySelectorAll('.service-card:not(.unavailable)').forEach(function(card) { 353 card.addEventListener('click', function() { 354 showServiceDetail(card.dataset.service); 355 }); 356 }); 357} 358 359function showServiceDetail(serviceKey) { 360 var service = selectedInstitution.services[serviceKey]; 361 var category = serviceCategories[serviceKey]; 362 363 var html = '<button class="back-button" onclick="hideServiceDetail()">Back to services</button>'; 364 html += '<div class="detail-card">'; 365 html += '<h2>' + service.title; 366 if (service.freeOfCharge) { 367 html += '<span class="badge badge-free">Free</span>'; 368 } 369 html += '</h2>'; 370 html += '<p class="description">' + (service.description || category.description) + '</p>'; 371 372 if (service.url || service.email || service.phone) { 373 if (service.url) { 374 html += '<div class="info-row">'; 375 html += '<span class="info-label">Website</span>'; 376 html += '<span class="info-value"><a href="' + service.url + '" target="_blank" rel="noopener noreferrer">' + service.url + '</a></span>'; 377 html += '</div>'; 378 } 379 if (service.email) { 380 html += '<div class="info-row">'; 381 html += '<span class="info-label">Email</span>'; 382 html += '<span class="info-value"><a href="mailto:' + service.email + '">' + service.email + '</a></span>'; 383 html += '</div>'; 384 } 385 if (service.phone) { 386 html += '<div class="info-row">'; 387 html += '<span class="info-label">Phone</span>'; 388 html += '<span class="info-value"><a href="tel:' + service.phone + '">' + service.phone + '</a></span>'; 389 html += '</div>'; 390 } 391 } 392 393 if (service.url) { 394 html += '<a href="' + service.url + '" target="_blank" rel="noopener noreferrer" class="visit-button">Visit Official Page</a>'; 395 } 396 397 html += '</div>'; 398 399 serviceDetailSection.querySelector('.container').innerHTML = html; 400 serviceDetailSection.classList.add('active'); 401 servicesSection.classList.remove('active'); 402 403 serviceDetailSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); 404} 405 406function hideServiceDetail() { 407 serviceDetailSection.classList.remove('active'); 408 servicesSection.classList.add('active'); 409} 410 411function renderNationalResources() { 412 var html = '<h2>National Support Resources</h2>'; 413 html += '<p>Available to all students in the Netherlands</p>'; 414 html += '<div class="resources-grid">'; 415 416 var sorted = Object.entries(nationalResources).sort(function(a, b) { 417 return a[1].priority - b[1].priority; 418 }); 419 420 sorted.forEach(function(entry) { 421 var key = entry[0]; 422 var resource = entry[1]; 423 var cardClass = ''; 424 if (key === 'emergency') cardClass = ' emergency'; 425 else if (key === 'suicidePrevention') cardClass = ' crisis'; 426 427 html += '<div class="resource-card' + cardClass + '">'; 428 html += '<h3>' + resource.title + '</h3>'; 429 if (resource.number) { 430 html += '<div class="phone">' + resource.number + '</div>'; 431 } 432 html += '<p>' + resource.description + '</p>'; 433 if (resource.website) { 434 html += '<a href="' + resource.website + '" target="_blank" rel="noopener noreferrer">Visit website</a>'; 435 } 436 html += '</div>'; 437 }); 438 439 html += '</div>'; 440 nationalResourcesSection.querySelector('.container').innerHTML = html; 441} 442 443window.hideServiceDetail = hideServiceDetail;
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.