Merge branch 'kamranahmedse:master' into master

pull/8480/head
Arnab Sarkar 3 weeks ago committed by GitHub
commit 4a92cc3879
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      .astro/settings.json
  2. 2
      .github/workflows/cloudfront-api-cache.yml
  3. 2
      .github/workflows/cloudfront-fe-cache.yml
  4. 23
      .github/workflows/greetings.yml
  5. 7
      .gitignore
  6. 19
      astro.config.mjs
  7. 16
      contributing.md
  8. 14
      editor/readonly-editor.tsx
  9. 14
      editor/renderer/index.tsx
  10. 5
      editor/renderer/renderer.ts
  11. 29
      package.json
  12. 0
      packages/.gitkeep
  13. 3861
      pnpm-lock.yaml
  14. 2
      pnpm-workspace.yaml
  15. 2
      public/roadmap-content/ai-engineer.json
  16. 15
      public/roadmap-content/aspnet-core.json
  17. 22
      public/roadmap-content/computer-science.json
  18. 2
      public/roadmap-content/cpp.json
  19. 4
      public/roadmap-content/sql.json
  20. 14
      renderer/index.tsx
  21. 5
      renderer/renderer.ts
  22. 2
      scripts/editor-roadmap-content-json.ts
  23. 2
      scripts/editor-roadmap-content.ts
  24. 2
      scripts/editor-roadmap-dirs.ts
  25. 39
      scripts/generate-renderer.sh
  26. 76
      scripts/migrate-editor-roadmap.ts
  27. 58
      scripts/rename-content.ts
  28. 2
      src/components/AccountSidebar.astro
  29. 4
      src/components/AccountStreak/AccountStreak.tsx
  30. 18
      src/components/AccountStreak/AccountStreakHeatmap.tsx
  31. 2
      src/components/Activity/ActivityTopicsModal.tsx
  32. 2
      src/components/Activity/ProjectProgress.tsx
  33. 2
      src/components/Activity/ResourceProgress.tsx
  34. 12
      src/components/AddTeamRoadmap.tsx
  35. 10
      src/components/AdvertiseForm.tsx
  36. 2
      src/components/Analytics/Clarity.astro
  37. 2
      src/components/Analytics/GoogleAd.astro
  38. 6
      src/components/Analytics/Hubspot.astro
  39. 2
      src/components/Analytics/RedditPixel.astro
  40. 1
      src/components/Analytics/analytics.ts
  41. 12
      src/components/AuthenticationFlow/CourseLoginPopup.tsx
  42. 6
      src/components/AuthenticationFlow/EmailLoginForm.tsx
  43. 16
      src/components/AuthenticationFlow/EmailSignupForm.tsx
  44. 4
      src/components/AuthenticationFlow/ForgotPasswordForm.tsx
  45. 18
      src/components/AuthenticationFlow/GitHubButton.tsx
  46. 21
      src/components/AuthenticationFlow/GoogleButton.tsx
  47. 13
      src/components/AuthenticationFlow/LinkedInButton.tsx
  48. 6
      src/components/AuthenticationFlow/ResetPasswordForm.tsx
  49. 30
      src/components/Befriend.tsx
  50. 2
      src/components/BestPracticeHeader.astro
  51. 67
      src/components/Billing/BillingPage.tsx
  52. 2
      src/components/Billing/EmptyBillingScreen.tsx
  53. 25
      src/components/Billing/UpgradeAccountModal.tsx
  54. 4
      src/components/Changelog/ChangelogItem.astro
  55. 4
      src/components/Changelog/ChangelogLaunch.astro
  56. 6
      src/components/ChangelogBanner.astro
  57. 12
      src/components/ChangelogImages.tsx
  58. 6
      src/components/CommandMenu/CommandMenu.tsx
  59. 4
      src/components/CreateTeam/RoadmapSelector.tsx
  60. 6
      src/components/CreateTeam/SelectRoadmapModal.tsx
  61. 2
      src/components/CreateTeam/Step0.tsx
  62. 10
      src/components/CreateTeam/Step1.tsx
  63. 2
      src/components/CreateTeam/Step2.tsx
  64. 4
      src/components/CreateTeam/Step3.tsx
  65. 4
      src/components/CreateTeam/UpdateTeamResourceModal.tsx
  66. 10
      src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx
  67. 2
      src/components/CustomRoadmap/EmbedRoadmapModal.tsx
  68. 4
      src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
  69. 8
      src/components/CustomRoadmap/PersonalRoadmapActionDropdown.tsx
  70. 4
      src/components/CustomRoadmap/PersonalRoadmapList.tsx
  71. 2
      src/components/CustomRoadmap/RateRoadmapForm.tsx
  72. 2
      src/components/CustomRoadmap/ResourceProgressStats.tsx
  73. 8
      src/components/CustomRoadmap/RoadmapActionButton.tsx
  74. 4
      src/components/CustomRoadmap/ShareRoadmapModal.tsx
  75. 2
      src/components/CustomRoadmap/SharedRoadmapList.tsx
  76. 2
      src/components/CustomRoadmap/Showcase/SubmitShowcaseWarning.tsx
  77. 21
      src/components/Dashboard/DashboardAiRoadmaps.tsx
  78. 2
      src/components/Dashboard/DashboardCardLink.tsx
  79. 2
      src/components/Dashboard/DashboardCustomProgressCard.tsx
  80. 2
      src/components/Dashboard/DashboardPage.tsx
  81. 2
      src/components/Dashboard/DashboardProgressCard.tsx
  82. 6
      src/components/Dashboard/DashboardProjectCard.tsx
  83. 2
      src/components/Dashboard/DashboardTeamRoadmaps.tsx
  84. 2
      src/components/Dashboard/EmptyStackMessage.tsx
  85. 11
      src/components/Dashboard/ListDashboardCustomProgress.tsx
  86. 2
      src/components/Dashboard/PersonalDashboard.tsx
  87. 10
      src/components/Dashboard/ProgressStack.tsx
  88. 2
      src/components/Dashboard/TeamDashboard.tsx
  89. 2
      src/components/DeleteAccount/DeleteAccount.astro
  90. 6
      src/components/DeleteAccount/DeleteAccountForm.tsx
  91. 8
      src/components/DeleteTeamPopup.tsx
  92. 8
      src/components/DiscoverRoadmaps/DiscoverRoadmapSorting.tsx
  93. 4
      src/components/DiscoverRoadmaps/SearchRoadmap.tsx
  94. 2
      src/components/EditorRoadmap/EditorRoadmap.tsx
  95. 12
      src/components/EditorRoadmap/EditorRoadmapRenderer.tsx
  96. 4
      src/components/ExploreAIRoadmap/AIRoadmapsList.tsx
  97. 2
      src/components/ExploreAIRoadmap/EmptyRoadmaps.tsx
  98. 4
      src/components/ExploreAIRoadmap/ExploreAISearch.tsx
  99. 8
      src/components/ExploreAIRoadmap/ExploreAISorting.tsx
  100. 2
      src/components/FeaturedGuides/FeaturedGuideList.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,8 +1,8 @@
{ {
"devToolbar": { "devToolbar": {
"enabled": false "enabled": false
}, },
"_variables": { "_variables": {
"lastUpdateCheck": 1742812122664 "lastUpdateCheck": 1743851801172
} }
} }

@ -2,7 +2,7 @@ name: Clears API Cloudfront Cache
on: on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
aws_costs: cloudfront_api_cache:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clear Cloudfront Caching - name: Clear Cloudfront Caching

@ -2,7 +2,7 @@ name: Clears Frontend Cloudfront Cache
on: on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
aws_costs: cloudfront_fe_cache:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clear Cloudfront Caching - name: Clear Cloudfront Caching

