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 = ""; - prepare.innerHTML = ""; - tips.innerHTML = ""; + skills.innerHTML = ""; + prepare.innerHTML = ""; + tips.innerHTML = ""; - 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}

- -
-
Timeline: ${timeline}
-
Level: ${difficulty}
-
Stipend: ${stipend}
-
+ `).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} +
+ +
+
Timeline: ${timeline}
+
Level: ${difficulty}
+
Stipend: ${stipend}
+
+ + ${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

Coding illustration + alt="Coding illustration" loading="lazy" />
@@ -2392,7 +2393,7 @@

About GirlScript Summer of Code

About Illustration + 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 Logo + Hacktoberfest logo - Annual October celebration of open source software

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 @@

+ + MLH Fellowship logo - Major League Hacking remote internship program + 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 @@

+ + Outreachy logo - Paid remote internship program for underrepresented groups in tech + 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 logo - Beginner-friendly open source program logo +

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"); - } - }); - }); - }); - + - - + + + \ No newline at end of file diff --git a/frontend/pages/guides.html b/frontend/pages/guides.html index 95c7642f..04c12617 100644 --- a/frontend/pages/guides.html +++ b/frontend/pages/guides.html @@ -3,18 +3,19 @@ - OpenSource Compass + OpenSource Compass - Guides - + + - + @@ -22,196 +23,14 @@ - - -
+

Beginner Open Source Guide

Open source is a way to collaborate on projects where the code is publicly available. Contributing helps you @@ -224,28 +43,30 @@

Beginner Open Source Guide

1

Learn Git & GitHub basics

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.

  • 2

    Explore small projects

    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.

  • 3

    Understand issues & labels

    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.

  • 4

    Make your first pull request

    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.

  • + +

    Step-by-Step Guide to Your First Pull Request

    + +

    Understanding GitHub Issues, Labels, and Workflows

    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.

    1. @@ -302,7 +125,7 @@

      Understanding GitHub Issues, Labels, and Workflows

      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.

    2. @@ -310,14 +133,15 @@

      How to find issues to work on

      Understanding issue labels

      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.

    3. How to comment effectively and ask for clarification

      -

      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.

    4. @@ -325,76 +149,77 @@

      How to comment effectively and ask for clarification

      How maintainers review contributions and merge PRs

      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.

    + +

    Best Practices for Contributing

    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.

    📝

    Write clear, concise commits

    -

    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.

    📚

    Follow contribution guidelines

    -

    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.

    🎨

    Maintain code style and formatting

    -

    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.

    🤝

    Communicate professionally

    -

    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.

    🔍

    Read and understand existing issues

    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.

    -
    💬

    Ask before working on large or breaking changes

    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.

    -
    🧩

    Keep pull requests small and focused

    Small, well-scoped pull requests are easier to review, test, and merge. + Aim for single-purpose PRs that address one issue or feature.

    -
    📄

    Update documentation when behavior changes

    Make sure README files or guides reflect any changes introduced by your - contribution. + contribution. Good documentation helps other users understand your changes.

    -
    + +

    Common Mistakes Beginners Should Avoid

      @@ -432,70 +257,22 @@

      Common Mistakes Beginners Should Avoid

    -
    - + + + + - - - - + diff --git a/frontend/pages/programs.html b/frontend/pages/programs.html index 184556e5..d759c1cc 100644 --- a/frontend/pages/programs.html +++ b/frontend/pages/programs.html @@ -22,7 +22,6 @@ - @@ -39,7 +38,6 @@

    Open Source Programs

    FILTER BAR ========================== -->
    -
    -
    - - - -
    -
    -

    GSSoC

    - -
    - Unpaid - India - Beginner-Friendly -
    - -

    GirlScript Summer of Code is a three-month-long Open Source program by GirlScript Foundation.

    - View Details -
    - -
    -
    -

    Hacktoberfest

    - -
    - Unpaid - Global - Beginner-Friendly -
    - -

    A month-long celebration of open-source software run by DigitalOcean.

    - View Details -
    - - - - - -
    -
    -

    Social Summer of Code

    - -
    - Unpaid - India - Beginner-Friendly -
    - -

    A program to promote open source and encourage student developers to build for social good.

    - View Details -
    - - - - - - - + +
    @@ -198,7 +86,6 @@

    Google Season of Docs

    - @@ -207,62 +94,7 @@

    Google Season of Docs

    - + \ No newline at end of file diff --git a/index.html b/index.html index 4c0fb979..f00d15cf 100644 --- a/index.html +++ b/index.html @@ -110,8 +110,6 @@

    - - @@ -169,229 +167,36 @@

    🎥 Video for Beginners

    "> - -
    -

    Open Source Programs

    -

    - Explore ongoing and upcoming open-source programs with timelines, - prerequisites, and official resources. -

    - -
    - -
    -
    -

    Google Summer of Code

    -

    - A global program focused on bringing new contributors into open - source software development. -

    -
    - - - - Explore - -
    + +
    +
    - -
    -
    -

    GSSoC

    -

    - GirlScript Summer of Code is a three-month-long Open Source program - by GirlScript Foundation. -

    -
    - - - - Explore - -
    -
    - -
    -
    -

    Hacktoberfest

    -

    - A month-long celebration of open-source software run by - DigitalOcean. -

    - -
    - - - - Explore - -
    -
    - - -
    -
    -

    Social Winter of Code

    -

    Beginner-focused winter open-source program.

    - -
    - - - - Explore - -
    -
    - - -
    -
    -

    Linux Foundation

    -

    - Explore various mentorship opportunities within the massive Linux - ecosystem. -

    -
    - - - - Explore - -
    -
    - - -
    -
    -

    Outreachy

    -

    - Provides internships to people subject to systemic bias and - underrepresented in tech. -

    -
    - - - - Explore - -
    -
    - - -
    -
    -

    MLH Fellowship

    -

    - A remote internship-like open source program where you collaborate in pods - on real-world projects with mentorship. -

    -
    - - - - Explore - -
    -
    - - - -
    -
    -

    Season of KDE

    -

    - A mentorship program by KDE that allows contributors to work on open - source desktop and Qt-based projects. -

    -
    - - - - Explore - -
    -
    - - -
    -
    -

    Hyperledger Mentorship

    -

    - A blockchain-focused mentorship program under the Hyperledger open source - ecosystem with guided project contributions. -

    -
    - + +
    +

    Open Source Programs

    +

    + Explore ongoing and upcoming open-source programs with timelines, + prerequisites, and official resources. +

    - - Explore - -
    -
    +
    + +
    +
    +

    Google Summer of Code

    +

    + A global program focused on bringing new contributors into open + source software development. +

    +
    + + + Explore + +
    @@ -402,14 +207,14 @@

    GSSoC

    GirlScript Summer of Code is a three-month-long Open Source program by GirlScript Foundation.

    - - - - + + Explore + +
    @@ -420,14 +225,14 @@

    Hacktoberfest

    A month-long celebration of open-source software run by DigitalOcean.

    - - - - + + Explore + + @@ -435,14 +240,14 @@

    Hacktoberfest

    Social Winter of Code

    Beginner-focused winter open-source program.

    - - - - + + Explore + + @@ -453,14 +258,14 @@

    Linux Foundation

    Explore various mentorship opportunities within the massive Linux ecosystem.

    - - - - + + Explore + + @@ -471,14 +276,14 @@

    Outreachy

    Provides internships to people subject to systemic bias and underrepresented in tech.

    - - - - + + Explore + + @@ -489,17 +294,16 @@

    MLH Fellowship

    A remote internship-like open source program where you collaborate in pods on real-world projects with mentorship.

    - - - - + + Explore + + -
    @@ -508,14 +312,14 @@

    Season of KDE

    A mentorship program by KDE that allows contributors to work on open source desktop and Qt-based projects.

    - - - - + + Explore + +
    @@ -526,14 +330,15 @@

    Hyperledger Mentorship

    A blockchain-focused mentorship program under the Hyperledger open source ecosystem with guided project contributions.

    - - - - + + Explore + + @@ -567,36 +372,10 @@

    Start with Strong Foundations

    Grow with Mentorship

    - 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.

    - - - -
    - - - - - - - -
    -