diff --git a/.github/workflows/program-classification-validator.yml b/.github/workflows/program-classification-validator.yml
index 95657f51b4..56e8afbcf9 100644
--- a/.github/workflows/program-classification-validator.yml
+++ b/.github/workflows/program-classification-validator.yml
@@ -81,24 +81,30 @@ jobs:
// ------------------------------------
// Metadata
// ------------------------------------
+
+ const normalizedBody = body.replace(/\r\n/g, '\n');
+ const has = (re) => re.test(normalizedBody);
if (
- body.includes('program: gssoc') ||
- body.includes('- [x] gssoc')
+ has(/\bprogram\s*:\s*gssoc\b/i) ||
+ has(/^\s*[-*]\s*\[x\]\s*(gssoc|i am contributing under gssoc|i am participating via gssoc)\b/im) ||
+ has(/\bprogram\n\s*gssoc\b/i)
) {
detected.add('gssoc');
}
if (
- body.includes('program: nsoc') ||
- body.includes('- [x] nsoc')
+ has(/\bprogram\s*:\s*nsoc\b/i) ||
+ has(/^\s*[-*]\s*\[x\]\s*(nsoc|i am contributing under nsoc|i am participating via nsoc)\b/im) ||
+ has(/\bprogram\n\s*nsoc\b/i)
) {
detected.add('nsoc');
}
if (
- body.includes('program: general') ||
- body.includes('- [x] general contribution')
+ has(/\bprogram\s*:\s*general\b/i) ||
+ has(/^\s*[-*]\s*\[x\]\s*(general contribution|i am a general contributor)\b/im) ||
+ has(/\bprogram\n\s*general\b/i)
) {
detected.add('general');
}
diff --git a/index.html b/index.html
index 09b54a5756..80eef1025b 100644
--- a/index.html
+++ b/index.html
@@ -38,35 +38,69 @@
}
-
-
-
-
-
@@ -75,7 +109,12 @@
// Theme restoration - runs early to prevent FOUC
(function(){
try {
- if (localStorage.getItem('theme') === 'dark') {
+ const savedTheme = localStorage.getItem('theme');
+ if (savedTheme === 'dark') {
+ document.documentElement.classList.add('dark');
+ } else if (savedTheme === 'light') {
+ document.documentElement.classList.remove('dark');
+ } else if (globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
} catch(e) {
diff --git a/src/js/app.js b/src/js/app.js
index d1c9415db2..1a64404e71 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -109,9 +109,26 @@ const CONTACT_TIPS = {
// ══════════════════════════════════════════════
(function initTheme() {
try {
- const saved = localStorage.getItem('theme') || 'light';
- document.documentElement.classList.toggle('dark', saved === 'dark');
+ const saved = localStorage.getItem('theme');
+ if (saved === 'dark') {
+ document.documentElement.classList.add('dark');
+ } else if (saved === 'light') {
+ document.documentElement.classList.remove('dark');
+ } else if (globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
updateThemeIcon();
+
+ if (globalThis.matchMedia) {
+ globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
+ if (!localStorage.getItem('theme')) {
+ document.documentElement.classList.toggle('dark', e.matches);
+ updateThemeIcon();
+ }
+ });
+ }
} catch (e) {
console.warn('Theme init failed:', e);
}
@@ -1071,9 +1088,17 @@ function searchComparator(a, b, search, sort) {
if (nameB === search && nameA !== search) return 1;
if (nameA.startsWith(search) && !nameB.startsWith(search)) return -1;
if (nameB.startsWith(search) && !nameA.startsWith(search)) return 1;
+
+ if (globalThis.orgSearchScores && globalThis.orgSearchScores.has(a.name) && globalThis.orgSearchScores.has(b.name)) {
+ const diff = globalThis.orgSearchScores.get(a.name) - globalThis.orgSearchScores.get(b.name);
+ if (diff !== 0) return diff;
+ }
+
return applySecondarySort(a, b, sort);
}
+let orgSearchFuse = null;
+
function applyFilters() {
const search = (document.getElementById('searchInput')?.value || '').trim().toLowerCase();
const categoryValue = document.getElementById('categoryFilter')?.value || 'all';
@@ -1081,7 +1106,30 @@ function applyFilters() {
const compF = document.getElementById('complexityFilter')?.value || 'all';
const sort = document.getElementById('sortSelect')?.value || 'alpha';
- filteredOrgs = ORGS.filter(o => matchesFilters(o, cat, compF, search));
+ globalThis.orgSearchScores = new Map();
+ let baseOrgs = ORGS;
+ let useFallbackSearch = false;
+
+ if (search) {
+ if (typeof Fuse !== 'undefined') {
+ if (!orgSearchFuse) {
+ orgSearchFuse = new Fuse(ORGS, {
+ includeScore: true,
+ threshold: 0.3,
+ keys: ['name', 'tags', 'cat']
+ });
+ }
+ const results = orgSearchFuse.search(search);
+ baseOrgs = results.map(r => {
+ globalThis.orgSearchScores.set(r.item.name, r.score);
+ return r.item;
+ });
+ } else {
+ useFallbackSearch = true;
+ }
+ }
+
+ filteredOrgs = baseOrgs.filter(o => matchesFilters(o, cat, compF, useFallbackSearch ? search : ''));
// Smart sorting: Exact match first, startsWith second, alphabetic/secondary sort third
if (search) {
@@ -2622,6 +2670,7 @@ if (typeof module !== 'undefined' && module.exports) {
githubUrlFromValue,
orgMatchesLanguages,
applySecondarySort,
+ searchComparator,
openModal,
renderModalHeader,
closeModal,
diff --git a/tests/filtering.test.js b/tests/filtering.test.js
index 44950f4191..123915f5f5 100644
--- a/tests/filtering.test.js
+++ b/tests/filtering.test.js
@@ -27,7 +27,7 @@ globalThis.document = {
globalThis.ORGS = require('../src/js/org.js');
// Import the modules
-const { orgMatchesLanguages, applySecondarySort } = require('../src/js/app.js');
+const { orgMatchesLanguages, applySecondarySort, searchComparator } = require('../src/js/app.js');
require('../src/js/skillExtractor.js'); // Populates LANGUAGE_MAP on global
test('orgMatchesLanguages returns true when languages set is empty', () => {
@@ -76,3 +76,29 @@ test('applySecondarySort sorts correctly', () => {
// alphabetical sort (default)
assert.ok(applySecondarySort(a, b, 'alpha') < 0);
});
+
+test('searchComparator sorts with fuzzy scores', () => {
+ const a = { name: 'The Python Foundation', years: 10, competition: 'hot' };
+ const b = { name: 'A Pyhton Group', years: 5, competition: 'chill' };
+ const c = { name: 'Some Other', years: 5, competition: 'chill' };
+
+ globalThis.orgSearchScores = new Map();
+ globalThis.orgSearchScores.set('The Python Foundation', 0.1);
+ globalThis.orgSearchScores.set('A Pyhton Group', 0.3);
+ globalThis.orgSearchScores.set('Some Other', 0.9);
+
+ // a has better score than b
+ assert.ok(searchComparator(a, b, 'pyhton', 'alpha') < 0);
+ // b has better score than c
+ assert.ok(searchComparator(b, c, 'pyhton', 'alpha') < 0);
+
+ // Exact match takes precedence over score
+ const exact = { name: 'pyhton', years: 1, competition: 'chill' };
+ globalThis.orgSearchScores.set('pyhton', 0.5); // Worse score somehow
+ assert.ok(searchComparator(exact, a, 'pyhton', 'alpha') < 0);
+
+ // StartsWith takes precedence over score
+ const starts = { name: 'pyhton starts', years: 1, competition: 'chill' };
+ globalThis.orgSearchScores.set('pyhton starts', 0.4);
+ assert.ok(searchComparator(starts, b, 'pyhton', 'alpha') < 0);
+});