mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 10:11:55 +08:00
Compare commits
1 Commits
fix/dashbo
...
fix/count
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3143cf68b |
@@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1725962974592
|
||||
"lastUpdateCheck": 1724925726721
|
||||
}
|
||||
}
|
||||
206
package-lock.json
generated
206
package-lock.json
generated
@@ -18,7 +18,7 @@
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"astro": "^4.15.4",
|
||||
"astro": "^4.14.6",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dom-to-image": "^2.6.0",
|
||||
@@ -506,9 +506,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
|
||||
"integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz",
|
||||
"integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.8",
|
||||
"@babel/helper-validator-identifier": "^7.24.7",
|
||||
@@ -2266,21 +2266,13 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.16.3.tgz",
|
||||
"integrity": "sha512-yETIvrETCeC39gSPIiSADmjri9FwKmxz0QvONMtTIUYlKZe90CJkvcjPksayC2VQOtzOJonEiULUa8v8crUQvA==",
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.14.1.tgz",
|
||||
"integrity": "sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^9.2.0",
|
||||
"@types/hast": "^3.0.4",
|
||||
"oniguruma-to-js": "0.3.3",
|
||||
"regex": "4.3.2"
|
||||
"@types/hast": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/vscode-textmate": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.2.tgz",
|
||||
"integrity": "sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg=="
|
||||
},
|
||||
"node_modules/@shuding/opentype.js": {
|
||||
"version": "1.4.0-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
|
||||
@@ -2908,17 +2900,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/astro": {
|
||||
"version": "4.15.4",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-4.15.4.tgz",
|
||||
"integrity": "sha512-wqy+m3qygt9DmCSqMsckxyK4ccCUFtti2d/WlLkEpAlqHgyDIg20zRTLHO2v/H4YeSlJ8sAcN0RW2FhOeYbINg==",
|
||||
"version": "4.14.6",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-4.14.6.tgz",
|
||||
"integrity": "sha512-MIDyNhtu3L4uakHvlTprh21eQPehYOtZSuSLtd+r6xZcl3lB+mlBz/hs1W3iHEQAORyJnKArWSY/aVOBKUyflA==",
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.10.3",
|
||||
"@astrojs/internal-helpers": "0.4.1",
|
||||
"@astrojs/markdown-remark": "5.2.0",
|
||||
"@astrojs/telemetry": "3.1.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/generator": "^7.25.5",
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.25.2",
|
||||
"@babel/types": "^7.25.6",
|
||||
"@babel/traverse": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"@oslojs/encoding": "^0.4.1",
|
||||
"@rollup/pluginutils": "^5.1.0",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
@@ -2941,8 +2936,8 @@
|
||||
"es-module-lexer": "^1.5.4",
|
||||
"esbuild": "^0.21.5",
|
||||
"estree-walker": "^3.0.3",
|
||||
"execa": "^8.0.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastq": "^1.17.1",
|
||||
"flattie": "^1.1.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
@@ -2951,7 +2946,6 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.11",
|
||||
"magicast": "^0.3.5",
|
||||
"micromatch": "^4.0.8",
|
||||
"mrmime": "^2.0.0",
|
||||
"neotraverse": "^0.6.18",
|
||||
@@ -2963,15 +2957,14 @@
|
||||
"prompts": "^2.4.2",
|
||||
"rehype": "^13.0.1",
|
||||
"semver": "^7.6.3",
|
||||
"shiki": "^1.16.1",
|
||||
"shiki": "^1.14.1",
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tinyexec": "^0.3.0",
|
||||
"tsconfck": "^3.1.3",
|
||||
"tsconfck": "^3.1.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.3",
|
||||
"vite": "^5.4.2",
|
||||
"vitefu": "^1.0.2",
|
||||
"vitefu": "^0.2.5",
|
||||
"which-pm": "^3.0.0",
|
||||
"xxhash-wasm": "^1.0.2",
|
||||
"yargs-parser": "^21.1.1",
|
||||
@@ -4047,6 +4040,28 @@
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^8.0.1",
|
||||
"human-signals": "^5.0.0",
|
||||
"is-stream": "^3.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^5.1.0",
|
||||
"onetime": "^6.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -4310,6 +4325,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.7.6",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz",
|
||||
@@ -4694,6 +4720,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
@@ -4893,6 +4927,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz",
|
||||
@@ -5207,16 +5252,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -5491,6 +5526,11 @@
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -6078,6 +6118,17 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-function": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||
@@ -6274,6 +6325,31 @@
|
||||
"npm": ">=8.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
@@ -6321,12 +6397,18 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/oniguruma-to-js": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.3.3.tgz",
|
||||
"integrity": "sha512-m90/WEhgs8g4BxG37+Nu3YrMfJDs2YXtYtIllhsEPR+wP3+K4EZk6dDUvy2v2K4MNFDDOYKL4/yqYPXDqyozTQ==",
|
||||
"node_modules/onetime": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
@@ -7174,11 +7256,6 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/regex/-/regex-4.3.2.tgz",
|
||||
"integrity": "sha512-kK/AA3A9K6q2js89+VMymcboLOlF5lZRCYJv3gzszXFHBr6kO6qLGzbm+UIugBEV8SMMKCTR59txoY6ctRHYVw=="
|
||||
},
|
||||
"node_modules/rehype": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.1.tgz",
|
||||
@@ -7715,12 +7792,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.16.3.tgz",
|
||||
"integrity": "sha512-GypUE+fEd06FqDs63LSAVlmq7WsahhPQU62cgZxGF+TJT5LjD2k7HTxXj4/CKOVuMM3+wWQ1t4Y5oooeJFRRBQ==",
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.14.1.tgz",
|
||||
"integrity": "sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "1.16.3",
|
||||
"@shikijs/vscode-textmate": "^9.2.0",
|
||||
"@shikijs/core": "1.14.1",
|
||||
"@types/hast": "^3.0.4"
|
||||
}
|
||||
},
|
||||
@@ -7942,6 +8018,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-outer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
|
||||
@@ -8163,11 +8250,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
|
||||
"integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg=="
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
@@ -8237,9 +8319,9 @@
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||
},
|
||||
"node_modules/tsconfck": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.3.tgz",
|
||||
"integrity": "sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz",
|
||||
"integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==",
|
||||
"bin": {
|
||||
"tsconfck": "bin/tsconfck.js"
|
||||
},
|
||||
@@ -9029,9 +9111,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.2.tgz",
|
||||
"integrity": "sha512-0/iAvbXyM3RiPPJ4lyD4w6Mjgtf4ejTK6TPvTNG3H32PLwuT0N/ZjJLiXug7ETE/LWtTeHw9WRv7uX/tIKYyKg==",
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
||||
"integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
|
||||
"peerDependencies": {
|
||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"astro": "^4.15.4",
|
||||
"astro": "^4.14.6",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dom-to-image": "^2.6.0",
|
||||
|
||||
230
pnpm-lock.yaml
generated
230
pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
||||
dependencies:
|
||||
'@astrojs/node':
|
||||
specifier: ^8.3.3
|
||||
version: 8.3.3(astro@4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))
|
||||
version: 8.3.3(astro@4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))
|
||||
'@astrojs/react':
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@5.4.2(@types/node@18.19.39))
|
||||
@@ -19,7 +19,7 @@ importers:
|
||||
version: 3.1.6
|
||||
'@astrojs/tailwind':
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(astro@4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))(tailwindcss@3.4.7)
|
||||
version: 5.1.0(astro@4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))(tailwindcss@3.4.7)
|
||||
'@fingerprintjs/fingerprintjs':
|
||||
specifier: ^4.4.3
|
||||
version: 4.4.3
|
||||
@@ -39,8 +39,8 @@ importers:
|
||||
specifier: ^18.3.0
|
||||
version: 18.3.0
|
||||
astro:
|
||||
specifier: ^4.15.4
|
||||
version: 4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4)
|
||||
specifier: ^4.14.6
|
||||
version: 4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4)
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -349,10 +349,6 @@ packages:
|
||||
resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.25.6':
|
||||
resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@emnapi/core@1.2.0':
|
||||
resolution: {integrity: sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==}
|
||||
|
||||
@@ -960,20 +956,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@shikijs/core@1.17.0':
|
||||
resolution: {integrity: sha512-Mkk4Mp4bNnW1kytU8I7S5PK5teNSe0iKlfqxPss4sdwnlcU8a2N62Z3te2gVmZfU9t1HF6L3wyWuM43IvEeEsg==}
|
||||
|
||||
'@shikijs/engine-javascript@1.17.0':
|
||||
resolution: {integrity: sha512-EiBVlxmzJZdC2ypzn8k+vxLngbBNgHLS4RilwrFOABGRc72kUZubbD/6Chrq2RcVtD3yq1GtiiIdFMGd9BTX3Q==}
|
||||
|
||||
'@shikijs/engine-oniguruma@1.17.0':
|
||||
resolution: {integrity: sha512-nsXzJGLQ0fhKmA4Gwt1cF7vC8VuZ1HSDrTRuj48h/qDeX/TzmOlTDXQ3uPtyuhyg/2rbZRzNhN8UFU4fSnQfXg==}
|
||||
|
||||
'@shikijs/types@1.17.0':
|
||||
resolution: {integrity: sha512-Tvu2pA69lbpXB+MmgIaROP1tio8y0uYvKb5Foh3q0TJBTAJuaoa5eDEtS/0LquyveacsiVrYF4uEZILju+7Ybg==}
|
||||
|
||||
'@shikijs/vscode-textmate@9.2.2':
|
||||
resolution: {integrity: sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==}
|
||||
'@shikijs/core@1.14.1':
|
||||
resolution: {integrity: sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==}
|
||||
|
||||
'@shuding/opentype.js@1.4.0-beta.0':
|
||||
resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==}
|
||||
@@ -1231,8 +1215,8 @@ packages:
|
||||
resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
astro@4.15.4:
|
||||
resolution: {integrity: sha512-wqy+m3qygt9DmCSqMsckxyK4ccCUFtti2d/WlLkEpAlqHgyDIg20zRTLHO2v/H4YeSlJ8sAcN0RW2FhOeYbINg==}
|
||||
astro@4.14.6:
|
||||
resolution: {integrity: sha512-MIDyNhtu3L4uakHvlTprh21eQPehYOtZSuSLtd+r6xZcl3lB+mlBz/hs1W3iHEQAORyJnKArWSY/aVOBKUyflA==}
|
||||
engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1654,6 +1638,10 @@ packages:
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
execa@8.0.1:
|
||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||
engines: {node: '>=16.17'}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1752,6 +1740,10 @@ packages:
|
||||
resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
get-stream@8.0.1:
|
||||
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==}
|
||||
|
||||
@@ -1820,9 +1812,6 @@ packages:
|
||||
hast-util-to-html@9.0.1:
|
||||
resolution: {integrity: sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==}
|
||||
|
||||
hast-util-to-html@9.0.2:
|
||||
resolution: {integrity: sha512-RP5wNpj5nm1Z8cloDv4Sl4RS8jH5HYa0v93YB6Wb4poEzgMo/dAAL0KcT4974dCjcNG5pkLqTImeFHHCwwfY3g==}
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
|
||||
|
||||
@@ -1859,6 +1848,10 @@ packages:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
human-signals@5.0.0:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
@@ -1934,6 +1927,10 @@ packages:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-stream@3.0.0:
|
||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
is-unicode-supported@1.3.0:
|
||||
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2058,9 +2055,6 @@ packages:
|
||||
magic-string@0.30.11:
|
||||
resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
|
||||
|
||||
magicast@0.3.5:
|
||||
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2117,6 +2111,9 @@ packages:
|
||||
memoize-one@5.2.1:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
|
||||
merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2226,6 +2223,10 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
mimic-fn@4.0.0:
|
||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
mimic-function@5.0.1:
|
||||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2313,6 +2314,10 @@ packages:
|
||||
engines: {node: ^18.18.0 || >=20.0.0, npm: '>=8.12.1'}
|
||||
hasBin: true
|
||||
|
||||
npm-run-path@5.3.0:
|
||||
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
@@ -2331,13 +2336,14 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
onetime@6.0.0:
|
||||
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
onetime@7.0.0:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
oniguruma-to-js@0.3.3:
|
||||
resolution: {integrity: sha512-m90/WEhgs8g4BxG37+Nu3YrMfJDs2YXtYtIllhsEPR+wP3+K4EZk6dDUvy2v2K4MNFDDOYKL4/yqYPXDqyozTQ==}
|
||||
|
||||
openai@4.53.2:
|
||||
resolution: {integrity: sha512-ohYEv6OV3jsFGqNrgolDDWN6Ssx1nFg6JDJQuaBFo4SL2i+MBoOQ16n2Pq1iBF5lH1PKnfCIOfqAGkmzPvdB9g==}
|
||||
hasBin: true
|
||||
@@ -2397,6 +2403,10 @@ packages:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-key@4.0.0:
|
||||
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
@@ -2636,9 +2646,6 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
regex@4.3.2:
|
||||
resolution: {integrity: sha512-kK/AA3A9K6q2js89+VMymcboLOlF5lZRCYJv3gzszXFHBr6kO6qLGzbm+UIugBEV8SMMKCTR59txoY6ctRHYVw==}
|
||||
|
||||
rehype-external-links@3.0.0:
|
||||
resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
|
||||
|
||||
@@ -2770,8 +2777,8 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shiki@1.17.0:
|
||||
resolution: {integrity: sha512-VZf8cPShRwfzPcaswv81+YP7qJEoFwRT+Ehy6bizim7M0zG9bk8Egug550C+xS9g7rKIOPhzAlp2uEyuCxbk/A==}
|
||||
shiki@1.14.1:
|
||||
resolution: {integrity: sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
@@ -2847,6 +2854,10 @@ packages:
|
||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
strip-final-newline@3.0.0:
|
||||
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-outer@1.0.1:
|
||||
resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2885,9 +2896,6 @@ packages:
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
tinyexec@0.3.0:
|
||||
resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==}
|
||||
|
||||
to-fast-properties@2.0.0:
|
||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2916,8 +2924,8 @@ packages:
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
tsconfck@3.1.3:
|
||||
resolution: {integrity: sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==}
|
||||
tsconfck@3.1.1:
|
||||
resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -3052,8 +3060,8 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
vitefu@1.0.2:
|
||||
resolution: {integrity: sha512-0/iAvbXyM3RiPPJ4lyD4w6Mjgtf4ejTK6TPvTNG3H32PLwuT0N/ZjJLiXug7ETE/LWtTeHw9WRv7uX/tIKYyKg==}
|
||||
vitefu@0.2.5:
|
||||
resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==}
|
||||
peerDependencies:
|
||||
vite: ^3.0.0 || ^4.0.0 || ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
@@ -3184,7 +3192,7 @@ snapshots:
|
||||
remark-parse: 11.0.0
|
||||
remark-rehype: 11.1.0
|
||||
remark-smartypants: 3.0.2
|
||||
shiki: 1.17.0
|
||||
shiki: 1.14.1
|
||||
unified: 11.0.5
|
||||
unist-util-remove-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
@@ -3193,9 +3201,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@astrojs/node@8.3.3(astro@4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))':
|
||||
'@astrojs/node@8.3.3(astro@4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))':
|
||||
dependencies:
|
||||
astro: 4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4)
|
||||
astro: 4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4)
|
||||
send: 0.18.0
|
||||
server-destroy: 1.0.1
|
||||
transitivePeerDependencies:
|
||||
@@ -3223,9 +3231,9 @@ snapshots:
|
||||
stream-replace-string: 2.0.0
|
||||
zod: 3.23.8
|
||||
|
||||
'@astrojs/tailwind@5.1.0(astro@4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))(tailwindcss@3.4.7)':
|
||||
'@astrojs/tailwind@5.1.0(astro@4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4))(tailwindcss@3.4.7)':
|
||||
dependencies:
|
||||
astro: 4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4)
|
||||
astro: 4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4)
|
||||
autoprefixer: 10.4.19(postcss@8.4.39)
|
||||
postcss: 8.4.39
|
||||
postcss-load-config: 4.0.2(postcss@8.4.39)
|
||||
@@ -3281,7 +3289,7 @@ snapshots:
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.24.7':
|
||||
dependencies:
|
||||
'@babel/types': 7.25.6
|
||||
'@babel/types': 7.25.4
|
||||
|
||||
'@babel/helper-compilation-targets@7.25.2':
|
||||
dependencies:
|
||||
@@ -3361,7 +3369,7 @@ snapshots:
|
||||
'@babel/helper-module-imports': 7.24.7
|
||||
'@babel/helper-plugin-utils': 7.24.8
|
||||
'@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2)
|
||||
'@babel/types': 7.25.6
|
||||
'@babel/types': 7.25.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3389,12 +3397,6 @@ snapshots:
|
||||
'@babel/helper-validator-identifier': 7.24.7
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
'@babel/types@7.25.6':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.24.8
|
||||
'@babel/helper-validator-identifier': 7.24.7
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
'@emnapi/core@1.2.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.0.1
|
||||
@@ -3869,32 +3871,9 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.21.1':
|
||||
optional: true
|
||||
|
||||
'@shikijs/core@1.17.0':
|
||||
'@shikijs/core@1.14.1':
|
||||
dependencies:
|
||||
'@shikijs/engine-javascript': 1.17.0
|
||||
'@shikijs/engine-oniguruma': 1.17.0
|
||||
'@shikijs/types': 1.17.0
|
||||
'@shikijs/vscode-textmate': 9.2.2
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.2
|
||||
|
||||
'@shikijs/engine-javascript@1.17.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 1.17.0
|
||||
oniguruma-to-js: 0.3.3
|
||||
regex: 4.3.2
|
||||
|
||||
'@shikijs/engine-oniguruma@1.17.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 1.17.0
|
||||
'@shikijs/vscode-textmate': 9.2.2
|
||||
|
||||
'@shikijs/types@1.17.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 9.2.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/vscode-textmate@9.2.2': {}
|
||||
|
||||
'@shuding/opentype.js@1.4.0-beta.0':
|
||||
dependencies:
|
||||
@@ -4184,15 +4163,18 @@ snapshots:
|
||||
|
||||
array-uniq@1.0.3: {}
|
||||
|
||||
astro@4.15.4(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4):
|
||||
astro@4.14.6(@types/node@18.19.39)(rollup@4.21.1)(typescript@5.5.4):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.10.3
|
||||
'@astrojs/internal-helpers': 0.4.1
|
||||
'@astrojs/markdown-remark': 5.2.0
|
||||
'@astrojs/telemetry': 3.1.0
|
||||
'@babel/core': 7.25.2
|
||||
'@babel/generator': 7.25.5
|
||||
'@babel/parser': 7.25.4
|
||||
'@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.25.2)
|
||||
'@babel/types': 7.25.6
|
||||
'@babel/traverse': 7.25.4
|
||||
'@babel/types': 7.25.4
|
||||
'@oslojs/encoding': 0.4.1
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.21.1)
|
||||
'@types/babel__core': 7.20.5
|
||||
@@ -4215,8 +4197,8 @@ snapshots:
|
||||
es-module-lexer: 1.5.4
|
||||
esbuild: 0.21.5
|
||||
estree-walker: 3.0.3
|
||||
execa: 8.0.1
|
||||
fast-glob: 3.3.2
|
||||
fastq: 1.17.1
|
||||
flattie: 1.1.1
|
||||
github-slugger: 2.0.0
|
||||
gray-matter: 4.0.3
|
||||
@@ -4225,7 +4207,6 @@ snapshots:
|
||||
js-yaml: 4.1.0
|
||||
kleur: 4.1.5
|
||||
magic-string: 0.30.11
|
||||
magicast: 0.3.5
|
||||
micromatch: 4.0.8
|
||||
mrmime: 2.0.0
|
||||
neotraverse: 0.6.18
|
||||
@@ -4237,15 +4218,14 @@ snapshots:
|
||||
prompts: 2.4.2
|
||||
rehype: 13.0.1
|
||||
semver: 7.6.3
|
||||
shiki: 1.17.0
|
||||
shiki: 1.14.1
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.1.0
|
||||
tinyexec: 0.3.0
|
||||
tsconfck: 3.1.3(typescript@5.5.4)
|
||||
tsconfck: 3.1.1(typescript@5.5.4)
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
vite: 5.4.2(@types/node@18.19.39)
|
||||
vitefu: 1.0.2(vite@5.4.2(@types/node@18.19.39))
|
||||
vitefu: 0.2.5(vite@5.4.2(@types/node@18.19.39))
|
||||
which-pm: 3.0.0
|
||||
xxhash-wasm: 1.0.2
|
||||
yargs-parser: 21.1.1
|
||||
@@ -4633,6 +4613,18 @@ snapshots:
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
execa@8.0.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
get-stream: 8.0.1
|
||||
human-signals: 5.0.0
|
||||
is-stream: 3.0.0
|
||||
merge-stream: 2.0.0
|
||||
npm-run-path: 5.3.0
|
||||
onetime: 6.0.0
|
||||
signal-exit: 4.1.0
|
||||
strip-final-newline: 3.0.0
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
dependencies:
|
||||
is-extendable: 0.1.1
|
||||
@@ -4727,6 +4719,8 @@ snapshots:
|
||||
|
||||
get-east-asian-width@1.2.0: {}
|
||||
|
||||
get-stream@8.0.1: {}
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
@@ -4853,20 +4847,6 @@ snapshots:
|
||||
stringify-entities: 4.0.4
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-html@9.0.2:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.2
|
||||
ccount: 2.0.1
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-whitespace: 3.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
property-information: 6.5.0
|
||||
space-separated-tokens: 2.0.2
|
||||
stringify-entities: 4.0.4
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -4916,6 +4896,8 @@ snapshots:
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -4972,6 +4954,8 @@ snapshots:
|
||||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
is-stream@3.0.0: {}
|
||||
|
||||
is-unicode-supported@1.3.0: {}
|
||||
|
||||
is-unicode-supported@2.0.0: {}
|
||||
@@ -5078,12 +5062,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
magicast@0.3.5:
|
||||
dependencies:
|
||||
'@babel/parser': 7.25.4
|
||||
'@babel/types': 7.25.6
|
||||
source-map-js: 1.2.0
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
semver: 6.3.1
|
||||
@@ -5222,6 +5200,8 @@ snapshots:
|
||||
|
||||
memoize-one@5.2.1: {}
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
micromark-core-commonmark@2.0.1:
|
||||
@@ -5433,6 +5413,8 @@ snapshots:
|
||||
|
||||
mime@1.6.0: {}
|
||||
|
||||
mimic-fn@4.0.0: {}
|
||||
|
||||
mimic-function@5.0.1: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
@@ -5494,6 +5476,10 @@ snapshots:
|
||||
|
||||
npm-check-updates@17.0.0: {}
|
||||
|
||||
npm-run-path@5.3.0:
|
||||
dependencies:
|
||||
path-key: 4.0.0
|
||||
|
||||
nth-check@2.1.1:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
@@ -5510,12 +5496,14 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
onetime@6.0.0:
|
||||
dependencies:
|
||||
mimic-fn: 4.0.0
|
||||
|
||||
onetime@7.0.0:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
oniguruma-to-js@0.3.3: {}
|
||||
|
||||
openai@4.53.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@types/node': 18.19.39
|
||||
@@ -5589,6 +5577,8 @@ snapshots:
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-key@4.0.0: {}
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
@@ -5773,8 +5763,6 @@ snapshots:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
regex@4.3.2: {}
|
||||
|
||||
rehype-external-links@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -6015,11 +6003,9 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
shiki@1.17.0:
|
||||
shiki@1.14.1:
|
||||
dependencies:
|
||||
'@shikijs/core': 1.17.0
|
||||
'@shikijs/types': 1.17.0
|
||||
'@shikijs/vscode-textmate': 9.2.2
|
||||
'@shikijs/core': 1.14.1
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
@@ -6088,6 +6074,8 @@ snapshots:
|
||||
|
||||
strip-bom@3.0.0: {}
|
||||
|
||||
strip-final-newline@3.0.0: {}
|
||||
|
||||
strip-outer@1.0.1:
|
||||
dependencies:
|
||||
escape-string-regexp: 1.0.5
|
||||
@@ -6151,8 +6139,6 @@ snapshots:
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tinyexec@0.3.0: {}
|
||||
|
||||
to-fast-properties@2.0.0: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
@@ -6173,7 +6159,7 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsconfck@3.1.3(typescript@5.5.4):
|
||||
tsconfck@3.1.1(typescript@5.5.4):
|
||||
optionalDependencies:
|
||||
typescript: 5.5.4
|
||||
|
||||
@@ -6303,7 +6289,7 @@ snapshots:
|
||||
'@types/node': 18.19.39
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitefu@1.0.2(vite@5.4.2(@types/node@18.19.39)):
|
||||
vitefu@0.2.5(vite@5.4.2(@types/node@18.19.39)):
|
||||
optionalDependencies:
|
||||
vite: 5.4.2(@types/node@18.19.39)
|
||||
|
||||
|
||||
@@ -184,36 +184,18 @@
|
||||
},
|
||||
"Dp2DOX10u2xJUjB8Okhzh": {
|
||||
"title": "Frame",
|
||||
"description": "**FrameLayout** is a simple ViewGroup subclass in Android that is designed to hold a single child view or a stack of overlapping child views. It positions each child in the top-left corner by default and allows them to overlap on top of each other, which makes it useful for situations where you need to layer views on top of one another.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Android developers: Frame Layout",
|
||||
"url": "https://developer.android.com/reference/android/widget/FrameLayout",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"U8iMGGOd2EgPxSuwSG39Z": {
|
||||
"title": "Linear",
|
||||
"description": "**LinearLayout** is a view group that aligns all children in a single directioni, vertically or horizontally. You can specify the layout direction with the `android:orientation` attribute.\n\n**LinearLayout** was commonly used in earlier Android development, but with the introduction of ConstraintLayout, it’s less frequently used in modern apps.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Android developers: Linear Layout",
|
||||
"url": "https://developer.android.com/develop/ui/views/layout/linear",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"yE0qAQZiEC9R8WvCdskpr": {
|
||||
"title": "Relative",
|
||||
"description": "A **RelativeLayout** in Android is a type of ViewGroup that allows you to position child views relative to each other or relative to the parent layout. It's a flexible layout where you can arrange the child views in relation to one another based on certain rules, making it suitable for creating complex UI designs.\n\n**RelativeLayout** was commonly used in earlier Android development, but with the introduction of `ConstraintLayout`, it's less frequently used in modern apps.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Android developers: Relative Layout",
|
||||
"url": "https://developer.android.com/develop/ui/views/layout/relative",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"3fFNMhQIuuh-NRzSXYpXO": {
|
||||
"title": "Constraint",
|
||||
@@ -412,14 +394,8 @@
|
||||
},
|
||||
"Bz-BkfzsDHAbAw3HD7WCd": {
|
||||
"title": "MVI",
|
||||
"description": "The **MVI** `Model-View-Intent` pattern is a reactive architectural pattern, similar to **MVVM** and **MVP**, focusing on immutability and handling states in unidirectional cycles. The data flow is unidirectional: Intents update the Model's state through the `ViewModel`, and then the View reacts to the new state. This ensures a clear and predictable cycle between logic and the interface.\n\n* Model: Represents the UI state. It is immutable and contains all the necessary information to represent a screen.\n* View: Displays the UI state and receives the user's intentions.\n* Intent: The user's intentions trigger state updates, managed by the `ViewModel`.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "MVI with Kotlin",
|
||||
"url": "https://proandroiddev.com/mvi-architecture-with-kotlin-flows-and-channels-d36820b2028d",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"pSU-NZtjBh-u0WKTYfjk_": {
|
||||
"title": "MVVM",
|
||||
@@ -620,19 +596,8 @@
|
||||
},
|
||||
"e3vHFaFFMV7kI9q6yf5e9": {
|
||||
"title": "Cloud Messaging",
|
||||
"description": "Firebase Cloud Messaging (FCM) is a powerful, battery-efficient messaging service that enables you to send messages reliably and securely to your Android applications. It enables you to send two types of messages: \"notification messages\" and \"data messages\". Notification messages are primarily meant for user notifications and will only be delivered when the application is in the foreground. On the other hand, data messages can handle even when the app is in the background or killed and can be used to send custom key-value pairs. FCM also supports various additional features, such as topic messaging to send messages to multiple devices subscribed to a common topic, device group messaging for sending messages to groups of user devices, and upstream messaging for sending messages from the client application to the FCM server.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Documentation",
|
||||
"url": "https://firebase.google.com/docs/cloud-messaging/android/client",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Firebase Cloud Messaging",
|
||||
"url": "https://www.youtube.com/watch?v=sioEY4tWmLI&list=PLl-K7zZEsYLkuHRCtHTpi6JYHka8oHLft",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
"description": "Firebase Cloud Messaging (FCM) is a powerful, battery-efficient messaging service that enables you to send messages reliably and securely to your Android applications. It enables you to send two types of messages: \"notification messages\" and \"data messages\". Notification messages are primarily meant for user notifications and will only be delivered when the application is in the foreground. On the other hand, data messages can handle even when the app is in the background or killed and can be used to send custom key-value pairs. FCM also supports various additional features, such as topic messaging to send messages to multiple devices subscribed to a common topic, device group messaging for sending messages to groups of user devices, and upstream messaging for sending messages from the client application to the FCM server.",
|
||||
"links": []
|
||||
},
|
||||
"3EEfKAd-ppIQpdQSEhbA1": {
|
||||
"title": "FireStore",
|
||||
|
||||
@@ -12,42 +12,12 @@
|
||||
},
|
||||
"DE3cMpeRYuUPw2ADtfS-3": {
|
||||
"title": "Angular Architecture",
|
||||
"description": "Angular follows a modular architecture pattern, dividing the application into distinct modules, components, services, and other elements, which enhances code organization and maintainability. The key building blocks include modules, which are containers grouping related components, services, directives, and other elements to ensure proper encapsulation and reusability. Components are the building blocks of Angular applications, representing parts of the user interface with associated logic, consisting of templates, styles, and a class defining behavior. Services encapsulate reusable business logic, data manipulation, and API communication, enabling data and functionality sharing across components. Directives are HTML attributes or elements that extend HTML functionality, allowing reusable behaviors across the application. Lastly, pipes transform data before displaying it in templates, providing convenient ways to format, filter, and sort data.\n\nVisit the following resources to learn more:",
|
||||
"description": "Visit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Angular coding style guide",
|
||||
"url": "https://angular.dev/style-guide",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "The Ultimate Guide to Angular Architecture: Best Practices for efficient coding with Angular Framework",
|
||||
"url": "https://angulardive.com/blog/the-ultimate-guide-to-angular-architecture-best-practices-for-efficient-coding-with-angular-framework/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Modern Architectures with Angular Part 1: Strategic design with Sheriff and Standalone Components",
|
||||
"url": "https://www.angulararchitects.io/en/blog/modern-architectures-with-angular-part-1-strategic-design-with-sheriff-and-standalone-components/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Optimizing the architecture of large web applications with Angular",
|
||||
"url": "https://albertobasalo.medium.com/optimizing-the-architecture-of-large-web-applications-with-angular-79d03b01a92b",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Angular Architecture Concepts and Patterns",
|
||||
"url": "https://www.bigscal.com/blogs/frontend/angular-architecture-concepts-and-patterns/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Top 10 Angular Architecture Mistakes",
|
||||
"url": "https://angularexperts.io/blog/top-10-angular-architecture-mistakes",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Architecting Angular: A Guide to effective project structure",
|
||||
"url": "https://medium.com/@nile.bits/architecting-angular-a-guide-to-effective-project-structure-9756bae92262",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -886,7 +856,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "Understanding Pipes",
|
||||
"url": "https://angular.dev/tutorials/learn-angular/22-pipes",
|
||||
"url": "https://angular.dev/guide/pipes",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1355,12 +1355,12 @@
|
||||
"description": "Parallel processing is an efficient form of data processing that allows Data Analysts to deal with larger volumes of data at a faster pace. It is a computational method that allows multiple tasks to be performed concurrently, instead of sequentially, thus, speeding up data processing. Parallel processing proves to be invaluable for Data Analysts, as they are often tasked with analyzing huge data sets and compiling reports in real-time. As the demand for rapid data processing and quick analytics is on the rise, the technique of parallel processing forms a critical element in the versatile toolkit of a Data Analyst.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What Is Parallel Processing?",
|
||||
"title": "What is parallel processing?",
|
||||
"url": "https://www.spiceworks.com/tech/iot/articles/what-is-parallel-processing/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "How Parallel Computing Works",
|
||||
"title": "How parallel computing works?",
|
||||
"url": "https://computer.howstuffworks.com/parallel-processing.htm",
|
||||
"type": "article"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -358,45 +358,13 @@
|
||||
},
|
||||
"KdFYmj36M2jrGfsYkukpo": {
|
||||
"title": "IDEs",
|
||||
"description": "The concept of Integrated Development Environments (IDEs) traces its roots back to the 1960s and 1970s when basic text editors and simple programming tools were used in early computing. The first notable IDEs emerged with the rise of Unix systems in the 1970s, such as the EMACS text editor, which included features like code editing and debugging. Today, IDEs have become essential for developers, supporting multiple programming languages and integrating cloud-based tools, continuous integration, and real-time collaboration. IDEs like Visual Studio Code, IntelliJ IDEA, and Eclipse are widely adopted by developers across industries.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "History of IDEs",
|
||||
"url": "https://multiqos.com/blogs/guide-to-integrated-development-environment/#:~:text=While%20TurboPascal%20may%20have%20popularized,significant%20popularity%20in%20the%201980s.",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Visual Studio Code",
|
||||
"url": "https://code.visualstudio.com/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "JetBrains IDEs",
|
||||
"url": "https://www.jetbrains.com/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"j5nNSYI8s-cH8EA6G1EWY": {
|
||||
"title": "VS Code",
|
||||
"description": "Visual Studio Code (VSCode) was first announced by Microsoft in 2015 and quickly became one of the most popular and widely used Integrated Development Environments (IDEs). Built on [Electron](https://www.electronjs.org/), a framework that allows web technologies like JavaScript, HTML, and CSS to create desktop applications, VSCode offers a lightweight and highly extensible platform for developers. VSCode focuses on being a streamlined code editor with the ability to install extensions that add features such as debugging, version control, and language-specific tooling. Microsoft's vision was to create a flexible environment that could cater to all types of developers, from beginners to seasoned professionals.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "video@FreeCodeCamp Crash Course",
|
||||
"url": "https://www.youtube.com/watch?v=WPqXP_kLzpo",
|
||||
"type": "course"
|
||||
},
|
||||
{
|
||||
"title": "video@VS Code basics",
|
||||
"url": "https://www.youtube.com/watch?v=B-s71n0dHUk",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Visual Studio Code",
|
||||
"url": "https://code.visualstudio.com/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"NCnKS435DCl-8vilr1_XE": {
|
||||
"title": "JetBrains IDEs",
|
||||
|
||||
@@ -2470,11 +2470,6 @@
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Building a simple offline-capable Notepad app ",
|
||||
"url": "https://www.amitmerchant.com/Building-Simple-Offline-Notepad-Using-Service-Worker/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Explore top posts about Web Development",
|
||||
"url": "https://app.daily.dev/tags/webdev?ref=roadmapsh",
|
||||
|
||||
@@ -378,17 +378,12 @@
|
||||
"links": []
|
||||
},
|
||||
"AaRZiItRcn8fYb5R62vfT": {
|
||||
"title": "GDScript",
|
||||
"description": "GDScript is a high-level, dynamically-typed programming language designed specifically for the Godot Engine, an open-source game development platform. It is tailored for ease of use and rapid development of game logic and functionality. GDScript features a syntax similar to Python, which simplifies learning and coding for developers familiar with Python, while providing direct access to Godot's rich set of built-in functions and game-specific APIs. The language integrates closely with Godot's scene system and scripting environment, enabling developers to create and manipulate game objects, handle input, and control game behavior efficiently.\n\nLearn more from the following resources:",
|
||||
"title": "Assembly",
|
||||
"description": "**Assembly** is a low-level programming language, often used for direct hardware manipulation, real-time systems, and to write performance-critical code. It provides a strong correspondence between its instructions and the architecture's machine-code instructions, since it directly represents the specific commands of the computer's CPU structure. However, it's closer to machine language (binary code) than to human language, which makes it difficult to read and understand. The syntax varies greatly, which depends upon the CPU architecture for which it's designed, thus Assembly language written for one type of processor can't be used on another. Despite its complexity, time-intensive coding process and machine-specific nature, Assembly language is still utilized for speed optimization and hardware manipulation where high-level languages may not be sufficient.",
|
||||
"links": [
|
||||
{
|
||||
"title": "GDScript Website",
|
||||
"url": "https://gdscript.com/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "How to program in Godot - GDScript Tutorial",
|
||||
"url": "https://www.youtube.com/watch?v=e1zJS31tr88",
|
||||
"title": "Code walkthrough of a game written in x64 assembly",
|
||||
"url": "https://www.youtube.com/watch?v=WUoqlp30M78",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
@@ -462,7 +457,7 @@
|
||||
},
|
||||
"odfZWKtPbb-lC35oeTCNV": {
|
||||
"title": "Specular",
|
||||
"description": "`Specular` reflections are mirror-like reflections. In these cases, the rays of light are reflected, more than they are absorbed. The angle of incidence is equal to the angle of reflection, that is to say that the angle at which the light enters the medium and then bounces off, the angle of the beam that bounced off would be the same.\n\nLearn more from the following resources:\n\n\\-[@video@Specular reflection](https://www.youtube.com/watch?v=2cFvJkc4pQk)",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"THMmnx8p_P0X-dSPoHvst": {
|
||||
@@ -472,35 +467,13 @@
|
||||
},
|
||||
"iBZ1JsEWI0xuLgUvfWfl-": {
|
||||
"title": "Texture",
|
||||
"description": "`Texture` is the visual quality of an object. Where the `mesh` determines the shape or `topology` of an object, the texture describes the quality of said object. For instance, if there is a spherical mesh, is it supposed to be shiny? is it supposed to be rough? is it supposed to be of rock or of wood? questions of this ilk are often resolved using textures. Textures are often just 2D images that are wrapped onto 3D meshes. The 3D mesh is first divided into segments and unfurled; the 3D meshes are converted into 2D chunks, this process is known as `UV Unwrapping`. Once a mesh has been unwrapped, the textures in the form of an image are applied to the 2D chunks of the 3D mesh, this way the texture knows how to properly wrap around the mesh and avoid any conflicts. Textures determine the visual feel and aesthetics of the game.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "How Nintendo textures work",
|
||||
"url": "https://www.youtube.com/watch?v=WrCMzHngLxI",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "How Pixar textures work",
|
||||
"url": "https://www.youtube.com/watch?v=o_I6jxlN-Ck",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"r4UkMd5QURbvJ3Jlr_H9H": {
|
||||
"title": "Bump",
|
||||
"description": "`Bump` is very similar to texture. It is, as a matter of fact, a type of texture itself. If you take the texture of a bricked wall, it will becoming increasingly obvious that the amount of detail present inside the wall, if geometrically processed would be incredibly demanding and wasteful. In order to combat this ineffeciency, the `bump` maps were created. Traditionally, a flat texture would just be an image of something called a `color map`, that is to say, where does each individual color of the pixel should be to represent a texture. When you take the picture of your floor, wall, or any object, that image in essence is the color map. The bump map is different as it informs the texture about it's `normal` values. So, if you take a flat 2D mesh and apply a bump map on it, it will render the same 2D mesh with all the normal values baked into the flat 2D mesh, creating a graphically effect mimicking 3-dimensionality.\n\nThere is also something known as a normal map, and displacement maps.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Normals, Normal maps and Bump maps",
|
||||
"url": "https://www.youtube.com/watch?v=l5PYyzsZED8",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "Bump, normal and displacement",
|
||||
"url": "https://www.youtube.com/watch?v=43Ilra6fNGc",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"YGeGleEN203nokiZIYJN8": {
|
||||
"title": "Parallax",
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
"url": "https://www.datacamp.com/blog/all-about-git",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Version Control (Git) - The Missing Semester of Your CS Education",
|
||||
"url": "https://missing.csail.mit.edu/2020/version-control/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "GUI Clients",
|
||||
"url": "https://git-scm.com/downloads/guis",
|
||||
@@ -220,11 +215,6 @@
|
||||
"title": ".gitignore",
|
||||
"description": "Ignored files are tracked in a special file named `.gitignore` that is checked in at the root of your repository. There is no explicit git ignore command: instead the `.gitignore` file must be edited and committed by hand when you have new files that you wish to ignore. `.gitignore` files contain patterns that are matched against file names in your repository to determine whether or not they should be ignored.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "gitignore - A collection of useful .gitignore templates",
|
||||
"url": "https://github.com/github/gitignore",
|
||||
"type": "opensource"
|
||||
},
|
||||
{
|
||||
"title": "gitignore Documentation",
|
||||
"url": "https://git-scm.com/docs/gitignore/en",
|
||||
@@ -972,21 +962,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"lONqOqD-4slxa9B5i9ADX": {
|
||||
"github-wikis@lONqOqD-4slxa9B5i9ADX.md": {
|
||||
"title": "GitHub Wikis",
|
||||
"description": "GitHub Wikis are collaborative documentation spaces integrated directly into GitHub repositories. They provide a platform for teams to create, edit, and organize project-related information, such as documentation, guidelines, and FAQs. Wikis support Markdown formatting, making it easy to structure content and include images or links. With version control and the ability to clone wiki repositories, teams can collaboratively maintain up-to-date documentation alongside their code, enhancing project understanding and facilitating knowledge sharing among contributors and users.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "About Wikis",
|
||||
"url": "https://docs.github.com/en/communities/documenting-your-project-with-wikis/about-wikis",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Documenting your project with Wikis",
|
||||
"url": "https://docs.github.com/en/communities/documenting-your-project-with-wikis",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"i3AbARgzQtxtlB-1AS8zv": {
|
||||
"title": "Clean Git History",
|
||||
@@ -1437,22 +1416,12 @@
|
||||
},
|
||||
"BKVA6Q7DXemAYjyQOA0nh": {
|
||||
"title": "git filter-branch",
|
||||
"description": "You can use `git filter-branch` to rewrite Git revision history by applying custom filters on each revision.\n\n* Filter types: You can modify trees (e.g., removing a file or running a Perl script) or information about each commit.\n* Preserving original data: The command preserves all original commit times, merge information, and other details unless specified otherwise.\n* Rewriting specific branches: Only the positive refs mentioned in the command line are rewritten; if no filters are specified, commits are recommitted without changes.\n\nNotably, there exists a simpler, safer, and more powerful alternative: `git filter-repo`. This tool is actively promoted by Git and offers a streamlined approach to filtering revisions, making it a preferred choice for rewriting your Git history, especially when managing large repositories.\n\nVisit the following resources to learn more:",
|
||||
"description": "You can use `git filter-branch` to rewrite Git revision history by applying custom filters on each revision.\n\n* Filter types: You can modify trees (e.g., removing a file or running a Perl script) or information about each commit.\n* Preserving original data: The command preserves all original commit times, merge information, and other details unless specified otherwise.\n* Rewriting specific branches: Only the positive refs mentioned in the command line are rewritten; if no filters are specified, commits are recommitted without changes.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "git filter-branch",
|
||||
"url": "https://git-scm.com/docs/git-filter-branch",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "git filter-repo",
|
||||
"url": "https://github.com/newren/git-filter-repo",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Removing sensitive data from a repository",
|
||||
"url": "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2219,7 +2188,7 @@
|
||||
},
|
||||
"qrdOARfqGPF9xhF6snbAn": {
|
||||
"title": "OAuth Apps",
|
||||
"description": "GitHub OAuth Apps allow developers to integrate with GitHub using OAuth 2.0 authentication. They enable secure, token-based access to specific GitHub resources like repositories, issues, and pull requests. OAuth Apps can automate tasks, personalize interactions, and provide real-time notifications through webhooks, all while allowing users to approve only the necessary permissions without sharing their credentials.\n\nVisit the following resources to learn more:",
|
||||
"description": "GitHub OAuth Apps are a way to integrate with the GitHub platform using OAuth authentication. They allow developers to create custom integrations that can automate tasks, provide real-time notifications, and build custom workflows.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Creating an OAuth app",
|
||||
|
||||
@@ -965,11 +965,6 @@
|
||||
"title": "SwiftUI",
|
||||
"description": "SwiftUI is Apple's modern declarative framework for building user interfaces across all Apple platforms. Introduced in 2019, it allows developers to create UIs using Swift code, describing the desired layout and behavior rather than implementing them imperatively. SwiftUI offers a more concise and intuitive approach to UI development, with features like automatic support for Dark Mode, dynamic type, and localization. It uses a state-driven approach, automatically updating the UI when underlying data changes. While newer than UIKit, SwiftUI is rapidly evolving and gaining adoption, offering seamless integration with UIKit when needed.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "HackingWithSwift - 100 Days of SwiftUI",
|
||||
"url": "https://www.hackingwithswift.com/100/swiftui",
|
||||
"type": "course"
|
||||
},
|
||||
{
|
||||
"title": "SwiftUI Documentation",
|
||||
"url": "https://developer.apple.com/xcode/swiftui/",
|
||||
|
||||
@@ -429,11 +429,6 @@
|
||||
"title": "String",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "JavaScript Strings",
|
||||
"url": "https://javascript.info/string",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2077,11 +2072,6 @@
|
||||
"title": "Network request - Fetch",
|
||||
"url": "https://javascript.info/fetch",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Abort a fetch request manually in JavaScript",
|
||||
"url": "https://www.amitmerchant.com/abort-fetch-request-manually-in-javascript/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -351,13 +351,8 @@
|
||||
},
|
||||
"UljuqA89_SlCSDWWMD_C_": {
|
||||
"title": "Spark",
|
||||
"description": "Apache Spark is an open-source distributed computing system designed for big data processing and analytics. It offers a unified interface for programming entire clusters, enabling efficient handling of large-scale data with built-in support for data parallelism and fault tolerance. Spark excels in processing tasks like batch processing, real-time data streaming, machine learning, and graph processing. It’s known for its speed, ease of use, and ability to process data in-memory, significantly outperforming traditional MapReduce systems. Spark is widely used in big data ecosystems for its scalability and versatility across various data processing tasks.\n\nVisit the following resources to learn more:",
|
||||
"description": "Apache Spark is an open-source distributed computing system used for big data processing and analytics. It provides an interface for programming entire clusters with implicit data parallelism and fault tolerance.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "ApacheSpark",
|
||||
"url": "https://spark.apache.org/documentation.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Spark By Examples",
|
||||
"url": "https://sparkbyexamples.com",
|
||||
@@ -393,7 +388,7 @@
|
||||
},
|
||||
"o6GQ3-8DgDtHzdX6yeg1w": {
|
||||
"title": "Flink",
|
||||
"description": "Apache Flink is an open-source stream processing framework designed for real-time and batch data processing with low latency and high throughput. It supports event time processing, fault tolerance, and stateful operations, making it ideal for applications like real-time analytics, fraud detection, and event-driven systems. Flink is highly scalable, integrates with various data systems, and is widely used in industries for large-scale, real-time data processing tasks.\n\nVisit the following resources to learn more:",
|
||||
"description": "Apache Flink is a distributed stream processing framework that is used to process large amounts of data in real-time. It is designed to be highly scalable and fault-tolerant. Flink is built on top of the Apache Kafka messaging system and is used to process data streams in real-time.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Apache Flink Documentation",
|
||||
@@ -404,11 +399,6 @@
|
||||
"title": "Explore top posts about Apache Flink",
|
||||
"url": "https://app.daily.dev/tags/apache-flink?ref=roadmapsh",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Apache Flink Tutorialpoint",
|
||||
"url": "https://www.tutorialspoint.com/apache_flink/apache_flink_introduction.htm",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2085,58 +2085,7 @@
|
||||
},
|
||||
"M62lAWBOrTe99TfpFOQ-Y": {
|
||||
"title": "Common Built-in Modules",
|
||||
"description": "These are the core modules that come with `Node.js` out of the box. This module provides tools or APIs for performing out certain standard `Node.js` operations. like interacting with the file system, url parsing, or logging information to the console.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Nodejs fs module",
|
||||
"url": "https://nodejs.org/api/fs.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs url module",
|
||||
"url": "https://nodejs.org/api/url.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs console module",
|
||||
"url": "https://nodejs.org/api/console.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs util module",
|
||||
"url": "https://nodejs.org/api/util.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs events module",
|
||||
"url": "https://nodejs.org/api/events.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs os module",
|
||||
"url": "https://nodejs.org/api/os.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs worker threads module",
|
||||
"url": "https://nodejs.org/api/worker_threads.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs child process module",
|
||||
"url": "https://nodejs.org/api/child_process.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs process object",
|
||||
"url": "https://nodejs.org/api/process.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nodejs crypto module",
|
||||
"url": "https://nodejs.org/api/crypto.html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "These are the common modules that come with `Node.js` out of the box. This module provides tools or APIs for performing out certain standard `Node.js` operations. like interacting with the file system, url parsing, or logging information to the console.",
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,7 @@
|
||||
"lDIy56RyC1XM7IfORsSLD": {
|
||||
"title": "Introduction",
|
||||
"description": "PostgreSQL is a powerful, open-source Object-Relational Database Management System (ORDBMS) that is known for its robustness, extensibility, and SQL compliance. It was initially developed at the University of California, Berkeley, in the 1980s and has since become one of the most popular open-source databases in the world.",
|
||||
"links": [
|
||||
{
|
||||
"title": "History of POSTGRES to PostgreSQL",
|
||||
"url": "https://www.postgresql.org/docs/current/history.html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"links": []
|
||||
},
|
||||
"soar-NBWCr4xVKj7ttfnc": {
|
||||
"title": "What are Relational Databases?",
|
||||
@@ -312,12 +306,7 @@
|
||||
"description": "Multi-Version Concurrency Control (MVCC) is a technique used by PostgreSQL to allow multiple transactions to access the same data concurrently without conflicts or delays. It ensures that each transaction has a consistent snapshot of the database and can operate on its own version of the data.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Intro to MVCC",
|
||||
"url": "https://www.postgresql.org/docs/current/mvcc-intro.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Multiversion concurrency control - Wikipedia",
|
||||
"title": "",
|
||||
"url": "https://en.wikipedia.org/wiki/Multiversion_concurrency_control",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -769,11 +758,6 @@
|
||||
"title": "PostgreSQL INTERSECT Operator",
|
||||
"url": "https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-intersect/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "PostgreSQL EXCEPT Operator",
|
||||
"url": "https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-except/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -336,6 +336,11 @@
|
||||
"url": "https://www.programiz.com/dsa",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "DSA Course by Google",
|
||||
"url": "https://www.udacity.com/course/data-structures-and-algorithms-in-python--ud513",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Explore top posts about Algorithms",
|
||||
"url": "https://app.daily.dev/tags/algorithms?ref=roadmapsh",
|
||||
|
||||
@@ -1155,21 +1155,10 @@
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"6FDGecsHbqY-cm32yTZJa": {
|
||||
"functional-programming@6FDGecsHbqY-cm32yTZJa.md": {
|
||||
"title": "Functional Programming",
|
||||
"description": "Functional programming is a programming paradigm designed to handle pure mathematical functions. This paradigm is totally focused on writing more compounded and pure functions.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Functional Programming with JavaScript",
|
||||
"url": "https://www.telerik.com/blogs/functional-programming-javascript",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Learning Functional Programming",
|
||||
"url": "https://youtube.com/watch?v=e-5obm1G_FY",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"mCiYCbKIOVU34qil_q7Hg": {
|
||||
"title": "React, Vue, Angular",
|
||||
@@ -1448,29 +1437,8 @@
|
||||
},
|
||||
"PKqwKvoffm0unwcFwpojk": {
|
||||
"title": "Scrum",
|
||||
"description": "`Scrum` is a popular agile framework used for project management, particularly in software development. It emphasizes iterative development, collaboration, and flexibility to deliver high-quality products.\n\nKey elements of Scrum:\n\n* **Sprints**: Time-boxed iterations (usually 2-4 weeks) where teams work on specific goals.\n* **Product Backlog**: Prioritized list of features or requirements for the product.\n* **Sprint Backlog**: Selected items from the Product Backlog to be completed during a Sprint.\n* **Daily Scrum (Stand-up)**: Brief daily meeting where team members share progress, challenges, and plans for the day.\n* **Sprint Review**: Meeting at the end of a Sprint to demonstrate completed work and gather feedback.\n* **Sprint Retrospective**: Meeting to reflect on the Sprint, identify improvements, and adjust processes for the next Sprint.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is scrum and how to get started",
|
||||
"url": "https://www.atlassian.com/agile/scrum.",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Scrum Methodology: The Complete Guide & Best Practices",
|
||||
"url": "https://thedigitalprojectmanager.com/projects/pm-methodology/scrum-methodology-complete-guide/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Essential Topics for the Scrum Product Owner",
|
||||
"url": "https://www.scrum.org/resources/blog/essential-topics-scrum-product-owner",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Scrum • Topics - Thriving Technologist",
|
||||
"url": "https://thrivingtechnologist.com/topics/scrum/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"7fL9lSu4BD1wRjnZy9tM9": {
|
||||
"title": "XP",
|
||||
@@ -1511,24 +1479,8 @@
|
||||
},
|
||||
"UCCT7-E_QUKPg3jAsjobx": {
|
||||
"title": "TCP/IP Model",
|
||||
"description": "The `TCP/IP model` defines how devices should transmit data between them and enables communication over networks and large distances. The model represents how data is exchanged and organized over networks. It is split into four layers, which set the standards for data exchange and represent how data is handled and packaged when being delivered between applications, devices, and servers.\n\n* **Network Access Layer**: The network access layer is a group of applications requiring network communications. This layer is responsible for generating the data and requesting connections.\n \n* **Internet Layer**: The internet layer is responsible for sending packets from a network and controlling their movement across a network to ensure they reach their destination.\n \n* **Transport Layer**: The transport layer is responsible for providing a solid and reliable data connection between the original application or device and its intended destination.\n \n* **Application Layer**: The application layer refers to programs that need TCP/IP to help them communicate with each other.\n \n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is Transmission Control Protocol TCP/IP? - Fortinet",
|
||||
"url": "https://www.fortinet.com/resources/cyberglossary/tcp-ip#:~:text=The%20TCP%2FIP%20model%20defines,exchanged%20and%20organized%20over%20networks.",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "TCP/IP Model",
|
||||
"url": "https://www.geeksforgeeks.org/tcp-ip-model/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is TCP/IP and How Does it Work?",
|
||||
"url": "https://www.techtarget.com/searchnetworking/definition/TCP-IP",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"Nq6o6Ty8VyNRsvg-UWp7D": {
|
||||
"title": "HTTP, HTTPS",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"R9DQNc0AyAQ2HLpP4HOk6": {
|
||||
"title": "What are Relational Databases?",
|
||||
"description": "Relational databases are a type of database management system (DBMS) that stores and provides access to data points that are related to one another. Based on the relational model introduced by E.F. Codd in 1970, they use a structure that allows data to be organized into tables with rows and columns. Key features include:\n\n* Use of SQL (Structured Query Language) for querying and managing data\n* Support for ACID transactions (Atomicity, Consistency, Isolation, Durability)\n* Enforcement of data integrity through constraints (e.g., primary keys, foreign keys)\n* Ability to establish relationships between tables, enabling complex queries and data retrieval\n* Scalability and support for multi-user environments\n\nExamples of popular relational database systems include MySQL, PostgreSQL, Oracle, and Microsoft SQL Server. They are widely used in various applications, from small-scale projects to large enterprise systems, due to their reliability, consistency, and powerful querying capabilities.\n\nLearn more from the following resources:",
|
||||
"description": "Relational databases are a type of database management system (DBMS) that stores and provides access to data points that are related to one another. Based on the relational model introduced by E.F. Codd in 1970, they use a structure that allows data to be organized into tables with rows and columns. Key features include:\n\n* Use of SQL (Structured Query Language) for querying and managing data\n* Support for ACID transactions (Atomicity, Consistency, Isolation, Durability)\n* Enforcement of data integrity through constraints (e.g., primary keys, foreign keys)\n* bility to establish relationships between tables, enabling complex queries and data retrieval\n* Scalability and support for multi-user environments\n\nExamples of popular relational database systems include MySQL, PostgreSQL, Oracle, and Microsoft SQL Server. They are widely used in various applications, from small-scale projects to large enterprise systems, due to their reliability, consistency, and powerful querying capabilities.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is a relational database - AWS",
|
||||
|
||||
@@ -449,14 +449,8 @@
|
||||
},
|
||||
"fm8oUyNvfdGWTgLsYANUr": {
|
||||
"title": "Environment Variables",
|
||||
"description": "Environment variables can be used to customize various aspects of Terraform. You can set these variables to change the default behaviour of terraform such as increase verbosity, update log file path, set workspace, etc. Envrionment variables are optional and terraform does not need them by default.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Environment Variables",
|
||||
"url": "https://developer.hashicorp.com/terraform/cli/config/environment-variables",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"rdphcVd-Vq972y4H8CxIj": {
|
||||
"title": "Variable Definition File",
|
||||
@@ -476,30 +470,13 @@
|
||||
},
|
||||
"U2n2BtyUrOFLnw9SZYV_w": {
|
||||
"title": "Validation Rules",
|
||||
"description": "Validation rules can be used to specify custom validations to a variable. The motive of adding validation rules is to make the variable comply with the rules. The validation rules can be added using a `validation` block.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Custom Validation Rules",
|
||||
"url": "https://developer.hashicorp.com/terraform/language/values/variables#custom-validation-rules",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"1mFih8uFs3Lc-1PLgwiAU": {
|
||||
"title": "Local Values",
|
||||
"description": "Local values can be understood as a name assigned to any expression to use it multiple times directly by the name in your terraform module. Local values are referred to as locals and can be declared using the `locals` block. Local values can be a literal constants, resource attributes, variables, or other local values. Local values are helpful to define expressions or values that you need to use multiple times in the module as it allows the value to be updated easily just by updating the local value. A local value can be accessed using the `local` argument like `local.<value_name>`.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Local Values",
|
||||
"url": "https://developer.hashicorp.com/terraform/language/values/locals",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "@Article@Terraform Locals",
|
||||
"url": "https://spacelift.io/blog/terraform-locals",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"7GK4fQf1FRKrZgZkxNahj": {
|
||||
"title": "Outputs",
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
"url": "https://www.businessballs.com/improving-workplace-performance/nudge-theory/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Nudge Theory Explained with Examples (on YouTube)",
|
||||
"url": "https://www.youtube.com/watch?v=u3yxxteiyya&ab_channel=epm",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "Nudge Theory Explained in less than 10 minutes",
|
||||
"url": "https://youtu.be/fA5eGIMZTRQ",
|
||||
@@ -53,7 +58,7 @@
|
||||
},
|
||||
"2NlgbLeLBYwZX2u2rKkIO": {
|
||||
"title": "BJ Fogg's Behavior Model",
|
||||
"description": "B.J. Fogg, a renowned psychologist, and researcher at Stanford University, proposed the [Fogg Behavior Model (FBM)](https://www.behaviormodel.org/). This insightful model helps UX designers understand and influence user behavior by focusing on three core elements. These key factors are motivation, ability, and prompts.\n\n* **Motivation**: This element emphasizes the user's desire to perform a certain action or attain specific outcomes. Motivation can be linked to three core elements specified as sensation (pleasure/pain), anticipation (hope/fear), and social cohesion (belonging/rejection).\n \n* **Ability**: Ability refers to the user's capacity, both physical and mental, to perform desired actions. To enhance the ability of users, UX designers should follow the principle of simplicity. The easier it is to perform an action, the more likely users will engage with the product. Some factors to consider are time, financial resources, physical efforts, and cognitive load.\n \n* **Prompts**: Prompts are the cues, notifications, or triggers that signal users to take an action. For an action to occur, prompts should be presented at the right time when the user has adequate motivation and ability.\n \n\nUX designers should strive to find the balance between these three factors to facilitate the desired user behavior. By understanding your audience and their needs, implementing clear and concise prompts, and minimizing the effort required for action, the FBM can be an effective tool for designing user-centered products.",
|
||||
"description": "B.J. Fogg, a renowned psychologist, and researcher at Stanford University, proposed the [Fogg Behavior Model (FBM)](https://www.behaviormodel.org/). This insightful model helps UX designers understand and influence user behavior by focusing on three core elements. These key factors are motivation, ability, and triggers.\n\n* **Motivation**: This element emphasizes the user's desire to perform a certain action or attain specific outcomes. Motivation can be linked to three core elements specified as sensation (pleasure/pain), anticipation (hope/fear), and social cohesion (belonging/rejection).\n \n* **Ability**: Ability refers to the user's capacity, both physical and mental, to perform desired actions. To enhance the ability of users, UX designers should follow the principle of simplicity. The easier it is to perform an action, the more likely users will engage with the product. Some factors to consider are time, financial resources, physical efforts, and cognitive load.\n \n* **Triggers**: Triggers are the cues, notifications, or prompts that signal users to take an action. For an action to occur, triggers should be presented at the right time when the user has adequate motivation and ability.\n \n\nUX designers should strive to find the balance between these three factors to facilitate the desired user behavior. By understanding your audience and their needs, implementing clear and concise triggers, and minimizing the effort required for action, the FBM can be an effective tool for designing user-centered products.",
|
||||
"links": [
|
||||
{
|
||||
"title": "meaning of BJ fogg's behavior model",
|
||||
@@ -64,7 +69,7 @@
|
||||
},
|
||||
"kcG4IpneJzA6di0uqTiwb": {
|
||||
"title": "CREATE Action Funnel",
|
||||
"description": "Stephen Wendel's CREATE Action Funnel is a behavioral design framework aimed at helping individuals or organizations encourage specific behaviors in others, especially in the context of product design. It breaks down the process of motivating action into six key stages. Each stage helps identify where users might drop off or face barriers, allowing designers or strategists to address these pain points effectively. These stages are:\n\n* **CUE:** The user must notice a cue or prompt that tells them to act. This could be a notification, a visual element, or any kind of reminder.\n \n* **REACTION:** The user must react positively to the cue. This stage involves emotional and cognitive processing, where the individual decides if the action is relevant or attractive.\n \n* **EVALUATION:** The user evaluates whether the action is worth their time, energy, or resources. They assess the benefits versus the effort required.\n \n* **ABILITY:** The user must feel capable of taking the action. This involves ensuring that the action is easy enough to do and aligns with their skills and resources.\n \n* **TIMING:** The action needs to happen at the right time. Users need to have the opportunity and be in the right context to act.\n \n* **EXECUTION:** Finally, the action must be carried out successfully. This is the stage where the behavior is completed.\n \n\nThe CREATE Action Funnel is helpful for product designers, marketers, or behavior change professionals, as it provides a structured way to understand user actions and design interventions to improve completion rates. It identifies and solves the gaps that occur between intention and action.",
|
||||
"description": "Stephen Wendell's Create Action Funnel is a UX design framework focused on converting website visitors into active customers through a systematic and engaging process. The approach emphasizes on understanding user behavior, catering to their needs, and directing them towards specific actions. The Action Funnel consists of four major steps:\n\n* **Establish the Objectives:** Before diving into the design, clearly define the goals you want to achieve through the website or app. Determine what actions you want the users to take (e.g., sign up, make a purchase, share content) and what constitutes a successful conversion.\n \n* **Understand User Mindsets:** Identify your target audience and recognize their needs, preferences, emotions, and pain points. Accomplishing this requires user research, creating personas, storyboarding, and empathy mapping, among other methods.\n \n* **Design the Optimal User Flow:** Craft a seamless and intuitive user journey by designing a clear path from the landing page to the desired action. Prioritize simplicity, usability, and efficiency. Make sure to include meaningful touchpoints and interactions to engage the users and make it easy for them to complete the intended action.\n \n* **Refine and Test the Experience:** Use wireframes and prototypes to test and iteratively refine the user experience. Employ user testing, A/B testing, and analytics to gather insight on user behavior, preferences, and engagement. Continuously use feedback to make improvements, ensuring that the design effectively leads visitors down the action funnel.\n \n\nBy implementing Stephen Wendell's `Create Action Funnel`, you can effectively guide users through an engaging journey that motivates them to become active customers, ultimately increasing conversion rates and overall satisfaction.",
|
||||
"links": [
|
||||
{
|
||||
"title": "Behavioral Science Crash Course: Steve Wendel's CREATE Action Funnel",
|
||||
@@ -75,7 +80,7 @@
|
||||
},
|
||||
"0Df110GZcDw5wbAe1eKoA": {
|
||||
"title": "Spectrum of Thinking Interventions",
|
||||
"description": "The _Spectrum of Thinking Interventions_ provides a structure for understanding the different types of decision-making processes by illustrating how our minds would respond in a _default, lowest energy way_, if we didn't consciously do something different. This spectrum ranges from situations requiring minimal thought to those demanding intensive thinking, and includes the mechanisms (\"interventions\") that our minds will likely use.\n\n* **Habits:** Triggering a learned routine based on familiar cues\n* **Other intuitive responses:** Used in familiar or semi-familiar situations, with responses based on past experiences\n* **Active mindset or self-concept:** Used in ambiguous scenarios with multiple possible interpretations\n* **Heuristics:** Used in situations requiring conscious attention, but where decisions can be made more easily\n* **Focused, conscious calculation:** Used in unfamiliar scenarios or crucial decisions where deliberate focus is needed\n\nWith this spectrum in mind, it is essential as a UX designer to leverage on the mind's decision-making process, analyze which mechanisms are most applicable to your target users and design the most accessible and effective solutions.",
|
||||
"description": "The _Spectrum of Thinking Interventions_ provides a structure to guide your UX design process, helping you identify the types and range of thinking interventions that the user may require. This spectrum encompasses four primary categories: guidance, explanation, exploration, and creation.\n\nGuidance\n--------\n\nGuidance-based interventions are designed to help users navigate through a digital product or service with minimal effort. They may be aimed at full-fledged beginners, casual users, or experts in their respective domains. Such interventions may include signposts, tooltips, and clearly articulated labels.\n\n_Examples:_\n\n* Visual cues (e.g., icons, colors)\n* Signposting (e.g., breadcrumbs)\n* In-context information (e.g., tool tips, hints)\n\nExplanation\n-----------\n\nExplanation-based interventions provide users with detailed narratives, overviews, or background information that helps them make informed decisions. This may include tutorials, articles, videos, or any other mediums that help explain complex concepts or instructions.\n\n_Examples:_\n\n* Multimedia tutorials\n* Articles or blog posts\n* Infographics or diagrams\n\nExploration\n-----------\n\nExploration-based interventions encourage users to understand and interact with the product by investigating, asking questions, or searching for solutions on their own. This can be done by providing interactive elements, multiple pathways, and opportunities for discovery.\n\n_Examples:_\n\n* Interactive simulations or models\n* Advanced search capabilities\n* Multiple UI paths for task completion\n\nCreation\n--------\n\nCreation-based interventions engage users by offering them the tools and resources to co-create or customize their experience. This type of intervention often involves a more extensive level of input and involvement from the user as they become active participants in the design process.\n\n_Examples:_\n\n* Customizable user interfaces\n* Allowing users to create their content\n* Enabling users to manage their preferences, settings, and configurations\n\nWith this spectrum in mind, it is essential as a UX designer to analyze which types of thinking interventions are most relevant to your target users and design the most accessible and effective solutions. Always consider how these interventions will influence users' decision-making processes and their overall satisfaction with your digital product or service.",
|
||||
"links": []
|
||||
},
|
||||
"kWA8CvocP1pkom2N7O4gb": {
|
||||
|
||||
@@ -242,25 +242,13 @@
|
||||
},
|
||||
"NCIzs3jbQTv1xXhAaGfZN": {
|
||||
"title": "v-text",
|
||||
"description": "The `v-text` directive is used to set the textContent property of an element. It's important to note that when using this directive it will overwrite the HTML content inside the element. The expected input is a string, so it's important to wrap any text in single quotes.\n\nExample:\n\n <template>\n <p v-text=\"'I am some text'\"></p>\n </template>\n \n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "v-text documentation",
|
||||
"url": "https://vuejs.org/api/built-in-directives.html#v-text",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"bZxtIBeIfeUcR32LZWrPW": {
|
||||
"title": "v-html",
|
||||
"description": "The `v-thml` directive is similar to the `v-text` directive, but the difference is that `v-html` renders its content as HTML. This means that if you pass an HTML element it will be rendered as an element and not plain text. Since the content is render as HTMl, it can pose a security risk if the content contains malicius JavaScript code. For this reason you should never use this directive in combination with user input, unless the input is first properly sanitized.\n\nExample:\n\n <template>\n <p v-html=\"'<h1>Text</h1>'\"></p>\n </template>\n \n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "v-html documentation",
|
||||
"url": "https://vuejs.org/api/built-in-directives.html#v-html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"_TlbGTKFCMO0wdLbC6xHX": {
|
||||
"title": "v-show",
|
||||
@@ -297,25 +285,13 @@
|
||||
},
|
||||
"a9caVhderJaVo0v14w8WB": {
|
||||
"title": "v-else-if",
|
||||
"description": "This directive is used to add additional conditions to a v-if and v-else block.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "v-else-if Documentation",
|
||||
"url": "https://vuejs.org/api/built-in-directives.html#v-else-if",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"3ftwRjQ9e1-qDT9BV53zr": {
|
||||
"title": "v-for",
|
||||
"description": "The `v-for` directive is used to render an HTML element, a block of elements, or even a component based on an array, an object, or a set number of times. When using this directive it is important to assign a unique key to each item to avoid issues and improve perfomance. This directive follows the `item in items` syntax.\n\nExample:\n\n <script setup>\n import { ref } from 'vue';\n const foods = ref([\n {id: 1, name: \"apple\"},\n {id: 2, name: \"pear\"},\n {id: 3, name: \"pizza\"}\n ]);\n </script>\n \n <template>\n <p v-for=\"food in foods\" :key=\"food.id\">{{ food.name }}</p>\n </template>\n \n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "v-for documentation",
|
||||
"url": "https://vuejs.org/guide/essentials/list#v-for",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"hVuRmhXVP65IPtuHTORjJ": {
|
||||
"title": "v-on",
|
||||
@@ -324,14 +300,8 @@
|
||||
},
|
||||
"cuM9q9vYy8JpZPGeBffd1": {
|
||||
"title": "v-bind",
|
||||
"description": "The `v-bind` directive dynamically binds an HTML attribute to data.\n\nThe shorthand for this directive is `:`\n\nExample:\n\n <script setup>\n import { ref } from 'vue';\n const image_url = ref(\"path/to/image.png\")\n </script>\n \n <template>\n <img :src=\"image_url\" />\n </template>\n \n\nVisit the following resources for more information:",
|
||||
"links": [
|
||||
{
|
||||
"title": "v-bind documentation",
|
||||
"url": "https://vuejs.org/api/built-in-directives.html#v-bind",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"cxu2Wbt306SxM4JKQQqnL": {
|
||||
"title": "v-model",
|
||||
@@ -351,25 +321,13 @@
|
||||
},
|
||||
"5k9CrbzhNy9iiS6ez2UE6": {
|
||||
"title": "v-once",
|
||||
"description": "The `v-once` directive makes an HTML element render only once, skipping every future update.\n\nExample:\n\n <script setup>\n import { ref } from 'vue';\n const input = ref(\"Some Text\");\n </script>\n \n <template>\n <input v-model=\"input\">\n <p v-once>{{ input }}</p>\n </template>\n \n\nIn this example the **p** element will not change its text even if the input variable is changed through the **input** element.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "v-once documentation",
|
||||
"url": "https://vuejs.org/api/built-in-directives.html#v-once",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"mlsrhioiEkqnRIL6O3hNa": {
|
||||
"title": "v-pre",
|
||||
"description": "The `v-pre` directive makes an element render its content as-is, skipping its compilation. The most common use case is when displaying raw mustache syntax.\n\nExample:\n\n <script setup>\n import { ref } from 'vue';\n const text = ref(\"Some Text\")\n </script>\n \n <template>\n <p v-pre >{{ text }}</p>\n </template>\n \n\nThe **p** element will display: `{{ text }}` and not `Some Text` because the compilation is skipped.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "v-pre Documentation",
|
||||
"url": "https://vuejs.org/api/built-in-directives.html#v-pre",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"RrSekP8Ub01coegMwLP6a": {
|
||||
"title": "v-cloak",
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { type APIContext } from 'astro';
|
||||
import { api } from './api.ts';
|
||||
|
||||
export type LeadeboardUserDetails = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type ListLeaderboardStatsResponse = {
|
||||
streaks: {
|
||||
active: LeadeboardUserDetails[];
|
||||
lifetime: LeadeboardUserDetails[];
|
||||
};
|
||||
projectSubmissions: {
|
||||
currentMonth: LeadeboardUserDetails[];
|
||||
lifetime: LeadeboardUserDetails[];
|
||||
};
|
||||
};
|
||||
|
||||
export function leaderboardApi(context: APIContext) {
|
||||
return {
|
||||
listLeaderboardStats: async function () {
|
||||
return api(context).get<ListLeaderboardStatsResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-leaderboard-stats`,
|
||||
{},
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type APIContext } from 'astro';
|
||||
import { api } from './api.ts';
|
||||
import type { RoadmapDocument } from '../components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||
import type { PageType } from '../components/CommandMenu/CommandMenu.tsx';
|
||||
|
||||
export type ListShowcaseRoadmapResponse = {
|
||||
data: Pick<
|
||||
@@ -38,30 +37,3 @@ export function roadmapApi(context: APIContext) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type ProjectPageType = {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export async function getProjectList() {
|
||||
const baseUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3000'
|
||||
: 'https://roadmap.sh';
|
||||
const pages = await fetch(`${baseUrl}/pages.json`).catch((err) => {
|
||||
console.error(err);
|
||||
return [];
|
||||
});
|
||||
|
||||
const pagesJson = await (pages as any).json();
|
||||
const projects: ProjectPageType[] = pagesJson
|
||||
.filter((page: any) => page?.group?.toLowerCase() === 'projects')
|
||||
.map((page: any) => ({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
url: page.url,
|
||||
}));
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type APIContext } from 'astro';
|
||||
import { api } from './api.ts';
|
||||
import type { ResourceType } from '../lib/resource-progress.ts';
|
||||
import type { ProjectStatusDocument } from '../components/Projects/ListProjectSolutions.tsx';
|
||||
|
||||
export const allowedRoadmapVisibility = ['all', 'none', 'selected'] as const;
|
||||
export type AllowedRoadmapVisibility =
|
||||
@@ -100,7 +99,6 @@ export type GetPublicProfileResponse = Omit<
|
||||
> & {
|
||||
activity: UserActivityCount;
|
||||
roadmaps: ProgressResponse[];
|
||||
projects: ProjectStatusDocument[];
|
||||
isOwnProfile: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { Flame, X, Zap, ZapOff } from 'lucide-react';
|
||||
import {Flame, X, Zap, ZapOff} from 'lucide-react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { StreakDay } from './StreakDay';
|
||||
import {
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from '../../stores/page.ts';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { $accountStreak } from '../../stores/streak.ts';
|
||||
|
||||
type StreakResponse = {
|
||||
count: number;
|
||||
@@ -28,7 +27,12 @@ export function AccountStreak(props: AccountStreakProps) {
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const accountStreak = useStore($accountStreak);
|
||||
const [accountStreak, setAccountStreak] = useState<StreakResponse>({
|
||||
count: 0,
|
||||
longestCount: 0,
|
||||
firstVisitAt: new Date(),
|
||||
lastVisitAt: new Date(),
|
||||
});
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
const $roadmapsDropdownOpen = useStore(roadmapsDropdownOpen);
|
||||
@@ -45,11 +49,6 @@ export function AccountStreak(props: AccountStreakProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountStreak) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { response, error } = await httpGet<StreakResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-streak`,
|
||||
@@ -61,7 +60,7 @@ export function AccountStreak(props: AccountStreakProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
$accountStreak.set(response);
|
||||
setAccountStreak(response);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
@@ -77,7 +76,7 @@ export function AccountStreak(props: AccountStreakProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { count: currentCount = 0 } = accountStreak || {};
|
||||
let { count: currentCount } = accountStreak;
|
||||
const previousCount =
|
||||
accountStreak?.previousCount || accountStreak?.count || 0;
|
||||
|
||||
@@ -111,7 +110,7 @@ export function AccountStreak(props: AccountStreakProps) {
|
||||
ref={dropdownRef}
|
||||
className="absolute right-0 top-full z-50 w-[335px] translate-y-1 rounded-lg bg-slate-800 shadow-xl"
|
||||
>
|
||||
<div className="py-3 pl-4 pr-5">
|
||||
<div className="pl-4 pr-5 py-3">
|
||||
<div className="flex items-center justify-between gap-2 text-sm text-slate-500">
|
||||
<p>
|
||||
Current Streak
|
||||
@@ -181,13 +180,8 @@ export function AccountStreak(props: AccountStreakProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="-mt-[0px] mb-[1.75px] text-center text-xs tracking-wide text-slate-600">
|
||||
Visit every day to keep your streak going!
|
||||
</p>
|
||||
<p className='text-xs mt-1.5 text-center'>
|
||||
<a href="/leaderboard" className="text-purple-400 hover:underline underline-offset-2">
|
||||
See how you compare to others
|
||||
</a>
|
||||
<p className="text-center text-xs text-slate-600 tracking-wide mb-[1.75px] -mt-[0px]">
|
||||
Visit every day to keep your streak alive!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,6 @@ import { ResourceProgress } from './ResourceProgress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { EmptyActivity } from './EmptyActivity';
|
||||
import { ActivityStream, type UserStreamActivity } from './ActivityStream';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { ProjectProgress } from './ProjectProgress';
|
||||
|
||||
type ProgressResponse = {
|
||||
updatedAt: string;
|
||||
@@ -51,14 +47,11 @@ export type ActivityResponse = {
|
||||
};
|
||||
}[];
|
||||
activities: UserStreamActivity[];
|
||||
projects: ProjectStatusDocument[];
|
||||
};
|
||||
|
||||
export function ActivityPage() {
|
||||
const toast = useToast();
|
||||
const [activity, setActivity] = useState<ActivityResponse>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [projectDetails, setProjectDetails] = useState<PageType[]>([]);
|
||||
|
||||
async function loadActivity() {
|
||||
const { error, response } = await httpGet<ActivityResponse>(
|
||||
@@ -75,29 +68,11 @@ export function ActivityPage() {
|
||||
setActivity(response);
|
||||
}
|
||||
|
||||
async function loadAllProjectDetails() {
|
||||
const { error, response } = await httpGet<PageType[]>(`/pages.json`);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allProjects = response.filter((page) => page.group === 'Projects');
|
||||
setProjectDetails(allProjects);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
Promise.allSettled([loadActivity(), loadAllProjectDetails()]).finally(
|
||||
() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
},
|
||||
);
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const learningRoadmaps = activity?.learning.roadmaps || [];
|
||||
@@ -131,17 +106,6 @@ export function ActivityPage() {
|
||||
learningRoadmapsToShow.length !== 0 ||
|
||||
learningBestPracticesToShow.length !== 0;
|
||||
|
||||
const enrichedProjects = activity?.projects.map((project) => {
|
||||
const projectDetail = projectDetails.find(
|
||||
(page) => page.id === project.projectId,
|
||||
);
|
||||
|
||||
return {
|
||||
...project,
|
||||
title: projectDetail?.title || 'N/A',
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActivityCounters
|
||||
@@ -237,19 +201,6 @@ export function ActivityPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{enrichedProjects && enrichedProjects?.length > 0 && (
|
||||
<div className="mx-0 px-0 py-5 pb-0 md:-mx-10 md:px-8 md:py-8 md:pb-0">
|
||||
<h2 className="mb-3 text-xs uppercase text-gray-400">
|
||||
Your Projects
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
|
||||
{enrichedProjects.map((project) => (
|
||||
<ProjectProgress key={project._id} projectStatus={project} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasProgress && (
|
||||
<ActivityStream activities={activity?.activities || []} />
|
||||
)}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { getPercentage } from '../../helper/number';
|
||||
import { ProjectProgressActions } from './ProjectProgressActions';
|
||||
import { cn } from '../../lib/classname';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import { ProjectStatus } from './ProjectStatus';
|
||||
import { ThumbsUp } from 'lucide-react';
|
||||
|
||||
type ProjectProgressType = {
|
||||
projectStatus: ProjectStatusDocument & {
|
||||
title: string;
|
||||
};
|
||||
showActions?: boolean;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export function ProjectProgress(props: ProjectProgressType) {
|
||||
const {
|
||||
projectStatus,
|
||||
showActions = true,
|
||||
userId: defaultUserId = getUser()?.id,
|
||||
} = props;
|
||||
|
||||
const shouldShowActions =
|
||||
projectStatus.submittedAt &&
|
||||
projectStatus.submittedAt !== null &&
|
||||
showActions;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<a
|
||||
className={cn(
|
||||
'group relative flex w-full items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 pr-7 text-left text-sm transition-all hover:border-gray-400',
|
||||
shouldShowActions ? '' : 'pr-3',
|
||||
)}
|
||||
href={`/projects/${projectStatus.projectId}`}
|
||||
target="_blank"
|
||||
>
|
||||
<ProjectStatus projectStatus={projectStatus} />
|
||||
<span className="ml-2 flex-grow truncate">{projectStatus?.title}</span>
|
||||
<span className="inline-flex items-center gap-1 text-xs text-gray-400">
|
||||
{projectStatus.upvotes}
|
||||
<ThumbsUp className="size-2.5 stroke-[2.5px]" />
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{shouldShowActions && (
|
||||
<div className="absolute right-2 top-0 flex h-full items-center">
|
||||
<ProjectProgressActions
|
||||
userId={defaultUserId!}
|
||||
projectId={projectStatus.projectId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { MoreVertical, X } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon';
|
||||
import { ShareIcon } from '../ReactIcons/ShareIcon';
|
||||
|
||||
type ProjectProgressActionsType = {
|
||||
userId: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export function ProjectProgressActions(props: ProjectProgressActionsType) {
|
||||
const { userId, projectId } = props;
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { copyText, isCopied } = useCopyText();
|
||||
|
||||
const projectSolutionUrl = `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/projects/${projectId}/solutions?u=${userId}`;
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative h-full" ref={dropdownRef}>
|
||||
<button
|
||||
className="h-full text-gray-400 hover:text-gray-700"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<MoreVertical size={16} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-8 z-10 w-48 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
|
||||
<button
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1.5 p-2 text-xs font-medium hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-70 sm:text-sm',
|
||||
isCopied ? 'text-green-500' : 'text-gray-500 hover:text-black',
|
||||
)}
|
||||
onClick={() => {
|
||||
copyText(projectSolutionUrl);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckIcon additionalClasses="h-3.5 w-3.5" /> Link Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShareIcon className="h-3.5 w-3.5 stroke-[2.5px]" /> Share
|
||||
Solution
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { CircleDashed } from 'lucide-react';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon';
|
||||
|
||||
type ProjectStatusType = {
|
||||
projectStatus: ProjectStatusDocument & {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function ProjectStatus(props: ProjectStatusType) {
|
||||
const { projectStatus } = props;
|
||||
|
||||
const { submittedAt, repositoryUrl } = projectStatus;
|
||||
const status = submittedAt && repositoryUrl ? 'submitted' : 'started';
|
||||
|
||||
if (status === 'submitted') {
|
||||
return <CheckIcon additionalClasses="size-3 text-gray-500 shrink-0" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CircleDashed className="size-3 shrink-0 stroke-[2.5px] text-gray-400" />
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useId, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
|
||||
@@ -53,16 +53,13 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
|
||||
setError(error?.message || 'Something went wrong. Please try again later.');
|
||||
};
|
||||
|
||||
const emailFieldId = `form:${useId()}`;
|
||||
const passwordFieldId = `form:${useId()}`;
|
||||
|
||||
return (
|
||||
<form className="w-full" onSubmit={handleFormSubmit}>
|
||||
<label htmlFor={emailFieldId} className="sr-only">
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id={emailFieldId}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
@@ -72,11 +69,11 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
|
||||
value={email}
|
||||
onInput={(e) => setEmail(String((e.target as any).value))}
|
||||
/>
|
||||
<label htmlFor={passwordFieldId} className="sr-only">
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id={passwordFieldId}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
|
||||
@@ -48,7 +48,6 @@ function handleGuest() {
|
||||
'/team/members',
|
||||
'/team/member',
|
||||
'/team/settings',
|
||||
'/dashboard',
|
||||
];
|
||||
|
||||
showHideAuthElements('hide');
|
||||
|
||||
@@ -9,31 +9,28 @@ export function ContentConfirmationModal(props: ContentConfirmationModalProps) {
|
||||
const { onClose, onClick } = props;
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} wrapperClassName="max-w-lg">
|
||||
<Modal onClose={onClose}>
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold">
|
||||
Copy Node Details and Resources?
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold">Roadmap Content</h2>
|
||||
<p className="balanc text-gray-600">
|
||||
This will just copy the roadmap in your team. Would you like to copy
|
||||
the resource links and node details as well?
|
||||
Do you want to copy the content of this roadmap?
|
||||
</p>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
className="rounded-lg border p-2.5 font-normal"
|
||||
className="rounded-lg border p-2.5 font-medium"
|
||||
onClick={() => {
|
||||
onClick(false);
|
||||
}}
|
||||
>
|
||||
No, copy roadmap only
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg border bg-black p-2.5 font-normal text-white hover:opacity-80"
|
||||
className="rounded-lg border bg-black p-2.5 font-medium text-white hover:opacity-80"
|
||||
onClick={() => {
|
||||
onClick(true);
|
||||
}}
|
||||
>
|
||||
Yes, also copy resources
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
import { DashboardCustomProgressCard } from './DashboardCustomProgressCard';
|
||||
import { DashboardCardLink } from './DashboardCardLink';
|
||||
import { useState } from 'react';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
||||
import { Simulate } from 'react-dom/test-utils';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bot,
|
||||
BrainCircuit,
|
||||
Map,
|
||||
PencilRuler,
|
||||
} from 'lucide-react';
|
||||
|
||||
type DashboardAiRoadmapsProps = {
|
||||
roadmaps: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
}[];
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
|
||||
const { roadmaps, isLoading = false } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 mt-6 flex items-center justify-between gap-2">
|
||||
<h2 className="text-xs uppercase text-gray-400">
|
||||
AI Generated Roadmaps
|
||||
</h2>
|
||||
|
||||
{!isLoading && roadmaps.length !== 0 && (
|
||||
<a
|
||||
href="/ai/explore"
|
||||
className="flex items-center gap-1 text-xs text-gray-500 hover:text-black"
|
||||
>
|
||||
<ArrowUpRight size={12} />
|
||||
AI Generated Roadmaps
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isLoading && roadmaps.length === 0 && (
|
||||
<DashboardCardLink
|
||||
className="mt-0"
|
||||
icon={BrainCircuit}
|
||||
href="/ai"
|
||||
title="Generate Roadmaps with AI"
|
||||
description="You can generate your own roadmap with AI"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{isLoading && (
|
||||
<>
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<RoadmapCardSkeleton key={index} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoading && roadmaps.length > 0 && (
|
||||
<>
|
||||
{roadmaps.map((roadmap) => (
|
||||
<a
|
||||
href={`/ai/${roadmap.slug}`}
|
||||
className="relative truncate rounded-md border bg-white p-2.5 text-left text-sm shadow-sm hover:border-gray-400 hover:bg-gray-50"
|
||||
>
|
||||
{roadmap.title}
|
||||
</a>
|
||||
))}
|
||||
|
||||
<a
|
||||
className="flex items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-2.5 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600"
|
||||
href={'/ai'}
|
||||
>
|
||||
+ Generate New
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type CustomProgressCardSkeletonProps = {};
|
||||
|
||||
function RoadmapCardSkeleton(props: CustomProgressCardSkeletonProps) {
|
||||
return (
|
||||
<div className="h-[42px] w-full animate-pulse rounded-md bg-gray-200" />
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Bookmark } from 'lucide-react';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
|
||||
type DashboardBookmarkCardProps = {
|
||||
bookmark: UserProgress;
|
||||
};
|
||||
|
||||
export function DashboardBookmarkCard(props: DashboardBookmarkCardProps) {
|
||||
const {
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceTitle,
|
||||
roadmapSlug,
|
||||
isCustomResource,
|
||||
} = props.bookmark;
|
||||
|
||||
let url =
|
||||
resourceType === 'roadmap'
|
||||
? `/${resourceId}`
|
||||
: `/best-practices/${resourceId}`;
|
||||
|
||||
if (isCustomResource) {
|
||||
url = `/r/${roadmapSlug}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
key={resourceId}
|
||||
className="group relative flex flex-row items-center gap-2 rounded-md border border-gray-300 bg-white px-1.5 py-2 text-left text-sm transition-all hover:border-gray-400"
|
||||
>
|
||||
<Bookmark className="size-4 fill-current text-gray-300" />
|
||||
<h4 className="truncate text-gray-900">{resourceTitle}</h4>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { ArrowUpRight, type LucideIcon } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type DashboardCardLinkProps = {
|
||||
href: string;
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
description: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function DashboardCardLink(props: DashboardCardLinkProps) {
|
||||
const { href, title, description, icon: Icon, className } = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={cn(
|
||||
'relative mt-4 flex min-h-[220px] flex-col justify-end rounded-lg border border-gray-300 bg-gradient-to-br from-white to-gray-50 py-5 px-6 hover:border-gray-400 hover:from-white hover:to-gray-100',
|
||||
className,
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
<Icon className="mb-4 size-10 text-gray-300" strokeWidth={1.25} />
|
||||
<h4 className="text-xl font-semibold tracking-wide">{title}</h4>
|
||||
<p className="mt-1 text-gray-500">{description}</p>
|
||||
<ArrowUpRight className="absolute right-3 top-3 size-4" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { getPercentage } from '../../helper/number';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
|
||||
type DashboardCustomProgressCardProps = {
|
||||
progress: UserProgress;
|
||||
};
|
||||
|
||||
export function DashboardCustomProgressCard(props: DashboardCustomProgressCardProps) {
|
||||
const { progress } = props;
|
||||
|
||||
const {
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceTitle,
|
||||
total: totalCount,
|
||||
done: doneCount,
|
||||
skipped: skippedCount,
|
||||
roadmapSlug,
|
||||
isCustomResource,
|
||||
updatedAt,
|
||||
} = progress;
|
||||
|
||||
let url =
|
||||
resourceType === 'roadmap'
|
||||
? `/${resourceId}`
|
||||
: `/best-practices/${resourceId}`;
|
||||
|
||||
if (isCustomResource) {
|
||||
url = `/r/${roadmapSlug}`;
|
||||
}
|
||||
|
||||
const totalMarked = doneCount + skippedCount;
|
||||
const progressPercentage = getPercentage(totalMarked, totalCount);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
className="group relative flex min-h-[80px] w-full flex-col justify-between overflow-hidden rounded-md border bg-white p-3 text-left text-sm shadow-sm transition-all hover:border-gray-400 hover:bg-gray-50"
|
||||
>
|
||||
<h4 className="truncate font-medium text-gray-900">{resourceTitle}</h4>
|
||||
|
||||
<div className="mt-6 flex items-center gap-2">
|
||||
<div className="h-2 w-full overflow-hidden rounded-md bg-black/10">
|
||||
<div
|
||||
className="h-full bg-black/20"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{Math.floor(+progressPercentage)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
{isCustomResource ? (
|
||||
<>Last updated {getRelativeTimeString(updatedAt)}</>
|
||||
) : (
|
||||
<>Last practiced {getRelativeTimeString(updatedAt)}</>
|
||||
)}
|
||||
</p>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $teamList } from '../../stores/team';
|
||||
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
|
||||
import { DashboardTab } from './DashboardTab';
|
||||
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
|
||||
import { TeamDashboard } from './TeamDashboard';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
|
||||
type DashboardPageProps = {
|
||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps?: BuiltInRoadmap[];
|
||||
builtInBestPractices?: BuiltInRoadmap[];
|
||||
};
|
||||
|
||||
export function DashboardPage(props: DashboardPageProps) {
|
||||
const { builtInRoleRoadmaps, builtInBestPractices, builtInSkillRoadmaps } =
|
||||
props;
|
||||
|
||||
const currentUser = getUser();
|
||||
const toast = useToast();
|
||||
const teamList = useStore($teamList);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string>();
|
||||
|
||||
async function getAllTeams() {
|
||||
if (teamList.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response, error } = await httpGet<TeamListResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`,
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
$teamList.set(response);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllTeams().finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
const userAvatar =
|
||||
currentUser?.avatar && !isLoading
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${currentUser.avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pb-20 pt-8">
|
||||
<div className="container">
|
||||
<div className="mb-6 sm:mb-8 flex flex-wrap items-center gap-1.5">
|
||||
<DashboardTab
|
||||
label="Personal"
|
||||
isActive={!selectedTeamId}
|
||||
onClick={() => setSelectedTeamId(undefined)}
|
||||
avatar={userAvatar}
|
||||
/>
|
||||
{isLoading && (
|
||||
<>
|
||||
<DashboardTabSkeleton />
|
||||
<DashboardTabSkeleton />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoading && (
|
||||
<>
|
||||
{teamList.map((team) => {
|
||||
const { avatar } = team;
|
||||
const avatarUrl = avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
return (
|
||||
<DashboardTab
|
||||
key={team._id}
|
||||
label={team.name}
|
||||
isActive={team._id === selectedTeamId}
|
||||
{...(team.status === 'invited'
|
||||
? {
|
||||
href: `/respond-invite?i=${team.memberId}`,
|
||||
}
|
||||
: {
|
||||
href: `/team/activity?t=${team._id}`,
|
||||
// onClick: () => {
|
||||
// setSelectedTeamId(team._id);
|
||||
// },
|
||||
})}
|
||||
avatar={avatarUrl}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<DashboardTab
|
||||
label="+ Create Team"
|
||||
isActive={false}
|
||||
href="/team/new"
|
||||
className="border border-dashed border-gray-300 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-gray-600 hover:text-black"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!selectedTeamId && (
|
||||
<PersonalDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||
builtInBestPractices={builtInBestPractices}
|
||||
/>
|
||||
)}
|
||||
{selectedTeamId && <TeamDashboard teamId={selectedTeamId} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardTabSkeleton() {
|
||||
return (
|
||||
<div className="h-[30px] w-[114px] animate-pulse rounded-md border bg-white"></div>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { getPercentage } from '../../helper/number';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
import { ArrowUpRight, ExternalLink } from 'lucide-react';
|
||||
|
||||
type DashboardProgressCardProps = {
|
||||
progress: UserProgress;
|
||||
};
|
||||
|
||||
export function DashboardProgressCard(props: DashboardProgressCardProps) {
|
||||
const { progress } = props;
|
||||
const {
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceTitle,
|
||||
total: totalCount,
|
||||
done: doneCount,
|
||||
skipped: skippedCount,
|
||||
roadmapSlug,
|
||||
isCustomResource,
|
||||
updatedAt,
|
||||
} = progress;
|
||||
|
||||
let url =
|
||||
resourceType === 'roadmap'
|
||||
? `/${resourceId}`
|
||||
: `/best-practices/${resourceId}`;
|
||||
|
||||
if (isCustomResource) {
|
||||
url = `/r/${roadmapSlug}`;
|
||||
}
|
||||
|
||||
const totalMarked = doneCount + skippedCount;
|
||||
const progressPercentage = getPercentage(totalMarked, totalCount);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
key={resourceId}
|
||||
className="group relative flex w-full items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400"
|
||||
>
|
||||
<span className="flex-grow truncate">{resourceTitle}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{parseInt(progressPercentage, 10)}%
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="absolute left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 transition-colors group-hover:bg-black/10"
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Check, CircleCheck, CircleDashed } from 'lucide-react';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { getRelativeTimeString } from '../../lib/date.ts';
|
||||
|
||||
type DashboardProjectCardProps = {
|
||||
project: ProjectStatusDocument & {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function DashboardProjectCard(props: DashboardProjectCardProps) {
|
||||
const { project } = props;
|
||||
|
||||
const { title, projectId, submittedAt, startedAt, repositoryUrl } = project;
|
||||
|
||||
const url = `/projects/${projectId}`;
|
||||
const status = submittedAt && repositoryUrl ? 'submitted' : 'started';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
key={projectId}
|
||||
className="group relative flex w-full items-center gap-2 text-left text-sm underline-offset-2"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full',
|
||||
{
|
||||
'border border-green-500 bg-green-500 group-hover:border-green-600 group-hover:bg-green-600':
|
||||
status === 'submitted',
|
||||
'border border-dashed border-gray-400 bg-transparent group-hover:border-gray-500':
|
||||
status === 'started',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{status === 'submitted' && (
|
||||
<Check
|
||||
className="relative top-[0.5px] size-3 text-white"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="flex-grow truncate group-hover:underline">{title.replace(/(System)|(Service)/, '')}</span>
|
||||
<span className="flex-shrink-0 bg-transparent text-xs text-gray-400 no-underline">
|
||||
{!!startedAt &&
|
||||
status === 'started' &&
|
||||
getRelativeTimeString(startedAt)}
|
||||
{!!submittedAt &&
|
||||
status === 'submitted' &&
|
||||
getRelativeTimeString(submittedAt)}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type DashboardTabProps = {
|
||||
label: string | ReactNode;
|
||||
isActive: boolean;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
href?: string;
|
||||
avatar?: string;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
export function DashboardTab(props: DashboardTabProps) {
|
||||
const { isActive, onClick, label, className, href, avatar, icon } = props;
|
||||
|
||||
const Slot = href ? 'a' : 'button';
|
||||
|
||||
return (
|
||||
<Slot
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border bg-white p-1.5 px-2 text-sm leading-none text-gray-600',
|
||||
isActive ? 'border-gray-500 bg-gray-200 text-gray-900' : '',
|
||||
className,
|
||||
)}
|
||||
{...(href ? { href } : {})}
|
||||
>
|
||||
{avatar && (
|
||||
<img
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
className="h-4 w-4 mr-0.5 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{icon}
|
||||
{label}
|
||||
</Slot>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type EmptyStackMessageProps = {
|
||||
number: number | string;
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
bodyClassName?: string;
|
||||
};
|
||||
|
||||
export function EmptyStackMessage(props: EmptyStackMessageProps) {
|
||||
const { number, title, description, buttonText, buttonLink, bodyClassName } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-black/50">
|
||||
<div
|
||||
className={cn(
|
||||
'flex max-w-[200px] flex-col items-center justify-center rounded-md bg-white p-4 shadow-sm',
|
||||
bodyClassName,
|
||||
)}
|
||||
>
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-300 text-white">
|
||||
{number}
|
||||
</span>
|
||||
<div className="my-3 text-center">
|
||||
<h3 className="text-sm font-medium text-black">{title}</h3>
|
||||
<p className="text-center text-xs text-gray-500">{description}</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={buttonLink}
|
||||
className="rounded-md bg-black px-3 py-1 text-xs text-white transition-transform hover:scale-105 hover:bg-gray-900"
|
||||
>
|
||||
{buttonText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
import { DashboardCustomProgressCard } from './DashboardCustomProgressCard';
|
||||
import { DashboardCardLink } from './DashboardCardLink';
|
||||
import { useState } from 'react';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
||||
import { Simulate } from 'react-dom/test-utils';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bot,
|
||||
BrainCircuit,
|
||||
Map,
|
||||
PencilRuler,
|
||||
} from 'lucide-react';
|
||||
|
||||
type ListDashboardCustomProgressProps = {
|
||||
progresses: UserProgress[];
|
||||
isLoading?: boolean;
|
||||
isCustomResources?: boolean;
|
||||
isAIGeneratedRoadmaps?: boolean;
|
||||
};
|
||||
|
||||
export function ListDashboardCustomProgress(
|
||||
props: ListDashboardCustomProgressProps,
|
||||
) {
|
||||
const {
|
||||
progresses,
|
||||
isLoading = false,
|
||||
isAIGeneratedRoadmaps = false,
|
||||
} = props;
|
||||
const [isCreateCustomRoadmapModalOpen, setIsCreateCustomRoadmapModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const customRoadmapModal = isCreateCustomRoadmapModalOpen ? (
|
||||
<CreateRoadmapModal
|
||||
onClose={() => setIsCreateCustomRoadmapModalOpen(false)}
|
||||
onCreated={(roadmap) => {
|
||||
window.location.href = `${
|
||||
import.meta.env.PUBLIC_EDITOR_APP_URL
|
||||
}/${roadmap?._id}`;
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{customRoadmapModal}
|
||||
|
||||
<div className="mb-2 mt-6 flex items-center justify-between gap-2">
|
||||
<h2 className="text-xs uppercase text-gray-400">
|
||||
{isAIGeneratedRoadmaps ? 'AI Generated Roadmaps' : 'Custom Roadmaps'}
|
||||
</h2>
|
||||
|
||||
{!isLoading && progresses.length !== 0 && (
|
||||
<a
|
||||
href="/ai/explore"
|
||||
className="flex items-center gap-1 text-xs text-gray-500 hover:text-black"
|
||||
>
|
||||
<ArrowUpRight size={12} />
|
||||
Community Roadmaps
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isLoading && progresses.length === 0 && isAIGeneratedRoadmaps && (
|
||||
<DashboardCardLink
|
||||
className="mt-0"
|
||||
icon={BrainCircuit}
|
||||
href="/ai"
|
||||
title="Generate Roadmaps with AI"
|
||||
description="You can generate your own roadmap with AI"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoading && progresses.length === 0 && !isAIGeneratedRoadmaps && (
|
||||
<DashboardCardLink
|
||||
className="mt-0"
|
||||
icon={PencilRuler}
|
||||
href="https://draw.roadmap.sh"
|
||||
title="Draw your own Roadmap"
|
||||
description="Use our editor to draw your own roadmap"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
|
||||
{isLoading && (
|
||||
<>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<CustomProgressCardSkeleton key={index} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoading && progresses.length > 0 && (
|
||||
<>
|
||||
{progresses.map((progress) => (
|
||||
<DashboardCustomProgressCard
|
||||
key={progress.resourceId}
|
||||
progress={progress}
|
||||
/>
|
||||
))}
|
||||
|
||||
<a
|
||||
className="flex min-h-[80px] items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-4 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600"
|
||||
href={'/ai'}
|
||||
onClick={(e) => {
|
||||
if (!isAIGeneratedRoadmaps) {
|
||||
e.preventDefault();
|
||||
setIsCreateCustomRoadmapModalOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isAIGeneratedRoadmaps ? '+ Generate New' : '+ Create New'}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type CustomProgressCardSkeletonProps = {};
|
||||
|
||||
export function CustomProgressCardSkeleton(
|
||||
props: CustomProgressCardSkeletonProps,
|
||||
) {
|
||||
return (
|
||||
<div className="h-[106px] w-full animate-pulse rounded-md bg-gray-200" />
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
type LoadingProgressProps = {};
|
||||
|
||||
export function LoadingProgress(props: LoadingProgressProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-[38px] w-full animate-pulse rounded-md border border-gray-300 bg-gray-100"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
import { type JSXElementConstructor, useEffect, useState } from 'react';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { getCurrentPeriod } from '../../lib/date';
|
||||
import { ListDashboardCustomProgress } from './ListDashboardCustomProgress';
|
||||
import { RecommendedRoadmaps } from './RecommendedRoadmaps';
|
||||
import { ProgressStack } from './ProgressStack';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $accountStreak, type StreakResponse } from '../../stores/streak';
|
||||
import { CheckEmoji } from '../ReactIcons/CheckEmoji.tsx';
|
||||
import { ConstructionEmoji } from '../ReactIcons/ConstructionEmoji.tsx';
|
||||
import { BookEmoji } from '../ReactIcons/BookEmoji.tsx';
|
||||
import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx';
|
||||
|
||||
type UserDashboardResponse = {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
headline: string;
|
||||
username: string;
|
||||
progresses: UserProgress[];
|
||||
projects: ProjectStatusDocument[];
|
||||
aiRoadmaps: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
}[];
|
||||
topicDoneToday: number;
|
||||
};
|
||||
|
||||
export type BuiltInRoadmap = {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
isFavorite?: boolean;
|
||||
relatedRoadmapIds?: string[];
|
||||
};
|
||||
|
||||
type PersonalDashboardProps = {
|
||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps?: BuiltInRoadmap[];
|
||||
builtInBestPractices?: BuiltInRoadmap[];
|
||||
};
|
||||
|
||||
export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
const {
|
||||
builtInRoleRoadmaps = [],
|
||||
builtInBestPractices = [],
|
||||
builtInSkillRoadmaps = [],
|
||||
} = props;
|
||||
|
||||
const toast = useToast();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [personalDashboardDetails, setPersonalDashboardDetails] =
|
||||
useState<UserDashboardResponse>();
|
||||
const [projectDetails, setProjectDetails] = useState<PageType[]>([]);
|
||||
const accountStreak = useStore($accountStreak);
|
||||
|
||||
const loadAccountStreak = async () => {
|
||||
if (accountStreak) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { response, error } = await httpGet<StreakResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-streak`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Failed to load account streak');
|
||||
return;
|
||||
}
|
||||
|
||||
$accountStreak.set(response);
|
||||
};
|
||||
|
||||
async function loadProgress() {
|
||||
const { response: progressList, error } =
|
||||
await httpGet<UserDashboardResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-user-dashboard`,
|
||||
);
|
||||
|
||||
if (error || !progressList) {
|
||||
return;
|
||||
}
|
||||
|
||||
progressList?.progresses?.forEach((progress) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mark-favorite', {
|
||||
detail: {
|
||||
resourceId: progress.resourceId,
|
||||
resourceType: progress.resourceType,
|
||||
isFavorite: progress.isFavorite,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
setPersonalDashboardDetails(progressList);
|
||||
}
|
||||
|
||||
async function loadAllProjectDetails() {
|
||||
const { error, response } = await httpGet<PageType[]>(`/pages.json`);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allProjects = response.filter((page) => page.group === 'Projects');
|
||||
setProjectDetails(allProjects);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
Promise.allSettled([
|
||||
loadProgress(),
|
||||
loadAllProjectDetails(),
|
||||
loadAccountStreak(),
|
||||
]).finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('refresh-favorites', loadProgress);
|
||||
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
||||
}, []);
|
||||
|
||||
const learningRoadmapsToShow = (personalDashboardDetails?.progresses || [])
|
||||
.filter((progress) => !progress.isCustomResource)
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
|
||||
if (a.isFavorite && !b.isFavorite) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!a.isFavorite && b.isFavorite) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
});
|
||||
|
||||
const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || [];
|
||||
const customRoadmaps = (personalDashboardDetails?.progresses || [])
|
||||
.filter((progress) => progress.isCustomResource)
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
});
|
||||
|
||||
const { avatar, name } = personalDashboardDetails || {};
|
||||
const avatarLink = avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
|
||||
const allRoadmapsAndBestPractices = [
|
||||
...builtInRoleRoadmaps,
|
||||
...builtInSkillRoadmaps,
|
||||
...builtInBestPractices,
|
||||
];
|
||||
|
||||
const relatedRoadmapIds = allRoadmapsAndBestPractices
|
||||
// take the ones that user is learning
|
||||
.filter((roadmap) =>
|
||||
learningRoadmapsToShow?.some(
|
||||
(learningRoadmap) => learningRoadmap.resourceId === roadmap.id,
|
||||
),
|
||||
)
|
||||
.flatMap((roadmap) => roadmap.relatedRoadmapIds)
|
||||
// remove the ones that user is already learning or has bookmarked
|
||||
.filter(
|
||||
(roadmapId) =>
|
||||
!learningRoadmapsToShow.some((lr) => lr.resourceId === roadmapId),
|
||||
);
|
||||
|
||||
const recommendedRoadmapIds = new Set(
|
||||
relatedRoadmapIds.length === 0
|
||||
? [
|
||||
'frontend',
|
||||
'backend',
|
||||
'devops',
|
||||
'ai-data-scientist',
|
||||
'full-stack',
|
||||
'api-design',
|
||||
]
|
||||
: relatedRoadmapIds,
|
||||
);
|
||||
|
||||
const recommendedRoadmaps = allRoadmapsAndBestPractices.filter((roadmap) =>
|
||||
recommendedRoadmapIds.has(roadmap.id),
|
||||
);
|
||||
|
||||
const enrichedProjects = personalDashboardDetails?.projects
|
||||
.map((project) => {
|
||||
const projectDetail = projectDetails.find(
|
||||
(page) => page.id === project.projectId,
|
||||
);
|
||||
|
||||
return {
|
||||
...project,
|
||||
title: projectDetail?.title || 'N/A',
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.repositoryUrl && !b.repositoryUrl) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!a.repositoryUrl && b.repositoryUrl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<section>
|
||||
{isLoading ? (
|
||||
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
|
||||
) : (
|
||||
<div className="flex items-start sm:items-center justify-between flex-col sm:flex-row gap-1">
|
||||
<h2 className="text-lg font-medium">
|
||||
Hi {name}, good {getCurrentPeriod()}!
|
||||
</h2>
|
||||
<a
|
||||
href="/home"
|
||||
className="text-xs font-medium bg-gray-200 hover:bg-gray-300 px-2.5 py-1 rounded-full text-gray-700 hover:text-black"
|
||||
>
|
||||
Visit Homepage
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<DashboardCardSkeleton />
|
||||
<DashboardCardSkeleton />
|
||||
<DashboardCardSkeleton />
|
||||
<DashboardCardSkeleton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DashboardCard
|
||||
imgUrl={avatarLink}
|
||||
title={name!}
|
||||
description="Setup your profile"
|
||||
href="/account/update-profile"
|
||||
/>
|
||||
|
||||
<DashboardCard
|
||||
icon={BookEmoji}
|
||||
title="Visit Roadmaps"
|
||||
description="Learn new skills"
|
||||
href="/roadmaps"
|
||||
/>
|
||||
|
||||
<DashboardCard
|
||||
icon={ConstructionEmoji}
|
||||
title="Build Projects"
|
||||
description="Practice what you learn"
|
||||
href="/projects"
|
||||
/>
|
||||
<DashboardCard
|
||||
icon={CheckEmoji}
|
||||
title="Best Practices"
|
||||
description="Do things the right way"
|
||||
href="/best-practices"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProgressStack
|
||||
progresses={learningRoadmapsToShow}
|
||||
projects={enrichedProjects || []}
|
||||
isLoading={isLoading}
|
||||
accountStreak={accountStreak}
|
||||
topicDoneToday={personalDashboardDetails?.topicDoneToday || 0}
|
||||
/>
|
||||
|
||||
<ListDashboardCustomProgress
|
||||
progresses={customRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<DashboardAiRoadmaps
|
||||
roadmaps={aiGeneratedRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<RecommendedRoadmaps
|
||||
roadmaps={recommendedRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
type DashboardCardProps = {
|
||||
icon?: JSXElementConstructor<any>;
|
||||
imgUrl?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
function DashboardCard(props: DashboardCardProps) {
|
||||
const { icon: Icon, imgUrl, title, description, href } = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="flex flex-col overflow-hidden rounded-lg border border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-50"
|
||||
>
|
||||
{Icon && (
|
||||
<div className="px-4 pb-3 pt-4">
|
||||
<Icon className="size-6" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{imgUrl && (
|
||||
<div className="px-4 pb-1.5 pt-3.5">
|
||||
<img src={imgUrl} alt={title} className="size-8 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex grow flex-col justify-center gap-0.5 p-4">
|
||||
<h3 className="truncate font-medium text-black">{title}</h3>
|
||||
<p className="text-xs text-black">{description}</p>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardCardSkeleton() {
|
||||
return (
|
||||
<div className="h-[128px] animate-pulse rounded-lg border border-gray-300 bg-white"></div>
|
||||
);
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bookmark,
|
||||
FolderKanban,
|
||||
type LucideIcon,
|
||||
Map,
|
||||
} from 'lucide-react';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import { DashboardBookmarkCard } from './DashboardBookmarkCard';
|
||||
import { DashboardProjectCard } from './DashboardProjectCard';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { DashboardProgressCard } from './DashboardProgressCard';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $accountStreak, type StreakResponse } from '../../stores/streak';
|
||||
import { EmptyStackMessage } from './EmptyStackMessage.tsx';
|
||||
|
||||
type ProgressStackProps = {
|
||||
progresses: UserProgress[];
|
||||
projects: (ProjectStatusDocument & {
|
||||
title: string;
|
||||
})[];
|
||||
accountStreak?: StreakResponse;
|
||||
isLoading: boolean;
|
||||
topicDoneToday: number;
|
||||
};
|
||||
|
||||
const MAX_PROGRESS_TO_SHOW = 11;
|
||||
const MAX_PROJECTS_TO_SHOW = 8;
|
||||
|
||||
type ProgressLaneProps = {
|
||||
title: string;
|
||||
linkText?: string;
|
||||
linkHref?: string;
|
||||
isLoading?: boolean;
|
||||
isEmpty?: boolean;
|
||||
loadingWrapperClassName?: string;
|
||||
loadingSkeletonCount?: number;
|
||||
loadingSkeletonClassName?: string;
|
||||
children: React.ReactNode;
|
||||
emptyMessage?: string;
|
||||
emptyIcon?: LucideIcon;
|
||||
emptyLinkText?: string;
|
||||
emptyLinkHref?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ProgressLane(props: ProgressLaneProps) {
|
||||
const {
|
||||
title,
|
||||
linkText,
|
||||
linkHref,
|
||||
isLoading = false,
|
||||
loadingWrapperClassName = '',
|
||||
loadingSkeletonCount = 4,
|
||||
loadingSkeletonClassName = '',
|
||||
children,
|
||||
isEmpty = false,
|
||||
emptyIcon: EmptyIcon = Map,
|
||||
emptyMessage = `No ${title.toLowerCase()} to show`,
|
||||
emptyLinkHref = '/roadmaps',
|
||||
emptyLinkText = 'Explore',
|
||||
className,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full flex-col rounded-md border bg-white px-4 py-3 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className={'flex flex-row justify-between'}>
|
||||
<div className="h-[16px] w-[75px] animate-pulse rounded-md bg-gray-100"></div>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !isEmpty && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-xs uppercase text-gray-500">{title}</h3>
|
||||
|
||||
{linkText && linkHref && (
|
||||
<a
|
||||
href={linkHref}
|
||||
className="flex items-center gap-1 text-xs text-gray-500 hover:text-black"
|
||||
>
|
||||
<ArrowUpRight size={12} />
|
||||
{linkText}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex flex-grow flex-col gap-1.5">
|
||||
{isLoading && (
|
||||
<div
|
||||
className={cn('grid grid-cols-2 gap-2', loadingWrapperClassName)}
|
||||
>
|
||||
{Array.from({ length: loadingSkeletonCount }).map((_, index) => (
|
||||
<CardSkeleton key={index} className={loadingSkeletonClassName} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && children}
|
||||
|
||||
{!isLoading && isEmpty && (
|
||||
<div className="flex flex-grow flex-col items-center justify-center text-gray-500">
|
||||
<EmptyIcon
|
||||
size={37}
|
||||
strokeWidth={1.5}
|
||||
className={'mb-3 text-gray-200'}
|
||||
/>
|
||||
<span className="mb-0.5 text-sm">{emptyMessage}</span>
|
||||
<a
|
||||
href={emptyLinkHref}
|
||||
className="text-xs font-medium text-gray-600 underline-offset-2 hover:underline"
|
||||
>
|
||||
{emptyLinkText}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProgressStack(props: ProgressStackProps) {
|
||||
const { progresses, projects, isLoading, accountStreak, topicDoneToday } =
|
||||
props;
|
||||
|
||||
const [showAllProgresses, setShowAllProgresses] = useState(false);
|
||||
const sortedProgresses = progresses.sort((a, b) => {
|
||||
if (a.isFavorite && !b.isFavorite) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!a.isFavorite && b.isFavorite) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
const userProgressesToShow = showAllProgresses
|
||||
? sortedProgresses
|
||||
: sortedProgresses.slice(0, MAX_PROGRESS_TO_SHOW);
|
||||
|
||||
const [showAllProjects, setShowAllProjects] = useState(false);
|
||||
const projectsToShow = showAllProjects
|
||||
? projects
|
||||
: projects.slice(0, MAX_PROJECTS_TO_SHOW);
|
||||
|
||||
const totalProjectFinished = projects.filter(
|
||||
(project) => project.repositoryUrl,
|
||||
).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
<StatsCard
|
||||
title="Current Streak"
|
||||
value={accountStreak?.count || 0}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Topics Done Today"
|
||||
value={topicDoneToday}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Projects Finished"
|
||||
value={totalProjectFinished}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid min-h-[330px] grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div className="relative col-span-2">
|
||||
{!isLoading && userProgressesToShow.length === 0 && (
|
||||
<EmptyStackMessage
|
||||
number={1}
|
||||
title={'Bookmark some Roadmaps'}
|
||||
description={
|
||||
'Bookmark some roadmaps to access them quickly and start updating your progress'
|
||||
}
|
||||
buttonText={'Explore Roadmaps'}
|
||||
buttonLink={'/roadmaps'}
|
||||
bodyClassName="max-w-[280px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProgressLane
|
||||
title="Progress & Bookmarks"
|
||||
isLoading={isLoading}
|
||||
loadingSkeletonCount={MAX_PROGRESS_TO_SHOW}
|
||||
linkHref="/roadmaps"
|
||||
linkText="Roadmaps"
|
||||
isEmpty={userProgressesToShow.length === 0}
|
||||
emptyIcon={Bookmark}
|
||||
emptyMessage={'No bookmarks to show'}
|
||||
emptyLinkHref={'/roadmaps'}
|
||||
emptyLinkText={'Explore Roadmaps'}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{userProgressesToShow.length > 0 && (
|
||||
<>
|
||||
{userProgressesToShow.map((progress) => {
|
||||
const isFavorite =
|
||||
progress.isFavorite &&
|
||||
!progress.done &&
|
||||
!progress.skipped;
|
||||
|
||||
if (isFavorite) {
|
||||
return (
|
||||
<DashboardBookmarkCard
|
||||
key={progress.resourceId}
|
||||
bookmark={progress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardProgressCard
|
||||
key={progress.resourceId}
|
||||
progress={progress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{sortedProgresses.length > MAX_PROGRESS_TO_SHOW && (
|
||||
<ShowAllButton
|
||||
showAll={showAllProgresses}
|
||||
setShowAll={setShowAllProgresses}
|
||||
count={sortedProgresses.length}
|
||||
maxCount={MAX_PROGRESS_TO_SHOW}
|
||||
className="min-h-[38px] rounded-md border border-dashed leading-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ProgressLane>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<ProgressLane
|
||||
title={'Projects'}
|
||||
linkHref={'/projects'}
|
||||
linkText={'Projects'}
|
||||
isLoading={isLoading}
|
||||
loadingWrapperClassName="grid-cols-1"
|
||||
loadingSkeletonClassName={'h-5'}
|
||||
loadingSkeletonCount={8}
|
||||
isEmpty={projectsToShow.length === 0}
|
||||
emptyMessage={'No projects started'}
|
||||
emptyIcon={FolderKanban}
|
||||
emptyLinkText={'Explore Projects'}
|
||||
emptyLinkHref={'/projects'}
|
||||
>
|
||||
{!isLoading && projectsToShow.length === 0 && (
|
||||
<EmptyStackMessage
|
||||
number={2}
|
||||
title={'Build your first project'}
|
||||
description={'Pick a project to practice and start building'}
|
||||
buttonText={'Explore Projects'}
|
||||
buttonLink={'/projects'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{projectsToShow.map((project) => {
|
||||
return (
|
||||
<DashboardProjectCard
|
||||
key={project.projectId}
|
||||
project={project}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{projects.length > MAX_PROJECTS_TO_SHOW && (
|
||||
<ShowAllButton
|
||||
showAll={showAllProjects}
|
||||
setShowAll={setShowAllProjects}
|
||||
count={projects.length}
|
||||
maxCount={MAX_PROJECTS_TO_SHOW}
|
||||
className="mb-0.5 mt-3"
|
||||
/>
|
||||
)}
|
||||
</ProgressLane>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type ShowAllButtonProps = {
|
||||
showAll: boolean;
|
||||
setShowAll: (showAll: boolean) => void;
|
||||
count: number;
|
||||
maxCount: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ShowAllButton(props: ShowAllButtonProps) {
|
||||
const { showAll, setShowAll, count, maxCount, className } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex w-full items-center justify-center text-sm text-gray-500 hover:text-gray-700',
|
||||
className,
|
||||
)}
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
>
|
||||
{!showAll ? <>+ show {count - maxCount} more</> : <>- show less</>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
type CardSkeletonProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardSkeleton(props: CardSkeletonProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'h-[38px] w-full animate-pulse rounded-md bg-gray-100',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type StatsCardProps = {
|
||||
title: string;
|
||||
value: number;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
function StatsCard(props: StatsCardProps) {
|
||||
const { title, value, isLoading = false } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 rounded-md border bg-white p-4 shadow-sm">
|
||||
<h3 className="mb-1 text-xs uppercase text-gray-500">{title}</h3>
|
||||
{isLoading ? (
|
||||
<CardSkeleton className="h-8" />
|
||||
) : (
|
||||
<span className="text-2xl font-medium text-black">{value}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { BuiltInRoadmap } from './PersonalDashboard';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { MarkFavorite } from '../FeaturedItems/MarkFavorite.tsx';
|
||||
|
||||
type RecommendedRoadmapsProps = {
|
||||
roadmaps: BuiltInRoadmap[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) {
|
||||
const { roadmaps: roadmapsToShow, isLoading } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 mt-8 flex items-center justify-between gap-2">
|
||||
<h2 className="text-xs uppercase text-gray-400">
|
||||
Recommended Roadmaps
|
||||
</h2>
|
||||
|
||||
<a
|
||||
href="/roadmaps"
|
||||
className="flex items-center gap-1 rounded-full bg-gray-500 px-2 py-0.5 text-xs font-medium text-white transition-colors hover:bg-black"
|
||||
>
|
||||
<ArrowUpRight size={12} strokeWidth={2.5} />
|
||||
All Roadmaps
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<RecommendedCardSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
|
||||
{roadmapsToShow.map((roadmap) => (
|
||||
<RecommendedRoadmapCard key={roadmap.id} roadmap={roadmap} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 text-sm text-gray-500">
|
||||
Need some help getting started? Check out our{' '}<a href="/get-started" className="text-blue-600 underline">Getting Started Guide</a>.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type RecommendedRoadmapCardProps = {
|
||||
roadmap: BuiltInRoadmap;
|
||||
};
|
||||
|
||||
export function RecommendedRoadmapCard(props: RecommendedRoadmapCardProps) {
|
||||
const { roadmap } = props;
|
||||
const { title, url, description } = roadmap;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
className="font-regular text-sm sm:text-sm group relative block rounded-lg border border-gray-200 bg-white px-2.5 py-2 text-black no-underline hover:border-gray-400 hover:bg-gray-50"
|
||||
>
|
||||
<MarkFavorite className={'opacity-30'} resourceType={'roadmap'} resourceId={roadmap.id} />
|
||||
{title}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function RecommendedCardSkeleton() {
|
||||
return (
|
||||
<div className="h-[38px] w-full animate-pulse rounded-md bg-gray-200" />
|
||||
);
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { TeamMember } from '../TeamProgress/TeamProgressPage';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { LoadingProgress } from './LoadingProgress';
|
||||
import { ResourceProgress } from '../Activity/ResourceProgress';
|
||||
import { TeamActivityPage } from '../TeamActivity/TeamActivityPage';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
type TeamDashboardProps = {
|
||||
teamId: string;
|
||||
};
|
||||
|
||||
export function TeamDashboard(props: TeamDashboardProps) {
|
||||
const { teamId } = props;
|
||||
|
||||
const toast = useToast();
|
||||
const currentUser = getUser();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
|
||||
|
||||
async function getTeamProgress() {
|
||||
const { response, error } = await httpGet<TeamMember[]>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-progress/${teamId}`,
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Failed to get team progress');
|
||||
return;
|
||||
}
|
||||
|
||||
setTeamMembers(
|
||||
response.sort((a, b) => {
|
||||
if (a.email === currentUser?.email) {
|
||||
return -1;
|
||||
}
|
||||
if (b.email === currentUser?.email) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setTeamMembers([]);
|
||||
getTeamProgress().finally(() => setIsLoading(false));
|
||||
}, [teamId]);
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentMember = teamMembers.find(
|
||||
(member) => member.email === currentUser.email,
|
||||
);
|
||||
const learningRoadmapsToShow =
|
||||
currentMember?.progress?.filter(
|
||||
(progress) => progress.resourceType === 'roadmap',
|
||||
) || [];
|
||||
|
||||
const allMembersWithoutCurrentUser = teamMembers.sort((a, b) => {
|
||||
if (a.email === currentUser.email) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.email === currentUser.email) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-3 text-xs uppercase text-gray-400">Roadmaps</h2>
|
||||
{isLoading && <LoadingProgress />}
|
||||
{!isLoading && learningRoadmapsToShow.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-3">
|
||||
{learningRoadmapsToShow.map((roadmap) => {
|
||||
const learningCount = roadmap.learning || 0;
|
||||
const doneCount = roadmap.done || 0;
|
||||
const totalCount = roadmap.total || 0;
|
||||
const skippedCount = roadmap.skipped || 0;
|
||||
|
||||
return (
|
||||
<ResourceProgress
|
||||
key={roadmap.resourceId}
|
||||
isCustomResource={roadmap?.isCustomResource || false}
|
||||
doneCount={doneCount > totalCount ? totalCount : doneCount}
|
||||
learningCount={
|
||||
learningCount > totalCount ? totalCount : learningCount
|
||||
}
|
||||
totalCount={totalCount}
|
||||
skippedCount={skippedCount}
|
||||
resourceId={roadmap.resourceId}
|
||||
resourceType="roadmap"
|
||||
updatedAt={roadmap.updatedAt}
|
||||
title={roadmap.resourceTitle}
|
||||
showActions={false}
|
||||
roadmapSlug={roadmap.roadmapSlug}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 className="mb-3 mt-6 text-xs uppercase text-gray-400">
|
||||
Team Members
|
||||
</h2>
|
||||
{isLoading && <TeamMemberLoading className="mb-6" />}
|
||||
{!isLoading && (
|
||||
<div className="mb-6 flex flex-wrap gap-2">
|
||||
{allMembersWithoutCurrentUser.map((member) => {
|
||||
const avatar = member?.avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${member.avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
return (
|
||||
<span className="group relative" key={member.email}>
|
||||
<figure className="relative aspect-square size-8 overflow-hidden rounded-md bg-gray-100">
|
||||
<img
|
||||
src={avatar}
|
||||
alt={member.name || ''}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
</figure>
|
||||
<Tooltip position="top-center" additionalClass="text-sm">
|
||||
{member.name}
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TeamActivityPage teamId={teamId} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
type TeamMemberLoadingProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function TeamMemberLoading(props: TeamMemberLoadingProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-2', className)}>
|
||||
{Array.from({ length: 15 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="size-8 animate-pulse rounded-md bg-gray-200"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export function FeatureAnnouncement(props: FeatureAnnouncementProps) {
|
||||
</span>
|
||||
Projects are live on the{' '}
|
||||
<a
|
||||
href={'/projects'}
|
||||
href={'/backend/projects'}
|
||||
className="font-medium text-blue-500 underline underline-offset-2"
|
||||
>
|
||||
backend roadmap
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import type { MouseEvent } from "react";
|
||||
import { httpPatch } from '../../lib/http';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
@@ -7,7 +7,6 @@ import { showLoginPopup } from '../../lib/popup';
|
||||
import { FavoriteIcon } from './FavoriteIcon';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type MarkFavoriteType = {
|
||||
resourceType: ResourceType;
|
||||
@@ -28,9 +27,7 @@ export function MarkFavorite({
|
||||
const toast = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isFavorite, setIsFavorite] = useState(
|
||||
isAuthenticated
|
||||
? (favorite ?? localStorage.getItem(localStorageKey) === '1')
|
||||
: false,
|
||||
isAuthenticated ? (favorite ?? localStorage.getItem(localStorageKey) === '1') : false
|
||||
);
|
||||
|
||||
async function toggleFavoriteHandler(e: MouseEvent<HTMLButtonElement>) {
|
||||
@@ -51,7 +48,7 @@ export function MarkFavorite({
|
||||
{
|
||||
resourceType,
|
||||
resourceId,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -71,7 +68,7 @@ export function MarkFavorite({
|
||||
resourceType,
|
||||
isFavorite: !isFavorite,
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('refresh-favorites', {}));
|
||||
@@ -102,18 +99,11 @@ export function MarkFavorite({
|
||||
aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
onClick={toggleFavoriteHandler}
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'absolute right-1.5 top-1.5 z-30 focus:outline-0',
|
||||
isFavorite ? '' : 'opacity-30 hover:opacity-100',
|
||||
className,
|
||||
)}
|
||||
data-is-favorite={isFavorite}
|
||||
className={`${isFavorite ? '' : 'opacity-30 hover:opacity-100'} ${
|
||||
className || 'absolute right-1.5 top-1.5 z-30 focus:outline-0'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner isDualRing={false} />
|
||||
) : (
|
||||
<FavoriteIcon isFavorite={isFavorite} />
|
||||
)}
|
||||
{isLoading ? <Spinner isDualRing={false} /> : <FavoriteIcon isFavorite={isFavorite} />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ export function ProgressNudge(props: ProgressNudgeProps) {
|
||||
const $totalRoadmapNodes = useStore(totalRoadmapNodes);
|
||||
const $roadmapProgress = useStore(roadmapProgress);
|
||||
|
||||
const done =
|
||||
($roadmapProgress?.done?.length || 0) +
|
||||
($roadmapProgress?.skipped?.length || 0);
|
||||
const done = $roadmapProgress?.done?.length || 0;
|
||||
|
||||
const hasProgress = done > 0;
|
||||
|
||||
@@ -53,8 +51,7 @@ export function ProgressNudge(props: ProgressNudgeProps) {
|
||||
<span className="relative -top-[0.45px] mr-2 text-xs font-medium uppercase text-yellow-400">
|
||||
Progress
|
||||
</span>
|
||||
<span>{done > $totalRoadmapNodes ? $totalRoadmapNodes : done}</span> of{' '}
|
||||
<span>{$totalRoadmapNodes}</span> Done
|
||||
<span>{done > $totalRoadmapNodes ? $totalRoadmapNodes : done}</span> of <span>{$totalRoadmapNodes}</span> Done
|
||||
</span>
|
||||
|
||||
<span
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import { FavoriteRoadmaps } from './FavoriteRoadmaps';
|
||||
import { FeatureAnnouncement } from "../FeatureAnnouncement";
|
||||
---
|
||||
|
||||
@@ -30,4 +31,5 @@ import { FeatureAnnouncement } from "../FeatureAnnouncement";
|
||||
their career.
|
||||
</p>
|
||||
</div>
|
||||
<FavoriteRoadmaps client:only='react' />
|
||||
</div>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { AppError } from '../../api/api';
|
||||
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
|
||||
|
||||
type ErrorPageProps = {
|
||||
error: AppError;
|
||||
};
|
||||
|
||||
export function ErrorPage(props: ErrorPageProps) {
|
||||
const { error } = props;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container py-10">
|
||||
<div className="flex min-h-[250px] flex-col items-center justify-center px-5 py-3 sm:px-0 sm:py-20">
|
||||
<ErrorIcon additionalClasses="mb-4 h-8 w-8 sm:h-14 sm:w-14" />
|
||||
<h2 className="mb-1 text-lg font-semibold sm:text-xl">
|
||||
Oops! Something went wrong
|
||||
</h2>
|
||||
<p className="mb-3 text-balance text-center text-xs text-gray-800 sm:text-sm">
|
||||
{error?.message || 'An error occurred while fetching'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import type {
|
||||
LeadeboardUserDetails,
|
||||
ListLeaderboardStatsResponse,
|
||||
} from '../../api/leaderboard';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { FolderKanban, Zap, Trophy } from 'lucide-react';
|
||||
import { RankBadgeIcon } from '../ReactIcons/RankBadgeIcon';
|
||||
import { TrophyEmoji } from '../ReactIcons/TrophyEmoji';
|
||||
import { SecondPlaceMedalEmoji } from '../ReactIcons/SecondPlaceMedalEmoji';
|
||||
import { ThirdPlaceMedalEmoji } from '../ReactIcons/ThirdPlaceMedalEmoji';
|
||||
|
||||
type LeaderboardPageProps = {
|
||||
stats: ListLeaderboardStatsResponse;
|
||||
};
|
||||
|
||||
export function LeaderboardPage(props: LeaderboardPageProps) {
|
||||
const { stats } = props;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container py-10">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="mb-2 flex items-center justify-center gap-3">
|
||||
<Trophy className="size-8 text-yellow-500" />
|
||||
<h2 className="text-2xl font-bold sm:text-3xl">Leaderboard</h2>
|
||||
</div>
|
||||
<p className="mx-auto max-w-2xl text-balance text-sm text-gray-500 sm:text-base">
|
||||
Top users based on their activity on roadmap.sh
|
||||
</p>
|
||||
|
||||
<div className="mt-8 grid gap-2 md:grid-cols-2">
|
||||
<LeaderboardLane
|
||||
title="Longest Visit Streak"
|
||||
tabs={[
|
||||
{
|
||||
title: 'Active',
|
||||
users: stats.streaks?.active || [],
|
||||
emptyIcon: <Zap className="size-16 text-gray-300" />,
|
||||
emptyText: 'No users with streaks yet',
|
||||
},
|
||||
{
|
||||
title: 'Lifetime',
|
||||
users: stats.streaks?.lifetime || [],
|
||||
emptyIcon: <Zap className="size-16 text-gray-300" />,
|
||||
emptyText: 'No users with streaks yet',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<LeaderboardLane
|
||||
title="Projects Completed"
|
||||
tabs={[
|
||||
{
|
||||
title: 'This Month',
|
||||
users: stats.projectSubmissions.currentMonth,
|
||||
emptyIcon: <FolderKanban className="size-16 text-gray-300" />,
|
||||
emptyText: 'No projects submitted this month',
|
||||
},
|
||||
{
|
||||
title: 'Lifetime',
|
||||
users: stats.projectSubmissions.lifetime,
|
||||
emptyIcon: <FolderKanban className="size-16 text-gray-300" />,
|
||||
emptyText: 'No projects submitted yet',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type LeaderboardLaneProps = {
|
||||
title: string;
|
||||
tabs: {
|
||||
title: string;
|
||||
users: LeadeboardUserDetails[];
|
||||
emptyIcon?: ReactNode;
|
||||
emptyText?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
function LeaderboardLane(props: LeaderboardLaneProps) {
|
||||
const { title, tabs } = props;
|
||||
|
||||
const [activeTab, setActiveTab] = useState(tabs[0]);
|
||||
const { users: usersToShow, emptyIcon, emptyText } = activeTab;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border bg-white shadow-sm">
|
||||
<div className="flex items-center justify-between gap-2 bg-gray-100 px-3 py-3 mb-3">
|
||||
<h3 className="text-base font-medium">{title}</h3>
|
||||
|
||||
{tabs.length > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab === activeTab;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.title}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={cn(
|
||||
'text-sm font-medium underline-offset-2 transition-colors',
|
||||
{
|
||||
'text-black underline': isActive,
|
||||
'text-gray-400 hover:text-gray-600': !isActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{usersToShow.length === 0 && emptyText && (
|
||||
<div className="flex flex-col items-center justify-center p-8">
|
||||
{emptyIcon}
|
||||
<p className="mt-4 text-sm text-gray-500">{emptyText}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usersToShow.length > 0 && (
|
||||
<ul className="divide-y divide-gray-100 pb-4">
|
||||
{usersToShow.map((user, counter) => {
|
||||
const avatar = user?.avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
const rank = counter + 1;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={user.id}
|
||||
className="flex items-center justify-between gap-1 pl-2 pr-5 py-2.5 hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
'relative text-xs mr-1 flex size-6 shrink-0 items-center justify-center rounded-full tabular-nums',
|
||||
{
|
||||
'text-black': rank <= 3,
|
||||
'text-gray-400': rank > 3,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{rank}
|
||||
</span>
|
||||
|
||||
<img
|
||||
src={avatar}
|
||||
alt={user.name}
|
||||
className="size-7 shrink-0 rounded-full"
|
||||
/>
|
||||
<span className="truncate">{user.name}</span>
|
||||
{rank === 1 ? (
|
||||
<TrophyEmoji className="size-5" />
|
||||
) : rank === 2 ? (
|
||||
<SecondPlaceMedalEmoji className="size-5" />
|
||||
) : rank === 3 ? (
|
||||
<ThirdPlaceMedalEmoji className="size-5" />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-gray-500">{user.count}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const links = [
|
||||
Icon: Waypoints,
|
||||
},
|
||||
{
|
||||
link: '/projects',
|
||||
link: '/backend/projects',
|
||||
label: 'Projects',
|
||||
description: 'Skill-up with real-world projects',
|
||||
Icon: FolderKanban,
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface ProjectStatusDocument {
|
||||
|
||||
isVisible?: boolean;
|
||||
|
||||
updatedAt: Date;
|
||||
updated1t: Date;
|
||||
}
|
||||
|
||||
const allowedVoteType = ['upvote', 'downvote'] as const;
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { Box, Filter, Group, X } from 'lucide-react';
|
||||
import {
|
||||
deleteUrlParam,
|
||||
getUrlParams,
|
||||
setUrlParams,
|
||||
} from '../../lib/browser.ts';
|
||||
import { CategoryFilterButton } from '../Roadmaps/CategoryFilterButton.tsx';
|
||||
import {
|
||||
projectDifficulties,
|
||||
type ProjectFileType,
|
||||
} from '../../lib/project.ts';
|
||||
import { ProjectCard } from './ProjectCard.tsx';
|
||||
|
||||
type ProjectGroup = {
|
||||
id: string;
|
||||
title: string;
|
||||
projects: ProjectFileType[];
|
||||
};
|
||||
|
||||
type ProjectsPageProps = {
|
||||
roadmapsProjects: ProjectGroup[];
|
||||
userCounts: Record<string, number>;
|
||||
};
|
||||
|
||||
export function ProjectsPage(props: ProjectsPageProps) {
|
||||
const { roadmapsProjects, userCounts } = props;
|
||||
const allUniqueProjectIds = new Set<string>(
|
||||
roadmapsProjects.flatMap((group) =>
|
||||
group.projects.map((project) => project.id),
|
||||
),
|
||||
);
|
||||
const allUniqueProjects = useMemo(
|
||||
() =>
|
||||
Array.from(allUniqueProjectIds)
|
||||
.map((id) =>
|
||||
roadmapsProjects
|
||||
.flatMap((group) => group.projects)
|
||||
.find((project) => project.id === id),
|
||||
)
|
||||
.filter(Boolean) as ProjectFileType[],
|
||||
[allUniqueProjectIds],
|
||||
);
|
||||
|
||||
const [activeGroup, setActiveGroup] = useState<string>('');
|
||||
const [visibleProjects, setVisibleProjects] =
|
||||
useState<ProjectFileType[]>(allUniqueProjects);
|
||||
|
||||
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const { g } = getUrlParams() as { g: string };
|
||||
if (!g) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveGroup(g);
|
||||
const group = roadmapsProjects.find((group) => group.id === g);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisibleProjects(group.projects);
|
||||
}, []);
|
||||
|
||||
const sortedVisibleProjects = useMemo(
|
||||
() =>
|
||||
visibleProjects.sort((a, b) => {
|
||||
const projectADifficulty = a?.frontmatter.difficulty || 'beginner';
|
||||
const projectBDifficulty = b?.frontmatter.difficulty || 'beginner';
|
||||
return (
|
||||
projectDifficulties.indexOf(projectADifficulty) -
|
||||
projectDifficulties.indexOf(projectBDifficulty)
|
||||
);
|
||||
}),
|
||||
[visibleProjects],
|
||||
);
|
||||
|
||||
const activeGroupDetail = roadmapsProjects.find(
|
||||
(group) => group.id === activeGroup,
|
||||
);
|
||||
|
||||
const requiredSortOrder = [
|
||||
'Frontend',
|
||||
'Backend',
|
||||
'DevOps',
|
||||
'Full-stack',
|
||||
'JavaScript',
|
||||
'Go',
|
||||
'Python',
|
||||
'Node.js',
|
||||
'Java',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border-t bg-gray-100">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsFilterOpen(!isFilterOpen);
|
||||
}}
|
||||
id="filter-button"
|
||||
className={cn(
|
||||
'-mt-1 flex w-full items-center justify-center bg-gray-300 py-2 text-sm text-black focus:shadow-none focus:outline-0 sm:hidden',
|
||||
{
|
||||
'mb-3': !isFilterOpen,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{!isFilterOpen && <Filter size={13} className="mr-1" />}
|
||||
{isFilterOpen && <X size={13} className="mr-1" />}
|
||||
Categories
|
||||
</button>
|
||||
<div className="container relative flex flex-col gap-4 sm:flex-row">
|
||||
<div
|
||||
className={cn(
|
||||
'hidden w-full flex-col from-gray-100 sm:w-[160px] sm:shrink-0 sm:border-r sm:bg-gradient-to-l sm:pt-6',
|
||||
{
|
||||
'hidden sm:flex': !isFilterOpen,
|
||||
'z-50 flex': isFilterOpen,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 -mx-4 w-full bg-white pb-0 shadow-xl sm:sticky sm:top-10 sm:mx-0 sm:bg-transparent sm:pb-20 sm:shadow-none">
|
||||
<div className="grid grid-cols-1">
|
||||
<CategoryFilterButton
|
||||
onClick={() => {
|
||||
setActiveGroup('');
|
||||
setVisibleProjects(allUniqueProjects);
|
||||
setIsFilterOpen(false);
|
||||
deleteUrlParam('g');
|
||||
}}
|
||||
category={'All Projects'}
|
||||
selected={activeGroup === ''}
|
||||
/>
|
||||
|
||||
{roadmapsProjects
|
||||
.sort((a, b) => {
|
||||
const aIndex = requiredSortOrder.indexOf(a.title);
|
||||
const bIndex = requiredSortOrder.indexOf(b.title);
|
||||
|
||||
if (aIndex === -1 && bIndex === -1) {
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
|
||||
if (aIndex === -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (bIndex === -1) {
|
||||
return -1;
|
||||
}
|
||||
return aIndex - bIndex;
|
||||
})
|
||||
.map((group) => (
|
||||
<CategoryFilterButton
|
||||
key={group.id}
|
||||
onClick={() => {
|
||||
setActiveGroup(group.id);
|
||||
setIsFilterOpen(false);
|
||||
document
|
||||
?.getElementById('filter-button')
|
||||
?.scrollIntoView();
|
||||
setVisibleProjects(group.projects);
|
||||
setUrlParams({ g: group.id });
|
||||
}}
|
||||
category={group.title}
|
||||
selected={activeGroup === group.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col pb-20 pt-2 sm:pt-6">
|
||||
<div className="mb-4 flex items-center justify-between text-sm text-gray-500">
|
||||
<h3 className={'flex items-center'}>
|
||||
<Box size={15} className="mr-1" strokeWidth={2} />
|
||||
{activeGroupDetail
|
||||
? `Projects in ${activeGroupDetail?.title}`
|
||||
: 'All Projects'}
|
||||
</h3>
|
||||
<p className="text-left">
|
||||
Matches found ({sortedVisibleProjects.length})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
|
||||
{sortedVisibleProjects.map((project) => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
userCount={userCounts[project.id] || 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||
import { showLoginPopup } from '../../lib/popup.ts';
|
||||
|
||||
export function ProjectsPageHeader() {
|
||||
return (
|
||||
<div className="bg-white py-3 sm:py-12">
|
||||
<div className="container">
|
||||
<div className="flex flex-col items-start bg-white sm:items-center">
|
||||
<h1 className="text-2xl font-bold sm:text-5xl">Project Ideas</h1>
|
||||
<p className="mt-1 text-sm sm:mt-4 sm:text-lg">
|
||||
Browse the ever-growing list of projects ideas and solutions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Flag, Play, Send, Share, Square, StopCircle, X } from 'lucide-react';
|
||||
import { Flag, Play, Send, Share } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { cn } from '../../../lib/classname.ts';
|
||||
import { useStickyStuck } from '../../../hooks/use-sticky-stuck.tsx';
|
||||
import { StepperAction } from './StepperAction.tsx';
|
||||
import { StepperStepSeparator } from './StepperStepSeparator.tsx';
|
||||
import { MilestoneStep } from './MilestoneStep.tsx';
|
||||
import { httpGet, httpPost } from '../../../lib/http.ts';
|
||||
import { httpGet } from '../../../lib/http.ts';
|
||||
import { StartProjectModal } from '../StartProjectModal.tsx';
|
||||
import { getRelativeTimeString } from '../../../lib/date.ts';
|
||||
import { getUser, isLoggedIn } from '../../../lib/jwt.ts';
|
||||
@@ -13,7 +13,6 @@ import { showLoginPopup } from '../../../lib/popup.ts';
|
||||
import { SubmitProjectModal } from '../SubmitProjectModal.tsx';
|
||||
import { useCopyText } from '../../../hooks/use-copy-text.ts';
|
||||
import { CheckIcon } from '../../ReactIcons/CheckIcon.tsx';
|
||||
import { pageProgressMessage } from '../../../stores/page.ts';
|
||||
|
||||
type ProjectStatusResponse = {
|
||||
id?: string;
|
||||
@@ -41,8 +40,6 @@ export function ProjectStepper(props: ProjectStepperProps) {
|
||||
const [isSubmittingProject, setIsSubmittingProject] = useState(false);
|
||||
const { copyText, isCopied } = useCopyText();
|
||||
|
||||
const [isStoppingProject, setIsStoppingProject] = useState(false);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeStep, setActiveStep] = useState<number>(0);
|
||||
const [isLoadingStatus, setIsLoadingStatus] = useState(true);
|
||||
@@ -81,27 +78,6 @@ export function ProjectStepper(props: ProjectStepperProps) {
|
||||
setIsLoadingStatus(false);
|
||||
}
|
||||
|
||||
const stopProject = async () => {
|
||||
setIsStoppingProject(true);
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-stop-project/${projectId}`,
|
||||
{},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setIsStoppingProject(false);
|
||||
setError(error?.message || 'Error stopping project');
|
||||
return;
|
||||
}
|
||||
|
||||
pageProgressMessage.set('Update project status');
|
||||
setActiveStep(0);
|
||||
loadProjectStatus().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsStoppingProject(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadProjectStatus().finally(() => {});
|
||||
}, []);
|
||||
@@ -213,20 +189,6 @@ export function ProjectStepper(props: ProjectStepperProps) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{projectStatus?.startedAt && !projectStatus?.submittedAt && (
|
||||
<button
|
||||
className={cn(
|
||||
'ml-auto hidden items-center gap-1.5 text-sm hover:text-black disabled:opacity-50 sm:flex',
|
||||
)}
|
||||
onClick={stopProject}
|
||||
disabled={isStoppingProject}
|
||||
>
|
||||
<Square className="h-3 w-3 fill-current stroke-[2.5px]" />
|
||||
<span className="hidden md:inline">Stop Working</span>
|
||||
<span className="md:hidden">Stop</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{activeStep >= 2 && (
|
||||
<button
|
||||
className={cn(
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export function BookEmoji(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#3e721d"
|
||||
d="M35 26a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V6.313C1 4.104 6.791 0 9 0h20.625C32.719 0 35 2.312 35 5.375z"
|
||||
></path>
|
||||
<path
|
||||
fill="#ccd6dd"
|
||||
d="M33 30a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V6c0-4.119-.021-4 5-4h21a4 4 0 0 1 4 4z"
|
||||
></path>
|
||||
<path
|
||||
fill="#e1e8ed"
|
||||
d="M31 31a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h24a3 3 0 0 1 3 3z"
|
||||
></path>
|
||||
<path
|
||||
fill="#5c913b"
|
||||
d="M31 32a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V10a4 4 0 0 1 4-4h21a4 4 0 0 1 4 4z"
|
||||
></path>
|
||||
<path
|
||||
fill="#77b255"
|
||||
d="M29 32a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V12a4 4 0 0 1 4-4h19.335C27.544 8 29 9.456 29 11.665z"
|
||||
></path>
|
||||
<path
|
||||
fill="#3e721d"
|
||||
d="M6 6C4.312 6 4.269 4.078 5 3.25C5.832 2.309 7.125 2 9.438 2H11V0H8.281C4.312 0 1 2.5 1 5.375V32a4 4 0 0 0 4 4h2V6z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export function BuildEmoji(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#66757f"
|
||||
d="M28.25 8.513a.263.263 0 0 0-.263-.263h-.475a.263.263 0 0 0-.263.263v11.475c0 .145.117.263.263.263h.475a.263.263 0 0 0 .263-.263z"
|
||||
></path>
|
||||
<g fill="#f19020">
|
||||
<circle cx={27.75} cy={19.75} r={1.5}></circle>
|
||||
<circle cx={27.75} cy={22.25} r={1}></circle>
|
||||
</g>
|
||||
<path
|
||||
fill="#bd2032"
|
||||
d="M33.25 8.25h-4.129L9.946.29L9.944.289h-.001c-.016-.007-.032-.005-.047-.01C9.849.265 9.802.25 9.75.25h-.002a.5.5 0 0 0-.19.038a.5.5 0 0 0-.122.082c-.012.009-.026.014-.037.025a.5.5 0 0 0-.11.164V.56c-.004.009-.003.02-.006.029l-5.541 7.81l-.006.014a.99.99 0 0 0-.486.837v2a1 1 0 0 0 1 1h1.495L2.031 34H.25v2h18.958v-2h-1.74l-3.713-21.75H33.25a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1m-21.769 4L9.75 13.639L8.02 12.25zM9.75 21.3l3.667 2.404l-3.667 2l-3.667-2zm-3.639.71l.474-2.784l1.866 1.223zm4.938-1.561l1.87-1.225l.477 2.789zm-1.299-.866l-2.828-1.885l2.828-2.322l2.828 2.322zm-2.563-3.887l.362-2.127l1.131.928zm3.633-1.198l1.132-.929l.364 2.13zM5.073 8.25L9.25 2.362V6.25h-2a1 1 0 0 0-1 1v1zm.53 16.738l2.73 1.489l-3.29 1.794zM15.443 34H4.067l.686-4.024L9.75 27.25l5.006 2.731zm-1.54-9.015l.562 3.291l-3.298-1.799zM13.25 8.25v-1a1 1 0 0 0-1-1h-2V1.499L26.513 8.25zm2 3h-1.16v-2h1.16zm3 0h-2v-2h2zm3 0h-2v-2h2zm3 0h-2v-2h2zm3 0h-2v-2h2zm3 0h-2v-2h2zm3-.5a.5.5 0 0 1-.5.5h-1.5v-2h1.5a.5.5 0 0 1 .5.5z"
|
||||
></path>
|
||||
<path
|
||||
fill="#4b545d"
|
||||
d="M12.25 7.25h-2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h3v-4z"
|
||||
></path>
|
||||
<path fill="#cdd7df" d="M11.25 7.25h2v4h-2z"></path>
|
||||
<path
|
||||
fill="#66757f"
|
||||
d="M34.844 24v-1H20.656v1h.844v2.469h-.844v1h14.188v-1H34V24z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// twitter bulb emoji
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
type BulbEmojiProps = SVGProps<SVGSVGElement>;
|
||||
|
||||
export function BulbEmoji(props: BulbEmojiProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#FFD983"
|
||||
d="M29 11.06c0 6.439-5 7.439-5 13.44c0 3.098-3.123 3.359-5.5 3.359c-2.053 0-6.586-.779-6.586-3.361C11.914 18.5 7 17.5 7 11.06C7 5.029 12.285.14 18.083.14C23.883.14 29 5.029 29 11.06"
|
||||
></path>
|
||||
<path
|
||||
fill="#CCD6DD"
|
||||
d="M22.167 32.5c0 .828-2.234 2.5-4.167 2.5s-4.167-1.672-4.167-2.5S16.066 32 18 32s4.167-.328 4.167.5"
|
||||
></path>
|
||||
<path
|
||||
fill="#FFCC4D"
|
||||
d="M22.707 10.293a1 1 0 0 0-1.414 0L18 13.586l-3.293-3.293a.999.999 0 1 0-1.414 1.414L17 15.414V26a1 1 0 1 0 2 0V15.414l3.707-3.707a1 1 0 0 0 0-1.414"
|
||||
></path>
|
||||
<path
|
||||
fill="#99AAB5"
|
||||
d="M24 31a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2v-6h12z"
|
||||
></path>
|
||||
<path
|
||||
fill="#CCD6DD"
|
||||
d="M11.999 32a1 1 0 0 1-.163-1.986l12-2a.994.994 0 0 1 1.15.822a1 1 0 0 1-.822 1.15l-12 2a1 1 0 0 1-.165.014m0-4a1 1 0 0 1-.163-1.986l12-2a.995.995 0 0 1 1.15.822a1 1 0 0 1-.822 1.15l-12 2a1 1 0 0 1-.165.014"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export function CheckEmoji(props: SVGProps<SVGSVGElement>) {
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 36 36" {...props}><path fill="#77b255" d="M36 32a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4h28a4 4 0 0 1 4 4z"></path><path fill="#fff" d="M29.28 6.362a2.5 2.5 0 0 0-3.458.736L14.936 23.877l-5.029-4.65a2.5 2.5 0 1 0-3.394 3.671l7.209 6.666c.48.445 1.09.665 1.696.665c.673 0 1.534-.282 2.099-1.139c.332-.506 12.5-19.27 12.5-19.27a2.5 2.5 0 0 0-.737-3.458"></path></svg>);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export function ConstructionEmoji(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#ffcc4d"
|
||||
d="M36 15a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h28a4 4 0 0 1 4 4z"
|
||||
></path>
|
||||
<path
|
||||
fill="#292f33"
|
||||
d="M6 3H4a4 4 0 0 0-4 4v2zm6 0L0 15c0 1.36.682 2.558 1.72 3.28L17 3zM7 19h5L28 3h-5zm16 0L35.892 6.108A4 4 0 0 0 33.64 3.36L18 19zm13-4v-3l-7 7h3a4 4 0 0 0 4-4"
|
||||
></path>
|
||||
<path fill="#99aab5" d="M4 19h5v14H4zm23 0h5v14h-5z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export function RankBadgeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M0 0L11 0V10.0442L5.73392 6.32786L0 10.0442L0 0Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export function SecondPlaceMedalEmoji(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path fill="#55acee" d="m18 8l-7-8H0l14 17l11.521-4.75z"></path>
|
||||
<path fill="#3b88c3" d="m25 0l-7 8l5.39 7.312l1.227-1.489L36 0z"></path>
|
||||
<path
|
||||
fill="#ccd6dd"
|
||||
d="M23.205 16.026c.08-.217.131-.448.131-.693a2 2 0 0 0-2-2h-6.667a2 2 0 0 0-2 2c0 .245.05.476.131.693c-3.258 1.826-5.464 5.307-5.464 9.307C7.335 31.224 12.111 36 18.002 36s10.667-4.776 10.667-10.667c0-4-2.206-7.481-5.464-9.307"
|
||||
></path>
|
||||
<path
|
||||
fill="#627077"
|
||||
d="M22.002 28.921h-3.543c.878-1.234 2.412-3.234 3.01-4.301c.449-.879.729-1.439.729-2.43c0-2.076-1.57-3.777-4.244-3.777c-2.225 0-3.74 1.832-3.74 1.832c-.131.15-.112.374.019.487l1.141 1.159a.36.36 0 0 0 .523 0c.355-.393 1.047-.935 1.813-.935c1.047 0 1.646.635 1.646 1.346c0 .523-.243 1.047-.486 1.421c-1.104 1.682-3.871 5.441-4.955 6.862v.374c0 .188.149.355.355.355h7.732a.37.37 0 0 0 .355-.355v-1.682a.367.367 0 0 0-.355-.356"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export function ThirdPlaceMedalEmoji(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path fill="#55ACEE" d="m18 8l-7-8H0l14 17l11.521-4.75z"></path>
|
||||
<path fill="#3B88C3" d="m25 0l-7 8l5.39 7.312l1.227-1.489L36 0z"></path>
|
||||
<path
|
||||
fill="#FF8A3B"
|
||||
d="M23.205 16.026c.08-.217.131-.448.131-.693a2 2 0 0 0-2-2h-6.667a2 2 0 0 0-2 2c0 .245.05.476.131.693c-3.258 1.826-5.464 5.307-5.464 9.307C7.335 31.224 12.111 36 18.002 36s10.667-4.776 10.667-10.667c0-4-2.206-7.481-5.464-9.307"
|
||||
></path>
|
||||
<path
|
||||
fill="#7C4119"
|
||||
d="m14.121 29.35l1.178-1.178a.345.345 0 0 1 .467-.038s1.159.861 2.056.861c.805 0 1.628-.673 1.628-1.496s-.842-1.514-2.225-1.514h-.639a.367.367 0 0 1-.354-.355v-1.552c0-.206.168-.355.354-.355h.639c1.309 0 2-.635 2-1.439c0-.805-.691-1.402-1.496-1.402c-.823 0-1.346.43-1.626.747c-.132.15-.355.15-.504.02l-1.141-1.122c-.151-.132-.132-.355 0-.486c0 0 1.533-1.646 3.57-1.646c2.169 0 4.039 1.328 4.039 3.422c0 1.439-1.085 2.505-1.926 2.897v.057c.879.374 2.262 1.533 2.262 3.141c0 2.038-1.776 3.572-4.357 3.572c-2.354 0-3.552-1.16-3.944-1.664c-.113-.134-.093-.34.019-.47"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export function TrophyEmoji(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 36 36"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#ffac33"
|
||||
d="M5.123 5h6C12.227 5 13 4.896 13 6V4c0-1.104-.773-2-1.877-2h-8c-2 0-3.583 2.125-3 5c0 0 1.791 9.375 1.917 9.958C2.373 18.5 4.164 20 6.081 20h6.958c1.105 0-.039-1.896-.039-3v-2c0 1.104-.773 2-1.877 2h-4c-1.104 0-1.833-1.042-2-2S3.539 7.667 3.539 7.667C3.206 5.75 4.018 5 5.123 5m25.812 0h-6C23.831 5 22 4.896 22 6V4c0-1.104 1.831-2 2.935-2h8c2 0 3.584 2.125 3 5c0 0-1.633 9.419-1.771 10c-.354 1.5-2.042 3-4 3h-7.146C21.914 20 22 18.104 22 17v-2c0 1.104 1.831 2 2.935 2h4c1.104 0 1.834-1.042 2-2s1.584-7.333 1.584-7.333C32.851 5.75 32.04 5 30.935 5M20.832 22c0-6.958-2.709 0-2.709 0s-3-6.958-3 0s-3.291 10-3.291 10h12.292c-.001 0-3.292-3.042-3.292-10"
|
||||
></path>
|
||||
<path
|
||||
fill="#ffcc4d"
|
||||
d="M29.123 6.577c0 6.775-6.77 18.192-11 18.192s-11-11.417-11-18.192c0-5.195 1-6.319 3-6.319c1.374 0 6.025-.027 8-.027l7-.001c2.917-.001 4 .684 4 6.347"
|
||||
></path>
|
||||
<path
|
||||
fill="#c1694f"
|
||||
d="M27 33c0 1.104.227 2-.877 2h-16C9.018 35 9 34.104 9 33v-1c0-1.104 1.164-2 2.206-2h13.917c1.042 0 1.877.896 1.877 2z"
|
||||
></path>
|
||||
<path
|
||||
fill="#c1694f"
|
||||
d="M29 34.625c0 .76.165 1.375-1.252 1.375H8.498C7.206 36 7 35.385 7 34.625v-.25C7 33.615 7.738 33 8.498 33h19.25c.759 0 1.252.615 1.252 1.375z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
interface TwitterIconProps {
|
||||
className?: string;
|
||||
boxColor?: string;
|
||||
}
|
||||
|
||||
export function TwitterIcon(props: TwitterIconProps) {
|
||||
const { className, boxColor = 'transparent' } = props;
|
||||
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg
|
||||
width="23"
|
||||
@@ -15,10 +13,10 @@ export function TwitterIcon(props: TwitterIconProps) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="23" height="23" rx="3" fill={boxColor} />
|
||||
<rect width="23" height="23" rx="3" fill="currentColor" />
|
||||
<path
|
||||
d="M12.9285 10.3522L18.5135 4H17.1905L12.339 9.5144L8.467 4H4L9.8565 12.3395L4 19H5.323L10.443 13.1754L14.533 19H19M5.8005 4.97619H7.833L17.1895 18.0718H15.1565"
|
||||
fill='currentColor'
|
||||
fill="#E5E5E5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,6 @@ import { cn } from '../../lib/classname.ts';
|
||||
import { Filter, X } from 'lucide-react';
|
||||
import { CategoryFilterButton } from './CategoryFilterButton.tsx';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click.ts';
|
||||
import {
|
||||
deleteUrlParam,
|
||||
getUrlParams,
|
||||
setUrlParams,
|
||||
} from '../../lib/browser.ts';
|
||||
|
||||
const groupNames = [
|
||||
'Absolute Beginners',
|
||||
@@ -473,15 +468,6 @@ export function RoadmapsPage() {
|
||||
]);
|
||||
}, [activeGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
const { g } = getUrlParams() as { g: AllowGroupNames };
|
||||
if (!g) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveGroup(g);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="border-t bg-gray-100">
|
||||
<button
|
||||
@@ -516,7 +502,6 @@ export function RoadmapsPage() {
|
||||
onClick={() => {
|
||||
setActiveGroup('');
|
||||
setIsFilterOpen(false);
|
||||
deleteUrlParam('g');
|
||||
}}
|
||||
category={'All Roadmaps'}
|
||||
selected={activeGroup === ''}
|
||||
@@ -529,7 +514,6 @@ export function RoadmapsPage() {
|
||||
setActiveGroup(group.group);
|
||||
setIsFilterOpen(false);
|
||||
document?.getElementById('filter-button')?.scrollIntoView();
|
||||
setUrlParams({ g: group.group });
|
||||
}}
|
||||
category={group.group}
|
||||
selected={activeGroup === group.group}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function ShareRoadmapButton(props: ShareRoadmapButtonProps) {
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 z-[999] mt-1 w-40 rounded-md bg-slate-800 text-sm text-white shadow-lg ring-1 ring-black ring-opacity-5 w-[175px]">
|
||||
<div className="absolute right-0 z-[999] mt-1 w-40 rounded-md bg-slate-800 text-sm text-white shadow-lg ring-1 ring-black ring-opacity-5">
|
||||
<div className="flex flex-col px-1 py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -103,7 +103,7 @@ export function ShareRoadmapButton(props: ShareRoadmapButtonProps) {
|
||||
target={'_blank'}
|
||||
className="mt-1 flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] flex-shrink-0 items-center justify-center">
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<TwitterIcon className="h-[16px] text-slate-400" />
|
||||
</div>
|
||||
Twitter
|
||||
|
||||
@@ -49,13 +49,8 @@ type GetTeamActivityResponse = {
|
||||
perPage: number;
|
||||
};
|
||||
|
||||
type TeamActivityPageProps = {
|
||||
teamId?: string;
|
||||
};
|
||||
|
||||
export function TeamActivityPage(props: TeamActivityPageProps) {
|
||||
const { teamId: defaultTeamId } = props;
|
||||
const { t: teamId = defaultTeamId } = getUrlParams();
|
||||
export function TeamActivityPage() {
|
||||
const { t: teamId } = getUrlParams();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@@ -97,18 +92,6 @@ export function TeamActivityPage(props: TeamActivityPageProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setTeamActivities({
|
||||
data: {
|
||||
users: [],
|
||||
activities: [],
|
||||
},
|
||||
totalCount: 0,
|
||||
totalPages: 0,
|
||||
currPage: 1,
|
||||
perPage: 21,
|
||||
});
|
||||
setCurrPage(1);
|
||||
getTeamProgress().then(() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -12,7 +12,6 @@ type TeamMemberProps = {
|
||||
userId: string;
|
||||
index: number;
|
||||
teamId: string;
|
||||
canViewProgress: boolean;
|
||||
canManageCurrentTeam: boolean;
|
||||
onDeleteMember: () => void;
|
||||
onUpdateMember: () => void;
|
||||
@@ -30,12 +29,11 @@ export function TeamMemberItem(props: TeamMemberProps) {
|
||||
userId,
|
||||
onDeleteMember,
|
||||
onSendProgressReminder,
|
||||
canViewProgress = true,
|
||||
} = props;
|
||||
|
||||
const currentTeam = useStore($currentTeam);
|
||||
const canManageTeam = useStore($canManageCurrentTeam);
|
||||
const showNoProgressBadge = canViewProgress && !member.hasProgress && member.status === 'joined';
|
||||
const showNoProgressBadge = !member.hasProgress && member.status === 'joined';
|
||||
const allowProgressReminder =
|
||||
canManageTeam &&
|
||||
!member.hasProgress &&
|
||||
|
||||
@@ -61,7 +61,7 @@ export function TeamMembersPage() {
|
||||
|
||||
async function loadTeam() {
|
||||
const { response, error } = await httpGet<TeamDocument>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}`,
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}`
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
@@ -75,7 +75,7 @@ export function TeamMembersPage() {
|
||||
|
||||
async function getTeamMemberList() {
|
||||
const { response, error } = await httpGet<TeamMemberItem[]>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-list/${teamId}`,
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-list/${teamId}`
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Failed to load team member list');
|
||||
@@ -100,7 +100,7 @@ export function TeamMembersPage() {
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-delete-member/${teamId}/${memberId}`,
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -118,7 +118,7 @@ export function TeamMembersPage() {
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-resend-invite/${teamId}/${memberId}`,
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -135,7 +135,7 @@ export function TeamMembersPage() {
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-send-progress-reminder/${teamId}/${memberId}`,
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -147,13 +147,13 @@ export function TeamMembersPage() {
|
||||
}
|
||||
|
||||
const joinedMembers = teamMembers.filter(
|
||||
(member) => member.status === 'joined',
|
||||
(member) => member.status === 'joined'
|
||||
);
|
||||
const invitedMembers = teamMembers.filter(
|
||||
(member) => member.status === 'invited',
|
||||
(member) => member.status === 'invited'
|
||||
);
|
||||
const rejectedMembers = teamMembers.filter(
|
||||
(member) => member.status === 'rejected',
|
||||
(member) => member.status === 'rejected'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -205,11 +205,6 @@ export function TeamMembersPage() {
|
||||
index={index}
|
||||
teamId={teamId}
|
||||
userId={user?.id!}
|
||||
canViewProgress={
|
||||
canManageCurrentTeam ||
|
||||
!team?.personalProgressOnly ||
|
||||
String(member.userId) === user?.id
|
||||
}
|
||||
onResendInvite={() => {
|
||||
resendInvite(teamId, member._id!).finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
@@ -246,7 +241,6 @@ export function TeamMembersPage() {
|
||||
index={index}
|
||||
teamId={teamId}
|
||||
userId={user?.id!}
|
||||
canViewProgress={false}
|
||||
onResendInvite={() => {
|
||||
resendInvite(teamId, member._id!).finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
@@ -275,9 +269,7 @@ export function TeamMembersPage() {
|
||||
|
||||
{rejectedMembers.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xs uppercase text-gray-400">
|
||||
Rejected Invites
|
||||
</h3>
|
||||
<h3 className="text-xs uppercase text-gray-400">Rejected Invites</h3>
|
||||
<div className="mt-2 rounded-b-sm rounded-t-md border">
|
||||
{rejectedMembers.map((member, index) => {
|
||||
return (
|
||||
@@ -286,7 +278,6 @@ export function TeamMembersPage() {
|
||||
member={member}
|
||||
index={index}
|
||||
teamId={teamId}
|
||||
canViewProgress={false}
|
||||
userId={user?.id!}
|
||||
onResendInvite={() => {
|
||||
resendInvite(teamId, member._id!).finally(() => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { getUrlParams, setUrlParams } from '../../lib/browser';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { MemberProgressModal } from './MemberProgressModal';
|
||||
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
|
||||
import { canManageCurrentRoadmap } from '../../stores/roadmap.ts';
|
||||
|
||||
export type UserProgress = {
|
||||
resourceTitle: string;
|
||||
@@ -24,7 +23,6 @@ export type UserProgress = {
|
||||
updatedAt: string;
|
||||
isCustomResource?: boolean;
|
||||
roadmapSlug?: string;
|
||||
aiRoadmapId?: string;
|
||||
};
|
||||
|
||||
export type TeamMember = {
|
||||
@@ -193,7 +191,7 @@ export function TeamProgressPage() {
|
||||
key={grouping.value}
|
||||
className={`rounded-md border p-1 px-2 text-sm ${
|
||||
selectedGrouping === grouping.value
|
||||
? 'border-gray-400 bg-gray-200'
|
||||
? ' border-gray-400 bg-gray-200 '
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => setSelectedGrouping(grouping.value)}
|
||||
@@ -225,32 +223,21 @@ export function TeamProgressPage() {
|
||||
)}
|
||||
{selectedGrouping === 'member' && (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{teamMembers.map((member) => {
|
||||
const canViewMemberProgress =
|
||||
currentTeam?.role !== 'member' ||
|
||||
!currentTeam?.personalProgressOnly ||
|
||||
member.email === user?.email;
|
||||
|
||||
if (!canViewMemberProgress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MemberProgressItem
|
||||
key={member._id}
|
||||
member={member}
|
||||
teamId={teamId}
|
||||
isMyProgress={member?.email === user?.email}
|
||||
onShowResourceProgress={(resourceId, isCustomResource) => {
|
||||
setShowMemberProgress({
|
||||
resourceId,
|
||||
member,
|
||||
isCustomResource,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{teamMembers.map((member) => (
|
||||
<MemberProgressItem
|
||||
key={member._id}
|
||||
member={member}
|
||||
teamId={teamId}
|
||||
isMyProgress={member?.email === user?.email}
|
||||
onShowResourceProgress={(resourceId, isCustomResource) => {
|
||||
setShowMemberProgress({
|
||||
resourceId,
|
||||
member,
|
||||
isCustomResource,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -307,7 +307,6 @@ export function TeamRoadmaps() {
|
||||
{pickRoadmapOptionModal}
|
||||
{addRoadmapModal}
|
||||
{createRoadmapModal}
|
||||
{confirmationContentIdModal}
|
||||
|
||||
<RoadmapIcon className="mb-4 h-24 w-24 opacity-10" />
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { ProjectPageType } from '../../api/roadmap';
|
||||
import type { GetPublicProfileResponse } from '../../api/user';
|
||||
import { PrivateProfileBanner } from './PrivateProfileBanner';
|
||||
import { UserActivityHeatmap } from './UserPublicActivityHeatmap';
|
||||
import { UserPublicProfileHeader } from './UserPublicProfileHeader';
|
||||
import { UserPublicProgresses } from './UserPublicProgresses';
|
||||
import { UserPublicProjects } from './UserPublicProjects';
|
||||
|
||||
type UserPublicProfilePageProps = GetPublicProfileResponse & {
|
||||
projectDetails: ProjectPageType[];
|
||||
};
|
||||
type UserPublicProfilePageProps = GetPublicProfileResponse;
|
||||
|
||||
export function UserPublicProfilePage(props: UserPublicProfilePageProps) {
|
||||
const {
|
||||
@@ -18,11 +14,10 @@ export function UserPublicProfilePage(props: UserPublicProfilePageProps) {
|
||||
profileVisibility,
|
||||
_id: userId,
|
||||
createdAt,
|
||||
projectDetails,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="min-h-full flex-grow bg-gray-200/40 pb-36 pt-10">
|
||||
<div className="bg-gray-200/40 min-h-full flex-grow pt-10 pb-36">
|
||||
<div className="container flex flex-col gap-8">
|
||||
<PrivateProfileBanner
|
||||
isOwnProfile={isOwnProfile}
|
||||
@@ -32,19 +27,12 @@ export function UserPublicProfilePage(props: UserPublicProfilePageProps) {
|
||||
<UserPublicProfileHeader userDetails={props!} />
|
||||
|
||||
<UserActivityHeatmap joinedAt={createdAt} activity={activity!} />
|
||||
<div>
|
||||
<UserPublicProgresses
|
||||
username={username!}
|
||||
userId={userId!}
|
||||
roadmaps={props.roadmaps}
|
||||
publicConfig={props.publicConfig}
|
||||
/>
|
||||
<UserPublicProjects
|
||||
userId={userId!}
|
||||
projects={props.projects}
|
||||
projectDetails={projectDetails}
|
||||
/>
|
||||
</div>
|
||||
<UserPublicProgresses
|
||||
username={username!}
|
||||
userId={userId!}
|
||||
roadmaps={props.roadmaps}
|
||||
publicConfig={props.publicConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { ProjectPageType } from '../../api/roadmap';
|
||||
import { ProjectProgress } from '../Activity/ProjectProgress';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
|
||||
type UserPublicProjectsProps = {
|
||||
userId: string;
|
||||
projects: ProjectStatusDocument[];
|
||||
projectDetails: ProjectPageType[];
|
||||
};
|
||||
|
||||
export function UserPublicProjects(props: UserPublicProjectsProps) {
|
||||
const { projects, projectDetails } = props;
|
||||
|
||||
const enrichedProjects =
|
||||
projects
|
||||
.map((project) => {
|
||||
const projectDetail = projectDetails.find(
|
||||
(projectDetail) => projectDetail.id === project.projectId,
|
||||
);
|
||||
|
||||
return {
|
||||
...project,
|
||||
title: projectDetail?.title || 'N/A',
|
||||
};
|
||||
})
|
||||
?.sort((a, b) => {
|
||||
const isPendingA = !a.repositoryUrl && !a.submittedAt;
|
||||
const isPendingB = !b.repositoryUrl && !b.submittedAt;
|
||||
|
||||
if (isPendingA && !isPendingB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!isPendingA && isPendingB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<h2 className="mb-2 text-xs uppercase tracking-wide text-gray-400">
|
||||
Projects I have worked on
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
|
||||
{enrichedProjects.map((project) => (
|
||||
<ProjectProgress
|
||||
key={project._id}
|
||||
projectStatus={project}
|
||||
showActions={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'What is a DevOps Engineer? Responsibilities & Roles in @currentYear@'
|
||||
title: 'What is a DevOps Engineer? Responsbilities & Roles in @currentYear@'
|
||||
description: 'Explore the responsibilities and roles of a DevOps Engineer in @currentYear@. Gain insights into the evolving field of DevOps and what it takes to succeed.'
|
||||
authorId: ekene
|
||||
excludedBySlug: '/devops/devops-engineer'
|
||||
@@ -21,13 +21,13 @@ tags:
|
||||
|
||||

|
||||
|
||||
Are you a developer monitoring recent changes in the ecosystem, looking to change careers or pick up new skills in 2024? If your choice is [DevOps](https://roadmap.sh/devops), you might be wondering what it entails, what it will take to become one in 2024, and how it is affected by the recent changes in the tech ecosystem.
|
||||
Are you a developer monitoring recent changes in the ecosystem, looking to change careers or pick up new skills in 2024? If your choice is DevOps, you might be wondering what it entails, what it will take to become one in 2024, and how it is affected by the recent changes in the tech ecosystem.
|
||||
|
||||
In recent years, the technology ecosystem has experienced a constant shift in the way hiring managers reach out, companies hire, and the roles and responsibilities described in job postings. Particularly, 2023 proved to be a challenging year as layoffs in the technology sector grew significantly, with more than [262,000 employees laid off across 1,180 firms](https://www.statista.com/statistics/199999/worldwide-tech-layoffs-covid-19/).
|
||||
|
||||
Despite this change, DevOps, a field within the same ecosystem, has experienced continuous growth. In fact, the DevOps market size is expected to grow to [25.5 billion USD by 2028](https://www.marketsandmarkets.com/Market-Reports/devops-market-824.html#:~:text=The%20global%20DevOps%20market%20size,USD%2010.4%20billion%20in%202023.). This indicates that the roles and responsibilities of a DevOps engineer in the modern technology environment will evolve alongside this increasing demand.
|
||||
|
||||
In this guide, we'll discuss the roles and responsibilities of a DevOps engineer, the importance of DevOps in teams, common roles within a DevOps team, and best practices for DevOps teams. Finally, the guide will offer roadmaps for your DevOps journey.
|
||||
In this guide, we'll discuss the roles and responsibilities of a [DevOps engineer](https://roadmap.sh/devops), the importance of DevOps in teams, common roles within a DevOps team, and best practices for DevOps teams. Finally, the guide will offer roadmaps for your DevOps journey.
|
||||
|
||||
A DevOps engineer's roles and responsibilities include:
|
||||
|
||||
|
||||
@@ -1,472 +0,0 @@
|
||||
---
|
||||
title: '10+ In-Demand DevOps Engineer Skills to Master'
|
||||
description: 'Find out exactly what it takes to be a successful DevOps engineer with my recommendations for essential DevOps skills'
|
||||
authorId: fernando
|
||||
excludedBySlug: '/devops/skills'
|
||||
seo:
|
||||
title: '10+ In-Demand DevOps Engineer Skills to Master'
|
||||
description: 'Find out exactly what it takes to be a successful DevOps engineer with my recommendations for essential DevOps skills'
|
||||
ogImageUrl: 'https://assets.roadmap.sh/guest/devops-engineer-skills-tlace.jpg'
|
||||
isNew: true
|
||||
type: 'textual'
|
||||
date: 2024-09-12
|
||||
sitemap:
|
||||
priority: 0.7
|
||||
changefreq: 'weekly'
|
||||
tags:
|
||||
- 'guide'
|
||||
- 'textual-guide'
|
||||
- 'guide-sitemap'
|
||||
---
|
||||
|
||||

|
||||
|
||||
The role of the DevOps engineer is not always very well defined; some companies see it as the old-school sysadmin whose sole purpose is to take care of the platform's infrastructure. Others see it as the person in charge of the Terraform configuration files. In the end, properly understanding what DevOps is and what you should expect from this role is critical to properly taking advantage of it and adding the value it’s meant to be adding to your company.
|
||||
|
||||
While you can work on becoming a DevOps engineer from scratch (there is actually a [DevOps roadmap for that](https://roadmap.sh/devops)), usually, a DevOps engineer is someone who has spent enough years either as a developer or in an operations role and wants to start helping solve the problems they’ve experienced throughout their entire career. This person sits between both sides and has intimate knowledge of one of them and a great deal of knowledge about the other side.
|
||||
|
||||
With that said, understanding everything there is to know to become a DevOps engineer who excels at their job is not trivial, and that’s why in this article, we’re going to cover the top 10 DevOps skills to help you level up your game.
|
||||
|
||||
The top 10 DevOps engineer skills to master are:
|
||||
|
||||
1. Understanding Linux and some scripting languages.
|
||||
2. Knowing how to set up your CI/CD pipelines.
|
||||
3. Embracing containerization and orchestration.
|
||||
4. Learning about Infrastructure as Code.
|
||||
5. Understanding cloud computing.
|
||||
6. Knowing how to monitor your infrastructure and manage your logs.
|
||||
7. Having a good grasp of security practices and tools.
|
||||
8. Know how to set up your networking and what that entails for your infrastructure.
|
||||
9. Knowing about version control.
|
||||
10. And finally, understanding configuration management.
|
||||
|
||||
Now, let’s get started.
|
||||
|
||||
## 1\. Proficiency in Linux and Scripting
|
||||
|
||||

|
||||
|
||||
Linux is one of the most common operating systems in the world of software development because of its incredible support, performance, and flexibility, which makes mastering it one of the main DevOps skills to work on.
|
||||
|
||||
Granted, the word “master” is loaded and there are many aspects of the OS that you don’t really need to worry about these days (with all the containers and IaC tools around), however without pushing yourself too hard and becoming a full-blown developer, investing part of your time into learning one or more scripting languages is definitely a good call.
|
||||
|
||||
As a DevOps engineer, you will be scripting and automating tasks, so pick a couple of popular scripting languages and make sure you understand them enough to get the job done. For example, picking Bash is a safe bet, as Bash is the native scripting language in most Linux distros. On top of that, you can pick something like Python or Ruby; both are great options. With an English-like syntax that’s very easy to read and understand and a set of very powerful automation libraries and DevOps tools available, you should be more than fine. For example, if you’re picking Python, you’ll be able to work on Ansible playbooks or custom modules, and if you go with Ruby, you can write Chef cookbooks.
|
||||
|
||||
In the end, it’s either about your own preference or the company’s standards if there are any, just pick one and figure out the tools at your disposal.
|
||||
|
||||
## 2\. Understanding of Continuous Integration and Continuous Deployment (CI/CD)
|
||||
|
||||

|
||||
|
||||
Continuous Integration and Continuous Deployment (CI/CD) form the backbone of a successful DevOps methodology. As a DevOps engineer, mastering CI/CD is non-negotiable.
|
||||
|
||||
### Understanding CI/CD
|
||||
|
||||
At its core, Continuous Integration (CI) is about automatically integrating code changes from multiple contributors into a shared repository as many times a day as needed (which can be one, zero, or hundreds; the number should be irrelevant).
|
||||
|
||||
The idea is to catch and fix integration bugs early and often, which is crucial for maintaining the health of your project.
|
||||
|
||||
On the other hand, Continuous Deployment (CD) takes this a step further by automatically deploying the integrated code to production environments once it passes all necessary tests. Together, both practices minimize manual intervention, reducing errors and allowing for rapid and reliable delivery of software.
|
||||
|
||||
### Key Tools for CI/CD
|
||||
|
||||
To effectively implement CI/CD pipelines, you'll need to be proficient with the tools that make it possible. There are tons of them out there; some of the most common (and arguably, best ones) are:
|
||||
|
||||
* **Jenkins**: An open-source automation server, Jenkins is highly customizable and supports a huge number of integration plugins.
|
||||
* **GitLab CI**: Part of the larger GitLab platform, GitLab CI is tightly integrated with GitLab's version control and issue-tracking features.
|
||||
* **CircleCI**: Known for its speed and simplicity, CircleCI is perfect for environments that prioritize cloud-native solutions. It provides a user-friendly interface and integrates well with popular tools like Docker, AWS, and Kubernetes.
|
||||
* **GitHub Actions**: GitHub Actions is a powerful CI/CD tool integrated directly into GitHub, allowing developers to automate, test, and deploy their code right from their repositories. It supports custom workflows, integration with other DevOps tools, and provides flexibility to run complex automation tasks across multiple environments.
|
||||
|
||||
### Best Practices for Setting Up and Managing CI/CD Pipelines
|
||||
|
||||
Setting up and managing CI/CD pipelines isn’t just about getting the tools to work; it’s about ensuring that they work well (whatever “well” means for your specific context).
|
||||
|
||||
Here are some best practices to follow:
|
||||
|
||||
1. **Start Small and Iterate**: Don’t try to automate everything at once. Start with the most critical parts of your workflow, then gradually expand the pipeline.
|
||||
2. **Ensure Fast Feedback**: The faster your CI/CD pipeline can provide feedback, the quicker your team can address issues.
|
||||
3. **Maintain a Stable Master Branch**: Always keep your master branch in a deployable state. Implement branch protection rules and require code reviews and automated tests to pass before any changes are merged.
|
||||
4. **Automate Everything Possible**: From testing to deployment, automate as many steps in your pipeline as possible.
|
||||
5. **Monitor and Optimize**: Continuously monitor your CI/CD pipelines for performance bottlenecks, failures, and inefficiencies. Use this data to refine your process.
|
||||
6. **Security Considerations**: Integrate security checks into your CI/CD pipelines to catch vulnerabilities early. Tools like static code analysis, dependency checking, and container scanning can help ensure that your code is secure before it reaches production.
|
||||
|
||||
## 3\. Containerization and Orchestration
|
||||
|
||||
These technologies are at the heart of modern DevOps practices, enabling scalability, portability, and efficiency.
|
||||
|
||||
### Basics of Containerization
|
||||
|
||||
Containerization is a method of packaging applications and their dependencies into isolated units called containers. Unlike traditional virtual machines, which require a full operating system, containers share the host OS’s kernel while running isolated user spaces.
|
||||
|
||||
This makes containers not only lightweight but also faster to start, and more resource-efficient.
|
||||
|
||||
There are many benefits to this technology, as you can probably glean by now, but the primary benefits include:
|
||||
|
||||
* **Portability**: Containers encapsulate everything an application needs to run, making it easy to move them across different environments. As long as there is a way to run containers in an OS, then your code can run on it.
|
||||
* **Scalability**: Containers can be easily scaled up or down based on demand. This flexibility is crucial when you need to handle dynamic workloads.
|
||||
* **Consistency**: By using containers, you can ensure that your applications run the same way across different environments, reducing the infamous "it works on my machine" problem.
|
||||
* **Isolation**: With container applications, don’t use resources outside of the ones defined for them. This means each application is isolated from others running on the same host server, avoiding interference.
|
||||
|
||||
### Key Containerization Tools
|
||||
|
||||
When it comes to containerization tools, Docker is the most popular and widely adopted alternative. However, other tools like Podman are also gaining traction, especially in environments that prioritize security and compatibility with Kubernetes.
|
||||
|
||||
Both tools offer robust features for managing containers, but the choice between them often comes down to specific use cases, security requirements, and integration with other tools in your DevOps toolkit.
|
||||
|
||||
### Orchestration Tools
|
||||
|
||||
While containerization simplifies application deployment, managing containers at scale requires something else: orchestration.
|
||||
|
||||
Orchestration tools like Kubernetes and Docker Swarm automate the deployment, scaling, and management of containerized applications, ensuring that they run efficiently and reliably across distributed environments.
|
||||
|
||||
* **Kubernetes**: Kubernetes is the de facto standard for container orchestration. Kubernetes provides a comprehensive platform for automating the deployment, scaling, and operation of containerized applications by managing clusters of containers.
|
||||
* **Docker Swarm**: Docker Swarm is Docker’s native clustering and orchestration tool. It’s simpler to set up and use compared to Kubernetes, making it a good choice for smaller teams or less complex projects.
|
||||
|
||||
## 4\. Infrastructure as Code (IaC)
|
||||
|
||||
Infrastructure as Code (IaC) has become a foundational practice for DevOps teams. IaC allows you to manage and provision your infrastructure through code, offering a level of automation and consistency that manual processes simply can’t match.
|
||||
|
||||
### Importance of IaC in Modern DevOps Practices
|
||||
|
||||
IaC is crucial in modern DevOps because it brings consistency, scalability, and speed to infrastructure.
|
||||
|
||||
IaC allows teams to define their infrastructure in code, which can be versioned, reviewed, and tested just like application code. If you think about it, IaC is the perfect example of what DevOps means: the merger of both worlds to achieve something that is greater than the sum of its parts.
|
||||
|
||||
Nowadays, IaC is not just a “best practice” but rather, an indispensable part of a DevOps engineer’s workflow, and here is why:
|
||||
|
||||
* **Consistency Across Environments**: As we’ve already mentioned, with IaC, you can ensure that your environments are all configured exactly the same way.
|
||||
* **Scalability**: Whether you need to add more servers, databases, or other resources, you can do it quickly and reliably by updating your code and reapplying it.
|
||||
* **Version Control and Collaboration**: By storing your infrastructure configurations in a version control system like Git, you enable better collaboration and control.
|
||||
* **Automation and Efficiency**: Once your infrastructure is defined in code, you can automate the provisioning, updating, and teardown of resources, allowing your team to focus on higher-value tasks.
|
||||
|
||||
### Key IaC Tools: Terraform, Ansible, Chef, Puppet
|
||||
|
||||
Several tools have become go-to solutions for IaC, each offering unique strengths.
|
||||
|
||||
Here are some of the most popular ones; however, feel free to pick others if they fit better in your particular use case/context:
|
||||
|
||||
* **Terraform**: Terraform is one of the most widely used IaC tools. It’s cloud-agnostic, meaning you can use it to manage infrastructure across different cloud providers like AWS, Azure, and Google Cloud.
|
||||
* **Ansible**: While Ansible’s main focus is automating configuration tasks across multiple servers, it is capable of also working as an IaC tool by providing support for infrastructure provisioning, application deployment, and orchestration.
|
||||
* **Chef**: Chef is another strong player in the configuration management space. It uses a Ruby-based language to create "recipes" that automate the deployment and management of infrastructure.
|
||||
* **Puppet**: Puppet offers a solid solution for configuration management, using its own declarative language to define the state of your infrastructure.
|
||||
|
||||
### Best Practices for Writing and Managing Infrastructure Code
|
||||
|
||||
Like with any coding project, writing clean and easy-to-read code will help a great deal in making the project itself a success. That’s no different in the case of IaC, the words “clean code” need to be engraved in every DevOp’s mind.
|
||||
|
||||
And this is what “clean” means in this context:
|
||||
|
||||
1. **Modularize Your Code**: Break down your infrastructure code into smaller, reusable modules. This approach is especially useful for larger projects where the infrastructure files grow in number; this way you can reuse sections and simplify maintenance.
|
||||
2. **Use Version Control**: Store all your infrastructure code in a version control system like Git. This practice enables you to track changes, collaborate with others, and roll back if something goes wrong.
|
||||
3. **Test Your Infrastructure Code**: Just like application code, infrastructure code should be tested. Tools like Terraform provide validation for configurations, and frameworks like Inspec or Testinfra can verify that your infrastructure is working as expected after deployment.
|
||||
4. **Follow the Principle of Least Privilege**: When defining infrastructure, ensure that each component has only the permissions it needs to perform its function. This practice reduces security risks by limiting the potential impact of a breach or misconfiguration.
|
||||
5. **Keep Secrets Secure**: Avoid the rooky mistake of hardcoding sensitive information, such as API keys or passwords, directly into your infrastructure code. Use tools like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault to manage secrets securely.
|
||||
6. **Document Your Code**: Just like application code, your infrastructure code should also be commented on and explained, not for you now, but for you next week or someone else next month. The easier it is to understand the code, the faster they’ll be able to work with it.
|
||||
7. **Integrate IaC into CI/CD Pipelines**: Automate as much as possible in the IaC workflow. That includes the validation, testing, and deployment of your infrastructure code by integrating it into your continuous integration and continuous deployment (CI/CD) pipelines. This ensures that your environments are always up-to-date and consistent with your codebase, reducing the risk of drift.
|
||||
|
||||
## 5\. Cloud Computing Expertise
|
||||
|
||||

|
||||
|
||||
In the DevOps ecosystem, cloud computing is more than just a trend companies are starting to follow—it's a fundamental element that defines modern software development and deployment practices.
|
||||
|
||||
And because of that, it’s one of the main DevOps skills you’ll want to develop.
|
||||
|
||||
### Importance of Cloud Platforms in DevOps
|
||||
|
||||
Cloud platforms have revolutionized the way software is developed, deployed, and managed. The cloud allows organizations to leverage vast computing resources on demand, scale their operations effortlessly, and reduce infrastructure costs.
|
||||
|
||||
Specifically for DevOps teams, cloud platforms offer several key benefits:
|
||||
|
||||
* **Scalability**: One of the most significant advantages of cloud computing is its ability to scale resources up or down based on demand. This elasticity is crucial for handling varying workloads, ensuring that applications remain responsive and, most importantly (as some would argue), cost-effective.
|
||||
* **Speed and Agility**: Provisioning of resources can be done with just a few clicks, allowing DevOps teams to spin up development, testing, and production environments in minutes. This speed accelerates the software development lifecycle, enabling faster releases and more frequent updates.
|
||||
* **Global Reach**: Cloud providers operate data centers around the world, making it easier for organizations to deploy applications closer to their users, reducing latency and improving performance.
|
||||
* **Cost Efficiency**: This is a recurring topic when discussing cloud platforms, as they help reduce the need for large upfront capital investments in hardware. Instead, organizations can pay for the resources they use, optimizing costs and reducing waste.
|
||||
* **Automation**: Cloud environments are highly automatable, allowing DevOps teams to automate infrastructure provisioning, scaling, and management.
|
||||
|
||||
### Key Cloud Providers: AWS, Azure, Google Cloud Platform (GCP)
|
||||
|
||||
When it comes to cloud providers, three providers dominate the market: Amazon Web Services (AWS), Microsoft Azure, and Google Cloud Platform (GCP). While they’re not the only ones, and in some regions of the world, they’re not even the top providers. In general, they own most of the cloud market.
|
||||
|
||||
Each of them offers a vast array of services and tools that cater to different needs, making them the go-to choices for DevOps professionals.
|
||||
|
||||
* **Amazon Web Services (AWS)**: AWS is the largest and most mature of the three, offering an extensive range of services, including computing power (EC2), storage (S3), databases (RDS), and more. AWS is known for its large number of features, including advanced networking, security, and analytics tools. For DevOps engineers, AWS provides powerful services like AWS Lambda (serverless computing), AWS CodePipeline (CI/CD), and CloudFormation (IaC), which are essential for building and managing cloud-native applications.
|
||||
* **Microsoft Azure**: Azure is a close competitor to AWS, particularly strong in enterprise environments where Microsoft technologies like Windows Server, SQL Server, and .NET are prevalent. Azure offers a very rich list of cloud services, including, like the other two, virtual machines, AI, and machine learning tools. Azure also offers DevOps-specific services like Azure DevOps, which integrates CI/CD, version control, and agile planning into a single platform. Azure's hybrid cloud capabilities also make it a popular choice for organizations that need to integrate on-premises infrastructure with cloud resources.
|
||||
* **Google Cloud Platform (GCP)**: GCP, while newer to the cloud market compared to AWS and Azure, has quickly gained a reputation for its data analytics, machine learning, and container orchestration services. Google’s Kubernetes Engine (GKE), for instance, is quite liked by the DevOps community for managing containerized applications at scale. GCP is also known for its strong support of open-source technologies, making it a favorite among developers who prioritize flexibility and innovation.
|
||||
|
||||
### Understanding Cloud-Native Tools, Services, and Architectural Patterns
|
||||
|
||||
Another key technical skill for DevOps engineers is to understand not only cloud-native tools and services but also the architectural patterns that define modern application development.
|
||||
|
||||
These patterns define how applications are structured and interact with cloud infrastructure, directly affecting areas such as scalability, resilience, and maintainability.
|
||||
|
||||
* **Microservices Architecture**: In a microservices architecture, applications are composed of small, independent services that communicate over APIs. Key tools to understand by DevOps engineers include **API gateways** (like AWS API Gateway), **service meshes** (such as Istio), and **message queues** (like Amazon SQS or Google Pub/Sub).
|
||||
* **Service-Oriented Architecture (SOA)**: SOA is a broader (and older) architectural style where services are designed to provide specific business functionalities and can communicate with each other over a network. Tools like **Enterprise Service Buses (ESBs)** and **message brokers** (such as RabbitMQ) are often used to facilitate SOA architectures.
|
||||
* **Serverless Architecture**: Serverless computing allows developers to build and deploy applications without managing the underlying infrastructure. In a serverless architecture, code is executed in response to events, such as HTTP requests or changes in data, using services like **AWS Lambda**, **Azure Functions**, or **Google Cloud Functions**.
|
||||
* **Event-Driven Architecture**: In an event-driven architecture, applications respond to events in real-time, often using tools like **event streams** (e.g., Apache Kafka) and **message queues**.
|
||||
|
||||
## 6\. Monitoring and Logging!
|
||||
|
||||
[monitoring logging servers][https://assets.roadmap.sh/guest/monitoring-logging-servers-ztf1a.png]
|
||||
|
||||
Monitoring and logging are vital components of a robust DevOps strategy. They provide visibility into the health and performance of your systems, allowing you to detect issues early, troubleshoot, and ensure the reliability of your applications.
|
||||
|
||||
### Importance of Monitoring and Logging for Maintaining System Health
|
||||
|
||||
No matter what type of application you’re running, maintaining the health and performance of your systems is crucial if your business depends on it.
|
||||
|
||||
Monitoring and logging has turned into one of the most relevant DevOps skills out there.
|
||||
|
||||
Through monitoring you can track the performance of your infrastructure and applications in real-time, alerting you to any potential problems such as resource bottlenecks, slowdowns, or outages.
|
||||
|
||||
Logging, on the other hand, captures detailed records of system events and user interactions, providing invaluable information for diagnosing problems and understanding system behavior.
|
||||
|
||||
The reasons why you want to have effective monitoring and logging, are:
|
||||
|
||||
* **Proactive Issue Detection**: By continuously monitoring system metrics, you can detect issues before they escalate into critical problems, reducing downtime and improving overall system reliability.
|
||||
* **Troubleshooting and Root Cause Analysis**: Logs provide detailed information about system events, making it easier to pinpoint the root cause of issues. This speeds up the resolution process and minimizes the impact on users.
|
||||
* **Performance Optimization**: Monitoring allows you to track key performance indicators (KPIs) and identify areas where your systems can be optimized, leading to better resource utilization and cost savings.
|
||||
* **Compliance and Auditing**: Logging is essential for maintaining compliance with regulatory requirements. Logs can be used to audit system access, track changes, and ensure that your systems meet security and operational standards.
|
||||
|
||||
### Key Tools for Monitoring: Prometheus, Grafana, Nagios
|
||||
|
||||
Several tools have become essential for monitoring systems in DevOps environments. Each offers unique features tailored to different needs, from real-time metrics collection to visual dashboards and alerting.
|
||||
|
||||
* **Prometheus**: Prometheus is an open-source monitoring tool designed for reliability and scalability. It collects real-time metrics from your systems and applications, stores them in a time-series database, and supports powerful query languages for analysis.
|
||||
* **Grafana**: Grafana is a popular open-source platform for visualizing monitoring data. It integrates with Prometheus and other data sources, allowing you to create interactive, customizable dashboards that provide insights into system performance at a glance.
|
||||
* **Nagios**: Nagios is one of the oldest and most widely used monitoring tools. It provides comprehensive monitoring of network services, host resources, and infrastructure components.
|
||||
|
||||
### Logging Tools: ELK Stack (Elasticsearch, Logstash, Kibana), Splunk
|
||||
|
||||
Effective logging requires tools that can collect, store, and analyze large volumes of log data efficiently, given how much information modern systems can generate.
|
||||
|
||||
The following tools are among the most widely used in the industry:
|
||||
|
||||
* **ELK Stack**: The ELK Stack is a powerful open-source solution for managing logs. It consists of **Elastic** for storing and searching logs, **Logstash** to act as a data processing/ingestion pipeline, and **Kibana** for data visualization.
|
||||
* **Splunk**: Splunk is a commercial tool that offers advanced log management and analysis capabilities. It can ingest data from a wide variety of sources, index it in real time, and provide powerful search and reporting features.
|
||||
|
||||
### Best Practices for Setting Up Effective Monitoring and Logging Systems
|
||||
|
||||
While both practices are crucial for a successful DevOps strategy, if you ignore best practices the results you’ll get will be subpar, at best.
|
||||
|
||||
Instead, try to follow these (or some of them) guidelines to ensure you get the most out of your monitoring and logging efforts.
|
||||
|
||||
1. **Define Clear Objectives**: Before setting up your monitoring and logging systems, define what you want to achieve. Identify the key metrics and logs that are most critical to your operations, such as CPU usage, memory consumption, application response times, and error rates.
|
||||
2. **Implement Comprehensive Monitoring**: Monitor all layers of your infrastructure, from hardware and networks to applications and services. Use a combination of tools to ensure that no aspect of your system goes unmonitored. If you ignore one area, you’ll end up having blindspots when debugging and trying to troubleshoot problems.
|
||||
3. **Centralize Log Management**: Centralizing your logs in a single platform like the ELK Stack or Splunk allows for easier management, search, and analysis. This centralization is particularly important in distributed systems where logs are generated across multiple servers and services.
|
||||
4. **Set Up Alerts and Notifications**: Monitoring without alerting is like watching a movie without any sound; if you constantly pay attention to the picture, you might figure out what’s happening on a general level, but you’ll miss the details. And with monitoring, it’s the same thing: set up alerts and notifications so when a threshold is exceeded (say, the number of error responses in the last 10 minutes), you’ll know, even if it’s in the middle of the night.
|
||||
5. **Ensure Scalability**: As your infrastructure grows, your monitoring and logging systems need to scale accordingly. Choose tools that can handle increasing volumes of data without compromising performance. In other words, don’t turn your logging/monitoring setup into a bottleneck for your platform.
|
||||
6. **Regularly Review and Tune**: Continuously review and adjust your monitoring and logging configurations. As your systems evolve, your monitoring and logging needs may change, requiring you to add new metrics, refine alert thresholds, or optimize data retention policies.
|
||||
7. **Secure Your Monitoring and Logging Infrastructure**: Protect your monitoring and logging data from unauthorized access. Ensure that logs containing sensitive information are encrypted and access to monitoring dashboards is restricted based on roles.
|
||||
|
||||
## 7\. Security Practices and Tools (DevSecOps)
|
||||
|
||||
As DevOps has transformed software development by integrating development and operations teams together into a seamless process, security can no longer be treated as an afterthought. The rise of DevSecOps emphasizes the need for DevOps engineers to develop their security skills.
|
||||
|
||||
### Integrating Security into the DevOps Pipeline
|
||||
|
||||
DevSecOps shifts the classical paradigm (having security reviews happen at the end of the development lifecycle) by integrating security into every phase of the DevOps pipeline—from code development to deployment and beyond. That, in turn, involves the following:
|
||||
|
||||
* **Shift-Left Security**: This principle involves moving security practices earlier in the SDLC, such as during the coding and design phases.
|
||||
* **Continuous Security**: Security checks should be continuous and automated throughout the pipeline. This ensures that each code change, build, and deployment is evaluated for security risks.
|
||||
* **Collaboration and Culture**: DevSecOps is as much about culture as it is about tools. Developers, operations, and security teams must collaborate closely, sharing responsibility for security.
|
||||
|
||||
### Key Security Practices
|
||||
|
||||
To effectively integrate security into the DevOps pipeline, certain practices are essential:
|
||||
|
||||
* **Automated Security Testing**: Automation is key to scaling security practices within a rapidly growing DevOps environment. Automated security testing involves integrating security checks into your CI/CD pipelines. This can include static application security testing (SAST) to analyze source code for security flaws, dynamic application security testing (DAST) to evaluate running applications, and interactive application security testing (IAST) that combines both approaches.
|
||||
* **Vulnerability Scanning**: Regular vulnerability scanning is crucial for identifying and mitigating risks across your infrastructure and applications. Scanning tools can detect known vulnerabilities in code, dependencies, container images, and cloud configurations.
|
||||
* **Security as Code**: Just as Infrastructure as Code (IaC) treats infrastructure configuration as code, Security as Code applies the same principles to security configurations. This involves automating the provisioning and management of security controls, policies, and compliance checks.
|
||||
|
||||
### Tools for DevSecOps
|
||||
|
||||
Several tools have emerged to support the integration of security into the DevOps practice. These tools help automate security tasks, identify vulnerabilities, and enforce security policies, making it easier for teams to adopt DevSecOps practices.
|
||||
|
||||
Some examples are:
|
||||
|
||||
* **Aqua Security**: Aqua Security specializes in securing cloud-native applications, particularly those that run in containers. Aqua provides a comprehensive platform for securing the entire container lifecycle, from development to runtime.
|
||||
* **Snyk**: Snyk is a developer-friendly security platform that helps identify and fix vulnerabilities in open-source libraries, container images, and infrastructure as code. Snyk integrates with CI/CD pipelines, providing automated security testing and real-time feedback.
|
||||
* **Trivy**: Trivy is an open-source vulnerability scanner that is particularly well-suited for container environments. It scans container images, file systems, and Git repositories for known vulnerabilities, misconfigurations, and secrets.
|
||||
|
||||
## 8\. Networking and System Administration
|
||||
|
||||

|
||||
|
||||
Networking and system administration are foundational DevOps skills. These disciplines ensure that the infrastructure supporting your applications is robust, secure, and efficient.
|
||||
|
||||
### Which networking concepts are most relevant to DevOps?
|
||||
|
||||
Networking is the backbone of any IT infrastructure, connecting systems, applications, and users. A solid understanding of networking concepts is crucial for DevOps engineers to design, deploy, and manage systems effectively.
|
||||
|
||||
Some of the most important concepts include:
|
||||
|
||||
* **TCP/IP Networking**: TCP/IP (Transmission Control Protocol/Internet Protocol) is the fundamental protocol suite for the Internet and most private networks. Understanding how TCP/IP works is essential.
|
||||
* **Network Topologies**: Network topology refers to the arrangement of different elements (links, nodes, etc.) in a computer network. Common topologies include star, mesh, and hybrid configurations.
|
||||
* **Load Balancing**: Load balancing is the process of distributing network or application traffic across multiple servers to ensure no single server becomes overwhelmed. DevOps engineers need to understand different load balancing algorithms (round-robin, least connections, IP hash) and how to implement load balancers (like NGINX, HAProxy, or cloud-native solutions).
|
||||
* **Firewalls and Security Groups**: Firewalls are essential for controlling incoming and outgoing network traffic based on predetermined security rules. In cloud environments, security groups serve a similar function by acting as virtual firewalls for instances.
|
||||
* **DNS (Domain Name System)**: DNS is the system that translates human-readable domain names (like [www.example.com](http://www.example.com)) into IP addresses that computers use to identify each other on the network.
|
||||
* **VPNs and Secure Communication**: Virtual Private Networks (VPNs) allow secure communication over public networks by encrypting data between remote devices and the network.
|
||||
|
||||
### System Administration Tasks and Best Practices
|
||||
|
||||
System administration involves the management of computer systems, including servers, networks, and applications. DevOps engineers often take on system administration tasks to ensure that infrastructure is stable, secure, and performing optimally.
|
||||
|
||||
Some of these tasks include:
|
||||
|
||||
* **User and Permission Management**: Managing user accounts, groups, and permissions is fundamental to system security.
|
||||
* **Server Configuration and Management**: Configuring servers to meet the needs of applications and ensuring they run efficiently is a core task.
|
||||
* **System Monitoring and Maintenance**: As we’ve already mentioned, regular monitoring of system performance metrics is essential for proactive maintenance.
|
||||
* **Backup and Recovery**: Regular backups of data and configurations are crucial for disaster recovery.
|
||||
* **Patch Management**: Keeping systems up to date with the latest security patches and software updates is critical for maintaining your infrastructure secure.
|
||||
* **Security Hardening**: Security hardening involves reducing the attack surface of a system by configuring systems securely, removing unnecessary services, and applying best practices.
|
||||
* **Script Automation**: Developing your automation skills is key, as you’ll be automating routine tasks with scripts every day. Common scripting languages include Bash for Linux and PowerShell for Windows.
|
||||
|
||||
### Best Practices for Networking and System Administration
|
||||
|
||||
1. **Automate Repetitive Tasks**: Use automation tools and scripts to handle routine tasks such as backups, patch management, and monitoring setup.
|
||||
2. **Implement Redundancy and Failover**: Design your network and systems with redundancy and failover mechanisms. This includes setting up redundant network paths, using load balancers, and configuring failover for critical services to minimize downtime.
|
||||
3. **Enforce Strong Security Practices**: Regularly audit user access, apply patches promptly, and follow security best practices for hardening systems.
|
||||
4. **Regularly Review and Update Documentation**: Keep detailed documentation of your network configurations, system setups, and processes.
|
||||
5. **Monitor Proactively**: Set up comprehensive monitoring for all critical systems and networks. Alerts should be used to catch issues early, and logs should be reviewed regularly to spot potential security or performance issues.
|
||||
6. **Test Disaster Recovery Plans**: Regularly test your backup and disaster recovery procedures to ensure they work as expected.
|
||||
|
||||
## 9\. Familiarity with Version Control Systems
|
||||
|
||||
Version control systems (VCS) are at the center of modern software development, enabling teams to collaborate, track changes, and manage their codebase.
|
||||
|
||||
In a DevOps environment, where continuous integration and continuous deployment (CI/CD) are central practices, mastering version control is not just beneficial—it's essential.
|
||||
|
||||
### Importance of Version Control in DevOps Workflows
|
||||
|
||||
Version control is crucial in DevOps for several reasons:
|
||||
|
||||
* **Collaboration**: Version control systems allow multiple developers to work on the same codebase simultaneously without overwriting each other's changes.
|
||||
* **Change Tracking**: Every change to the codebase is tracked, with a history of who made the change, when, and why.
|
||||
* **Branching and Merging**: Version control systems enable the creation of branches, allowing developers to work on new features, bug fixes, or experiments in isolation.
|
||||
* **Continuous Integration/Continuous Deployment (CI/CD)**: Version control is crucial to CI/CD pipelines, where code changes are automatically tested, integrated, and deployed.
|
||||
* **Disaster Recovery**: In case of errors or issues, version control allows you to revert to previous stable versions of the codebase, minimizing downtime and disruption.
|
||||
|
||||
### Mastering Git: Key Commands, Workflows, and Best Practices
|
||||
|
||||
Git is the most widely used version control system in the DevOps world, known for its flexibility, performance, and breadth of features. Having a deep understanding of Git is crucial for any DevOps engineer, as it is the foundation upon which most CI/CD workflows are built.
|
||||
|
||||
The key commands you should try to master first are `init`, `clone`, `commit`, `pull`/`push`, `branch`, `checkout`, `merge`, and one that is definitely useful in your context: `log`.
|
||||
|
||||
#### Git Workflows
|
||||
|
||||
Git can be used as the driving force for your development workflow. However, there are many ways to use it. Some of the most common ones are:
|
||||
|
||||
* **Feature Branch Workflow**: Developers create a new branch for each feature or bug fix. Once complete, the branch is merged back into the main branch, often through a pull request, where code reviews and automated tests are conducted.
|
||||
* **Gitflow Workflow**: A more structured workflow that uses feature branches, a develop branch for integration, and a master branch for production-ready code. It also includes hotfix branches for urgent bug fixes in production.
|
||||
* **Forking Workflow**: Common in open-source projects, this workflow involves forking a repository, making changes in the fork, and then submitting a pull request to the original repository for review and integration.
|
||||
|
||||
#### Best practices when using Git
|
||||
|
||||
* **Commit Often, Commit Early**: Make small, frequent commits with clear, descriptive messages. This practice makes it easier to track changes and revert specific parts of the codebase if needed.
|
||||
* **Use Meaningful Branch Names**: Name branches based on the work they are doing, such as `feature/user-authentication` or `bugfix/login-issue`.
|
||||
* **Perform Code Reviews**: Use pull requests and code reviews as part of the merge process.
|
||||
* **Keep a Clean Commit History**: Use tools like `git rebase` to clean up your commit history before merging branches. A clean commit history makes it easier to understand the evolution of the project and debug issues.
|
||||
* **Resolve Conflicts Early**: When working on shared branches, regularly pull changes from the remote repository to minimize and resolve merge conflicts as early as possible.
|
||||
|
||||
### Tools for Managing Version Control: GitHub, GitLab, Bitbucket
|
||||
|
||||
While Git itself is a command-line tool, various platforms provide user-friendly interfaces and additional features to manage Git repositories effectively.
|
||||
|
||||
* **GitHub**: GitHub is the most popular platform for hosting Git repositories. It offers many collaboration features and on top of them, GitHub Actions integrates CI/CD directly into the platform, automating workflows from development to deployment.
|
||||
* **GitLab**: GitLab is a complete DevOps platform that includes Git repository management, CI/CD, issue tracking, and more. GitLab can be hosted on-premises, which is a significant advantage for organizations with strict data security requirements.
|
||||
* **Bitbucket**: Bitbucket, developed by Atlassian, integrates tightly with other Atlassian tools like Jira and Confluence. It supports Git and Mercurial and offers features like pull requests, code reviews, and CI/CD pipelines through Bitbucket Pipelines.
|
||||
|
||||
As usual, the right set of DevOps tools will drastically depend on your specific context and needs.
|
||||
|
||||
## 10\. Knowledge of Configuration Management
|
||||
|
||||
Configuration management is a critical component of DevOps, enabling teams to automate the setup and maintenance of systems and applications across different environments.
|
||||
|
||||
### The Role of Configuration Management in DevOps
|
||||
|
||||
This practice involves maintaining the consistency of a system's performance and functionality by ensuring that its configurations are properly set up and managed.
|
||||
|
||||
In DevOps, where continuous integration and continuous deployment (CI/CD) are key practices, understanding how to manage your configurations ensures that environments are consistently configured, regardless of where they are deployed.
|
||||
|
||||
Configure once and deploy endless times; that’s the DevOps way.
|
||||
|
||||
The main reasons why this is such an important practice in DevOps are:
|
||||
|
||||
* **Automation tools**: These tools automate the process of setting up and maintaining infrastructure, reducing manual effort and minimizing the risk of human error.
|
||||
* **Consistency Across Environments**: By defining configurations as code, conf. management ensures that all environments are configured identically.
|
||||
* **Scalability**: As systems scale, manually managing configurations becomes impractical. Configuration management allows you to scale infrastructure and applications, ensuring that new instances are configured correctly from the start.
|
||||
* **Compliance and Auditing**: These tools provide a clear and auditable record of system configurations. This is essential for compliance with industry standards and regulations.
|
||||
* **Disaster Recovery**: In the event of a system failure, configuration management tools can quickly restore systems to their desired state, reducing downtime and minimizing the impact on business operations.
|
||||
|
||||
### DevOps Configuration Management Tools to Master
|
||||
|
||||
Several tools have become staples in this landscape, each offering unique features and strengths. For example:
|
||||
|
||||
* **Ansible**: Ansible, developed by Red Hat, is an open-source tool known for its simplicity and ease of use. It uses YAML for configuration files, known as playbooks, which are easy to read and write. Ansible is ideal for automating tasks like software installation, service management, and configuration updates across multiple servers.
|
||||
* **Puppet**: Puppet is a powerful tool that uses a declarative language to define system configurations. Puppet’s strength lies in its scalability and ability to manage large, complex environments.
|
||||
* **Chef**: Chef is another popular tool that uses a Ruby-based DSL (Domain-Specific Language) to write recipes and cookbooks, which define how systems should be configured and managed.
|
||||
|
||||
### Best Practices for Managing Configurations Across Environments
|
||||
|
||||
Effective configuration management requires you to follow best practices that ensure consistency, reliability, and security across all environments.
|
||||
|
||||
1. **Use Configuration as Code (CaC)**: Treat configurations as code by storing them in version control systems like Git.
|
||||
2. **Modularize Configurations**: Break down configurations into reusable modules or roles. This approach allows you to apply the same configuration logic across different environments.
|
||||
3. **Test Configurations in Multiple Environments**: Before deploying configurations to production, test them thoroughly in staging or testing environments.
|
||||
4. **Implement Idempotency**: Ensure that your processes are idempotent, meaning that applying the same configuration multiple times does not change the system after the first application.
|
||||
5. **Centralization**: Use a centralized tool to maintain a single source of truth for all configurations.
|
||||
6. **Encrypt Sensitive Data**: When managing configurations that include sensitive data (e.g., passwords, API keys), use encryption and secure storage solutions like HashiCorp Vault.
|
||||
7. **Document Configurations and Changes**: Maintain detailed documentation for your configurations and any changes made to them.
|
||||
8. **Monitor and Audit Configurations**: Continuously monitor configurations to detect and prevent drift (when configurations deviate from the desired state).
|
||||
|
||||
## Bonus: Collaboration and Communication Skills
|
||||
|
||||
While technical skills are critical to becoming a successful DevOps engineer, the importance of soft skills—particularly collaboration and communication—cannot be ignored.
|
||||
|
||||
In a DevOps environment, where cross-functional teams work closely together to deliver software, effective communication, and collaboration are essential for success.
|
||||
|
||||
### Importance of Soft Skills in a DevOps Environment
|
||||
|
||||
DevOps is not just about tools and processes; it's also about people and how they work together.
|
||||
|
||||
Key reasons why soft skills are crucial in a DevOps environment:
|
||||
|
||||
* **Cross-Functional Collaboration**: DevOps brings together diverse teams with different expertise—developers, operations, QA, security, and more.
|
||||
* **Problem Solving and Conflict Resolution**: In software development in general, issues and conflicts are inevitable. Strong communication skills help teams navigate these challenges, finding quick resolutions and keeping the focus on delivering value to the customer.
|
||||
* **Agility and Adaptability**: DevOps teams often need to adapt to changing requirements and priorities. Effective communication ensures that these changes are understood and implemented without issues.
|
||||
|
||||
### Tools for Effective Collaboration: Slack, Microsoft Teams, Jira
|
||||
|
||||
Several tools are essential for facilitating communication and collaboration in a DevOps environment.
|
||||
|
||||
Is there an absolute best one? The answer to that question is “no.”, the best option depends on your needs and preferences, so study the list and figure out yourself which software (or combination of) helps your specific teams.
|
||||
|
||||
* **Slack**: Slack is a popular communication platform designed for team collaboration. It offers integration with other DevOps tools like GitHub, Jenkins, and Jira.
|
||||
* **Microsoft Teams**: Microsoft Teams is another powerful collaboration tool, especially popular in organizations using the Microsoft ecosystem.
|
||||
* **Jira**: Jira, developed by Atlassian, is a robust project management tool that helps teams track work, manage backlogs, and plan sprints. It’s particularly useful in Agile and DevOps environments where transparency and continuous improvement are key.
|
||||
|
||||
### Best Practices for Fostering a Collaborative Culture
|
||||
|
||||
Building a collaborative culture in a DevOps environment requires effort and ongoing commitment.
|
||||
|
||||
Here are some general guidelines you can follow to help achieve that collaborative environment:
|
||||
|
||||
1. **Promote Open Communication**: Encourage team members to communicate openly about their work, challenges, and ideas.
|
||||
2. **Regular Stand-Ups and Check-Ins**: Implement daily stand-ups or regular check-ins to ensure that everyone is on the same page. Whether they’re in person, during a video call, or asynchronous, these check-ins help find blockers and solve them fast.
|
||||
3. **Use Collaborative Documentation**: Maintain shared documentation using tools like Confluence or Google Docs.
|
||||
4. **Encourage Cross-Functional Training**: Facilitate training sessions or workshops where team members from different disciplines can learn about each other's work.
|
||||
5. **Foster a Blameless Culture**: In a DevOps environment, mistakes and failures should be viewed as learning opportunities rather than reasons to assign blame. Encourage a blameless culture where issues are discussed openly, and the focus is on understanding the root cause.
|
||||
|
||||
## Conclusion
|
||||
|
||||
In the world of DevOps, mastering a diverse set of skills is not an option but rather an absolute must. From understanding the details of cloud computing and infrastructure as code to implementing monitoring and security practices, each skill plays a crucial role in fulfilling the main goal of any DevOps practice: enabling fast, reliable, and secure software delivery.
|
||||
|
||||
For those looking to deepen their understanding or get started on their DevOps journey, here are some valuable resources:
|
||||
|
||||
* [**Expanded DevOps Roadmap**](https://roadmap.sh/devops): A comprehensive guide that details the full range of DevOps skills, tools, and technologies you need to master as a DevOps engineer.
|
||||
* [**Simplified DevOps Roadmap**](https://roadmap.sh/devops?r=devops-beginner): A more streamlined version that highlights the core components of a DevOps career, making it easier for beginners to navigate the field.
|
||||
|
||||
Success in DevOps is about cultivating a well-rounded skill set that combines technical expertise with strong collaboration, communication, and problem-solving abilities.
|
||||
|
||||
As the industry continues to evolve, so too will the tools, practices, and challenges that DevOps engineers face. By committing to continuous learning and staying adaptable, you can ensure that you remain at the forefront of this dynamic field, driving innovation and delivering value in your organization.
|
||||
36
src/data/projects/basic-github-actions.md
Normal file
36
src/data/projects/basic-github-actions.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: 'Github Actions Workflow'
|
||||
description: 'Write GitHub Actions workflow to deploy a simple GitHub Pages site.'
|
||||
isNew: true
|
||||
sort: 3
|
||||
difficulty: 'beginner'
|
||||
nature: 'CI/CD'
|
||||
skills:
|
||||
- 'devops'
|
||||
- 'github actions'
|
||||
- 'ci/cd'
|
||||
seo:
|
||||
title: 'Github Actions Workflow'
|
||||
description: 'Write GitHub Actions workflow to deploy a simple GitHub Pages site.'
|
||||
keywords:
|
||||
- 'basic ci/cd'
|
||||
- 'devops'
|
||||
- 'devops projects'
|
||||
roadmapIds:
|
||||
- 'git-github'
|
||||
- 'devops'
|
||||
---
|
||||
|
||||
In this project, you will write a basic HTML file and setup a GitHub Actions workflow to test, build & deploy it to GitHub Pages.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Create a simple HTML file (the content is up to you)
|
||||
- Create a GitHub Actions workflow that will test, build & deploy the website to [GitHub Pages](https://pages.github.com/).
|
||||
- Failures in the workflow should be clearly indicated and failures will halt the workflow
|
||||
- The workflow should be in the `.github/workflows` directory
|
||||
- The workflow file should be named `main.yml`
|
||||
|
||||
<hr />
|
||||
|
||||
If you are looking to build a more advanced version of this project, you can either create a more advanced Astro website or you can build a more advanced GitHub Actions workflow.
|
||||
42
src/data/projects/basic-iac-with-terraform.md
Normal file
42
src/data/projects/basic-iac-with-terraform.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: 'Basic Infrastructure as Code with Terraform'
|
||||
description: 'Provision a simple cloud infrastructure using Terraform'
|
||||
isNew: false
|
||||
sort: 4
|
||||
difficulty: 'beginner'
|
||||
nature: 'CLI'
|
||||
skills:
|
||||
- 'terraform'
|
||||
- 'devops'
|
||||
- 'iac'
|
||||
- 'cloud'
|
||||
seo:
|
||||
title: 'Basic Infrastructure as Code with Terraform'
|
||||
description: 'Learn to provision cloud resources using Terraform'
|
||||
keywords:
|
||||
- 'terraform'
|
||||
- 'infrastructure as code'
|
||||
- 'cloud provisioning'
|
||||
- 'devops'
|
||||
roadmapIds:
|
||||
- 'devops'
|
||||
- 'terraform'
|
||||
- 'aws'
|
||||
---
|
||||
|
||||
In this project, you will use Terraform to provision a virtual machine in AWS.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Install Terraform on your local machine.
|
||||
- Set up an account with a AWS and obtain necessary credentials.
|
||||
- Create a `main.tf` file in the root directory of your project.
|
||||
- Write Terraform configuration to provision a basic resource (e.g., an EC2 instance on AWS or a VM on Azure).
|
||||
- Use Terraform commands to initialize, plan, apply, and destroy your infrastructure.
|
||||
- The provisioned resource should be accessible and verifiable in your cloud provider's console.
|
||||
|
||||
You can learn more about Terraform basics [here](https://learn.hashicorp.com/terraform).
|
||||
|
||||
<hr />
|
||||
|
||||
For a more advanced version of this project, consider adding multiple resources, using variables and outputs, or implementing a modular structure for your Terraform configuration.
|
||||
39
src/data/projects/docker-webserver.md
Normal file
39
src/data/projects/docker-webserver.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: 'Docker Web Server'
|
||||
description: 'Create a Web Server using Docker & NGINX'
|
||||
isNew: false
|
||||
sort: 3
|
||||
difficulty: 'beginner'
|
||||
nature: 'CLI'
|
||||
skills:
|
||||
- 'docker'
|
||||
- 'devops'
|
||||
- 'nginx'
|
||||
- 'web'
|
||||
seo:
|
||||
title: 'Docker Web Server'
|
||||
description: 'Create a Web Server using Docker & NGINX'
|
||||
keywords:
|
||||
- 'docker web server'
|
||||
- 'docker'
|
||||
- 'system administration'
|
||||
- 'web server'
|
||||
roadmapIds:
|
||||
- 'devops'
|
||||
- 'docker'
|
||||
---
|
||||
|
||||
In this project, you will create an NGINX web server that will serve a simple HTML page using Docker.
|
||||
|
||||
## Requirements
|
||||
|
||||
- The Dockerfile should be named `Dockerfile`.
|
||||
- The Dockerfile should be in the root directory of the project.
|
||||
- The build process will add a local HTML file to the container, which will be accessible to NGINX.
|
||||
- The simple HTML page will be accessible to you from `localhost:8080`
|
||||
|
||||
You can learn more about writing a Dockerfile [here](https://docs.docker.com/engine/reference/builder/).
|
||||
|
||||
<hr />
|
||||
|
||||
If you are looking to build a more advanced version of this project, you can consider using the `alpine:latest` image and setting up NGINX yourself rather than using the official NGINX image.
|
||||
57
src/data/projects/local-monitoring-stack.md
Normal file
57
src/data/projects/local-monitoring-stack.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: 'Local Monitoring System with Docker'
|
||||
description: 'Set up a local monitoring system using Docker with Grafana and Prometheus'
|
||||
isNew: false
|
||||
sort: 1
|
||||
difficulty: 'intermediate'
|
||||
nature: 'CLI'
|
||||
skills:
|
||||
- 'docker'
|
||||
- 'devops'
|
||||
- 'monitoring'
|
||||
- 'grafana'
|
||||
- 'prometheus'
|
||||
seo:
|
||||
title: 'Local Monitoring System with Docker, Grafana, and Prometheus'
|
||||
description: 'Learn to set up a local monitoring system using Docker with Grafana and Prometheus'
|
||||
keywords:
|
||||
- 'docker'
|
||||
- 'monitoring'
|
||||
- 'grafana'
|
||||
- 'prometheus'
|
||||
- 'devops'
|
||||
roadmapIds:
|
||||
- 'devops'
|
||||
- 'docker'
|
||||
---
|
||||
|
||||
In this project, you will set up a local monitoring system using Docker, Grafana, and Prometheus. This setup will allow you to collect metrics and visualize them in a dashboard.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker and Docker Compose installed on your local machine.
|
||||
- Create a `docker-compose.yml` file in the root directory of your project.
|
||||
- Set up Prometheus as the metrics collection system.
|
||||
- Configure Grafana as the visualization tool.
|
||||
- Create a simple dashboard in Grafana to display system metrics.
|
||||
- Add a sample application to monitor, such as a simple web server.
|
||||
|
||||
Your `docker-compose.yml` file should define services for:
|
||||
1. Prometheus
|
||||
2. Grafana
|
||||
3. A sample application to monitor
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create the `docker-compose.yml` file with services for Prometheus and Grafana.
|
||||
2. Configure Prometheus to scrape metrics (you'll need a `prometheus.yml` configuration file).
|
||||
3. Set up Grafana to use Prometheus as a data source.
|
||||
4. Create a simple dashboard in Grafana to display metrics.
|
||||
5. Use `docker-compose up` to start your monitoring stack.
|
||||
6. Access Grafana through your web browser and verify that metrics are being collected and displayed.
|
||||
|
||||
You can learn more about Prometheus [here](https://prometheus.io/docs/introduction/overview/) and Grafana [here](https://grafana.com/docs/grafana/latest/).
|
||||
|
||||
<hr />
|
||||
|
||||
For a more advanced version of this project, consider adding alerting rules in Prometheus, setting up additional exporters to collect more diverse metrics, or monitoring a multi-container application.
|
||||
@@ -1,8 +1,3 @@
|
||||
# Cloud Messaging
|
||||
|
||||
Firebase Cloud Messaging (FCM) is a powerful, battery-efficient messaging service that enables you to send messages reliably and securely to your Android applications. It enables you to send two types of messages: "notification messages" and "data messages". Notification messages are primarily meant for user notifications and will only be delivered when the application is in the foreground. On the other hand, data messages can handle even when the app is in the background or killed and can be used to send custom key-value pairs. FCM also supports various additional features, such as topic messaging to send messages to multiple devices subscribed to a common topic, device group messaging for sending messages to groups of user devices, and upstream messaging for sending messages from the client application to the FCM server.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Documentation](https://firebase.google.com/docs/cloud-messaging/android/client)
|
||||
- [@video@Firebase Cloud Messaging](https://www.youtube.com/watch?v=sioEY4tWmLI&list=PLl-K7zZEsYLkuHRCtHTpi6JYHka8oHLft)
|
||||
Firebase Cloud Messaging (FCM) is a powerful, battery-efficient messaging service that enables you to send messages reliably and securely to your Android applications. It enables you to send two types of messages: "notification messages" and "data messages". Notification messages are primarily meant for user notifications and will only be delivered when the application is in the foreground. On the other hand, data messages can handle even when the app is in the background or killed and can be used to send custom key-value pairs. FCM also supports various additional features, such as topic messaging to send messages to multiple devices subscribed to a common topic, device group messaging for sending messages to groups of user devices, and upstream messaging for sending messages from the client application to the FCM server.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Frame Layout
|
||||
|
||||
**FrameLayout** is a simple ViewGroup subclass in Android that is designed to hold a single child view or a stack of overlapping child views. It positions each child in the top-left corner by default and allows them to overlap on top of each other, which makes it useful for situations where you need to layer views on top of one another.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Android developers: Frame Layout](https://developer.android.com/reference/android/widget/FrameLayout)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# LinearLayout
|
||||
|
||||
**LinearLayout** is a view group that aligns all children in a single directioni, vertically or horizontally. You can specify the layout direction with the `android:orientation` attribute.
|
||||
|
||||
**LinearLayout** was commonly used in earlier Android development, but with the introduction of ConstraintLayout, it’s less frequently used in modern apps.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Android developers: Linear Layout](https://developer.android.com/develop/ui/views/layout/linear)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# MVI
|
||||
|
||||
The **MVI** `Model-View-Intent` pattern is a reactive architectural pattern, similar to **MVVM** and **MVP**, focusing on immutability and handling states in unidirectional cycles. The data flow is unidirectional: Intents update the Model's state through the `ViewModel`, and then the View reacts to the new state. This ensures a clear and predictable cycle between logic and the interface.
|
||||
|
||||
- Model: Represents the UI state. It is immutable and contains all the necessary information to represent a screen.
|
||||
- View: Displays the UI state and receives the user's intentions.
|
||||
- Intent: The user's intentions trigger state updates, managed by the `ViewModel`.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@MVI with Kotlin](https://proandroiddev.com/mvi-architecture-with-kotlin-flows-and-channels-d36820b2028d)
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# Angular Architecture
|
||||
|
||||
Angular follows a modular architecture pattern, dividing the application into distinct modules, components, services, and other elements, which enhances code organization and maintainability. The key building blocks include modules, which are containers grouping related components, services, directives, and other elements to ensure proper encapsulation and reusability. Components are the building blocks of Angular applications, representing parts of the user interface with associated logic, consisting of templates, styles, and a class defining behavior. Services encapsulate reusable business logic, data manipulation, and API communication, enabling data and functionality sharing across components. Directives are HTML attributes or elements that extend HTML functionality, allowing reusable behaviors across the application. Lastly, pipes transform data before displaying it in templates, providing convenient ways to format, filter, and sort data.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Angular coding style guide](https://angular.dev/style-guide)
|
||||
- [@article@The Ultimate Guide to Angular Architecture: Best Practices for efficient coding with Angular Framework](https://angulardive.com/blog/the-ultimate-guide-to-angular-architecture-best-practices-for-efficient-coding-with-angular-framework/)
|
||||
- [@article@Modern Architectures with Angular Part 1: Strategic design with Sheriff and Standalone Components](https://www.angulararchitects.io/en/blog/modern-architectures-with-angular-part-1-strategic-design-with-sheriff-and-standalone-components/)
|
||||
- [@article@Optimizing the architecture of large web applications with Angular](https://albertobasalo.medium.com/optimizing-the-architecture-of-large-web-applications-with-angular-79d03b01a92b)
|
||||
- [@article@Angular Architecture Concepts and Patterns](https://www.bigscal.com/blogs/frontend/angular-architecture-concepts-and-patterns/)
|
||||
- [@article@Top 10 Angular Architecture Mistakes](https://angularexperts.io/blog/top-10-angular-architecture-mistakes)
|
||||
- [@article@Architecting Angular: A Guide to effective project structure](https://medium.com/@nile.bits/architecting-angular-a-guide-to-effective-project-structure-9756bae92262)
|
||||
@@ -6,5 +6,5 @@ Use pipes to transform strings, currency amounts, dates, and other data for disp
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Understanding Pipes](https://angular.dev/tutorials/learn-angular/22-pipes)
|
||||
- [@official@Understanding Pipes](https://angular.dev/guide/pipes)
|
||||
- [@article@BuiltIn Pipes - examples](https://codecraft.tv/courses/angular/pipes/built-in-pipes/)
|
||||
|
||||
@@ -4,7 +4,6 @@ AWS (Amazon Web Services) offers a broad set of global cloud-based products incl
|
||||
|
||||
Learn more from the following links:
|
||||
|
||||
- [@article@How to create an AWS Account](https://grapplingdev.com/tutorials/how-to-create-aws-account)
|
||||
- [@article@AWS Documentation](https://docs.aws.amazon.com/)
|
||||
- [@article@Introduction of AWS](https://docs.aws.amazon.com/whitepapers/latest/aws-overview/introduction.html)
|
||||
- [@video@AWS Tutorial for Beginners](https://www.youtube.com/watch?v=zA8guDqfv40)
|
||||
- [@video@AWS Tutorial for Beginners](https://www.youtube.com/watch?v=zA8guDqfv40)
|
||||
@@ -1,6 +1,6 @@
|
||||
# ACID
|
||||
|
||||
ACID is an acronym representing four key properties that guarantee reliable processing of database transactions. It stands for Atomicity, Consistency, Isolation, and Durability. Atomicity ensures that a transaction is treated as a single, indivisible unit that either completes entirely or fails completely. Consistency maintains the database in a valid state before and after the transaction. Isolation ensures that concurrent transactions do not interfere with each other, appearing to execute sequentially. Durability guarantees that once a transaction is committed, it remains so, even in the event of system failures. These properties are crucial in maintaining data integrity and reliability in database systems, particularly in scenarios involving multiple, simultaneous transactions or where data accuracy is critical, such as in financial systems or e-commerce platforms.
|
||||
ACID are the four properties of relational database systems that help in making sure that we are able to perform the transactions in a reliable manner. It's an acronym which refers to the presence of four properties: atomicity, consistency, isolation and durability
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Apache
|
||||
|
||||
Apache, officially known as the Apache HTTP Server, is a free, open-source web server software developed and maintained by the Apache Software Foundation. It's one of the most popular web servers worldwide, known for its robustness, flexibility, and extensive feature set. Apache supports a wide range of operating systems and can handle various content types and programming languages through its modular architecture. It offers features like virtual hosting, SSL/TLS support, and URL rewriting. Apache's configuration files allow for detailed customization of server behavior. While it has faced competition from newer alternatives like Nginx, especially in high-concurrency scenarios, Apache remains widely used due to its stability, comprehensive documentation, and large community support. It's particularly favored for its ability to integrate with other open-source technologies in the LAMP (Linux, Apache, MySQL, PHP/Perl/Python) stack.
|
||||
Apache is a free, open-source HTTP server, available on many operating systems, but mainly used on Linux distributions. It is one of the most popular options for web developers, as it accounts for over 30% of all the websites, as estimated by W3Techs.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Apache Server Website](https://httpd.apache.org/)
|
||||
- [@article@Apache Server Website](https://httpd.apache.org/)
|
||||
- [@video@What is Apache Web Server?](https://www.youtube.com/watch?v=kaaenHXO4t4)
|
||||
- [@video@Apache vs NGINX](https://www.youtube.com/watch?v=9nyiY-psbMs)
|
||||
- [@feed@Explore top posts about Apache](https://app.daily.dev/tags/apache?ref=roadmapsh)
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
# Authentication
|
||||
|
||||
API authentication is the process of verifying the identity of clients attempting to access an API, ensuring that only authorized users or applications can interact with the API's resources. Common methods include API keys, OAuth 2.0, JSON Web Tokens (JWT), basic authentication, and OpenID Connect. These techniques vary in complexity and security level, from simple token-based approaches to more sophisticated protocols that handle both authentication and authorization. API authentication protects sensitive data, prevents unauthorized access, enables usage tracking, and can provide granular control over resource access. The choice of authentication method depends on factors such as security requirements, types of clients, ease of implementation, and scalability needs. Implementing robust API authentication is crucial for maintaining the integrity, security, and controlled usage of web services and applications in modern, interconnected software ecosystems.
|
||||
The API authentication process validates the identity of the client attempting to make a connection by using an authentication protocol. The protocol sends the credentials from the remote client requesting the connection to the remote access server in either plain text or encrypted form. The server then knows whether it can grant access to that remote client or not.
|
||||
|
||||
Here is the list of common ways of authentication:
|
||||
|
||||
- JWT Authentication
|
||||
- Token based Authentication
|
||||
- Session based Authentication
|
||||
- Basic Authentication
|
||||
- OAuth - Open Authorization
|
||||
- SSO - Single Sign On
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@User Authentication: Understanding the Basics & Top Tips](https://swoopnow.com/user-authentication/)
|
||||
- [@article@An overview about authentication methods](https://betterprogramming.pub/how-do-you-authenticate-mate-f2b70904cc3a)
|
||||
- [@roadmap.sh@SSO - Single Sign On](https://roadmap.sh/guides/sso)
|
||||
- [@roadmap.sh@OAuth - Open Authorization](https://roadmap.sh/guides/oauth)
|
||||
- [@roadmap.sh@JWT Authentication](https://roadmap.sh/guides/jwt-authentication)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# AWS Neptune
|
||||
|
||||
Amazon Neptune is a fully managed graph database service provided by Amazon Web Services (AWS). It's designed to store and navigate highly connected data, supporting both property graph and RDF (Resource Description Framework) models. Neptune uses graph query languages like Gremlin and SPARQL, making it suitable for applications involving complex relationships, such as social networks, recommendation engines, fraud detection systems, and knowledge graphs. It offers high availability, with replication across multiple Availability Zones, and supports up to 15 read replicas for improved performance. Neptune integrates with other AWS services, provides encryption at rest and in transit, and offers fast recovery from failures. Its scalability and performance make it valuable for handling large-scale, complex data relationships in enterprise-level applications.
|
||||
AWS Neptune is a fully managed graph database service designed for applications that require highly connected data.
|
||||
|
||||
Learn more from the following resources:
|
||||
It supports two popular graph models: Property Graph and RDF (Resource Description Framework), allowing you to build applications that traverse billions of relationships with millisecond latency.
|
||||
|
||||
- [@official@AWS Neptune Website](https://aws.amazon.com/neptune/)
|
||||
- [@video@Getting Started with Neptune Serverless](https://www.youtube.com/watch?v=b04-jjM9t4g)
|
||||
- [@article@Setting Up Amazon Neptune Graph Database](https://cliffordedsouza.medium.com/setting-up-amazon-neptune-graph-database-2b73512a7388)
|
||||
Neptune is optimized for storing and querying graph data, making it ideal for use cases like social networks, recommendation engines, fraud detection, and knowledge graphs.
|
||||
|
||||
It offers high availability, automatic backups, and multi-AZ (Availability Zone) replication, ensuring data durability and fault tolerance.
|
||||
|
||||
Additionally, Neptune integrates seamlessly with other AWS services and supports open standards like Gremlin, SPARQL, and Apache TinkerPop, making it flexible and easy to integrate into existing applications.
|
||||
@@ -1,9 +1,15 @@
|
||||
# Backpressure
|
||||
|
||||
Back pressure is a flow control mechanism in systems processing asynchronous data streams, where the receiving component signals its capacity to handle incoming data to the sending component. This feedback loop prevents overwhelming the receiver with more data than it can process, ensuring system stability and optimal performance. In software systems, particularly those dealing with high-volume data or event-driven architectures, back pressure helps manage resource allocation, prevent memory overflows, and maintain responsiveness. It's commonly implemented in reactive programming, message queues, and streaming data processing systems. By allowing the receiver to control the flow of data, back pressure helps create more resilient, efficient systems that can gracefully handle varying loads and prevent cascading failures in distributed systems.
|
||||
Backpressure is a design pattern that is used to manage the flow of data through a system, particularly in situations where the rate of data production exceeds the rate of data consumption. It is commonly used in cloud computing environments to prevent overloading of resources and to ensure that data is processed in a timely and efficient manner.
|
||||
|
||||
There are several ways to implement backpressure in a cloud environment:
|
||||
|
||||
- Buffering: This involves storing incoming data in a buffer until it can be processed, allowing the system to continue receiving data even if it is temporarily unable to process it.
|
||||
- Batching: This involves grouping incoming data into batches and processing the batches in sequence, rather than processing each piece of data individually.
|
||||
- Flow control: This involves using mechanisms such as flow control signals or windowing to regulate the rate at which data is transmitted between systems.
|
||||
|
||||
Backpressure is an important aspect of cloud design, as it helps to ensure that data is processed efficiently and that the system remains stable and available. It is often used in conjunction with other design patterns, such as auto-scaling and load balancing, to provide a scalable and resilient cloud environment.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Awesome Architecture: Backpressure](https://awesome-architecture.com/back-pressure/)
|
||||
- [@article@Backpressure explained — the resisted flow of data through software](https://medium.com/@jayphelps/backpressure-explained-the-flow-of-data-through-software-2350b3e77ce7)
|
||||
- [@video@What is Back Pressure](https://www.youtube.com/watch?v=viTGm_cV7lE)
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
# Base
|
||||
|
||||
Oracle Base Database Service enables you to maintain absolute control over your data while using the combined capabilities of Oracle Database and Oracle Cloud Infrastructure. Oracle Base Database Service offers database systems (DB systems) on virtual machines. They are available as single-node DB systems and multi-node RAC DB systems on Oracle Cloud Infrastructure (OCI). You can manage these DB systems by using the OCI Console, the OCI API, the OCI CLI, the Database CLI (DBCLI), Enterprise Manager, or SQL Developer.
|
||||
|
||||
Learn more from the following resources:
|
||||
|
||||
- [@official@Base Database Website](https://docs.oracle.com/en-us/iaas/base-database/index.html)
|
||||
# Base
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user