@ -1,23 +0,0 @@
name: Greetings
on:
issues:
types: [opened]
pull_request_target:
branches: [master]
types: [opened]
jobs:
greet:
name: Greet New Contributors
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |
Thank you for your first ever contribution to [roadmap.sh](https://roadmap.sh)! 🎉
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.
Thanks for choosing to contribute, and for helping make this project better! 🌟

7
.gitignore vendored

@ -28,9 +28,6 @@ pnpm-debug.log*
/playwright-report/ /playwright-report/
/playwright/.cache/ /playwright/.cache/
tests-examples tests-examples
*.csv *.csveditor/
/editor/* packages/editor
!/editor/readonly-editor.tsx
!/editor/renderer/renderer.ts
!/editor/renderer/index.tsx

@ -1,10 +1,10 @@
// https://astro.build/config // https://astro.build/config
import sitemap from '@astrojs/sitemap'; import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';
import node from '@astrojs/node'; import node from '@astrojs/node';
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import rehypeExternalLinks from 'rehype-external-links'; import rehypeExternalLinks from 'rehype-external-links';
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs'; import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
import tailwindcss from '@tailwindcss/vite';
import react from '@astrojs/react'; import react from '@astrojs/react';
@ -16,6 +16,10 @@ export default defineConfig({
status: 301, status: 301,
destination: '/devops', destination: '/devops',
}, },
'/ai-tutor': {
status: 301,
destination: '/ai',
},
}, },
vite: { vite: {
server: { server: {
@ -51,21 +55,22 @@ export default defineConfig({
], ],
], ],
}, },
output: 'hybrid', output: 'server',
adapter: node({ adapter: node({
mode: 'standalone', mode: 'standalone',
}), }),
trailingSlash: 'never', trailingSlash: 'never',
integrations: [ integrations: [
tailwind({
config: {
applyBaseStyles: false,
},
}),
sitemap({ sitemap({
filter: shouldIndexPage, filter: shouldIndexPage,
serialize: serializeSitemap, serialize: serializeSitemap,
}), }),
react(), react(),
], ],
vite: {
plugins: [tailwindcss()],
ssr: {
noExternal: [/^@roadmapsh\/editor.*$/],
},
},
}); });

@ -125,6 +125,22 @@ It's important to add a valid type, this will help us categorize the content and
- PR's that don't follow our style guide, have no description, and a default title. - PR's that don't follow our style guide, have no description, and a default title.
- Links to your own blog articles. - Links to your own blog articles.
## Local Development
For local development, you can use the following commands:
```bash
git clone git@github.com:kamranahmedse/developer-roadmap.git --depth 1
cd developer-roadmap
pnpm add @roadmapsh/editor@npm:@roadmapsh/dummy-editor -w
pnpm install
```
Run the development server with:
```bash
pnpm dev
```
*** ***
Have a look at the [License](./license) file. Have a look at the [License](./license) file.

@ -1,14 +0,0 @@
export function ReadonlyEditor(props: any) {
return (
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
<p className="mb-4">
Renderer is a private component. If you are a collaborator and have
access to it. Run the following command:
</p>
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
npm run generate-renderer
</code>
</div>
);
}

@ -1,14 +0,0 @@
export function Renderer(props: any) {
return (
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
<p className="mb-4">
Renderer is a private component. If you are a collaborator and have
access to it. Run the following command:
</p>
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
npm run generate-renderer
</code>
</div>
);
}

@ -1,5 +0,0 @@
export function renderFlowJSON(data: any, options?: any) {
console.warn("renderFlowJSON is not implemented");
console.warn("run the following command to generate the renderer:");
console.warn("> npm run generate-renderer");
}

@ -20,28 +20,31 @@
"editor-roadmap-content": "tsx scripts/editor-roadmap-content.ts", "editor-roadmap-content": "tsx scripts/editor-roadmap-content.ts",
"roadmap-content": "node scripts/roadmap-content.cjs", "roadmap-content": "node scripts/roadmap-content.cjs",
"generate-renderer": "sh scripts/generate-renderer.sh", "generate-renderer": "sh scripts/generate-renderer.sh",
"generate-renderer-dummy": "sh scripts/generate-renderer-dummy.sh",
"best-practice-dirs": "node scripts/best-practice-dirs.cjs", "best-practice-dirs": "node scripts/best-practice-dirs.cjs",
"best-practice-content": "node scripts/best-practice-content.cjs", "best-practice-content": "node scripts/best-practice-content.cjs",
"generate:og": "node ./scripts/generate-og-images.mjs", "generate:og": "node ./scripts/generate-og-images.mjs",
"warm:urls": "sh ./scripts/warm-urls.sh https://roadmap.sh/sitemap-0.xml", "warm:urls": "sh ./scripts/warm-urls.sh https://roadmap.sh/sitemap-0.xml",
"compress:images": "tsx ./scripts/compress-images.ts", "compress:images": "tsx ./scripts/compress-images.ts",
"generate:roadmap-content-json": "tsx ./scripts/editor-roadmap-content-json.ts", "generate:roadmap-content-json": "tsx ./scripts/editor-roadmap-content-json.ts",
"migrate:editor-roadmaps": "tsx ./scripts/migrate-editor-roadmap.ts",
"test:e2e": "playwright test" "test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^8.3.4", "@astrojs/node": "^9.1.3",
"@astrojs/react": "^3.6.2", "@astrojs/react": "^4.2.3",
"@astrojs/sitemap": "^3.2.0", "@astrojs/sitemap": "^3.3.0",
"@astrojs/tailwind": "^5.1.2",
"@fingerprintjs/fingerprintjs": "^4.5.0", "@fingerprintjs/fingerprintjs": "^4.5.0",
"@microsoft/clarity": "^1.0.0", "@microsoft/clarity": "^1.0.0",
"@nanostores/react": "^0.8.0", "@nanostores/react": "^0.8.0",
"@napi-rs/image": "^1.9.2", "@napi-rs/image": "^1.9.2",
"@resvg/resvg-js": "^2.6.2", "@resvg/resvg-js": "^2.6.2",
"@roadmapsh/editor": "workspace:*",
"@tailwindcss/vite": "^4.1.3",
"@tanstack/react-query": "^5.59.16", "@tanstack/react-query": "^5.59.16",
"@types/react": "^18.3.11", "@types/react": "^19.0.0",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^19.0.0",
"astro": "^4.16.1", "astro": "^5.6.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
@ -60,14 +63,13 @@
"npm-check-updates": "^17.1.3", "npm-check-updates": "^17.1.3",
"playwright": "^1.48.0", "playwright": "^1.48.0",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"react": "^18.3.1", "react": "^19.0.0",
"react-calendar-heatmap": "^1.9.0", "react-calendar-heatmap": "^1.9.0",
"react-confetti": "^6.1.0", "react-confetti": "^6.1.0",
"react-dom": "^18.3.1", "react-dom": "^19.0.0",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"react-textarea-autosize": "^8.5.7", "react-textarea-autosize": "^8.5.7",
"react-tooltip": "^5.28.0", "react-tooltip": "^5.28.0",
"reactflow": "^11.11.4",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"roadmap-renderer": "^1.0.6", "roadmap-renderer": "^1.0.6",
@ -78,11 +80,11 @@
"shiki": "^3.1.0", "shiki": "^3.1.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"tailwind-merge": "^2.5.3", "tailwind-merge": "^2.5.3",
"tailwindcss": "^3.4.13", "tailwindcss": "^4.1.3",
"tiptap-markdown": "^0.8.10", "tiptap-markdown": "^0.8.10",
"turndown": "^7.2.0", "turndown": "^7.2.0",
"unified": "^11.0.5", "unified": "^11.0.5",
"zustand": "^4.5.5" "zustand": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@ai-sdk/google": "^1.1.19", "@ai-sdk/google": "^1.1.19",
@ -91,6 +93,7 @@
"@types/dom-to-image": "^2.6.7", "@types/dom-to-image": "^2.6.7",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/prismjs": "^1.26.4", "@types/prismjs": "^1.26.4",
"@types/react-calendar-heatmap": "^1.6.7", "@types/react-calendar-heatmap": "^1.6.7",
"@types/react-slick": "^0.23.13", "@types/react-slick": "^0.23.13",
@ -104,7 +107,7 @@
"openai": "^4.67.3", "openai": "^4.67.3",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1", "prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.8", "prettier-plugin-tailwindcss": "^0.6.11",
"tsx": "^4.19.1" "tsx": "^4.19.1"
} }
} }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,2 @@
packages:
- packages/*

@ -328,7 +328,7 @@
"links": [ "links": [
{ {
"title": "Managing Context", "title": "Managing Context",
"url": "https://platform.openai.com/docs/guides/text-generation/managing-context-for-text-generation", "url": "https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#managing-context-for-text-generation",
"type": "article" "type": "article"
}, },
{ {

@ -594,8 +594,19 @@
}, },
"ipABerBcM9zCte9pYaIse": { "ipABerBcM9zCte9pYaIse": {
"title": "Minimal APIs", "title": "Minimal APIs",
"description": "", "description": "Minimal APIs is a lightweight approach to building HTTP APIs in .NET with minimal ceremony. It is designed for simplicity and performance, making it ideal for microservices, serverless applications, and small web services. Minimal APIs provide a streamlined way to define routes, handle requests, and return responses without requiring controllers or extensive configuration. They leverage top-level statements, reducing boilerplate code while maintaining flexibility and scalability.\n\nMinimal APIs support dependency injection, middleware, model binding, and validation. They also integrate seamlessly with OpenAPI (Swagger) for API documentation. Their simplicity makes them an excellent choice for building fast and efficient web applications with .NET.\n\nTo learn more, visit the following resources:",
"links": [] "links": [
{
"title": "Minimal APIs in .NET 8: A Simplified Approach to Build Services",
"url": "https://medium.com/codenx/minimal-apis-in-net-8-a-simplified-approach-to-build-services-eb50df56819f",
"type": "article"
},
{
"title": "Introduction to ASP.NET Core Minimal APIs",
"url": "https://blog.jetbrains.com/dotnet/2023/04/25/introduction-to-asp-net-core-minimal-apis/",
"type": "article"
}
]
}, },
"POQPoN98eqOH2873ZI6Hm": { "POQPoN98eqOH2873ZI6Hm": {
"title": "Object Relational Mapping", "title": "Object Relational Mapping",

@ -2809,26 +2809,10 @@
} }
] ]
}, },
"IaPd_zuLbiOCwoSHQLoIG": { "2-3-4-trees@IaPd_zuLbiOCwoSHQLoIG.md": {
"title": "2 3 4 Trees", "title": "2 3 4 Trees",
"description": "In practice: For every 2-4 tree, there are corresponding red–black trees with data elements in the same order. The insertion and deletion operations on 2-4 trees are also equivalent to color-flipping and rotations in red–black trees. This makes 2-4 trees an important tool for understanding the logic behind red–black trees, and this is why many introductory algorithm texts introduce 2-4 trees just before red–black trees, even though 2-4 trees are not often used in practice.\n\nVisit the following resources to learn more:", "description": "",
"links": [ "links": []
{
"title": "CS 61B Lecture 26: Balanced Search Trees",
"url": "https://archive.org/details/ucberkeley_webcast_zqrqYXkth6Q",
"type": "article"
},
{
"title": "Bottom Up 234-Trees",
"url": "https://www.youtube.com/watch?v=DQdMYevEyE4&index=4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6",
"type": "video"
},
{
"title": "Top Down 234-Trees",
"url": "https://www.youtube.com/watch?v=2679VQ26Fp4&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6&index=5",
"type": "video"
}
]
}, },
"UOYeM-hqIKCrB9hGez4Q_": { "UOYeM-hqIKCrB9hGez4Q_": {
"title": "K-ary / M-ary Tree", "title": "K-ary / M-ary Tree",

@ -546,7 +546,7 @@
"links": [] "links": []
}, },
"vvE1aUsWbF1OFcmMUHbJa": { "vvE1aUsWbF1OFcmMUHbJa": {
"title": "Standardds", "title": "Standards",
"description": "C++ standards are a set of rules and guidelines that define the language's features, syntax, and semantics. The International Organization for Standardization (ISO) is responsible for maintaining and updating the C++ standards. The main purpose of the standards is to ensure consistency, efficiency, and maintainability across multiple platforms and compilers.\n\nHere's a brief summary of the different C++ standards released to date:\n\n* **C++98/C++03**: The first standardized version of C++, which introduced many features like templates, exceptions, and the Standard Template Library (STL). C++03 is a minor update to C++98 with some bug fixes and performance improvements.\n \n* **C++11**: A major upgrade to the language, which introduced features such as:\n \n * Lambda expressions:\n \n auto sum = [](int a, int b) -> int { return a + b; };\n \n \n * Range-based for loops:\n \n std::vector<int> numbers = {1, 2, 3, 4};\n for (int num : numbers) {\n std::cout << num << '\\n';\n }\n \n \n * Smart pointers like `std::shared_ptr` and `std::unique_ptr`.\n* **C++14**: A minor update to C++11, which added features such as:\n \n * Generic lambda expressions:\n \n auto generic_sum = [](auto a, auto b) { return a + b; };\n \n \n * Binary literals:\n \n int binary_number = 0b1010;\n \n \n* **C++17**: Another major update that introduced features such as:\n \n * `if` and `switch` with initializers:\n \n if (auto it = my_map.find(key); it != my_map.end()) {\n // use 'it' here\n }\n \n \n * Structured bindings:\n \n std::map<std::string, int> my_map = {{\"A\", 1}, {\"B\", 2}};\n for (const auto& [key, value] : my_map) {\n // use 'key' and 'value' here\n }\n \n \n* **C++20**: The latest major update to the language, with features such as:\n \n * Concepts:\n \n template<typename T>\n concept Addable = requires(T a, T b) {\n { a + b } -> std::same_as<T>;\n };\n \n \n * Ranges:\n \n std::vector<int> numbers = {1, 2, 3, 4};\n auto doubled = numbers | std::views::transform([](int n) { return n * 2; });\n \n \n * Coroutines and more.\n\nRemember that to use these language features, you might need to configure your compiler to use the specific C++ standard version. For example, with GCC or Clang, you can use the `-std=c++11`, `-std=c++14`, `-std=c++17`, or `-std=c++20` flags.", "description": "C++ standards are a set of rules and guidelines that define the language's features, syntax, and semantics. The International Organization for Standardization (ISO) is responsible for maintaining and updating the C++ standards. The main purpose of the standards is to ensure consistency, efficiency, and maintainability across multiple platforms and compilers.\n\nHere's a brief summary of the different C++ standards released to date:\n\n* **C++98/C++03**: The first standardized version of C++, which introduced many features like templates, exceptions, and the Standard Template Library (STL). C++03 is a minor update to C++98 with some bug fixes and performance improvements.\n \n* **C++11**: A major upgrade to the language, which introduced features such as:\n \n * Lambda expressions:\n \n auto sum = [](int a, int b) -> int { return a + b; };\n \n \n * Range-based for loops:\n \n std::vector<int> numbers = {1, 2, 3, 4};\n for (int num : numbers) {\n std::cout << num << '\\n';\n }\n \n \n * Smart pointers like `std::shared_ptr` and `std::unique_ptr`.\n* **C++14**: A minor update to C++11, which added features such as:\n \n * Generic lambda expressions:\n \n auto generic_sum = [](auto a, auto b) { return a + b; };\n \n \n * Binary literals:\n \n int binary_number = 0b1010;\n \n \n* **C++17**: Another major update that introduced features such as:\n \n * `if` and `switch` with initializers:\n \n if (auto it = my_map.find(key); it != my_map.end()) {\n // use 'it' here\n }\n \n \n * Structured bindings:\n \n std::map<std::string, int> my_map = {{\"A\", 1}, {\"B\", 2}};\n for (const auto& [key, value] : my_map) {\n // use 'key' and 'value' here\n }\n \n \n* **C++20**: The latest major update to the language, with features such as:\n \n * Concepts:\n \n template<typename T>\n concept Addable = requires(T a, T b) {\n { a + b } -> std::same_as<T>;\n };\n \n \n * Ranges:\n \n std::vector<int> numbers = {1, 2, 3, 4};\n auto doubled = numbers | std::views::transform([](int n) { return n * 2; });\n \n \n * Coroutines and more.\n\nRemember that to use these language features, you might need to configure your compiler to use the specific C++ standard version. For example, with GCC or Clang, you can use the `-std=c++11`, `-std=c++14`, `-std=c++17`, or `-std=c++20` flags.",
"links": [] "links": []
}, },

@ -1260,7 +1260,7 @@
}, },
"Ps9Yv2s-bKvEegGAbPsiA": { "Ps9Yv2s-bKvEegGAbPsiA": {
"title": "Query Optimization", "title": "Query Optimization",
"description": "Query optimization in SQL involves refining queries to enhance their execution speed and reduce resource consumption. Key strategies include indexing columns used in `WHERE`, `JOIN`, and `ORDER BY` clauses to accelerate data retrieval, minimizing data processed by limiting the number of columns selected and filtering rows early in the query. Using appropriate join types and arranging joins in the most efficient order are crucial. Avoiding inefficient patterns like `SELECT`, replacing subqueries with joins or common table expressions (CTEs), and leveraging query hints or execution plan analysis can also improve performance. Regularly updating statistics and ensuring that queries are structured to take advantage of database-specific optimizations are essential practices for maintaining optimal performance.\n\nLearn more from the following resources:", "description": "Query optimization in SQL involves refining queries to enhance their execution speed and reduce resource consumption. Key strategies include indexing columns used in `WHERE`, `JOIN`, and `ORDER BY` clauses to accelerate data retrieval, minimizing data processed by limiting the number of columns selected and filtering rows early in the query. Using appropriate join types and arranging joins in the most efficient order are crucial. Avoiding inefficient patterns like `SELECT *`, replacing subqueries with joins or common table expressions (CTEs), and leveraging query hints or execution plan analysis can also improve performance. Regularly updating statistics and ensuring that queries are structured to take advantage of database-specific optimizations are essential practices for maintaining optimal performance.\n\nLearn more from the following resources:",
"links": [ "links": [
{ {
"title": "12 Ways to Optimize SQL Queries", "title": "12 Ways to Optimize SQL Queries",
@ -1495,7 +1495,7 @@
}, },
"UVTgbZrqpbYl1bQvQejcF": { "UVTgbZrqpbYl1bQvQejcF": {
"title": "Reducing Subqueries", "title": "Reducing Subqueries",
"description": "Recursive queries in SQL allow for iterative processing of hierarchical or tree-structured data within a single query. They consist of an anchor member (the base case) and a recursive member that references the query itself, enabling the exploration of parent-child relationships, traversal of graphs, or generation of series data. This powerful feature is particularly useful for tasks like querying organizational hierarchies, bill of materials structures, or navigating complex relationships in data that would otherwise require multiple separate queries or procedural code.\n\nLearn more from the following resources:", "description": "Reducing subqueries is a common SQL optimization technique, especially when dealing with complex logic or large datasets. Correlated subqueries, which are evaluated once for each row in the outer query, can degrade the performance. Subqueries can often be replaced with JOIN operations. In cases where subqueries are reused, consider replacing them with Common Table Expressions (CTEs), which offer modularity and avoid repeated executions of the same logic. Limiting the result set returned by subqueries and storing the results of expensive subqueries in temporary tables for reuse can also improve performance.\n\nLearn more from the following resources:",
"links": [] "links": []
}, },
"w53CSY53nAAN0ux-XeJ4c": { "w53CSY53nAAN0ux-XeJ4c": {

@ -1,14 +0,0 @@
export function Renderer(props: any) {
return (
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
<p className="mb-4">
Renderer is a private component. If you are a collaborator and have
access to it. Run the following command:
</p>
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
npm run generate-renderer
</code>
</div>
);
}

@ -1,5 +0,0 @@
export function renderFlowJSON(data: any, options?: any) {
console.warn("renderFlowJSON is not implemented");
console.warn("run the following command to generate the renderer:");
console.warn("> npm run generate-renderer");
}

@ -1,7 +1,7 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import type { Node } from 'reactflow'; import type { Node } from '@roadmapsh/editor';
import matter from 'gray-matter'; import matter from 'gray-matter';
import type { RoadmapFrontmatter } from '../src/lib/roadmap'; import type { RoadmapFrontmatter } from '../src/lib/roadmap';
import { slugify } from '../src/lib/slugger'; import { slugify } from '../src/lib/slugger';

@ -1,7 +1,7 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import type { Edge, Node } from 'reactflow'; import type { Edge, Node } from '@roadmapsh/editor';
import matter from 'gray-matter'; import matter from 'gray-matter';
import type { RoadmapFrontmatter } from '../src/lib/roadmap'; import type { RoadmapFrontmatter } from '../src/lib/roadmap';
import { slugify } from '../src/lib/slugger'; import { slugify } from '../src/lib/slugger';

@ -1,7 +1,7 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import type { Node } from 'reactflow'; import type { Node } from '@roadmapsh/editor';
import matter from 'gray-matter'; import matter from 'gray-matter';
import type { RoadmapFrontmatter } from '../src/lib/roadmap'; import type { RoadmapFrontmatter } from '../src/lib/roadmap';
import { slugify } from '../src/lib/slugger'; import { slugify } from '../src/lib/slugger';

@ -2,33 +2,24 @@
set -e set -e
# ignore cloning if .temp/web-draw already exists # Remove old editor
rm -rf editor
if [ ! -d ".temp/web-draw" ]; then if [ ! -d ".temp/web-draw" ]; then
mkdir -p .temp git clone ssh://git@github.com/roadmapsh/web-draw.git .temp/web-draw --depth 1
git clone git@github.com:roadmapsh/web-draw.git .temp/web-draw
fi fi
rm -rf editor # Make dir
mkdir editor mkdir -p packages/editor
mkdir -p packages/editor/dist
# copy the files at /src/editor/* to /editor
# while replacing any existing files
cp -rf .temp/web-draw/src/editor/* editor
# Add @ts-nocheck to the top of each ts and tsx file # Copy the editor dist, package.json
# so that the typescript compiler doesn't complain cp -rf .temp/web-draw/packages/editor/dist packages/editor
# about the missing types cp -rf .temp/web-draw/packages/editor/package.json packages/editor
find editor -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= read -r -d '' file; do
if [ -f "$file" ]; then
echo "// @ts-nocheck" > temp
cat "$file" >> temp
mv temp "$file"
echo "Added @ts-nocheck to $file"
fi
done
# Remove temp directory
rm -rf .temp
# ignore the worktree changes for the editor directory # Reinstall so that the editor which was setup gets used
git update-index --assume-unchanged editor/readonly-editor.tsx || true rm -rf node_modules
git update-index --assume-unchanged editor/renderer/index.tsx || true pnpm install
git update-index --assume-unchanged editor/renderer/renderer.ts || true

@ -0,0 +1,76 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Node } from '@roadmapsh/editor';
import matter from 'gray-matter';
import type { RoadmapFrontmatter } from '../src/lib/roadmap';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Directory containing the roadmaps
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
const allRoadmaps = await fs.readdir(ROADMAP_CONTENT_DIR);
const editorRoadmapIds = new Set<string>();
for (const roadmapId of allRoadmaps) {
const roadmapFrontmatterDir = path.join(
ROADMAP_CONTENT_DIR,
roadmapId,
`${roadmapId}.md`,
);
const roadmapFrontmatterRaw = await fs.readFile(
roadmapFrontmatterDir,
'utf-8',
);
const { data } = matter(roadmapFrontmatterRaw);
const roadmapFrontmatter = data as RoadmapFrontmatter;
if (roadmapFrontmatter.renderer === 'editor') {
editorRoadmapIds.add(roadmapId);
}
}
for (const roadmapId of editorRoadmapIds) {
const roadmapJSONDir = path.join(
ROADMAP_CONTENT_DIR,
roadmapId,
`${roadmapId}.json`,
);
const roadmapJSONRaw = await fs.readFile(roadmapJSONDir, 'utf-8');
const roadmapJSON = JSON.parse(roadmapJSONRaw);
const roadmapNodes = roadmapJSON.nodes as Node[];
const updatedNodes = roadmapNodes.map((node) => {
const width = +(node?.width || node?.style?.width || 0);
const height = +(node?.height || node?.style?.height || 0);
const ADDITIONAL_WIDTH = 1;
// adding one `1px` in width to avoid the node to be cut in half
// this is a quick fix to avoid the issue
if (node?.style?.width) {
node.style.width = width + ADDITIONAL_WIDTH;
}
if (node?.width) {
node.width = width + ADDITIONAL_WIDTH;
}
return {
...node,
measured: {
width: width + ADDITIONAL_WIDTH,
height,
},
};
});
const updatedRoadmapJSON = {
...roadmapJSON,
nodes: updatedNodes,
};
const updatedRoadmapJSONString = JSON.stringify(updatedRoadmapJSON, null, 2);
await fs.writeFile(roadmapJSONDir, updatedRoadmapJSONString, 'utf-8');
}

@ -0,0 +1,58 @@
import fs from 'fs';
import path from 'path';
const roadmapDirs = fs.readdirSync(
path.join(__dirname, '..', 'src', 'data', 'roadmaps'),
);
roadmapDirs.forEach((roadmapDir) => {
const roadmapDirPath = path.join(
__dirname,
'..',
'src',
'data',
'roadmaps',
roadmapDir,
'content',
);
const roadmapDirContent = fs.readdirSync(roadmapDirPath);
roadmapDirContent.forEach((content) => {
const contentPath = path.join(roadmapDirPath, content);
const contentStats = fs.statSync(contentPath);
const oldName = path.basename(contentPath);
const newName = oldName.replace(/^(\d+)-/, '');
fs.renameSync(contentPath, path.join(roadmapDirPath, newName));
if (contentStats.isDirectory()) {
const contentDirContent = fs.readdirSync(contentPath);
contentDirContent.forEach((contentDir) => {
const contentDirPath = path.join(contentPath, contentDir);
const contentDirStats = fs.statSync(contentDirPath);
const oldName = path.basename(contentDirPath);
const newName = oldName.replace(/^(\d+)-/, '');
fs.renameSync(contentDirPath, path.join(contentPath, newName));
if (contentDirStats.isDirectory()) {
const contentDirContent = fs.readdirSync(contentDirPath);
contentDirContent.forEach((contentDir) => {
const contentDirPath2 = path.join(contentDirPath, contentDir);
const contentDirStats2 = fs.statSync(contentDirPath2);
const oldName2 = path.basename(contentDirPath2);
const newName2 = oldName2.replace(/^(\d+)-/, '');
fs.renameSync(contentDirPath2, path.join(contentDirPath, newName2));
});
}
});
}
});
});

@ -163,7 +163,7 @@ const sidebarLinks = [
: 'border-r-transparent text-gray-500 hover:border-r-gray-300' : 'border-r-transparent text-gray-500 hover:border-r-gray-300'
}`} }`}
> >
<span class='flex flex-grow items-center'> <span class='flex grow items-center'>
{sidebarLink.icon.component ? ( {sidebarLink.icon.component ? (
<sidebarLink.icon.component <sidebarLink.icon.component
className={`${sidebarLink.icon.classes} mr-2`} className={`${sidebarLink.icon.classes} mr-2`}

@ -83,10 +83,10 @@ export function AccountStreak(props: AccountStreakProps) {
const totalCircles = leftCircleCount + currentCircleCount + remainingCount; const totalCircles = leftCircleCount + currentCircleCount + remainingCount;
return ( return (
<div className="relative z-[90] animate-fade-in"> <div className="relative z-90 animate-fade-in">
<button <button
className={cn( className={cn(
'flex items-center justify-center rounded-lg p-1.5 px-2 text-purple-400 hover:bg-purple-100/10 focus:outline-none', 'flex items-center justify-center rounded-lg p-1.5 px-2 text-purple-400 hover:bg-purple-100/10 focus:outline-hidden',
{ {
'bg-purple-100/10': showDropdown, 'bg-purple-100/10': showDropdown,
}, },

@ -128,20 +128,20 @@ export function AccountStreakHeatmap(props: AccountStreakHeatmapProps) {
]} ]}
classForValue={(value) => { classForValue={(value) => {
if (!value) { if (!value) {
return 'fill-slate-700 rounded-md [rx:2px] focus:outline-none'; return 'fill-slate-700 rounded-md [rx:2px] focus:outline-hidden';
} }
const { count } = value; const { count } = value;
if (count >= 20) { if (count >= 20) {
return 'fill-slate-200 rounded-md [rx:2px] focus:outline-none'; return 'fill-slate-200 rounded-md [rx:2px] focus:outline-hidden';
} else if (count >= 10) { } else if (count >= 10) {
return 'fill-slate-300 rounded-md [rx:2px] focus:outline-none'; return 'fill-slate-300 rounded-md [rx:2px] focus:outline-hidden';
} else if (count >= 5) { } else if (count >= 5) {
return 'fill-slate-400 rounded-md [rx:2px] focus:outline-none'; return 'fill-slate-400 rounded-md [rx:2px] focus:outline-hidden';
} else if (count >= 3) { } else if (count >= 3) {
return 'fill-slate-500 rounded-md [rx:2px] focus:outline-none'; return 'fill-slate-500 rounded-md [rx:2px] focus:outline-hidden';
} else { } else {
return 'fill-slate-600 rounded-md [rx:2px] focus:outline-none'; return 'fill-slate-600 rounded-md [rx:2px] focus:outline-hidden';
} }
}} }}
tooltipDataAttrs={(value: any) => { tooltipDataAttrs={(value: any) => {
@ -159,7 +159,7 @@ export function AccountStreakHeatmap(props: AccountStreakHeatmapProps) {
<ReactTooltip <ReactTooltip
id="user-activity-tip" id="user-activity-tip"
className="!rounded-lg !bg-slate-900 !p-1 !px-2 !text-xs" className="rounded-lg! bg-slate-900! p-1! px-2! text-xs!"
/> />
<div className="mt-2 flex items-center justify-end"> <div className="mt-2 flex items-center justify-end">
@ -173,14 +173,14 @@ export function AccountStreakHeatmap(props: AccountStreakHeatmapProps) {
data-tooltip-content={`${legend.count} Updates`} data-tooltip-content={`${legend.count} Updates`}
> >
<div <div
className={`h-2.5 w-2.5 ${legend.color} mr-1 rounded-sm`} className={`h-2.5 w-2.5 ${legend.color} mr-1 rounded-xs`}
></div> ></div>
</div> </div>
))} ))}
<span className="ml-2 text-xs text-slate-500">More</span> <span className="ml-2 text-xs text-slate-500">More</span>
<ReactTooltip <ReactTooltip
id="user-activity-tip" id="user-activity-tip"
className="!rounded-lg !bg-slate-900 !p-1 !px-2 !text-sm" className="rounded-lg! bg-slate-900! p-1! px-2! text-sm!"
/> />
</div> </div>
</div> </div>

@ -43,7 +43,7 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
onClose(); onClose();
}} }}
> >
<div className={`popup-body relative rounded-lg bg-white p-4 shadow`}> <div className={`popup-body relative rounded-lg bg-white p-4 shadow-sm`}>
<span className="mb-2 flex items-center justify-between text-lg font-semibold capitalize"> <span className="mb-2 flex items-center justify-between text-lg font-semibold capitalize">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{actionType.replace('_', ' ')} {actionType.replace('_', ' ')}

@ -36,7 +36,7 @@ export function ProjectProgress(props: ProjectProgressType) {
target="_blank" target="_blank"
> >
<ProjectStatus projectStatus={projectStatus} /> <ProjectStatus projectStatus={projectStatus} />
<span className="ml-2 flex-grow truncate">{projectStatus?.title}</span> <span className="ml-2 grow truncate">{projectStatus?.title}</span>
<span className="inline-flex items-center gap-1 text-xs text-gray-400"> <span className="inline-flex items-center gap-1 text-xs text-gray-400">
{projectStatus.upvotes} {projectStatus.upvotes}
<ThumbsUp className="size-2.5 stroke-[2.5px]" /> <ThumbsUp className="size-2.5 stroke-[2.5px]" />

@ -73,7 +73,7 @@ export function ResourceProgress(props: ResourceProgressType) {
showActions ? 'pr-7' : '', showActions ? 'pr-7' : '',
)} )}
> >
<span className="flex-grow truncate">{title}</span> <span className="grow truncate">{title}</span>
<span className="text-xs text-gray-400"> <span className="text-xs text-gray-400">
{parseInt(progressPercentage, 10)}% {parseInt(progressPercentage, 10)}%
</span> </span>

@ -69,7 +69,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
<div className="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
className="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow-sm"
> >
{isLoading && ( {isLoading && (
<> <>
@ -99,7 +99,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
<button <button
onClick={onClose} onClick={onClose}
type="button" type="button"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300" className="grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
> >
Done Done
</button> </button>
@ -110,7 +110,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
setIsLoading(false); setIsLoading(false);
}} }}
type="button" type="button"
className="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white" className="grow cursor-pointer rounded-lg bg-black py-2 text-center text-white"
> >
+ Add More + Add More
</button> </button>
@ -126,7 +126,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
<button <button
onClick={onClose} onClick={onClose}
type="button" type="button"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300" className="grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
> >
Cancel Cancel
</button> </button>
@ -152,7 +152,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
setSelectedRoadmap(roadmapId); setSelectedRoadmap(roadmapId);
}); });
}} }}
inputClassName="mt-2 mb-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400" inputClassName="mt-2 mb-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-hidden placeholder:text-gray-400 focus:border-gray-400"
placeholder={'Search for roadmap'} placeholder={'Search for roadmap'}
/> />
@ -160,7 +160,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
<button <button
onClick={onClose} onClick={onClose}
type="button" type="button"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300" className="grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
> >
Cancel Cancel
</button> </button>

@ -30,7 +30,7 @@ function Input(props: InputProps) {
value={value} value={value}
onChange={onChange} onChange={onChange}
rows={rows} rows={rows}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-xs focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
autoComplete="off" autoComplete="off"
data-1p-ignore="" data-1p-ignore=""
data-form-type="other" data-form-type="other"
@ -45,7 +45,7 @@ function Input(props: InputProps) {
value={value} value={value}
onChange={onChange} onChange={onChange}
required={required} required={required}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-xs focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
autoComplete="off" autoComplete="off"
data-1p-ignore="" data-1p-ignore=""
data-form-type="other" data-form-type="other"
@ -120,7 +120,7 @@ export function AdvertiseForm() {
Ready to learn more? Fill out the form below to get started! Ready to learn more? Fill out the form below to get started!
</h2> </h2>
{error && ( {error && (
<div className="relative mb-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"> <div className="relative mb-4 rounded-sm border border-red-400 bg-red-100 px-4 py-3 text-red-700">
{error} {error}
</div> </div>
)} )}
@ -199,7 +199,7 @@ export function AdvertiseForm() {
type="checkbox" type="checkbox"
checked={formData.updates} checked={formData.updates}
onChange={handleInputChange} onChange={handleInputChange}
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" className="h-4 w-4 rounded-sm border-gray-300 text-indigo-600 focus:ring-indigo-500"
/> />
</div> </div>
<div className="ml-3 text-sm"> <div className="ml-3 text-sm">
@ -213,7 +213,7 @@ export function AdvertiseForm() {
<div> <div>
<button <button
type="submit" type="submit"
className="flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" className="flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-xs hover:bg-indigo-700 focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
> >
Send Send
</button> </button>

@ -1,4 +1,4 @@
<script type='text/javascript'> <script type='text/javascript' is:inline>
(function (c, l, a, r, i, t, y) { (function (c, l, a, r, i, t, y) {
c[a] = c[a] =
c[a] || c[a] ||

@ -10,8 +10,8 @@
'roadmaps', 'roadmaps',
'community', 'community',
'start-here', 'start-here',
'ai-roadmaps',
'ai', 'ai',
'ai-tutor',
'teams', 'teams',
'about', 'about',
'account', 'account',

@ -0,0 +1,6 @@
<script
type='text/javascript'
id='hs-script-loader'
async
defer
src='//js.hs-scripts.com/46095657.js?businessUnitId=2306992'></script>

@ -1,4 +1,4 @@
<script> <script is:inline>
// @ts-nocheck // @ts-nocheck
!(function (w, d) { !(function (w, d) {
if (!w.rdt) { if (!w.rdt) {

@ -1,6 +1,7 @@
declare global { declare global {
interface Window { interface Window {
gtag: any; gtag: any;
varify: any;
fireEvent: (props: { fireEvent: (props: {
action: string; action: string;
category: string; category: string;

@ -12,6 +12,7 @@ type CourseLoginPopupProps = {
}; };
export const CHECKOUT_AFTER_LOGIN_KEY = 'checkoutAfterLogin'; export const CHECKOUT_AFTER_LOGIN_KEY = 'checkoutAfterLogin';
export const SAMPLE_AFTER_LOGIN_KEY = 'sampleAfterLogin';
export function CourseLoginPopup(props: CourseLoginPopupProps) { export function CourseLoginPopup(props: CourseLoginPopupProps) {
const { onClose: parentOnClose, checkoutAfterLogin = true } = props; const { onClose: parentOnClose, checkoutAfterLogin = true } = props;
@ -27,6 +28,7 @@ export function CourseLoginPopup(props: CourseLoginPopupProps) {
// if user didn't login and closed the popup, we remove the checkoutAfterLogin flag // 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 // so that login from other buttons on course page will trigger purchase
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY); localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
localStorage.removeItem(SAMPLE_AFTER_LOGIN_KEY);
parentOnClose(); parentOnClose();
} }
@ -40,7 +42,7 @@ export function CourseLoginPopup(props: CourseLoginPopupProps) {
if (emailNature) { if (emailNature) {
const emailHeader = ( const emailHeader = (
<div className="mb-7 text-center"> <div className="mb-7 text-center">
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900"> <p className="mb-3.5 pt-2 text-2xl leading-5 font-semibold text-slate-900">
{emailNature === 'login' {emailNature === 'login'
? 'Login to your account' ? 'Login to your account'
: 'Create an account'} : 'Create an account'}
@ -80,7 +82,7 @@ export function CourseLoginPopup(props: CourseLoginPopupProps) {
return ( return (
<Modal onClose={onClose} bodyClassName="p-5 h-auto"> <Modal onClose={onClose} bodyClassName="p-5 h-auto">
<div className="mb-7 text-center"> <div className="mb-7 text-center">
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900"> <p className="mb-3.5 pt-2 text-2xl leading-5 font-semibold text-slate-900">
Create or login to Enroll Create or login to Enroll
</p> </p>
<p className="mt-2 text-sm leading-4 text-slate-600"> <p className="mt-2 text-sm leading-4 text-slate-600">
@ -115,7 +117,7 @@ export function CourseLoginPopup(props: CourseLoginPopupProps) {
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
{!isUsingEmail && ( {!isUsingEmail && (
<button <button
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100" className="grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
onClick={() => setIsUsingEmail(true)} onClick={() => setIsUsingEmail(true)}
> >
Use your email address Use your email address
@ -124,13 +126,13 @@ export function CourseLoginPopup(props: CourseLoginPopupProps) {
{isUsingEmail && ( {isUsingEmail && (
<> <>
<button <button
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100" className="grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
onClick={() => setEmailNature('login')} onClick={() => setEmailNature('login')}
> >
Already have an account Already have an account
</button> </button>
<button <button
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100" className="grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
onClick={() => setEmailNature('signup')} onClick={() => setEmailNature('signup')}
> >
Create an account Create an account

@ -70,7 +70,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
type="email" type="email"
autoComplete="email" autoComplete="email"
required required
className="block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Email Address" placeholder="Email Address"
value={email} value={email}
onInput={(e) => setEmail(String((e.target as any).value))} onInput={(e) => setEmail(String((e.target as any).value))}
@ -84,7 +84,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
type="password" type="password"
autoComplete="current-password" autoComplete="current-password"
required required
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Password" placeholder="Password"
value={password} value={password}
onInput={(e) => setPassword(String((e.target as any).value))} onInput={(e) => setPassword(String((e.target as any).value))}
@ -106,7 +106,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
<button <button
type="submit" type="submit"
disabled={isLoading || isDisabled} disabled={isLoading || isDisabled}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-hidden focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
> >
{isLoading ? 'Please wait...' : 'Continue'} {isLoading ? 'Please wait...' : 'Continue'}
</button> </button>

@ -1,6 +1,11 @@
import { type FormEvent, useEffect, useState } from 'react'; import { type FormEvent, useEffect, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
import { deleteUrlParam, getUrlParams } from '../../lib/browser'; import {
deleteUrlParam,
getLastPath,
getUrlParams,
urlToId,
} from '../../lib/browser';
import { isLoggedIn, setAIReferralCode } from '../../lib/jwt'; import { isLoggedIn, setAIReferralCode } from '../../lib/jwt';
type EmailSignupFormProps = { type EmailSignupFormProps = {
@ -34,6 +39,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
email, email,
password, password,
name, name,
src: urlToId(getLastPath() || window.location.pathname),
}, },
); );
@ -74,7 +80,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
min={3} min={3}
max={50} max={50}
required required
className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Full Name" placeholder="Full Name"
value={name} value={name}
onInput={(e) => setName(String((e.target as any).value))} onInput={(e) => setName(String((e.target as any).value))}
@ -87,7 +93,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
type="email" type="email"
autoComplete="email" autoComplete="email"
required required
className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Email Address" placeholder="Email Address"
value={email} value={email}
onInput={(e) => setEmail(String((e.target as any).value))} onInput={(e) => setEmail(String((e.target as any).value))}
@ -102,7 +108,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
min={6} min={6}
max={50} max={50}
required required
className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Password" placeholder="Password"
value={password} value={password}
onInput={(e) => setPassword(String((e.target as any).value))} onInput={(e) => setPassword(String((e.target as any).value))}
@ -115,7 +121,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
<button <button
type="submit" type="submit"
disabled={isLoading || isDisabled} disabled={isLoading || isDisabled}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-hidden focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
> >
{isLoading ? 'Please wait...' : 'Continue to Verify Email'} {isLoading ? 'Please wait...' : 'Continue to Verify Email'}
</button> </button>

@ -33,7 +33,7 @@ export function ForgotPasswordForm() {
<input <input
type="email" type="email"
name="email" name="email"
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required required
placeholder="Email Address" placeholder="Email Address"
value={email} value={email}
@ -55,7 +55,7 @@ export function ForgotPasswordForm() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="mt-3 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" className="mt-3 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-hidden focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
> >
{isLoading ? 'Please wait...' : 'Continue'} {isLoading ? 'Please wait...' : 'Continue'}
</button> </button>

@ -9,7 +9,7 @@ import { cn } from '../../lib/classname.ts';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx';
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx'; import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
import { triggerUtmRegistration } from '../../lib/browser.ts'; import { getLastPath, triggerUtmRegistration, urlToId } from '../../lib/browser.ts';
type GitHubButtonProps = { type GitHubButtonProps = {
isDisabled?: boolean; isDisabled?: boolean;
@ -38,10 +38,12 @@ export function GitHubButton(props: GitHubButtonProps) {
setIsLoading(true); setIsLoading(true);
setIsDisabled?.(true); setIsDisabled?.(true);
const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE);
httpGet<{ token: string; isNewUser: boolean }>( httpGet<{ token: string; isNewUser: boolean }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${ `${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
window.location.search window.location.search
}`, }&src=${urlToId(lastPageBeforeGithub || getLastPath() || window.location.pathname)}`,
) )
.then(({ response, error }) => { .then(({ response, error }) => {
if (!response?.token) { if (!response?.token) {
@ -57,7 +59,6 @@ export function GitHubButton(props: GitHubButtonProps) {
let redirectUrl = new URL('/', window.location.origin); let redirectUrl = new URL('/', window.location.origin);
const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT); const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT);
const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE);
// If the social redirect is there and less than 30 seconds old // If the social redirect is there and less than 30 seconds old
// redirect to the page that user was on before they clicked the github login button // redirect to the page that user was on before they clicked the github login button
@ -127,9 +128,12 @@ export function GitHubButton(props: GitHubButtonProps) {
// For non authentication pages, we want to redirect back to the page // For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button // the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) { if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend', '/r', '/ai'].includes( const pagePath = [
window.location.pathname, '/respond-invite',
) '/befriend',
'/r',
'/ai-roadmaps',
].includes(window.location.pathname)
? window.location.pathname + window.location.search ? window.location.pathname + window.location.search
: window.location.pathname; : window.location.pathname;
@ -144,7 +148,7 @@ export function GitHubButton(props: GitHubButtonProps) {
<> <>
<button <button
className={cn( 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', 'inline-flex h-10 w-full items-center justify-center gap-2 rounded-sm border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-hidden 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, className,
)} )}
disabled={isLoading || isDisabled} disabled={isLoading || isDisabled}

@ -1,19 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Cookies from 'js-cookie'; import { FIRST_LOGIN_PARAM, setAuthToken } from '../../lib/jwt';
import {
FIRST_LOGIN_PARAM,
TOKEN_COOKIE_NAME,
setAuthToken,
} from '../../lib/jwt';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { COURSE_PURCHASE_PARAM } from '../../lib/jwt'; import { COURSE_PURCHASE_PARAM } from '../../lib/jwt';
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx'; import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
import { Spinner } from '../ReactIcons/Spinner.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx';
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx'; import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
import { import { triggerUtmRegistration, urlToId, getLastPath } from '../../lib/browser.ts';
getStoredUtmParams,
triggerUtmRegistration,
} from '../../lib/browser.ts';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
type GoogleButtonProps = { type GoogleButtonProps = {
@ -43,10 +35,12 @@ export function GoogleButton(props: GoogleButtonProps) {
setIsLoading(true); setIsLoading(true);
setIsDisabled?.(true); setIsDisabled?.(true);
const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE);
httpGet<{ token: string; isNewUser: boolean }>( httpGet<{ token: string; isNewUser: boolean }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${ `${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
window.location.search window.location.search
}`, }&src=${urlToId(lastPageBeforeGoogle || getLastPath() || window.location.pathname)}`,
) )
.then(({ response, error }) => { .then(({ response, error }) => {
if (!response?.token) { if (!response?.token) {
@ -61,7 +55,6 @@ export function GoogleButton(props: GoogleButtonProps) {
let redirectUrl = new URL('/', window.location.origin); let redirectUrl = new URL('/', window.location.origin);
const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT); const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT);
const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE);
// If the social redirect is there and less than 30 seconds old // If the social redirect is there and less than 30 seconds old
// redirect to the page that user was on before they clicked the github login button // redirect to the page that user was on before they clicked the github login button
@ -132,7 +125,7 @@ export function GoogleButton(props: GoogleButtonProps) {
'/respond-invite', '/respond-invite',
'/befriend', '/befriend',
'/r', '/r',
'/ai', '/ai-roadmaps',
].includes(window.location.pathname) ].includes(window.location.pathname)
? window.location.pathname + window.location.search ? window.location.pathname + window.location.search
: window.location.pathname; : window.location.pathname;
@ -154,7 +147,7 @@ export function GoogleButton(props: GoogleButtonProps) {
<> <>
<button <button
className={cn( 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', 'inline-flex h-10 w-full items-center justify-center gap-2 rounded-sm border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-hidden 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, className,
)} )}
disabled={isLoading || isDisabled} disabled={isLoading || isDisabled}

@ -1,9 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import { import {
FIRST_LOGIN_PARAM, FIRST_LOGIN_PARAM,
COURSE_PURCHASE_PARAM, COURSE_PURCHASE_PARAM,
TOKEN_COOKIE_NAME,
setAuthToken, setAuthToken,
} from '../../lib/jwt'; } from '../../lib/jwt';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
@ -11,7 +9,7 @@ import { httpGet } from '../../lib/http';
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx'; import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
import { Spinner } from '../ReactIcons/Spinner.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx';
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx'; import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
import { triggerUtmRegistration } from '../../lib/browser.ts'; import { getLastPath, triggerUtmRegistration, urlToId } from '../../lib/browser.ts';
type LinkedInButtonProps = { type LinkedInButtonProps = {
isDisabled?: boolean; isDisabled?: boolean;
@ -40,10 +38,12 @@ export function LinkedInButton(props: LinkedInButtonProps) {
setIsLoading(true); setIsLoading(true);
setIsDisabled?.(true); setIsDisabled?.(true);
const lastPageBeforeLinkedIn = localStorage.getItem(LINKEDIN_LAST_PAGE);
httpGet<{ token: string; isNewUser: boolean }>( httpGet<{ token: string; isNewUser: boolean }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${ `${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
window.location.search window.location.search
}`, }&src=${urlToId(lastPageBeforeLinkedIn || getLastPath() || window.location.pathname)}`,
) )
.then(({ response, error }) => { .then(({ response, error }) => {
if (!response?.token) { if (!response?.token) {
@ -58,7 +58,6 @@ export function LinkedInButton(props: LinkedInButtonProps) {
let redirectUrl = new URL('/', window.location.origin); let redirectUrl = new URL('/', window.location.origin);
const linkedInRedirectAt = localStorage.getItem(LINKEDIN_REDIRECT_AT); const linkedInRedirectAt = localStorage.getItem(LINKEDIN_REDIRECT_AT);
const lastPageBeforeLinkedIn = localStorage.getItem(LINKEDIN_LAST_PAGE);
// If the social redirect is there and less than 30 seconds old // If the social redirect is there and less than 30 seconds old
// redirect to the page that user was on before they clicked the github login button // redirect to the page that user was on before they clicked the github login button
@ -131,7 +130,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
'/respond-invite', '/respond-invite',
'/befriend', '/befriend',
'/r', '/r',
'/ai', '/ai-roadmaps',
].includes(window.location.pathname) ].includes(window.location.pathname)
? window.location.pathname + window.location.search ? window.location.pathname + window.location.search
: window.location.pathname; : window.location.pathname;
@ -153,7 +152,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
<> <>
<button <button
className={cn( 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', 'inline-flex h-10 w-full items-center justify-center gap-2 rounded-sm border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-hidden hover:border-gray-400 focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60',
className, className,
)} )}
disabled={isLoading || isDisabled} disabled={isLoading || isDisabled}

@ -61,7 +61,7 @@ export function ResetPasswordForm() {
<form className="mx-auto w-full" onSubmit={handleSubmit}> <form className="mx-auto w-full" onSubmit={handleSubmit}>
<input <input
type="password" type="password"
className="mb-2 mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mb-2 mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required required
minLength={6} minLength={6}
placeholder="New Password" placeholder="New Password"
@ -71,7 +71,7 @@ export function ResetPasswordForm() {
<input <input
type="password" type="password"
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required required
minLength={6} minLength={6}
placeholder="Confirm New Password" placeholder="Confirm New Password"
@ -88,7 +88,7 @@ export function ResetPasswordForm() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="mt-2 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" className="mt-2 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-hidden focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
> >
{isLoading ? 'Please wait...' : 'Reset Password'} {isLoading ? 'Please wait...' : 'Reset Password'}
</button> </button>

@ -125,7 +125,7 @@ export function Befriend() {
<div> <div>
<a <a
href="/" href="/"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center" className="grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center"
> >
Back to home Back to home
</a> </a>
@ -141,7 +141,7 @@ export function Befriend() {
const isMe = currentUser?.id === user.id; const isMe = currentUser?.id === user.id;
return ( return (
<div className="container !max-w-[400px] text-center"> <div className="container max-w-[400px]! text-center">
<img <img
alt={'join team'} alt={'join team'}
src={userAvatar} src={userAvatar}
@ -169,7 +169,7 @@ export function Befriend() {
}); });
}} }}
type="button" type="button"
className="w-full flex-grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40" className="w-full grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40"
> >
{isMe ? "You can't add yourself" : 'Add Friend'} {isMe ? "You can't add yourself" : 'Add Friend'}
</button> </button>
@ -177,7 +177,7 @@ export function Befriend() {
{user.status === 'sent' && ( {user.status === 'sent' && (
<> <>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<CheckIcon additionalClasses="mr-2 h-4 w-4" /> <CheckIcon additionalClasses="mr-2 h-4 w-4" />
Request Sent Request Sent
</span> </span>
@ -188,7 +188,7 @@ export function Befriend() {
setIsConfirming(true); setIsConfirming(true);
}} }}
type="button" type="button"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700" className="flex w-full grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
> >
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" /> <DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Withdraw Request Withdraw Request
@ -196,7 +196,7 @@ export function Befriend() {
)} )}
{isConfirming && ( {isConfirming && (
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '} Are you sure?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"
@ -225,7 +225,7 @@ export function Befriend() {
{user.status === 'accepted' && ( {user.status === 'accepted' && (
<> <>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<AddedUserIcon additionalClasses="mr-2 h-5 w-5" /> <AddedUserIcon additionalClasses="mr-2 h-5 w-5" />
You are friends You are friends
</span> </span>
@ -236,7 +236,7 @@ export function Befriend() {
setIsConfirming(true); setIsConfirming(true);
}} }}
type="button" type="button"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700" className="flex w-full grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
> >
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" /> <DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Remove Friend Remove Friend
@ -244,7 +244,7 @@ export function Befriend() {
)} )}
{isConfirming && ( {isConfirming && (
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '} Are you sure?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"
@ -271,12 +271,12 @@ export function Befriend() {
{user.status === 'rejected' && ( {user.status === 'rejected' && (
<> <>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<DeleteUserIcon additionalClasses="mr-2 h-4 w-4" /> <DeleteUserIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected Request Rejected
</span> </span>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Changed your mind?{' '} Changed your mind?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"
@ -296,7 +296,7 @@ export function Befriend() {
{user.status === 'got_rejected' && ( {user.status === 'got_rejected' && (
<> <>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-500 px-3 py-2 text-center text-red-500"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-red-500 px-3 py-2 text-center text-red-500">
<StopIcon additionalClasses="mr-2 h-4 w-4" /> <StopIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected Request Rejected
</span> </span>
@ -311,7 +311,7 @@ export function Befriend() {
pageProgressMessage.set(''); pageProgressMessage.set('');
}); });
}} }}
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-gray-800 bg-gray-800 px-3 py-2 text-center text-white hover:bg-black" className="flex w-full grow cursor-pointer items-center justify-center rounded-lg border border-gray-800 bg-gray-800 px-3 py-2 text-center text-white hover:bg-black"
> >
<CheckIcon additionalClasses="mr-2 h-4 w-4" /> <CheckIcon additionalClasses="mr-2 h-4 w-4" />
Accept Request Accept Request
@ -323,7 +323,7 @@ export function Befriend() {
setIsConfirming(true); setIsConfirming(true);
}} }}
type="button" type="button"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-white px-3 py-2 text-center text-red-600 hover:bg-red-100" className="flex w-full grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-white px-3 py-2 text-center text-red-600 hover:bg-red-100"
> >
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" /> <DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Reject Request Reject Request
@ -331,7 +331,7 @@ export function Befriend() {
)} )}
{isConfirming && ( {isConfirming && (
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600"> <span className="flex w-full grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '} Are you sure?{' '}
<button <button
className="ml-2 text-red-700 underline" className="ml-2 text-red-700 underline"

@ -27,7 +27,7 @@ const isBestPracticeReady = !isUpcoming;
<MarkFavorite <MarkFavorite
resourceId={bestPracticeId} resourceId={bestPracticeId}
resourceType="best-practice" resourceType="best-practice"
className="text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-5 sm:[&>svg]:w-5 ml-1.5 relative focus:outline-0" className="text-gray-500 opacity-100! hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 [&>svg]:hover:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-5 sm:[&>svg]:w-5 ml-1.5 relative focus:outline-0"
client:load client:load
/> />
</h1> </h1>

@ -19,8 +19,10 @@ import {
CreditCard, CreditCard,
ArrowRightLeft, ArrowRightLeft,
CircleX, CircleX,
AlertCircle,
} from 'lucide-react'; } from 'lucide-react';
import { BillingWarning } from './BillingWarning'; import { BillingWarning } from './BillingWarning';
import { cn } from '../../lib/classname';
export type CreateCustomerPortalBody = {}; export type CreateCustomerPortalBody = {};
@ -40,8 +42,12 @@ export function BillingPage() {
); );
const isCanceled = const isCanceled =
billingDetails?.status === 'canceled' || billingDetails?.cancelAtPeriodEnd; billingDetails?.status === 'canceled' ||
billingDetails?.status === 'incomplete_expired' ||
billingDetails?.cancelAtPeriodEnd;
const isPastDue = billingDetails?.status === 'past_due'; const isPastDue = billingDetails?.status === 'past_due';
const isIncomplete = billingDetails?.status === 'incomplete';
const { const {
mutate: createCustomerPortal, mutate: createCustomerPortal,
@ -117,6 +123,19 @@ export function BillingPage() {
!isLoadingBillingDetails && !isLoadingBillingDetails &&
priceDetails && ( priceDetails && (
<div className="mt-1"> <div className="mt-1">
{isIncomplete && (
<BillingWarning
icon={AlertCircle}
message="Your subscription is incomplete "
buttonText="please pay invoice on Stripe."
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
{isCanceled && ( {isCanceled && (
<BillingWarning <BillingWarning
icon={CircleX} icon={CircleX}
@ -157,7 +176,7 @@ export function BillingPage() {
<RefreshCw className="size-5 text-gray-600" /> <RefreshCw className="size-5 text-gray-600" />
</div> </div>
<div> <div>
<span className="text-xs uppercase tracking-wider text-gray-400"> <span className="text-xs tracking-wider text-gray-400 uppercase">
Payment Payment
</span> </span>
<h3 className="flex items-baseline text-lg font-semibold text-black"> <h3 className="flex items-baseline text-lg font-semibold text-black">
@ -170,27 +189,35 @@ export function BillingPage() {
</div> </div>
</div> </div>
<div className="mt-6 border-t border-gray-100 pt-6"> <div
<div className="flex items-start gap-4"> className={cn(
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100"> 'mt-6 pt-6',
<Calendar className="size-5 text-gray-600" /> !isIncomplete && 'border-t border-gray-100',
</div> isIncomplete && '-mt-6',
<div> )}
<span className="text-xs uppercase tracking-wider text-gray-400"> >
{billingDetails?.cancelAtPeriodEnd {!isIncomplete && (
? 'Expires On' <div className="flex items-start gap-4">
: 'Renews On'} <div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
</span> <Calendar className="size-5 text-gray-600" />
<h3 className="text-lg font-semibold text-black"> </div>
{formattedNextBillDate} <div>
</h3> <span className="text-xs tracking-wider text-gray-400 uppercase">
{billingDetails?.cancelAtPeriodEnd
? 'Expires On'
: 'Renews On'}
</span>
<h3 className="text-lg font-semibold text-black">
{formattedNextBillDate}
</h3>
</div>
</div> </div>
</div> )}
<div className="mt-8 flex gap-3 max-sm:flex-col"> <div className="mt-8 flex gap-3 max-sm:flex-col">
{!isCanceled && ( {!isCanceled && !isIncomplete && (
<button <button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 max-sm:flex-grow" className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-xs transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-black focus:ring-offset-2 focus:outline-hidden max-sm:grow"
onClick={() => { onClick={() => {
setShowUpgradeModal(true); setShowUpgradeModal(true);
}} }}
@ -201,7 +228,7 @@ export function BillingPage() {
)} )}
<button <button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-xs transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-black focus:ring-offset-2 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => { onClick={() => {
createCustomerPortal({}); createCustomerPortal({});
}} }}

@ -59,7 +59,7 @@ export function EmptyBillingScreen(props: EmptyBillingScreenProps) {
<button <button
onClick={onUpgrade} onClick={onUpgrade}
className="inline-flex items-center justify-center rounded-lg bg-black px-6 py-2.5 text-sm font-medium text-white transition-colors hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2" className="inline-flex items-center justify-center rounded-lg bg-black px-6 py-2.5 text-sm font-medium text-white transition-colors hover:opacity-80 focus:outline-hidden focus:ring-2 focus:ring-black focus:ring-offset-2"
> >
Upgrade Account Upgrade Account
</button> </button>

@ -5,6 +5,7 @@ import {
MessageSquare, MessageSquare,
Sparkles, Sparkles,
Heart, Heart,
MapIcon,
} from 'lucide-react'; } from 'lucide-react';
import type { LucideIcon } from 'lucide-react'; import type { LucideIcon } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -33,17 +34,22 @@ type Perk = {
const PREMIUM_PERKS: Perk[] = [ const PREMIUM_PERKS: Perk[] = [
{ {
icon: Zap, icon: Zap,
title: 'Unlimited AI Course Generations', title: 'AI Course Generations',
description: 'Generate as many custom courses as you need', description: 'No limits on the number of AI courses',
},
{
icon: MapIcon,
title: 'AI Roadmaps',
description: 'No limits on the number of AI roadmaps',
}, },
{ {
icon: Infinity, icon: Infinity,
title: 'No Daily Limits on course features', title: 'Extended Daily Limits',
description: 'Use all features without restrictions', description: 'Generate more content in a day',
}, },
{ {
icon: MessageSquare, icon: MessageSquare,
title: 'Unlimited Course Follow-ups', title: 'Course Follow-ups',
description: 'Ask as many questions as you need', description: 'Ask as many questions as you need',
}, },
{ {
@ -236,12 +242,12 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
</p> </p>
</div> </div>
<div className="flex-grow"></div> <div className="grow"></div>
<div> <div>
<button <button
className={cn( className={cn(
'flex min-h-9 w-full items-center justify-center rounded-md py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 disabled:cursor-not-allowed disabled:opacity-60 sm:min-h-11 sm:py-2.5 sm:text-base', 'flex min-h-9 w-full items-center justify-center rounded-md py-2 text-sm font-medium transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-yellow-400 disabled:cursor-not-allowed disabled:opacity-60 sm:min-h-11 sm:py-2.5 sm:text-base',
'bg-yellow-400 text-black hover:bg-yellow-500', 'bg-yellow-400 text-black hover:bg-yellow-500',
)} )}
disabled={ disabled={
@ -284,7 +290,10 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
{PREMIUM_PERKS.map((perk, index) => { {PREMIUM_PERKS.map((perk, index) => {
const Icon = perk.icon; const Icon = perk.icon;
return ( return (
<div key={index} className="flex items-start space-x-2 sm:space-x-3"> <div
key={index}
className="flex items-start space-x-2 sm:space-x-3"
>
<Icon className="mt-0.5 h-4 w-4 text-yellow-500 sm:h-5 sm:w-5" /> <Icon className="mt-0.5 h-4 w-4 text-yellow-500 sm:h-5 sm:w-5" />
<div> <div>
<h4 className="text-sm font-medium text-black sm:text-base"> <h4 className="text-sm font-medium text-black sm:text-base">

@ -17,11 +17,11 @@ const formattedDate = DateTime.fromISO(frontmatter.date).toFormat(
<div class='relative mb-6' id={changelog.id}> <div class='relative mb-6' id={changelog.id}>
<span <span
class='absolute -left-6 top-2 h-2 w-2 flex-shrink-0 rounded-full bg-gray-300' class='absolute -left-6 top-2 h-2 w-2 shrink-0 rounded-full bg-gray-300'
></span> ></span>
<div class='mb-3 flex flex-col sm:flex-row items-start sm:items-center gap-0.5 sm:gap-2'> <div class='mb-3 flex flex-col sm:flex-row items-start sm:items-center gap-0.5 sm:gap-2'>
<span class='flex-shrink-0 text-xs tracking-wide text-gray-400'> <span class='shrink-0 text-xs tracking-wide text-gray-400'>
{formattedDate} {formattedDate}
</span> </span>
<span class='truncate text-base font-medium text-balance'> <span class='truncate text-base font-medium text-balance'>

@ -6,13 +6,13 @@ const formattedDate = DateTime.fromISO('2024-09-13').toFormat('dd LLL, yyyy');
<div class='relative mb-6'> <div class='relative mb-6'>
<span <span
class='absolute -left-6 top-2 h-2 w-2 flex-shrink-0 rounded-full bg-gray-300' class='absolute -left-6 top-2 h-2 w-2 shrink-0 rounded-full bg-gray-300'
></span> ></span>
<div <div
class='mb-3 flex flex-col items-start gap-0.5 sm:flex-row sm:items-center sm:gap-2' class='mb-3 flex flex-col items-start gap-0.5 sm:flex-row sm:items-center sm:gap-2'
> >
<span class='flex-shrink-0 text-xs tracking-wide text-gray-400'> <span class='shrink-0 text-xs tracking-wide text-gray-400'>
{formattedDate} {formattedDate}
</span> </span>
<span class='truncate text-balance text-base font-medium'> <span class='truncate text-balance text-base font-medium'>

@ -7,7 +7,7 @@ const top10Changelogs = allChangelogs.slice(0, 10);
--- ---
<div class='border-t bg-white py-6 text-left sm:py-16 sm:text-center'> <div class='border-t bg-white py-6 text-left sm:py-16 sm:text-center'>
<div class='container !max-w-[650px]'> <div class='container max-w-[650px]!'>
<p class='text-2xl font-bold sm:text-5xl'> <p class='text-2xl font-bold sm:text-5xl'>
<img <img
src='/images/gifs/rocket.gif' src='/images/gifs/rocket.gif'
@ -40,10 +40,10 @@ const top10Changelogs = allChangelogs.slice(0, 10);
href={`/changelog#${changelog.id}`} href={`/changelog#${changelog.id}`}
class='flex flex-col items-start sm:flex-row sm:items-center' class='flex flex-col items-start sm:flex-row sm:items-center'
> >
<span class='flex-shrink-0 pr-0 text-right text-sm tracking-wide text-gray-400 sm:w-[120px] sm:pr-4'> <span class='shrink-0 pr-0 text-right text-sm tracking-wide text-gray-400 sm:w-[120px] sm:pr-4'>
{formattedDate} {formattedDate}
</span> </span>
<span class='hidden h-3 w-3 flex-shrink-0 rounded-full bg-gray-300 sm:block' /> <span class='hidden h-3 w-3 shrink-0 rounded-full bg-gray-300 sm:block' />
<span class='text-balance text-base font-medium text-gray-900 sm:pl-8'> <span class='text-balance text-base font-medium text-gray-900 sm:pl-8'>
{changelog.frontmatter.title} {changelog.frontmatter.title}
</span> </span>

@ -1,4 +1,4 @@
import { ChevronLeft, ChevronRight, MoveRight } from 'lucide-react'; import { ChevronLeft, ChevronRight } from 'lucide-react';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
interface ChangelogImagesProps { interface ChangelogImagesProps {
@ -63,17 +63,17 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
alt={title} alt={title}
className="h-[120px] w-full object-cover object-left-top" className="h-[120px] w-full object-cover object-left-top"
/> />
<span className="absolute group-hover:opacity-0 inset-0 bg-gradient-to-b from-transparent to-black/40" /> <span className="absolute group-hover:opacity-0 inset-0 bg-linear-to-b from-transparent to-black/40" />
<div className="absolute font-medium inset-x-0 top-full group-hover:inset-y-0 flex items-center justify-center px-2 text-center text-xs bg-black/50 text-white py-0.5 opacity-0 group-hover:opacity-100 cursor-pointer"> <div className="absolute font-medium inset-x-0 top-full group-hover:inset-y-0 flex items-center justify-center px-2 text-center text-xs bg-black/50 text-white py-0.5 opacity-0 group-hover:opacity-100 cursor-pointer">
<span className='bg-black py-0.5 rounded px-1'>{title}</span> <span className='bg-black py-0.5 rounded-sm px-1'>{title}</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
{enlargedImage && ( {enlargedImage && (
<div <div
className="fixed inset-0 z-[999] flex items-center justify-center bg-black bg-opacity-75" className="fixed inset-0 z-999 flex items-center justify-center bg-black/75"
onClick={handleCloseEnlarged} onClick={handleCloseEnlarged}
> >
<img <img
@ -82,7 +82,7 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
className="max-h-[90%] max-w-[90%] rounded-xl object-contain" className="max-h-[90%] max-w-[90%] rounded-xl object-contain"
/> />
<button <button
className="absolute left-4 top-1/2 -translate-y-1/2 transform rounded-full bg-white hover:bg-opacity-100 bg-opacity-50 p-2" className="absolute left-4 top-1/2 -translate-y-1/2 transform rounded-full bg-white/50 hover:bg-white/100 p-2"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleNavigation('prev'); handleNavigation('prev');
@ -91,7 +91,7 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
<ChevronLeft size={24} /> <ChevronLeft size={24} />
</button> </button>
<button <button
className="absolute right-4 top-1/2 -translate-y-1/2 transform rounded-full bg-white hover:bg-opacity-100 bg-opacity-50 p-2" className="absolute right-4 top-1/2 -translate-y-1/2 transform rounded-full bg-white/50 hover:bg-white/100 p-2"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleNavigation('next'); handleNavigation('next');

@ -194,13 +194,13 @@ export function CommandMenu() {
return ( return (
<div className="fixed left-0 right-0 top-0 z-50 flex h-full justify-center overflow-y-auto overflow-x-hidden bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 flex h-full justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div className="relative top-0 h-full w-full max-w-lg p-2 sm:mt-20 md:h-auto"> <div className="relative top-0 h-full w-full max-w-lg p-2 sm:mt-20 md:h-auto">
<div className="relative rounded-lg bg-white shadow" ref={modalRef}> <div className="relative rounded-lg bg-white shadow-sm" ref={modalRef}>
<input <input
ref={inputRef} ref={inputRef}
autoFocus={true} autoFocus={true}
type="text" type="text"
value={searchedText} value={searchedText}
className="w-full rounded-t-md border-b p-4 text-sm focus:bg-gray-50 focus:outline-none" className="w-full rounded-t-md border-b p-4 text-sm focus:bg-gray-50 focus:outline-hidden"
placeholder="Search roadmaps, guides or pages .." placeholder="Search roadmaps, guides or pages .."
autoComplete="off" autoComplete="off"
onInput={(e) => { onInput={(e) => {
@ -249,7 +249,7 @@ export function CommandMenu() {
)} )}
<a <a
className={cn( className={cn(
'flex w-full items-center rounded p-2 text-sm', 'flex w-full items-center rounded-sm p-2 text-sm',
counter === activeCounter ? 'bg-gray-100' : '', counter === activeCounter ? 'bg-gray-100' : '',
)} )}
onMouseOver={() => setActiveCounter(counter)} onMouseOver={() => setActiveCounter(counter)}

@ -286,7 +286,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
className="relative flex flex-col items-start overflow-hidden rounded-md border border-gray-300" className="relative flex flex-col items-start overflow-hidden rounded-md border border-gray-300"
key={resourceId} key={resourceId}
> >
<div className={'w-full flex-grow px-3 pb-2 pt-4'}> <div className={'w-full grow px-3 pb-2 pt-4'}>
<span className="mb-0.5 block text-base font-medium leading-snug text-black"> <span className="mb-0.5 block text-base font-medium leading-snug text-black">
{roadmapTitle} {roadmapTitle}
</span> </span>
@ -341,7 +341,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
<button <button
type="button" type="button"
className={ className={
'text-xs text-gray-500 underline hover:text-black focus:outline-none' 'text-xs text-gray-500 underline hover:text-black focus:outline-hidden'
} }
onClick={() => { onClick={() => {
if (isCustomResource) { if (isCustomResource) {

@ -68,11 +68,11 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
); );
return ( 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 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 className="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto"> <div className="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
className="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow" className="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow-sm"
> >
<button <button
type="button" type="button"
@ -86,7 +86,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
ref={searchInputEl} ref={searchInputEl}
type="text" type="text"
placeholder="Search roadmaps" placeholder="Search roadmaps"
className="block w-full border-b px-5 pb-3.5 pt-4 outline-none placeholder:text-gray-400" className="block w-full border-b px-5 pb-3.5 pt-4 outline-hidden placeholder:text-gray-400"
value={searchText} value={searchText}
onInput={(e) => setSearchText((e.target as HTMLInputElement).value)} onInput={(e) => setSearchText((e.target as HTMLInputElement).value)}
/> />

@ -76,7 +76,7 @@ export function Step0(props: Step0Props) {
{validTeamTypes.map((validTeamType) => ( {validTeamTypes.map((validTeamType) => (
<button <button
key={validTeamType.value} key={validTeamType.value}
className={`flex flex-grow flex-col items-center rounded-lg border px-5 pb-10 pt-12 ${ className={`flex grow flex-col items-center rounded-lg border px-5 pb-10 pt-12 ${
validTeamType.value == selectedTeamType validTeamType.value == selectedTeamType
? 'border-gray-400 bg-gray-100' ? 'border-gray-400 bg-gray-100'
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50' : 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'

@ -135,7 +135,7 @@ export function Step1(props: Step1Props) {
ref={nameRef as any} ref={nameRef as any}
autoFocus={true} autoFocus={true}
id="name" id="name"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Roadmap Inc." placeholder="Roadmap Inc."
disabled={isLoading} disabled={isLoading}
required required
@ -157,7 +157,7 @@ export function Step1(props: Step1Props) {
name="website" name="website"
required required
id="website" id="website"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="https://roadmap.sh" placeholder="https://roadmap.sh"
disabled={isLoading} disabled={isLoading}
value={website} value={website}
@ -178,7 +178,7 @@ export function Step1(props: Step1Props) {
type="url" type="url"
name="website" name="website"
id="website" id="website"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="https://www.linkedin.com/company/roadmapsh" placeholder="https://www.linkedin.com/company/roadmapsh"
disabled={isLoading} disabled={isLoading}
value={linkedInUrl} value={linkedInUrl}
@ -200,7 +200,7 @@ export function Step1(props: Step1Props) {
type="url" type="url"
name="website" name="website"
id="website" id="website"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="https://github.com/roadmapsh" placeholder="https://github.com/roadmapsh"
disabled={isLoading} disabled={isLoading}
value={gitHubUrl} value={gitHubUrl}
@ -219,7 +219,7 @@ export function Step1(props: Step1Props) {
<select <select
name="team-size" name="team-size"
id="team-size" id="team-size"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-xs outline-hidden placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
required={selectedTeamType === 'company'} required={selectedTeamType === 'company'}
disabled={isLoading} disabled={isLoading}
value={teamSize} value={teamSize}

@ -50,7 +50,7 @@ export function Step2(props: Step2Props) {
onClick={onNext} onClick={onNext}
disabled={teamResourceConfig.length !== 0} disabled={teamResourceConfig.length !== 0}
className={ className={
'flex-grow rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black md:flex-auto disabled:opacity-50 disabled:pointer-events-none' 'grow rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black md:flex-auto disabled:opacity-50 disabled:pointer-events-none'
} }
> >
Skip for Now Skip for Now

@ -109,7 +109,7 @@ export function Step3(props: Step3Props) {
setUsers(newUsers); setUsers(newUsers);
}} }}
className="flex-grow rounded-md border border-gray-200 bg-white px-4 py-2 text-gray-900" className="grow rounded-md border border-gray-200 bg-white px-4 py-2 text-gray-900"
/> />
<RoleDropdown <RoleDropdown
selectedRole={user.role} selectedRole={user.role}
@ -180,7 +180,7 @@ export function Step3(props: Step3Props) {
onClick={onNext} onClick={onNext}
disabled={users.filter((u) => u.email).length !== 0} disabled={users.filter((u) => u.email).length !== 0}
className={ className={
'rounded-md flex-grow md:flex-auto border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black disabled:opacity-50 disabled:pointer-events-none' 'rounded-md grow md:flex-auto border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black disabled:opacity-50 disabled:pointer-events-none'
} }
> >
Skip for Now Skip for Now

@ -148,12 +148,12 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
}, []); }, []);
return ( 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 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 className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"> <div className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
<div <div
id={'customized-roadmap'} id={'customized-roadmap'}
ref={popupBodyEl} ref={popupBodyEl}
className="popup-body relative rounded-lg bg-white shadow" className="popup-body relative rounded-lg bg-white shadow-sm"
> >
<div <div
className={ className={

@ -179,7 +179,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="title" name="title"
id="title" id="title"
required required
className="block w-full rounded-md border border-gray-300 px-2.5 py-2 text-black outline-none focus:border-black sm:text-sm" className="block w-full rounded-md border border-gray-300 px-2.5 py-2 text-black outline-hidden focus:border-black sm:text-sm"
placeholder="Enter Title" placeholder="Enter Title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
@ -199,7 +199,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="description" name="description"
required required
className={cn( className={cn(
'block h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 text-black outline-none focus:border-black sm:text-sm', 'block h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 text-black outline-hidden focus:border-black sm:text-sm',
isInvalidDescription && 'border-red-300 bg-red-100', isInvalidDescription && 'border-red-300 bg-red-100',
)} )}
placeholder="Enter Description" placeholder="Enter Description"
@ -219,7 +219,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
onClick={onClose} onClick={onClose}
type="button" type="button"
className={cn( className={cn(
'block h-9 rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-black outline-none hover:border-gray-300 hover:bg-gray-50 focus:border-gray-300 focus:bg-gray-100', 'block h-9 rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-black outline-hidden hover:border-gray-300 hover:bg-gray-50 focus:border-gray-300 focus:bg-gray-100',
!teamId && 'w-full', !teamId && 'w-full',
)} )}
> >
@ -232,7 +232,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
disabled={isLoading} disabled={isLoading}
type="button" type="button"
onClick={(e) => handleSubmit(e, false)} onClick={(e) => handleSubmit(e, false)}
className="flex h-9 items-center justify-center rounded-md border border-black bg-white px-4 py-2 text-sm font-medium text-black outline-none hover:bg-black hover:text-white focus:bg-black focus:text-white" className="flex h-9 items-center justify-center rounded-md border border-black bg-white px-4 py-2 text-sm font-medium text-black outline-hidden hover:bg-black hover:text-white focus:bg-black focus:text-white"
> >
{isLoading ? ( {isLoading ? (
<Loader2 size={16} className="animate-spin" /> <Loader2 size={16} className="animate-spin" />
@ -246,7 +246,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
disabled={isLoading} disabled={isLoading}
type="submit" type="submit"
className={cn( className={cn(
'flex h-9 items-center justify-center rounded-md border border-transparent bg-black px-4 py-2 text-sm font-medium text-white outline-none hover:bg-gray-800 focus:bg-gray-800', 'flex h-9 items-center justify-center rounded-md border border-transparent bg-black px-4 py-2 text-sm font-medium text-white outline-hidden hover:bg-gray-800 focus:bg-gray-800',
teamId ? 'hidden sm:flex' : 'w-full', teamId ? 'hidden sm:flex' : 'w-full',
)} )}
> >

@ -53,7 +53,7 @@ export function EmbedRoadmapModal(props: ShareRoadmapModalProps) {
<div className="flex items-center justify-between px-4 pb-4 pt-2"> <div className="flex items-center justify-between px-4 pb-4 pt-2">
<button <button
className={cn( className={cn(
'flex h-9 w-full items-center justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white outline-none', 'flex h-9 w-full items-center justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white outline-hidden',
{ {
'bg-green-500 hover:bg-green-600 focus:bg-green-600': isCopied, 'bg-green-500 hover:bg-green-600 focus:bg-green-600': isCopied,
'bg-gray-900 hover:bg-gray-800 focus:bg-gray-800': !isCopied, 'bg-gray-900 hover:bg-gray-800 focus:bg-gray-800': !isCopied,

@ -1,4 +1,4 @@
import { ReadonlyEditor } from '../../../editor/readonly-editor'; import { ReadonlyEditor } from '@roadmapsh/editor';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal'; import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import { import {
refreshProgressCounters, refreshProgressCounters,
@ -9,7 +9,7 @@ import {
} from '../../lib/resource-progress'; } from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import type { Node } from 'reactflow'; import type { Node } from '@roadmapsh/editor';
import { type MouseEvent, useCallback, useRef, useState } from 'react'; import { type MouseEvent, useCallback, useRef, useState } from 'react';
import { EmptyRoadmap } from './EmptyRoadmap'; import { EmptyRoadmap } from './EmptyRoadmap';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';

@ -34,7 +34,7 @@ export function PersonalRoadmapActionDropdown(
<button <button
disabled={false} disabled={false}
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1 rounded-md border border-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-none sm:hidden" className="flex items-center gap-1 rounded-md border border-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-hidden sm:hidden"
> >
<MoreVertical size={14} /> <MoreVertical size={14} />
Options Options
@ -53,7 +53,7 @@ export function PersonalRoadmapActionDropdown(
setIsOpen(false); setIsOpen(false);
onUpdateSharing(); onUpdateSharing();
}} }}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center rounded-sm p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
> >
<Lock size={14} className="mr-2" /> <Lock size={14} className="mr-2" />
Sharing Sharing
@ -67,7 +67,7 @@ export function PersonalRoadmapActionDropdown(
setIsOpen(false); setIsOpen(false);
onCustomize(); onCustomize();
}} }}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center rounded-sm p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
> >
<Shapes size={14} className="mr-2" /> <Shapes size={14} className="mr-2" />
Customize Customize
@ -81,7 +81,7 @@ export function PersonalRoadmapActionDropdown(
setIsOpen(false); setIsOpen(false);
onDelete(); onDelete();
}} }}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center rounded-sm p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
> >
<Trash2 size={14} className="mr-2" /> <Trash2 size={14} className="mr-2" />
Delete Delete

@ -176,7 +176,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
<a <a
href={editorLink} href={editorLink}
className={ className={
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-black hover:bg-gray-50 focus:outline-none' 'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-black hover:bg-gray-50 focus:outline-hidden'
} }
target={'_blank'} target={'_blank'}
> >
@ -186,7 +186,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
<a <a
href={`/r/${roadmap?.slug}`} href={`/r/${roadmap?.slug}`}
className={ className={
'ml-2 flex items-center gap-2 rounded-md border border-blue-400 bg-white px-2 py-1.5 text-xs text-blue-600 hover:bg-blue-50 focus:outline-none' 'ml-2 flex items-center gap-2 rounded-md border border-blue-400 bg-white px-2 py-1.5 text-xs text-blue-600 hover:bg-blue-50 focus:outline-hidden'
} }
target={'_blank'} target={'_blank'}
> >

@ -231,7 +231,7 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) {
</label> </label>
<textarea <textarea
id="rating-feedback" id="rating-feedback"
className="min-h-24 rounded-md border p-2 text-sm outline-none focus:border-gray-500" className="min-h-24 rounded-md border p-2 text-sm outline-hidden focus:border-gray-500"
placeholder="Share your thoughts with the roadmap creator" placeholder="Share your thoughts with the roadmap creator"
value={userFeedback} value={userFeedback}
onChange={(e) => { onChange={(e) => {

@ -55,7 +55,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
className="flex text-sm opacity-0 transition-opacity duration-300" className="flex text-sm opacity-0 transition-opacity duration-300"
data-progress-nums="" data-progress-nums=""
> >
<span className="mr-2.5 rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <span className="mr-2.5 rounded-xs bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span data-progress-percentage="">0</span>% Done <span data-progress-percentage="">0</span>% Done
</span> </span>

@ -32,7 +32,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
{isOpen && ( {isOpen && (
<div <div
ref={menuRef} ref={menuRef}
className="align-right absolute right-0 top-full z-[9999] mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md" className="align-right absolute right-0 top-full z-9999 mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md"
> >
<ul> <ul>
{onCustomize && ( {onCustomize && (
@ -42,7 +42,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
setIsOpen(false); setIsOpen(false);
onCustomize(); onCustomize();
}} }}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center rounded-sm p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
> >
<PenSquare size={14} className="mr-2" /> <PenSquare size={14} className="mr-2" />
Edit Edit
@ -56,7 +56,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
setIsOpen(false); setIsOpen(false);
onUpdateSharing(); onUpdateSharing();
}} }}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center rounded-sm p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
> >
<Lock size={14} className="mr-2" /> <Lock size={14} className="mr-2" />
Sharing Sharing
@ -70,7 +70,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
setIsOpen(false); setIsOpen(false);
onDelete(); onDelete();
}} }}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center rounded-sm p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
> >
<Trash2 size={14} className="mr-2" /> <Trash2 size={14} className="mr-2" />
Delete Delete

@ -128,7 +128,7 @@ export function ShareRoadmapModal(props: ShareRoadmapModalProps) {
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<button <button
disabled={isLoading} disabled={isLoading}
className="flex h-9 items-center rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-black outline-none hover:border-gray-300 hover:bg-gray-50 focus:border-gray-300 focus:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-70" className="flex h-9 items-center rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-black outline-hidden hover:border-gray-300 hover:bg-gray-50 focus:border-gray-300 focus:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-70"
onClick={onClose} onClick={onClose}
> >
{isLoading ? ( {isLoading ? (
@ -141,7 +141,7 @@ export function ShareRoadmapModal(props: ShareRoadmapModalProps) {
)} )}
</button> </button>
<button <button
className="flex h-9 items-center justify-center rounded-md border border-transparent bg-gray-900 px-4 py-2 text-sm font-medium text-white outline-none hover:bg-gray-800 focus:bg-gray-800" className="flex h-9 items-center justify-center rounded-md border border-transparent bg-gray-900 px-4 py-2 text-sm font-medium text-white outline-hidden hover:bg-gray-800 focus:bg-gray-800"
onClick={handleCopy} onClick={handleCopy}
> >
{isCopied ? ( {isCopied ? (

@ -92,7 +92,7 @@ export function SharedRoadmapList(props: SharedRoadmapListProps) {
> >
<a <a
href={`/r/=${roadmap?.slug}`} href={`/r/=${roadmap?.slug}`}
className="group inline-grid w-full grid-cols-[auto,16px] items-center justify-between gap-2 px-3 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-100 hover:text-black" className="group inline-grid w-full grid-cols-[auto_16px] items-center justify-between gap-2 px-3 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-100 hover:text-black"
target={'_blank'} target={'_blank'}
> >
<span className="w-full truncate"> <span className="w-full truncate">

@ -79,7 +79,7 @@ export function SubmitShowcaseWarning(props: SubmitShowcaseWarningProps) {
<div className="mt-4 grid grid-cols-2 gap-2"> <div className="mt-4 grid grid-cols-2 gap-2">
<button <button
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center text-sm hover:bg-gray-300" className="grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center text-sm hover:bg-gray-300"
onClick={onClose} onClick={onClose}
disabled={submit.isPending} disabled={submit.isPending}
> >

@ -1,15 +1,6 @@
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import { DashboardCustomProgressCard } from './DashboardCustomProgressCard';
import { DashboardCardLink } from './DashboardCardLink'; import { DashboardCardLink } from './DashboardCardLink';
import { useState } from 'react';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { Simulate } from 'react-dom/test-utils';
import { import {
ArrowUpRight, BrainCircuit
Bot,
BrainCircuit,
Map,
PencilRuler,
} from 'lucide-react'; } from 'lucide-react';
type DashboardAiRoadmapsProps = { type DashboardAiRoadmapsProps = {
@ -30,7 +21,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
<h2 className="text-xs uppercase text-gray-400">My AI Roadmaps</h2> <h2 className="text-xs uppercase text-gray-400">My AI Roadmaps</h2>
<a <a
href="/ai/explore" href="/ai-roadmaps/explore"
className="rounded-full bg-gray-200 px-2.5 py-0.5 text-xs font-medium text-gray-700 hover:bg-gray-300 hover:text-black" className="rounded-full bg-gray-200 px-2.5 py-0.5 text-xs font-medium text-gray-700 hover:bg-gray-300 hover:text-black"
> >
AI Generated Roadmaps AI Generated Roadmaps
@ -41,7 +32,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
<DashboardCardLink <DashboardCardLink
className="mt-0" className="mt-0"
icon={BrainCircuit} icon={BrainCircuit}
href="/ai" href="/ai-roadmaps"
title="Generate Roadmaps with AI" title="Generate Roadmaps with AI"
description="You can generate your own roadmap with AI" description="You can generate your own roadmap with AI"
/> />
@ -61,8 +52,8 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
{roadmaps.map((roadmap) => ( {roadmaps.map((roadmap) => (
<a <a
key={roadmap.id} key={roadmap.id}
href={`/ai/${roadmap.slug}`} href={`/ai-roadmaps/${roadmap.slug}`}
className="relative truncate rounded-md border bg-white p-2.5 text-left text-sm shadow-sm hover:border-gray-400 hover:bg-gray-50" className="relative truncate rounded-md border bg-white p-2.5 text-left text-sm shadow-xs hover:border-gray-400 hover:bg-gray-50"
> >
{roadmap.title} {roadmap.title}
</a> </a>
@ -70,7 +61,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
<a <a
className="flex items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-2.5 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600" className="flex items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-2.5 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600"
href={'/ai'} href={'/ai-roadmaps'}
> >
+ Generate New + Generate New
</a> </a>

@ -15,7 +15,7 @@ export function DashboardCardLink(props: DashboardCardLinkProps) {
return ( return (
<a <a
className={cn( className={cn(
'relative mt-4 flex min-h-[220px] flex-col justify-end rounded-lg border border-gray-300 bg-gradient-to-br from-white to-gray-50 py-5 px-6 hover:border-gray-400 hover:from-white hover:to-gray-100', 'relative mt-4 flex min-h-[220px] flex-col justify-end rounded-lg border border-gray-300 bg-linear-to-br from-white to-gray-50 py-5 px-6 hover:border-gray-400 hover:from-white hover:to-gray-100',
className, className,
)} )}
href={href} href={href}

@ -36,7 +36,7 @@ export function DashboardCustomProgressCard(props: DashboardCustomProgressCardPr
return ( return (
<a <a
href={url} href={url}
className="group relative flex min-h-[80px] w-full flex-col justify-between overflow-hidden rounded-md border bg-white p-3 text-left text-sm shadow-sm transition-all hover:border-gray-400 hover:bg-gray-50" className="group relative flex min-h-[80px] w-full flex-col justify-between overflow-hidden rounded-md border bg-white p-3 text-left text-sm shadow-xs transition-all hover:border-gray-400 hover:bg-gray-50"
> >
<h4 className="truncate font-medium text-gray-900">{resourceTitle}</h4> <h4 className="truncate font-medium text-gray-900">{resourceTitle}</h4>

@ -1,6 +1,5 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { cn } from '../../../editor/utils/classname';
import { useParams } from '../../hooks/use-params'; import { useParams } from '../../hooks/use-params';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
@ -13,6 +12,7 @@ import { TeamDashboard } from './TeamDashboard';
import type { QuestionGroupType } from '../../lib/question-group'; import type { QuestionGroupType } from '../../lib/question-group';
import type { GuideFileType } from '../../lib/guide'; import type { GuideFileType } from '../../lib/guide';
import type { VideoFileType } from '../../lib/video'; import type { VideoFileType } from '../../lib/video';
import { cn } from '../../lib/classname';
type DashboardPageProps = { type DashboardPageProps = {
builtInRoleRoadmaps?: BuiltInRoadmap[]; builtInRoleRoadmaps?: BuiltInRoadmap[];

@ -37,7 +37,7 @@ export function DashboardProgressCard(props: DashboardProgressCardProps) {
key={resourceId} key={resourceId}
className="group relative flex w-full items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400" className="group relative flex w-full items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400"
> >
<span className="flex-grow truncate">{resourceTitle}</span> <span className="grow truncate">{resourceTitle}</span>
<span className="text-xs text-gray-400"> <span className="text-xs text-gray-400">
{parseInt(progressPercentage, 10)}% {parseInt(progressPercentage, 10)}%
</span> </span>

@ -25,7 +25,7 @@ export function DashboardProjectCard(props: DashboardProjectCardProps) {
> >
<span <span
className={cn( className={cn(
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full', 'flex h-5 w-5 shrink-0 items-center justify-center rounded-full',
{ {
'border border-green-500 bg-green-500 group-hover:border-green-600 group-hover:bg-green-600': 'border border-green-500 bg-green-500 group-hover:border-green-600 group-hover:bg-green-600':
status === 'submitted', status === 'submitted',
@ -41,8 +41,8 @@ export function DashboardProjectCard(props: DashboardProjectCardProps) {
/> />
)} )}
</span> </span>
<span className="flex-grow truncate group-hover:underline">{title.replace(/(System)|(Service)/, '')}</span> <span className="grow truncate group-hover:underline">{title.replace(/(System)|(Service)/, '')}</span>
<span className="flex-shrink-0 bg-transparent text-xs text-gray-400 no-underline"> <span className="shrink-0 bg-transparent text-xs text-gray-400 no-underline">
{!!startedAt && {!!startedAt &&
status === 'started' && status === 'started' &&
getRelativeTimeString(startedAt)} getRelativeTimeString(startedAt)}

@ -206,7 +206,7 @@ export function DashboardTeamRoadmaps(props: DashboardTeamRoadmapsProps) {
const roadmapHeading = ( const roadmapHeading = (
<div className="mb-3 flex h-[20px] items-center justify-between gap-2 text-xs"> <div className="mb-3 flex h-[20px] items-center justify-between gap-2 text-xs">
<h2 className="uppercase text-gray-400">Roadmaps</h2> <h2 className="uppercase text-gray-400">Roadmaps</h2>
<span className="mx-3 h-[1px] flex-grow bg-gray-200" /> <span className="mx-3 h-[1px] grow bg-gray-200" />
{canManageCurrentTeam && ( {canManageCurrentTeam && (
<a <a
href={`/team/roadmaps?t=${teamId}`} href={`/team/roadmaps?t=${teamId}`}

@ -17,7 +17,7 @@ export function EmptyStackMessage(props: EmptyStackMessageProps) {
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-black/50"> <div className="absolute inset-0 flex items-center justify-center rounded-md bg-black/50">
<div <div
className={cn( className={cn(
'flex max-w-[200px] flex-col items-center justify-center rounded-md bg-white p-4 shadow-sm', 'flex max-w-[200px] flex-col items-center justify-center rounded-md bg-white p-4 shadow-xs',
bodyClassName, bodyClassName,
)} )}
> >

@ -3,13 +3,8 @@ import { DashboardCustomProgressCard } from './DashboardCustomProgressCard';
import { DashboardCardLink } from './DashboardCardLink'; import { DashboardCardLink } from './DashboardCardLink';
import { useState } from 'react'; import { useState } from 'react';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { Simulate } from 'react-dom/test-utils';
import { import {
ArrowUpRight, BrainCircuit, PencilRuler
Bot,
BrainCircuit,
Map,
PencilRuler,
} from 'lucide-react'; } from 'lucide-react';
type ListDashboardCustomProgressProps = { type ListDashboardCustomProgressProps = {
@ -63,7 +58,7 @@ export function ListDashboardCustomProgress(
<DashboardCardLink <DashboardCardLink
className="mt-0" className="mt-0"
icon={BrainCircuit} icon={BrainCircuit}
href="/ai" href="/ai-roadmaps"
title="Generate Roadmaps with AI" title="Generate Roadmaps with AI"
description="You can generate your own roadmap with AI" description="You can generate your own roadmap with AI"
/> />
@ -99,7 +94,7 @@ export function ListDashboardCustomProgress(
<a <a
className="flex min-h-[80px] items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-4 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600" className="flex min-h-[80px] items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-4 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600"
href={'/ai'} href={'/ai-roadmaps'}
onClick={(e) => { onClick={(e) => {
if (!isAIGeneratedRoadmaps) { if (!isAIGeneratedRoadmaps) {
e.preventDefault(); e.preventDefault();

@ -392,7 +392,7 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
isLoading={isLoading} isLoading={isLoading}
/> />
<div className="bg-gradient-to-b from-slate-900 to-black pb-12"> <div className="bg-linear-to-b from-slate-900 to-black pb-12">
<div className="relative mt-6 border-t border-t-[#1e293c] pt-12"> <div className="relative mt-6 border-t border-t-[#1e293c] pt-12">
<div className="container"> <div className="container">
<h2 <h2

@ -67,7 +67,7 @@ function ProgressLane(props: ProgressLaneProps) {
return ( return (
<div <div
className={cn( className={cn(
'flex h-full flex-col rounded-md border bg-white px-4 py-3 shadow-sm', 'flex h-full flex-col rounded-md border bg-white px-4 py-3 shadow-xs',
className, className,
)} )}
> >
@ -92,7 +92,7 @@ function ProgressLane(props: ProgressLaneProps) {
</div> </div>
)} )}
<div className="mt-4 flex flex-grow flex-col gap-1.5"> <div className="mt-4 flex grow flex-col gap-1.5">
{isLoading && ( {isLoading && (
<div <div
className={cn('grid grid-cols-2 gap-2', loadingWrapperClassName)} className={cn('grid grid-cols-2 gap-2', loadingWrapperClassName)}
@ -105,7 +105,7 @@ function ProgressLane(props: ProgressLaneProps) {
{!isLoading && children} {!isLoading && children}
{!isLoading && isEmpty && ( {!isLoading && isEmpty && (
<div className="flex flex-grow flex-col items-center justify-center text-gray-500"> <div className="flex grow flex-col items-center justify-center text-gray-500">
<EmptyIcon <EmptyIcon
size={37} size={37}
strokeWidth={1.5} strokeWidth={1.5}
@ -201,7 +201,7 @@ export function ProgressStack(props: ProgressStackProps) {
emptyLinkHref={'/roadmaps'} emptyLinkHref={'/roadmaps'}
emptyLinkText={'Explore Roadmaps'} emptyLinkText={'Explore Roadmaps'}
> >
<div className="grid flex-grow auto-rows-min grid-cols-2 items-start gap-2"> <div className="grid grow auto-rows-min grid-cols-2 items-start gap-2">
{userProgressesToShow.length > 0 && ( {userProgressesToShow.length > 0 && (
<> <>
{userProgressesToShow.map((progress) => { {userProgressesToShow.map((progress) => {
@ -351,7 +351,7 @@ function StatsCard(props: StatsCardProps) {
const { title, value, isLoading = false } = props; const { title, value, isLoading = false } = props;
return ( return (
<div className="flex flex-col gap-1 rounded-md border bg-white p-4 shadow-sm"> <div className="flex flex-col gap-1 rounded-md border bg-white p-4 shadow-xs">
<h3 className="mb-1 text-xs uppercase text-gray-500">{title}</h3> <h3 className="mb-1 text-xs uppercase text-gray-500">{title}</h3>
{isLoading ? ( {isLoading ? (
<CardSkeleton className="h-8" /> <CardSkeleton className="h-8" />

@ -111,7 +111,7 @@ export function TeamDashboard(props: TeamDashboardProps) {
<h2 className="mb-3 mt-6 flex h-[20px] items-center justify-between text-xs uppercase text-gray-400"> <h2 className="mb-3 mt-6 flex h-[20px] items-center justify-between text-xs uppercase text-gray-400">
Team Members Team Members
<span className="flex-grow h-[1px] bg-gray-200 mx-3" /> <span className="grow h-[1px] bg-gray-200 mx-3" />
{canManageCurrentTeam && ( {canManageCurrentTeam && (
<a <a
href={`/team/members?t=${teamId}`} href={`/team/members?t=${teamId}`}

@ -10,7 +10,7 @@ import DeleteAccountPopup from "./DeleteAccountPopup.astro";
<button <button
data-popup='delete-account-popup' data-popup='delete-account-popup'
class="mt-4 w-full rounded-lg bg-red-600 py-2 text-base font-regular text-white outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-1" class="mt-4 w-full rounded-lg bg-red-600 py-2 text-base font-regular text-white outline-hidden focus:ring-2 focus:ring-red-500 focus:ring-offset-1"
> >
Delete Account Delete Account
</button> </button>

@ -53,7 +53,7 @@ export function DeleteAccountForm() {
type="text" type="text"
name="delete-account" name="delete-account"
id="delete-account" id="delete-account"
className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400" className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-hidden placeholder:text-gray-400 focus:border-gray-400"
placeholder={'Type "delete" to confirm'} placeholder={'Type "delete" to confirm'}
required required
autoFocus autoFocus
@ -72,14 +72,14 @@ export function DeleteAccountForm() {
type="button" type="button"
disabled={isLoading} disabled={isLoading}
onClick={handleClosePopup} onClick={handleClosePopup}
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center" className="grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={isLoading || confirmationText.toUpperCase() !== 'DELETE'} disabled={isLoading || confirmationText.toUpperCase() !== 'DELETE'}
className="flex-grow cursor-pointer rounded-lg bg-red-500 py-2 text-white disabled:opacity-40" className="grow cursor-pointer rounded-lg bg-red-500 py-2 text-white disabled:opacity-40"
> >
{isLoading ? 'Please wait ..' : 'Confirm'} {isLoading ? 'Please wait ..' : 'Confirm'}
</button> </button>

@ -73,7 +73,7 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
<div className="relative h-full w-full max-w-md p-4 md:h-auto"> <div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div <div
ref={popupBodyEl} ref={popupBodyEl}
className="popup-body relative rounded-lg bg-white p-4 shadow" className="popup-body relative rounded-lg bg-white p-4 shadow-sm"
> >
<h2 className="text-2xl font-semibold text-black">Delete Team</h2> <h2 className="text-2xl font-semibold text-black">Delete Team</h2>
<p className="text-gray-500"> <p className="text-gray-500">
@ -90,7 +90,7 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
type="text" type="text"
name="delete-account" name="delete-account"
id="delete-account" id="delete-account"
className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400" className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-hidden placeholder:text-gray-400 focus:border-gray-400"
placeholder={'Type "delete" to confirm'} placeholder={'Type "delete" to confirm'}
required required
autoFocus autoFocus
@ -111,7 +111,7 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
type="button" type="button"
disabled={isLoading} disabled={isLoading}
onClick={handleClosePopup} onClick={handleClosePopup}
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center" className="grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center"
> >
Cancel Cancel
</button> </button>
@ -120,7 +120,7 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
disabled={ disabled={
isLoading || confirmationText.toUpperCase() !== 'DELETE' isLoading || confirmationText.toUpperCase() !== 'DELETE'
} }
className="flex-grow cursor-pointer rounded-lg bg-red-500 py-2 text-white disabled:opacity-40" className="grow cursor-pointer rounded-lg bg-red-500 py-2 text-white disabled:opacity-40"
> >
{isLoading ? 'Please wait ..' : 'Confirm'} {isLoading ? 'Please wait ..' : 'Confirm'}
</button> </button>

@ -1,4 +1,4 @@
import { ArrowDownWideNarrow, Check, ChevronDown } from 'lucide-react'; import { Check, ChevronDown } from 'lucide-react';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import type { SortByValues } from './DiscoverRoadmaps'; import type { SortByValues } from './DiscoverRoadmaps';
@ -41,11 +41,11 @@ export function DiscoverRoadmapSorting(props: DiscoverRoadmapSortingProps) {
return ( return (
<div <div
className="min-auto relative flex flex-shrink-0 sm:min-w-[140px]" className="min-auto relative flex shrink-0 sm:min-w-[140px]"
ref={dropdownRef} ref={dropdownRef}
> >
<button <button
className="py-15 flex w-full items-center justify-between gap-2 rounded-md border px-2 text-sm bg-white" className="flex w-full items-center justify-between gap-2 rounded-md border bg-white px-2 text-sm"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
> >
<span>{selectedValue?.label}</span> <span>{selectedValue?.label}</span>
@ -56,7 +56,7 @@ export function DiscoverRoadmapSorting(props: DiscoverRoadmapSortingProps) {
</button> </button>
{isOpen && ( {isOpen && (
<div className="absolute right-0 top-10 z-10 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg"> <div className="absolute top-10 right-0 z-10 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
{sortingLabels.map((item) => ( {sortingLabels.map((item) => (
<button <button
key={item.value} key={item.value}

@ -53,7 +53,7 @@ export function SearchRoadmap(props: SearchRoadmapProps) {
type="text" type="text"
minLength={3} minLength={3}
placeholder="Type 3 or more characters to search..." placeholder="Type 3 or more characters to search..."
className="w-full rounded-md border border-gray-200 px-3 py-2 pl-9 text-sm transition-colors focus:border-black focus:outline-none" className="w-full rounded-md border border-gray-200 px-3 py-2 pl-9 text-sm transition-colors focus:border-black focus:outline-hidden"
value={term} value={term}
onChange={(e) => setTerm(e.target.value)} onChange={(e) => setTerm(e.target.value)}
/> />
@ -64,7 +64,7 @@ export function SearchRoadmap(props: SearchRoadmapProps) {
)} )}
</form> </form>
{total > 0 && ( {total > 0 && (
<p className="hidden flex-shrink-0 text-sm text-gray-500 sm:block"> <p className="hidden shrink-0 text-sm text-gray-500 sm:block">
{Intl.NumberFormat('en-US', { {Intl.NumberFormat('en-US', {
notation: 'compact', notation: 'compact',
}).format(total)}{' '} }).format(total)}{' '}

@ -11,8 +11,6 @@ import {
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { ProgressNudge } from '../FrameRenderer/ProgressNudge'; import { ProgressNudge } from '../FrameRenderer/ProgressNudge';
import { getUrlParams } from '../../lib/browser.ts'; import { getUrlParams } from '../../lib/browser.ts';
import { cn } from '../../lib/classname.ts';
import { getUser } from '../../lib/jwt.ts';
type EditorRoadmapProps = { type EditorRoadmapProps = {
resourceId: string; resourceId: string;

@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef } from 'react';
import './EditorRoadmapRenderer.css'; import './EditorRoadmapRenderer.css';
import { lazy, useCallback, useEffect, useRef } from 'react';
import { import {
renderResourceProgress, renderResourceProgress,
updateResourceProgress, updateResourceProgress,
@ -9,12 +10,17 @@ import {
} from '../../lib/resource-progress'; } from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import type { Edge, Node } from 'reactflow'; import type { Edge, Node } from '@roadmapsh/editor';
import { Renderer } from '../../../editor/renderer';
import { slugify } from '../../lib/slugger'; import { slugify } from '../../lib/slugger';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
const Renderer = lazy(() =>
import('@roadmapsh/editor').then((mod) => ({
default: mod.Renderer,
})),
);
export type RoadmapRendererProps = { export type RoadmapRendererProps = {
resourceId: string; resourceId: string;
nodes: Node[]; nodes: Node[];

@ -26,7 +26,7 @@ export function AIRoadmapsList(props: AIRoadmapsListProps) {
return ( return (
<ul className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3"> <ul className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{roadmaps.map((roadmap) => { {roadmaps.map((roadmap) => {
const roadmapLink = `/ai/${roadmap.slug}`; const roadmapLink = `/ai-roadmaps/${roadmap.slug}`;
return ( return (
<a <a
@ -35,7 +35,7 @@ export function AIRoadmapsList(props: AIRoadmapsListProps) {
className="flex min-h-[95px] flex-col rounded-md border transition-colors hover:bg-gray-100" className="flex min-h-[95px] flex-col rounded-md border transition-colors hover:bg-gray-100"
target={'_blank'} target={'_blank'}
> >
<h2 className="flex-grow px-2.5 py-2.5 text-base font-medium leading-tight"> <h2 className="grow px-2.5 py-2.5 text-base font-medium leading-tight">
{roadmap.title} {roadmap.title}
</h2> </h2>
<div className="flex items-center justify-between gap-2 px-2.5 py-2"> <div className="flex items-center justify-between gap-2 px-2.5 py-2">

@ -12,7 +12,7 @@ export function EmptyRoadmaps() {
</p> </p>
<div className="flex flex-col items-center gap-1 sm:flex-row sm:gap-1.5"> <div className="flex flex-col items-center gap-1 sm:flex-row sm:gap-1.5">
<a <a
href="/ai" href="/ai-roadmaps"
className="flex w-full items-center gap-1.5 rounded-md bg-gray-900 px-3 py-1.5 text-xs text-white sm:w-auto sm:text-sm" className="flex w-full items-center gap-1.5 rounded-md bg-gray-900 px-3 py-1.5 text-xs text-white sm:w-auto sm:text-sm"
> >
<Wand2 className="h-4 w-4" /> <Wand2 className="h-4 w-4" />

@ -46,7 +46,7 @@ export function ExploreAISearch(props: ExploreAISearchProps) {
name="search" name="search"
type="text" type="text"
placeholder="Type 3 or more characters to search..." placeholder="Type 3 or more characters to search..."
className="w-full rounded-md border border-gray-200 px-3 py-2 pl-9 text-sm transition-colors focus:border-black focus:outline-none" className="w-full rounded-md border border-gray-200 px-3 py-2 pl-9 text-sm transition-colors focus:border-black focus:outline-hidden"
value={term} value={term}
onChange={(e) => setTerm(e.target.value)} onChange={(e) => setTerm(e.target.value)}
/> />
@ -57,7 +57,7 @@ export function ExploreAISearch(props: ExploreAISearchProps) {
)} )}
</div> </div>
{total > 0 && ( {total > 0 && (
<p className="flex-shrink-0 text-sm text-gray-500 hidden sm:block"> <p className="shrink-0 text-sm text-gray-500 hidden sm:block">
{Intl.NumberFormat('en-US', { {Intl.NumberFormat('en-US', {
notation: 'compact', notation: 'compact',
}).format(total)}{' '} }).format(total)}{' '}

@ -1,4 +1,4 @@
import { ArrowDownWideNarrow, Check, ChevronDown } from 'lucide-react'; import { Check, ChevronDown } from 'lucide-react';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
@ -37,11 +37,11 @@ export function ExploreAISorting(props: ExploreAISortingProps) {
return ( return (
<div <div
className="min-auto relative flex flex-shrink-0 sm:min-w-[140px]" className="min-auto relative flex shrink-0 sm:min-w-[140px]"
ref={dropdownRef} ref={dropdownRef}
> >
<button <button
className="py-15 flex w-full items-center justify-between gap-2 rounded-md border px-2 text-sm" className="flex w-full items-center justify-between gap-2 rounded-md border px-2 text-sm"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
> >
<span>{selectedValue?.label}</span> <span>{selectedValue?.label}</span>
@ -52,7 +52,7 @@ export function ExploreAISorting(props: ExploreAISortingProps) {
</button> </button>
{isOpen && ( {isOpen && (
<div className="absolute right-0 top-10 z-10 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg"> <div className="absolute top-10 right-0 z-10 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
{sortingLabels.map((item) => ( {sortingLabels.map((item) => (
<button <button
key={item.value} key={item.value}

@ -33,7 +33,7 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
<a <a
href="/guides" 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" className="hidden rounded-full bg-linear-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 &rarr; View All Guides &rarr;
</a> </a>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save