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/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 = "
- Explore ongoing and upcoming open-source programs with timelines, - prerequisites, and official resources. -
- -- A global program focused on bringing new contributors into open - source software development. -
-- GirlScript Summer of Code is a three-month-long Open Source program - by GirlScript Foundation. -
-- A month-long celebration of open-source software run by - DigitalOcean. -
- -- Explore various mentorship opportunities within the massive Linux - ecosystem. -
-- Provides internships to people subject to systemic bias and - underrepresented in tech. -
-- A remote internship-like open source program where you collaborate in pods - on real-world projects with mentorship. -
-- A mentorship program by KDE that allows contributors to work on open - source desktop and Qt-based projects. -
-- A blockchain-focused mentorship program under the Hyperledger open source - ecosystem with guided project contributions. -
-+ Explore ongoing and upcoming open-source programs with timelines, + prerequisites, and official resources. +
- - Explore - -+ A global program focused on bringing new contributors into open + source software development. +
+Beginner-focused winter open-source program.
-- Learn alongside maintainers and contributors who’ve already - walked the path you’re starting today. + Learn alongside maintainers and contributors who've already + walked the path you're starting today.