PageSourceSearch

https://nlstudentsupport.github.io/js/app.js

js nlstudentsupport.github.io collected 2026-10-03 09:41:23 UTC 14,793 bytes, 443 lines download raw bytes

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">&#8250;</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.