How to find issues to work on
Learn to identify beginner-friendly issues or areas of the project that match your skills and interest. -
+ Check labels like "good first issue," "beginner," or "help wanted" for starting points.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/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/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 = "
${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 + ? `${desc}
+ +
+
+
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
+
Open source is a way to collaborate on projects where the code is publicly available. Contributing helps you @@ -224,28 +43,30 @@
Start by understanding Git version control and creating repositories on GitHub. This is the foundation for - contributing to open source projects.
+ contributing to open source projects. Git tracks changes, while GitHub hosts repositories and facilitates collaboration.Contribute to beginner-friendly projects to build confidence and experience. Look for tags like "good first - issue."
+ issue" and "help wanted" to find suitable starting points for your first contributions.Learn how to read issues, use labels, and understand project workflows to know where you can contribute - effectively.
+ effectively. Labels help categorize issues by type, difficulty, and priority.Submit your first PR to fix a bug or add a small feature. This helps you practice collaboration and the - contribution process.
+ contribution process. Remember to follow the project's contribution guidelines.Mastering GitHub issues and workflows helps you contribute effectively and communicate clearly with project - maintainers.
+ maintainers. This section covers the essential skills for navigating open source projects.Learn to identify beginner-friendly issues or areas of the project that match your skills and interest. -
+ Check labels like "good first issue," "beginner," or "help wanted" for starting points.Recognize labels like "good first issue" or "help wanted" to know what the project expects and where you - can contribute.
+ can contribute. Labels also indicate priority, type (bug/feature), and status.Communicate clearly in issue threads to ask questions, offer help, or provide updates on your work.
+Communicate clearly in issue threads to ask questions, offer help, or provide updates on your work. + Be respectful and specific in your questions to get better responses.
Understand the review process, how maintainers provide feedback, and the workflow for merging your pull - requests.
+ requests. Reviews may include change requests, approvals, or discussions.Follow these best practices to make your contributions effective, professional, and easy for maintainers to - review.
+ review. Adopting good habits early will make you a valued contributor.Meaningful commit messages make it easier for others to understand your changes.
+Meaningful commit messages make it easier for others to understand your changes. Use the imperative mood and keep subject lines under 50 characters.
Adhering to project guidelines ensures your PR meets expectations and saves time for maintainers.
+Adhering to project guidelines ensures your PR meets expectations and saves time for maintainers. Most projects have a CONTRIBUTING.md file.
Keep your code consistent with the project's style and formatting conventions.
+Keep your code consistent with the project's style and formatting conventions. Use linters and formatters when available.
Be polite, constructive, and respectful when discussing changes or providing feedback.
+Be polite, constructive, and respectful when discussing changes or providing feedback. Assume good intentions and be patient with questions.
Review open and closed issues to avoid duplicate efforts and to understand - current discussions or decisions. + current discussions or decisions. This shows respect for maintainers' time.
Discuss major changes with maintainers beforehand to ensure alignment with - the project's direction. + the project's direction. This prevents wasted effort on rejected features.
Small, well-scoped pull requests are easier to review, test, and merge. + Aim for single-purpose PRs that address one issue or feature.
Make sure README files or guides reflect any changes introduced by your - contribution. + contribution. Good documentation helps other users understand your changes.
A global program focused on bringing new contributors into open source software development.
- View Details -GirlScript Summer of Code is a three-month-long Open Source program by GirlScript Foundation.
- View Details -A month-long celebration of open-source software run by DigitalOcean.
- View Details -Explore various mentorship opportunities within the massive Linux ecosystem.
- View Details -Provides internships to people subject to systemic bias and underrepresented in tech.
- View Details -A program to promote open source and encourage student developers to build for social good.
- View Details -A 12-week remote internship alternative for aspiring engineers.
- View Details -Supports open source documentation projects.
- View Details -