diff --git a/backend/.env.example b/backend/.env.example
deleted file mode 100644
index fa719061..00000000
--- a/backend/.env.example
+++ /dev/null
@@ -1,15 +0,0 @@
-PORT=5000
-MONGO_URI=mongodb://localhost:27017/os-compass
-JWT_SECRET=your_jwt_secret_here
-SESSION_SECRET=your_session_secret_here
-GITHUB_CLIENT_ID=your_github_client_id
-GITHUB_CLIENT_SECRET=your_github_client_secret
-GITHUB_CALLBACK_URL=http://localhost:5000/api/auth/github/callback
-FRONTEND_URL=http://localhost:5500
-EMAIL_HOST=smtp.mailtrap.io
-EMAIL_PORT=2525
-EMAIL_USER=your_user
-EMAIL_PASS=your_pass
-EMAIL_FROM=noreply@opensource-compass.org
-GEMINI_API_KEY=
-GEMINI_MODEL=gemini-2.5-flash
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 75ec0189..11ceffa3 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -22,6 +22,9 @@
"nodemailer": "^7.0.13",
"passport": "^0.7.0",
"passport-github2": "^0.1.12"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
}
},
"node_modules/@google/generative-ai": {
@@ -70,6 +73,20 @@
"node": ">= 0.6"
}
},
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -87,6 +104,16 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
@@ -105,6 +132,19 @@
"bcrypt": "bin/bcrypt"
}
},
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/body-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
@@ -129,6 +169,32 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/bson": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz",
@@ -182,6 +248,31 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -502,6 +593,19 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/finalhandler": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
@@ -598,6 +702,21 @@
"node": ">= 0.8"
}
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -644,6 +763,19 @@
"node": ">= 0.4"
}
},
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -656,6 +788,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -731,6 +873,13 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -746,6 +895,52 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@@ -907,6 +1102,22 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/mongodb": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz",
@@ -1029,6 +1240,45 @@
"node": ">=6.0.0"
}
},
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/oauth": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
@@ -1167,6 +1417,19 @@
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1186,6 +1449,13 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -1243,6 +1513,19 @@
"node": ">= 0.10"
}
},
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -1426,6 +1709,19 @@
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
"license": "MIT"
},
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
@@ -1444,6 +1740,32 @@
"node": ">= 0.8"
}
},
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1453,6 +1775,16 @@
"node": ">=0.6"
}
},
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
@@ -1497,6 +1829,13 @@
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
"license": "MIT"
},
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/backend/package.json b/backend/package.json
index ac12de85..ed0f5148 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -25,5 +25,8 @@
"passport": "^0.7.0",
"passport-github2": "^0.1.12"
},
- "description": ""
+ "description": "",
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
}
diff --git a/frontend/css/faq.css b/frontend/css/faq.css
new file mode 100644
index 00000000..f0a8f382
--- /dev/null
+++ b/frontend/css/faq.css
@@ -0,0 +1,334 @@
+/* FAQ Page Specific Styles */
+:root {
+ --primary-gold: #d4af37;
+ --text-dark: #333;
+ --text-light: #f4f4f4;
+ --bg-card: #ffffff;
+ --transition-speed: 0.3s;
+ --gold-glow: rgba(212, 175, 55, 0.3);
+ --transition-smooth: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+ --bg-color: #ffffff;
+ --section-bg: #fcfbf7;
+ --text-main: #1a1a1a;
+ --text-secondary: #555;
+ --card-bg: #ffffff;
+ --border-color: #e2d1a8;
+ --footer-bg: #0a0c10;
+ --text-gray: #a1a1a1;
+}
+
+/* =========================
+ NAVIGATION & LAYOUT
+========================= */
+body {
+ font-family: 'Inter', sans-serif;
+ line-height: 1.6;
+ margin: 0;
+ transition: background 0.3s ease;
+}
+
+main {
+ padding: 4rem 10%;
+ text-align: center;
+}
+
+main h3 {
+ font-family: 'Playfair Display', serif;
+ font-size: 2.5rem;
+ margin-bottom: 0.5rem;
+ color: var(--text-main);
+}
+
+main > section > p {
+ color: var(--text-secondary);
+ font-size: 1.1rem;
+ margin-bottom: 2rem;
+}
+
+/* =========================
+ FAQ ACCORDION
+========================= */
+.faq-container {
+ max-width: 850px;
+ margin: 3rem auto;
+ text-align: left;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.faq-item {
+ margin-bottom: 1rem;
+ border-radius: 12px;
+ background: var(--bg-card);
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ transition: all var(--transition-speed) cubic-bezier(0.4, 0, 0.2, 1);
+ overflow: hidden;
+}
+
+/* Focus State for Accessibility */
+.faq-item:focus-within {
+ outline: 2px solid var(--primary-gold);
+ outline-offset: 2px;
+}
+
+/* Hover State */
+.faq-item:hover {
+ border-color: var(--primary-gold);
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
+}
+
+.faq-question {
+ width: 100%;
+ background: none;
+ border: none;
+ padding: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 1.1rem;
+ font-weight: 600;
+ cursor: pointer;
+ color: var(--text-dark);
+ transition: background 0.2s ease;
+ font-family: 'Inter', sans-serif;
+ text-align: left;
+}
+
+.faq-question span {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #2d3748;
+ transition: color 0.3s ease;
+}
+
+.faq-question:hover {
+ background: rgba(212, 175, 55, 0.05);
+}
+
+.faq-question:focus-visible {
+ outline: 2px solid var(--primary-gold);
+ outline-offset: -2px;
+ border-radius: 12px 12px 0 0;
+}
+
+.faq-question i {
+ transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ color: var(--primary-gold);
+ font-size: 1rem;
+}
+
+.faq-answer {
+ max-height: 0;
+ overflow: hidden;
+ opacity: 0;
+ transition: all 0.4s ease;
+}
+
+.faq-answer p {
+ padding: 1.5rem;
+ margin: 0;
+ font-size: 1.05rem;
+ line-height: 1.6;
+ color: #4b5563;
+ text-align: left;
+ border-top: 1px solid rgba(212, 175, 55, 0.25);
+ background: linear-gradient(135deg, rgba(212, 175, 55, 0.05), rgba(255, 255, 255, 0.02));
+}
+
+/* Active State */
+.faq-item.active {
+ border-color: var(--primary-gold);
+ box-shadow: 0 4px 12px rgba(212, 175, 55, 0.15);
+}
+
+.faq-item.active .faq-answer {
+ max-height: 500px;
+ opacity: 1;
+}
+
+.faq-item.active .faq-question i {
+ transform: rotate(180deg);
+}
+
+.faq-item.active .faq-answer p {
+ border-left: 4px solid var(--primary-gold);
+}
+
+/* CTA Card Styles */
+.faq-cta-card {
+ max-width: 600px;
+ margin: 4rem auto 0;
+ padding: 2.5rem;
+ background: linear-gradient(135deg, #fff9f0 0%, #fff 100%);
+ border-radius: 20px;
+ border: 1px solid rgba(212, 175, 55, 0.3);
+ box-shadow: 0 10px 30px rgba(212, 175, 55, 0.15);
+}
+
+.faq-cta-content h4 {
+ font-family: 'Playfair Display', serif;
+ font-size: 1.8rem;
+ margin-bottom: 1rem;
+ color: var(--text-main);
+}
+
+.faq-cta-content p {
+ color: var(--text-secondary);
+ margin-bottom: 2rem;
+ font-size: 1.1rem;
+}
+
+.faq-cta-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.8rem;
+ background: var(--primary-gold);
+ color: #000;
+ padding: 1rem 2rem;
+ border-radius: 50px;
+ text-decoration: none;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ border: 2px solid transparent;
+}
+
+.faq-cta-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(212, 175, 55, 0.3);
+ background: #c49b2c;
+}
+
+.faq-cta-btn:focus-visible {
+ outline: 2px solid var(--primary-gold);
+ outline-offset: 2px;
+}
+
+/* =========================
+ DARK MODE OVERRIDES
+========================= */
+body.dark-mode {
+ background-color: #121212;
+ color: var(--text-light);
+}
+
+body.dark-mode .nav-links a {
+ color: #ccc;
+}
+
+body.dark-mode .nav-links a.active-page {
+ color: var(--primary-gold);
+}
+
+body.dark-mode .faq-item {
+ background-color: #1e1e1e;
+ border-color: #333;
+}
+
+body.dark-mode .faq-question {
+ color: #e0e0e0;
+}
+
+body.dark-mode .faq-question span {
+ color: #f5f5f5;
+}
+
+body.dark-mode .faq-answer {
+ background-color: #1a1a1a;
+}
+
+body.dark-mode .faq-answer p {
+ color: #d1d5db;
+}
+
+body.dark-mode .faq-item.active {
+ border-color: var(--primary-gold);
+ box-shadow: 0 4px 15px rgba(212, 175, 55, 0.25);
+}
+
+body.dark-mode .faq-item:hover {
+ border-color: var(--primary-gold);
+ box-shadow: 0 6px 20px rgba(212, 175, 55, 0.4);
+}
+
+body.dark-mode .faq-cta-card {
+ background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
+ border-color: var(--primary-gold);
+}
+
+body.dark-mode .faq-cta-content h4 {
+ color: #f5f5f5;
+}
+
+body.dark-mode .faq-cta-content p {
+ color: #bbb;
+}
+
+body.dark-mode main h3,
+body.dark-mode main section h3 {
+ color: var(--primary-gold);
+}
+
+/* Scroll to Top Button */
+#scrollTopBtn {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: var(--primary-gold);
+ color: #000;
+ border: none;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ font-size: 1.2rem;
+ cursor: pointer;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ z-index: 1000;
+}
+
+#scrollTopBtn:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgba(212, 175, 55, 0.4);
+}
+
+#scrollTopBtn:focus-visible {
+ outline: 2px solid var(--primary-gold);
+ outline-offset: 2px;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ main {
+ padding: 2rem 5%;
+ }
+
+ main h3 {
+ font-size: 2rem;
+ }
+
+ .faq-question {
+ padding: 1.2rem;
+ }
+
+ .faq-question span {
+ font-size: 1rem;
+ }
+
+ .faq-answer p {
+ padding: 1.2rem;
+ font-size: 0.95rem;
+ }
+
+ .faq-cta-card {
+ margin: 3rem 1rem;
+ padding: 1.5rem;
+ }
+
+ .faq-cta-content h4 {
+ font-size: 1.5rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/css/guides.css b/frontend/css/guides.css
index fd0469a4..52cb20eb 100644
--- a/frontend/css/guides.css
+++ b/frontend/css/guides.css
@@ -1003,3 +1003,395 @@ body.dark-mode .guide-list h4,
body.dark-mode .mistakes-list p {
color: #f9fafb;
}
+
+/* =========================================
+ GUIDES PAGE STYLES
+========================================= */
+
+/* Read Time Badge Styles */
+.read-time-badge {
+ display: inline-block;
+ margin-left: 15px;
+ padding: 4px 10px;
+ background: rgba(212, 175, 55, 0.1);
+ border-radius: 20px;
+ font-size: 0.8rem;
+ color: var(--primary-gold, #d4af37);
+ font-weight: 500;
+ vertical-align: middle;
+ transition: all 0.3s ease;
+}
+
+.read-time-badge i {
+ margin-right: 5px;
+ font-size: 0.75rem;
+}
+
+.read-time-badge:hover {
+ background: rgba(212, 175, 55, 0.2);
+ transform: scale(1.05);
+}
+
+/* Section Headings with Badges */
+section h3 {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+/* Dark Mode Support for Read Time Badge */
+body.dark-mode .read-time-badge {
+ background: rgba(212, 175, 55, 0.15);
+ color: #d4af37;
+ border: 1px solid rgba(212, 175, 55, 0.3);
+}
+
+body.dark-mode .read-time-badge:hover {
+ background: rgba(212, 175, 55, 0.25);
+}
+
+/* =========================================
+ EXISTING STYLES FROM YOUR CODE (Consolidated)
+========================================= */
+
+/* Global Smooth Transition */
+body.dark-mode {
+ background-color: #000000 !important;
+ color: #ffffff !important;
+}
+
+body.dark-mode * {
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.4s ease;
+}
+
+body.dark-mode {
+ background-color: #121212 !important;
+}
+
+body.dark-mode section,
+body.dark-mode main {
+ background: transparent !important;
+ border: none !important;
+}
+
+/* Force Main Backgrounds */
+body.dark-mode .guide,
+body.dark-mode .pull-request-guide,
+body.dark-mode .github-guide-timeline,
+body.dark-mode .contributing-best-practices,
+body.dark-mode .common-mistakes {
+ background-color: #121212 !important;
+ color: #e0e0e0 !important;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 30px;
+}
+
+body.dark-mode .guide h3,
+body.dark-mode .pull-request-guide h3,
+body.dark-mode .github-guide-timeline h3,
+body.dark-mode .contributing-best-practices h3,
+body.dark-mode .common-mistakes h2 {
+ color: #d4af37 !important;
+}
+
+/* Enhanced Card Backgrounds */
+body.dark-mode .guide-list li,
+body.dark-mode .pr-steps li,
+body.dark-mode .timeline-list li,
+body.dark-mode .practice-item,
+body.dark-mode .mistakes-list li {
+ background: #1e1e1e !important;
+ border: 1px solid #2a2a2a !important;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
+ position: relative;
+ overflow: hidden;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+}
+
+/* Golden Headlines */
+body.dark-mode .guide-list h4,
+body.dark-mode .pr-steps h4,
+body.dark-mode .timeline-list h4,
+body.dark-mode .practice-item h4 {
+ color: #b8860b !important;
+}
+
+/* Descriptions */
+body.dark-mode .guide-list p,
+body.dark-mode .pr-steps p,
+body.dark-mode .timeline-list p,
+body.dark-mode .practice-item p {
+ color: #b0b0b0 !important;
+}
+
+/* Advanced Hover Effects */
+body.dark-mode .guide-list li:hover,
+body.dark-mode .pr-steps li:hover,
+body.dark-mode .timeline-list li:hover,
+body.dark-mode .practice-item:hover,
+body.dark-mode .mistakes-list li:hover {
+ transform: translateY(-8px) scale(1.02);
+ border-color: #d4af37 !important;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.6), 0 0 15px rgba(212, 175, 55, 0.2) !important;
+ z-index: 10;
+}
+
+/* Subtle Shimmer Overlay on Hover */
+body.dark-mode .guide-list li::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.05), transparent);
+ transition: 0.5s;
+ pointer-events: none;
+}
+
+body.dark-mode .guide-list li:hover::after {
+ left: 100%;
+}
+
+/* Specific fix for Mistakes List text visibility in Dark Mode */
+body.dark-mode .mistakes-list p {
+ color: #ffffff !important;
+ opacity: 0.9;
+}
+
+body.dark-mode .mistake-icon {
+ color: #121212 !important;
+ background-color: #d4af37 !important;
+}
+
+/* Equal Size Practice Items - 4 Rows x 2 Columns */
+.practices-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1.5rem;
+ align-items: stretch;
+ margin-top: 20px;
+}
+
+/* Responsive: Stack to 1 column on mobile */
+@media (max-width: 768px) {
+ .practices-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.practice-item {
+ display: flex;
+ flex-direction: column;
+ min-height: 300px;
+ height: 100%;
+ padding: 1.5rem;
+ box-sizing: border-box;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.practice-item .icon {
+ flex-shrink: 0;
+ font-size: 2.5rem;
+ margin-bottom: 1rem;
+ height: 2.5rem;
+ line-height: 1;
+}
+
+.practice-item h4 {
+ flex-shrink: 0;
+ min-height: 4.5em;
+ margin-bottom: 0.75rem;
+ line-height: 1.4;
+ color: #333;
+}
+
+.practice-item p {
+ flex-grow: 1;
+ margin: 0;
+ line-height: 1.6;
+ color: #666;
+}
+
+/* Guide Lists Styling */
+.guide-list,
+.pr-steps,
+.timeline-list,
+.mistakes-list {
+ list-style: none;
+ padding: 0;
+ margin: 20px 0;
+}
+
+.guide-list li,
+.pr-steps li,
+.timeline-list li,
+.mistakes-list li {
+ position: relative;
+ padding: 20px 20px 20px 70px;
+ margin-bottom: 15px;
+ background: #f9f9f9;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+}
+
+.step {
+ position: absolute;
+ left: 20px;
+ top: 20px;
+ width: 30px;
+ height: 30px;
+ background: #d4af37;
+ color: #fff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+}
+
+.mistake-icon {
+ position: absolute;
+ left: 20px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 30px;
+ height: 30px;
+ background: #d4af37;
+ color: #fff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+}
+
+/* Timeline List Specific */
+.timeline-list li {
+ display: flex;
+ align-items: flex-start;
+ padding: 20px;
+}
+
+.timeline-list .icon {
+ flex-shrink: 0;
+ width: 50px;
+ height: 50px;
+ background: #d4af37;
+ color: #fff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 20px;
+ font-size: 1.2rem;
+}
+
+.timeline-list .content {
+ flex-grow: 1;
+}
+
+.timeline-list .content h4 {
+ margin: 0 0 10px 0;
+ color: #333;
+}
+
+.timeline-list .content p {
+ margin: 0;
+ color: #666;
+}
+
+/* Completed Button Styles */
+.track-guide-btn.completed {
+ background: #2ecc71 !important;
+ color: #ffffff !important;
+ border: none !important;
+ cursor: pointer !important;
+}
+
+.track-guide-btn.completed:hover {
+ background: #27ae60 !important;
+}
+
+/* Scroll to Top Button */
+#scrollTopBtn {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: #d4af37;
+ color: #fff;
+ border: none;
+ cursor: pointer;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+ transition: all 0.3s ease;
+ z-index: 1000;
+}
+
+#scrollTopBtn:hover {
+ background: #b8860b;
+ transform: translateY(-3px);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+}
+
+#scrollTopBtn.show {
+ display: flex;
+}
+
+/* Dark Mode Scroll to Top */
+body.dark-mode #scrollTopBtn {
+ background: #d4af37;
+ color: #121212;
+}
+
+body.dark-mode #scrollTopBtn:hover {
+ background: #b8860b;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ section h3 {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .read-time-badge {
+ margin-left: 0;
+ margin-top: 5px;
+ }
+
+ .guide-list li,
+ .pr-steps li,
+ .mistakes-list li {
+ padding: 60px 20px 20px 20px;
+ }
+
+ .step,
+ .mistake-icon {
+ left: 50%;
+ transform: translateX(-50%);
+ top: 15px;
+ }
+
+ .timeline-list li {
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ }
+
+ .timeline-list .icon {
+ margin-right: 0;
+ margin-bottom: 15px;
+ }
+}
diff --git a/frontend/css/program.css b/frontend/css/program.css
index ca5a6214..50240244 100644
--- a/frontend/css/program.css
+++ b/frontend/css/program.css
@@ -507,4 +507,83 @@ body.dark-mode .label.intermediate {
body.dark-mode .label.advanced {
background: rgba(220, 38, 38, 0.2);
color: #f87171;
+}
+
+/* Add these skeleton loading styles to your existing program.css */
+
+.skeleton-card {
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: loading 1.5s infinite;
+ pointer-events: none;
+ border: none;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+}
+
+.skeleton-card .skeleton-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background: #ddd;
+ margin: 0 auto 20px;
+}
+
+.skeleton-card .skeleton-title {
+ height: 24px;
+ width: 80%;
+ background: #ddd;
+ margin: 10px auto;
+ border-radius: 4px;
+}
+
+.skeleton-card .skeleton-text {
+ height: 16px;
+ width: 90%;
+ background: #ddd;
+ margin: 8px auto;
+ border-radius: 4px;
+}
+
+.skeleton-card .skeleton-badge {
+ height: 30px;
+ width: 100px;
+ background: #ddd;
+ margin: 20px auto 0;
+ border-radius: 20px;
+}
+
+.skeleton-card .skeleton-stats {
+ display: flex;
+ justify-content: space-around;
+ margin-top: 15px;
+ padding: 0 10px;
+}
+
+.skeleton-card .skeleton-stat {
+ height: 20px;
+ width: 80px;
+ background: #ddd;
+ border-radius: 4px;
+}
+
+@keyframes loading {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+/* Dark mode support for skeleton cards */
+.dark-mode .skeleton-card {
+ background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
+}
+
+.dark-mode .skeleton-card .skeleton-icon,
+.dark-mode .skeleton-card .skeleton-title,
+.dark-mode .skeleton-card .skeleton-text,
+.dark-mode .skeleton-card .skeleton-badge,
+.dark-mode .skeleton-card .skeleton-stat {
+ background: #444;
}
\ No newline at end of file
diff --git a/frontend/js/faq.js b/frontend/js/faq.js
new file mode 100644
index 00000000..ffef0710
--- /dev/null
+++ b/frontend/js/faq.js
@@ -0,0 +1,159 @@
+
+
+document.addEventListener("DOMContentLoaded", () => {
+ const faqItems = document.querySelectorAll(".faq-item");
+
+ // Don't proceed if no FAQ items found
+ if (!faqItems.length) return;
+
+ // Initialize each FAQ item
+ faqItems.forEach((item, index) => {
+ const button = item.querySelector(".faq-question");
+
+ // Skip if button doesn't exist
+ if (!button) return;
+
+ // Add keyboard and ARIA attributes
+ button.setAttribute('tabindex', '0');
+ button.setAttribute('role', 'button');
+
+ // Set initial ARIA expanded state based on active class
+ const isInitiallyActive = item.classList.contains("active");
+ button.setAttribute('aria-expanded', isInitiallyActive ? 'true' : 'false');
+
+ // Add aria-controls to associate button with answer
+ const answerId = `faq-answer-${index}`;
+ const answer = item.querySelector('.faq-answer');
+ if (answer) {
+ answer.id = answerId;
+ button.setAttribute('aria-controls', answerId);
+ }
+
+ // Click handler
+ button.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleFAQ(item, button);
+ });
+
+ // Keyboard handler for navigation
+ button.addEventListener("keydown", (e) => {
+ const key = e.key;
+
+
+ if (key === 'Enter' || key === ' ') {
+ e.preventDefault();
+ toggleFAQ(item, button);
+ }
+
+ // Arrow Down - Move to next FAQ
+ else if (key === 'ArrowDown') {
+ e.preventDefault();
+ const nextIndex = index + 1;
+ if (nextIndex < faqItems.length) {
+ const nextButton = faqItems[nextIndex].querySelector('.faq-question');
+ if (nextButton) {
+ nextButton.focus();
+ }
+ }
+ }
+
+ // Arrow Up - Move to previous FAQ
+ else if (key === 'ArrowUp') {
+ e.preventDefault();
+ const prevIndex = index - 1;
+ if (prevIndex >= 0) {
+ const prevButton = faqItems[prevIndex].querySelector('.faq-question');
+ if (prevButton) {
+ prevButton.focus();
+ }
+ }
+ }
+
+ // Home - Move to first FAQ
+ else if (key === 'Home') {
+ e.preventDefault();
+ const firstButton = faqItems[0].querySelector('.faq-question');
+ if (firstButton) {
+ firstButton.focus();
+ }
+ }
+
+ // End - Move to last FAQ
+ else if (key === 'End') {
+ e.preventDefault();
+ const lastButton = faqItems[faqItems.length - 1].querySelector('.faq-question');
+ if (lastButton) {
+ lastButton.focus();
+ }
+ }
+ });
+
+
+ button.addEventListener('focus', () => {
+
+ });
+ });
+
+ /**
+ * Toggle FAQ item open/closed
+ * @param {HTMLElement} item - The FAQ item container
+ * @param {HTMLElement} button - The button element
+ */
+ function toggleFAQ(item, button) {
+ // Check if current item is active
+ const isActive = item.classList.contains("active");
+
+ // Close all FAQ items first (for accordion behavior)
+ faqItems.forEach(i => {
+ i.classList.remove("active");
+ const btn = i.querySelector('.faq-question');
+ if (btn) {
+ btn.setAttribute('aria-expanded', 'false');
+ }
+ });
+
+ // If the clicked item wasn't active, open it
+ if (!isActive) {
+ item.classList.add("active");
+ button.setAttribute('aria-expanded', 'true');
+
+ // Announce to screen readers that the panel is expanded
+ const answer = item.querySelector('.faq-answer p');
+ if (answer) {
+ answer.setAttribute('aria-live', 'polite');
+ }
+ }
+ }
+
+ // Optional: Add keyboard shortcut hint for screen reader users
+ const addKeyboardHint = () => {
+ const container = document.querySelector('.faq-container');
+ if (container && !document.querySelector('.keyboard-hint')) {
+ const hint = document.createElement('div');
+ hint.className = 'keyboard-hint';
+ hint.setAttribute('aria-hidden', 'true');
+ hint.style.cssText = `
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-bottom: 1rem;
+ text-align: center;
+ `;
+ hint.innerHTML = '💡 Use arrow keys (↑/↓), Home, and End to navigate between questions';
+ container.parentNode.insertBefore(hint, container);
+ }
+ };
+
+
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
+ }
+ });
+ });
+
+ observer.observe(document.body, { childList: true, subtree: true });
+});
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { toggleFAQ };
+}
\ No newline at end of file
diff --git a/frontend/js/guides.js b/frontend/js/guides.js
index a4b52e2d..4c81d908 100644
--- a/frontend/js/guides.js
+++ b/frontend/js/guides.js
@@ -1,171 +1,287 @@
-// Toggle sidebar on mobile
-const toggleBtn = document.getElementById('toggle-sidebar');
-const sidebar = document.getElementById('sidebar');
-const overlay = document.getElementById('overlay');
-if (toggleBtn && sidebar && overlay) {
+function calculateReadTime() {
+ // Select all paragraphs that contain guide content
+ const guides = document.querySelectorAll('.guide-list p, .pr-steps p, .timeline-list p, .practice-item p, .contributing-best-practices > p');
+ let totalWords = 0;
+
+ // Count total words across all guide content
+ guides.forEach(p => {
+ const text = p.textContent || p.innerText;
+ // Split by whitespace and filter empty strings
+ const words = text.split(/\s+/).filter(word => word.length > 0);
+ totalWords += words.length;
+ });
+
+ // Calculate read time (round up to nearest minute)
+ const readTime = Math.ceil(totalWords / 200);
+
+ // Add read time badge to each major section
+ const sections = document.querySelectorAll('.guide, .pull-request-guide, .github-guide-timeline, .contributing-best-practices');
+
+ sections.forEach(section => {
+ const heading = section.querySelector('h3');
+ if (heading && !heading.querySelector('.read-time')) {
+ const timeBadge = document.createElement('span');
+ timeBadge.className = 'read-time-badge';
+ timeBadge.innerHTML = ` ${readTime} min read`;
+ heading.appendChild(timeBadge);
+ }
+ });
+
+ console.log(`Total read time calculated: ${readTime} minutes`); // Debug log
+}
+
+const TRACK_API_URL = 'http://localhost:5000/api/auth/track-guide';
+const STATUS_API_URL = 'http://localhost:5000/api/auth/me';
+
+/**
+ * Initialize progress tracking functionality
+ */
+function initProgressTracking() {
+ // Check and mark completed guides on page load
+ checkCompletedGuides();
+
+ // Add click handlers to all track guide buttons
+ document.querySelectorAll('.track-guide-btn').forEach(btn => {
+ btn.addEventListener('click', handleGuideCompletion);
+ });
+}
+
+/**
+ * Handle guide completion button click
+ */
+async function handleGuideCompletion(event) {
+ const btn = event.currentTarget;
+ const guideId = btn.getAttribute('data-guide-id');
+ const user = JSON.parse(localStorage.getItem('currentUser'));
+
+ if (!user) {
+ alert('Please sign in to save your learning progress!');
+ window.location.href = 'login.html';
+ return;
+ }
+
+ // UI Feedback - saving state
+ const originalText = btn.textContent;
+ btn.textContent = 'Saving...';
+ btn.disabled = true;
+
+ try {
+ const response = await fetch(TRACK_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ guideId })
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setButtonCompleted(btn);
+ // Update local storage with new progress
+ localStorage.setItem('currentUser', JSON.stringify(data.user));
+ } else {
+ // Reset button on error
+ btn.textContent = originalText;
+ btn.disabled = false;
+ const data = await response.json();
+ alert(data.message || 'Error tracking progress');
+ }
+ } catch (error) {
+ console.error('Failed to track progress:', error);
+ btn.textContent = originalText;
+ btn.disabled = false;
+ alert('Network error. Please try again.');
+ }
+}
+
+/**
+ * Check which guides are completed and update UI
+ */
+async function checkCompletedGuides() {
+ const currentUser = JSON.parse(localStorage.getItem('currentUser'));
+
+ if (currentUser) {
+ try {
+ const res = await fetch(STATUS_API_URL, { credentials: 'include' });
+ if (res.ok) {
+ const data = await res.json();
+ localStorage.setItem('currentUser', JSON.stringify(data.user));
+ markCompletedGuides(data.user.completedGuides);
+ } else {
+ markCompletedGuides(currentUser.completedGuides);
+ }
+ } catch (e) {
+ console.warn('Using cached user data for completed guides');
+ markCompletedGuides(currentUser.completedGuides);
+ }
+ }
+}
+
+function markCompletedGuides(completedGuides) {
+ if (!completedGuides || !Array.isArray(completedGuides)) return;
+
+ document.querySelectorAll('.track-guide-btn').forEach(btn => {
+ const guideId = btn.getAttribute('data-guide-id');
+ if (completedGuides.some(g => g.guideId === guideId)) {
+ setButtonCompleted(btn);
+ }
+ });
+}
+
+function setButtonCompleted(btn) {
+ btn.textContent = '✓ Completed';
+ btn.classList.add('completed');
+ btn.style.background = '#2ecc71';
+ btn.style.color = '#ffffff';
+ btn.style.border = 'none';
+ btn.disabled = true;
+}
+
+function initScrollToTop() {
+ const scrollBtn = document.getElementById('scrollTopBtn');
+
+ if (!scrollBtn) return;
+
+ window.addEventListener('scroll', () => {
+ if (window.pageYOffset > 300) {
+ scrollBtn.classList.add('show');
+ } else {
+ scrollBtn.classList.remove('show');
+ }
+ });
+
+ scrollBtn.addEventListener('click', () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ });
+}
+
+function initSidebar() {
+ const toggleBtn = document.getElementById('toggle-sidebar');
+ const sidebar = document.getElementById('sidebar');
+ const overlay = document.getElementById('overlay');
+
+ if (toggleBtn && sidebar && overlay) {
toggleBtn.addEventListener('click', () => {
- sidebar.classList.add('active');
- overlay.classList.add('active');
+ sidebar.classList.add('active');
+ overlay.classList.add('active');
});
overlay.addEventListener('click', () => {
- sidebar.classList.remove('active');
- overlay.classList.remove('active');
+ sidebar.classList.remove('active');
+ overlay.classList.remove('active');
});
+ }
}
-// Copy to clipboard functionality
-document.querySelectorAll('.copy-btn').forEach(button => {
+function initCopyButtons() {
+ document.querySelectorAll('.copy-btn').forEach(button => {
button.addEventListener('click', () => {
- const codeBlock = button.closest('.code-block').querySelector('pre');
- const code = codeBlock.innerText;
+ const codeBlock = button.closest('.code-block')?.querySelector('pre');
+ if (!codeBlock) return;
+
+ const code = codeBlock.innerText;
- navigator.clipboard.writeText(code).then(() => {
- button.textContent = 'Copied!';
- button.classList.add('copied');
+ navigator.clipboard.writeText(code).then(() => {
+ button.textContent = 'Copied!';
+ button.classList.add('copied');
- setTimeout(() => {
- button.textContent = 'Copy';
- button.classList.remove('copied');
- }, 2000);
- });
+ setTimeout(() => {
+ button.textContent = 'Copy';
+ button.classList.remove('copied');
+ }, 2000);
+ }).catch(err => {
+ console.error('Failed to copy:', err);
+ button.textContent = 'Error!';
+ setTimeout(() => {
+ button.textContent = 'Copy';
+ }, 2000);
+ });
});
-});
+ });
+}
-// Smooth scrolling for sidebar links
-document.querySelectorAll('.sidebar-menu a').forEach(anchor => {
+function initSmoothScroll() {
+ document.querySelectorAll('.sidebar-menu a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
- if (this.getAttribute('href').startsWith('#')) {
- e.preventDefault();
-
- document.querySelectorAll('.sidebar-menu a').forEach(link => {
- link.classList.remove('active');
- });
-
- this.classList.add('active');
-
- const targetId = this.getAttribute('href');
- const targetElement = document.querySelector(targetId);
-
- if (targetElement) {
- window.scrollTo({
- top: targetElement.offsetTop - 100,
- behavior: 'smooth'
- });
- }
-
- if (window.innerWidth <= 992) {
- if (sidebar) sidebar.classList.remove('active');
- if (overlay) overlay.classList.remove('active');
- }
- }
+ e.preventDefault();
+
+ // Update active state
+ document.querySelectorAll('.sidebar-menu a').forEach(link => {
+ link.classList.remove('active');
+ });
+ this.classList.add('active');
+
+ const targetId = this.getAttribute('href');
+ const targetElement = document.querySelector(targetId);
+
+ if (targetElement) {
+ window.scrollTo({
+ top: targetElement.offsetTop - 100,
+ behavior: 'smooth'
+ });
+ }
+
+ // Close sidebar on mobile after click
+ const sidebar = document.getElementById('sidebar');
+ const overlay = document.getElementById('overlay');
+ if (window.innerWidth <= 992) {
+ if (sidebar) sidebar.classList.remove('active');
+ if (overlay) overlay.classList.remove('active');
+ }
});
-});
+ });
+}
-// Handle active sidebar item on scroll
-window.addEventListener('scroll', () => {
- const sections = document.querySelectorAll('section');
+function initScrollSpy() {
+ window.addEventListener('scroll', () => {
+ const sections = document.querySelectorAll('section[id]');
let currentSection = '';
sections.forEach(section => {
- const sectionTop = section.offsetTop - 150;
- if (window.pageYOffset >= sectionTop) {
- currentSection = '#' + section.getAttribute('id');
- }
+ const sectionTop = section.offsetTop - 150;
+ if (window.pageYOffset >= sectionTop) {
+ currentSection = '#' + section.getAttribute('id');
+ }
});
- document.querySelectorAll('.sidebar-menu a').forEach(link => {
- if (link.getAttribute('href').startsWith('#')) {
- link.classList.remove('active');
- if (link.getAttribute('href') === currentSection) {
- link.classList.add('active');
- }
- }
+ document.querySelectorAll('.sidebar-menu a[href^="#"]').forEach(link => {
+ link.classList.remove('active');
+ if (link.getAttribute('href') === currentSection) {
+ link.classList.add('active');
+ }
});
-});
-
-// --- Progress Tracking (Backend Integration) ---
-const TRACK_API_URL = 'http://localhost:5000/api/auth/track-guide';
-const STATUS_API_URL = 'http://localhost:5000/api/auth/me';
-
-document.addEventListener('DOMContentLoaded', () => {
- checkCompletedGuides();
-
- document.querySelectorAll('.track-guide-btn').forEach(btn => {
- btn.addEventListener('click', async () => {
- const guideId = btn.getAttribute('data-guide-id');
- const user = JSON.parse(localStorage.getItem('currentUser'));
-
- if (!user) {
- alert('Please sign in to save your learning progress!');
- window.location.href = 'login.html';
- return;
- }
-
- // UI Feedback
- const originalText = btn.textContent;
- btn.textContent = 'Saving...';
- btn.disabled = true;
-
- try {
- const response = await fetch(TRACK_API_URL, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ guideId })
- });
-
- if (response.ok) {
- const data = await response.json();
- btn.textContent = '✓ Completed';
- btn.classList.add('completed-style');
- // Update local storage with new progress
- localStorage.setItem('currentUser', JSON.stringify(data.user));
- } else {
- btn.textContent = originalText;
- btn.disabled = false;
- const data = await response.json();
- alert(data.message || 'Error tracking progress');
- }
- } catch (error) {
- console.error('Failed to track progress:', error);
- btn.textContent = originalText;
- btn.disabled = false;
- }
- });
- });
-});
+ });
+}
-async function checkCompletedGuides() {
- const currentUser = JSON.parse(localStorage.getItem('currentUser'));
-
- // For local first-load consistency, fetch fresh data if logged in
- if (currentUser) {
- try {
- const res = await fetch(STATUS_API_URL, { credentials: 'include' });
- if (res.ok) {
- const data = await res.json();
- localStorage.setItem('currentUser', JSON.stringify(data.user));
- markUI(data.user.completedGuides);
- } else {
- markUI(currentUser.completedGuides);
- }
- } catch (e) {
- markUI(currentUser.completedGuides);
- }
- }
+function initGuidePage() {
+ // Calculate and display read time
+ calculateReadTime();
+
+ // Initialize progress tracking
+ initProgressTracking();
+
+ // Initialize UI components
+ initScrollToTop();
+ initSidebar();
+ initCopyButtons();
+ initSmoothScroll();
+ initScrollSpy();
+
+ console.log('Guide page initialized with read time feature');
}
-function markUI(completedGuides) {
- if (!completedGuides) return;
+document.addEventListener('DOMContentLoaded', initGuidePage);
- document.querySelectorAll('.track-guide-btn').forEach(btn => {
- const guideId = btn.getAttribute('data-guide-id');
- if (completedGuides.some(g => g.guideId === guideId)) {
- btn.textContent = '✓ Completed';
- btn.disabled = true;
- btn.classList.add('completed-style');
- }
- });
+window.recalculateReadTime = calculateReadTime;
+
+// Export for testing (if using modules)
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ calculateReadTime,
+ initProgressTracking,
+ setButtonCompleted
+ };
}
\ No newline at end of file
diff --git a/frontend/js/home.js b/frontend/js/home.js
index c896e5b8..69dc3fe3 100644
--- a/frontend/js/home.js
+++ b/frontend/js/home.js
@@ -1,4 +1,74 @@
document.addEventListener('DOMContentLoaded', () => {
+ // Replace existing newsletter form handler with enhanced validation
+ const newsletterForm = document.querySelector('.newsletter-form');
+
+ if (newsletterForm) {
+ newsletterForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ const emailInput = newsletterForm.querySelector('input[type="email"]');
+ const email = emailInput.value.trim();
+ const messageDiv = document.getElementById('newsletterMessage') || createMessageDiv(newsletterForm);
+
+ // Enhanced email validation - more comprehensive regex
+ const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
+
+ // Clear previous messages
+ messageDiv.textContent = '';
+ messageDiv.className = 'newsletter-message';
+
+ // Validate email format
+ if (!emailRegex.test(email)) {
+ showMessage(messageDiv, 'Please enter a valid email address (e.g., name@example.com)', 'error');
+ emailInput.focus();
+ return;
+ }
+
+ try {
+ // Store in localStorage for demo
+ const subscribers = JSON.parse(localStorage.getItem('newsletter_subscribers') || '[]');
+
+ if (!subscribers.includes(email)) {
+ subscribers.push(email);
+ localStorage.setItem('newsletter_subscribers', JSON.stringify(subscribers));
+ showMessage(messageDiv, `Thank you for subscribing! We'll send updates to ${email}`, 'success');
+ emailInput.value = '';
+ } else {
+ showMessage(messageDiv, 'This email is already subscribed!', 'info');
+ }
+ } catch (error) {
+ console.error('Newsletter subscription error:', error);
+ showMessage(messageDiv, 'An error occurred. Please try again.', 'error');
+ }
+ });
+ }
+
+ // Helper function to create message div if it doesn't exist
+ function createMessageDiv(form) {
+ let messageDiv = document.getElementById('newsletterMessage');
+ if (!messageDiv) {
+ messageDiv = document.createElement('div');
+ messageDiv.id = 'newsletterMessage';
+ messageDiv.className = 'newsletter-message';
+ form.appendChild(messageDiv);
+ }
+ return messageDiv;
+ }
+
+ // Helper function to show messages
+ function showMessage(element, text, type) {
+ element.textContent = text;
+ element.classList.add(type);
+
+ // Auto-hide success messages after 5 seconds
+ if (type === 'success') {
+ setTimeout(() => {
+ element.textContent = '';
+ element.classList.remove(type);
+ }, 5000);
+ }
+ }
+
// Fade-in sections/cards
const prefersReducedMotion =
window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
@@ -24,9 +94,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
// Stats counter animation
- const statItems = document.querySelectorAll('.stat-item');
- if (statItems.length && 'IntersectionObserver' in window) {
- // Function to animate counter
+ const statNumbers = document.querySelectorAll('.stat-number');
+ if (statNumbers.length && 'IntersectionObserver' in window) {
function animateCounter(element, target, suffix, duration = 2000) {
const startTime = performance.now();
const startValue = 0;
@@ -34,45 +103,53 @@ document.addEventListener('DOMContentLoaded', () => {
function updateCounter(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
-
+
// Easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
- const currentValue = Math.floor(easeOutQuart * target);
-
- element.textContent = currentValue + suffix;
+ let currentValue = Math.floor(easeOutQuart * target);
+
+ // Format large numbers
+ if (target >= 1000) {
+ currentValue = Math.floor(currentValue / 1000);
+ element.textContent = currentValue + 'K' + suffix;
+ } else {
+ element.textContent = currentValue + suffix;
+ }
if (progress < 1) {
requestAnimationFrame(updateCounter);
} else {
// Ensure final value is exact
- element.textContent = target + suffix;
+ if (target >= 1000) {
+ element.textContent = Math.floor(target / 1000) + 'K' + suffix;
+ } else {
+ element.textContent = target + suffix;
+ }
}
}
requestAnimationFrame(updateCounter);
}
- // Create observer for stats
const statsObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
- const statItem = entry.target;
- const numberElement = statItem.querySelector('h2');
-
+ const numberElement = entry.target;
+
if (numberElement && !numberElement.hasAttribute('data-animated')) {
- const target = parseFloat(numberElement.getAttribute('data-target') || '0');
+ const target = parseInt(numberElement.getAttribute('data-target') || '0');
const suffix = numberElement.getAttribute('data-suffix') || '';
-
+
// Mark as animated to prevent re-running
numberElement.setAttribute('data-animated', 'true');
-
+
// Start animation with small delay
setTimeout(() => {
animateCounter(numberElement, target, suffix);
}, 200);
-
- observer.unobserve(statItem);
+
+ observer.unobserve(numberElement);
}
}
});
@@ -83,8 +160,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
);
- // Observe each stat item
- statItems.forEach(item => statsObserver.observe(item));
+ statNumbers.forEach(item => statsObserver.observe(item));
}
// Lightweight parallax: move only the visual block (not the whole hero)
@@ -107,18 +183,6 @@ document.addEventListener('DOMContentLoaded', () => {
);
}
}
-
- // Newsletter demo
- const form = document.querySelector('.newsletter form');
- if (form) {
- form.addEventListener('submit', (e) => {
- e.preventDefault();
- const email = form.querySelector('input[type="email"]')?.value?.trim();
- if (!email) return;
- alert(`Thanks! You'll get updates at ${email}.`);
- form.reset();
- });
- }
});
// ===============================
@@ -190,6 +254,7 @@ if (scrollProgressBar) {
cursor.style.opacity = '';
});
})();
+
// ===============================
// Back to Top Button
// ===============================
@@ -197,7 +262,6 @@ const scrollTopBtn = document.getElementById('scrollTopBtn');
const scrollThreshold = 300;
if (scrollTopBtn) {
-
const toggleScrollButton = () => {
if (window.scrollY > scrollThreshold) {
scrollTopBtn.classList.add('show');
@@ -212,130 +276,11 @@ if (scrollTopBtn) {
scrollTopBtn.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
-
}
-
-document.addEventListener("DOMContentLoaded", () => {
- const counters = document.querySelectorAll(".stat-number");
-
- const animateCounter = (el) => {
- const target = +el.dataset.target;
- const suffix = el.dataset.suffix || "";
- const duration = 1500;
- const startTime = performance.now();
-
- const update = (currentTime) => {
- const progress = Math.min((currentTime - startTime) / duration, 1);
- const value = Math.floor(progress * target);
-
- if (target >= 1000) {
- el.textContent = `${Math.floor(value / 1000)}K${suffix}`;
- } else {
- el.textContent = `${value}${suffix}`;
- }
-
- if (progress < 1) {
- requestAnimationFrame(update);
- } else {
- el.textContent =
- target >= 1000
- ? `${target / 1000}K${suffix}`
- : `${target}${suffix}`;
- }
- };
-
- requestAnimationFrame(update);
- };
-
- const observer = new IntersectionObserver(
- (entries, obs) => {
- entries.forEach((entry) => {
- if (entry.isIntersecting) {
- animateCounter(entry.target);
- obs.unobserve(entry.target);
- }
- });
- },
- { threshold: 0.6 }
- );
-
- counters.forEach((counter) => observer.observe(counter));
-});
-
-document.addEventListener("DOMContentLoaded", () => {
- const journey = document.querySelector(".why-journey");
-
- if (!journey) return;
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry.isIntersecting) {
- journey.classList.add("is-visible");
- observer.unobserve(journey);
- }
- },
- { threshold: 0.4 }
- );
-
- observer.observe(journey);
-});
-
-document.addEventListener("DOMContentLoaded", () => {
- const counters = document.querySelectorAll(".counter");
- const statsSection = document.querySelector(".stats");
-
- let hasAnimated = false;
-
- const animateCounter = (counter) => {
- const target = +counter.getAttribute("data-target");
- const suffix = counter.getAttribute("data-suffix") || "";
- const duration = 1200; // animation duration
- const startTime = performance.now();
-
- const updateCounter = (currentTime) => {
- const elapsed = currentTime - startTime;
- const progress = Math.min(elapsed / duration, 1);
- const value = Math.floor(progress * target);
-
- // Format large numbers
- let displayValue = value;
- if (target >= 1000) {
- displayValue = Math.floor(value / 1000);
- }
-
- counter.innerText = displayValue + suffix;
-
- if (progress < 1) {
- requestAnimationFrame(updateCounter);
- } else {
- // Final accurate value
- if (target >= 1000) {
- counter.innerText = Math.floor(target / 1000) + suffix;
- } else {
- counter.innerText = target + suffix;
- }
- }
- };
-
- requestAnimationFrame(updateCounter);
- };
-
- const observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting && !hasAnimated) {
- counters.forEach(counter => animateCounter(counter));
- hasAnimated = true;
- observer.disconnect();
- }
- });
- }, {
- threshold: 0.4
- });
-
- observer.observe(statsSection);
-});
-
+// ===============================
+// Modal Functions
+// ===============================
function openModal(program) {
const modal = document.getElementById("programModal");
const title = document.getElementById("modalTitle");
@@ -370,29 +315,241 @@ function openModal(program) {
"Submit quality PRs",
"Show consistency"
]
+ },
+ gssoc: {
+ title: "GirlScript Summer of Code",
+ basic: `
+ 📅 Duration: 3 Months
+ 🎓 Eligibility: Students & beginners
+ 🏆 Perks: Certificate & swag
+ ⏳ Timeline: Feb–May
+ `,
+ skills: [
+ "Basic programming knowledge",
+ "Git & GitHub basics",
+ "Willingness to learn"
+ ],
+ prepare: [
+ "Join the community",
+ "Start with beginner issues",
+ "Follow project guidelines"
+ ],
+ tips: [
+ "Be consistent",
+ "Ask for help when stuck",
+ "Document your learning"
+ ]
+ },
+ hack: {
+ title: "Hacktoberfest",
+ basic: `
+ 📅 Duration: 1 Month
+ 🌍 Eligibility: Everyone
+ 🎁 Perks: Swags & goodies
+ ⏳ Timeline: October
+ `,
+ skills: [
+ "Basic Git knowledge",
+ "Ability to read code",
+ "Problem solving"
+ ],
+ prepare: [
+ "Find repositories you like",
+ "Look for hacktoberfest-labeled issues",
+ "Understand contribution guidelines"
+ ],
+ tips: [
+ "Quality over quantity",
+ "Don't spam PRs",
+ "Engage with maintainers"
+ ]
+ },
+ swoc: {
+ title: "Social Winter of Code",
+ basic: `
+ 📅 Duration: 3 Months
+ 👩💻 Eligibility: Beginners
+ 🏅 Perks: Certificate
+ ⏳ Timeline: Dec–Feb
+ `,
+ skills: [
+ "Basic programming",
+ "Enthusiasm to learn",
+ "Git basics"
+ ],
+ prepare: [
+ "Explore the platform",
+ "Join Discord/Slack",
+ "Find a mentor"
+ ],
+ tips: [
+ "Start early",
+ "Be active in community",
+ "Complete tasks consistently"
+ ]
+ },
+ linux: {
+ title: "Linux Foundation Mentorship",
+ basic: `
+ 📅 Duration: Varies
+ 👨💻 Eligibility: Developers
+ 💰 Stipend: Paid mentorship
+ 🌐 Timeline: Yearly
+ `,
+ skills: [
+ "Strong programming skills",
+ "Linux/Unix familiarity",
+ "Open source experience"
+ ],
+ prepare: [
+ "Contribute to LF projects",
+ "Learn about the ecosystem",
+ "Connect with mentors"
+ ],
+ tips: [
+ "Show long-term commitment",
+ "Build a portfolio",
+ "Network in the community"
+ ]
+ },
+ outreachy: {
+ title: "Outreachy",
+ basic: `
+ 📅 Duration: 3 Months
+ 🌍 Eligibility: Underrepresented groups
+ 💰 Stipend: Paid internship
+ ⏳ Timeline: Twice a year
+ `,
+ skills: [
+ "Project-specific skills",
+ "Communication",
+ "Self-motivation"
+ ],
+ prepare: [
+ "Make initial contributions",
+ "Complete the application",
+ "Engage with community"
+ ],
+ tips: [
+ "Apply early",
+ "Be thorough in application",
+ "Show genuine interest"
+ ]
+ },
+ mlh: {
+ title: "MLH Fellowship",
+ basic: `
+ 📅 Duration: 12 Weeks
+ 🎓 Eligibility: Students
+ 💰 Stipend: Paid
+ ⏳ Timeline: Spring/Fall
+ `,
+ skills: [
+ "Team collaboration",
+ "Project-based learning",
+ "Open source interest"
+ ],
+ prepare: [
+ "Build personal projects",
+ "Join MLH events",
+ "Practice coding"
+ ],
+ tips: [
+ "Show passion for tech",
+ "Be a team player",
+ "Learn continuously"
+ ]
+ },
+ kde: {
+ title: "Season of KDE",
+ basic: `
+ 📅 Duration: 3 Months
+ 🌍 Eligibility: Everyone
+ 🏆 Perks: Mentorship
+ ⏳ Timeline: Jan–Apr
+ `,
+ skills: [
+ "Qt/C++ basics",
+ "Open source interest",
+ "Git skills"
+ ],
+ prepare: [
+ "Try KDE applications",
+ "Join the community",
+ "Look at beginner bugs"
+ ],
+ tips: [
+ "Start with documentation",
+ "Ask questions",
+ "Be patient"
+ ]
+ },
+ hyperledger: {
+ title: "Hyperledger Mentorship",
+ basic: `
+ 📅 Duration: 3 Months
+ 🔗 Focus: Blockchain
+ 💰 Stipend: Paid
+ ⏳ Timeline: Summer
+ `,
+ skills: [
+ "Blockchain basics",
+ "Programming (Go/JavaScript)",
+ "Distributed systems interest"
+ ],
+ prepare: [
+ "Learn Hyperledger projects",
+ "Join the community calls",
+ "Explore documentation"
+ ],
+ tips: [
+ "Focus on one project",
+ "Show blockchain interest",
+ "Contribute early"
+ ]
}
};
const programData = data[program];
+
+ if (programData) {
+ title.innerHTML = programData.title;
+ basicInfo.innerHTML = programData.basic;
- title.innerHTML = programData.title;
- basicInfo.innerHTML = programData.basic;
-
- skills.innerHTML = "
" + programData.skills.map(item => `- ${item}
`).join("") + "
";
- prepare.innerHTML = "" + programData.prepare.map(item => `- ${item}
`).join("") + "
";
- tips.innerHTML = "" + programData.tips.map(item => `- ${item}
`).join("") + "
";
+ skills.innerHTML = "" + programData.skills.map(item => `- ${item}
`).join("") + "
";
+ prepare.innerHTML = "" + programData.prepare.map(item => `- ${item}
`).join("") + "
";
+ tips.innerHTML = "" + programData.tips.map(item => `- ${item}
`).join("") + "
";
- modal.style.display = "flex";
+ modal.style.display = "flex";
+ }
}
function closeModal() {
document.getElementById("programModal").style.display = "none";
}
-/* Accordion Toggle */
+// Accordion Toggle
document.addEventListener("click", function (e) {
if (e.target.classList.contains("accordion-header")) {
const body = e.target.nextElementSibling;
+
+ // Close other accordions
+ const allBodies = document.querySelectorAll('.accordion-body');
+ allBodies.forEach(b => {
+ if (b !== body) {
+ b.style.display = 'none';
+ }
+ });
+
+ // Toggle current
body.style.display = body.style.display === "block" ? "none" : "block";
}
-});
\ No newline at end of file
+});
+
+// Close modal when clicking outside
+window.onclick = function(e) {
+ const modal = document.getElementById("programModal");
+ if (e.target === modal) {
+ modal.style.display = "none";
+ }
+};
\ No newline at end of file
diff --git a/frontend/js/programs-page.js b/frontend/js/programs-page.js
index 9adb3f49..ed4c151e 100644
--- a/frontend/js/programs-page.js
+++ b/frontend/js/programs-page.js
@@ -3,14 +3,10 @@
document.addEventListener('DOMContentLoaded', () => {
const grid = document.getElementById('programs-grid');
+ const noResults = document.getElementById('noResults');
if (!grid) return;
- // -------------------------
- // Loading state
- // -------------------------
- grid.innerHTML = `
- Loading programs…
- `;
+ showLoadingState();
// Fetch program data
fetch('../data/programs.json')
@@ -29,8 +25,11 @@ document.addEventListener('DOMContentLoaded', () => {
String(a?.name || '').localeCompare(String(b?.name || ''))
);
- // Render cards
- grid.innerHTML = sorted.map(renderProgramCard).join('');
+ // Render actual cards
+ renderProgramCards(sorted);
+
+ // Re-attach filter functionality
+ initializeFilters();
})
.catch((err) => {
console.error('Failed to load programs:', err);
@@ -38,63 +37,198 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
-function renderProgramCard(program) {
- const name = escapeHtml(program?.name || 'Open Source Program');
- const desc = escapeHtml(program?.description || 'No description provided.');
- const timeline = escapeHtml(program?.timeline || 'N/A');
- const difficulty = escapeHtml(program?.difficulty || 'Beginner');
- const stipend = escapeHtml(program?.stipend || 'N/A');
- const contributors = program?.contributors || 0;
- const organizations = program?.organizations || 0;
- const issues = program?.issues || 0;
- const skills = Array.isArray(program?.skills) ? program.skills : program?.contributions || [];
- const url = typeof program?.url === 'string' ? program.url.trim() : '';
-
- // Difficulty color
- let diffColor = 'var(--primary-gold)';
- if (difficulty.toLowerCase().includes('intermediate')) diffColor = 'var(--secondary-gold)';
- if (difficulty.toLowerCase().includes('advanced')) diffColor = '#e74c3c';
-
- // Skills / Contributions tags
- const skillsHtml = skills.length
- ? `
- ${skills.map(s => `${escapeHtml(s)}`).join('')}
-
`
- : '';
-
- // Stats badges
- const statsHtml = `
-
-
${contributors} contributors
-
${organizations} orgs
-
${issues} issues
+/**
+ * Show loading skeleton cards
+ */
+function showLoadingState() {
+ const grid = document.getElementById('programs-grid');
+ if (!grid) return;
+
+ grid.innerHTML = Array(8).fill(0).map((_, i) => `
+
- `;
-
- return `
-
-
-
-
${name}
-
${desc}
-
-
+ `).join('');
+}
- ${skillsHtml}
- ${statsHtml}
+/**
+ * Render actual program cards
+ */
+function renderProgramCards(programs) {
+ const grid = document.getElementById('programs-grid');
+ const noResults = document.getElementById('noResults');
+
+ // Hide no results message initially
+ if (noResults) noResults.style.display = 'none';
+
+ // Render cards with data attributes for filtering
+ grid.innerHTML = programs.map(program => {
+ const name = escapeHtml(program?.name || 'Open Source Program');
+ const desc = escapeHtml(program?.description || 'No description provided.');
+ const timeline = escapeHtml(program?.timeline || 'N/A');
+ const difficulty = escapeHtml(program?.difficulty || 'Beginner');
+ const stipend = escapeHtml(program?.stipend || 'N/A');
+ const contributors = program?.contributors || 0;
+ const organizations = program?.organizations || 0;
+ const issues = program?.issues || 0;
+ const skills = Array.isArray(program?.skills) ? program.skills : program?.contributions || [];
+ const url = typeof program?.url === 'string' ? program.url.trim() : '';
+
+ // Extract filter classes from program data
+ const levelClass = getLevelClass(difficulty);
+ const typeClass = getTypeClass(stipend);
+ const regionClass = program?.region === 'India' ? 'india' : 'global';
+ const statusClass = program?.status === 'ongoing' ? 'ongoing' : 'seasonal';
+
+ // Difficulty color
+ let diffColor = 'var(--primary-gold)';
+ if (difficulty.toLowerCase().includes('intermediate')) diffColor = 'var(--secondary-gold)';
+ if (difficulty.toLowerCase().includes('advanced')) diffColor = '#e74c3c';
- ${url ? `
- Visit Official Website
- ` : ''}
+ // Skills / Contributions tags
+ const skillsHtml = skills.length
+ ? `
+ ${skills.map(s => `${escapeHtml(s)}`).join('')}
+
`
+ : '';
+
+ // Stats badges
+ const statsHtml = `
+
+ ${contributors} contributors
+ ${organizations} orgs
+ ${issues} issues
-
- `;
+ `;
+
+ return `
+
+
+
+
${name}
+
${desc}
+
+
+ ${typeClass === 'paid' ? 'Paid' : 'Unpaid'}
+ ${regionClass === 'india' ? 'India' : 'Global'}
+ ${difficulty}
+
+
+
+
+ ${skillsHtml}
+ ${statsHtml}
+
+ ${url ? `
+ Visit Official Website
+ ` : ''}
+
+
+ `;
+ }).join('');
+}
+
+/**
+ * Helper function to get level class
+ */
+function getLevelClass(difficulty) {
+ const diff = difficulty.toLowerCase();
+ if (diff.includes('beginner')) return 'beginner';
+ if (diff.includes('intermediate')) return 'intermediate';
+ if (diff.includes('advanced')) return 'advanced';
+ return 'beginner';
+}
+
+/**
+ * Helper function to get type class based on stipend
+ */
+function getTypeClass(stipend) {
+ const stipendLower = stipend.toLowerCase();
+ if (stipendLower.includes('paid') || stipendLower.includes('$') || stipendLower.includes('stipend')) {
+ return 'paid';
+ }
+ return 'unpaid';
}
+/**
+ * Initialize filter functionality
+ */
+function initializeFilters() {
+ const filters = {
+ level: document.getElementById("levelFilter"),
+ type: document.getElementById("typeFilter"),
+ region: document.getElementById("regionFilter"),
+ status: document.getElementById("statusFilter"),
+ };
+
+ const cards = document.querySelectorAll(".program-card:not(.skeleton-card)");
+ const noResults = document.getElementById("noResults");
+
+ function applyFilters() {
+ let visibleCount = 0;
+
+ cards.forEach(card => {
+ const matchLevel =
+ filters.level.value === "all" || card.classList.contains(filters.level.value);
+
+ const matchType =
+ filters.type.value === "all" || card.classList.contains(filters.type.value);
+
+ const matchRegion =
+ filters.region.value === "all" || card.classList.contains(filters.region.value);
+
+ const matchStatus =
+ filters.status.value === "all" || card.classList.contains(filters.status.value);
+
+ if (matchLevel && matchType && matchRegion && matchStatus) {
+ card.style.display = "flex";
+ card.style.opacity = "1";
+ card.style.transform = "scale(1)";
+ visibleCount++;
+ } else {
+ card.style.display = "none";
+ }
+ });
+
+ // Show "No Results" message
+ if (noResults) {
+ noResults.style.display = visibleCount === 0 ? "block" : "none";
+ }
+ }
+
+ // Add active filter UI feedback
+ Object.values(filters).forEach(filter => {
+ filter.addEventListener("change", () => {
+ // Highlight active filters
+ Object.values(filters).forEach(f => f.classList.remove("active-filter"));
+ if (filter.value !== "all") {
+ filter.classList.add("active-filter");
+ }
+ applyFilters();
+ });
+ });
+
+ // Initial filter application
+ applyFilters();
+}
function escapeHtml(str) {
return String(str)
@@ -103,4 +237,4 @@ function escapeHtml(str) {
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
-}
+}
\ No newline at end of file
diff --git a/frontend/pages/Event/gssoc.html b/frontend/pages/Event/gssoc.html
index 2556b01d..03d7f128 100644
--- a/frontend/pages/Event/gssoc.html
+++ b/frontend/pages/Event/gssoc.html
@@ -2085,6 +2085,7 @@
.protips-grid {
grid-template-columns: 1fr;
}
+ }
/* ---------- Pro Tips Section ---------- */
.pro-tips-section {
@@ -2367,7 +2368,7 @@
GirlScript Summer of Code 2026

+ alt="Coding illustration" loading="lazy" />
@@ -2392,7 +2393,7 @@ About GirlScript Summer of Code

+ alt="About GirlScript Summer of Code illustration showing program benefits" loading="lazy" />
diff --git a/frontend/pages/Event/hacktober.html b/frontend/pages/Event/hacktober.html
index ad92a341..708ee92e 100644
--- a/frontend/pages/Event/hacktober.html
+++ b/frontend/pages/Event/hacktober.html
@@ -1041,7 +1041,7 @@
-

+
Hacktoberfest 2026
diff --git a/frontend/pages/Event/mlh.html b/frontend/pages/Event/mlh.html
index ca6e8612..d23827e3 100644
--- a/frontend/pages/Event/mlh.html
+++ b/frontend/pages/Event/mlh.html
@@ -322,6 +322,12 @@
+
+

+
Global Remote Fellowship
diff --git a/frontend/pages/Event/outreachy.html b/frontend/pages/Event/outreachy.html
index 0a55af8b..518a0107 100644
--- a/frontend/pages/Event/outreachy.html
+++ b/frontend/pages/Event/outreachy.html
@@ -155,6 +155,8 @@
transform: translateY(-10px) scale(1.02);
box-shadow: var(--shadow-hover);
border-color: rgba(212, 175, 55, 0.4);
+ }
+
.cohorts-container {
position: relative;
max-width: 1200px;
@@ -1063,9 +1065,10 @@
margin-bottom: 4.5rem;
}
- /* Grid Table */
+/* Grid Table */
/* ===============================
PREMIUM COMPARISON TABLE
+=============================== */
.compare-grid {
display: grid;
@@ -1776,6 +1779,12 @@
+
+

+
Fully Remote • Paid Internships
Start Your Open Source Journey with Outreachy
diff --git a/frontend/pages/Event/ssoc.html b/frontend/pages/Event/ssoc.html
index d3a6b914..3ed1b551 100644
--- a/frontend/pages/Event/ssoc.html
+++ b/frontend/pages/Event/ssoc.html
@@ -1541,6 +1541,12 @@
+
+

+
Social Summer of Code
diff --git a/frontend/pages/faq.html b/frontend/pages/faq.html
index 7a75fa40..cd65e119 100644
--- a/frontend/pages/faq.html
+++ b/frontend/pages/faq.html
@@ -18,470 +18,7 @@
-
-
+
@@ -500,194 +37,171 @@
Programs
Resources
Contributors
- FAQ
+ FAQ
Contribute
Feedback
+ id="themeToggle"
+ aria-label="Toggle dark mode"
+ title="Toggle dark mode"
+ style="background: none; border: none; color: var(--primary-gold); cursor: pointer; font-size: 1.1rem; margin-left: 0.5rem; transition: transform 0.3s ease;"
+ >
+
+
-
-
- Frequently Asked Questions
- Common questions about OpenSource Compass and open source contributions.
-
-
-
-
-
-
OpenSource Compass is a platform that helps beginners navigate open source by providing structured guides, resources, and program information.
-
+
+
+ Frequently Asked Questions
+ Common questions about OpenSource Compass and open source contributions.
+
+
+
+
+
+
OpenSource Compass is a platform that helps beginners navigate open source by providing structured guides, resources, and program information.
+
-
-
-
-
No. You can contribute to open source through documentation, design, testing, community support, and many other non-coding roles.
-
+
+
+
+
No. You can contribute to open source through documentation, design, testing, community support, and many other non-coding roles.
+
-
-
-
-
Start by reading contribution guides, choosing a beginner-friendly project, and working on small issues such as documentation or bug fixes.
-
+
+
+
+
Start by reading contribution guides, choosing a beginner-friendly project, and working on small issues such as documentation or bug fixes.
+
-
-
-
-
Pick projects that match your interests and skill level. Look for repositories with labels like “good first issue” or “help wanted”.
-
+
+
+
+
Pick projects that match your interests and skill level. Look for repositories with labels like “good first issue” or “help wanted”.
+
-
-
-
-
Contributing helps you build a portfolio, improve technical skills, collaborate globally, and gain real-world development experience.
-
+
+
+
+
Contributing helps you build a portfolio, improve technical skills, collaborate globally, and gain real-world development experience.
+
-
-
-
-
Most projects have active communities on GitHub, Discord, or Slack. Asking questions is encouraged and is part of open-source culture.
-
+
+
+
+
Most projects have active communities on GitHub, Discord, or Slack. Asking questions is encouraged and is part of open-source culture.
+
-
-
-
-
Look for repositories with labels like good first issue, beginner, or help wanted.
-
+
+
+
+
Look for repositories with labels like good first issue, beginner, or help wanted.
+
-
-
-
-
A good first issue is a small, well-defined task that helps beginners understand the project without deep knowledge of the entire codebase.
-
+
+
+
+
A good first issue is a small, well-defined task that helps beginners understand the project without deep knowledge of the entire codebase.
+
-
-
-
-
Basic Git knowledge is helpful, but not mandatory. Many projects provide step-by-step contribution guides.
-
+
+
+
+
Basic Git knowledge is helpful, but not mandatory. Many projects provide step-by-step contribution guides.
+
-
-
-
-
Be polite, clear, and patient. Use GitHub issues or discussions to ask questions and seek feedback before starting work.
-
+
+
+
+
Be polite, clear, and patient. Use GitHub issues or discussions to ask questions and seek feedback before starting work.
+
-
-
-
-
Don’t be discouraged. Read the feedback carefully, make the suggested changes, or ask for clarification.
-
+
+
+
+
Don’t be discouraged. Read the feedback carefully, make the suggested changes, or ask for clarification.
+
-
-
-
-
Didn’t find your answer?
-
-
- Ask your question or share your feedback and we’ll help you out.
- Your query might also help improve this platform for others.
-
-
-
- Ask Your Question
-
-
+
+
+
+
Didn't find your answer?
+
+ Ask your question or share your feedback and we'll help you out.
+ Your query might also help improve this platform for others.
+
+
+
+ Ask Your Question
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
- if (!isActive) {
- item.classList.add("active");
- }
- });
- });
- });
-
+
-
-
+
+
+