mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-14 02:32:00 +08:00
Compare commits
133 Commits
fix/modal-
...
testimonia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfaf120314 | ||
|
|
be5ccbbc0b | ||
|
|
12950051d6 | ||
|
|
03cd25b6c0 | ||
|
|
829615ffec | ||
|
|
6a24436418 | ||
|
|
c726a1a342 | ||
|
|
7d7835ee9c | ||
|
|
17d30c0e8d | ||
|
|
62bd5c339f | ||
|
|
111f702b9b | ||
|
|
3dd115dce1 | ||
|
|
75925cb53a | ||
|
|
de5bed02f8 | ||
|
|
72c670570b | ||
|
|
a450b4ed5a | ||
|
|
66be61efa3 | ||
|
|
caddd0f93d | ||
|
|
cc32cbe79c | ||
|
|
f9d39db24a | ||
|
|
203bbc6eae | ||
|
|
31a852113f | ||
|
|
66119e935b | ||
|
|
3374fafe5b | ||
|
|
8ed47a2e71 | ||
|
|
932b513d98 | ||
|
|
e0ae5dd309 | ||
|
|
70bc2a1038 | ||
|
|
86c1120559 | ||
|
|
c3135e1470 | ||
|
|
23346ec007 | ||
|
|
c92a183ef8 | ||
|
|
ca7888aa37 | ||
|
|
33b36a7017 | ||
|
|
bff98e6448 | ||
|
|
5dd2bc439f | ||
|
|
24d10d212f | ||
|
|
4127f77aac | ||
|
|
dbebf593fc | ||
|
|
0e0b550f98 | ||
|
|
80741df13b | ||
|
|
14b6aea3b1 | ||
|
|
e0da1e4f0e | ||
|
|
5cc4b834d1 | ||
|
|
314eb5d7d2 | ||
|
|
ad2597f610 | ||
|
|
8d25eabe3a | ||
|
|
6186e12b05 | ||
|
|
158857c928 | ||
|
|
8e6959cc60 | ||
|
|
e351f653a1 | ||
|
|
83e315aef7 | ||
|
|
b15bdd5f78 | ||
|
|
0783330a70 | ||
|
|
7ec56fd1ff | ||
|
|
201632941a | ||
|
|
126501b40a | ||
|
|
34ba9162b2 | ||
|
|
e093eddabc | ||
|
|
1e4a4c96b8 | ||
|
|
0c3ea981cb | ||
|
|
5de9539af5 | ||
|
|
3722e5f3bd | ||
|
|
7f90b8a0b0 | ||
|
|
2bafd61f71 | ||
|
|
adc66cf97c | ||
|
|
c754a971c3 | ||
|
|
dbda69fc23 | ||
|
|
6f1087981c | ||
|
|
ec8a0917c1 | ||
|
|
60e5e38a55 | ||
|
|
5d77d36236 | ||
|
|
37b3140516 | ||
|
|
34b68478cc | ||
|
|
9bb86408c5 | ||
|
|
d07fd3d183 | ||
|
|
9aa363a01e | ||
|
|
9f2a33f078 | ||
|
|
91cfa88b3d | ||
|
|
d87ea1c972 | ||
|
|
dbf2353a41 | ||
|
|
8d78c17c77 | ||
|
|
698dbbd7d8 | ||
|
|
25216d4052 | ||
|
|
2be0d61a1e | ||
|
|
f3ee75e92d | ||
|
|
db0b5e77ad | ||
|
|
c239886049 | ||
|
|
aab03074f8 | ||
|
|
433302b910 | ||
|
|
a583937f5c | ||
|
|
412e3b5935 | ||
|
|
1ffa292c98 | ||
|
|
0bef28fa20 | ||
|
|
1af013d5f8 | ||
|
|
28af19cd1c | ||
|
|
4696af9c6a | ||
|
|
e2dece0e6a | ||
|
|
a5f483c335 | ||
|
|
4552d3f9c8 | ||
|
|
f213bd9604 | ||
|
|
de65909357 | ||
|
|
9a77ca8a17 | ||
|
|
7ab3f758fd | ||
|
|
bcf4126b3a | ||
|
|
f9dbd16afe | ||
|
|
21ed4a647f | ||
|
|
68cf2c00c6 | ||
|
|
d3b0c32eb7 | ||
|
|
37ca08f956 | ||
|
|
b40ec83137 | ||
|
|
edc7de8226 | ||
|
|
5bffdebeb8 | ||
|
|
e5e902a268 | ||
|
|
5842b0a692 | ||
|
|
f1592571db | ||
|
|
4058dff406 | ||
|
|
5a813eea04 | ||
|
|
e577ba095f | ||
|
|
069cb6fc06 | ||
|
|
b0a3cdc8c4 | ||
|
|
536a79ef59 | ||
|
|
5a132b6f07 | ||
|
|
e3b56ab716 | ||
|
|
2d8030a175 | ||
|
|
dca2ba5fac | ||
|
|
16fc4b08ff | ||
|
|
7a7590d872 | ||
|
|
073708e7e9 | ||
|
|
66a8c2e73c | ||
|
|
34425019b7 | ||
|
|
699e05a3b4 | ||
|
|
3bd379692f |
@@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1737069970237
|
||||
"lastUpdateCheck": 1739229597159
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
PUBLIC_API_URL=https://api.roadmap.sh
|
||||
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars
|
||||
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh
|
||||
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh
|
||||
PUBLIC_COURSE_APP_URL=http://localhost:5173
|
||||
2
.github/workflows/cloudfront-fe-cache.yml
vendored
2
.github/workflows/cloudfront-fe-cache.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
-H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/roadmapsh/infra-ansible/actions/workflows/playbook.yml/dispatches \
|
||||
-d '{ "ref":"master", "inputs": { "playbook": "roadmap_web.yml", "tags": "cloudfront", "is_verbose": false } }'
|
||||
-d '{ "ref":"master", "inputs": { "playbook": "roadmap_web.yml", "tags": "cloudfront,cloudfront-course", "is_verbose": false } }'
|
||||
|
||||
16
.github/workflows/greetings.yml
vendored
16
.github/workflows/greetings.yml
vendored
@@ -15,19 +15,9 @@ jobs:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
🙌 Hello! Thank you for taking the time to file an issue.
|
||||
|
||||
If this is a bug report, please include any relevant logs or details that can help us debug the problem. Your help is greatly appreciated! 💡
|
||||
|
||||
We'll get back to you as soon as possible, kindly be patient for a response from a maintainer.
|
||||
pr-message: |
|
||||
🎉 Warm regards and welcome! Thank you for your first ever contribution to **Roadmap.sh**!
|
||||
Thank you for your first ever contribution to [roadmap.sh](https://roadmap.sh)! 🎉
|
||||
|
||||
We appreciate your effort and enthusiasm. Before diving in, we kindly ask you to take a moment to go through our [Contribution Guidelines](https://github.com/kamranahmedse/developer-roadmap/blob/master/contributing.md) 📘 to ensure your contribution aligns with the project's standards and goals.
|
||||
Please make sure to follow the [contribution guidelines](https://github.com/kamranahmedse/developer-roadmap/blob/master/contributing.md) when contributing to this project. Any PRs that don't follow the guidelines will be closed.
|
||||
|
||||
If you are fixing a bug, please reference the associated issue number in your pull request description. 🐛
|
||||
|
||||
If you're working on a new feature, feel free to check with the community on [discord](https://roadmap.sh/discord) to ensure the feature will be accepted. *Also, kindly refrain pinging the maintainer(s).* 🚀
|
||||
|
||||
Thanks for choosing to contribute, and for making this project better! 🌟
|
||||
Thanks for choosing to contribute, and for helping make this project better! 🌟
|
||||
@@ -67,10 +67,12 @@
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"sanitize-html": "^2.13.1",
|
||||
"satori": "^0.11.2",
|
||||
"satori-html": "^0.3.2",
|
||||
"sharp": "^0.33.5",
|
||||
"slugify": "^1.6.6",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"turndown": "^7.2.0",
|
||||
@@ -86,6 +88,7 @@
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@types/react-calendar-heatmap": "^1.6.7",
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
|
||||
2718
pnpm-lock.yaml
generated
2718
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/pdfs/roadmaps/cloudflare.pdf
Normal file
BIN
public/pdfs/roadmaps/cloudflare.pdf
Normal file
Binary file not shown.
@@ -42,7 +42,7 @@
|
||||
]
|
||||
},
|
||||
"mwPJh33MEUQ4Co_LiVEOb": {
|
||||
"title": "Differential Calculus",
|
||||
"title": "Differential Calculus ",
|
||||
"description": "",
|
||||
"links": [
|
||||
{
|
||||
|
||||
@@ -632,10 +632,26 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"bias-and-fareness@lhIU0ulpvDAn1Xc3ooYz_.md": {
|
||||
"title": "Bias and Fareness",
|
||||
"description": "",
|
||||
"links": []
|
||||
"lhIU0ulpvDAn1Xc3ooYz_": {
|
||||
"title": "Bias and Fairness",
|
||||
"description": "Bias and fairness in AI refer to the challenges of ensuring that machine learning models do not produce discriminatory or skewed outcomes. Bias can arise from imbalanced training data, flawed assumptions, or biased algorithms, leading to unfair treatment of certain groups based on race, gender, or other factors. Fairness aims to address these issues by developing techniques to detect, mitigate, and prevent biases in AI systems. Ensuring fairness involves improving data diversity, applying fairness constraints during model training, and continuously monitoring models in production to avoid unintended consequences, promoting ethical and equitable AI use.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What Do We Do About the Biases in AI?",
|
||||
"url": "https://hbr.org/2019/10/what-do-we-do-about-the-biases-in-ai",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "AI Bias - What Is It and How to Avoid It?",
|
||||
"url": "https://levity.ai/blog/ai-bias-how-to-avoid",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What about fairness, bias and discrimination?",
|
||||
"url": "https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/artificial-intelligence/guidance-on-ai-and-data-protection/how-do-we-ensure-fairness-in-ai/what-about-fairness-bias-and-discrimination/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sWBT-j2cRuFqRFYtV_5TK": {
|
||||
"title": "Security and Privacy Concerns",
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "API Authorization Methods",
|
||||
"url": "https://konghq.com/blog/engineering/common-api-authentication-methods",
|
||||
"url": "https://www.pingidentity.com/en/resources/identity-fundamentals/authorization/authorization-methods.html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -629,7 +629,7 @@
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "MySQL Full Course for free",
|
||||
"title": "MySQL Complete Course",
|
||||
"url": "https://www.youtube.com/watch?v=5OdVJbNCSso",
|
||||
"type": "video"
|
||||
}
|
||||
|
||||
519
public/roadmap-content/cloudflare.json
Normal file
519
public/roadmap-content/cloudflare.json
Normal file
@@ -0,0 +1,519 @@
|
||||
{
|
||||
"PcYnYAAkKMbzoiCnBfjqO": {
|
||||
"title": "JavaScript Fundamentals",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"q9oQTt_NqhdWvJfA5XH1V": {
|
||||
"title": "Basic Command-line Knowledge",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"9iSdASlRxyod9YwZ2IUry": {
|
||||
"title": "HTTP and Web Protocols",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"C08pIguX1N45Iw0kh0Fvu": {
|
||||
"title": "Git Basics",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"dSBYTGGkol3MAXyg7G7_J": {
|
||||
"title": "Node.js and NPM",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"zR84MFLL6y0dygz9hXXPA": {
|
||||
"title": "TypeScript Fundamentals",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"Isl5anwDvb1MacA-JH4ej": {
|
||||
"title": "Understand Serverless Architecture",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"TB6vGzDgGZ9yAd9MGR7vw": {
|
||||
"title": "Workers Runtime Environment",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"aGWLomYHGkIfn7GFc0_Yl": {
|
||||
"title": "Edge Computing Fundamentals",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"HNEXPU6r_T7UYvwLv2wnt": {
|
||||
"title": "Request/Response Handling",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"_2UnRlbUplHvs5-Stj4O4": {
|
||||
"title": "Fetch API and Runtime APIs",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"i6L9FI6fBDXr0XtMrc_uR": {
|
||||
"title": "Workers Lifecycle",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"WZSi9inWPhqZQvDN-C8BV": {
|
||||
"title": "Service Bindings",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"uBjcQ9PJUfwzu5N_2CNjN": {
|
||||
"title": "Caching Strategies",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"aStbAF4zraqhJ-N3RH4ha": {
|
||||
"title": "Middleware Patterns",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"9ef2VPCru8lCmRxxGe-Eo": {
|
||||
"title": "Bindings",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"-8MsWNvuqwQCbLpOx_kov": {
|
||||
"title": "Hono",
|
||||
"description": "Hono is a small, simple and ultrafast web framework built on web standards. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js. Hono is more known for supporting a lot more than the basics.\n\nUse-cases\n---------\n\nHono is a simple web application framework similar to the well known javascript framework Express, without a frontend. But it runs on CDN Edges and allows you to construct larger applications when combined with middleware.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Hono JS Examples",
|
||||
"url": "https://github.com/honojs/examples",
|
||||
"type": "opensource"
|
||||
},
|
||||
{
|
||||
"title": "Official Documentation",
|
||||
"url": "https://hono.dev/docs/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Hono.js: A Small Framework with Big Potential",
|
||||
"url": "https://medium.com/@appvintechnologies/hono-js-a-small-framework-with-big-potential-15a093fc5c07",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Quick Start with Hono: Simple Setup Guide",
|
||||
"url": "https://dev.to/koshirok096/quick-start-with-hono-simple-setup-guide-bite-sized-article-lhe",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Learn with me",
|
||||
"url": "https://www.youtube.com/watch?v=gY-TK33G6kQ",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
"15jl6CSCkqnh_eFfysLDM": {
|
||||
"title": "Itty Router",
|
||||
"description": "Itty Router is a lightweight router with the motto \"less is more\" that supports Cloudflare workers and pages. While other libraries may suffer from feature creep/bloat to please a wider audience, Itty Router painfully consider every single byte added to itty. Our router options range from ~450 bytes to ~970 bytes for a batteries-included version with built-in defaults, error handling, formatting, etc. On top of that, the following concepts aim to keep YOUR code tiny (and readable) as well.\n\nSimple Projects ideas\n---------------------\n\nItty Router is a lightweight router system that supports typescript. You can create easy and good routers for Cloudflare workers or pages. With a simple project like a URL shortener, you can use Itty Router and Cloudflare KV.\n\nOther project ideas can be found:\n\n* Webhook Relay\n * Transform webhook data or API data towards another API so you can transform the data as you like.\n* Micro URL Monitoring\n * Monitor any URL and give back responses on the specific endpoint.\n* Single-Use Download Links (Watch out for costs from Cloudflare!)\n * Generate links that expire after one download, ideal for file sharing.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Official Documentation",
|
||||
"url": "https://itty.dev/itty-router/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Tzx93tvoGrc9_fKQqkorN": {
|
||||
"title": "Wrangler",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"uoaOrypiMkyoikXvTHeVS": {
|
||||
"title": "DevTools Integration",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"8Y6TIYoWIXrxtmzDVdS0b": {
|
||||
"title": "CI/CD Pipelines",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"zSwio18XdBfqwSneAx_AP": {
|
||||
"title": "Any Frontend Framework",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"o4sBgniPmLqwej6TlIPcl": {
|
||||
"title": "Miniflare",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"1dGFfQauOgHP7T4ReMpCU": {
|
||||
"title": "Workers KV",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"EBTHbXOOZiqrcYJvKhcWV": {
|
||||
"title": "Key-value Operations",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"sQlRIYLnZcugATgpogJmw": {
|
||||
"title": "Metadata Handling",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"i64-aCpZHygq76fBU6eXD": {
|
||||
"title": "Bulk Operations",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"OgW-iIrJZ5-sOWKnFpIZd": {
|
||||
"title": "Caching Patterns",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"gxLUlXGuaY5Q-0xmBgQwz": {
|
||||
"title": "R2 Storage",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"K9iW2H6riKwddWmpWJFJw": {
|
||||
"title": "Object Storage",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"BPahk1qH9Hk11tsE2hw3A": {
|
||||
"title": "Large File Handling",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"3jU5753Uza2aS-gZo7w4k": {
|
||||
"title": "Asset Management",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"UNE6XK4su5r2jcxhY7hOG": {
|
||||
"title": "Bucket Operations",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"YvgmmF9sWfURgijFV7E31": {
|
||||
"title": "Bucket Lifecycle",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"mKN0Ta3zSk7PCm_uHYKFN": {
|
||||
"title": "D1",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"PnhP47woPJb_JnLpMMiTw": {
|
||||
"title": "Schema Management",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"M8rSSVFUHixgWZRfaBPHb": {
|
||||
"title": "Migrations",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"65xDESm6jbHWkVO4NgHqx": {
|
||||
"title": "Query Optimization",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"MpWO1sroeF106SEMU1V1a": {
|
||||
"title": "Drizzle",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"h3MHLZZwkYqqb5PSfMhpB": {
|
||||
"title": "Prisma",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"zyRgTtlng6idboSgL9YTt": {
|
||||
"title": "Queues",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"EFA8m0EdhygxcBWzwmbnT": {
|
||||
"title": "Message Processing",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"qgvDGyLjc6lMmVPjHozFM": {
|
||||
"title": "Background Jobs",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"MInAsLLJtIq6WQDSj5yGH": {
|
||||
"title": "Rate Limiting",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"Grl59SjY31Q3sgf9uX-xf": {
|
||||
"title": "Dead Letter Queues",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"G-xBbtaniYFRE9Dgs18px": {
|
||||
"title": "Durable Objects",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"EQjhRlM7zpANNWkypScIl": {
|
||||
"title": "State Management",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"RYm0oBFCxm-S-aCwZ21p6": {
|
||||
"title": "Coordination",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"36w4Q73XkCwo5Cva0XsF8": {
|
||||
"title": "Persistence",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"rxxibrJUo1rQ3XCuUIP59": {
|
||||
"title": "Transactional Operations",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"rAl7zXcODiqIpS__3qf1A": {
|
||||
"title": "Workflows",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"a0S0_JLwLLNGLUAHrqG4P": {
|
||||
"title": "Workers AI",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"zMwmoCUp9429_aXU-Bz4H": {
|
||||
"title": "Text Generation",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"S7laV14zsx31O0Tsj2SRL": {
|
||||
"title": "Image Processing",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"HJbJ8OxjJzznYwLlIOSO2": {
|
||||
"title": "Speech Rcognition",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"QxPoNHsL-Pj_z3aU6qEP4": {
|
||||
"title": "AI Model Integration",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"NWGVtH1vxQuO4lly0Omuy": {
|
||||
"title": "Vectorize",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"UIWaR1ZdjSm0UAS69Kz_5": {
|
||||
"title": "Vector Embeddings",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"pg3GtykCegK411DYDN8sN": {
|
||||
"title": "Similarity Search",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"Ep9_oV_YnkbH1gHM-n3gO": {
|
||||
"title": "AI-powered Search",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"LoT3NtpNj9uAgQRV-MD_E": {
|
||||
"title": "Stream",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"zQp7XfDKWJgMf2LexRJhN": {
|
||||
"title": "Video Delivery",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"RiQSPAV9uRFgwQFJckTFV": {
|
||||
"title": "Live streaming",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"3B6Z7F0D3Sf8ZBlV3kkGx": {
|
||||
"title": "Video Processing",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"8bOWuopxHtBWUSFaVT54P": {
|
||||
"title": "Images",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"vHQdMgaL2EEr2o_eJmOuV": {
|
||||
"title": "Calls",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"aKEH4ZxI6J1nwjp_AgH5r": {
|
||||
"title": "Logging and Monitoring",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"z-1Ye5hcNdr9r6Gwdw7mv": {
|
||||
"title": "Email Workers",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"-lsYPD6JueIV94RybGH_Y": {
|
||||
"title": "Routing",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"6bNUqx5f_w5NuDL25BABN": {
|
||||
"title": "Processing",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"kdIfqTCcOSvV4KDpjr7nu": {
|
||||
"title": "Filtering",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"vu8yJsS1WccsdcEVUqwNd": {
|
||||
"title": "AI Gateway",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"qkFRW_tJB8_1IYpYskQ5M": {
|
||||
"title": "Browser Rendering",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"76xovsBrKOnlRBVjsqNq1": {
|
||||
"title": "Security & Performance",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"8IF7jftushwZrn7JXpC_v": {
|
||||
"title": "Workers Security Model",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"uNinrB9wm5ahjGXu5fc0g": {
|
||||
"title": "Isolates Architecture",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"KWix4jeNUKJ07Iu95Mqj_": {
|
||||
"title": "Web Security Headers",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"JP5U6c2fZjtkU-Xzwtapx": {
|
||||
"title": "Rate Limiting",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"ui3pUfsGMxv4WRzHkgbF0": {
|
||||
"title": "Cache API",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"INiqdtppBmCthOEXuHb-V": {
|
||||
"title": "HTML Rewriting",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"sXBxaQtwJ-luGVXdqVXk1": {
|
||||
"title": "Edge SSL/TLS",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"So-cKAVfbgsw2zzFREu7Q": {
|
||||
"title": "Bot Management",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"wvurOKbemF4Tt2WZcmqDL": {
|
||||
"title": "Integration & Workflows",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"SaHqm7T4FFVrsgyfImo66": {
|
||||
"title": "Pages Functions",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"JfpVexcbuWCx_R3EjFmbo": {
|
||||
"title": "Service Bindings",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"jYAUIKozuhsNK5LbkeAJ6": {
|
||||
"title": "Inter Worker Communication",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"4g5w6IAdzefdlRTxbRbdS": {
|
||||
"title": "External API Integration",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"uOUjI6CPrhZIlz6mRCtOW": {
|
||||
"title": "Webhook Handling",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"Z9Yywlf7rXFBtxTq5B2Y5": {
|
||||
"title": "Event-driven Architectures",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"gsCRhwwjXuyueaYHSPOVZ": {
|
||||
"title": "Development Tools",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"n0vIbHmUZHrF4WjEhYdb8": {
|
||||
"title": "Wrangler",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"vZHBp4S6WaS5sa5rfUOk-": {
|
||||
"title": "Miniflare",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"G6YQZUQh_x8Qxm1oBseLQ": {
|
||||
"title": "DevTools",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"jyWxaMx7_nojt5HsyAv7K": {
|
||||
"title": "Testing Frameworks",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"Cy2T8978yUAPGol-yzxv_": {
|
||||
"title": "Monitoring Tools",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"TmQC7fTL6b9EsBDYibv4g": {
|
||||
"title": "Debugging Techniques",
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"8WZpSKBHCeYfTEL9tBNKr": {
|
||||
"title": "Tunnels",
|
||||
"description": "",
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
@@ -2668,7 +2668,7 @@
|
||||
},
|
||||
"0btHNkzWL1w_-pUgU_k2y": {
|
||||
"title": "P = NP",
|
||||
"description": "The P = NP problem is one of the most famous problems in computer science. It asks if the problem of determining if a given input belongs to a certain class of problems is as hard as the problem of solving the given input. In other words, it asks if the problem of determining if a given input belongs to a certain class of problems is as hard as the problem of determining if a given input belongs to a certain class of problems. This problem is also known as the Halting Problem.\n\nVisit the following resources to learn more:",
|
||||
"description": "The P = NP problem is one of the most famous problems in computer science. It asks whether a problem that can be solved in polynomial time on a non-deterministic machine (i.e., the problem is in NP) can also be solved in polynomial time on a deterministic machine (i.e., the problem is in P).\n\nIf you can find a polynomial-time solution to an NP-complete problem, then all problems in NP can be solved in polynomial time. This shows that P = NP.\n\nIf you can prove for any single NP-complete problem that it is only solvable in exponential time, then all NP-complete problems are only solvable in exponential time. This shows that P ≠ NP.\n\nSo far, we don't know whether P = NP or P ≠ NP.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Whats P=NP?, and why is it such a famous question?",
|
||||
@@ -3366,8 +3366,19 @@
|
||||
},
|
||||
"tcQSH-eAvJUZuePTDjAIb": {
|
||||
"title": "DML",
|
||||
"description": "The SQL commands that deals with the manipulation of data present in the database belong to DML or Data Manipulation Language and this includes most of the SQL statements. It is the component of the SQL statement that controls access to data and to the database. Basically, DCL statements are grouped with DML statements.\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"description": "The SQL commands that manipulate data in the database belong to DML, or Data Manipulation Language, and this includes most of the SQL statements. DCL is the component of the SQL statement that controls access to data and to the database. Basically, DCL statements are grouped with DML statements.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "DML: Data Manipulation Language",
|
||||
"url": "https://satoricyber.com/glossary/dml-data-manipulation-language",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Difference Between DDL and DML",
|
||||
"url": "https://appmaster.io/blog/difference-between-ddl-and-dml",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"05lkb3B86Won7Rkf-8DeD": {
|
||||
"title": "DQL",
|
||||
@@ -3931,7 +3942,7 @@
|
||||
},
|
||||
"GDLKJkKgB-i7n0YcV2NDa": {
|
||||
"title": "How Computers Calculate",
|
||||
"description": "Visit the following resources to learn more:",
|
||||
"description": "Computers calculate using the binary system, where all data is represented as 0s and 1s. These binary states correspond to the ON/OFF positions of transistors, which are the building blocks of logic gates (AND, OR, NOT). Numbers, characters, and instructions are broken into binary sequences (bits), and grouped into bytes (8 bits). Arithmetic operations like addition are performed through logic gates, which combine binary values. The CPU executes these calculations by following a fetch-decode-execute cycle. Complex calculations, such as handling decimals, use floating-point representation. Programs written in high-level languages are compiled into machine code for the CPU to execute.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "How computers calculate - ALU",
|
||||
|
||||
@@ -6,12 +6,18 @@
|
||||
},
|
||||
"yCnn-NfSxIybUQ2iTuUGq": {
|
||||
"title": "What is Data Analytics",
|
||||
"description": "Data Analytics is a core component of a Data Analyst's role. The field involves extracting meaningful insights from raw data to drive decision-making processes. It includes a wide range of techniques and disciplines ranging from the simple data compilation to advanced algorithms and statistical analysis. As a data analyst, you are expected to understand and interpret complex digital data, such as the usage statistics of a website, the sales figures of a company, or client engagement over social media, etc. This knowledge enables data analysts to support businesses in identifying trends, making informed decisions, predicting potential outcomes - hence playing a crucial role in shaping business strategies.",
|
||||
"links": []
|
||||
"description": "Data Analytics is a core component of a Data Analyst's role. The field involves extracting meaningful insights from raw data to drive decision-making processes. It includes a wide range of techniques and disciplines ranging from the simple data compilation to advanced algorithms and statistical analysis. As a data analyst, you are expected to understand and interpret complex digital data, such as the usage statistics of a website, the sales figures of a company, or client engagement over social media, etc. This knowledge enables data analysts to support businesses in identifying trends, making informed decisions, predicting potential outcomes - hence playing a crucial role in shaping business strategies.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Introduction to Data Analytics",
|
||||
"url": "https://www.coursera.org/learn/introduction-to-data-analytics",
|
||||
"type": "course"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Lsapbmg-eMIYJAHpV97nO": {
|
||||
"title": "Types of Data Analytics",
|
||||
"description": "Data Analytics has proven to be a critical part of decision-making in modern business ventures. It is responsible for discovering, interpreting, and transforming data into valuable information. Different types of data analytics look at past, present, or predictive views of business operations.\n\nData Analysts, as ambassadors of this domain, employ these types, to answer various questions:\n\n* Descriptive Analytics _(what happened in the past?)_\n* Diagnostic Analytics _(why did it happened in the past?)_\n* Predictive Analytics _(what will happen in the future?)_\n* Prescriptive Analytics _(how can we make it happen?)_\n\nUnderstanding these types gives data analysts the power to transform raw datasets into strategic insights.\n\nVisit the following resources to learn more:",
|
||||
"description": "Data Analytics has proven to be a critical part of decision-making in modern business ventures. It is responsible for discovering, interpreting, and transforming data into valuable information. Different types of data analytics look at past, present, or predictive views of business operations.\n\nData Analysts, as ambassadors of this domain, employ these types, to answer various questions:\n\n* Descriptive Analytics _(what happened in the past?)_\n* Diagnostic Analytics _(why did it happened in the past?)_\n* Predictive Analytics _(what will happen in the future?)_\n* Prescriptive Analytics _(how can we make it happen?)_\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Data Analytics and its type",
|
||||
@@ -72,12 +78,12 @@
|
||||
"description": "Predictive analysis is a crucial type of data analytics that any competent data analyst should comprehend. It refers to the practice of extracting information from existing data sets in order to determine patterns and forecast future outcomes and trends. Data analysts apply statistical algorithms, machine learning techniques, and artificial intelligence to the data to anticipate future results. Predictive analysis enables organizations to be proactive, forward-thinking, and strategic by providing them valuable insights on future occurrences. It's a powerful tool that gives companies a significant competitive edge by enabling risk management, opportunity identification, and strategic decision-making.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is predictive analytics? - Google",
|
||||
"title": "What is Predictive Analytics? - Google",
|
||||
"url": "https://cloud.google.com/learn/what-is-predictive-analytics",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is predictive analytics?",
|
||||
"title": "What is Predictive Analytics?",
|
||||
"url": "https://www.youtube.com/watch?v=cVibCHRSxB0",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -125,7 +131,7 @@
|
||||
"description": "The Cleanup of Data is a critical component of a Data Analyst's role. It involves the process of inspecting, cleaning, transforming, and modeling data to discover useful information, inform conclusions, and support decision making. This process is crucial for Data Analysts to generate accurate and significant insights from data, ultimately resulting in better and more informed business decisions. A solid understanding of data cleanup procedures and techniques is a fundamental skill for any Data Analyst. Hence, it is necessary to hold a high emphasis on maintaining data quality by managing data integrity, accuracy, and consistency during the data cleanup process.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Top 10 ways to clean your data",
|
||||
"title": "Top 10 Ways to Clean Your Data",
|
||||
"url": "https://support.microsoft.com/en-gb/office/top-ten-ways-to-clean-your-data-2844b620-677c-47a7-ac3e-c2e157d1db19",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -157,7 +163,7 @@
|
||||
"description": "The visualization of data is an essential skill in the toolkit of every data analyst. This practice is about transforming complex raw data into a graphical format that allows for an easier understanding of large data sets, trends, outliers, and important patterns. Whether pie charts, line graphs, bar graphs, or heat maps, data visualization techniques not only streamline data analysis, but also facilitate a more effective communication of the findings to others. This key concept underscores the importance of presenting data in a digestible and visually appealing manner to drive data-informed decision making in an organization.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Data visualization beginner's guide",
|
||||
"title": "Data Visualization Beginner's Guide",
|
||||
"url": "https://www.tableau.com/en-gb/learn/articles/data-visualization",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -223,7 +229,7 @@
|
||||
},
|
||||
"yBlJrNo9eO470dLp6OaQZ": {
|
||||
"title": "DATEDIF",
|
||||
"description": "The `DATEDIF` function is an incredibly valuable tool for a Data Analyst in Excel or Google Sheets, by providing the ability to calculate the difference between two dates. This function takes in three parameters: start date, end date and the type of difference required (measured in years, months, days, etc.). In Data Analysis, particularly when dealing with time-series data or when you need to uncover trends over specific periods, the `DATEDIF` function is a necessary asset. Recognizing its functionality will enable a data analyst to manipulate or shape data progressively and efficiently.\n\n* `DATEDIF` is technically still supported, but wont show as an option. For additional information, see Excel \"Help\" page.\n\nLearn more from the following resources:",
|
||||
"description": "The `DATEDIF` function is an incredibly valuable tool for a Data Analyst in Excel or Google Sheets, by providing the ability to calculate the difference between two dates. This function takes in three parameters: start date, end date and the type of difference required (measured in years, months, days, etc.). In Data Analysis, particularly when dealing with time-series data or when you need to uncover trends over specific periods, the `DATEDIF` function is a necessary asset. Recognizing its functionality will enable a data analyst to manipulate or shape data progressively and efficiently.\n\n`DATEDIF` is technically still supported, but wont show as an option. For additional information, see Excel \"Help\" page.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "DATEDIF function",
|
||||
@@ -407,17 +413,17 @@
|
||||
"description": "Data Analysts recurrently find the need to summarize, investigate, and analyze their data to make meaningful and insightful decisions. One of the most powerful tools to accomplish this in Microsoft Excel is the Pivot Table. Pivot Tables allow analysts to organize and summarize large quantities of data in a concise, tabular format. The strength of pivot tables comes from their ability to manipulate data dynamically, leading to quicker analysis and richer insights. Understanding and employing Pivot Tables efficiently is a fundamental skill for any data analyst, as it directly impacts their ability to derive significant information from raw datasets.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Create a pivot table",
|
||||
"title": "Create a Pivot Table",
|
||||
"url": "https://support.microsoft.com/en-gb/office/create-a-pivottable-to-analyze-worksheet-data-a9a84538-bfe9-40a9-a8e9-f99134456576",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Pivot tables in excel",
|
||||
"title": "Pivot Tables in Excel",
|
||||
"url": "https://www.excel-easy.com/data-analysis/pivot-tables.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "How to create a pivot table in excel",
|
||||
"title": "How to Create a Pivot Table in Excel",
|
||||
"url": "https://www.youtube.com/watch?v=PdJzy956wo4",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -452,15 +458,31 @@
|
||||
},
|
||||
"M1QtGTLyygIjePoCfvjve": {
|
||||
"title": "Data Manipulation Libraries",
|
||||
"description": "Data manipulation libraries are essential tools in data science and analytics, enabling efficient handling, transformation, and analysis of large datasets. Python, a popular language for data science, offers several powerful libraries for this purpose. Pandas is a highly versatile library that provides data structures like DataFrames, which allow for easy manipulation and analysis of tabular data. NumPy, another fundamental library, offers support for large, multi-dimensional arrays and matrices, along with a collection of mathematical functions to operate on these arrays. Together, Pandas and NumPy form the backbone of data manipulation in Python, facilitating tasks such as data cleaning, merging, reshaping, and statistical analysis, thus streamlining the data preparation process for machine learning and other data-driven applications.",
|
||||
"links": []
|
||||
"description": "Data manipulation libraries are essential tools in data science and analytics, enabling efficient handling, transformation, and analysis of large datasets. Python, a popular language for data science, offers several powerful libraries for this purpose. Pandas is a highly versatile library that provides data structures like DataFrames, which allow for easy manipulation and analysis of tabular data. NumPy, another fundamental library, offers support for large, multi-dimensional arrays and matrices, along with a collection of mathematical functions to operate on these arrays. Together, Pandas and NumPy form the backbone of data manipulation in Python, facilitating tasks such as data cleaning, merging, reshaping, and statistical analysis, thus streamlining the data preparation process for machine learning and other data-driven applications.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Pandas",
|
||||
"url": "https://pandas.pydata.org/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "NumPy",
|
||||
"url": "https://numpy.org/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Top Python Libraries for Data Science",
|
||||
"url": "https://www.simplilearn.com/top-python-libraries-for-data-science-article",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"8OXmF2Gn6TYJotBRvDjqA": {
|
||||
"title": "Pandas",
|
||||
"description": "Pandas is a widely acknowledged and highly useful data manipulation library in the world of data analysis. Known for its robust features like data cleaning, wrangling and analysis, pandas has become one of the go-to tools for data analysts. Built on NumPy, it provides high-performance, easy-to-use data structures and data analysis tools. In essence, its flexibility and versatility make it a critical part of the data analyst's toolkit, as it holds the capability to cater to virtually every data manipulation task.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Pandas Website",
|
||||
"title": "Pandas",
|
||||
"url": "https://pandas.pydata.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -473,7 +495,7 @@
|
||||
},
|
||||
"l1SnPc4EMqGdaIAhIQfrT": {
|
||||
"title": "Data Visualisation Libraries",
|
||||
"description": "Data visualization libraries are crucial in data science for transforming complex datasets into clear and interpretable visual representations, facilitating better understanding and communication of data insights. In Python, several libraries are widely used for this purpose. Matplotlib is a foundational library that offers comprehensive tools for creating static, animated, and interactive plots. Seaborn, built on top of Matplotlib, provides a high-level interface for drawing attractive and informative statistical graphics with minimal code. Plotly is another powerful library that allows for the creation of interactive and dynamic visualizations, which can be easily embedded in web applications. Additionally, libraries like Bokeh and Altair offer capabilities for creating interactive plots and dashboards, enhancing exploratory data analysis and the presentation of data findings. Together, these libraries enable data scientists to effectively visualize trends, patterns, and outliers in their data, making the analysis more accessible and actionable.",
|
||||
"description": "Data visualization libraries are crucial in data science for transforming complex datasets into clear and interpretable visual representations, facilitating better understanding and communication of data insights. In Python, several libraries are widely used for this purpose. Matplotlib is a foundational library that offers comprehensive tools for creating static, animated, and interactive plots. Seaborn, built on top of Matplotlib, provides a high-level interface for drawing attractive and informative statistical graphics with minimal code. Plotly is another powerful library that allows for the creation of interactive and dynamic visualizations, which can be easily embedded in web applications. Additionally, libraries like Bokeh and Altair offer capabilities for creating interactive plots and dashboards, enhancing exploratory data analysis and the presentation of data findings. Together, these libraries enable data scientists to effectively visualize trends, patterns, and outliers in their data, making the analysis more accessible and actionable.\n\nLearn more from the following resources:",
|
||||
"links": []
|
||||
},
|
||||
"uGkXxdMXUMY-3fQFS1jK8": {
|
||||
@@ -481,7 +503,7 @@
|
||||
"description": "Matplotlib is a paramount data visualization library used extensively by data analysts for generating a wide array of plots and graphs. Through Matplotlib, data analysts can convey results clearly and effectively, driving insights from complex data sets. It offers a hierarchical environment which is very natural for a data scientist to work with. Providing an object-oriented API, it allows for extensive customization and integration into larger applications. From histograms, bar charts, scatter plots to 3D graphs, the versatility of Matplotlib assists data analysts in the better comprehension and compelling representation of data.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Matplotlib Website",
|
||||
"title": "Matplotlib",
|
||||
"url": "https://matplotlib.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -497,7 +519,7 @@
|
||||
"description": "Dplyr is a powerful and popular toolkit for data manipulation in R. As a data analyst, this library provides integral functions to manipulate, clean, and process data efficiently. It has been designed to be easy and intuitive, ensuring a robust and consistent syntax. Dplyr ensures data reliability and fast processing, essential for analysts dealing with large datasets. With a strong focus on efficiency, dplyr functions like select, filter, arrange, mutate, summarise, and group\\_by optimise data analysis operations, making data manipulation a smoother and hassle-free procedure for data analysts.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "dplyr website",
|
||||
"title": "dplyr",
|
||||
"url": "https://dplyr.tidyverse.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -513,7 +535,7 @@
|
||||
"description": "When it comes to data visualization in R programming, ggplot2 stands tall as one of the primary tools for data analysts. This data visualization library, which forms part of the tidyverse suite of packages, facilitates the creation of complex and sophisticated visual narratives. With its grammar of graphics philosophy, ggplot2 enables analysts to build graphs and charts layer by layer, thereby offering detailed control over graphical features and design. Its versatility in creating tailored and aesthetically pleasing graphics is a vital asset for any data analyst tackling exploratory data analysis, reporting, or dashboard building.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "ggplot2 website",
|
||||
"title": "ggplot2",
|
||||
"url": "https://ggplot2.tidyverse.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -526,21 +548,27 @@
|
||||
},
|
||||
"_sjXCLHHTbZromJYn6fnu": {
|
||||
"title": "Data Collection",
|
||||
"description": "In the context of the Data Analyst role, data collection is a foundational process that entails gathering relevant data from various sources. This data can be quantitative or qualitative and may be sourced from databases, online platforms, customer feedback, among others. The gathered information is then cleaned, processed, and interpreted to extract meaningful insights. A data analyst performs this whole process carefully, as the quality of data is paramount to ensuring accurate analysis, which in turn informs business decisions and strategies. This highlights the importance of an excellent understanding, proper tools, and precise techniques when it comes to data collection in data analysis.",
|
||||
"links": []
|
||||
"description": "Data collection is a foundational process that entails gathering relevant data from various sources. This data can be quantitative or qualitative and may be sourced from databases, online platforms, customer feedback, among others. The gathered information is then cleaned, processed, and interpreted to extract meaningful insights. A data analyst performs this whole process carefully, as the quality of data is paramount to ensuring accurate analysis, which in turn informs business decisions and strategies. This highlights the importance of an excellent understanding, proper tools, and precise techniques when it comes to data collection in data analysis.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Data Collection",
|
||||
"url": "https://en.wikipedia.org/wiki/Data_collection",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tYPeLCxbqvMFlTkCGjdHg": {
|
||||
"title": "Databases",
|
||||
"description": "Behind every strong data analyst, there's not just a rich assortment of data, but a set of robust databases that enable effective data collection. Databases are a fundamental aspect of data collection in a world where the capability to manage, organize, and evaluate large volumes of data is critical. As a data analyst, the understanding and use of databases is instrumental in capturing the necessary data for conducting qualitative and quantitative analysis, forecasting trends and making data-driven decisions. Thorough knowledge of databases, therefore, can be considered a key component of a data analyst's arsenal. These databases can vary from relational databases like SQL to NoSQL databases like MongoDB, each serving a unique role in the data collection process.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "PostgreSQL Roadmap",
|
||||
"url": "https://roadmap.sh/postgresql-dba",
|
||||
"title": "Visit Dedicated SQL Roadmap",
|
||||
"url": "https://roadmap.sh/sql",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "MongoDB Roadmap",
|
||||
"url": "https://roadmap.sh/mongodb",
|
||||
"title": "Visit Dedicated PostgreSQL Roadmap",
|
||||
"url": "https://roadmap.sh/postgresql-dba",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
@@ -571,7 +599,7 @@
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "A beginners guide to APIs",
|
||||
"title": "A Beginner's Guide to APIs",
|
||||
"url": "https://www.postman.com/what-is-an-api/",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -582,12 +610,12 @@
|
||||
"description": "Web scraping plays a significant role in collecting unique datasets for data analysis. In the realm of a data analyst's tasks, web scraping refers to the method of extracting information from websites and converting it into a structured usable format like a CSV, Excel spreadsheet, or even into databases. This technique allows data analysts to gather large sets of data from the internet, which otherwise could be time-consuming if done manually. The capability of web scraping and parsing data effectively can give data analysts a competitive edge in their data analysis process, from unlocking in-depth, insightful information to making data-driven decisions.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is web scraping what is it used for?",
|
||||
"title": "What is Web Scraping & What is it used for?",
|
||||
"url": "https://www.parsehub.com/blog/what-is-web-scraping/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is web scraping?",
|
||||
"title": "What is Web Scraping?",
|
||||
"url": "https://www.youtube.com/watch?v=dlj_QL-ENJM",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -595,8 +623,14 @@
|
||||
},
|
||||
"E6cpb6kvluJM8OGuDcFBT": {
|
||||
"title": "Data Cleanup",
|
||||
"description": "Data cleaning, which is often referred as data cleansing or data scrubbing, is one of the most important and initial steps in the data analysis process. As a data analyst, the bulk of your work often revolves around understanding, cleaning, and standardizing raw data before analysis. Data cleaning involves identifying, correcting or removing any errors or inconsistencies in datasets in order to improve their quality. The process is crucial because it directly determines the accuracy of the insights you generate - garbage in, garbage out. Even the most sophisticated models and visualizations would not be of much use if they're based on dirty data. Therefore, mastering data cleaning techniques is essential for any data analyst.",
|
||||
"links": []
|
||||
"description": "Data cleaning, which is often referred as data cleansing or data scrubbing, is one of the most important and initial steps in the data analysis process. As a data analyst, the bulk of your work often revolves around understanding, cleaning, and standardizing raw data before analysis. Data cleaning involves identifying, correcting or removing any errors or inconsistencies in datasets in order to improve their quality. The process is crucial because it directly determines the accuracy of the insights you generate - garbage in, garbage out. Even the most sophisticated models and visualizations would not be of much use if they're based on dirty data. Therefore, mastering data cleaning techniques is essential for any data analyst.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Data Cleaning",
|
||||
"url": "https://www.tableau.com/learn/articles/what-is-data-cleaning#:~:text=tools%20and%20software-,What%20is%20data%20cleaning%3F,to%20be%20duplicated%20or%20mislabeled.",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"X9WmfHOks82BIAzs6abqO": {
|
||||
"title": "Handling Missing Data",
|
||||
@@ -636,14 +670,14 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "Outliers",
|
||||
"url": "%5Bhttps://www.mathsisfun.com/data/outliers.html",
|
||||
"url": "https://www.mathsisfun.com/data/outliers.html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"t_BRtEharsrOZxoyX0OzV": {
|
||||
"title": "Data Transformation",
|
||||
"description": "Data Transformation, also known as Data Wrangling, is an essential part of a Data Analyst's role. This process involves the conversion of data from a raw format into another format to make it more appropriate and valuable for a variety of downstream purposes such as analytics. Data Analysts transform data to make the data more suitable for analysis, ensure accuracy, and to improve data quality. The right transformation techniques can give the data a structure, multiply its value, and enhance the accuracy of the analytics performed by serving meaningful results.",
|
||||
"description": "Data Transformation, also known as Data Wrangling, is an essential part of a Data Analyst's role. This process involves the conversion of data from a raw format into another format to make it more appropriate and valuable for a variety of downstream purposes such as analytics. Data Analysts transform data to make the data more suitable for analysis, ensure accuracy, and to improve data quality. The right transformation techniques can give the data a structure, multiply its value, and enhance the accuracy of the analytics performed by serving meaningful results.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is data transformation?",
|
||||
@@ -662,7 +696,7 @@
|
||||
"description": "In the realms of data analysis, data cleaning is a crucial preliminary process, this is where `pandas` - a popular python library - shines. Primarily used for data manipulation and analysis, pandas adopts a flexible and powerful data structure (DataFrames and Series) that greatly simplifies the process of cleaning raw, messy datasets. Data analysts often work with large volumes of data, some of which may contain missing or inconsistent data that can negatively impact the results of their analysis. By utilizing pandas, data analysts can quickly identify, manage and fill these missing values, drop unnecessary columns, rename column headings, filter specific data, apply functions for more complex data transformations and much more. Thus, making pandas an invaluable tool for effective data cleaning in data analysis.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Pandas Website",
|
||||
"title": "Pandas",
|
||||
"url": "https://pandas.pydata.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -678,7 +712,7 @@
|
||||
"description": "Data cleaning plays a crucial role in the data analysis pipeline, where it rectifies and enhances the quality of data to increase the efficiency and authenticity of the analytical process. The `dplyr` package, an integral part of the `tidyverse` suite in R, has become a staple in the toolkit of data analysts dealing with data cleaning. `dplyr` offers a coherent set of verbs that significantly simplifies the process of manipulating data structures, such as dataframes and databases. This involves selecting, sorting, filtering, creating or modifying variables, and aggregating records, among other operations. Incorporating `dplyr` into the data cleaning phase enables data analysts to perform operations more effectively, improve code readability, and handle large and complex data with ease.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "dplyr website",
|
||||
"title": "dplyr",
|
||||
"url": "https://dplyr.tidyverse.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -790,7 +824,7 @@
|
||||
"description": "When focusing on data analysis, understanding key statistical concepts is crucial. Amongst these, central tendency is a foundational element. Central Tendency refers to the measure that determines the center of a distribution. The average is a commonly used statistical tool by which data analysts discern trends and patterns. As one of the most recognized forms of central tendency, figuring out the \"average\" involves summing all values in a data set and dividing by the number of values. This provides analysts with a 'typical' value, around which the remaining data tends to cluster, facilitating better decision-making based on existing data.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "How to calculate the average",
|
||||
"title": "How to Calculate the Average",
|
||||
"url": "https://support.microsoft.com/en-gb/office/calculate-the-average-of-a-group-of-numbers-e158ef61-421c-4839-8290-34d7b1e68283#:~:text=Average%20This%20is%20the%20arithmetic,by%206%2C%20which%20is%205.",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -806,7 +840,7 @@
|
||||
"description": "The concept of Range refers to the spread of a dataset, primarily in the realm of statistics and data analysis. This measure is crucial for a data analyst as it provides an understanding of the variability amongst the numbers within a dataset. Specifically in a role such as Data Analyst, understanding the range and dispersion aids in making more precise analyses and predictions. Understanding the dispersion within a range can highlight anomalies, identify standard norms, and form the foundation for statistical conclusions like the standard deviation, variance, and interquartile range. It allows for the comprehension of the reliability and stability of particular datasets, which can help guide strategic decisions in many industries. Therefore, range is a key concept that every data analyst must master.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "How to find the range of a data set",
|
||||
"title": "How to Find the Range of a Data Set",
|
||||
"url": "https://www.scribbr.co.uk/stats/range-statistics/",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -817,12 +851,12 @@
|
||||
"description": "Data analysts heavily rely on statistical concepts to analyze and interpret data, and one such fundamental concept is variance. Variance, an essential measure of dispersion, quantifies the spread of data, providing insight into the level of variability within the dataset. Understanding variance is crucial for data analysts as the reliability of many statistical models depends on the assumption of constant variance across observations. In other words, it helps analysts determine how much data points diverge from the expected value or mean, which can be pivotal in identifying outliers, understanding data distribution, and driving decision-making processes. However, variance can't be interpreted in the original units of measurement due to its squared nature, which is why it is often used in conjunction with its square root, the standard deviation.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is variance?",
|
||||
"title": "What is Variance?",
|
||||
"url": "https://www.investopedia.com/terms/v/variance.asp",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "How to calculate variance",
|
||||
"title": "How to Calculate Variance",
|
||||
"url": "https://www.scribbr.co.uk/stats/variance-meaning/",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -892,7 +926,7 @@
|
||||
"description": "Tableau is a powerful data visualization tool utilized extensively by data analysts worldwide. Its primary role is to transform raw, unprocessed data into an understandable format without any technical skills or coding. Data analysts use Tableau to create data visualizations, reports, and dashboards that help businesses make more informed, data-driven decisions. They also use it to perform tasks like trend analysis, pattern identification, and forecasts, all within a user-friendly interface. Moreover, Tableau's data visualization capabilities make it easier for stakeholders to understand complex data and act on insights quickly.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Tableau Website",
|
||||
"title": "Tableau",
|
||||
"url": "https://www.tableau.com/en-gb",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -908,7 +942,7 @@
|
||||
"description": "PowerBI, an interactive data visualization and business analytics tool developed by Microsoft, plays a crucial role in the field of a data analyst's work. It helps data analysts to convert raw data into meaningful insights through it's easy-to-use dashboards and reports function. This tool provides a unified view of business data, allowing analysts to track and visualize key performance metrics and make better-informed business decisions. With PowerBI, data analysts also have the ability to manipulate and produce visualizations of large data sets that can be shared across an organization, making complex statistical information more digestible.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Power BI Website",
|
||||
"title": "Power BI",
|
||||
"url": "https://www.microsoft.com/en-us/power-platform/products/power-bi",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -924,7 +958,7 @@
|
||||
"description": "For a Data Analyst, understanding data and being able to represent it in a visually insightful form is a crucial part of effective decision-making in any organization. Matplotlib, a plotting library for the Python programming language, is an extremely useful tool for this purpose. It presents a versatile framework for generating line plots, scatter plots, histogram, bar charts and much more in a very straightforward manner. This library also allows for comprehensive customizations, offering a high level of control over the look and feel of the graphics it produces, which ultimately enhances the quality of data interpretation and communication.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Matplotlib Website",
|
||||
"title": "Matplotlib",
|
||||
"url": "https://matplotlib.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -940,7 +974,7 @@
|
||||
"description": "Seaborn is a robust, comprehensive Python library focused on the creation of informative and attractive statistical graphics. As a data analyst, seaborn plays an essential role in elaborating complex visual stories with the data. It aids in understanding the data by providing an interface for drawing attractive and informative statistical graphics. Seaborn is built on top of Python's core visualization library Matplotlib, and is integrated with data structures from Pandas. This makes seaborn an integral tool for data visualization in the data analyst's toolkit, making the exploration and understanding of data easier and more intuitive.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Seaborn Website",
|
||||
"title": "Seaborn",
|
||||
"url": "https://seaborn.pydata.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -972,12 +1006,12 @@
|
||||
"description": "As a vital tool in the data analyst's arsenal, bar charts are essential for analyzing and interpreting complex data. Bar charts, otherwise known as bar graphs, are frequently used graphical displays for dealing with categorical data groups or discrete variables. With their stark visual contrast and definitive measurements, they provide a simple yet effective means of identifying trends, understanding data distribution, and making data-driven decisions. By analyzing the lengths or heights of different bars, data analysts can effectively compare categories or variables against each other and derive meaningful insights effectively. Simplicity, readability, and easy interpretation are key features that make bar charts a favorite in the world of data analytics.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "A complete guide to bar charts",
|
||||
"title": "A Complete Guide to Bar Charts",
|
||||
"url": "https://www.atlassian.com/data/charts/bar-chart-complete-guide",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is a bar chart?",
|
||||
"title": "What is a Bar Chart?",
|
||||
"url": "https://www.youtube.com/watch?v=WTVdncVCvKo",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -1004,7 +1038,7 @@
|
||||
"description": "A scatter plot, a crucial aspect of data visualization, is a mathematical diagram using Cartesian coordinates to represent values from two different variables. As a data analyst, understanding and interpreting scatter plots can be instrumental in identifying correlations and trends within a dataset, drawing meaningful insights, and showcasing these findings in a clear, visual manner. In addition, scatter plots are paramount in predictive analytics as they reveal patterns which can be used to predict future occurrences.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Mastering scatter plots",
|
||||
"title": "Mastering Scatter Plots",
|
||||
"url": "https://www.atlassian.com/data/charts/what-is-a-scatter-plot",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -1052,7 +1086,7 @@
|
||||
"description": "A stacked chart is an essential tool for a data analyst in the field of data visualization. This type of chart presents quantitative data in a visually appealing manner and allows users to easily compare different categories while still being able to compare the total sizes. These charts are highly effective when trying to measure part-to-whole relationships, displaying accumulated totals over time or when presenting data with multiple variables. Data analysts often use stacked charts to detect patterns, trends and anomalies which can aid in strategic decision making.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is a stacked chart?",
|
||||
"title": "What is a Stacked Chart?",
|
||||
"url": "https://www.spotfire.com/glossary/what-is-a-stacked-chart",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -1068,12 +1102,12 @@
|
||||
"description": "Heatmaps are a crucial component of data visualization that Data Analysts regularly employ in their analyses. As one of many possible graphical representations of data, heatmaps show the correlation or scale of variation between two or more variables in a dataset, making them extremely useful for pattern recognition and outlier detection. Individual values within a matrix are represented in a heatmap as colors, with differing intensities indicating the degree or strength of an occurrence. In short, a Data Analyst would use a heatmap to decode complex multivariate data and turn it into an easily understandable visual that aids in decision making.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "A complete guide to heatmaps",
|
||||
"title": "A Complete Guide to Heatmaps",
|
||||
"url": "https://www.hotjar.com/heatmaps/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is a heatmap?",
|
||||
"title": "What is a Heatmap?",
|
||||
"url": "https://www.atlassian.com/data/charts/heatmap-complete-guide",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -1084,12 +1118,12 @@
|
||||
"description": "As a data analyst, understanding and efficiently using various forms of data visualization is crucial. Among these, Pie Charts represent a significant tool. Essentially, pie charts are circular statistical graphics divided into slices to illustrate numerical proportions. Each slice of the pie corresponds to a particular category. The pie chart's beauty lies in its simplicity and visual appeal, making it an effective way to convey relative proportions or percentages at a glance. For a data analyst, it's particularly useful when you want to show a simple distribution of categorical data. Like any tool, though, it's important to use pie charts wisely—ideally, when your data set has fewer than seven categories, and the proportions between categories are distinct.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "A complete guide to pie charts",
|
||||
"title": "A Complete Guide to Pie Charts",
|
||||
"url": "https://www.atlassian.com/data/charts/pie-chart-complete-guide",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is a a pie chart",
|
||||
"title": "What is a Pie Chart",
|
||||
"url": "https://www.youtube.com/watch?v=GjJdZaQrItg",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -1097,13 +1131,30 @@
|
||||
},
|
||||
"2g19zjEASJw2ve57hxpr0": {
|
||||
"title": "Data Visualisation",
|
||||
"description": "Data Visualization is a fundamental fragment of the responsibilities of a data analyst. It involves the presentation of data in a graphical or pictorial format which allows decision-makers to see analytics visually. This practice can help them comprehend difficult concepts or establish new patterns. With interactive visualization, data analysts can take the data analysis process to a whole new level — drill down into charts and graphs for more detail, and interactively changing what data is presented or how it’s processed. Thereby it forms a crucial link in the chain of converting raw data to actionable insights which is one of the primary roles of a Data Analyst.",
|
||||
"links": []
|
||||
"description": "Data Visualization is a fundamental fragment of the responsibilities of a data analyst. It involves the presentation of data in a graphical or pictorial format which allows decision-makers to see analytics visually. This practice can help them comprehend difficult concepts or establish new patterns. With interactive visualization, data analysts can take the data analysis process to a whole new level — drill down into charts and graphs for more detail, and interactively changing what data is presented or how it’s processed. Thereby it forms a crucial link in the chain of converting raw data to actionable insights which is one of the primary roles of a Data Analyst.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is Data Visualization?",
|
||||
"url": "https://www.ibm.com/think/topics/data-visualization",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TeewVruErSsD4VLXcaDxp": {
|
||||
"title": "Statistical Analysis",
|
||||
"description": "Statistical analysis is a core component of a data analyst's toolkit. As professionals dealing with vast amount of structured and unstructured data, data analysts often turn to statistical methods to extract insights and make informed decisions. The role of statistical analysis in data analytics involves gathering, reviewing, and interpreting data for various applications, enabling businesses to understand their performance, trends, and growth potential. Data analysts use a range of statistical techniques from modeling, machine learning, and data mining, to convey vital information that supports strategic company actions.\n\nLearn more from the following resources:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "Understanding Statistical Analysis",
|
||||
"url": "https://www.simplilearn.com/what-is-statistical-analysis-article",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Statistical Analysis",
|
||||
"url": "https://www.youtube.com/watch?v=XjMBZE1DuBY",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Xygwu0m5TeYT6S_8FKKXh": {
|
||||
"title": "Hypothesis Testing",
|
||||
@@ -1155,7 +1206,7 @@
|
||||
},
|
||||
"mCUW07rx74_dUNi7OGVlj": {
|
||||
"title": "Visualizing Distributions",
|
||||
"description": "Visualising Distributions, from a data analyst's perspective, plays a key role in understanding the overall distribution and identifying patterns within data. It aids in summarising, structuring, and plotting structured data graphically to provide essential insights. This includes using different chart types like bar graphs, histograms, and scatter plots for interval data, and pie or bar graphs for categorical data. Ultimately, the aim is to provide a straightforward and effective manner to comprehend the data's characteristics and underlying structure. A data analyst uses these visualisation techniques to make initial conclusions, detect anomalies, and decide on further analysis paths.\n\nLearn more from the following resources:",
|
||||
"description": "Visualising Distributions, from a data analyst's perspective, plays a key role in understanding the overall distribution and identifying patterns within data. It aids in summarizing, structuring, and plotting structured data graphically to provide essential insights. This includes using different chart types like bar graphs, histograms, and scatter plots for interval data, and pie or bar graphs for categorical data. Ultimately, the aim is to provide a straightforward and effective manner to comprehend the data's characteristics and underlying structure. A data analyst uses these visualisation techniques to make initial conclusions, detect anomalies, and decide on further analysis paths.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Data Visualizations that Capture Distributions",
|
||||
@@ -1195,12 +1246,12 @@
|
||||
"description": "Unsupervised learning, as a fundamental aspect of Machine Learning, holds great implications in the realm of data analytics. It is an approach where a model learns to identify patterns and relationships within a dataset that isn't labelled or classified. It is especially useful for a Data Analyst as it can assist in recognizing unforeseen trends, providing new insights or preparing data for other machine learning tasks. This ability to infer without direct supervision allows a vast potential for latent structure discovery and new knowledge derivation from raw data.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is unsupervised learning?",
|
||||
"title": "What is Unsupervised Learning?",
|
||||
"url": "https://cloud.google.com/discover/what-is-unsupervised-learning",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Introduction to unsupervised learning",
|
||||
"title": "Introduction to Unsupervised Learning",
|
||||
"url": "https://www.datacamp.com/blog/introduction-to-unsupervised-learning",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -1211,7 +1262,7 @@
|
||||
"description": "Supervised machine learning forms an integral part of the toolset for a Data Analyst. With a direct focus on building predictive models from labeled datasets, it involves training an algorithm based on these known inputs and outputs, helping Data Analysts establish correlations and make reliable predictions. Fortifying a Data Analyst's role, supervised machine learning enables the accurate interpretation of complex data, enhancing decision-making processes.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is supervised learning?",
|
||||
"title": "What is Supervised Learning?",
|
||||
"url": "https://cloud.google.com/discover/what-is-supervised-learning",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -1275,12 +1326,12 @@
|
||||
"description": "As a data analyst, it's crucial to understand various model evaluation techniques. These techniques involve different methods to measure the performance or accuracy of machine learning models. For instance, using confusion matrix, precision, recall, F1 score, ROC curves or Root Mean Squared Error (RMSE) among others. Knowing how to apply these techniques effectively not only helps in selecting the best model for a specific problem but also guides in tuning the performance of the models for optimal results. Understanding these model evaluation techniques also allows data analysts to interpret evaluation results and determine the effectiveness and applicability of a model.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is model evaluation",
|
||||
"title": "What is Model Evaluation",
|
||||
"url": "https://domino.ai/data-science-dictionary/model-evaluation",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Model evaluation metrics",
|
||||
"title": "Model Evaluation Metrics",
|
||||
"url": "https://www.markovml.com/blog/model-evaluation-metrics",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -1288,8 +1339,14 @@
|
||||
},
|
||||
"_aUQZWUhFRvNu0MZ8CPit": {
|
||||
"title": "Big Data Technologies",
|
||||
"description": "In the modern digitized world, Big Data refers to extremely large datasets that are challenging to manage and analyze using traditional data processing applications. These datasets often come from numerous different sources and are not only voluminous but also diverse in nature, including structured and unstructured data. The role of a data analyst in the context of big data is crucial. Data analysts are responsible for inspecting, cleaning, transforming, and modeling big data to discover useful information, conclude and support decision-making. They leverage their analytical skills and various big data tools and technologies to extract insights that can benefit the organization and drive strategic business initiatives.",
|
||||
"links": []
|
||||
"description": "In the modern digitized world, Big Data refers to extremely large datasets that are challenging to manage and analyze using traditional data processing applications. These datasets often come from numerous different sources and are not only voluminous but also diverse in nature, including structured and unstructured data. The role of a data analyst in the context of big data is crucial. Data analysts are responsible for inspecting, cleaning, transforming, and modeling big data to discover useful information, conclude and support decision-making. They leverage their analytical skills and various big data tools and technologies to extract insights that can benefit the organization and drive strategic business initiatives.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Big Data Analytics",
|
||||
"url": "https://www.ibm.com/think/topics/big-data-analytics",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"m1IfG2sEedUxMXrv_B8GW": {
|
||||
"title": "Big Data Concepts",
|
||||
@@ -1360,7 +1417,7 @@
|
||||
"description": "Hadoop is a critical element in the realm of data processing frameworks, offering an effective solution for storing, managing, and analyzing massive amounts of data. Unraveling meaningful insights from a large deluge of data is a challenging pursuit faced by many data analysts. Regular data processing tools fail to handle large-scale data, paving the way for advanced frameworks like Hadoop. This open-source platform by Apache Software Foundation excels at storing and processing vast data across clusters of computers. Notably, Hadoop comprises two key modules - the Hadoop Distributed File System (HDFS) for storage and MapReduce for processing. Hadoop’s ability to handle both structured and unstructured data further broadens its capacity. For any data analyst, a thorough understanding of Hadoop can unlock powerful ways to manage data effectively and construct meaningful analytics.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Apache Hadoop Website",
|
||||
"title": "Apache Hadoop",
|
||||
"url": "https://hadoop.apache.org/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -1381,7 +1438,7 @@
|
||||
"type": "opensource"
|
||||
},
|
||||
{
|
||||
"title": "Apache Spark Website",
|
||||
"title": "Apache Spark",
|
||||
"url": "https://spark.apache.org/",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -1421,15 +1478,21 @@
|
||||
},
|
||||
"SiYUdtYMDImRPmV2_XPkH": {
|
||||
"title": "Deep Learning (Optional)",
|
||||
"description": "Deep learning, a subset of machine learning technique, is increasingly becoming a critical tool for data analysts. Deep learning algorithms utilize multiple layers of neural networks to understand and interpret intricate structures in large data, a skill that is integral to the daily functions of a data analyst. With the ability to learn from unstructured or unlabeled data, deep learning opens a whole new range of possibilities for data analysts in terms of data processing, prediction, and categorization. It has applications in a variety of industries from healthcare to finance to e-commerce and beyond. A deeper understanding of deep learning methodologies can augment a data analyst's capability to evaluate and interpret complex datasets and provide valuable insights for decision making.",
|
||||
"links": []
|
||||
"description": "Deep learning, a subset of machine learning technique, is increasingly becoming a critical tool for data analysts. Deep learning algorithms utilize multiple layers of neural networks to understand and interpret intricate structures in large data, a skill that is integral to the daily functions of a data analyst. With the ability to learn from unstructured or unlabeled data, deep learning opens a whole new range of possibilities for data analysts in terms of data processing, prediction, and categorization. It has applications in a variety of industries from healthcare to finance to e-commerce and beyond. A deeper understanding of deep learning methodologies can augment a data analyst's capability to evaluate and interpret complex datasets and provide valuable insights for decision making.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Deep Learning for Data Analysis",
|
||||
"url": "https://www.ibm.com/think/topics/deep-learning",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gGHsKcS92StK5FolzmVvm": {
|
||||
"title": "Neural Networks",
|
||||
"description": "Neural Networks play a pivotal role in the landscape of deep learning, offering a plethora of benefits and applications for data analysts. They are computational models that emulate the way human brain processes information, enabling machines to make intelligent decisions. As a data analyst, understanding and utilizing neural networks can greatly enhance decision-making process as it allows to quickly and effectively analyze large datasets, recognize patterns, and forecast future trends. In deep learning, these networks are used for creating advanced models that can tackle complex tasks such as image recognition, natural language processing, and speech recognition, to name but a few. Therefore, an in-depth knowledge of neural networks is a significant asset for any aspiring or professional data analyst.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is a neural network?",
|
||||
"title": "What is a Neural Network?",
|
||||
"url": "https://aws.amazon.com/what-is/neural-network/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -1461,12 +1524,12 @@
|
||||
"description": "Recurrent Neural Networks(RNNs) are a type of Artificial Neural Networks(ANNs) which introduces us to the realm of Deep Learning, an aspect that has been significantly contributing to the evolution of Data Analysis. RNNs are specifically designed to recognize patterns in sequences of data, such as text, genomes, handwriting, or the spoken word. This inherent feature of RNNs makes them extremely useful and versatile for a data analyst.\n\nA data analyst leveraging RNNs can effectively charter the intrinsic complexity of data sequences, classify them, and make accurate predictions. With the fundamental understanding of deep learning, data analysts can unlock the full potential of RNNs in delivering insightful data analysis that goes beyond traditional statistical methods. Modern research and applications of RNNs extend to multiple domains including natural language processing, speech recognition, and even in the financial sphere for stock price prediction making this a key tool in a data analyst’s arsenal.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is a recurrent neural network (RNN)?",
|
||||
"title": "What is a Recurrent Neural Network (RNN)?",
|
||||
"url": "https://www.ibm.com/topics/recurrent-neural-networks",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Recurrent Neural Networks cheatsheet",
|
||||
"title": "Recurrent Neural Networks Cheat-sheet",
|
||||
"url": "https://stanford.edu/~shervine/teaching/cs-230/cheatsheet-recurrent-neural-networks",
|
||||
"type": "article"
|
||||
}
|
||||
@@ -1477,10 +1540,15 @@
|
||||
"description": "TensorFlow, developed by Google Brain Team, has become a crucial tool in the realm of data analytics, particularly within the field of deep learning. It's an open-source platform for machine learning, offering a comprehensive and flexible ecosystem of tools, libraries, and community resources. As a data analyst, understanding and implementing TensorFlow for deep learning models allows us to identify complex patterns and make insightful predictions which standard analysis could miss. It's in-demand skill that enhances our ability to generate accurate insights from colossal and complicated structured or unstructured data sets.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Tensorflow Website",
|
||||
"title": "Tensorflow",
|
||||
"url": "https://www.tensorflow.org/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Tensorflow Documentation",
|
||||
"url": "https://www.tensorflow.org/learn",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Tensorflow in 100 seconds",
|
||||
"url": "https://www.youtube.com/watch?v=i8NETqtGHms",
|
||||
@@ -1493,10 +1561,15 @@
|
||||
"description": "PyTorch, an open-source machine learning library, has gained considerable popularity among data analysts due to its simplicity and high performance in tasks such as natural language processing and artificial intelligence. Specifically, in the domain of deep learning, PyTorch stands out due to its dynamic computational graph, allowing for a highly intuitive and flexible platform for building complex models. For data analysts, mastering PyTorch can open up a broad range of opportunities for data model development, data processing, and integration of machine learning algorithms.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "PyTorch Website",
|
||||
"title": "PyTorch",
|
||||
"url": "https://pytorch.org/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "PyTorch Documentation",
|
||||
"url": "https://pytorch.org/docs/stable/index.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "PyTorch in 100 seconds",
|
||||
"url": "https://www.youtube.com/watch?v=ORMx45xqWkA",
|
||||
@@ -1509,7 +1582,7 @@
|
||||
"description": "Image Recognition has become a significant domain because of its diverse applications, including facial recognition, object detection, character recognition, and much more. As a Data Analyst, understanding Image Recognition under Deep Learning becomes crucial. The data analyst's role in this context involves deciphering complex patterns and extracting valuable information from image data. This area of machine learning combines knowledge of data analysis, image processing, and deep neural networks to provide accurate results, contributing significantly to the progression of fields like autonomous vehicles, medical imaging, surveillance, among others. Therefore, proficiency in this field paves the way for proficient data analysis, leading to innovative solutions and improved decision-making.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is image recognition?",
|
||||
"title": "What is Image Recognition?",
|
||||
"url": "https://www.techtarget.com/searchenterpriseai/definition/image-recognition",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -1541,12 +1614,12 @@
|
||||
"description": "As a business enterprise expands, so does its data. For data analysts, the surge in information means they need efficient and scalable data storage solutions to manage vast volumes of structured and unstructured data, collectively referred to as Big Data. Big Data storage solutions are critical in preserving the integrity of data while also providing quick and easy access to the data when needed. These solutions use software and hardware components to securely store massive amounts of information across numerous servers, allowing data analysts to perform robust data extraction, data processing and complex data analyses. There are several options, from the traditional Relational Database Management Systems (RDBMS) to the more recent NoSQL databases, Hadoop ecosystems, and Cloud storage solutions, each offering unique capabilities and benefits to cater for different big data needs.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "SQL Roadmap",
|
||||
"title": "Visit Dedicated SQL Roadmap",
|
||||
"url": "https://roadmap.sh/sql",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "PostgreSQL Roadmap",
|
||||
"title": "Visit Dedicated PostgreSQL Roadmap",
|
||||
"url": "https://roadmap.sh/postgresql-dba",
|
||||
"type": "article"
|
||||
}
|
||||
|
||||
@@ -3026,14 +3026,14 @@
|
||||
"title": "Envoy",
|
||||
"description": "Originally created at Lyft, Envoy is a high-performance data plane designed for service mesh architectures. Lyft open sourced it and donated it to the CNCF, where it is now one of the CNCF’s graduated open source projects. Envoy is a self contained process that is designed to run alongside every application server. All of the Envoys form a transparent communication mesh in which each application sends and receives messages to and from localhost and is unaware of the network topology.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Envoy Website",
|
||||
"url": "https://www.envoyproxy.io/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "envoyproxy/envoy",
|
||||
"url": "https://github.com/envoyproxy/envoy",
|
||||
"type": "opensource"
|
||||
},
|
||||
{
|
||||
"title": "Envoy",
|
||||
"url": "https://www.envoyproxy.io/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -170,6 +170,11 @@
|
||||
"title": "HTML",
|
||||
"description": "HTML (Hypertext Markup Language) is the standard markup language used to create web pages and web applications. It provides a structure for content on the World Wide Web, using a system of elements and attributes to define the layout and content of a document. HTML elements are represented by tags, which browsers interpret to render the visual and auditory elements of a web page. The language has evolved through several versions, with HTML5 being the current standard, introducing semantic elements, improved multimedia support, and enhanced form controls. HTML works in conjunction with CSS for styling and JavaScript for interactivity, forming the foundation of modern web development.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Responsive Web Design Certification - Co-Learn HTML & CSS with guided projects",
|
||||
"url": "https://www.freecodecamp.org/learn/2022/responsive-web-design/",
|
||||
"type": "course"
|
||||
},
|
||||
{
|
||||
"title": "W3Schools: Learn HTML",
|
||||
"url": "https://www.w3schools.com/html/html_intro.asp",
|
||||
@@ -321,6 +326,11 @@
|
||||
"url": "https://www.youtube.com/watch?v=G3e-cpL7ofc",
|
||||
"type": "course"
|
||||
},
|
||||
{
|
||||
"title": "Responsive Web Design Certification - Co-Learn HTML & CSS with guided projects",
|
||||
"url": "https://www.freecodecamp.org/learn/2022/responsive-web-design/",
|
||||
"type": "course"
|
||||
},
|
||||
{
|
||||
"title": "Web.dev by Google — Learn CSS",
|
||||
"url": "https://web.dev/learn/css/",
|
||||
|
||||
@@ -599,16 +599,16 @@
|
||||
},
|
||||
"vWLKYK2KUzV1fO-vQunzW": {
|
||||
"title": "EPA",
|
||||
"description": "The **EPA**, also known as the _Environmental Protection Agency_, is not typically related to game development or the concept of intersection within this context. However, in game development, EPA might refer to an 'Event-driven Process chain Architecture' or some other game-specific acronym. In this domain, different terminologies and acronyms are often used to express complex architectures, designs, or functionalities. If you have encountered EPA in a game development context, it might be best to refer to the specific documentation or guide where it was described for a better understanding. Understanding the context is key to untangle the meaning of such abbreviations.\n\nVisit the following resources to learn more:",
|
||||
"description": "The **EPA** (Expanding Polytope Algorithm) is an iterative algorithm used for calculating the penetration depth between two shapes in collision detection. It is commonly used in physics engines and robotics. The algorithm takes the resulting simplex from a previously applied GJK algorithm, iteratively expanding the polytope towards the Minkowski Difference boundary until it finds the closest point to the origin. The vector from that point to the origin is the penetration vector and its magnitude is equal to the penetration depth between the two shapes.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Environmental Sustainability in Game Development",
|
||||
"url": "https://polydin.com/environmental-sustainability-in-game-development/",
|
||||
"title": "EPA: Collision response algorithm for 2D/3D - winter.dev",
|
||||
"url": "https://winter.dev/articles/epa-algorithm",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Gaming Sustainability - Microsoft Game Dev",
|
||||
"url": "https://learn.microsoft.com/en-us/gaming/sustainability/sustainability-overview",
|
||||
"title": "EPA (Expanding Polytope Algorithm) - dyn4j",
|
||||
"url": "https://dyn4j.org/2010/05/epa-expanding-polytope-algorithm/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1023,16 +1023,16 @@
|
||||
},
|
||||
"XteNExIZN3_g95_dPCopY": {
|
||||
"title": "Exitting / Exit Codes",
|
||||
"description": "`Exiting` is a way of terminating a Node.js process by using node.js process module.\n\nVisit the following resources to learn more:",
|
||||
"description": "Exiting is a way of terminating a Node.js process by using node.js process module.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Node.js Docs on exit",
|
||||
"url": "https://nodejs.org/docs/latest/api/process.html",
|
||||
"title": "Exit Documentation",
|
||||
"url": "https://nodejs.org/api/process.html#event-exit",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "How to Exit a Process in Node.js",
|
||||
"url": "https://www.knowledgehut.com/blog/web-development/node-js-process-exit",
|
||||
"url": "https://betterstack.com/community/questions/how-to-exit-in-node-js/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -319,8 +319,13 @@
|
||||
"description": "The Null Safe Operator is a handy feature in PHP which deals with an issue that often pops up when working with objects: trying to access properties or methods on an object that might be null. Instead of a fatal error, the PHP Null Safe Operator (indicated by ?->) allows null values to be returned safely, making your code more robust. Here's a quick example, consider $session?->user?->name. If $session or user is null, PHP will stop further execution and simply return null. This makes PHP more resilient when processing unpredictable data.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Null Safe Operator",
|
||||
"url": "https://www.php.net/manual/en/language.oop5.nullsafe.php",
|
||||
"title": "The Basics - Manual",
|
||||
"url": "https://www.php.net/manual/en/language.oop5.basic.php",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "PHP RFC: Nullsafe operator",
|
||||
"url": "https://wiki.php.net/rfc/nullsafe_operator",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
@@ -393,7 +398,7 @@
|
||||
},
|
||||
"RkNjYva8o_jXp9suz5YdG": {
|
||||
"title": "Named Arguments",
|
||||
"description": "Named arguments in PHP, introduced with PHP 8.0, allow you to specify the values of required parameters by their names, instead of their position in the function call, thus making your code more readable, reducing mistakes, and allowing for unimportant arguments to be skipped. Here's an array\\_fill() function using named arguments:\n\n <?php\n $a = array_fill(start_index: 0, num: 100, value: 50);\n \n\nIn this code snippet, the parameters are passed by their names ('start\\_index', 'num', 'value'), not by their order in the function definition.\n\nVisit the following resources to learn more:",
|
||||
"description": "Named arguments in PHP, introduced with PHP 8.0, allow you to specify the values of required parameters by their names, instead of their position in the function call, thus making your code more readable, reducing mistakes, and allowing for unimportant arguments to be skipped. Here's an array\\_fill() function using named arguments:\n\n <?php\n $a = array_fill(start_index: 0, count: 100, value: 50);\n \n\nIn this code snippet, the parameters are passed by their names ('start\\_index', 'count', 'value'), not by their order in the function definition.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Named Arguments",
|
||||
@@ -712,14 +717,8 @@
|
||||
},
|
||||
"J9yIXZTtwbFzH2u4dI1ep": {
|
||||
"title": "CSRF Protection",
|
||||
"description": "Cross-Site Request Forgery (CSRF) Protection in PHP is a method where a website can defend itself against unwanted actions performed on behalf of the users without their consent. It's a critical aspect of security as it safeguards users against potential harmful activities. Here's an example: if users are logged into a website and get tricked into clicking a deceitful link, CSRF attacks could be triggered. To protect your PHP applications from such attacks, you can generate a unique token for every session and include it as a hidden field for all form submissions. Afterwards, you need to verify this token on the server side before performing any action.\n\n <?php\n // Generate CSRF token\n if(empty($_SESSION['csrf'])) {\n $_SESSION['csrf'] = bin2hex(random_bytes(32));\n }\n \n // Verify CSRF token\n if(isset($_POST['csrf']) && $_POST['csrf'] === $_SESSION['csrf']) {\n // valid CSRF token, perform action\n }\n ?>\n \n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Security Guide",
|
||||
"url": "https://php.net/manual/en/security.csrf.php",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "Cross-Site Request Forgery (CSRF) Protection in PHP is a method where a website can defend itself against unwanted actions performed on behalf of the users without their consent. It's a critical aspect of security as it safeguards users against potential harmful activities. Here's an example: if users are logged into a website and get tricked into clicking a deceitful link, CSRF attacks could be triggered. To protect your PHP applications from such attacks, you can generate a unique token for every session and include it as a hidden field for all form submissions. Afterwards, you need to verify this token on the server side before performing any action.\n\n <?php\n // Generate CSRF token\n if(empty($_SESSION['csrf'])) {\n $_SESSION['csrf'] = bin2hex(random_bytes(32));\n }\n \n // Verify CSRF token\n if(isset($_POST['csrf']) && $_POST['csrf'] === $_SESSION['csrf']) {\n // valid CSRF token, perform action\n }\n ?>\n \n\nVisit the following resources to learn more:\n\n* \\[@article@PHP Tutorial CSRF\\] ([https://www.phptutorial.net/php-tutorial/php-csrf/](https://www.phptutorial.net/php-tutorial/php-csrf/))",
|
||||
"links": []
|
||||
},
|
||||
"JbWFfJiCRrXDhnuIx_lqx": {
|
||||
"title": "Password Hashing",
|
||||
@@ -1021,12 +1020,12 @@
|
||||
"description": "Symfony is a set of PHP components and a framework for web projects. It aims to speed up the creation and maintenance of web applications and replace the recurring coding tasks. Symfony uses Composer, a PHP dependency manager, to manage its components. Below is an example of creating a new Symfony project:\n\n composer create-project symfony/website-skeleton myproject\n \n\nThis will download and install a new Symfony project in the 'myproject' directory. Symfony's components are reusable PHP libraries that will help you complete tasks, like routing, templating, or even creating form handling.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Symphony",
|
||||
"title": "Symfony",
|
||||
"url": "https://symfony.com/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Symphony Documentation",
|
||||
"title": "Symfony Documentation",
|
||||
"url": "https://symfony.com/doc/current/index.html",
|
||||
"type": "article"
|
||||
}
|
||||
|
||||
@@ -583,7 +583,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "pg_ctlcluster",
|
||||
"url": "https://www.postgresql.org/docs/current/pgctlcluster.html",
|
||||
"url": "https://manpages.ubuntu.com/manpages/focal/man1/pg_ctlcluster.1.html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
@@ -1075,7 +1075,7 @@
|
||||
]
|
||||
},
|
||||
"S20aJB-VuSpXYyd0-0S8c": {
|
||||
"title": "Object Priviliges",
|
||||
"title": "Object Privileges",
|
||||
"description": "Object privileges in PostgreSQL are the permissions given to different user roles to access or modify database objects like tables, views, sequences, and functions. Ensuring proper object privileges is crucial for maintaining a secure and well-functioning database.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
@@ -1117,7 +1117,7 @@
|
||||
]
|
||||
},
|
||||
"t18XjeHP4uRyERdqhHpl5": {
|
||||
"title": "Default Priviliges",
|
||||
"title": "Default Privileges",
|
||||
"description": "PostgreSQL allows you to define object privileges for various types of database objects. These privileges determine if a user can access and manipulate objects like tables, views, sequences, or functions. In this section, we will focus on understanding default privileges in PostgreSQL.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
|
||||
@@ -399,8 +399,19 @@
|
||||
},
|
||||
"gS3ofDrqDRKbecIskIyGi": {
|
||||
"title": "Product Roadmap",
|
||||
"description": "The product roadmap is a strategic document that provides a detailed overview of the product's direction and vision. It outlines the product's plans, both tactical and strategic - including the specific steps necessary to achieve the company's goals and vision. As a Product Manager, you are expected to guide the creation of the product roadmap, communicating the product’s evolution to the team, stakeholders, and customers. This tool serves as an essential reference point helping to align all stakeholders with the key priorities and vision of the product, and acts as a guide for decisions around product development.",
|
||||
"links": []
|
||||
"description": "The product roadmap is a strategic document that provides a detailed overview of the product's direction and vision. It outlines the product's plans, both tactical and strategic - including the specific steps necessary to achieve the company's goals and vision. As a Product Manager, you are expected to guide the creation of the product roadmap, communicating the product’s evolution to the team, stakeholders, and customers. This tool serves as an essential reference point helping to align all stakeholders with the key priorities and vision of the product, and acts as a guide for decisions around product development.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is a Product Roadmap? - Product Plan",
|
||||
"url": "https://www.productplan.com/learn/what-is-a-product-roadmap/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is a Product Roadmap? - Vibhor Chandel",
|
||||
"url": "https://www.youtube.com/watch?v=BJR70jnpHog&ab_channel=VibhorChandel",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
"eiqV86PWizZPWsyqoBU5k": {
|
||||
"title": "Creating a Roadmap",
|
||||
|
||||
@@ -179,6 +179,21 @@
|
||||
"title": "Tuples Documentation",
|
||||
"url": "https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "When and How to Use Tuples",
|
||||
"url": "https://thenewstack.io/python-for-beginners-when-and-how-to-use-tuples/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Python's tuple Data Type: A Deep Dive With Examples",
|
||||
"url": "https://realpython.com/python-tuple/#getting-started-with-pythons-tuple-data-type",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "why are Tuples even a thing?",
|
||||
"url": "https://www.youtube.com/watch?v=fR_D_KIAYrE",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -205,7 +220,7 @@
|
||||
},
|
||||
"bc9CL_HMT-R6nXO1eR-gP": {
|
||||
"title": "Dictionaries",
|
||||
"description": "In Python, a dictionary is a built-in data type that allows you to store key-value pairs. Each key in the dictionary is unique, and each key is associated with a value. Dictionaries are unordered collections, meaning the order of items is not guaranteed.\n\nLearn more from the following resources:",
|
||||
"description": "In Python, a dictionary is a built-in data type that allows you to store key-value pairs. Each key in the dictionary is unique, and each key is associated with a value. Starting from Python 3.7, dictionaries maintain the order of items as they were added.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Dictionaries in Python",
|
||||
@@ -216,6 +231,11 @@
|
||||
"title": "W3 Schools - Dictionaries",
|
||||
"url": "https://www.w3schools.com/python/python_dictionaries.asp",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Dictionaries in Python",
|
||||
"url": "https://realpython.com/python-dicts/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -533,6 +553,11 @@
|
||||
"title": "Modules in Python",
|
||||
"url": "https://www.programiz.com/python-programming/modules",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Python Modules and Packages",
|
||||
"url": "https://realpython.com/python-modules-packages/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -596,6 +621,11 @@
|
||||
"title": "Python Iterators",
|
||||
"url": "https://www.programiz.com/python-programming/iterator",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Iterators and Iterables in Python",
|
||||
"url": "https://realpython.com/python-iterators-iterables/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -534,7 +534,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "RPOP Documentation",
|
||||
"url": "https://redis.io/docs/latest/commands/rpush/",
|
||||
"url": "https://redis.io/docs/latest/commands/rpop/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
@@ -954,8 +954,19 @@
|
||||
},
|
||||
"jrgaoDnt_RxTu79hk4hCD": {
|
||||
"title": "Atomicity in Redis",
|
||||
"description": "Atomicity in Redis refers to the property that ensures a set of operations is executed as a single, indivisible unit. This means that either all the operations are executed successfully or none of them are. Atomicity is crucial in Redis to maintain consistency, especially when multiple operations need to be performed together.\n\nLearn more from the following resources:\n\n* [@official@Atomicity with Lua](https://redis.io/learn/develop/java/spring/rate-limiting/fixed-window/reactive-lua) -[@article@Atomicity in Redis operations](https://lucaspin.medium.com/atomicity-in-redis-operations-a1d7bc9f4a90)",
|
||||
"links": []
|
||||
"description": "Atomicity in Redis refers to the property that ensures a set of operations is executed as a single, indivisible unit. This means that either all the operations are executed successfully or none of them are. Atomicity is crucial in Redis to maintain consistency, especially when multiple operations need to be performed together.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Atomicity with Lua",
|
||||
"url": "https://redis.io/learn/develop/java/spring/rate-limiting/fixed-window/reactive-lua",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Atomicity in Redis operations",
|
||||
"url": "https://lucaspin.medium.com/atomicity-in-redis-operations-a1d7bc9f4a90",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"LHlwjN3WHYUBUafzzwsWQ": {
|
||||
"title": "Pipelining",
|
||||
|
||||
@@ -496,7 +496,7 @@
|
||||
},
|
||||
"_U0VoTkqM1d6NR13p5azS": {
|
||||
"title": "Patterns & Design Principles",
|
||||
"description": "",
|
||||
"description": "In the realm of software architecture, patterns and design principles are foundational tools that enable architects to create robust, scalable, and maintainable systems. They offer proven solutions to common problems and guide decision-making throughout the software development lifecycle. Understanding these concepts is essential for anyone following a software architect roadmap, as they bridge the gap between high-level architecture and practical implementation.",
|
||||
"links": []
|
||||
},
|
||||
"AMDLJ_Bup-AY1chl_taV3": {
|
||||
|
||||
@@ -557,11 +557,11 @@
|
||||
},
|
||||
"fY8zgbB13wxZ1CFtMSdZZ": {
|
||||
"title": "SQL Tuning",
|
||||
"description": "SQL tuning is a broad topic and many books have been written as reference. It's important to benchmark and profile to simulate and uncover bottlenecks.\n\n* Benchmark - Simulate high-load situations with tools such as ab.\n* Profile - Enable tools such as the slow query log to help track performance issues.\n\nBenchmarking and profiling might point you to the following optimizations.\n\nTo learn more, visit the following links:",
|
||||
"description": "SQL tuning is the attempt to diagnose and repair SQL statements that fail to meet a performance standard. It is a broad topic and many books have been written as reference. It's important to benchmark and profile to simulate and uncover bottlenecks.\n\n* Benchmark - Simulate high-load situations with tools such as ab.\n* Profile - Enable tools such as the slow query log to help track performance issues.\n\nBenchmarking and profiling might point you to the following optimizations.\n\nTo learn more, visit the following links:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Optimizing MySQL Queries",
|
||||
"url": "https://aiddroid.com/10-tips-optimizing-mysql-queries-dont-suck/",
|
||||
"title": "Introduction to SQL Tuning - Oracle",
|
||||
"url": "https://docs.oracle.com/en/database/oracle/oracle-database/23/tgsql/introduction-to-sql-tuning.html#GUID-B653E5F3-F078-4BBC-9516-B892960046A2",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
@@ -1326,16 +1326,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"LncTxPg-wx8loy55r5NmV": {
|
||||
"queu-based-load-leveling@LncTxPg-wx8loy55r5NmV.md": {
|
||||
"title": "Queu-based Load Leveling",
|
||||
"description": "Use a queue that acts as a buffer between a task and a service it invokes in order to smooth intermittent heavy loads that can cause the service to fail or the task to time out. This can help to minimize the impact of peaks in demand on availability and responsiveness for both the task and the service.\n\nLearn more from the following links:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Queue-Based Load Leveling pattern",
|
||||
"url": "https://learn.microsoft.com/en-us/azure/architecture/patterns/queue-based-load-leveling",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"2ryzJhRDTo98gGgn9mAxR": {
|
||||
"title": "Publisher/Subscriber",
|
||||
@@ -1665,10 +1659,21 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"backends-for-frontend@n4It-lr7FFtSY83DcGydX.md": {
|
||||
"n4It-lr7FFtSY83DcGydX": {
|
||||
"title": "Backends for Frontend",
|
||||
"description": "",
|
||||
"links": []
|
||||
"description": "Create separate backend services to be consumed by specific frontend applications or interfaces. This pattern is useful when you want to avoid customizing a single backend for multiple interfaces. This pattern was first described by Sam Newman.\n\nTo learn more, visit the following links:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Backends for Frontends pattern",
|
||||
"url": "https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Explore top posts about Frontend Development",
|
||||
"url": "https://app.daily.dev/tags/frontend?ref=roadmapsh",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"4hi7LvjLcv8eR6m-uk8XQ": {
|
||||
"title": "Anti-Corruption Layer",
|
||||
|
||||
@@ -387,8 +387,14 @@
|
||||
},
|
||||
"90_M5qABC1vZ1nsXVyqFJ": {
|
||||
"title": "Good Layout Rules",
|
||||
"description": "In the world of UX design, a good layout is crucial to ensure your prototype is intuitive and user-friendly. By following these good layout rules, you can ensure your designs are efficient, attractive, and easy to navigate for users.\n\nConsistency\n-----------\n\nBeing consistent with your design is vital in creating an easy-to-navigate interface. Utilize the same color schemes, typography, and other design elements consistently throughout your prototype to make it visually cohesive and user-friendly.\n\nAlignment and Spacing\n---------------------\n\nEnsure all the elements on your prototype are aligned and spaced properly. This helps create a well-structured and clean look, while also making it easy for users to navigate and understand your design.\n\nVisual Hierarchy\n----------------\n\nEstablish clear visual hierarchy by using size, color, contrast, and white space effectively. This helps users identify important elements on the screen quickly and understand the flow of your design easily.\n\nGrouping of Elements\n--------------------\n\nGroup related elements together, such as navigation menus or form input fields. This helps users recognize the purpose and function of each section more quickly and intuitively.\n\nBalance and Proportion\n----------------------\n\nCreate a balanced and proportional look by distributing elements on the screen evenly. This can be achieved through the use of grids or other layout techniques that help maintain a sense of harmony and order in your design.\n\nAccessibility\n-------------\n\nEnsure your design is accessible to all users by considering factors such as text size, contrast, and color combinations. Aim to create an inclusive prototype that caters to people of different abilities and preferences.\n\nResponsiveness and Flexibility\n------------------------------\n\nMake sure your prototype can adapt to different screen sizes and devices, ensuring a seamless user experience across various platforms. This is particularly important when designing for web and mobile applications.\n\nIterating and Testing\n---------------------\n\nAs you develop your design, continually test and iterate on your layout based on user feedback and data. This process will help refine your design and ensure it meets the needs and expectations of your users.\n\nBy incorporating these good layout rules into your prototyping process, you'll be well on your way to creating a user-friendly and effective design that meets the goals and objectives of your project.",
|
||||
"links": []
|
||||
"description": "In the world of UX design, a good layout is crucial to ensure your prototype is intuitive and user-friendly. By following these good layout rules, you can ensure your designs are efficient, attractive, and easy to navigate for users.\n\nConsistency\n-----------\n\nBeing consistent with your design is vital in creating an easy-to-navigate interface. Utilize the same color schemes, typography, and other design elements consistently throughout your prototype to make it visually cohesive and user-friendly.\n\nAlignment and Spacing\n---------------------\n\nEnsure all the elements on your prototype are aligned and spaced properly. This helps create a well-structured and clean look, while also making it easy for users to navigate and understand your design.\n\nVisual Hierarchy\n----------------\n\nEstablish clear visual hierarchy by using size, color, contrast, and white space effectively. This helps users identify important elements on the screen quickly and understand the flow of your design easily.\n\nGrouping of Elements\n--------------------\n\nGroup related elements together, such as navigation menus or form input fields. This helps users recognize the purpose and function of each section more quickly and intuitively.\n\nBalance and Proportion\n----------------------\n\nCreate a balanced and proportional look by distributing elements on the screen evenly. This can be achieved through the use of grids or other layout techniques that help maintain a sense of harmony and order in your design.\n\nAccessibility\n-------------\n\nEnsure your design is accessible to all users by considering factors such as text size, contrast, and color combinations. Aim to create an inclusive prototype that caters to people of different abilities and preferences.\n\nResponsiveness and Flexibility\n------------------------------\n\nMake sure your prototype can adapt to different screen sizes and devices, ensuring a seamless user experience across various platforms. This is particularly important when designing for web and mobile applications.\n\nIterating and Testing\n---------------------\n\nAs you develop your design, continually test and iterate on your layout based on user feedback and data. This process will help refine your design and ensure it meets the needs and expectations of your users.\n\nBy incorporating these good layout rules into your prototyping process, you'll be well on your way to creating a user-friendly and effective design that meets the goals and objectives of your project.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "User Interface Design Guidelines: 10 Rules of Thumb",
|
||||
"url": "https://www.interaction-design.org/literature/article/user-interface-design-guidelines-10-rules-of-thumb",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"t46s6Piyd8MoJYzdDTsjr": {
|
||||
"title": "Figma",
|
||||
|
||||
@@ -234,6 +234,11 @@
|
||||
"title": "Global Properties",
|
||||
"description": "Global properties allows you to add properties or methods that can be accessed throughout your application. This is particularly useful for sharing functionality or data across components without the need to pass props explicitly.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Application API - globalProperties",
|
||||
"url": "https://vuejs.org/api/application.html#app-config-globalproperties",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Vue.js Global Properties",
|
||||
"url": "https://blog.logrocket.com/vue-js-globalproperties/",
|
||||
@@ -544,7 +549,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "Modifiers",
|
||||
"url": "https://v2.vuejs.org/v2/guide/components-custom-events.html",
|
||||
"url": "https://vuejs.org/guide/essentials/forms.html#modifiers",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
BIN
public/roadmaps/cloudflare.png
Normal file
BIN
public/roadmaps/cloudflare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 506 KiB |
@@ -43,6 +43,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [AI and Data Scientist Roadmap](https://roadmap.sh/ai-data-scientist)
|
||||
- [AI Engineer Roadmap](https://roadmap.sh/ai-engineer)
|
||||
- [AWS Roadmap](https://roadmap.sh/aws)
|
||||
- [Cloudflare Roadmap](https://roadmap.sh/cloudflare)
|
||||
- [Linux Roadmap](https://roadmap.sh/linux)
|
||||
- [Terraform Roadmap](https://roadmap.sh/terraform)
|
||||
- [Data Analyst Roadmap](https://roadmap.sh/data-analyst)
|
||||
|
||||
@@ -86,9 +86,6 @@ export function AdvertiseForm() {
|
||||
|
||||
pageProgressMessage.set('Please wait');
|
||||
|
||||
// Placeholder function to send data
|
||||
console.log('Form data:', formData);
|
||||
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-advertise`,
|
||||
formData,
|
||||
|
||||
@@ -6,6 +6,7 @@ declare global {
|
||||
category: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
callback?: () => void;
|
||||
}) => void;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +18,7 @@ declare global {
|
||||
* @returns void
|
||||
*/
|
||||
window.fireEvent = (props) => {
|
||||
const { action, category, label, value } = props;
|
||||
const { action, category, label, value, callback } = props;
|
||||
if (!window.gtag) {
|
||||
console.warn('Missing GTAG - Analytics disabled');
|
||||
return;
|
||||
@@ -25,11 +26,16 @@ window.fireEvent = (props) => {
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('Analytics event fired', props);
|
||||
callback?.();
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value,
|
||||
...(callback ? { event_callback: callback } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
import { parse } from 'node-html-parser';
|
||||
import type { Attributes } from 'node-html-parser/dist/nodes/html';
|
||||
|
||||
export interface Props {
|
||||
icon: string;
|
||||
@@ -15,7 +14,6 @@ async function getSVG(name: string) {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
|
||||
if (!(filepath in files)) {
|
||||
throw new Error(`${filepath} not found`);
|
||||
}
|
||||
|
||||
143
src/components/AuthenticationFlow/CourseLoginPopup.tsx
Normal file
143
src/components/AuthenticationFlow/CourseLoginPopup.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Modal } from '../Modal';
|
||||
import { GitHubButton } from './GitHubButton';
|
||||
import { GoogleButton } from './GoogleButton';
|
||||
import { LinkedInButton } from './LinkedInButton';
|
||||
import { EmailLoginForm } from './EmailLoginForm';
|
||||
import { EmailSignupForm } from './EmailSignupForm';
|
||||
|
||||
type CourseLoginPopupProps = {
|
||||
onClose: () => void;
|
||||
checkoutAfterLogin?: boolean;
|
||||
};
|
||||
|
||||
export const CHECKOUT_AFTER_LOGIN_KEY = 'checkoutAfterLogin';
|
||||
|
||||
export function CourseLoginPopup(props: CourseLoginPopupProps) {
|
||||
const { onClose: parentOnClose, checkoutAfterLogin = true } = props;
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [isUsingEmail, setIsUsingEmail] = useState(false);
|
||||
|
||||
const [emailNature, setEmailNature] = useState<'login' | 'signup' | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function onClose() {
|
||||
// if user didn't login and closed the popup, we remove the checkoutAfterLogin flag
|
||||
// so that login from other buttons on course page will trigger purchase
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
parentOnClose();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
CHECKOUT_AFTER_LOGIN_KEY,
|
||||
checkoutAfterLogin ? '1' : '0',
|
||||
);
|
||||
}, [checkoutAfterLogin]);
|
||||
|
||||
if (emailNature) {
|
||||
const emailHeader = (
|
||||
<div className="mb-7 text-center">
|
||||
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900">
|
||||
{emailNature === 'login'
|
||||
? 'Login to your account'
|
||||
: 'Create an account'}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-4 text-slate-600">
|
||||
Fill in the details below to continue
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} bodyClassName="p-5 h-auto">
|
||||
{emailHeader}
|
||||
{emailNature === 'login' && (
|
||||
<EmailLoginForm
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
)}
|
||||
{emailNature === 'signup' && (
|
||||
<EmailSignupForm
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="mt-2 w-full rounded-md border border-gray-400 py-2 text-center text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setEmailNature(null)}
|
||||
>
|
||||
Back to Options
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} bodyClassName="p-5 h-auto">
|
||||
<div className="mb-7 text-center">
|
||||
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900">
|
||||
Create or login to your account
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-4 text-slate-600">
|
||||
Login or sign up for an account to start learning
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<GitHubButton
|
||||
className="rounded-md border-gray-400 hover:bg-gray-100"
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
<GoogleButton
|
||||
className="rounded-md border-gray-400 hover:bg-gray-100"
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
<LinkedInButton
|
||||
className="rounded-md border-gray-400 hover:bg-gray-100"
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-4 py-6 text-sm text-gray-600">
|
||||
<div className="h-px w-full bg-gray-200" />
|
||||
OR
|
||||
<div className="h-px w-full bg-gray-200" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
{!isUsingEmail && (
|
||||
<button
|
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setIsUsingEmail(true)}
|
||||
>
|
||||
Use your email address
|
||||
</button>
|
||||
)}
|
||||
{isUsingEmail && (
|
||||
<>
|
||||
<button
|
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setEmailNature('login')}
|
||||
>
|
||||
Already have an account
|
||||
</button>
|
||||
<button
|
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setEmailNature('signup')}
|
||||
>
|
||||
Create an account
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useId, useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { FIRST_LOGIN_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
|
||||
type EmailLoginFormProps = {
|
||||
isDisabled?: boolean;
|
||||
@@ -24,19 +24,22 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
|
||||
setIsDisabled?.(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-login`,
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
);
|
||||
const { response, error } = await httpPost<{
|
||||
token: string;
|
||||
isNewUser: boolean;
|
||||
}>(`${import.meta.env.PUBLIC_API_URL}/v1-login`, {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
// Log the user in and reload the page
|
||||
if (response?.token) {
|
||||
setAuthToken(response.token);
|
||||
window.location.reload();
|
||||
|
||||
const currentLocation = window.location.href;
|
||||
const url = new URL(currentLocation, window.location.origin);
|
||||
url.searchParams.set(FIRST_LOGIN_PARAM, response?.isNewUser ? '1' : '0');
|
||||
window.location.href = url.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
COURSE_PURCHASE_PARAM,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
|
||||
type GitHubButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const GITHUB_REDIRECT_AT = 'githubRedirectAt';
|
||||
const GITHUB_LAST_PAGE = 'githubLastPage';
|
||||
|
||||
export function GitHubButton(props: GitHubButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
const { isDisabled, setIsDisabled, className } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -32,7 +38,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
@@ -49,7 +55,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT);
|
||||
const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE);
|
||||
|
||||
@@ -61,20 +67,37 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGithub;
|
||||
redirectUrl = new URL(lastPageBeforeGithub, window.location.origin);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
localStorage.removeItem(GITHUB_REDIRECT_AT);
|
||||
localStorage.removeItem(GITHUB_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
redirectUrl.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
@@ -120,7 +143,10 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className={cn(
|
||||
'inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none hover:border-gray-400 hover:bg-gray-50 focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { COURSE_PURCHASE_PARAM } from '../../lib/jwt';
|
||||
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import {
|
||||
getStoredUtmParams,
|
||||
triggerUtmRegistration,
|
||||
} from '../../lib/browser.ts';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type GoogleButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const GOOGLE_REDIRECT_AT = 'googleRedirectAt';
|
||||
const GOOGLE_LAST_PAGE = 'googleLastPage';
|
||||
|
||||
export function GoogleButton(props: GoogleButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
const { isDisabled, setIsDisabled, className } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -35,14 +43,12 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
const utmParams = getStoredUtmParams();
|
||||
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
@@ -53,7 +59,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT);
|
||||
const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE);
|
||||
|
||||
@@ -65,20 +71,37 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGoogle;
|
||||
redirectUrl = new URL(lastPageBeforeGoogle, window.location.origin);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
redirectUrl.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
localStorage.removeItem(GOOGLE_REDIRECT_AT);
|
||||
localStorage.removeItem(GOOGLE_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
@@ -130,7 +153,10 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className={cn(
|
||||
'inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none hover:border-gray-400 hover:bg-gray-50 focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
COURSE_PURCHASE_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
|
||||
type LinkedInButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const LINKEDIN_REDIRECT_AT = 'linkedInRedirectAt';
|
||||
const LINKEDIN_LAST_PAGE = 'linkedInLastPage';
|
||||
|
||||
export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
const { isDisabled, setIsDisabled, className } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -32,7 +40,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
@@ -48,7 +56,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const linkedInRedirectAt = localStorage.getItem(LINKEDIN_REDIRECT_AT);
|
||||
const lastPageBeforeLinkedIn = localStorage.getItem(LINKEDIN_LAST_PAGE);
|
||||
|
||||
@@ -60,20 +68,39 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeLinkedIn;
|
||||
redirectUrl = new URL(
|
||||
lastPageBeforeLinkedIn,
|
||||
window.location.origin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
redirectUrl.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
|
||||
localStorage.removeItem(LINKEDIN_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
@@ -125,14 +152,17 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className={cn(
|
||||
'inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none hover:border-gray-400 focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
|
||||
) : (
|
||||
<LinkedInIcon className={'h-[18px] w-[18px]'} />
|
||||
<LinkedInIcon className={'h-[18px] w-[18px] text-blue-700'} />
|
||||
)}
|
||||
Continue with LinkedIn
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
@@ -13,7 +17,7 @@ export function TriggerVerifyAccount() {
|
||||
const triggerVerify = (code: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
httpPost<{ token: string }>(
|
||||
httpPost<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
|
||||
{
|
||||
code,
|
||||
@@ -30,7 +34,13 @@ export function TriggerVerifyAccount() {
|
||||
triggerUtmRegistration();
|
||||
|
||||
setAuthToken(response.token);
|
||||
window.location.href = '/';
|
||||
|
||||
const url = new URL('/', window.location.origin);
|
||||
url.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
window.location.href = url.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import { getAllChangelogs } from '../lib/changelog';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import AstroIcon from './AstroIcon.astro';
|
||||
const allChangelogs = await getAllChangelogs();
|
||||
const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
---
|
||||
@@ -12,16 +12,18 @@ const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
<img
|
||||
src='/images/gifs/rocket.gif'
|
||||
alt='Rocket'
|
||||
class='mr-2 hidden sm:inline h-12 w-12'
|
||||
class='mr-2 hidden h-12 w-12 sm:inline'
|
||||
/>
|
||||
Actively Maintained
|
||||
</p>
|
||||
<p class='mt-1 mb-2 sm:my-2 text-sm leading-relaxed text-gray-600 sm:my-5 sm:text-lg'>
|
||||
<p
|
||||
class='mb-2 mt-1 text-sm leading-relaxed text-gray-600 sm:my-2 sm:my-5 sm:text-lg'
|
||||
>
|
||||
We are always improving our content, adding new resources and adding
|
||||
features to enhance your learning experience.
|
||||
</p>
|
||||
|
||||
<div class='relative mt-2 sm:mt-8 text-left'>
|
||||
<div class='relative mt-2 text-left sm:mt-8'>
|
||||
<div
|
||||
class='absolute inset-y-0 left-[120px] hidden w-px -translate-x-[0.5px] translate-x-[5.75px] bg-gray-300 sm:block'
|
||||
>
|
||||
@@ -36,13 +38,13 @@ const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
<li class='relative'>
|
||||
<a
|
||||
href={`/changelog#${changelog.id}`}
|
||||
class='flex flex-col sm:flex-row items-start sm:items-center'
|
||||
class='flex flex-col items-start sm:flex-row sm:items-center'
|
||||
>
|
||||
<span class='sm:w-[120px] flex-shrink-0 pr-0 sm:pr-4 text-right text-sm tracking-wide text-gray-400'>
|
||||
<span class='flex-shrink-0 pr-0 text-right text-sm tracking-wide text-gray-400 sm:w-[120px] sm:pr-4'>
|
||||
{formattedDate}
|
||||
</span>
|
||||
<span class='h-3 w-3 flex-shrink-0 rounded-full bg-gray-300 hidden sm:block' />
|
||||
<span class='text-balance sm:pl-8 text-base font-medium text-gray-900'>
|
||||
<span class='hidden h-3 w-3 flex-shrink-0 rounded-full bg-gray-300 sm:block' />
|
||||
<span class='text-balance text-base font-medium text-gray-900 sm:pl-8'>
|
||||
{changelog.frontmatter.title}
|
||||
</span>
|
||||
</a>
|
||||
@@ -52,13 +54,23 @@ const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class='mt-2 sm:mt-8 text-left sm:text-center'>
|
||||
<div
|
||||
class='mt-2 flex flex-col gap-2 sm:flex-row sm:mt-8 sm:items-center sm:justify-center'
|
||||
>
|
||||
<a
|
||||
href='/changelog'
|
||||
class='inline-block text-sm sm:text-base rounded-lg sm:rounded-full bg-gray-800 px-4 sm:px-6 py-2 text-white transition-colors hover:bg-gray-700'
|
||||
class='inline-block rounded-lg border border-black bg-black px-4 py-2 text-sm text-white transition-colors hover:bg-gray-700 sm:rounded-full sm:px-6 sm:text-base'
|
||||
>
|
||||
View Full Changelog
|
||||
</a>
|
||||
<button
|
||||
data-guest-required
|
||||
data-popup='login-popup'
|
||||
class='flex flex-row items-center gap-2 rounded-lg border border-black bg-white px-4 py-2 text-sm text-black transition-all hover:bg-black hover:text-white sm:rounded-full sm:pl-4 sm:pr-5 sm:text-base'
|
||||
>
|
||||
<AstroIcon icon='bell' class='h-5 w-5' />
|
||||
Subscribe for Notifications
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../../editor/utils/classname';
|
||||
import { useParams } from '../../hooks/use-params';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { $teamList } from '../../stores/team';
|
||||
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
|
||||
import { DashboardTab } from './DashboardTab';
|
||||
import { DashboardTabButton } from './DashboardTabButton';
|
||||
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
|
||||
import { TeamDashboard } from './TeamDashboard';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { useParams } from '../../hooks/use-params';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
|
||||
type DashboardPageProps = {
|
||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps?: BuiltInRoadmap[];
|
||||
builtInBestPractices?: BuiltInRoadmap[];
|
||||
isTeamPage?: boolean;
|
||||
questionGroups?: QuestionGroupType[];
|
||||
guides?: GuideFileType[];
|
||||
videos?: VideoFileType[];
|
||||
};
|
||||
|
||||
export function DashboardPage(props: DashboardPageProps) {
|
||||
@@ -23,6 +30,9 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
builtInBestPractices,
|
||||
builtInSkillRoadmaps,
|
||||
isTeamPage = false,
|
||||
questionGroups,
|
||||
guides,
|
||||
videos,
|
||||
} = props;
|
||||
|
||||
const currentUser = getUser();
|
||||
@@ -66,78 +76,79 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
: '/images/default-avatar.png';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pb-20 pt-8">
|
||||
<div className="container">
|
||||
<div className="mb-6 flex flex-wrap items-center gap-1.5 sm:mb-8">
|
||||
<DashboardTab
|
||||
label="Personal"
|
||||
isActive={!selectedTeamId && !isTeamPage}
|
||||
href="/dashboard"
|
||||
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?t=${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
|
||||
className={cn('bg-slate-900', {
|
||||
'striped-loader-slate': isLoading,
|
||||
})}
|
||||
>
|
||||
<div className="bg-slate-800/30 py-5 min-h-[70px]">
|
||||
<div className="container flex flex-wrap items-center gap-1.5">
|
||||
{!isLoading && (
|
||||
<>
|
||||
<DashboardTabButton
|
||||
label="Personal"
|
||||
isActive={!selectedTeamId && !isTeamPage}
|
||||
href="/dashboard"
|
||||
avatar={userAvatar}
|
||||
/>
|
||||
{teamList.map((team) => {
|
||||
const { avatar } = team;
|
||||
const avatarUrl = avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
return (
|
||||
<DashboardTabButton
|
||||
key={team._id}
|
||||
label={team.name}
|
||||
isActive={team._id === selectedTeamId}
|
||||
{...(team.status === 'invited'
|
||||
? {
|
||||
href: `/respond-invite?i=${team.memberId}`,
|
||||
}
|
||||
: {
|
||||
href: `/team?t=${team._id}`,
|
||||
})}
|
||||
avatar={avatarUrl}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<DashboardTabButton
|
||||
label="+ Create Team"
|
||||
isActive={false}
|
||||
href="/team/new"
|
||||
className="border border-dashed border-slate-700 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-solid hover:border-slate-700 hover:text-gray-400"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
{!selectedTeamId && !isTeamPage && (
|
||||
<PersonalDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||
builtInBestPractices={builtInBestPractices}
|
||||
/>
|
||||
<div className="bg-slate-900">
|
||||
<PersonalDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||
builtInBestPractices={builtInBestPractices}
|
||||
questionGroups={questionGroups}
|
||||
guides={guides}
|
||||
videos={videos}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(selectedTeamId || isTeamPage) && (
|
||||
<TeamDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps!}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps!}
|
||||
teamId={selectedTeamId!}
|
||||
/>
|
||||
<div className="container">
|
||||
<TeamDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps!}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps!}
|
||||
teamId={selectedTeamId!}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardTabSkeleton() {
|
||||
return (
|
||||
<div className="h-[30px] w-[114px] animate-pulse rounded-md border bg-white"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type DashboardTabProps = {
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
export function DashboardTab(props: DashboardTabProps) {
|
||||
export function DashboardTabButton(props: DashboardTabProps) {
|
||||
const { isActive, onClick, label, className, href, avatar, icon } = props;
|
||||
|
||||
const Slot = href ? 'a' : 'button';
|
||||
@@ -20,8 +20,10 @@ export function DashboardTab(props: DashboardTabProps) {
|
||||
<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' : '',
|
||||
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border border-slate-700 bg-slate-800 p-1.5 pl-2 pr-3 text-sm leading-none text-gray-400 transition-colors hover:bg-slate-700',
|
||||
isActive
|
||||
? 'border-slate-200 bg-slate-200 text-gray-900 hover:bg-slate-200'
|
||||
: '',
|
||||
className,
|
||||
)}
|
||||
{...(href ? { href } : {})}
|
||||
@@ -30,7 +32,7 @@ export function DashboardTab(props: DashboardTabProps) {
|
||||
<img
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
className="h-4 w-4 mr-0.5 rounded-full object-cover"
|
||||
className="mr-0.5 h-4 w-4 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{icon}
|
||||
@@ -1,23 +1,47 @@
|
||||
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';
|
||||
import {
|
||||
ChartColumn,
|
||||
CheckSquare,
|
||||
FolderGit2,
|
||||
SquarePen,
|
||||
Zap,
|
||||
type LucideIcon
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { AllowedProfileVisibility } from '../../api/user.ts';
|
||||
import { PencilIcon, type LucideIcon } from 'lucide-react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
import { $accountStreak, type StreakResponse } from '../../stores/streak';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import { FeaturedGuideList } from '../FeaturedGuides/FeaturedGuideList';
|
||||
import { FeaturedVideoList } from '../FeaturedVideos/FeaturedVideoList';
|
||||
import {
|
||||
FavoriteRoadmaps,
|
||||
type AIRoadmapType,
|
||||
} from '../HeroSection/FavoriteRoadmaps.tsx';
|
||||
import { HeroRoadmap } from '../HeroSection/HeroRoadmap.tsx';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
|
||||
const projectGroups = [
|
||||
{
|
||||
title: 'Frontend',
|
||||
id: 'frontend',
|
||||
},
|
||||
{
|
||||
title: 'Backend',
|
||||
id: 'backend',
|
||||
},
|
||||
{
|
||||
title: 'DevOps',
|
||||
id: 'devops',
|
||||
},
|
||||
];
|
||||
|
||||
type UserDashboardResponse = {
|
||||
name: string;
|
||||
@@ -28,11 +52,7 @@ type UserDashboardResponse = {
|
||||
profileVisibility: AllowedProfileVisibility;
|
||||
progresses: UserProgress[];
|
||||
projects: ProjectStatusDocument[];
|
||||
aiRoadmaps: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
}[];
|
||||
aiRoadmaps: AIRoadmapType[];
|
||||
topicDoneToday: number;
|
||||
};
|
||||
|
||||
@@ -42,6 +62,7 @@ export type BuiltInRoadmap = {
|
||||
title: string;
|
||||
description: string;
|
||||
isFavorite?: boolean;
|
||||
isNew?: boolean;
|
||||
relatedRoadmapIds?: string[];
|
||||
renderer?: AllowedRoadmapRenderer;
|
||||
metadata?: Record<string, any>;
|
||||
@@ -51,16 +72,162 @@ type PersonalDashboardProps = {
|
||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps?: BuiltInRoadmap[];
|
||||
builtInBestPractices?: BuiltInRoadmap[];
|
||||
questionGroups?: QuestionGroupType[];
|
||||
guides?: GuideFileType[];
|
||||
videos?: VideoFileType[];
|
||||
};
|
||||
|
||||
type DashboardStatItemProps = {
|
||||
icon: LucideIcon;
|
||||
iconClassName: string;
|
||||
value: number;
|
||||
label: string;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
function DashboardStatItem(props: DashboardStatItemProps) {
|
||||
const { icon: Icon, iconClassName, value, label, isLoading } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3',
|
||||
{
|
||||
'striped-loader-slate striped-loader-slate-fast text-transparent':
|
||||
isLoading,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
size={16}
|
||||
className={cn(iconClassName, { 'text-transparent': isLoading })}
|
||||
/>
|
||||
<span>
|
||||
<span className="tabular-nums">{value}</span> {label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ProfileButtonProps = {
|
||||
isLoading: boolean;
|
||||
name?: string;
|
||||
username?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
function PersonalProfileButton(props: ProfileButtonProps) {
|
||||
const { isLoading, name, username, avatar } = props;
|
||||
|
||||
if (isLoading || !username) {
|
||||
return (
|
||||
<a
|
||||
href="/account/update-profile"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 font-medium outline-slate-700 hover:bg-slate-800 hover:outline-slate-400',
|
||||
{
|
||||
'striped-loader-slate striped-loader-slate-fast text-transparent':
|
||||
isLoading,
|
||||
'bg-blue-500/10 text-blue-500 hover:bg-blue-500/20': !isLoading,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<CheckSquare className="h-4 w-4" strokeWidth={2.5} />
|
||||
Set up your profile
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-1.5">
|
||||
<a
|
||||
href={`/u/${username}`}
|
||||
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-300 transition-colors hover:bg-slate-800/70"
|
||||
>
|
||||
<img
|
||||
src={avatar}
|
||||
alt={name || 'Profile'}
|
||||
className="h-5 w-5 rounded-full ring-1 ring-slate-700"
|
||||
/>
|
||||
<span className="font-medium">Visit Profile</span>
|
||||
</a>
|
||||
<a
|
||||
href="/account/update-profile"
|
||||
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-400 transition-colors hover:bg-slate-800/70 hover:text-slate-300"
|
||||
title="Edit Profile"
|
||||
>
|
||||
<SquarePen className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DashboardStatsProps = {
|
||||
profile: ProfileButtonProps;
|
||||
accountStreak?: StreakResponse;
|
||||
topicsDoneToday?: number;
|
||||
finishedProjectsCount?: number;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
function DashboardStats(props: DashboardStatsProps) {
|
||||
const {
|
||||
accountStreak,
|
||||
topicsDoneToday = 0,
|
||||
finishedProjectsCount = 0,
|
||||
isLoading,
|
||||
profile,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="container mb-3 flex flex-col gap-4 pb-2 pt-6 text-sm text-slate-400 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex w-full flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<PersonalProfileButton
|
||||
isLoading={isLoading}
|
||||
name={profile.name}
|
||||
username={profile.username}
|
||||
avatar={profile.avatar}
|
||||
/>
|
||||
<div className="hidden flex-wrap items-center gap-2 md:flex">
|
||||
<DashboardStatItem
|
||||
icon={Zap}
|
||||
iconClassName="text-yellow-500"
|
||||
value={accountStreak?.count || 0}
|
||||
label="day streak"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<DashboardStatItem
|
||||
icon={ChartColumn}
|
||||
iconClassName="text-green-500"
|
||||
value={topicsDoneToday}
|
||||
label="learnt today"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<DashboardStatItem
|
||||
icon={FolderGit2}
|
||||
iconClassName="text-blue-500"
|
||||
value={finishedProjectsCount}
|
||||
label="projects finished"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
const {
|
||||
builtInRoleRoadmaps = [],
|
||||
builtInBestPractices = [],
|
||||
builtInSkillRoadmaps = [],
|
||||
questionGroups = [],
|
||||
guides = [],
|
||||
videos = [],
|
||||
} = props;
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [personalDashboardDetails, setPersonalDashboardDetails] =
|
||||
useState<UserDashboardResponse>();
|
||||
@@ -138,7 +305,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
||||
}, []);
|
||||
|
||||
const learningRoadmapsToShow = (personalDashboardDetails?.progresses || [])
|
||||
const learningRoadmapsToShow: UserProgress[] = (
|
||||
personalDashboardDetails?.progresses || []
|
||||
)
|
||||
.filter((progress) => !progress.isCustomResource)
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
@@ -156,7 +325,10 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
});
|
||||
|
||||
const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || [];
|
||||
const customRoadmaps = (personalDashboardDetails?.progresses || [])
|
||||
|
||||
const customRoadmaps: UserProgress[] = (
|
||||
personalDashboardDetails?.progresses || []
|
||||
)
|
||||
.filter((progress) => progress.isCustomResource)
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
@@ -169,43 +341,6 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
? `${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(
|
||||
@@ -232,165 +367,200 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
const { username } = personalDashboardDetails || {};
|
||||
|
||||
return (
|
||||
<section>
|
||||
{isLoading ? (
|
||||
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
|
||||
) : (
|
||||
<div className="flex flex-col items-start justify-between gap-1 sm:flex-row sm:items-center">
|
||||
<h2 className="text-lg font-medium">
|
||||
Hi {name}, good {getCurrentPeriod()}!
|
||||
</h2>
|
||||
<a
|
||||
href="/home"
|
||||
className="rounded-full bg-gray-200 px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-300 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={
|
||||
username ? 'View your profile' : 'Setup your profile'
|
||||
}
|
||||
href={username ? `/u/${username}` : '/account/update-profile'}
|
||||
{...(username && {
|
||||
externalLinkIcon: PencilIcon,
|
||||
externalLinkHref: '/account/update-profile',
|
||||
externalLinkText: 'Edit',
|
||||
})}
|
||||
className={
|
||||
!username
|
||||
? 'border-dashed border-gray-500 bg-gray-100 hover:border-gray-500 hover:bg-gray-200'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
<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 || []}
|
||||
<div>
|
||||
<DashboardStats
|
||||
profile={{
|
||||
name,
|
||||
username,
|
||||
avatar: avatarLink,
|
||||
isLoading,
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
accountStreak={accountStreak}
|
||||
topicDoneToday={personalDashboardDetails?.topicDoneToday || 0}
|
||||
topicsDoneToday={personalDashboardDetails?.topicDoneToday}
|
||||
finishedProjectsCount={
|
||||
enrichedProjects?.filter((p) => p.submittedAt && p.repositoryUrl)
|
||||
.length
|
||||
}
|
||||
/>
|
||||
|
||||
<ListDashboardCustomProgress
|
||||
progresses={customRoadmaps}
|
||||
<FavoriteRoadmaps
|
||||
progress={learningRoadmapsToShow}
|
||||
customRoadmaps={customRoadmaps}
|
||||
aiRoadmaps={aiGeneratedRoadmaps}
|
||||
projects={enrichedProjects || []}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<DashboardAiRoadmaps
|
||||
roadmaps={aiGeneratedRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<div className="bg-gradient-to-b from-slate-900 to-black pb-12">
|
||||
<div className="relative mt-6 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2
|
||||
id="role-based-roadmaps"
|
||||
className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2"
|
||||
>
|
||||
Role Based Roadmaps
|
||||
</h2>
|
||||
|
||||
<RecommendedRoadmaps
|
||||
roadmaps={recommendedRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{builtInRoleRoadmaps.map((roadmap) => {
|
||||
const roadmapProgress = learningRoadmapsToShow.find(
|
||||
(lr) => lr.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
type DashboardCardProps = {
|
||||
icon?: JSXElementConstructor<any>;
|
||||
imgUrl?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
externalLinkIcon?: LucideIcon;
|
||||
externalLinkText?: string;
|
||||
externalLinkHref?: string;
|
||||
className?: string;
|
||||
};
|
||||
const percentageDone =
|
||||
(((roadmapProgress?.skipped || 0) +
|
||||
(roadmapProgress?.done || 0)) /
|
||||
(roadmapProgress?.total || 1)) *
|
||||
100;
|
||||
|
||||
function DashboardCard(props: DashboardCardProps) {
|
||||
const {
|
||||
icon: Icon,
|
||||
imgUrl,
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
externalLinkHref,
|
||||
externalLinkIcon: ExternalLinkIcon,
|
||||
externalLinkText,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={cn('relative overflow-hidden', className)}>
|
||||
<a
|
||||
href={href}
|
||||
className="flex flex-col 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" />
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={roadmap.id}
|
||||
resourceId={roadmap.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={roadmap.title}
|
||||
isFavorite={roadmap.isFavorite}
|
||||
percentageDone={percentageDone}
|
||||
isNew={roadmap.isNew}
|
||||
url={`/${roadmap.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{externalLinkHref && (
|
||||
<a
|
||||
href={externalLinkHref}
|
||||
className="absolute right-1 top-1 flex items-center gap-1.5 rounded-md bg-gray-200 p-1 px-2 text-xs text-gray-600 hover:bg-gray-300 hover:text-black"
|
||||
>
|
||||
{ExternalLinkIcon && <ExternalLinkIcon className="size-3" />}
|
||||
{externalLinkText}
|
||||
</a>
|
||||
)}
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Skill Based Roadmaps
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{builtInSkillRoadmaps.map((roadmap) => {
|
||||
const roadmapProgress = learningRoadmapsToShow.find(
|
||||
(lr) => lr.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
const percentageDone =
|
||||
(((roadmapProgress?.skipped || 0) +
|
||||
(roadmapProgress?.done || 0)) /
|
||||
(roadmapProgress?.total || 1)) *
|
||||
100;
|
||||
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={roadmap.id}
|
||||
resourceId={roadmap.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={roadmap.title}
|
||||
isFavorite={roadmap.isFavorite}
|
||||
percentageDone={percentageDone}
|
||||
isNew={roadmap.isNew}
|
||||
url={`/${roadmap.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Project Ideas
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{projectGroups.map((projectGroup) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
percentageDone={0}
|
||||
key={projectGroup.id}
|
||||
resourceId={projectGroup.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={projectGroup.title}
|
||||
url={`/${projectGroup.id}/projects`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Best Practices
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{builtInBestPractices.map((roadmap) => {
|
||||
const roadmapProgress = learningRoadmapsToShow.find(
|
||||
(lr) => lr.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
const percentageDone =
|
||||
(((roadmapProgress?.skipped || 0) +
|
||||
(roadmapProgress?.done || 0)) /
|
||||
(roadmapProgress?.total || 1)) *
|
||||
100;
|
||||
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={roadmap.id}
|
||||
resourceId={roadmap.id}
|
||||
resourceType="best-practice"
|
||||
resourceTitle={roadmap.title}
|
||||
isFavorite={roadmap.isFavorite}
|
||||
percentageDone={percentageDone}
|
||||
isNew={roadmap.isNew}
|
||||
url={`/best-practices/${roadmap.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Questions
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{questionGroups.map((questionGroup) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
percentageDone={0}
|
||||
key={questionGroup.id}
|
||||
resourceId={questionGroup.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={questionGroup.frontmatter.briefTitle}
|
||||
url={`/questions/${questionGroup.id}`}
|
||||
allowFavorite={false}
|
||||
isNew={questionGroup.frontmatter.isNew}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5 bg-gray-50 px-4 py-5 sm:gap-16 sm:px-0 sm:py-16">
|
||||
<FeaturedGuideList
|
||||
heading="Guides"
|
||||
guides={guides}
|
||||
questions={questionGroups
|
||||
.filter((questionGroup) => questionGroup.frontmatter.authorId)
|
||||
.slice(0, 7)}
|
||||
/>
|
||||
<FeaturedVideoList heading="Videos" videos={videos} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardCardSkeleton() {
|
||||
return (
|
||||
<div className="h-[128px] animate-pulse rounded-lg border border-gray-300 bg-white"></div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,80 +6,24 @@ import { isMobileScreen } from '../lib/is-mobile.ts';
|
||||
type FeatureAnnouncementProps = {};
|
||||
|
||||
export function FeatureAnnouncement(props: FeatureAnnouncementProps) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const videoModal = (
|
||||
<Modal
|
||||
onClose={() => setIsPlaying(false)}
|
||||
bodyClassName={'h-auto overflow-hidden'}
|
||||
wrapperClassName={'md:max-w-3xl lg:max-w-4xl xl:max-w-5xl'}
|
||||
>
|
||||
<div className="text-balance bg-gradient-to-r from-gray-100 px-4 py-2 text-left text-sm md:py-3 lg:text-base">
|
||||
<span
|
||||
className="relative -top-px mr-1.5 rounded bg-blue-300 px-1.5 py-0.5 text-xs font-semibold uppercase text-gray-800"
|
||||
style={{ lineHeight: '1.5' }}
|
||||
>
|
||||
New
|
||||
</span>
|
||||
Projects are live on the{' '}
|
||||
<a
|
||||
href={'/projects'}
|
||||
className="font-medium text-blue-500 underline underline-offset-2"
|
||||
>
|
||||
several of our roadmaps
|
||||
</a>
|
||||
<span className={'hidden md:inline'}>
|
||||
{' '}
|
||||
and are coming soon on the others
|
||||
</span>
|
||||
<PartyPopper className="relative -top-[3px] ml-2 inline-block h-5 w-5 text-blue-500 md:ml-1 md:h-6 md:w-6" />
|
||||
</div>
|
||||
<div
|
||||
className="iframe-container"
|
||||
style={{
|
||||
position: 'relative',
|
||||
paddingBottom: '56.25%',
|
||||
height: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/*https://www.youtube.com/embed/?playsinline=1&disablekb=1&&iv_load_policy=3&cc_load_policy=0&controls=0&rel=0&autoplay=1&mute=1&origin=https%3A%2F%2Fytch.xyz&widgetid=1*/}
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/9lS3slfJ0x0?start=31&autoplay=1&disablekb=1&rel=0&cc_load_policy=0&rel=0&autoplay=1&origin=https%3A%2F%2Froadmap.sh&widgetid=1&showinfo=0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{isPlaying && videoModal}
|
||||
<button
|
||||
<a
|
||||
className="rounded-md border border-dashed border-purple-600 px-3 py-1.5 text-purple-400 transition-colors hover:border-purple-400 hover:text-purple-200"
|
||||
onClick={() => {
|
||||
setIsPlaying(true);
|
||||
}}
|
||||
href="/courses/sql"
|
||||
>
|
||||
<span className="relative sm:-top-[1px] mr-1 text-xs font-semibold uppercase text-white">
|
||||
<PlayCircle className="inline-block h-4 w-4 relative -top-[2px] mr-1" />
|
||||
Watch
|
||||
<PartyPopper className="inline-block h-4 w-4 relative -top-[2px] mr-1" />
|
||||
Courses
|
||||
</span>{' '}
|
||||
<span className={'hidden sm:inline'}>
|
||||
Practice your skills with projects
|
||||
Our first paid course about SQL is now live!
|
||||
</span>
|
||||
<span className={'inline text-sm sm:hidden'}>
|
||||
Build projects to skill up
|
||||
Our SQL course is now live!
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
import type { GuideFileType } from '../lib/guide';
|
||||
import GuideListItem from './GuideListItem.astro';
|
||||
import { QuestionGroupType } from '../lib/question-group';
|
||||
|
||||
export interface Props {
|
||||
heading: string;
|
||||
guides: GuideFileType[];
|
||||
questions: QuestionGroupType[];
|
||||
}
|
||||
|
||||
const { heading, guides, questions = [] } = Astro.props;
|
||||
|
||||
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
|
||||
...guides,
|
||||
...questions,
|
||||
].sort((a, b) => {
|
||||
const aDate = new Date(a.frontmatter.date);
|
||||
const bDate = new Date(b.frontmatter.date);
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
---
|
||||
|
||||
<div class='container'>
|
||||
<h2 class='block text-2xl font-bold sm:text-3xl'>{heading}</h2>
|
||||
|
||||
<div class='mt-3 sm:my-5'>
|
||||
{sortedGuides.map((guide) => <GuideListItem guide={guide} />)}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href='/guides'
|
||||
class='hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline'
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
|
||||
<div class='mt-3 block sm:hidden'>
|
||||
<a
|
||||
href='/guides'
|
||||
class='font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50'
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
51
src/components/FeaturedGuides/FeaturedGuideList.tsx
Normal file
51
src/components/FeaturedGuides/FeaturedGuideList.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import { GuideListItem } from './GuideListItem';
|
||||
|
||||
export interface FeaturedGuidesProps {
|
||||
heading: string;
|
||||
guides: GuideFileType[];
|
||||
questions: QuestionGroupType[];
|
||||
}
|
||||
|
||||
export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
const { heading, guides, questions = [] } = props;
|
||||
|
||||
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
|
||||
...guides,
|
||||
...questions,
|
||||
].sort((a, b) => {
|
||||
const aDate = new Date(a.frontmatter.date as string);
|
||||
const bDate = new Date(b.frontmatter.date as string);
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h2 className="block text-2xl font-bold sm:text-3xl">{heading}</h2>
|
||||
|
||||
<div className="mt-3 sm:my-5">
|
||||
{sortedGuides.map((guide) => (
|
||||
<GuideListItem key={guide.id} guide={guide} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/guides"
|
||||
className="hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline"
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
|
||||
<div className="mt-3 block sm:hidden">
|
||||
<a
|
||||
href="/guides"
|
||||
className="font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50"
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/FeaturedGuides/GuideListItem.tsx
Normal file
57
src/components/FeaturedGuides/GuideListItem.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
|
||||
import { type QuestionGroupType } from '../../lib/question-group';
|
||||
|
||||
export interface GuideListItemProps {
|
||||
guide: GuideFileType | QuestionGroupType;
|
||||
}
|
||||
|
||||
function isQuestionGroupType(
|
||||
guide: GuideFileType | QuestionGroupType,
|
||||
): guide is QuestionGroupType {
|
||||
return (guide as QuestionGroupType).questions !== undefined;
|
||||
}
|
||||
|
||||
export function GuideListItem(props: GuideListItemProps) {
|
||||
const { guide } = props;
|
||||
const { frontmatter, id } = guide;
|
||||
|
||||
let pageUrl = '';
|
||||
let guideType = '';
|
||||
|
||||
if (isQuestionGroupType(guide)) {
|
||||
pageUrl = `/questions/${id}`;
|
||||
guideType = 'Questions';
|
||||
} else {
|
||||
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
|
||||
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
|
||||
guideType = (frontmatter as GuideFrontmatter).type;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
|
||||
href={pageUrl}
|
||||
>
|
||||
<span className="text-sm transition-transform group-hover:translate-x-2 md:text-base">
|
||||
{frontmatter.title}
|
||||
|
||||
{frontmatter.isNew && (
|
||||
<span className="ml-2.5 rounded-sm bg-green-300 px-1.5 py-0.5 text-xs font-medium uppercase text-green-900">
|
||||
New
|
||||
<span className="hidden sm:inline">
|
||||
·
|
||||
{new Date(frontmatter.date || '').toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="hidden text-xs capitalize text-gray-500 sm:block">
|
||||
{guideType}
|
||||
</span>
|
||||
|
||||
<span className="block text-xs text-gray-400 sm:hidden"> »</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
import type { VideoFileType } from '../lib/video';
|
||||
import VideoListItem from './VideoListItem.astro';
|
||||
|
||||
export interface Props {
|
||||
heading: string;
|
||||
videos: VideoFileType[];
|
||||
}
|
||||
|
||||
const { heading, videos } = Astro.props;
|
||||
---
|
||||
|
||||
<div class='container'>
|
||||
<h2 class='text-2xl sm:text-3xl font-bold block'>{heading}</h2>
|
||||
|
||||
<div class='mt-3 sm:my-5'>
|
||||
{videos.map((video) => <VideoListItem video={video} />)}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href='/videos'
|
||||
class='hidden sm:inline transition-colors py-2 px-3 text-xs font-medium rounded-full bg-gradient-to-r from-slate-600 to-black hover:from-blue-600 hover:to-blue-800 text-white'
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
|
||||
<div class='block sm:hidden mt-3'>
|
||||
<a
|
||||
href='/videos'
|
||||
class='text-sm font-regular block p-2 border border-black text-black rounded-md text-center hover:bg-black hover:text-gray-50'
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
39
src/components/FeaturedVideos/FeaturedVideoList.tsx
Normal file
39
src/components/FeaturedVideos/FeaturedVideoList.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
import { VideoListItem } from './VideoListItem';
|
||||
|
||||
export interface FeaturedVideoListProps {
|
||||
heading: string;
|
||||
videos: VideoFileType[];
|
||||
}
|
||||
|
||||
export function FeaturedVideoList(props: FeaturedVideoListProps) {
|
||||
const { heading, videos } = props;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h2 className="block text-2xl font-bold sm:text-3xl">{heading}</h2>
|
||||
|
||||
<div className="mt-3 sm:my-5">
|
||||
{videos.map((video) => (
|
||||
<VideoListItem key={video.id} video={video} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/videos"
|
||||
className="hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline"
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
|
||||
<div className="mt-3 block sm:hidden">
|
||||
<a
|
||||
href="/videos"
|
||||
className="font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50"
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/FeaturedVideos/VideoListItem.tsx
Normal file
38
src/components/FeaturedVideos/VideoListItem.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
|
||||
export interface VideoListItemProps {
|
||||
video: VideoFileType;
|
||||
}
|
||||
|
||||
export function VideoListItem(props: VideoListItemProps) {
|
||||
const { video } = props;
|
||||
const { frontmatter, id } = video;
|
||||
|
||||
return (
|
||||
<a
|
||||
className="block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b"
|
||||
href={`/videos/${id}`}
|
||||
>
|
||||
<span className="group-hover:translate-x-2 transition-transform">
|
||||
{frontmatter.title}
|
||||
|
||||
{frontmatter.isNew && (
|
||||
<span className="bg-green-300 text-green-900 text-xs font-medium px-1.5 py-0.5 rounded-sm uppercase ml-1.5">
|
||||
New
|
||||
<span className="hidden sm:inline">
|
||||
·
|
||||
{new Date(frontmatter.date).toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="capitalize text-gray-500 text-xs hidden sm:block">
|
||||
{frontmatter.duration}
|
||||
</span>
|
||||
|
||||
<span className="text-gray-400 text-xs block sm:hidden"> »</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
import type { GuideFileType, GuideFrontmatter } from '../lib/guide';
|
||||
import { type QuestionGroupType } from '../lib/question-group';
|
||||
|
||||
export interface Props {
|
||||
guide: GuideFileType | QuestionGroupType;
|
||||
}
|
||||
|
||||
function isQuestionGroupType(
|
||||
guide: GuideFileType | QuestionGroupType,
|
||||
): guide is QuestionGroupType {
|
||||
return (guide as QuestionGroupType).questions !== undefined;
|
||||
}
|
||||
|
||||
const { guide } = Astro.props;
|
||||
const { frontmatter, id } = guide;
|
||||
|
||||
let pageUrl = '';
|
||||
let guideType = '';
|
||||
|
||||
if (isQuestionGroupType(guide)) {
|
||||
pageUrl = `/questions/${id}`;
|
||||
guideType = 'Questions';
|
||||
} else {
|
||||
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
|
||||
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
|
||||
guideType = (frontmatter as GuideFrontmatter).type;
|
||||
}
|
||||
---
|
||||
|
||||
<a
|
||||
class:list={[
|
||||
'text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600',
|
||||
]}
|
||||
href={pageUrl}
|
||||
>
|
||||
<span
|
||||
class='text-sm transition-transform group-hover:translate-x-2 md:text-base'
|
||||
>
|
||||
{frontmatter.title}
|
||||
|
||||
{
|
||||
frontmatter.isNew && (
|
||||
<span class='ml-1.5 rounded-sm bg-green-300 px-1.5 py-0.5 text-xs font-medium uppercase text-green-900'>
|
||||
New
|
||||
<span class='hidden sm:inline'>
|
||||
·
|
||||
{new Date(frontmatter.date || '').toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
<span class='hidden text-xs capitalize text-gray-500 sm:block'>
|
||||
{guideType}
|
||||
</span>
|
||||
|
||||
<span class='block text-xs text-gray-400 sm:hidden'> »</span>
|
||||
</a>
|
||||
@@ -1,164 +1,229 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { EmptyProgress } from './EmptyProgress';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
|
||||
import {
|
||||
FolderKanban,
|
||||
MapIcon,
|
||||
Plus,
|
||||
Sparkle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Square,
|
||||
SquareCheckBig,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon.tsx';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage.tsx';
|
||||
import { HeroProject } from './HeroProject';
|
||||
import { HeroRoadmap } from './HeroRoadmap';
|
||||
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton.tsx';
|
||||
import { HeroItemsGroup } from './HeroItemsGroup';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||
|
||||
export type UserProgressResponse = {
|
||||
resourceId: string;
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceTitle: string;
|
||||
isFavorite: boolean;
|
||||
done: number;
|
||||
learning: number;
|
||||
skipped: number;
|
||||
total: number;
|
||||
updatedAt: Date;
|
||||
isCustomResource: boolean;
|
||||
roadmapSlug?: string;
|
||||
team?: {
|
||||
name: string;
|
||||
id: string;
|
||||
role: AllowedMemberRoles;
|
||||
};
|
||||
}[];
|
||||
export type AIRoadmapType = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
function renderProgress(progressList: UserProgressResponse) {
|
||||
progressList.forEach((progress) => {
|
||||
const href =
|
||||
progress.resourceType === 'best-practice'
|
||||
? `/best-practices/${progress.resourceId}`
|
||||
: `/${progress.resourceId}`;
|
||||
const element = document.querySelector(`a[href="${href}"]`);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
type FavoriteRoadmapsProps = {
|
||||
progress: UserProgress[];
|
||||
projects: (ProjectStatusDocument & {
|
||||
title: string;
|
||||
})[];
|
||||
customRoadmaps: UserProgress[];
|
||||
aiRoadmaps: AIRoadmapType[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mark-favorite', {
|
||||
detail: {
|
||||
resourceId: progress.resourceId,
|
||||
resourceType: progress.resourceType,
|
||||
isFavorite: progress.isFavorite,
|
||||
},
|
||||
}),
|
||||
);
|
||||
export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
|
||||
const { progress, isLoading, customRoadmaps, aiRoadmaps, projects } = props;
|
||||
const [showCompleted, setShowCompleted] = useState(false);
|
||||
const [isCreatingCustomRoadmap, setIsCreatingCustomRoadmap] = useState(false);
|
||||
|
||||
const totalDone = progress.done + progress.skipped;
|
||||
const percentageDone = (totalDone / progress.total) * 100;
|
||||
|
||||
const progressBar: HTMLElement | null =
|
||||
element.querySelector('[data-progress]');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${percentageDone}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type ProgressResponse = UserProgressResponse;
|
||||
|
||||
export function FavoriteRoadmaps() {
|
||||
const isAuthenticated = isLoggedIn();
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [isPreparing, setIsPreparing] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [progress, setProgress] = useState<ProgressResponse>([]);
|
||||
const [containerOpacity, setContainerOpacity] = useState(0);
|
||||
|
||||
function showProgressContainer() {
|
||||
const heroEl = document.getElementById('hero-text')!;
|
||||
if (!heroEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
heroEl.classList.add('opacity-0');
|
||||
setTimeout(() => {
|
||||
heroEl.parentElement?.removeChild(heroEl);
|
||||
setIsPreparing(false);
|
||||
|
||||
setTimeout(() => {
|
||||
setContainerOpacity(100);
|
||||
}, 50);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async function loadProgress() {
|
||||
setIsLoading(true);
|
||||
|
||||
const { response: progressList, error } = await httpGet<ProgressResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`,
|
||||
);
|
||||
|
||||
if (error || !progressList) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress(progressList);
|
||||
setIsLoading(false);
|
||||
showProgressContainer();
|
||||
|
||||
// render progress on featured items
|
||||
renderProgress(progressList);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadProgress().finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('refresh-favorites', loadProgress);
|
||||
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
||||
}, []);
|
||||
|
||||
if (isPreparing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasProgress = progress?.length > 0;
|
||||
const customRoadmaps = progress?.filter(
|
||||
(p) => p.isCustomResource && !p.team?.name,
|
||||
const completedProjects = projects.filter(
|
||||
(project) => project.submittedAt && project.repositoryUrl,
|
||||
);
|
||||
const inProgressProjects = projects.filter(
|
||||
(project) => !project.submittedAt || !project.repositoryUrl,
|
||||
);
|
||||
const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource);
|
||||
const teamRoadmaps: HeroTeamRoadmaps = progress
|
||||
?.filter((p) => p.isCustomResource && p.team?.name)
|
||||
.reduce((acc: HeroTeamRoadmaps, curr) => {
|
||||
const currTeam = curr.team!;
|
||||
if (!acc[currTeam.name]) {
|
||||
acc[currTeam.name] = [];
|
||||
}
|
||||
|
||||
acc[currTeam.name].push(curr);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
const projectsToShow = [
|
||||
...inProgressProjects,
|
||||
...(showCompleted ? completedProjects : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`transition-opacity duration-500 opacity-${containerOpacity}`}
|
||||
>
|
||||
<div
|
||||
className={`flex min-h-[192px] bg-gradient-to-b sm:min-h-[280px] ${
|
||||
hasProgress && `border-t border-t-[#1e293c]`
|
||||
}`}
|
||||
<div className="flex flex-col">
|
||||
{isCreatingCustomRoadmap && (
|
||||
<CreateRoadmapModal
|
||||
onClose={() => {
|
||||
setIsCreatingCustomRoadmap(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your progress and bookmarks"
|
||||
isEmpty={!isLoading && progress.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No bookmars found
|
||||
<a
|
||||
href="#role-based-roadmaps"
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Bookmark a roadmap
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="container min-h-full">
|
||||
{!isLoading && progress?.length == 0 && <EmptyProgress />}
|
||||
{hasProgress && (
|
||||
<HeroRoadmaps
|
||||
teamRoadmaps={teamRoadmaps}
|
||||
customRoadmaps={customRoadmaps}
|
||||
progress={defaultRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{progress.map((resource) => (
|
||||
<HeroRoadmap
|
||||
key={`${resource.resourceType}-${resource.resourceId}`}
|
||||
resourceId={resource.resourceId}
|
||||
resourceType={resource.resourceType}
|
||||
resourceTitle={resource.resourceTitle}
|
||||
isFavorite={resource.isFavorite}
|
||||
percentageDone={
|
||||
((resource.skipped + resource.done) / resource.total) * 100
|
||||
}
|
||||
url={
|
||||
resource.resourceType === 'roadmap'
|
||||
? `/${resource.resourceId}`
|
||||
: `/best-practices/${resource.resourceId}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</HeroItemsGroup>
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your custom roadmaps"
|
||||
isEmpty={!isLoading && customRoadmaps.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No custom roadmaps found
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsCreatingCustomRoadmap(true);
|
||||
}}
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Create custom roadmap
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{customRoadmaps.map((customRoadmap) => (
|
||||
<HeroRoadmap
|
||||
key={customRoadmap.resourceId}
|
||||
resourceId={customRoadmap.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={customRoadmap.resourceTitle}
|
||||
percentageDone={
|
||||
((customRoadmap.skipped + customRoadmap.done) /
|
||||
customRoadmap.total) *
|
||||
100
|
||||
}
|
||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
))}
|
||||
<CreateRoadmapButton />
|
||||
</HeroItemsGroup>
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<Sparkle className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your AI roadmaps"
|
||||
isEmpty={!isLoading && aiRoadmaps.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No AI roadmaps found
|
||||
<a
|
||||
href="/ai"
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Generate AI roadmap
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{aiRoadmaps.map((aiRoadmap) => (
|
||||
<HeroRoadmap
|
||||
key={aiRoadmap.id}
|
||||
resourceId={aiRoadmap.id}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={aiRoadmap.title}
|
||||
url={`/ai/${aiRoadmap.slug}`}
|
||||
percentageDone={0}
|
||||
allowFavorite={false}
|
||||
isTrackable={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
<a
|
||||
href="/ai"
|
||||
className={
|
||||
'flex h-full w-full items-center justify-center gap-1 overflow-hidden rounded-md border border-dashed border-gray-800 p-3 text-sm text-gray-400 hover:border-gray-600 hover:bg-gray-900 hover:text-gray-300'
|
||||
}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Generate New
|
||||
</a>
|
||||
</HeroItemsGroup>
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<FolderKanban className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your active projects"
|
||||
isEmpty={!isLoading && projectsToShow.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No active projects found
|
||||
<a
|
||||
href="/projects"
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Start a new project
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
rightContent={
|
||||
completedProjects.length > 0 && (
|
||||
<button
|
||||
onClick={() => setShowCompleted(!showCompleted)}
|
||||
className="hidden items-center gap-2 rounded-md text-xs text-slate-400 hover:text-slate-300 sm:flex"
|
||||
>
|
||||
{showCompleted ? (
|
||||
<EyeOff className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{completedProjects.length} Completed
|
||||
</button>
|
||||
)
|
||||
}
|
||||
className="border-b-0"
|
||||
>
|
||||
{projectsToShow.map((project) => (
|
||||
<HeroProject key={project._id} project={project} />
|
||||
))}
|
||||
|
||||
<a
|
||||
href="/projects"
|
||||
className="flex min-h-[80px] items-center justify-center gap-2 rounded-md border border-dashed border-slate-800 p-4 text-sm text-slate-400 hover:border-slate-600 hover:bg-slate-900/50 hover:text-slate-300"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Start a new project
|
||||
</a>
|
||||
</HeroItemsGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
78
src/components/HeroSection/HeroItemsGroup.tsx
Normal file
78
src/components/HeroSection/HeroItemsGroup.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { HeroTitle } from './HeroTitle';
|
||||
|
||||
type HeroItemsGroupProps = {
|
||||
icon: any;
|
||||
isLoading?: boolean;
|
||||
isEmpty?: boolean;
|
||||
emptyTitle?: ReactNode;
|
||||
title: string | ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function HeroItemsGroup(props: HeroItemsGroupProps) {
|
||||
const {
|
||||
icon,
|
||||
isLoading = false,
|
||||
isEmpty = false,
|
||||
emptyTitle,
|
||||
title,
|
||||
rightContent,
|
||||
children,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const storageKey = `hero-group-${title}-collapsed`;
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
function isCollapsedByStorage() {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
|
||||
return stored === 'true';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsCollapsed(isCollapsedByStorage());
|
||||
}, [isLoading]);
|
||||
|
||||
const isLoadingOrCollapsedOrEmpty = isLoading || isCollapsed || isEmpty;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-b border-gray-800/50',
|
||||
{
|
||||
'py-4': !isLoadingOrCollapsedOrEmpty,
|
||||
'py-4 ': isLoadingOrCollapsedOrEmpty,
|
||||
'opacity-50 transition-opacity hover:opacity-100':
|
||||
isCollapsed && !isLoading,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="container">
|
||||
<HeroTitle
|
||||
icon={icon}
|
||||
isLoading={isLoading}
|
||||
isEmpty={isEmpty}
|
||||
emptyTitle={emptyTitle}
|
||||
title={title}
|
||||
rightContent={rightContent}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={() => {
|
||||
setIsCollapsed(!isCollapsed);
|
||||
localStorage.setItem(storageKey, (!isCollapsed).toString());
|
||||
}}
|
||||
/>
|
||||
{!isLoadingOrCollapsedOrEmpty && (
|
||||
<div className="mt-4 grid grid-cols-1 gap-2.5 sm:grid-cols-2 md:grid-cols-3">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
src/components/HeroSection/HeroProject.tsx
Normal file
52
src/components/HeroSection/HeroProject.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ThumbsUp } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
|
||||
|
||||
type HeroProjectProps = {
|
||||
project: ProjectStatusDocument & {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function HeroProject({ project }: HeroProjectProps) {
|
||||
return (
|
||||
<a
|
||||
href={`/projects/${project.projectId}`}
|
||||
className="group relative flex flex-col justify-between gap-2 rounded-md border border-slate-800 bg-slate-900 p-3.5 hover:border-slate-600"
|
||||
>
|
||||
<div className="relative z-10 flex items-start justify-between gap-2">
|
||||
<h3 className="truncate font-medium text-slate-300 group-hover:text-slate-100">
|
||||
{project.title}
|
||||
</h3>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute -right-2 -top-2 flex flex-shrink-0 items-center gap-1 rounded-full text-xs uppercase tracking-wide',
|
||||
{
|
||||
'text-green-600/50': project.submittedAt && project.repositoryUrl,
|
||||
'text-yellow-600': !project.submittedAt || !project.repositoryUrl,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{project.submittedAt && project.repositoryUrl ? 'Done' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative z-10 flex items-center gap-2 text-xs text-slate-400">
|
||||
{project.submittedAt && project.repositoryUrl && (
|
||||
<span className="flex items-center gap-1">
|
||||
<ThumbsUp className="h-3 w-3" />
|
||||
{project.upvotes}
|
||||
</span>
|
||||
)}
|
||||
{project.startedAt && (
|
||||
<span>Started {getRelativeTimeString(project.startedAt)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-slate-800/50 via-transparent to-transparent" />
|
||||
{project.submittedAt && project.repositoryUrl && (
|
||||
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-green-950/20 via-transparent to-transparent" />
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
74
src/components/HeroSection/HeroRoadmap.tsx
Normal file
74
src/components/HeroSection/HeroRoadmap.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import type { ResourceType } from '../../lib/resource-progress.ts';
|
||||
import { MarkFavorite } from '../FeaturedItems/MarkFavorite.tsx';
|
||||
|
||||
type ProgressRoadmapProps = {
|
||||
url: string;
|
||||
percentageDone: number;
|
||||
allowFavorite?: boolean;
|
||||
|
||||
resourceId: string;
|
||||
resourceType: ResourceType;
|
||||
resourceTitle: string;
|
||||
isFavorite?: boolean;
|
||||
|
||||
isTrackable?: boolean;
|
||||
isNew?: boolean;
|
||||
};
|
||||
|
||||
export function HeroRoadmap(props: ProgressRoadmapProps) {
|
||||
const {
|
||||
url,
|
||||
percentageDone,
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceTitle,
|
||||
isFavorite,
|
||||
allowFavorite = true,
|
||||
isTrackable = true,
|
||||
isNew = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
className={cn(
|
||||
'relative flex flex-col overflow-hidden rounded-md border p-3 text-sm text-slate-400 hover:text-slate-300',
|
||||
{
|
||||
'border-slate-800 bg-slate-900 hover:border-slate-600': isTrackable,
|
||||
'border-slate-700/50 bg-slate-800/50 hover:border-slate-600/70':
|
||||
!isTrackable,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<span title={resourceTitle} className="relative z-20 truncate">
|
||||
{resourceTitle}
|
||||
</span>
|
||||
|
||||
{isTrackable && (
|
||||
<span
|
||||
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
|
||||
style={{ width: `${percentageDone}%` }}
|
||||
></span>
|
||||
)}
|
||||
|
||||
{allowFavorite && (
|
||||
<MarkFavorite
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
favorite={isFavorite}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isNew && (
|
||||
<span className="absolute bottom-1.5 right-2 flex items-center rounded-br rounded-tl text-xs font-medium text-purple-300">
|
||||
<span className="mr-1.5 flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-purple-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-purple-500" />
|
||||
</span>
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
import type { UserProgressResponse } from './FavoriteRoadmaps';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon';
|
||||
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import { MapIcon, Users2 } from 'lucide-react';
|
||||
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { FeatureAnnouncement } from '../FeatureAnnouncement.tsx';
|
||||
|
||||
type ProgressRoadmapProps = {
|
||||
url: string;
|
||||
percentageDone: number;
|
||||
allowFavorite?: boolean;
|
||||
|
||||
resourceId: string;
|
||||
resourceType: ResourceType;
|
||||
resourceTitle: string;
|
||||
isFavorite?: boolean;
|
||||
};
|
||||
function HeroRoadmap(props: ProgressRoadmapProps) {
|
||||
const {
|
||||
url,
|
||||
percentageDone,
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceTitle,
|
||||
isFavorite,
|
||||
allowFavorite = true,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
className="relative flex flex-col overflow-hidden rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300"
|
||||
>
|
||||
<span className="relative z-20">{resourceTitle}</span>
|
||||
|
||||
<span
|
||||
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
|
||||
style={{ width: `${percentageDone}%` }}
|
||||
></span>
|
||||
|
||||
{allowFavorite && (
|
||||
<MarkFavorite
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
favorite={isFavorite}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
type ProgressTitleProps = {
|
||||
icon: any;
|
||||
isLoading?: boolean;
|
||||
title: string | ReactNode;
|
||||
};
|
||||
|
||||
export function HeroTitle(props: ProgressTitleProps) {
|
||||
const { isLoading = false, title, icon } = props;
|
||||
|
||||
return (
|
||||
<p className="mb-4 flex items-center text-sm text-gray-400">
|
||||
{!isLoading && icon}
|
||||
{isLoading && (
|
||||
<span className="mr-1.5">
|
||||
<Spinner />
|
||||
</span>
|
||||
)}
|
||||
{title}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
export type HeroTeamRoadmaps = Record<string, UserProgressResponse>;
|
||||
|
||||
type ProgressListProps = {
|
||||
progress: UserProgressResponse;
|
||||
customRoadmaps: UserProgressResponse;
|
||||
teamRoadmaps?: HeroTeamRoadmaps;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export function HeroRoadmaps(props: ProgressListProps) {
|
||||
const {
|
||||
teamRoadmaps = {},
|
||||
progress,
|
||||
isLoading = false,
|
||||
customRoadmaps,
|
||||
} = props;
|
||||
|
||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
||||
const [creatingRoadmapTeamId, setCreatingRoadmapTeamId] = useState<string>();
|
||||
|
||||
return (
|
||||
<div className="relative pb-12 pt-4 sm:pt-7">
|
||||
<p className="mb-7 mt-2 text-sm">
|
||||
<FeatureAnnouncement />
|
||||
</p>
|
||||
{isCreatingRoadmap && (
|
||||
<CreateRoadmapModal
|
||||
teamId={creatingRoadmapTeamId}
|
||||
onClose={() => {
|
||||
setIsCreatingRoadmap(false);
|
||||
setCreatingRoadmapTeamId(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<HeroTitle
|
||||
icon={
|
||||
(<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />) as any
|
||||
}
|
||||
isLoading={isLoading}
|
||||
title="Your progress and favorite roadmaps."
|
||||
/>
|
||||
}
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{progress.map((resource) => (
|
||||
<HeroRoadmap
|
||||
key={`${resource.resourceType}-${resource.resourceId}`}
|
||||
resourceId={resource.resourceId}
|
||||
resourceType={resource.resourceType}
|
||||
resourceTitle={resource.resourceTitle}
|
||||
isFavorite={resource.isFavorite}
|
||||
percentageDone={
|
||||
((resource.skipped + resource.done) / resource.total) * 100
|
||||
}
|
||||
url={
|
||||
resource.resourceType === 'roadmap'
|
||||
? `/${resource.resourceId}`
|
||||
: `/best-practices/${resource.resourceId}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
{
|
||||
<HeroTitle
|
||||
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
title="Your custom roadmaps"
|
||||
/>
|
||||
}
|
||||
|
||||
{customRoadmaps.length === 0 && (
|
||||
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
|
||||
You haven't created any custom roadmaps yet.{' '}
|
||||
<button
|
||||
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
|
||||
onClick={() => setIsCreatingRoadmap(true)}
|
||||
>
|
||||
Create one!
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{customRoadmaps.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{customRoadmaps.map((customRoadmap) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={customRoadmap.resourceId}
|
||||
resourceId={customRoadmap.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={customRoadmap.resourceTitle}
|
||||
percentageDone={
|
||||
((customRoadmap.skipped + customRoadmap.done) /
|
||||
customRoadmap.total) *
|
||||
100
|
||||
}
|
||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<CreateRoadmapButton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Object.keys(teamRoadmaps).map((teamName) => {
|
||||
const currentTeam: UserProgressResponse[0]['team'] =
|
||||
teamRoadmaps?.[teamName]?.[0]?.team;
|
||||
const roadmapsList = teamRoadmaps[teamName].filter(
|
||||
(roadmap) => !!roadmap.resourceTitle,
|
||||
);
|
||||
const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!);
|
||||
|
||||
return (
|
||||
<div className="mt-5" key={teamName}>
|
||||
{
|
||||
<HeroTitle
|
||||
icon={<Users2 className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
title={
|
||||
<>
|
||||
Team{' '}
|
||||
<a
|
||||
className="mx-1 font-medium underline underline-offset-2 transition-colors hover:text-gray-300"
|
||||
href={`/team/activity?t=${currentTeam?.id}`}
|
||||
>
|
||||
{teamName}
|
||||
</a>
|
||||
Roadmaps
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
{roadmapsList.length === 0 && (
|
||||
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
|
||||
Team does not have any roadmaps yet.{' '}
|
||||
{canManageTeam && (
|
||||
<button
|
||||
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
|
||||
onClick={() => {
|
||||
setCreatingRoadmapTeamId(currentTeam?.id);
|
||||
setIsCreatingRoadmap(true);
|
||||
}}
|
||||
>
|
||||
Create one!
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{roadmapsList.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{roadmapsList.map((customRoadmap) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={customRoadmap.resourceId}
|
||||
resourceId={customRoadmap.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={customRoadmap.resourceTitle}
|
||||
percentageDone={
|
||||
((customRoadmap.skipped + customRoadmap.done) /
|
||||
customRoadmap.total) *
|
||||
100
|
||||
}
|
||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{canManageTeam && (
|
||||
<CreateRoadmapButton
|
||||
teamId={currentTeam?.id}
|
||||
text="Create Team Roadmap"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/components/HeroSection/HeroTitle.tsx
Normal file
71
src/components/HeroSection/HeroTitle.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { ChevronDown, ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type HeroTitleProps = {
|
||||
icon: any;
|
||||
isLoading?: boolean;
|
||||
title: string | ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
isCollapsed?: boolean;
|
||||
onToggleCollapse?: () => void;
|
||||
isEmpty?: boolean;
|
||||
emptyTitle?: ReactNode;
|
||||
};
|
||||
|
||||
export function HeroTitle(props: HeroTitleProps) {
|
||||
const {
|
||||
isLoading = false,
|
||||
title,
|
||||
icon,
|
||||
rightContent,
|
||||
isCollapsed = false,
|
||||
onToggleCollapse,
|
||||
isEmpty = false,
|
||||
emptyTitle,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="flex items-center gap-0.5 text-sm text-gray-400">
|
||||
{!isLoading && icon}
|
||||
{isLoading && (
|
||||
<span className="mr-1.5">
|
||||
<Spinner />
|
||||
</span>
|
||||
)}
|
||||
{!isEmpty ? title : emptyTitle || title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isCollapsed && rightContent}
|
||||
|
||||
{!isLoading && !isEmpty && (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className={cn(
|
||||
'ml-2 inline-flex items-center gap-1 rounded-md bg-slate-800 py-0.5 pl-1 pr-1.5 text-xs uppercase tracking-wider text-slate-400 hover:bg-slate-700',
|
||||
{
|
||||
'bg-slate-800 text-slate-500 hover:bg-slate-800 hover:text-slate-400':
|
||||
!isCollapsed,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{isCollapsed && (
|
||||
<>
|
||||
<ChevronsUpDown className="h-3.5 w-3.5" /> Expand
|
||||
</>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<ChevronsDownUp className="h-3.5 w-3.5" /> Collapse
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const { class: className } = Astro.props;
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'container prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-balance prose-h2:text-3xl prose-h3:mt-2 prose-h3:scroll-mt-5 prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 prose-h2:sm:scroll-mt-10 prose-h3:sm:scroll-mt-10',
|
||||
'container prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-balance prose-h2:text-3xl prose-h3:mt-2 prose-h4:text-2xl prose-h3:scroll-mt-5 prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 prose-h2:sm:scroll-mt-10 prose-h3:sm:scroll-mt-10',
|
||||
className,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
---
|
||||
import { Menu } from 'lucide-react';
|
||||
import { AccountStreak } from '../AccountStreak/AccountStreak';
|
||||
import Icon from '../AstroIcon.astro';
|
||||
import { NavigationDropdown } from '../NavigationDropdown';
|
||||
import { AccountDropdown } from './AccountDropdown';
|
||||
import NewIndicator from './NewIndicator.astro';
|
||||
import { AccountStreak } from '../AccountStreak/AccountStreak';
|
||||
import { RoadmapDropdownMenu } from '../RoadmapDropdownMenu/RoadmapDropdownMenu';
|
||||
import { AccountDropdown } from './AccountDropdown';
|
||||
import { CourseAnnouncement } from '../SQLCourse/CourseAnnouncement';
|
||||
---
|
||||
|
||||
<CourseAnnouncement client:load />
|
||||
|
||||
<div class='bg-slate-900 py-5 text-white sm:py-8'>
|
||||
<nav class='container flex items-center justify-between'>
|
||||
<div class='flex items-center gap-5'>
|
||||
@@ -48,7 +49,7 @@ import { RoadmapDropdownMenu } from '../RoadmapDropdownMenu/RoadmapDropdownMenu'
|
||||
</a>
|
||||
<a
|
||||
href='/changelog'
|
||||
class='group relative text-blue-300 hover:text-white hidden md:block ml-0.5'
|
||||
class='group relative ml-0.5 hidden text-blue-300 hover:text-white md:block'
|
||||
>
|
||||
Changelog
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export function NavigationDropdown() {
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[90] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
||||
{
|
||||
'pointer-events-auto visible translate-y-2.5 opacity-100':
|
||||
$navigationDropdownOpen,
|
||||
|
||||
@@ -37,6 +37,9 @@ export function OnboardingNudge(props: OnboardingNudgeProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @TODO put it back once <CourseAnnouncement /> is removed
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -16,6 +16,8 @@ import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import { SelectLanguages } from './SelectLanguages.tsx';
|
||||
import type { ProjectFrontmatter } from '../../lib/project.ts';
|
||||
import { ProjectSolutionModal } from './ProjectSolutionModal.tsx';
|
||||
import { SortProjects } from './SortProjects.tsx';
|
||||
import { ProjectSolutionRow } from './ProjectSolutionRow';
|
||||
|
||||
export interface ProjectStatusDocument {
|
||||
_id?: string;
|
||||
@@ -57,11 +59,13 @@ type ListProjectSolutionsResponse = {
|
||||
type QueryParams = {
|
||||
p?: string;
|
||||
l?: string;
|
||||
s?: string;
|
||||
};
|
||||
|
||||
type PageState = {
|
||||
currentPage: number;
|
||||
language: string;
|
||||
sort: string;
|
||||
};
|
||||
|
||||
type ListProjectSolutionsProps = {
|
||||
@@ -69,30 +73,6 @@ type ListProjectSolutionsProps = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const submittedAlternatives = [
|
||||
'submitted their solution',
|
||||
'got it done',
|
||||
'submitted their take',
|
||||
'finished the project',
|
||||
'submitted their work',
|
||||
'completed the project',
|
||||
'got it done',
|
||||
'delivered their project',
|
||||
'handed in their solution',
|
||||
'provided their deliverables',
|
||||
'submitted their approach',
|
||||
'sent in their project',
|
||||
'presented their take',
|
||||
'shared their completed task',
|
||||
'submitted their approach',
|
||||
'completed it',
|
||||
'finalized their solution',
|
||||
'delivered their approach',
|
||||
'turned in their project',
|
||||
'submitted their final draft',
|
||||
'delivered their solution',
|
||||
];
|
||||
|
||||
export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
const { projectId, project: projectData } = props;
|
||||
|
||||
@@ -100,6 +80,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
const [pageState, setPageState] = useState<PageState>({
|
||||
currentPage: 0,
|
||||
language: '',
|
||||
sort: 'rating',
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -108,12 +89,17 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
ListProjectSolutionsResponse['data'][number] | null
|
||||
>(null);
|
||||
|
||||
const loadSolutions = async (page = 1, language: string = '') => {
|
||||
const loadSolutions = async (
|
||||
page = 1,
|
||||
language: string = '',
|
||||
sort: string = 'rating',
|
||||
) => {
|
||||
const { response, error } = await httpGet<ListProjectSolutionsResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-solutions/${projectId}`,
|
||||
{
|
||||
currPage: page,
|
||||
...(language ? { languages: language } : {}),
|
||||
sort,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -178,6 +164,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
setPageState({
|
||||
currentPage: +(queryParams.p || '1'),
|
||||
language: queryParams.l || '',
|
||||
sort: queryParams.s || 'rating',
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -187,17 +174,27 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageState.currentPage !== 1 || pageState.language !== '') {
|
||||
if (
|
||||
pageState.currentPage !== 1 ||
|
||||
pageState.language !== '' ||
|
||||
pageState.sort !== 'rating'
|
||||
) {
|
||||
setUrlParams({
|
||||
p: String(pageState.currentPage),
|
||||
l: pageState.language,
|
||||
s: pageState.sort,
|
||||
});
|
||||
} else {
|
||||
deleteUrlParam('p');
|
||||
deleteUrlParam('l');
|
||||
deleteUrlParam('s');
|
||||
}
|
||||
|
||||
loadSolutions(pageState.currentPage, pageState.language).finally(() => {
|
||||
loadSolutions(
|
||||
pageState.currentPage,
|
||||
pageState.language,
|
||||
pageState.sort,
|
||||
).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [pageState]);
|
||||
@@ -216,6 +213,13 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
|
||||
const selectedLanguage = pageState.language;
|
||||
|
||||
const setSelectedLanguage = (language: string) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
language: prev.language === language ? '' : language,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 overflow-hidden rounded-lg border bg-white p-3 sm:p-5">
|
||||
{leavingRoadmapModal}
|
||||
@@ -224,19 +228,32 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
<h1 className="mb-1 text-xl font-semibold">
|
||||
{projectData.title} Solutions
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">{projectData.description}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Solutions submitted by the community
|
||||
</p>
|
||||
</div>
|
||||
{!isLoading && (
|
||||
<SelectLanguages
|
||||
projectId={projectId}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onSelectLanguage={(language) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
language: prev.language === language ? '' : language,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<SortProjects
|
||||
selectedSort={pageState.sort}
|
||||
onSelectSort={(sort) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
sort,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<SelectLanguages
|
||||
projectId={projectId}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onSelectLanguage={(language) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
language: prev.language === language ? '' : language,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -245,73 +262,16 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
) : (
|
||||
<>
|
||||
<div className="flex min-h-[500px] flex-col divide-y divide-gray-100">
|
||||
{solutions?.data.map((solution, counter) => {
|
||||
const avatar = solution.user.avatar || '';
|
||||
return (
|
||||
<div
|
||||
key={solution._id}
|
||||
className="flex flex-col gap-2 py-2 text-sm text-gray-500"
|
||||
>
|
||||
<div className="flex flex-col justify-between gap-2 text-sm text-gray-500 sm:flex-row sm:items-center sm:gap-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<img
|
||||
src={
|
||||
avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png'
|
||||
}
|
||||
alt={solution.user.name}
|
||||
className="mr-0.5 h-7 w-7 rounded-full"
|
||||
/>
|
||||
<span className="font-medium text-black">
|
||||
{solution.user.name}
|
||||
</span>
|
||||
<span className="hidden sm:inline">
|
||||
{submittedAlternatives[
|
||||
counter % submittedAlternatives.length
|
||||
] || 'submitted their solution'}
|
||||
</span>{' '}
|
||||
<span className="flex-grow text-right text-gray-400 sm:flex-grow-0 sm:text-left sm:font-medium sm:text-black">
|
||||
{getRelativeTimeString(solution?.submittedAt!)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span className="flex shrink-0 overflow-hidden rounded-full border">
|
||||
<VoteButton
|
||||
icon={ThumbsUp}
|
||||
isActive={solution?.voteType === 'upvote'}
|
||||
count={solution.upvotes || 0}
|
||||
onClick={() => {
|
||||
handleSubmitVote(solution._id!, 'upvote');
|
||||
}}
|
||||
/>
|
||||
|
||||
<VoteButton
|
||||
icon={ThumbsDown}
|
||||
isActive={solution?.voteType === 'downvote'}
|
||||
count={solution.downvotes || 0}
|
||||
hideCount={true}
|
||||
onClick={() => {
|
||||
handleSubmitVote(solution._id!, 'downvote');
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<button
|
||||
className="ml-1 flex items-center gap-1 rounded-full border px-2 py-1 text-xs text-black transition-colors hover:border-black hover:bg-black hover:text-white"
|
||||
onClick={() => {
|
||||
setShowLeavingRoadmapModal(solution);
|
||||
}}
|
||||
>
|
||||
<GitHubIcon className="h-4 w-4 text-current" />
|
||||
Visit Solution
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{solutions?.data.map((solution, counter) => (
|
||||
<ProjectSolutionRow
|
||||
key={solution._id}
|
||||
solution={solution}
|
||||
counter={counter}
|
||||
onVote={handleSubmitVote}
|
||||
onVisitSolution={setShowLeavingRoadmapModal}
|
||||
onLanguageClick={setSelectedLanguage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(solutions?.totalPages || 0) > 1 && (
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { ArrowUpRight, ThumbsDown, ThumbsUp } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
import { ModalLoader } from '../UserProgress/ModalLoader';
|
||||
import { Modal } from '../Modal';
|
||||
import { httpGet, httpPost } from '../../lib/http';
|
||||
import {
|
||||
submittedAlternatives,
|
||||
type AllowedVoteType,
|
||||
} from './ListProjectSolutions';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import { ArrowUpRight, ThumbsDown, ThumbsUp, Trophy } from 'lucide-react';
|
||||
import { VoteButton } from './VoteButton';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
|
||||
import { httpGet, httpPost } from '../../lib/http';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { Modal } from '../Modal';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
|
||||
import { ModalLoader } from '../UserProgress/ModalLoader';
|
||||
import { type AllowedVoteType } from './ListProjectSolutions';
|
||||
import { VoteButton } from './VoteButton';
|
||||
|
||||
type UserProjectSolutionResponse = {
|
||||
id?: string;
|
||||
@@ -135,8 +132,12 @@ export function ProjectSolutionModal(props: ProjectSolutionModalProps) {
|
||||
bodyClassName={'h-auto'}
|
||||
>
|
||||
<div className="relative p-6">
|
||||
<h1 className="text-2xl text-balance mb-1 font-bold text-gray-900">{projectTitle}</h1>
|
||||
<p className="text-sm text-balance text-gray-600">{projectDescription}</p>
|
||||
<h1 className="mb-1 text-balance text-2xl font-bold text-gray-900">
|
||||
{projectTitle}
|
||||
</h1>
|
||||
<p className="text-balance text-sm text-gray-600">
|
||||
{projectDescription}
|
||||
</p>
|
||||
|
||||
<div className="my-5 rounded-lg bg-gray-100 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -150,7 +151,9 @@ export function ProjectSolutionModal(props: ProjectSolutionModalProps) {
|
||||
className="h-12 w-12 rounded-full border-2 border-white shadow-md"
|
||||
/>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">{solution?.user.name}'s Solution</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{solution?.user.name}'s Solution
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Submitted their solution{' '}
|
||||
{getRelativeTimeString(solution?.submittedAt!)}
|
||||
|
||||
137
src/components/Projects/ProjectSolutionRow.tsx
Normal file
137
src/components/Projects/ProjectSolutionRow.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { ThumbsDown, ThumbsUp } from 'lucide-react';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import { VoteButton } from './VoteButton';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
|
||||
import type {
|
||||
AllowedVoteType,
|
||||
ProjectStatusDocument,
|
||||
} from './ListProjectSolutions';
|
||||
|
||||
export const submittedAlternatives = [
|
||||
'submitted their solution',
|
||||
'got it done',
|
||||
'submitted their take',
|
||||
'finished the project',
|
||||
'submitted their work',
|
||||
'completed the project',
|
||||
'got it done',
|
||||
'delivered their project',
|
||||
'handed in their solution',
|
||||
'provided their deliverables',
|
||||
'submitted their approach',
|
||||
'sent in their project',
|
||||
'presented their take',
|
||||
'shared their completed task',
|
||||
'submitted their approach',
|
||||
'completed it',
|
||||
'finalized their solution',
|
||||
'delivered their approach',
|
||||
'turned in their project',
|
||||
'submitted their final draft',
|
||||
'delivered their solution',
|
||||
];
|
||||
|
||||
type ProjectSolutionRowProps = {
|
||||
solution: ProjectStatusDocument & {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
voteType?: AllowedVoteType | 'none';
|
||||
};
|
||||
counter: number;
|
||||
onVote: (solutionId: string, voteType: AllowedVoteType) => void;
|
||||
onVisitSolution: (solution: ProjectSolutionRowProps['solution']) => void;
|
||||
onLanguageClick?: (language: string) => void;
|
||||
};
|
||||
|
||||
export function ProjectSolutionRow(props: ProjectSolutionRowProps) {
|
||||
const { solution, counter, onVote, onVisitSolution, onLanguageClick } = props;
|
||||
|
||||
const avatar = solution.user.avatar || '';
|
||||
|
||||
return (
|
||||
<div className="group flex flex-col border-gray-100 px-3 py-2.5 text-sm hover:bg-gray-50/50 sm:flex-row sm:justify-between">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<img
|
||||
src={
|
||||
avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png'
|
||||
}
|
||||
alt={solution.user.name}
|
||||
className="h-7 w-7 flex-shrink-0 rounded-full sm:h-8 sm:w-8"
|
||||
/>
|
||||
<div className="min-w-0 flex-auto">
|
||||
<div className="flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5">
|
||||
<span className="max-w-[150px] truncate font-medium text-gray-900 sm:max-w-[180px]">
|
||||
{solution.user.name}
|
||||
</span>
|
||||
<span className="hidden truncate text-xs text-gray-500 sm:block sm:text-sm">
|
||||
{submittedAlternatives[counter % submittedAlternatives.length] ||
|
||||
'submitted their solution'}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs text-gray-400"
|
||||
title={new Date(solution?.submittedAt!).toLocaleString()}
|
||||
>
|
||||
· {getRelativeTimeString(solution?.submittedAt!)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5 flex gap-1.5">
|
||||
<div className="flex gap-1">
|
||||
<span className="flex shrink-0 overflow-hidden rounded-full border">
|
||||
<VoteButton
|
||||
icon={ThumbsUp}
|
||||
isActive={solution?.voteType === 'upvote'}
|
||||
count={solution.upvotes || 0}
|
||||
onClick={() => {
|
||||
onVote(solution._id!, 'upvote');
|
||||
}}
|
||||
/>
|
||||
|
||||
<VoteButton
|
||||
icon={ThumbsDown}
|
||||
isActive={solution?.voteType === 'downvote'}
|
||||
count={solution.downvotes || 0}
|
||||
hideCount={true}
|
||||
onClick={() => {
|
||||
onVote(solution._id!, 'downvote');
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-full border px-2 py-1 text-xs text-black transition-colors hover:border-black hover:bg-black hover:text-white"
|
||||
onClick={() => {
|
||||
onVisitSolution(solution);
|
||||
}}
|
||||
>
|
||||
<GitHubIcon className="h-3.5 w-3.5 text-current" />
|
||||
<span>Visit Solution</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5 hidden sm:mt-0 sm:block sm:pl-4">
|
||||
{solution.languages && solution.languages.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{solution.languages.slice(0, 2).map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => onLanguageClick?.(lang)}
|
||||
className="inline-flex items-center rounded-md border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-700 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900"
|
||||
>
|
||||
{lang}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { ChevronDown, X } from 'lucide-react';
|
||||
import { ChevronDown, Search, X } from 'lucide-react';
|
||||
|
||||
type SelectLanguagesProps = {
|
||||
projectId: string;
|
||||
@@ -14,10 +14,44 @@ export function SelectLanguages(props: SelectLanguagesProps) {
|
||||
const { projectId, onSelectLanguage, selectedLanguage } = props;
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const optionsRef = useRef<HTMLDivElement>(null);
|
||||
const toast = useToast();
|
||||
|
||||
const [distinctLanguages, setDistinctLanguages] = useState<string[]>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
|
||||
const filteredLanguages = distinctLanguages.filter((language) =>
|
||||
language.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
// Handle scrolling of highlighted option into view
|
||||
useEffect(() => {
|
||||
if (!isOpen || !optionsRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = optionsRef.current.getElementsByTagName('button');
|
||||
const highlightedOption = options[highlightedIndex];
|
||||
if (!highlightedOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = optionsRef.current.getBoundingClientRect();
|
||||
const optionRect = highlightedOption.getBoundingClientRect();
|
||||
|
||||
const isAbove = optionRect.top < containerRect.top;
|
||||
const isBelow = optionRect.bottom > containerRect.bottom;
|
||||
|
||||
if (isAbove || isBelow) {
|
||||
highlightedOption.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
}, [highlightedIndex, isOpen]);
|
||||
|
||||
const loadDistinctLanguages = async () => {
|
||||
const { response, error } = await httpGet<string[]>(
|
||||
@@ -34,53 +68,124 @@ export function SelectLanguages(props: SelectLanguagesProps) {
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadDistinctLanguages().finally(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative flex">
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{selectedLanguage || 'Select Language'}
|
||||
useEffect(() => {
|
||||
if (isOpen && searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
{selectedLanguage && (
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) =>
|
||||
prev >= filteredLanguages.length - 1 ? 0 : prev + 1,
|
||||
);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) =>
|
||||
prev <= 0 ? filteredLanguages.length - 1 : prev - 1,
|
||||
);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (filteredLanguages[highlightedIndex]) {
|
||||
onSelectLanguage(filteredLanguages[highlightedIndex]);
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-shrink-0">
|
||||
<div className="relative">
|
||||
<button
|
||||
className="ml-1 text-red-500 text-xs border border-red-500 rounded-md px-2 py-1"
|
||||
onClick={() => onSelectLanguage('')}
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
Clear
|
||||
{selectedLanguage || 'Select Language'}
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{selectedLanguage && (
|
||||
<button
|
||||
className="absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-white hover:bg-red-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectLanguage('');
|
||||
}}
|
||||
>
|
||||
<X className="size-3" strokeWidth={2.5} />
|
||||
<span className="sr-only">Clear selection</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full z-10 w-full min-w-[200px] max-w-[200px] translate-y-1.5 overflow-hidden rounded-md border border-gray-300 bg-white p-1 shadow-lg"
|
||||
ref={dropdownRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{distinctLanguages.map((language) => {
|
||||
const isSelected = selectedLanguage === language;
|
||||
<div className="relative mb-1 px-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
className="w-full rounded-md border border-gray-200 py-1.5 pl-9 pr-3 text-sm focus:border-gray-300 focus:outline-none"
|
||||
placeholder="Search languages..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setHighlightedIndex(0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div ref={optionsRef} className="max-h-[200px] overflow-y-auto">
|
||||
{filteredLanguages.map((language, index) => {
|
||||
const isSelected = selectedLanguage === language;
|
||||
const isHighlighted = index === highlightedIndex;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={language}
|
||||
className="flex w-full items-center rounded-md px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 aria-selected:bg-gray-100"
|
||||
onClick={() => {
|
||||
onSelectLanguage(language);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
aria-selected={isSelected}
|
||||
>
|
||||
{language}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<button
|
||||
key={language}
|
||||
className={`flex w-full items-center rounded-md px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 aria-selected:bg-gray-100 ${
|
||||
isHighlighted ? 'bg-gray-100' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
onSelectLanguage(language);
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
}}
|
||||
aria-selected={isSelected}
|
||||
>
|
||||
{language}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filteredLanguages.length === 0 && (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
No languages found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
66
src/components/Projects/SortProjects.tsx
Normal file
66
src/components/Projects/SortProjects.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
type SortOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const sortOptions: SortOption[] = [
|
||||
{ label: 'Latest First', value: 'latest' },
|
||||
{ label: 'Oldest First', value: 'oldest' },
|
||||
{ label: 'Highest Rating', value: 'rating' },
|
||||
];
|
||||
|
||||
type SortProjectsProps = {
|
||||
selectedSort: string;
|
||||
onSelectSort: (sort: string) => void;
|
||||
};
|
||||
|
||||
export function SortProjects(props: SortProjectsProps) {
|
||||
const { selectedSort, onSelectSort } = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
const selectedOption =
|
||||
sortOptions.find((option) => option.value === selectedSort) ||
|
||||
sortOptions[0];
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0" ref={dropdownRef}>
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{selectedOption.label}
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full z-10 mt-1.5 min-w-[150px] overflow-hidden rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
<div className="py-1">
|
||||
{sortOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
className={`flex w-full items-center px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 ${
|
||||
selectedSort === option.value ? 'bg-gray-100' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
onSelectSort(option.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -287,28 +287,32 @@ export function ProjectStepper(props: ProjectStepperProps) {
|
||||
number={2}
|
||||
/>
|
||||
|
||||
<span className="text-gray-600 sm:hidden">·</span>
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm sm:hidden',
|
||||
isCopied ? 'text-green-500' : 'text-gray-600',
|
||||
)}
|
||||
onClick={() => {
|
||||
copyText(projectSolutionUrl);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckIcon additionalClasses="h-3 w-3" />
|
||||
URL Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share className="h-3 w-3 stroke-[2.5px]" />
|
||||
Share your Solution
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{activeStep > 1 && (
|
||||
<>
|
||||
<span className="text-gray-600 sm:hidden">·</span>
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm sm:hidden',
|
||||
isCopied ? 'text-green-500' : 'text-gray-600',
|
||||
)}
|
||||
onClick={() => {
|
||||
copyText(projectSolutionUrl);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckIcon additionalClasses="h-3 w-3" />
|
||||
URL Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share className="h-3 w-3 stroke-[2.5px]" />
|
||||
Share your Solution
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<StepperStepSeparator isActive={activeStep > 1} />
|
||||
<MilestoneStep
|
||||
|
||||
@@ -103,7 +103,7 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
|
||||
{question.isLongAnswer && (
|
||||
<div
|
||||
className={`qa-answer prose prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] flex-grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:!mb-6 prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
|
||||
className={`qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] flex-grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:!mb-6 prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(question.answer, false),
|
||||
}}
|
||||
|
||||
@@ -149,6 +149,11 @@ const { frontmatter: guideFrontmatter, author } = questionGroup;
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{questionGroup.ending && (
|
||||
<div class='mb-5'>
|
||||
<div set:html={markdownToHtml(questionGroup.ending, false)} />
|
||||
</div>
|
||||
)}
|
||||
</MarkdownFile>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
20
src/components/ReactIcons/HackerNewsIcon.tsx
Normal file
20
src/components/ReactIcons/HackerNewsIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
interface HackerNewsIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HackerNewsIcon(props: HackerNewsIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
fill="currentColor"
|
||||
className={cn('h-[26px] w-[26px]', className)}
|
||||
>
|
||||
<path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM21.2 229.2H21c.1-.1.2-.3.3-.4 0 .1 0 .3-.1.4zm218 53.9V384h-31.4V281.3L128 128h37.3c52.5 98.3 49.2 101.2 59.3 125.6 12.3-27 5.8-24.4 60.6-125.6H320l-80.8 155.1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
20
src/components/ReactIcons/RedditIcon.tsx
Normal file
20
src/components/ReactIcons/RedditIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
interface RedditIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RedditIcon(props: RedditIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
fill="currentColor"
|
||||
className={cn('h-[26px] w-[26px]', className)}
|
||||
>
|
||||
<path d="M283.2 345.5c2.7 2.7 2.7 6.8 0 9.2-24.5 24.5-93.8 24.6-118.4 0-2.7-2.4-2.7-6.5 0-9.2 2.4-2.4 6.5-2.4 8.9 0 18.7 19.2 81 19.6 100.5 0 2.4-2.3 6.6-2.3 9 0zm-91.3-53.8c0-14.9-11.9-26.8-26.5-26.8a26.67 26.67 0 0 0-26.8 26.8c0 14.6 11.9 26.5 26.8 26.5 14.6 0 26.5-11.9 26.5-26.5zm90.7-26.8c-14.6 0-26.5 11.9-26.5 26.8 0 14.6 11.9 26.5 26.5 26.5 14.9 0 26.8-11.9 26.8-26.5a26.67 26.67 0 0 0-26.8-26.8zM448 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h352c26.5 0 48 21.5 48 48zm-99.7 140.6c-10.1 0-19 4.2-25.6 10.7-24.1-16.7-56.5-27.4-92.5-28.6l18.7-84.2 59.5 13.4c0 14.6 11.9 26.5 26.5 26.5 14.9 0 26.8-12.2 26.8-26.8s-11.9-26.8-26.8-26.8c-10.4 0-19.3 6.2-23.8 14.9l-65.7-14.6c-3.3-.9-6.5 1.5-7.4 4.8l-20.5 92.8c-35.7 1.5-67.8 12.2-91.9 28.9-6.5-6.8-15.8-11-25.9-11-37.5 0-49.8 50.4-15.5 67.5-1.2 5.4-1.8 11-1.8 16.7 0 56.5 63.7 102.3 141.9 102.3 78.5 0 142.2-45.8 142.2-102.3 0-5.7-.6-11.6-2.1-17 33.6-17.2 21.2-67.2-16.1-67.2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
28
src/components/ReactIcons/RoadmapLogo.tsx
Normal file
28
src/components/ReactIcons/RoadmapLogo.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
type RoadmapLogoIconProps = SVGProps<SVGSVGElement> & {
|
||||
color?: 'white' | 'black';
|
||||
};
|
||||
|
||||
export function RoadmapLogoIcon(props: RoadmapLogoIconProps) {
|
||||
const { color = 'white', ...rest } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 283 283"
|
||||
{...rest}
|
||||
>
|
||||
<path
|
||||
fill={color === 'black' ? '#000' : '#fff'}
|
||||
d="M0 39C0 17.46 17.46 0 39 0h205c21.539 0 39 17.46 39 39v205c0 21.539-17.461 39-39 39H39c-21.54 0-39-17.461-39-39V39Z"
|
||||
/>
|
||||
<path
|
||||
fill={color === 'black' ? '#fff' : '#000'}
|
||||
d="M121.215 210.72c-1.867.56-4.854 1.12-8.96 1.68-3.92.56-8.027.84-12.32.84-4.107 0-7.84-.28-11.2-.84-3.174-.56-5.88-1.68-8.12-3.36s-4.014-3.92-5.32-6.72c-1.12-2.987-1.68-6.813-1.68-11.48v-84c0-4.293.746-7.933 2.24-10.92 1.68-3.173 4.013-5.973 7-8.4s6.626-4.573 10.92-6.44c4.48-2.053 9.24-3.827 14.28-5.32a106.176 106.176 0 0 1 15.68-3.36 95.412 95.412 0 0 1 16.24-1.4c8.96 0 16.053 1.773 21.28 5.32 5.226 3.36 7.84 8.96 7.84 16.8 0 2.613-.374 5.227-1.12 7.84-.747 2.427-1.68 4.667-2.8 6.72a133.1 133.1 0 0 0-12.04.56c-4.107.373-8.12.933-12.04 1.68s-7.654 1.587-11.2 2.52c-3.36.747-6.254 1.68-8.68 2.8v95.48zm45.172-22.4c0-7.84 2.426-14.373 7.28-19.6s11.48-7.84 19.88-7.84 15.026 2.613 19.88 7.84 7.28 11.76 7.28 19.6-2.427 14.373-7.28 19.6-11.48 7.84-19.88 7.84-15.027-2.613-19.88-7.84-7.28-11.76-7.28-19.6z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function TwitterIcon(props: TwitterIconProps) {
|
||||
<rect width="23" height="23" rx="3" fill={boxColor} />
|
||||
<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="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps';
|
||||
import { SelectionButton } from './SelectionButton';
|
||||
import type { UserProgressResponse } from '../Roadmaps/RoadmapsPage';
|
||||
|
||||
type RoadmapSelectProps = {
|
||||
selectedRoadmaps: string[];
|
||||
|
||||
@@ -17,7 +17,7 @@ const links = [
|
||||
isHighlighted: true,
|
||||
},
|
||||
{
|
||||
link: '/ai/explore',
|
||||
link: '/ai',
|
||||
label: 'AI Roadmaps',
|
||||
description: 'Generate roadmaps with AI',
|
||||
Icon: Sparkles,
|
||||
@@ -64,7 +64,7 @@ export function RoadmapDropdownMenu() {
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-2xl transition-all duration-100',
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[90] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-2xl transition-all duration-100',
|
||||
{
|
||||
'pointer-events-auto visible translate-y-2.5 opacity-100':
|
||||
$roadmapsDropdownOpen,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MarkFavorite } from './FeaturedItems/MarkFavorite';
|
||||
import { type RoadmapFrontmatter } from '../lib/roadmap';
|
||||
import { ShareRoadmapButton } from './ShareRoadmapButton';
|
||||
import { DownloadRoadmapButton } from './DownloadRoadmapButton';
|
||||
|
||||
import { CourseAnnouncement } from './SQLCourse/CourseAnnouncement';
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
@@ -10,8 +10,27 @@ import {
|
||||
} from '../../lib/browser.ts';
|
||||
import { RoadmapCard } from './RoadmapCard.tsx';
|
||||
import { httpGet } from '../../lib/http.ts';
|
||||
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps.tsx';
|
||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
|
||||
|
||||
export type UserProgressResponse = {
|
||||
resourceId: string;
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceTitle: string;
|
||||
isFavorite: boolean;
|
||||
done: number;
|
||||
learning: number;
|
||||
skipped: number;
|
||||
total: number;
|
||||
updatedAt: Date;
|
||||
isCustomResource: boolean;
|
||||
roadmapSlug?: string;
|
||||
team?: {
|
||||
name: string;
|
||||
id: string;
|
||||
role: AllowedMemberRoles;
|
||||
};
|
||||
}[];
|
||||
|
||||
const groupNames = [
|
||||
'Absolute Beginners',
|
||||
@@ -238,6 +257,12 @@ const groups: GroupType[] = [
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Cloudflare',
|
||||
link: '/cloudflare',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Linux',
|
||||
link: '/linux',
|
||||
|
||||
67
src/components/SQLCourse/AccountButton.tsx
Normal file
67
src/components/SQLCourse/AccountButton.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import {
|
||||
courseProgressOptions
|
||||
} from '../../queries/course-progress';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup';
|
||||
import { BuyButton, SQL_COURSE_SLUG } from './BuyButton';
|
||||
|
||||
export function AccountButton() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery(
|
||||
courseProgressOptions(SQL_COURSE_SLUG),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
}, []);
|
||||
|
||||
const buttonClasses =
|
||||
'rounded-full px-5 py-2 text-base font-medium text-yellow-700 hover:text-yellow-500 transition-colors';
|
||||
|
||||
const hasEnrolled = !!courseProgress?.enrolledAt;
|
||||
const loginModal = (
|
||||
<CourseLoginPopup
|
||||
checkoutAfterLogin={false}
|
||||
onClose={() => {
|
||||
setShowLoginModal(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isVisible || isLoadingCourseProgress) {
|
||||
return <button className={`${buttonClasses} opacity-0`}>...</button>;
|
||||
}
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowLoginModal(true)}
|
||||
className={`${buttonClasses} animate-fade-in`}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
{showLoginModal && loginModal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasEnrolled) {
|
||||
return <BuyButton variant="top-nav" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`${import.meta.env.PUBLIC_COURSE_APP_URL}/${SQL_COURSE_SLUG}`}
|
||||
className={`${buttonClasses} animate-fade-in`}
|
||||
>
|
||||
Start Learning
|
||||
</a>
|
||||
);
|
||||
}
|
||||
40
src/components/SQLCourse/AuthorQuoteMessage.tsx
Normal file
40
src/components/SQLCourse/AuthorQuoteMessage.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { QuoteIcon } from 'lucide-react';
|
||||
|
||||
export function AuthorQuoteMessage() {
|
||||
return (
|
||||
<div className="mx-auto mt-14 max-w-2xl sm:mt-20">
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-yellow-500/10 via-yellow-400/5 to-yellow-300/10 p-6 sm:p-10">
|
||||
<div className="relative">
|
||||
<p className="mb-6 text-base sm:text-xl leading-relaxed text-zinc-200">
|
||||
"As someone who has worked extensively with databases throughout my
|
||||
career, I know firsthand how crucial SQL skills are. I've created
|
||||
this course to share the practical knowledge that has helped me
|
||||
build and scale data systems at various companies."
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-4 border-t border-yellow-500/20 pt-6">
|
||||
<img
|
||||
src="https://assets.roadmap.sh/guest/kamran-lqjta.jpeg"
|
||||
alt="Kamran Ahmed"
|
||||
className="size-14 rounded-full ring-2 ring-yellow-500/20"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-medium text-yellow-500">Kamran Ahmed</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Founder roadmap.sh <span className="mx-1 sm:inline hidden">·</span>
|
||||
<a
|
||||
href="https://twitter.com/kamrify"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-0.5 text-yellow-500/80 underline underline-offset-4 hover:text-yellow-500"
|
||||
>
|
||||
@kamrify
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
256
src/components/SQLCourse/BuyButton.tsx
Normal file
256
src/components/SQLCourse/BuyButton.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import {
|
||||
COURSE_PURCHASE_PARAM,
|
||||
COURSE_PURCHASE_SUCCESS_PARAM,
|
||||
isLoggedIn,
|
||||
} from '../../lib/jwt';
|
||||
import { coursePriceOptions } from '../../queries/billing';
|
||||
import { courseProgressOptions } from '../../queries/course-progress';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { httpPost } from '../../lib/query-http';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
|
||||
export const SQL_COURSE_SLUG = 'sql';
|
||||
|
||||
type CreateCheckoutSessionBody = {
|
||||
courseId: string;
|
||||
success?: string;
|
||||
cancel?: string;
|
||||
};
|
||||
|
||||
type CreateCheckoutSessionResponse = {
|
||||
checkoutUrl: string;
|
||||
};
|
||||
|
||||
type BuyButtonProps = {
|
||||
variant?: 'main' | 'floating' | 'top-nav';
|
||||
};
|
||||
|
||||
export function BuyButton(props: BuyButtonProps) {
|
||||
const { variant = 'main' } = props;
|
||||
|
||||
const [isLoginPopupOpen, setIsLoginPopupOpen] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
const { data: coursePricing, isLoading: isLoadingCourse } = useQuery(
|
||||
coursePriceOptions({ courseSlug: SQL_COURSE_SLUG }),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery(
|
||||
courseProgressOptions(SQL_COURSE_SLUG),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: createCheckoutSession,
|
||||
isPending: isCreatingCheckoutSession,
|
||||
} = useMutation(
|
||||
{
|
||||
mutationFn: (body: CreateCheckoutSessionBody) => {
|
||||
return httpPost<CreateCheckoutSessionResponse>(
|
||||
'/v1-create-checkout-session',
|
||||
body,
|
||||
);
|
||||
},
|
||||
onMutate: () => {
|
||||
toast.loading('Creating checkout session...');
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (!window.gtag) {
|
||||
window.location.href = data.checkoutUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
window?.fireEvent({
|
||||
action: `${SQL_COURSE_SLUG}_begin_checkout`,
|
||||
category: 'course',
|
||||
label: `${SQL_COURSE_SLUG} Course Checkout Started`,
|
||||
callback: () => {
|
||||
window.location.href = data.checkoutUrl;
|
||||
},
|
||||
});
|
||||
|
||||
// Hacky way to make sure that we redirect in case
|
||||
// GA was blocked or not able to redirect the user.
|
||||
setTimeout(() => {
|
||||
window.location.href = data.checkoutUrl;
|
||||
}, 3000);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error(error?.message || 'Failed to create checkout session');
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = getUrlParams();
|
||||
const shouldTriggerPurchase = urlParams[COURSE_PURCHASE_PARAM] === '1';
|
||||
if (shouldTriggerPurchase) {
|
||||
deleteUrlParam(COURSE_PURCHASE_PARAM);
|
||||
initPurchase();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = getUrlParams();
|
||||
const param = urlParams?.[COURSE_PURCHASE_SUCCESS_PARAM];
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
|
||||
const success = param === '1';
|
||||
|
||||
if (success) {
|
||||
window?.fireEvent({
|
||||
action: `${SQL_COURSE_SLUG}_purchase_complete`,
|
||||
category: 'course',
|
||||
label: `${SQL_COURSE_SLUG} Course Purchase Completed`,
|
||||
});
|
||||
} else {
|
||||
window?.fireEvent({
|
||||
action: `${SQL_COURSE_SLUG}_purchase_canceled`,
|
||||
category: 'course',
|
||||
label: `${SQL_COURSE_SLUG} Course Purchase Canceled`,
|
||||
});
|
||||
}
|
||||
|
||||
deleteUrlParam(COURSE_PURCHASE_SUCCESS_PARAM);
|
||||
}, []);
|
||||
|
||||
const isLoadingPricing =
|
||||
isLoadingCourse || !coursePricing || isLoadingCourseProgress;
|
||||
const isAlreadyEnrolled = !!courseProgress?.enrolledAt;
|
||||
|
||||
function initPurchase() {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
createCheckoutSession({
|
||||
courseId: SQL_COURSE_SLUG,
|
||||
success: `/courses/${SQL_COURSE_SLUG}?${COURSE_PURCHASE_SUCCESS_PARAM}=1`,
|
||||
cancel: `/courses/${SQL_COURSE_SLUG}?${COURSE_PURCHASE_SUCCESS_PARAM}=0`,
|
||||
});
|
||||
}
|
||||
|
||||
function onBuyClick() {
|
||||
if (!isLoggedIn()) {
|
||||
setIsLoginPopupOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasEnrolled = !!courseProgress?.enrolledAt;
|
||||
if (hasEnrolled) {
|
||||
window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/${SQL_COURSE_SLUG}`;
|
||||
return;
|
||||
}
|
||||
|
||||
initPurchase();
|
||||
}
|
||||
|
||||
const courseLoginPopup = isLoginPopupOpen && (
|
||||
<CourseLoginPopup onClose={() => setIsLoginPopupOpen(false)} />
|
||||
);
|
||||
|
||||
if (variant === 'main') {
|
||||
return (
|
||||
<div className="relative flex w-full flex-col items-center gap-2 md:w-auto">
|
||||
{courseLoginPopup}
|
||||
<button
|
||||
onClick={onBuyClick}
|
||||
disabled={isLoadingPricing}
|
||||
className={cn(
|
||||
'group relative inline-flex w-full min-w-[235px] items-center justify-center overflow-hidden rounded-xl bg-gradient-to-r from-yellow-500 to-yellow-300 px-8 py-3 text-base font-semibold text-black transition-all duration-300 ease-out hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(234,179,8,0.4)] focus:outline-none active:ring-0 md:w-auto md:rounded-full md:text-lg',
|
||||
(isLoadingPricing || isCreatingCheckoutSession) &&
|
||||
'striped-loader-yellow pointer-events-none scale-105 bg-yellow-500',
|
||||
)}
|
||||
>
|
||||
{isLoadingPricing ? (
|
||||
<span className="relative flex items-center gap-2"> </span>
|
||||
) : isAlreadyEnrolled ? (
|
||||
<span className="relative flex items-center gap-2">
|
||||
Start Learning
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative flex items-center gap-2">
|
||||
Buy now for{' '}
|
||||
{coursePricing?.isEligibleForDiscount ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="hidden text-base line-through opacity-75 md:inline">
|
||||
${coursePricing?.fullPrice}
|
||||
</span>
|
||||
<span className="text-base md:text-xl">
|
||||
${coursePricing?.regionalPrice}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>${coursePricing?.regionalPrice}</span>
|
||||
)}
|
||||
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!isLoadingPricing && (
|
||||
<span className="absolute top-full translate-y-2.5 text-sm text-yellow-400">
|
||||
Lifetime access <span className="mx-1">·</span> Free updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'top-nav') {
|
||||
return (
|
||||
<button
|
||||
onClick={onBuyClick}
|
||||
disabled={isLoadingPricing}
|
||||
className={`animate-fade-in rounded-full px-5 py-2 text-base font-medium text-yellow-700 transition-colors hover:text-yellow-500`}
|
||||
>
|
||||
Purchase Course
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center gap-2">
|
||||
{courseLoginPopup}
|
||||
<button
|
||||
onClick={onBuyClick}
|
||||
disabled={isLoadingPricing}
|
||||
className={cn(
|
||||
'group relative inline-flex min-w-[220px] items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-yellow-500 to-yellow-300 px-8 py-2 font-medium text-black transition-all duration-300 ease-out hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(234,179,8,0.4)] focus:outline-none',
|
||||
(isLoadingPricing || isCreatingCheckoutSession) &&
|
||||
'striped-loader-yellow pointer-events-none bg-yellow-500',
|
||||
)}
|
||||
>
|
||||
{isLoadingPricing ? (
|
||||
<span className="relative flex items-center gap-2"> </span>
|
||||
) : isAlreadyEnrolled ? (
|
||||
<span className="relative flex items-center gap-2">
|
||||
Start Learning
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative flex items-center gap-2">
|
||||
Buy Now ${coursePricing?.regionalPrice}
|
||||
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!isLoadingPricing && !isAlreadyEnrolled && (
|
||||
<span className="top-full text-sm text-yellow-400">
|
||||
Lifetime access <span className="mx-1">·</span> Free updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
145
src/components/SQLCourse/ChapterRow.tsx
Normal file
145
src/components/SQLCourse/ChapterRow.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { ChevronDown, BookIcon, CodeIcon, FileQuestion, MessageCircleQuestionIcon, CircleDot } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type ChapterRowProps = {
|
||||
counter: number;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
lessonCount: number;
|
||||
challengeCount: number;
|
||||
isExpandable?: boolean;
|
||||
className?: string;
|
||||
lessons?: { title: string; type: 'lesson' | 'challenge' | 'quiz' }[];
|
||||
};
|
||||
|
||||
export function ChapterRow(props: ChapterRowProps) {
|
||||
const {
|
||||
counter,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
lessonCount,
|
||||
challengeCount,
|
||||
isExpandable = true,
|
||||
className,
|
||||
lessons = [],
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
const regularLessons = lessons.filter((l) => l.type === 'lesson');
|
||||
const challenges = lessons.filter((l) =>
|
||||
['challenge', 'quiz'].includes(l.type),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
setIsExpanded(!isMobile);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group relative select-none overflow-hidden', className)}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => isExpandable && setIsExpanded(!isExpanded)}
|
||||
className={cn(
|
||||
'relative rounded-xl border border-zinc-800 bg-zinc-800 p-6',
|
||||
'bg-gradient-to-br from-zinc-900/90 via-zinc-900/70 to-zinc-900/50',
|
||||
!isExpanded &&
|
||||
'hover:bg-gradient-to-br hover:from-zinc-900/95 hover:via-zinc-900/80 hover:to-zinc-900/60',
|
||||
!isExpanded &&
|
||||
'hover:cursor-pointer hover:shadow-[0_0_30px_rgba(0,0,0,0.2)]',
|
||||
isExpanded && 'rounded-b-none border-b-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="hidden flex-shrink-0 md:block">
|
||||
<div className="rounded-full bg-yellow-500/10 p-3">{icon}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow">
|
||||
<h3 className="text-xl font-semibold tracking-wide text-white">
|
||||
<span className="inline text-gray-500 md:hidden">
|
||||
{counter}.{' '}
|
||||
</span>
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-2 text-zinc-400">{description}</p>
|
||||
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-zinc-500">
|
||||
<span>{lessonCount} Lessons</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-zinc-500">
|
||||
<span>{challengeCount} Challenges</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpandable && (
|
||||
<div className="flex-shrink-0 rounded-full bg-zinc-800/80 p-2 text-zinc-400 group-hover:bg-zinc-800 group-hover:text-yellow-500">
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 transition-transform',
|
||||
isExpanded ? 'rotate-180' : '',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="rounded-b-xl border border-t-0 border-zinc-800 bg-gradient-to-br from-zinc-900/50 via-zinc-900/30 to-zinc-900/20">
|
||||
<div className="grid grid-cols-1 divide-zinc-800 md:grid-cols-2 md:divide-x">
|
||||
{regularLessons.length > 0 && (
|
||||
<div className="p-6 pb-0 md:pb-6">
|
||||
<h4 className="mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500">
|
||||
Lessons
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{regularLessons.map((lesson, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 text-zinc-400 hover:text-yellow-500"
|
||||
>
|
||||
<BookIcon className="h-4 w-4" />
|
||||
<span>{lesson.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{challenges.length > 0 && (
|
||||
<div className="p-6">
|
||||
<h4 className="mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500">
|
||||
Exercises
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{challenges.map((challenge, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 text-zinc-400 hover:text-yellow-500"
|
||||
>
|
||||
{challenge.type === 'challenge' ? (
|
||||
<CodeIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CircleDot className="h-4 w-4" />
|
||||
)}
|
||||
<span>{challenge.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
src/components/SQLCourse/CourseAnnouncement.tsx
Normal file
43
src/components/SQLCourse/CourseAnnouncement.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Database, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function CourseAnnouncement() {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-[91]">
|
||||
<a
|
||||
href="/courses/sql"
|
||||
className="flex items-center bg-yellow-400 py-1.5"
|
||||
>
|
||||
<span className="container mx-auto flex items-center justify-start sm:justify-center gap-2 text-center sm:gap-4">
|
||||
<span className="flex items-center gap-1.5 text-xs font-medium text-black md:text-base">
|
||||
<Database className="hidden h-4 w-4 flex-shrink-0 text-black sm:block" />
|
||||
<span className="hidden sm:block">
|
||||
Master SQL with our new paid course
|
||||
</span>
|
||||
<span className="block sm:hidden">Announcing our SQL course</span>
|
||||
</span>
|
||||
<span className="items-center gap-1.5 rounded-full bg-black px-2 py-0.5 text-sm text-xs font-medium uppercase tracking-wide text-white hover:bg-zinc-800 sm:px-3 sm:py-1">
|
||||
<span className="mr-1.5 hidden sm:inline">Start Learning</span>
|
||||
<span className="mr-1.5 inline sm:hidden">Visit</span>
|
||||
<span className="">→</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 rounded-lg px-1.5 py-1.5 text-gray-500 hover:bg-yellow-500 hover:text-gray-700"
|
||||
onClick={(e) => {
|
||||
setIsVisible(false);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/components/SQLCourse/CourseAuthor.tsx
Normal file
25
src/components/SQLCourse/CourseAuthor.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
export function CourseAuthor() {
|
||||
return (
|
||||
<div className="mt-8 w-full max-w-3xl space-y-4">
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<img
|
||||
src="https://assets.roadmap.sh/guest/kamran-lqjta.jpeg"
|
||||
className="size-12 rounded-full bg-yellow-500/10 md:size-16"
|
||||
alt="Kamran Ahmed"
|
||||
/>
|
||||
<a
|
||||
href="https://twitter.com/kamrify"
|
||||
target="_blank"
|
||||
className="flex flex-col"
|
||||
>
|
||||
<span className="text-lg font-medium text-zinc-200 md:text-2xl">
|
||||
Kamran Ahmed
|
||||
</span>
|
||||
<span className="text-sm text-zinc-500 md:text-lg">
|
||||
Software Engineer
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
src/components/SQLCourse/CourseFeature.tsx
Normal file
94
src/components/SQLCourse/CourseFeature.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { MinusIcon, PlusIcon, type LucideIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type CourseFeatureProps = {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
description: string;
|
||||
imgUrl?: string;
|
||||
};
|
||||
|
||||
export function CourseFeature(props: CourseFeatureProps) {
|
||||
const { title, icon: Icon, description, imgUrl } = props;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isZoomed, setIsZoomed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
if (isZoomed) {
|
||||
setIsZoomed(false);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, [isZoomed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZoomed && (
|
||||
<div
|
||||
onClick={() => {
|
||||
setIsZoomed(false);
|
||||
setIsExpanded(false);
|
||||
}}
|
||||
className="fixed inset-0 z-[999] flex cursor-zoom-out items-center justify-center bg-black bg-opacity-75"
|
||||
>
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt={title}
|
||||
className="max-h-[50%] max-w-[90%] rounded-xl object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-10 bg-black/70 opacity-100 transition-opacity duration-200 ease-out',
|
||||
{
|
||||
'pointer-events-none opacity-0': !isExpanded,
|
||||
},
|
||||
)}
|
||||
onClick={() => setIsExpanded(false)}
|
||||
></div>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={cn(
|
||||
'z-20 flex w-full items-center rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3 text-left transition-colors duration-200 ease-out hover:bg-zinc-800/40',
|
||||
{
|
||||
'relative bg-zinc-800 hover:bg-zinc-800': isExpanded,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-grow items-center space-x-3">
|
||||
<Icon />
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
{isExpanded ? (
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="absolute left-0 top-full z-20 translate-y-2 rounded-lg border border-zinc-800 bg-zinc-800 p-4">
|
||||
<p>{description}</p>
|
||||
{imgUrl && (
|
||||
<img
|
||||
onClick={() => {
|
||||
setIsZoomed(true);
|
||||
setIsExpanded(false);
|
||||
}}
|
||||
src={imgUrl}
|
||||
alt={title}
|
||||
className="mt-4 h-auto pointer-events-none md:pointer-events-auto w-full cursor-zoom-in rounded-lg object-right-top"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
113
src/components/SQLCourse/FAQSection.tsx
Normal file
113
src/components/SQLCourse/FAQSection.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
|
||||
type FAQItem = {
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
function FAQRow({ question, answer }: FAQItem) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex w-full items-center justify-between p-4 md:p-6 text-left gap-2"
|
||||
>
|
||||
<h3 className="text-lg md:text-xl text-balance font-normal text-white">{question}</h3>
|
||||
<ChevronDownIcon
|
||||
className={`h-5 w-5 text-zinc-400 transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="border-t border-zinc-800 p-6 pt-4 text-base md:text-lg leading-relaxed">
|
||||
<p>{answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FAQSection() {
|
||||
const faqs: FAQItem[] = [
|
||||
{
|
||||
question: 'What is the format of the course?',
|
||||
answer:
|
||||
'The course is written in textual format. There are several chapters; each chapter has a set of lessons, followed by a set of practice problems and quizzes. You can learn at your own pace and revisit the content anytime.',
|
||||
},
|
||||
{
|
||||
question: 'What prerequisites do I need for this course?',
|
||||
answer:
|
||||
'No prior SQL knowledge is required. The course starts from the basics and gradually progresses to advanced topics.',
|
||||
},
|
||||
{
|
||||
question: 'Do I need to have a local database to follow the course?',
|
||||
answer:
|
||||
'No, we have an integrated coding playground, populated with a sample databases depending on the lesson, that you can use to follow the course. You can also use your own database if you have one.',
|
||||
},
|
||||
{
|
||||
question: 'How long do I have access to the course?',
|
||||
answer:
|
||||
'You get lifetime access to the course including all future updates. Once you purchase, you can learn at your own pace and revisit the content anytime.',
|
||||
},
|
||||
{
|
||||
question: 'What kind of support is available?',
|
||||
answer:
|
||||
'You get access to an AI tutor within the course that can help you with queries 24/7. Additionally, you can use the community forums to discuss problems and get help from other learners.',
|
||||
},
|
||||
{
|
||||
question: 'Will I get a certificate upon completion?',
|
||||
answer:
|
||||
"Yes, upon completing the course and its challenges, you'll receive a certificate of completion that you can share with employers or add to your LinkedIn profile.",
|
||||
},
|
||||
{
|
||||
question: 'Can I use this for job interviews?',
|
||||
answer:
|
||||
'Absolutely! The course covers common SQL interview topics and includes practical challenges similar to what you might face in technical interviews. The hands-on experience will prepare you well for real-world scenarios.',
|
||||
},
|
||||
{
|
||||
question: "What if I don't like the course?",
|
||||
answer:
|
||||
'I will refund your purchase within 7 days of the purchase. No questions asked. However, I would love to hear your feedback so that I can improve the course. Send me an email at kamran@roadmap.sh',
|
||||
},
|
||||
{
|
||||
question: 'I already know SQL, can I still take this course?',
|
||||
answer:
|
||||
'Yes! The course starts from the basics and gradually progresses to advanced topics. You can skip the chapters that you already know and focus on the ones that you need.',
|
||||
},
|
||||
{
|
||||
question: 'Do you offer any team licenses?',
|
||||
answer: 'Yes, please contact me at kamran@roadmap.sh',
|
||||
},
|
||||
{
|
||||
question: 'How can I gift this course to someone?',
|
||||
answer:
|
||||
'Please contact me at kamran@roadmap.sh and I will be happy to help you.',
|
||||
},
|
||||
{
|
||||
question: 'What if I have a question that is not answered here?',
|
||||
answer:
|
||||
'Please contact me at kamran@roadmap.sh and I will be happy to help you.',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Frequently Asked Questions"
|
||||
description="Find answers to common questions about the course below."
|
||||
className="mt-10 md:mt-24"
|
||||
/>
|
||||
|
||||
<div className="mt-6 md:mt-8 w-full max-w-3xl space-y-2 md:space-y-6">
|
||||
{faqs.map((faq, index) => (
|
||||
<FAQRow key={index} {...faq} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
56
src/components/SQLCourse/FloatingPurchase.tsx
Normal file
56
src/components/SQLCourse/FloatingPurchase.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { BuyButton } from './BuyButton';
|
||||
|
||||
export function FloatingPurchase() {
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
setIsHidden(window.scrollY < 400);
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed bottom-0 left-0 right-0 z-[5] flex items-center justify-center transition-all duration-200 ease-out',
|
||||
{
|
||||
'pointer-events-none -bottom-10 opacity-0': isHidden,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{/* Desktop version */}
|
||||
<div className="hidden mb-5 md:flex w-full max-w-[800px] items-center justify-between rounded-2xl bg-yellow-950 p-5 shadow-lg ring-1 ring-yellow-500/40">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="mb-1 text-xl font-medium text-white">
|
||||
Go from Zero to Hero in SQL
|
||||
</h2>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Get instant access to the course and start learning today
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BuyButton variant="floating" />
|
||||
</div>
|
||||
|
||||
{/* Mobile version */}
|
||||
<div className="flex md:hidden w-full flex-col bg-yellow-950 px-4 pt-3 pb-4 shadow-lg ring-1 ring-yellow-500/40">
|
||||
<div className="flex flex-col items-center text-center mb-3">
|
||||
<h2 className="text-lg font-medium text-white">
|
||||
Master SQL Today
|
||||
</h2>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Get instant lifetime access
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BuyButton variant="floating" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/components/SQLCourse/PlatformDemo.tsx
Normal file
40
src/components/SQLCourse/PlatformDemo.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function PlatformDemo() {
|
||||
const [isZoomed, setIsZoomed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
if (isZoomed) {
|
||||
setIsZoomed(false);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, [isZoomed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZoomed && (
|
||||
<div
|
||||
onClick={() => setIsZoomed(false)}
|
||||
className="fixed inset-0 z-[999] flex cursor-zoom-out items-center justify-center bg-black bg-opacity-75"
|
||||
>
|
||||
<img
|
||||
src="https://assets.roadmap.sh/guest/course-environment-87jg8.png"
|
||||
alt="Course Environment"
|
||||
className="max-h-[90vh] max-w-[90vw] rounded-xl object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src="https://assets.roadmap.sh/guest/course-environment-87jg8.png"
|
||||
alt="Course Environment"
|
||||
onClick={() => setIsZoomed(true)}
|
||||
className="mt-12 sm:mt-20 w-full max-w-5xl rounded-xl cursor-zoom-in"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
163
src/components/SQLCourse/ReviewsSection.tsx
Normal file
163
src/components/SQLCourse/ReviewsSection.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { ChevronDownIcon, StarIcon, User2Icon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../../editor/utils/classname';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
|
||||
type Review = {
|
||||
name: string;
|
||||
role: string;
|
||||
rating: number;
|
||||
text: string;
|
||||
avatarUrl?: string;
|
||||
};
|
||||
|
||||
export function ReviewsSection() {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const reviews: Review[] = [
|
||||
{
|
||||
name: 'Tomáš Janků',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: "The course and it's interactivity is excellent and I'd honestly say it's **one of the best** on the SQL theme I've seen out there.",
|
||||
avatarUrl: 'https://github.com/jankudev.png',
|
||||
},
|
||||
{
|
||||
name: 'Gourav Khunger',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: 'This course was **absolutely brilliant!** The integrated database environment to practice what I learned was the best part.',
|
||||
avatarUrl: 'https://github.com/gouravkhunger.png',
|
||||
},
|
||||
{
|
||||
name: 'Meabed',
|
||||
role: 'CTO',
|
||||
rating: 5,
|
||||
text: 'Kamran has **clearly put a lot of thought** into this course. The content, structure and exercises were all great.',
|
||||
avatarUrl: 'https://github.com/meabed.png',
|
||||
},
|
||||
{
|
||||
name: 'Mohsin Aheer',
|
||||
role: 'Sr. Software Engineer',
|
||||
rating: 5,
|
||||
text: 'I already knew SQL but this course **taught me a bunch of new things.** Practical examples and challenges were great. Highly recommended!',
|
||||
avatarUrl: 'https://github.com/aheermohsinse.png',
|
||||
},
|
||||
{
|
||||
name: 'Reeve Tee',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: 'I found the course **highly comprehensive and incredibly valuable**. I would love to see more courses like this!',
|
||||
avatarUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Zeeshan',
|
||||
role: 'Sr. Software Engineer',
|
||||
rating: 5,
|
||||
text: 'Loved the teaching style and the way the course was structured. The **AI tutor was a great help** when I got stuck.',
|
||||
avatarUrl: 'https://github.com/ziishaned.png',
|
||||
},
|
||||
{
|
||||
name: 'Adnan Ahmed',
|
||||
role: 'Engineering Manager',
|
||||
rating: 5,
|
||||
text: 'Having the integrated IDE made a huge difference. Being able to immediately practice what I learned was **invaluable**.',
|
||||
avatarUrl: 'https://github.com/idnan.png',
|
||||
},
|
||||
{
|
||||
name: 'Kalvin Chakma',
|
||||
role: 'Jr. Software Engineer',
|
||||
rating: 5,
|
||||
text: "Best SQL course I've taken. The progression from basic to advanced concepts is **well thought out**, and the challenges are **excellent**.",
|
||||
avatarUrl: 'https://github.com/kalvin-chakma.png',
|
||||
},
|
||||
{
|
||||
name: 'Faisal Ahsan',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: 'The course and the learning experience was great. What I really liked was the **no-fluff explanations** and practical examples.',
|
||||
avatarUrl: 'https://github.com/faisalahsan.png',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative max-w-5xl">
|
||||
<div
|
||||
className={cn('rounded-2xl pb-0 pt-24', {
|
||||
'pb-8': isExpanded,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3',
|
||||
isExpanded ? '' : 'max-h-[400px] overflow-hidden',
|
||||
)}
|
||||
>
|
||||
{reviews.map((review, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="review-testimonial flex-shrink-0 break-inside-avoid-column rounded-xl bg-zinc-800/30 p-6 backdrop-blur [&_strong]:font-normal [&_strong]:text-yellow-300/70"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
{review.avatarUrl && (
|
||||
<img
|
||||
src={review.avatarUrl}
|
||||
alt={review.name}
|
||||
className="h-12 w-12 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{!review.avatarUrl && (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-zinc-800">
|
||||
<User2Icon className="h-6 w-6 text-zinc-400" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-semibold text-zinc-100">{review.name}</h3>
|
||||
<p className="text-sm text-zinc-400">{review.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex">
|
||||
{Array.from({ length: review.rating }).map((_, i) => (
|
||||
<StarIcon
|
||||
key={i}
|
||||
className="h-4 w-4 fill-yellow-500 text-yellow-500"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
className="mt-4 text-zinc-300"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(review.text),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'absolute bottom-0 left-0 right-0 h-40 bg-gradient-to-t from-[#121212] via-[#121212]/80 to-transparent',
|
||||
isExpanded ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn('absolute left-1/2 top-full -translate-x-1/2', {
|
||||
'-translate-y-1/2': !isExpanded,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex items-center gap-2 rounded-full bg-zinc-800 px-6 py-2 text-sm font-medium text-zinc-300 transition-all hover:bg-zinc-700 hover:text-zinc-100"
|
||||
>
|
||||
{isExpanded ? 'Show Less' : 'Show More Reviews'}
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 transition-transform ${
|
||||
isExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
425
src/components/SQLCourse/SQLCoursePage.tsx
Normal file
425
src/components/SQLCourse/SQLCoursePage.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpDownIcon,
|
||||
BarChartIcon,
|
||||
BrainIcon,
|
||||
ClipboardIcon,
|
||||
CodeIcon,
|
||||
DatabaseIcon,
|
||||
Eye,
|
||||
FileCheckIcon,
|
||||
FileQuestionIcon,
|
||||
GitBranchIcon,
|
||||
GitMergeIcon,
|
||||
LayersIcon,
|
||||
TableIcon,
|
||||
WrenchIcon,
|
||||
StarIcon,
|
||||
} from 'lucide-react';
|
||||
import { ChapterRow } from './ChapterRow';
|
||||
import { CourseFeature } from './CourseFeature';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
import { Spotlight } from './Spotlight';
|
||||
import { FloatingPurchase } from './FloatingPurchase';
|
||||
import { CourseAuthor } from './CourseAuthor';
|
||||
import { FAQSection } from './FAQSection';
|
||||
import { BuyButton } from './BuyButton';
|
||||
import { AccountButton } from './AccountButton';
|
||||
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
|
||||
import { PlatformDemo } from './PlatformDemo';
|
||||
import { AuthorQuoteMessage } from './AuthorQuoteMessage';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
type ChapterData = {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
lessonCount: number;
|
||||
challengeCount: number;
|
||||
lessons: { title: string; type: 'lesson' | 'challenge' | 'quiz' }[];
|
||||
};
|
||||
|
||||
export function SQLCoursePage() {
|
||||
const chapters: ChapterData[] = [
|
||||
{
|
||||
icon: <DatabaseIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Introduction',
|
||||
description:
|
||||
'Get comfortable with database concepts and SQL fundamentals.',
|
||||
lessonCount: 4,
|
||||
challengeCount: 1,
|
||||
lessons: [
|
||||
{ title: 'Basics of Databases', type: 'lesson' },
|
||||
{ title: 'What is SQL?', type: 'lesson' },
|
||||
{ title: 'Types of Queries', type: 'lesson' },
|
||||
{ title: 'Next Steps', type: 'lesson' },
|
||||
{ title: 'Introduction Quiz', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <TableIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'SQL Basics',
|
||||
description: 'Master the essential SQL query operations and syntax.',
|
||||
lessonCount: 9,
|
||||
challengeCount: 7,
|
||||
lessons: [
|
||||
{ title: 'SELECT Fundamentals', type: 'lesson' },
|
||||
{ title: 'Aliases and Constants', type: 'lesson' },
|
||||
{ title: 'Expressions in SELECT', type: 'lesson' },
|
||||
{ title: 'Selecting DISTINCT Values', type: 'lesson' },
|
||||
{ title: 'Filtering with WHERE', type: 'lesson' },
|
||||
{ title: 'Sorting with ORDER BY', type: 'lesson' },
|
||||
{ title: 'Limiting Results with LIMIT', type: 'lesson' },
|
||||
{ title: 'Handling NULL Values', type: 'lesson' },
|
||||
{ title: 'Comments', type: 'lesson' },
|
||||
{ title: 'Basic Queries Quiz', type: 'quiz' },
|
||||
{ title: 'Projection Challenge', type: 'challenge' },
|
||||
{ title: 'Select Expression', type: 'challenge' },
|
||||
{ title: 'Select Unique', type: 'challenge' },
|
||||
{ title: 'Logical Operators', type: 'challenge' },
|
||||
{ title: 'Sorting Challenge', type: 'challenge' },
|
||||
{ title: 'Sorting and Limiting', type: 'challenge' },
|
||||
{ title: 'Sorting and Filtering', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Manipulating Data',
|
||||
description: 'Learn how to modify and manipulate data in your database.',
|
||||
lessonCount: 3,
|
||||
challengeCount: 3,
|
||||
lessons: [
|
||||
{ title: 'INSERT Operations', type: 'lesson' },
|
||||
{ title: 'UPDATE Operations', type: 'lesson' },
|
||||
{ title: 'DELETE Operations', type: 'lesson' },
|
||||
{ title: 'Data Manipulation Quiz', type: 'quiz' },
|
||||
{ title: 'Inserting Customers', type: 'challenge' },
|
||||
{ title: 'Updating Bookstore', type: 'challenge' },
|
||||
{ title: 'Deleting Books', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <LayersIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Defining Tables',
|
||||
description: 'Master database schema design and table management.',
|
||||
lessonCount: 9,
|
||||
challengeCount: 7,
|
||||
lessons: [
|
||||
{ title: 'Creating Tables', type: 'lesson' },
|
||||
{ title: 'Data Types in SQLite', type: 'lesson' },
|
||||
{ title: 'Common Data Types', type: 'lesson' },
|
||||
{ title: 'More on Numeric Types', type: 'lesson' },
|
||||
{ title: 'Temporal Data Types', type: 'lesson' },
|
||||
{ title: 'CHECK Constraints', type: 'lesson' },
|
||||
{ title: 'Primary Key Constraint', type: 'lesson' },
|
||||
{ title: 'Modifying Tables', type: 'lesson' },
|
||||
{ title: 'Dropping and Truncating', type: 'lesson' },
|
||||
{ title: 'Defining Tables Quiz', type: 'quiz' },
|
||||
{ title: 'Simple Table Creation', type: 'challenge' },
|
||||
{ title: 'Data Types Challenge', type: 'challenge' },
|
||||
{ title: 'Constraints Challenge', type: 'challenge' },
|
||||
{ title: 'Temporal Validation', type: 'challenge' },
|
||||
{ title: 'Sales Data Analysis', type: 'challenge' },
|
||||
{ title: 'Modifying Tables', type: 'challenge' },
|
||||
{ title: 'Removing Table Data', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <GitMergeIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Multi-Table Queries',
|
||||
description:
|
||||
'Learn to work with multiple tables using JOINs and relationships.',
|
||||
lessonCount: 7,
|
||||
challengeCount: 10,
|
||||
lessons: [
|
||||
{ title: 'More on Relational Data', type: 'lesson' },
|
||||
{ title: 'Relationships and Types', type: 'lesson' },
|
||||
{ title: 'JOINs in Queries', type: 'lesson' },
|
||||
{ title: 'Self Joins and Usecases', type: 'lesson' },
|
||||
{ title: 'Foreign Key Constraint', type: 'lesson' },
|
||||
{ title: 'Set Operator Queries', type: 'lesson' },
|
||||
{ title: 'Views and Virtual Tables', type: 'lesson' },
|
||||
{ title: 'Multi-Table Queries Quiz', type: 'quiz' },
|
||||
{ title: 'Inactive Customers', type: 'challenge' },
|
||||
{ title: 'Recent 3 Orders', type: 'challenge' },
|
||||
{ title: 'High Value Orders', type: 'challenge' },
|
||||
{ title: 'Specific Book Customers', type: 'challenge' },
|
||||
{ title: 'Referred Customers', type: 'challenge' },
|
||||
{ title: 'Readers Like You', type: 'challenge' },
|
||||
{ title: 'Same Price Books', type: 'challenge' },
|
||||
{ title: 'Multi-Section Authors', type: 'challenge' },
|
||||
{ title: 'Expensive Books', type: 'challenge' },
|
||||
{ title: 'Trending Tech Books', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <WrenchIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Aggregate Functions',
|
||||
description:
|
||||
"Analyze and summarize data using SQL's powerful aggregation features.",
|
||||
lessonCount: 4,
|
||||
challengeCount: 10,
|
||||
lessons: [
|
||||
{ title: 'What is Aggregation?', type: 'lesson' },
|
||||
{ title: 'Basic Aggregation', type: 'lesson' },
|
||||
{ title: 'Grouping Data', type: 'lesson' },
|
||||
{ title: 'Grouping and Filtering', type: 'lesson' },
|
||||
{ title: 'Aggregate Queries Quiz', type: 'quiz' },
|
||||
{ title: 'Book Sales Summary', type: 'challenge' },
|
||||
{ title: 'Category Insights', type: 'challenge' },
|
||||
{ title: 'Author Tier Analysis', type: 'challenge' },
|
||||
{ title: 'Author Book Stats', type: 'challenge' },
|
||||
{ title: 'Daily Sales Report', type: 'challenge' },
|
||||
{ title: 'Publisher Stats', type: 'challenge' },
|
||||
{ title: 'High Value Publishers', type: 'challenge' },
|
||||
{ title: 'Premium Authors', type: 'challenge' },
|
||||
{ title: 'Sales Analysis', type: 'challenge' },
|
||||
{ title: 'Employee Performance', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <BarChartIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Scalar Functions',
|
||||
description:
|
||||
'Master built-in functions for data transformation and manipulation.',
|
||||
lessonCount: 6,
|
||||
challengeCount: 5,
|
||||
lessons: [
|
||||
{ title: 'What are they?', type: 'lesson' },
|
||||
{ title: 'String Functions', type: 'lesson' },
|
||||
{ title: 'Numeric Functions', type: 'lesson' },
|
||||
{ title: 'Date Functions', type: 'lesson' },
|
||||
{ title: 'Conversion Functions', type: 'lesson' },
|
||||
{ title: 'Logical Functions', type: 'lesson' },
|
||||
{ title: 'Scalar Functions Quiz', type: 'quiz' },
|
||||
{ title: 'Customer Contact List', type: 'challenge' },
|
||||
{ title: 'Membership Duration', type: 'challenge' },
|
||||
{ title: 'Book Performance', type: 'challenge' },
|
||||
{ title: 'Book Categories', type: 'challenge' },
|
||||
{ title: 'Monthly Sales Analysis', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <GitBranchIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Subqueries and CTEs',
|
||||
description:
|
||||
'Write complex queries using subqueries and common table expressions.',
|
||||
lessonCount: 4,
|
||||
challengeCount: 6,
|
||||
lessons: [
|
||||
{ title: 'What are Subqueries?', type: 'lesson' },
|
||||
{ title: 'Correlated Subqueries', type: 'lesson' },
|
||||
{ title: 'Common Table Expressions', type: 'lesson' },
|
||||
{ title: 'Recursive CTEs', type: 'lesson' },
|
||||
{ title: 'Subqueries Quiz', type: 'quiz' },
|
||||
{ title: 'Books Above Average', type: 'challenge' },
|
||||
{ title: 'Latest Category Books', type: 'challenge' },
|
||||
{ title: 'Low Stock by Category', type: 'challenge' },
|
||||
{ title: 'Bestseller Rankings', type: 'challenge' },
|
||||
{ title: 'New Customer Analysis', type: 'challenge' },
|
||||
{ title: 'Daily Sales Report', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <ArrowUpDownIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Window Functions',
|
||||
description:
|
||||
'Advanced analytics and calculations using window functions.',
|
||||
lessonCount: 5,
|
||||
challengeCount: 7,
|
||||
lessons: [
|
||||
{ title: 'What are they?', type: 'lesson' },
|
||||
{ title: 'OVER and PARTITION BY', type: 'lesson' },
|
||||
{ title: 'Use of ORDER BY', type: 'lesson' },
|
||||
{ title: 'Ranking Functions', type: 'lesson' },
|
||||
{ title: 'Window Frames', type: 'lesson' },
|
||||
{ title: 'Window Functions Quiz', type: 'quiz' },
|
||||
{ title: 'Basic Sales Metrics', type: 'challenge' },
|
||||
{ title: 'Bestseller Comparison', type: 'challenge' },
|
||||
{ title: 'Author Category Sales', type: 'challenge' },
|
||||
{ title: 'Top Authors', type: 'challenge' },
|
||||
{ title: 'Price Tier Rankings', type: 'challenge' },
|
||||
{ title: 'Month-over-Month Sales', type: 'challenge' },
|
||||
{ title: 'Price Range Analysis', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-grow flex-col items-center bg-gradient-to-b from-zinc-900 to-zinc-950 px-4 pb-52 pt-3 text-zinc-400 md:px-10 md:pt-8">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<a
|
||||
href="https://roadmap.sh"
|
||||
target="_blank"
|
||||
className="opacity-20 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<RoadmapLogoIcon />
|
||||
</a>
|
||||
<AccountButton />
|
||||
</div>
|
||||
<div className="relative mt-7 max-w-4xl text-left md:mt-20 md:text-center">
|
||||
<Spotlight className="left-[-170px] top-[-200px]" fill="#EAB308" />
|
||||
|
||||
<div className="inline-block rounded-full bg-yellow-500/10 px-4 py-1.5 text-base text-yellow-500 md:px-6 md:py-2 md:text-lg">
|
||||
<span className="hidden sm:block">
|
||||
Complete Course to Master Practical SQL
|
||||
</span>
|
||||
<span className="block sm:hidden">Complete SQL Course</span>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-5 text-4xl font-bold tracking-tight text-white md:mt-8 md:text-7xl">
|
||||
Master SQL <span className="hidden min-[384px]:inline">Queries</span>
|
||||
<div className="mt-2.5 bg-gradient-to-r from-yellow-500 to-yellow-300 bg-clip-text text-transparent md:text-6xl lg:text-7xl">
|
||||
From Basic to Advanced
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<p className="mx-auto my-5 max-w-2xl text-xl text-zinc-300 md:my-12 lg:text-2xl">
|
||||
A structured course to master database querying - perfect for
|
||||
developers, data analysts, and anyone working with data.
|
||||
</p>
|
||||
|
||||
<div className="hidden flex-row items-center justify-center gap-5 md:flex">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ClipboardIcon className="size-6 text-yellow-600" />
|
||||
<span>55+ Lessons</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<FileQuestionIcon className="size-6 text-yellow-600" />
|
||||
<span>100+ Challenges</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CodeIcon className="size-6 text-yellow-600" />
|
||||
<span>Integrated IDE</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<BrainIcon className="size-6 text-yellow-600" />
|
||||
<span>AI Tutor</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 flex justify-start md:mt-12 md:justify-center">
|
||||
<BuyButton variant="main" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReviewsSection />
|
||||
|
||||
<AuthorQuoteMessage />
|
||||
|
||||
<PlatformDemo />
|
||||
|
||||
<SectionHeader
|
||||
title="Not your average SQL course"
|
||||
description="Built around a text-based interactive approach and packed with practical challenges, this course stands out with features that make it truly unique."
|
||||
className="mt-16 md:mt-32"
|
||||
/>
|
||||
|
||||
<div className="mx-auto mt-6 w-full max-w-5xl md:mt-10">
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-4 lg:grid-cols-3">
|
||||
<CourseFeature
|
||||
title="Textual Course"
|
||||
icon={Eye}
|
||||
imgUrl="https://assets.roadmap.sh/guest/textual-course.png"
|
||||
description="Unlike video-based courses where you have to learn at the pace of the instructor, this course is text-based, allowing you to learn at your own pace."
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Coding Environment"
|
||||
icon={CodeIcon}
|
||||
imgUrl="https://assets.roadmap.sh/guest/coding-environment.png"
|
||||
description="With the integrated IDE, you can practice your SQL queries in real-time, getting instant feedback on your results."
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Practical Challenges"
|
||||
icon={FileQuestionIcon}
|
||||
imgUrl="https://assets.roadmap.sh/guest/coding-challenges.png"
|
||||
description="The course is packed with practical challenges and quizzes, allowing you to test your knowledge and skills."
|
||||
/>
|
||||
<CourseFeature
|
||||
title="AI Instructor"
|
||||
icon={BrainIcon}
|
||||
description="Powerful AI tutor to help you with your queries, provide additional explanations and help if you get stuck."
|
||||
imgUrl="https://assets.roadmap.sh/guest/ai-integration.png"
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Take Notes"
|
||||
icon={ClipboardIcon}
|
||||
description="The course allows you to take notes, where you can write down your thoughts and ideas. You can visit them later to review your progress."
|
||||
imgUrl="https://assets.roadmap.sh/guest/course-notes.png"
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Completion Certificate"
|
||||
icon={FileCheckIcon}
|
||||
imgUrl="https://assets.roadmap.sh/guest/course-certificate.jpg"
|
||||
description="The course provides a completion certificate, which you can share with your potential employers."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 w-full max-w-3xl text-left md:mt-9">
|
||||
<p className="text-lg leading-normal md:text-xl">
|
||||
Oh, and you get the{' '}
|
||||
<span className="bg-gradient-to-r from-yellow-500 to-yellow-300 bg-clip-text text-transparent">
|
||||
lifetime access
|
||||
</span>{' '}
|
||||
to the course including all the future updates. Also, there is a
|
||||
certificate of completion which you can share with your potential
|
||||
employers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SectionHeader
|
||||
title="Course Overview"
|
||||
description="The course is designed to help you go from SQL beginner to expert
|
||||
through hands-on practice with real-world scenarios, mastering
|
||||
everything from basic to complex queries."
|
||||
className="mt-8 md:mt-24"
|
||||
/>
|
||||
|
||||
<div className="mt-8 w-full max-w-3xl space-y-4 md:mt-12">
|
||||
{chapters.map((chapter, index) => (
|
||||
<ChapterRow key={index} counter={index + 1} {...chapter} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SectionHeader
|
||||
title="About the Author"
|
||||
className="mt-12 md:mt-24"
|
||||
description={
|
||||
<div className="mt-2 flex flex-col gap-4 text-lg leading-[1.52] md:mt-4 md:gap-6 md:text-xl">
|
||||
<p>
|
||||
I am Kamran Ahmed, an engineering leader with over a decade of
|
||||
experience in the tech industry. Throughout my career I have built
|
||||
and scaled software systems, architected complex data systems, and
|
||||
worked with large amounts of data to create efficient solutions.
|
||||
</p>
|
||||
<p>
|
||||
I am also the creator of{' '}
|
||||
<a
|
||||
href="https://roadmap.sh"
|
||||
target="_blank"
|
||||
className="text-yellow-400"
|
||||
>
|
||||
roadmap.sh
|
||||
</a>
|
||||
, a platform trusted by millions of developers to guide their
|
||||
learning journeys. I love to simplify complex topics and make
|
||||
learning practical and accessible.
|
||||
</p>
|
||||
<p>
|
||||
In this course, I will share everything I have learned about SQL
|
||||
from the basics to advanced concepts in a way that is easy to
|
||||
understand and apply. Whether you are just starting or looking to
|
||||
sharpen your skills, you are in the right place.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<CourseAuthor />
|
||||
|
||||
<FAQSection />
|
||||
|
||||
<FloatingPurchase />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
src/components/SQLCourse/SectionHeader.tsx
Normal file
29
src/components/SQLCourse/SectionHeader.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type SectionHeaderProps = {
|
||||
title: string;
|
||||
description: string | React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function SectionHeader(props: SectionHeaderProps) {
|
||||
const { title, description, className } = props;
|
||||
|
||||
return (
|
||||
<div className={cn('mx-auto w-full mt-24 max-w-3xl', className)}>
|
||||
<div className="relative w-full">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="inline-flex items-center rounded-xl ">
|
||||
<span className="text-2xl md:text-3xl font-medium text-zinc-200">{title}</span>
|
||||
</div>
|
||||
<div className="h-[1px] flex-grow bg-gradient-to-r from-yellow-500/20 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
{typeof description === 'string' ? (
|
||||
<p className="mt-2 md:mt-5 text-lg md:text-xl text-zinc-400">{description}</p>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/SQLCourse/Spotlight.tsx
Normal file
57
src/components/SQLCourse/Spotlight.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type SpotlightProps = {
|
||||
className?: string;
|
||||
fill?: string;
|
||||
};
|
||||
|
||||
export function Spotlight(props: SpotlightProps) {
|
||||
const { className, fill } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={cn(
|
||||
'animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[238%] opacity-0 lg:w-[138%]',
|
||||
className,
|
||||
)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 3787 2842"
|
||||
fill="none"
|
||||
>
|
||||
<g filter="url(#filter)">
|
||||
<ellipse
|
||||
cx="1924.71"
|
||||
cy="273.501"
|
||||
rx="1924.71"
|
||||
ry="273.501"
|
||||
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
|
||||
fill={fill || 'white'}
|
||||
fillOpacity="0.21"
|
||||
></ellipse>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter"
|
||||
x="0.860352"
|
||||
y="0.838989"
|
||||
width="3785.16"
|
||||
height="2840.26"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
></feBlend>
|
||||
<feGaussianBlur
|
||||
stdDeviation="151"
|
||||
result="effect1_foregroundBlur_1065_8"
|
||||
></feGaussianBlur>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
import Icon from '../AstroIcon.astro';
|
||||
|
||||
export interface Props {
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { pageUrl, description } = Astro.props;
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
---
|
||||
|
||||
<div class='absolute left-[-18px] top-[110px] h-full hidden' id='page-share-icons'>
|
||||
<div class='flex sticky top-[100px] flex-col gap-1.5 items-center'>
|
||||
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700 mb-0.5'>
|
||||
<Icon icon='twitter' />
|
||||
</a>
|
||||
<a href={fbUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='facebook' />
|
||||
</a>
|
||||
<a href={hnUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='hackernews' />
|
||||
</a>
|
||||
<a href={redditUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='reddit' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./sharer.js'></script>
|
||||
105
src/components/ShareIcons/ShareIcons.tsx
Normal file
105
src/components/ShareIcons/ShareIcons.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { FacebookIcon } from '../ReactIcons/FacebookIcon';
|
||||
import { HackerNewsIcon } from '../ReactIcons/HackerNewsIcon';
|
||||
import { RedditIcon } from '../ReactIcons/RedditIcon';
|
||||
import { TwitterIcon } from '../ReactIcons/TwitterIcon';
|
||||
|
||||
type ShareIconsProps = {
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export function ShareIcons(props: ShareIconsProps) {
|
||||
const { pageUrl, description, resourceType, resourceId } = props;
|
||||
|
||||
const shareIconsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
|
||||
const icons = [
|
||||
{
|
||||
url: twitterUrl,
|
||||
icon: (
|
||||
<TwitterIcon
|
||||
className="size-[24px] [&>path]:fill-[#E5E5E5]"
|
||||
boxColor="currentColor"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
url: fbUrl,
|
||||
icon: <FacebookIcon className="size-[26px]" />,
|
||||
},
|
||||
{
|
||||
url: hnUrl,
|
||||
icon: <HackerNewsIcon className="size-[26px]" />,
|
||||
},
|
||||
{
|
||||
url: redditUrl,
|
||||
icon: <RedditIcon className="size-[26px]" />,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const shareIcons = shareIconsRef.current;
|
||||
if (!shareIcons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
shareIcons.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
shareIcons.classList.remove('hidden');
|
||||
};
|
||||
|
||||
onScroll();
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-[-18px] top-[110px] hidden h-full"
|
||||
ref={shareIconsRef}
|
||||
>
|
||||
<div className="sticky top-[100px] flex flex-col items-center gap-1.5">
|
||||
{icons.map((icon, index) => {
|
||||
const host = new URL(icon.url).host;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={icon.url}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
'text-gray-500 hover:text-gray-700',
|
||||
index === 0 && 'mt-0.5',
|
||||
)}
|
||||
onClick={() => {
|
||||
window.fireEvent({
|
||||
category: 'RoadmapShareLink',
|
||||
action: `Share Roadmap / ${resourceType} / ${resourceId} / ${host}`,
|
||||
label: icon.url,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{icon.icon}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
export class Sharer {
|
||||
constructor() {
|
||||
this.init = this.init.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
|
||||
this.shareIconsId = 'page-share-icons';
|
||||
}
|
||||
|
||||
get shareIconsEl() {
|
||||
return document.getElementById(this.shareIconsId);
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
this.shareIconsEl.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.shareIconsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.shareIconsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const sharer = new Sharer();
|
||||
sharer.init();
|
||||
@@ -1,7 +1,8 @@
|
||||
import '../FrameRenderer/FrameRenderer.css';
|
||||
import '../EditorRoadmap/EditorRoadmapRenderer.css';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { wireframeJSONToSVG } from 'roadmap-renderer';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import '../FrameRenderer/FrameRenderer.css';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import type { TeamMember } from './TeamProgressPage';
|
||||
@@ -18,6 +19,9 @@ import { pageProgressMessage } from '../../stores/page';
|
||||
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
|
||||
import { replaceChildren } from '../../lib/dom.ts';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu.tsx';
|
||||
import { renderFlowJSON } from '../../../editor/renderer/renderer.ts';
|
||||
import { getResourceMeta } from '../../lib/roadmap.ts';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
@@ -56,6 +60,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
useState<MemberProgressResponse>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const toast = useToast();
|
||||
const [renderer, setRenderer] = useState<PageType['renderer']>('balsamiq');
|
||||
|
||||
let resourceJsonUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3000'
|
||||
@@ -88,14 +93,25 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
}
|
||||
|
||||
async function renderResource(jsonUrl: string) {
|
||||
const page = await getResourceMeta(resourceType, resourceId);
|
||||
if (!page) {
|
||||
toast.error('Resource not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const renderer = page.renderer || 'balsamiq';
|
||||
setRenderer(renderer);
|
||||
|
||||
const res = await fetch(jsonUrl, {});
|
||||
const json = await res.json();
|
||||
const svg: SVGElement | null = await wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
const svg =
|
||||
renderer === 'editor'
|
||||
? await renderFlowJSON(json as any)
|
||||
: await wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
}
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
@@ -136,10 +152,10 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
skipped = [],
|
||||
} = memberProgress;
|
||||
|
||||
done.forEach((id: string) => renderTopicProgress(id, 'done'));
|
||||
learning.forEach((id: string) => renderTopicProgress(id, 'learning'));
|
||||
skipped.forEach((id: string) => renderTopicProgress(id, 'skipped'));
|
||||
removed.forEach((id: string) => renderTopicProgress(id, 'removed'));
|
||||
done.forEach((id) => renderTopicProgress(id, 'done'));
|
||||
learning.forEach((id) => renderTopicProgress(id, 'learning'));
|
||||
skipped.forEach((id) => renderTopicProgress(id, 'skipped'));
|
||||
removed.forEach((id) => renderTopicProgress(id, 'removed'));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
@@ -262,7 +278,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-[100] h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
|
||||
<div
|
||||
id={'customized-roadmap'}
|
||||
id={renderer === 'editor' ? undefined : 'customized-roadmap'}
|
||||
className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { getUrlParams, setUrlParams } from '../../lib/browser';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { MemberProgressItem } from './MemberProgressItem';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $currentTeam } from '../../stores/team';
|
||||
import { GroupRoadmapItem } from './GroupRoadmapItem';
|
||||
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';
|
||||
import { MemberProgressItem } from './MemberProgressItem';
|
||||
import { MemberProgressModal } from './MemberProgressModal';
|
||||
|
||||
export type UserProgress = {
|
||||
resourceTitle: string;
|
||||
|
||||
@@ -108,7 +108,6 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
useKeydown(
|
||||
'r',
|
||||
() => {
|
||||
console.log(progress);
|
||||
if (progress === 'pending') {
|
||||
onClose();
|
||||
return;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useAuth } from '../../hooks/use-auth';
|
||||
import { ModalLoader } from './ModalLoader.tsx';
|
||||
import { UserProgressModalHeader } from './UserProgressModalHeader';
|
||||
import { X } from 'lucide-react';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu.tsx';
|
||||
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||
import { renderFlowJSON } from '../../../editor/renderer/renderer.ts';
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
import type { VideoFileType } from '../lib/video';
|
||||
|
||||
export interface Props {
|
||||
video: VideoFileType;
|
||||
}
|
||||
|
||||
const { video } = Astro.props;
|
||||
const { frontmatter, id } = video;
|
||||
---
|
||||
|
||||
<a
|
||||
class:list={[
|
||||
'block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b',
|
||||
]}
|
||||
href={`/videos/${id}`}
|
||||
>
|
||||
<span class='group-hover:translate-x-2 transition-transform'>
|
||||
{frontmatter.title}
|
||||
|
||||
{
|
||||
frontmatter.isNew && (
|
||||
<span class='bg-green-300 text-green-900 text-xs font-medium px-1.5 py-0.5 rounded-sm uppercase ml-1.5'>
|
||||
New
|
||||
<span class='hidden sm:inline'>
|
||||
·
|
||||
{new Date(frontmatter.date).toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
<span class='capitalize text-gray-500 text-xs hidden sm:block'>
|
||||
{frontmatter.duration}
|
||||
</span>
|
||||
|
||||
<span class='text-gray-400 text-xs block sm:hidden'> »</span>
|
||||
</a>
|
||||
@@ -4,7 +4,7 @@ pdfUrl: '/pdfs/best-practices/backend-performance.pdf'
|
||||
order: 1
|
||||
briefTitle: 'Backend Performance'
|
||||
briefDescription: 'Backend Performance Best Practices'
|
||||
isNew: true
|
||||
isNew: false
|
||||
isUpcoming: false
|
||||
title: 'Backend Performance Best Practices'
|
||||
description: 'Detailed list of best practices to improve your backend performance'
|
||||
|
||||
19
src/data/changelogs/sql-course.md
Normal file
19
src/data/changelogs/sql-course.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: "Our first paid course about SQL is live"
|
||||
description: 'We just launched our first paid SQL course'
|
||||
images:
|
||||
"SQL Course": "https://assets.roadmap.sh/guest/course-environment-87jg8.png"
|
||||
"Course Creator Platform": "https://assets.roadmap.sh/guest/course-creator-platform.png"
|
||||
seo:
|
||||
title: 'SQL Course is Live'
|
||||
description: ''
|
||||
date: 2025-02-04
|
||||
---
|
||||
|
||||
After months of work, I am excited to announce our [brand-new SQL course](/courses/sql) designed to help you master SQL with a hands-on, practical approach!
|
||||
|
||||
For the past few months, we have been working on building a course platform that would not only help us deliver high-quality educational content but also sustain the development of roadmap.sh. This SQL course is our first step in that direction.
|
||||
|
||||
The course has been designed with a focus on practical learning. Each topic is accompanied by hands-on exercises, real-world examples, and an integrated coding environment where you can practice what you learn. The AI integration provides personalized learning assistance, helping you grasp concepts better and faster.
|
||||
|
||||
Check out the course at [roadmap.sh/courses/sql](https://roadmap.sh/courses/sql)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user