Compare commits

...

70 Commits

Author SHA1 Message Date
Arik Chakma
97d5934b03 fix: pagination number 2025-04-17 17:24:31 +06:00
Kamran Ahmed
8b69b266d5 Fix flicker of paid plan 2025-04-17 11:41:41 +01:00
Kamran Ahmed
3f0db1526d Add upgrade button on ai page 2025-04-17 10:57:35 +01:00
Kamran Ahmed
69d9dd23b2 Add upgrade button on ai page 2025-04-17 10:56:00 +01:00
github-actions[bot]
3e1bc34d4a chore: update roadmap content json (#8493)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-04-17 14:54:58 +06:00
Kamran Ahmed
dea689b068 Email login to trigger purchase 2025-04-17 09:31:37 +01:00
Kamran Ahmed
de237ec6fc Keep button loading when redirecting 2025-04-17 09:21:06 +01:00
Kamran Ahmed
5ec61cc32f Add AI vs data science guide 2025-04-14 20:08:57 +01:00
Kamran Ahmed
7bffc1004d Add new guide about software engineering 2025-04-14 20:05:25 +01:00
Kamran Ahmed
c06218910d Add new guide data science vs cyber security 2025-04-14 19:52:25 +01:00
Kamran Ahmed
130e381054 Remove testing from course demo 2025-04-14 18:14:47 +01:00
Kamran Ahmed
d5d38ee919 Add protip 2025-04-14 14:29:40 +01:00
Kamran Ahmed
6b7138b8d8 Add protip 2025-04-14 14:21:52 +01:00
Kamran Ahmed
242e40ddd8 Add protip 2025-04-14 14:18:54 +01:00
Kamran Ahmed
9ea70fcc97 Update placeholder 2025-04-14 14:08:37 +01:00
Kamran Ahmed
823c31eac4 AI Tutor - Explore page, sidebar better search (#8476)
* Add sidebar to ai-tutor

* wip

* wip

* Fix mistakes and refacctor

* AI landing page changes

* Update sidebar design

* wip

* wip

* Update AI tutor sidebar

* wip

* Add ai-course dropdown

* Update

* fix: ai chat window position

* Course explanation changes

* Update course

* Tutor sidebar changes

* Refactor staff picks and community

* Update UI for a course

* Improve pagination

* Implement pagination of ai tutor ai courses

* AI explore page with search

* Fix pagination of tutor

* Update tutor header design

* Responsiveness of AI

* Fork alert changes

* Responsiveness of actions

* Forking functionality changes

* Fork confirmation changes

* Add upgrade indicator in sidebar

* fix: ai course access

* fix: next lesson

* Add login to view functionality

* Add search to my picks

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2025-04-14 10:54:38 +01:00
Kamran Ahmed
d4a1180c4d Add google tag manager 2025-04-11 14:28:00 +01:00
Kamran Ahmed
483c942338 Inline script 2025-04-11 13:04:41 +01:00
Kamran Ahmed
f28b018e99 Add varify 2025-04-11 12:58:35 +01:00
Sepand
c683db2757 Add resources for regression testing (#8482)
The QA roadmap does not have any resources for Regression Testing. I have added three resources for it.
2025-04-11 11:47:50 +01:00
Vedansh
6dd8f29bff Refactor spring boot roadmap (#8484) 2025-04-11 11:47:07 +01:00
Vedansh
671b59c0ac fix some topic content. (#8485) 2025-04-11 11:46:30 +01:00
sukalaper
1197a0fd6d doc: Proc Priorities Under Process Management (#8486) 2025-04-11 11:46:15 +01:00
web-svb
9ebb288f9b Remove broken link (#8489)
Removed obsolete link to outdated PostgreSQL 7.1 docs that no longer reflect current row behavior.
2025-04-11 11:45:40 +01:00
Kamran Ahmed
ca38c0cede Fix broken UI 2025-04-11 11:45:20 +01:00
Kamran Ahmed
ff7c981f2f Add loading delay 2025-04-10 18:32:19 +01:00
Kamran Ahmed
3455e6ef1c Add varify tracking 2025-04-10 18:17:11 +01:00
Kamran Ahmed
f7f409ca90 Add demo button 2025-04-10 18:12:05 +01:00
Kamran Ahmed
2538db4786 Implement course demo page (#8477)
* Add course demo button

* Read sample button on page

* GA event for buy button

* Add isTesting link
2025-04-10 17:55:18 +01:00
Kamran Ahmed
d5a8814add Handle incomplete 2025-04-10 12:46:07 +01:00
Kamran Ahmed
0cadde1092 Remove hubspot snippet 2025-04-09 19:28:17 +01:00
Kamran Ahmed
3f4bbef211 Treat expired as cancelled 2025-04-09 19:23:11 +01:00
Kamran Ahmed
715352eeab Add tracking code for hubspot 2025-04-09 11:22:39 +01:00
github-actions[bot]
e5e43de98a chore: update roadmap content json (#8471)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-04-08 18:18:53 +06:00
Kamran Ahmed
f085a226ba Fix broken best practices page 2025-04-08 09:21:01 +01:00
Kamran Ahmed
2e90823af4 Update premium pricing modal text 2025-04-07 20:24:43 +01:00
Kamran Ahmed
50df3eda0f Fix issue where in complete is shown as active subscription 2025-04-07 19:58:58 +01:00
Kamran Ahmed
69b0d7abb3 Improve course button visibility 2025-04-07 19:30:43 +01:00
Kamran Ahmed
c4af3c57f0 Add courses tab 2025-04-07 19:27:00 +01:00
Kamran Ahmed
2cee3a8859 Remove console.log 2025-04-07 17:11:46 +01:00
Kamran Ahmed
7f28a755dc Add 404 handling 2025-04-07 16:39:09 +01:00
Kamran Ahmed
a2e83e909e Fix pre-rendered 404 2025-04-07 16:28:09 +01:00
Kamran Ahmed
e4f53ed90e Fix path 2025-04-07 16:20:04 +01:00
Kamran Ahmed
5e836ab7a5 Fix path 2025-04-07 16:11:25 +01:00
Kamran Ahmed
9851978dbd Add debug info 2025-04-07 16:05:17 +01:00
Kamran Ahmed
82c52aca7e chore: upgrade dependencies (#8468)
* Upgrade paths

* Update topic rendering

* Fix file names

* Remove courses file
2025-04-07 15:52:48 +01:00
Kamran Ahmed
0d62847053 Add courses functionality 2025-04-07 13:49:43 +01:00
Kamran Ahmed
7a00234f9a Add courses tag 2025-04-07 13:49:43 +01:00
Kamran Ahmed
64a65fa2e9 Migrate to Tailwind 4 + Editor Upgrade (#8465)
* wip

* fix: roadmap editor

* fix: padding

* wip

* fix: remove editor package

* wip

* fix: update pnpm lock

* Add contribution docs

* UI changes for TW4

* Update deployment workflow

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2025-04-07 12:53:25 +01:00
jj
09d8c709d4 Fix SQL roadmap content (#8459)
- fix typo in query-optimization@Ps9Yv2s-bKvEegGAbPsiA.md
- fix content in reducing-subqueries@UVTgbZrqpbYl1bQvQejcF.md - the existing content is duplicate with recursive-queries
2025-04-06 23:13:58 +01:00
github-actions[bot]
6a14170e64 chore: update roadmap content json (#8455)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-04-06 00:37:01 +06:00
Kamran Ahmed
ac3ebb2162 Update editor 2025-04-05 12:45:24 +01:00
Kamran Ahmed
56ea91b11c Update generate-renderer 2025-04-05 00:42:03 +01:00
Kamran Ahmed
5a1f52892e Add generate-renderer and dummy renderer 2025-04-05 00:36:58 +01:00
Kamran Ahmed
74781d6e7b Add generate-renderer and dummy renderer 2025-04-05 00:28:41 +01:00
Kamran Ahmed
06bdfc42d2 Update deployment flow 2025-04-05 00:12:50 +01:00
Kamran Ahmed
0a42ea6f41 Add dummy generate renderer 2025-04-04 23:42:08 +01:00
Kamran Ahmed
2dc4041228 Add editor 2025-04-04 23:41:05 +01:00
Kamran Ahmed
4b7eab66da Make deployment script accept pat 2025-04-04 21:16:00 +01:00
Kamran Ahmed
999f6b09a8 Make deployment script accept pat 2025-04-04 21:15:13 +01:00
Kamran Ahmed
a9cd557dd3 Make deployment script accept pat 2025-04-04 21:11:47 +01:00
Kamran Ahmed
3d3423f8e5 Make deployment script accept pat 2025-04-04 21:07:40 +01:00
Kamran Ahmed
a5eb5231cb Make deployment script accept pat 2025-04-04 21:03:12 +01:00
Kamran Ahmed
8662416c96 Make deployment script accept pat 2025-04-04 20:59:44 +01:00
Kamran Ahmed
7564895d7a Make deployment script accept pat 2025-04-04 20:55:35 +01:00
Kamran Ahmed
7b15ed39a3 Make deployment script accept pat 2025-04-04 20:54:24 +01:00
Kamran Ahmed
e72622f2b2 Make deployment script accept pat 2025-04-04 20:52:20 +01:00
Kamran Ahmed
deb9aaafc2 Migrate roadmaps 2025-04-04 20:46:39 +01:00
Kamran Ahmed
63b6d471a2 Update generate-renderer 2025-04-04 20:41:35 +01:00
Arik Chakma
2485b716dd feat: xyflow upgrade (#7803)
* wip

* fix: reset the sizes

* fix: update zustand

* fix: update

* fix: add additional width

* wip

* fix: remove hacky code

* wip

* wip

* wip

* wip

* wip

* fix: try pre-commit

* fix: add check pre-commit

* fix: remove xyflow

* fix: remove unnecessary files

* fix: update packages

* Update scripts/generate-renderer.sh

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2025-04-04 20:33:01 +01:00
1713 changed files with 127616 additions and 18341 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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");
}

View File

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

0
packages/.gitkeep Normal file
View File

3863
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

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

View File

@@ -328,7 +328,7 @@
"links": [
{
"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"
},
{

View File

@@ -594,8 +594,19 @@
},
"ipABerBcM9zCte9pYaIse": {
"title": "Minimal APIs",
"description": "",
"links": []
"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": [
{
"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": {
"title": "Object Relational Mapping",

View File

@@ -2809,26 +2809,10 @@
}
]
},
"IaPd_zuLbiOCwoSHQLoIG": {
"2-3-4-trees@IaPd_zuLbiOCwoSHQLoIG.md": {
"title": "2 3 4 Trees",
"description": "In practice: For every 2-4 tree, there are corresponding redblack 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 redblack trees. This makes 2-4 trees an important tool for understanding the logic behind redblack trees, and this is why many introductory algorithm texts introduce 2-4 trees just before redblack trees, even though 2-4 trees are not often used in practice.\n\nVisit the following resources to learn more:",
"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"
}
]
"description": "",
"links": []
},
"UOYeM-hqIKCrB9hGez4Q_": {
"title": "K-ary / M-ary Tree",

View File

@@ -530,7 +530,7 @@
"links": []
},
"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.",
"links": []
},

View File

@@ -545,7 +545,7 @@
"description": "The stack trace is used to trace the active stack frames at a particular instance during the execution of a program. The stack trace is useful while debugging code as it shows the exact point that has caused an error.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Multiple ways to log the stack trace in node.js",
"title": "Multiple Ways to Log The Stack Trace in Node.js",
"url": "https://www.cloudhadoop.com/nodejs-print-stack-trace-error/",
"type": "article"
}
@@ -803,13 +803,13 @@
"description": "You can programmatically manipulate files in Node.js with the built-in `fs` module. The name is short for “file system,” and the module contains all the functions you need to read, write, and delete files on the local machine.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "How To Work with Files using the fs Module in Node.js",
"url": "https://www.digitalocean.com/community/tutorials/how-to-work-with-files-using-the-fs-module-in-node-js",
"title": "File System Module",
"url": "https://nodejs.org/docs/latest/api/fs.html",
"type": "article"
},
{
"title": "File system",
"url": "https://nodejs.org/docs/latest/api/fs.html",
"title": "How To Work with Files using the fs Module in Node.js",
"url": "https://www.digitalocean.com/community/tutorials/how-to-work-with-files-using-the-fs-module-in-node-js",
"type": "article"
},
{
@@ -861,7 +861,7 @@
"description": "File System or `fs` module is a built in module in Node that enables interacting with the file system using JavaScript. All file system operations have synchronous, callback, and promise-based forms, and are accessible using both CommonJS syntax and ES6 Modules.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "fs",
"title": "fs module",
"url": "https://nodejs.org/api/fs.html",
"type": "article"
},
@@ -952,7 +952,7 @@
"description": "Chokidar is a fast open-source file watcher for node. js. You give it a bunch of files, it watches them for changes and notifies you every time an old file is edited; or a new file is created.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "chokidar package",
"title": "chokidar",
"url": "https://www.npmjs.com/package/chokidar",
"type": "article"
}
@@ -1159,7 +1159,7 @@
"description": "`process.argv` is an array of parameters that are sent when you run a Node.js file or Node.js process.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Node.js Docs on process.argv",
"title": "process.argv",
"url": "https://nodejs.org/docs/latest/api/process.html#processargv",
"type": "article"
},
@@ -1248,7 +1248,7 @@
"type": "article"
},
{
"title": "Fastify Documentations",
"title": "Fastify Documentation",
"url": "https://www.fastify.io/docs/latest/",
"type": "article"
},
@@ -1358,7 +1358,7 @@
"type": "opensource"
},
{
"title": "npmjs.org",
"title": "Ky Package",
"url": "https://www.npmjs.com/package/ky/v/0.9.0",
"type": "article"
}
@@ -1607,6 +1607,11 @@
"title": "What is Database?",
"url": "https://en.wikipedia.org/wiki/Database",
"type": "article"
},
{
"title": "What is Database - AWS",
"url": "https://aws.amazon.com/what-is/database/",
"type": "article"
}
]
},
@@ -1619,6 +1624,11 @@
"url": "https://mongoosejs.com",
"type": "article"
},
{
"title": "Mongoose Documentation",
"url": "https://mongoosejs.com/docs/guide.html",
"type": "article"
},
{
"title": "Getting Started with MongoDB and Mongoose",
"url": "https://www.mongodb.com/developer/languages/javascript/getting-started-with-mongodb-and-mongoose/",
@@ -1654,8 +1664,14 @@
},
"5WqLm53CHDT5uBoMH-iPl": {
"title": "Native Drivers",
"description": "Another way to connect to different databases in Node.js is to use the official native drivers provided by the database.\n\nVisit the following resources to learn more:\n\n[@official@MongoDB Drivers](https://www.mongodb.com/docs/drivers/)",
"links": []
"description": "Another way to connect to different databases in Node.js is to use the official native drivers provided by the database.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "MongoDB Drivers",
"url": "https://www.mongodb.com/docs/drivers/",
"type": "article"
}
]
},
"HDDnt79_PCB5JU-KnHKUh": {
"title": "Knex",
@@ -1972,7 +1988,7 @@
"type": "article"
},
{
"title": "Pm2 Documentations",
"title": "Pm2 Documentation",
"url": "https://pm2.keymetrics.io/docs/usage/quick-start/",
"type": "article"
}
@@ -2015,7 +2031,7 @@
"description": "The Cluster module allows you to easily create child processes that each runs simultaneously on their own single thread, to handle workloads among their application threads.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Cluster Docs",
"title": "Node.js Cluster",
"url": "https://nodejs.org/api/cluster.html#cluster",
"type": "article"
}

View File

@@ -172,11 +172,6 @@
"title": "Rows",
"description": "A row in PostgreSQL represents a single, uniquely identifiable record with a specific set of fields in a table. Each row in a table is made up of one or more columns, where each column can store a specific type of data (e.g., integer, character, date, etc.). The structure of a table determines the schema of its rows, and each row in a table must adhere to this schema.\n\nLearn more from the following resources:",
"links": [
{
"title": "Concepts",
"url": "https://www.postgresql.org/docs/7.1/query-concepts.html",
"type": "article"
},
{
"title": "PostgreSQL - Rows",
"url": "https://www.postgresql.org/docs/current/functions-comparisons.html",

View File

@@ -545,8 +545,24 @@
},
"XCeXiKvBblmDArfbWjDvw": {
"title": "Regression Testing",
"description": "Regression Testing is a type of software testing to confirm that a recent program or code change has not adversely affected existing features. Regression testing is a black box testing technique. Test cases are re-executed to check the previous functionality of the application is working fine and that the new changes have not produced any bugs.",
"links": []
"description": "Regression Testing is a type of software testing to confirm that a recent program or code change has not adversely affected existing features. Regression testing is a black box testing technique. Test cases are re-executed to check the previous functionality of the application is working fine and that the new changes have not produced any bugs.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "What is Regression Testing?",
"url": "https://www.guru99.com/regression-testing.html",
"type": "article"
},
{
"title": "What is Regression Testing? Definition, Tools and Examples",
"url": "https://katalon.com/resources-center/blog/regression-testing",
"type": "article"
},
{
"title": "What is Regression Testing? A Software Testing FAQ - Why? How? When?",
"url": "https://www.youtube.com/watch?v=xmQuLTarGI4",
"type": "video"
}
]
},
"MVShii4LZiWW_gPTJzkty": {
"title": "Smoke Testing",

View File

@@ -1260,7 +1260,7 @@
},
"Ps9Yv2s-bKvEegGAbPsiA": {
"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": [
{
"title": "12 Ways to Optimize SQL Queries",
@@ -1495,7 +1495,7 @@
},
"UVTgbZrqpbYl1bQvQejcF": {
"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": []
},
"w53CSY53nAAN0ux-XeJ4c": {

View File

@@ -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>
);
}

View File

@@ -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");
}

View File

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

View File

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

View File

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

View File

@@ -2,33 +2,24 @@
set -e
# ignore cloning if .temp/web-draw already exists
# Remove old editor
rm -rf editor
if [ ! -d ".temp/web-draw" ]; then
mkdir -p .temp
git clone git@github.com:roadmapsh/web-draw.git .temp/web-draw
git clone ssh://git@github.com/roadmapsh/web-draw.git .temp/web-draw --depth 1
fi
rm -rf editor
mkdir editor
# Make dir
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
# Copy the editor dist, package.json
cp -rf .temp/web-draw/packages/editor/dist packages/editor
cp -rf .temp/web-draw/packages/editor/package.json packages/editor
# Add @ts-nocheck to the top of each ts and tsx file
# so that the typescript compiler doesn't complain
# about the missing types
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
git update-index --assume-unchanged editor/readonly-editor.tsx || true
git update-index --assume-unchanged editor/renderer/index.tsx || true
git update-index --assume-unchanged editor/renderer/renderer.ts || true
# Reinstall so that the editor which was setup gets used
rm -rf node_modules
pnpm install

View File

@@ -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');
}

58
scripts/rename-content.ts Normal file
View File

@@ -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));
});
}
});
}
});
});

View File

@@ -0,0 +1,128 @@
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { AICourseCard } from '../GenerateCourse/AICourseCard';
import { AILoadingState } from './AILoadingState';
import { AITutorHeader } from './AITutorHeader';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import {
listExploreAiCoursesOptions,
type ListExploreAiCoursesQuery,
} from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
import { Pagination } from '../Pagination/Pagination';
import { AICourseSearch } from '../GenerateCourse/AICourseSearch';
import { AITutorTallMessage } from './AITutorTallMessage';
import { BookOpen } from 'lucide-react';
export function AIExploreCourseListing() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const [pageState, setPageState] = useState<ListExploreAiCoursesQuery>({
perPage: '21',
currPage: '1',
query: '',
});
const {
data: exploreAiCourses,
isFetching: isExploreAiCoursesLoading,
isRefetching: isExploreAiCoursesRefetching,
} = useQuery(listExploreAiCoursesOptions(pageState), queryClient);
useEffect(() => {
setIsInitialLoading(false);
}, [exploreAiCourses]);
const courses = exploreAiCourses?.data ?? [];
useEffect(() => {
const queryParams = getUrlParams();
setPageState({
...pageState,
currPage: queryParams?.p || '1',
});
}, []);
useEffect(() => {
if (pageState?.currPage !== '1') {
setUrlParams({
p: pageState?.currPage || '1',
});
} else {
deleteUrlParam('p');
}
}, [pageState]);
return (
<>
{showUpgradePopup && (
<UpgradeAccountModal onClose={() => setShowUpgradePopup(false)} />
)}
<AITutorHeader
title="Explore Courses"
onUpgradeClick={() => setShowUpgradePopup(true)}
>
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
/>
</AITutorHeader>
{(isInitialLoading || isExploreAiCoursesLoading) && (
<AILoadingState
title="Loading courses"
subtitle="This may take a moment..."
/>
)}
{!isExploreAiCoursesLoading && courses && courses.length > 0 && (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
{courses.map((course) => (
<AICourseCard
key={course._id}
course={course}
showActions={false}
showProgress={false}
/>
))}
</div>
<Pagination
totalCount={exploreAiCourses?.totalCount || 0}
totalPages={exploreAiCourses?.totalPages || 0}
currPage={Number(exploreAiCourses?.currPage || 1)}
perPage={Number(exploreAiCourses?.perPage || 21)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
{!isInitialLoading &&
!isExploreAiCoursesLoading &&
courses.length === 0 && (
<AITutorTallMessage
title="No courses found"
subtitle="Try a different search or check back later."
icon={BookOpen}
buttonText="Create your first course"
onButtonClick={() => {
window.location.href = '/ai';
}}
/>
)}
</>
);
}

View File

@@ -0,0 +1,131 @@
import { useQuery } from '@tanstack/react-query';
import {
listFeaturedAiCoursesOptions,
type ListUserAiCoursesQuery,
} from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useEffect, useState } from 'react';
import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser';
import { AICourseCard } from '../GenerateCourse/AICourseCard';
import { Pagination } from '../Pagination/Pagination';
import { AITutorHeader } from './AITutorHeader';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AITutorTallMessage } from './AITutorTallMessage';
import { BookOpen } from 'lucide-react';
import { AILoadingState } from './AILoadingState';
import { AICourseSearch } from '../GenerateCourse/AICourseSearch';
export function AIFeaturedCoursesListing() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const [pageState, setPageState] = useState<ListUserAiCoursesQuery>({
perPage: '21',
currPage: '1',
query: '',
});
const { data: featuredAiCourses, isFetching: isFeaturedAiCoursesLoading } =
useQuery(listFeaturedAiCoursesOptions(pageState), queryClient);
useEffect(() => {
setIsInitialLoading(false);
}, [featuredAiCourses]);
const courses = featuredAiCourses?.data ?? [];
useEffect(() => {
const queryParams = getUrlParams();
setPageState({
...pageState,
currPage: queryParams?.p || '1',
query: queryParams?.q || '',
});
}, []);
useEffect(() => {
if (pageState?.currPage !== '1' || pageState?.query !== '') {
setUrlParams({
p: pageState?.currPage || '1',
q: pageState?.query || '',
});
} else {
deleteUrlParam('p');
deleteUrlParam('q');
}
}, [pageState]);
return (
<>
{showUpgradePopup && (
<UpgradeAccountModal onClose={() => setShowUpgradePopup(false)} />
)}
<AITutorHeader
title="Featured Courses"
onUpgradeClick={() => setShowUpgradePopup(true)}
>
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
/>
</AITutorHeader>
{(isFeaturedAiCoursesLoading || isInitialLoading) && (
<AILoadingState
title="Loading featured courses"
subtitle="This may take a moment..."
/>
)}
{!isFeaturedAiCoursesLoading &&
!isInitialLoading &&
courses.length > 0 && (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
{courses.map((course) => (
<AICourseCard
key={course._id}
course={course}
showActions={false}
showProgress={false}
/>
))}
</div>
<Pagination
totalCount={featuredAiCourses?.totalCount || 0}
totalPages={featuredAiCourses?.totalPages || 0}
currPage={Number(featuredAiCourses?.currPage || 1)}
perPage={Number(featuredAiCourses?.perPage || 10)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
{!isFeaturedAiCoursesLoading &&
!isInitialLoading &&
courses.length === 0 && (
<AITutorTallMessage
title="No featured courses"
subtitle="There are no featured courses available at the moment."
icon={BookOpen}
buttonText="Browse all courses"
onButtonClick={() => {
window.location.href = '/ai';
}}
/>
)}
</>
);
}

View File

@@ -0,0 +1,27 @@
import { Loader2 } from 'lucide-react';
type AILoadingStateProps = {
title: string;
subtitle?: string;
};
export function AILoadingState(props: AILoadingStateProps) {
const { title, subtitle } = props;
return (
<div className="flex flex-grow w-full flex-col items-center justify-center gap-4 rounded-lg border border-gray-200 bg-white p-8">
<div className="relative">
<Loader2 className="size-12 animate-spin text-gray-300" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="size-4 rounded-full bg-white"></div>
</div>
</div>
<div className="text-center">
<p className="text-lg font-medium text-gray-900">{title}</p>
{subtitle && (
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { useQuery } from '@tanstack/react-query';
import { AITutorLimits } from './AITutorLimits';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useIsPaidUser } from '../../queries/billing';
type AITutorHeaderProps = {
title: string;
onUpgradeClick: () => void;
children?: React.ReactNode;
};
export function AITutorHeader(props: AITutorHeaderProps) {
const { title, onUpgradeClick, children } = props;
const { data: limits } = useQuery(getAiCourseLimitOptions(), queryClient);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { used, limit } = limits ?? { used: 0, limit: 0 };
return (
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1">
<div className="flex items-center gap-2">
<h2 className="relative flex-shrink-0 top-0 lg:top-1 text-lg font-semibold">{title}</h2>
</div>
<div className="flex items-center gap-2">
<AITutorLimits
used={used}
limit={limit}
isPaidUser={isPaidUser}
isPaidUserLoading={isPaidUserLoading}
onUpgradeClick={onUpgradeClick}
/>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { Menu } from 'lucide-react';
import { useState } from 'react';
import { AITutorSidebar, type AITutorTab } from './AITutorSidebar';
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
type AITutorLayoutProps = {
children: React.ReactNode;
activeTab: AITutorTab;
};
export function AITutorLayout(props: AITutorLayoutProps) {
const { children, activeTab } = props;
const [isSidebarFloating, setIsSidebarFloating] = useState(false);
return (
<>
<div className="flex flex-row items-center justify-between border-b border-slate-200 px-4 py-3 lg:hidden">
<a href="/" className="flex flex-row items-center gap-1.5">
<RoadmapLogoIcon className="size-6 text-gray-500" color="black" />
</a>
<button
className="flex flex-row items-center gap-1"
onClick={() => setIsSidebarFloating(!isSidebarFloating)}
>
<Menu className="size-5 text-gray-500" />
</button>
</div>
<div className="flex flex-grow flex-row">
<AITutorSidebar
onClose={() => setIsSidebarFloating(false)}
isFloating={isSidebarFloating}
activeTab={activeTab}
/>
<div className="flex flex-grow flex-col overflow-y-scroll bg-gray-100 p-3 lg:px-4 lg:py-4">
{children}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,43 @@
import { Gift } from 'lucide-react';
import { cn } from '../../lib/classname';
type AITutorLimitsProps = {
used: number;
limit: number;
isPaidUser: boolean;
isPaidUserLoading: boolean;
onUpgradeClick: () => void;
};
export function AITutorLimits(props: AITutorLimitsProps) {
const limitUsedPercentage = Math.round((props.used / props.limit) * 100);
if (props.limit <= 0 || props.isPaidUserLoading) {
return null;
}
return (
<div
className={cn(
'pointer-events-none flex items-center gap-2 opacity-0 transition-opacity',
{
'pointer-events-auto opacity-100': !props.isPaidUser,
},
)}
>
<p className="flex items-center text-sm text-yellow-600">
<span className="max-md:hidden">
{limitUsedPercentage}% of daily limit used{' '}
</span>
<span className="inline md:hidden">{limitUsedPercentage}% used</span>
<button
onClick={props.onUpgradeClick}
className="ml-1.5 flex items-center gap-1 rounded-full bg-yellow-600 py-0.5 pr-2 pl-1.5 text-xs text-white"
>
<Gift className="size-4" />
Upgrade
</button>
</p>
</div>
);
}

View File

@@ -0,0 +1,143 @@
import { useEffect, useState } from 'react';
import { BookOpen, Compass, Plus, Star, X, Zap } from 'lucide-react';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { useIsPaidUser } from '../../queries/billing';
import { isLoggedIn } from '../../lib/jwt';
type AITutorSidebarProps = {
isFloating: boolean;
activeTab: AITutorTab;
onClose: () => void;
};
const sidebarItems = [
{
key: 'new',
label: 'New Course',
href: '/ai',
icon: Plus,
},
{
key: 'courses',
label: 'My Courses',
href: '/ai/courses',
icon: BookOpen,
},
{
key: 'staff-picks',
label: 'Staff Picks',
href: '/ai/staff-picks',
icon: Star,
},
{
key: 'community',
label: 'Community',
href: '/ai/community',
icon: Compass,
},
];
export type AITutorTab = (typeof sidebarItems)[number]['key'];
export function AITutorSidebar(props: AITutorSidebarProps) {
const { activeTab, isFloating, onClose } = props;
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
useEffect(() => {
setIsInitialLoad(false);
}, []);
return (
<>
{isUpgradeModalOpen && (
<UpgradeAccountModal onClose={() => setIsUpgradeModalOpen(false)} />
)}
<aside
className={`w-[255px] shrink-0 border-r border-slate-200 ${
isFloating
? 'fixed top-0 bottom-0 left-0 z-50 block border-r-0 bg-white shadow-xl'
: 'hidden lg:block'
}`}
>
{isFloating && (
<button className="absolute top-3 right-3" onClick={onClose}>
<X
strokeWidth={3}
className="size-3.5 text-gray-400 hover:text-black"
/>
</button>
)}
<div className="flex flex-col items-start justify-center px-6 py-5">
<div className="flex flex-row items-center gap-1">
<AITutorLogo className="size-11 text-gray-500" color="black" />
</div>
<div className="my-3 flex flex-col">
<h2 className="-mb-px text-base font-semibold text-black">
AI Tutor
</h2>
<span className="text-xs text-gray-500">
by{' '}
<a href="/" className="underline-offset-2 hover:underline">
roadmap.sh
</a>
</span>
</div>
<p className="max-w-[150px] text-xs text-gray-500">
Your personalized learning companion for any topic
</p>
</div>
<ul className="space-y-1">
{sidebarItems.map((item) => (
<li key={item.key}>
<a
href={item.href}
className={`font-regular flex w-full items-center border-r-2 px-5 py-2 text-sm transition-all ${
activeTab === item.key
? 'border-r-black bg-gray-100 text-black'
: 'border-r-transparent text-gray-500 hover:border-r-gray-300'
}`}
>
<span className="flex grow items-center">
<item.icon className="mr-2 size-4" />
{item.label}
</span>
</a>
</li>
))}
{!isInitialLoad &&
isLoggedIn() &&
!isPaidUser &&
!isPaidUserLoading && (
<li>
<button
onClick={() => {
setIsUpgradeModalOpen(true);
}}
className="mx-4 mt-4 rounded-xl bg-amber-100 p-4 text-left transition-colors hover:bg-amber-200/80"
>
<span className="mb-2 flex items-center gap-2">
<Zap className="size-4 text-amber-600" />
<span className="font-medium text-amber-900">Upgrade</span>
</span>
<span className="mt-1 block text-left text-xs leading-4 text-amber-700">
Get access to all features and benefits of the AI Tutor.
</span>
</button>
</li>
)}
</ul>
</aside>
{isFloating && (
<div className="fixed inset-0 z-40 bg-black/50" onClick={onClose} />
)}
</>
);
}

View File

@@ -0,0 +1,13 @@
import { Zap } from 'lucide-react';
<li>
<div className="mx-4 mt-4 rounded-lg bg-amber-50 p-3">
<div className="flex items-center gap-2">
<Zap className="size-4 text-amber-600" />
<span className="font-medium text-amber-900">Free Tier</span>
</div>
<p className="mt-1 text-xs text-amber-700">
Upgrade to Pro to unlock unlimited AI tutoring sessions
</p>
</div>
</li>

View File

@@ -0,0 +1,31 @@
import { type LucideIcon } from 'lucide-react';
type AITutorTallMessageProps = {
title: string;
subtitle?: string;
icon: LucideIcon;
buttonText?: string;
onButtonClick?: () => void;
};
export function AITutorTallMessage(props: AITutorTallMessageProps) {
const { title, subtitle, icon: Icon, buttonText, onButtonClick } = props;
return (
<div className="flex flex-grow flex-col items-center justify-center rounded-lg border border-gray-200 bg-white p-8">
<Icon className="size-12 text-gray-300" />
<div className="my-4 text-center">
<h2 className="mb-2 text-xl font-semibold">{title}</h2>
{subtitle && <p className="text-base text-gray-600">{subtitle}</p>}
</div>
{buttonText && onButtonClick && (
<button
onClick={onButtonClick}
className="rounded-lg bg-black px-4 py-2 text-sm text-white hover:opacity-80"
>
{buttonText}
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,69 @@
import { ChevronDown } from 'lucide-react';
import { useState, useRef, useEffect } from 'react';
import { cn } from '../../lib/classname';
import {
difficultyLevels,
type DifficultyLevel,
} from '../GenerateCourse/AICourse';
type DifficultyDropdownProps = {
value: DifficultyLevel;
onChange: (value: DifficultyLevel) => void;
};
export function DifficultyDropdown(props: DifficultyDropdownProps) {
const { value, onChange } = props;
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className={cn(
'flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200 hover:text-black',
)}
>
<span className="capitalize">{value}</span>
<ChevronDown size={16} className={cn(isOpen && 'rotate-180')} />
</button>
{isOpen && (
<div className="absolute z-10 mt-1 flex flex-col overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
{difficultyLevels.map((level) => (
<button
key={level}
type="button"
onClick={() => {
onChange(level);
setIsOpen(false);
}}
className={cn(
'px-5 py-2 text-left text-sm capitalize hover:bg-gray-100',
value === level && 'bg-gray-200 font-medium hover:bg-gray-200',
)}
>
{level}
</button>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { LockIcon } from 'lucide-react';
import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname';
type LoginToViewProps = {
className?: string;
};
export function LoginToView(props: LoginToViewProps) {
const { className } = props;
return (
<div
className={cn(
'mt-8 min-h-[402px] rounded-xl border border-gray-200/50 bg-gradient-to-br from-gray-50 to-gray-100/50 p-12 backdrop-blur-sm',
'flex flex-col items-center justify-center',
className,
)}
>
<LockIcon className="size-8 stroke-[1.5] text-gray-600" />
<div className="mt-5 mb-4 flex flex-col items-center gap-0.5 text-center">
<h3 className="text-xl font-semibold text-gray-700">Login Required</h3>
<p className="text-sm text-balance leading-relaxed text-gray-500">
Please login to access the content and all the features of the AI Tutor.
</p>
</div>
<button
onClick={() => showLoginPopup()}
className="rounded-full bg-black px-6 py-2 text-sm font-medium text-white transition-all duration-300 hover:opacity-80 hover:shadow-md active:scale-[0.98] active:transform"
>
Login to Continue
</button>
</div>
);
}

View File

@@ -163,7 +163,7 @@ const sidebarLinks = [
: '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
className={`${sidebarLink.icon.classes} mr-2`}

View File

@@ -83,10 +83,10 @@ export function AccountStreak(props: AccountStreakProps) {
const totalCircles = leftCircleCount + currentCircleCount + remainingCount;
return (
<div className="relative z-[90] animate-fade-in">
<div className="relative z-90 animate-fade-in">
<button
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,
},

View File

@@ -128,20 +128,20 @@ export function AccountStreakHeatmap(props: AccountStreakHeatmapProps) {
]}
classForValue={(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;
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) {
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) {
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) {
return 'fill-slate-500 rounded-md [rx:2px] focus:outline-none';
return 'fill-slate-500 rounded-md [rx:2px] focus:outline-hidden';
} 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) => {
@@ -159,7 +159,7 @@ export function AccountStreakHeatmap(props: AccountStreakHeatmapProps) {
<ReactTooltip
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">
@@ -173,14 +173,14 @@ export function AccountStreakHeatmap(props: AccountStreakHeatmapProps) {
data-tooltip-content={`${legend.count} Updates`}
>
<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>
))}
<span className="ml-2 text-xs text-slate-500">More</span>
<ReactTooltip
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>

View File

@@ -43,7 +43,7 @@ export function ActivityTopicsModal(props: ActivityTopicDetailsProps) {
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="flex items-center gap-2">
{actionType.replace('_', ' ')}

View File

@@ -36,7 +36,7 @@ export function ProjectProgress(props: ProjectProgressType) {
target="_blank"
>
<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">
{projectStatus.upvotes}
<ThumbsUp className="size-2.5 stroke-[2.5px]" />

View File

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

View File

@@ -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
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 && (
<>
@@ -99,7 +99,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
<button
onClick={onClose}
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
</button>
@@ -110,7 +110,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
setIsLoading(false);
}}
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
</button>
@@ -126,7 +126,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
<button
onClick={onClose}
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
</button>
@@ -152,7 +152,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
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'}
/>
@@ -160,7 +160,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
<button
onClick={onClose}
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
</button>

View File

@@ -30,7 +30,7 @@ function Input(props: InputProps) {
value={value}
onChange={onChange}
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"
data-1p-ignore=""
data-form-type="other"
@@ -45,7 +45,7 @@ function Input(props: InputProps) {
value={value}
onChange={onChange}
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"
data-1p-ignore=""
data-form-type="other"
@@ -120,7 +120,7 @@ export function AdvertiseForm() {
Ready to learn more? Fill out the form below to get started!
</h2>
{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}
</div>
)}
@@ -199,7 +199,7 @@ export function AdvertiseForm() {
type="checkbox"
checked={formData.updates}
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 className="ml-3 text-sm">
@@ -213,7 +213,7 @@ export function AdvertiseForm() {
<div>
<button
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
</button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
import Cookies from 'js-cookie';
import type { FormEvent } from 'react';
import { useId, useState } from 'react';
import { httpPost } from '../../lib/http';
import { FIRST_LOGIN_PARAM, setAuthToken } from '../../lib/jwt';
import {
COURSE_PURCHASE_PARAM, FIRST_LOGIN_PARAM,
setAuthToken
} from '../../lib/jwt';
type EmailLoginFormProps = {
isDisabled?: boolean;
@@ -38,7 +40,10 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
const currentLocation = window.location.href;
const url = new URL(currentLocation, window.location.origin);
url.searchParams.set(FIRST_LOGIN_PARAM, response?.isNewUser ? '1' : '0');
url.searchParams.set(COURSE_PURCHASE_PARAM, '1');
window.location.href = url.toString();
return;
}
@@ -70,7 +75,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
type="email"
autoComplete="email"
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"
value={email}
onInput={(e) => setEmail(String((e.target as any).value))}
@@ -84,13 +89,13 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
type="password"
autoComplete="current-password"
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"
value={password}
onInput={(e) => setPassword(String((e.target as any).value))}
/>
<p className="mb-3 mt-2 text-sm text-gray-500">
<p className="mt-2 mb-3 text-sm text-gray-500">
<a
href="/forgot-password"
className="text-blue-800 hover:text-blue-600"
@@ -106,7 +111,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
<button
type="submit"
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'}
</button>

View File

@@ -80,7 +80,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
min={3}
max={50}
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"
value={name}
onInput={(e) => setName(String((e.target as any).value))}
@@ -93,7 +93,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
type="email"
autoComplete="email"
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"
value={email}
onInput={(e) => setEmail(String((e.target as any).value))}
@@ -108,7 +108,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
min={6}
max={50}
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"
value={password}
onInput={(e) => setPassword(String((e.target as any).value))}
@@ -121,7 +121,7 @@ export function EmailSignupForm(props: EmailSignupFormProps) {
<button
type="submit"
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'}
</button>

View File

@@ -33,7 +33,7 @@ export function ForgotPasswordForm() {
<input
type="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
placeholder="Email Address"
value={email}
@@ -55,7 +55,7 @@ export function ForgotPasswordForm() {
<button
type="submit"
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'}
</button>

View File

@@ -148,7 +148,7 @@ export function GitHubButton(props: GitHubButtonProps) {
<>
<button
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,
)}
disabled={isLoading || isDisabled}

View File

@@ -147,7 +147,7 @@ export function GoogleButton(props: GoogleButtonProps) {
<>
<button
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,
)}
disabled={isLoading || isDisabled}

View File

@@ -152,7 +152,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
<>
<button
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,
)}
disabled={isLoading || isDisabled}

View File

@@ -61,7 +61,7 @@ export function ResetPasswordForm() {
<form className="mx-auto w-full" onSubmit={handleSubmit}>
<input
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
minLength={6}
placeholder="New Password"
@@ -71,7 +71,7 @@ export function ResetPasswordForm() {
<input
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
minLength={6}
placeholder="Confirm New Password"
@@ -88,7 +88,7 @@ export function ResetPasswordForm() {
<button
type="submit"
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'}
</button>

View File

@@ -125,7 +125,7 @@ export function Befriend() {
<div>
<a
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
</a>
@@ -141,7 +141,7 @@ export function Befriend() {
const isMe = currentUser?.id === user.id;
return (
<div className="container !max-w-[400px] text-center">
<div className="container max-w-[400px]! text-center">
<img
alt={'join team'}
src={userAvatar}
@@ -169,7 +169,7 @@ export function Befriend() {
});
}}
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'}
</button>
@@ -177,7 +177,7 @@ export function Befriend() {
{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" />
Request Sent
</span>
@@ -188,7 +188,7 @@ export function Befriend() {
setIsConfirming(true);
}}
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]" />
Withdraw Request
@@ -196,7 +196,7 @@ export function Befriend() {
)}
{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?{' '}
<button
className="ml-2 text-red-700 underline"
@@ -225,7 +225,7 @@ export function Befriend() {
{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" />
You are friends
</span>
@@ -236,7 +236,7 @@ export function Befriend() {
setIsConfirming(true);
}}
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]" />
Remove Friend
@@ -244,7 +244,7 @@ export function Befriend() {
)}
{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?{' '}
<button
className="ml-2 text-red-700 underline"
@@ -271,12 +271,12 @@ export function Befriend() {
{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" />
Request Rejected
</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?{' '}
<button
className="ml-2 text-red-700 underline"
@@ -296,7 +296,7 @@ export function Befriend() {
{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" />
Request Rejected
</span>
@@ -311,7 +311,7 @@ export function Befriend() {
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" />
Accept Request
@@ -323,7 +323,7 @@ export function Befriend() {
setIsConfirming(true);
}}
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]" />
Reject Request
@@ -331,7 +331,7 @@ export function Befriend() {
)}
{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?{' '}
<button
className="ml-2 text-red-700 underline"

View File

@@ -27,7 +27,7 @@ const isBestPracticeReady = !isUpcoming;
<MarkFavorite
resourceId={bestPracticeId}
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
/>
</h1>

View File

@@ -19,8 +19,10 @@ import {
CreditCard,
ArrowRightLeft,
CircleX,
AlertCircle,
} from 'lucide-react';
import { BillingWarning } from './BillingWarning';
import { cn } from '../../lib/classname';
export type CreateCustomerPortalBody = {};
@@ -40,8 +42,12 @@ export function BillingPage() {
);
const isCanceled =
billingDetails?.status === 'canceled' || billingDetails?.cancelAtPeriodEnd;
billingDetails?.status === 'canceled' ||
billingDetails?.status === 'incomplete_expired' ||
billingDetails?.cancelAtPeriodEnd;
const isPastDue = billingDetails?.status === 'past_due';
const isIncomplete = billingDetails?.status === 'incomplete';
const {
mutate: createCustomerPortal,
@@ -117,6 +123,19 @@ export function BillingPage() {
!isLoadingBillingDetails &&
priceDetails && (
<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 && (
<BillingWarning
icon={CircleX}
@@ -157,7 +176,7 @@ export function BillingPage() {
<RefreshCw className="size-5 text-gray-600" />
</div>
<div>
<span className="text-xs uppercase tracking-wider text-gray-400">
<span className="text-xs tracking-wider text-gray-400 uppercase">
Payment
</span>
<h3 className="flex items-baseline text-lg font-semibold text-black">
@@ -170,27 +189,35 @@ export function BillingPage() {
</div>
</div>
<div className="mt-6 border-t border-gray-100 pt-6">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<Calendar className="size-5 text-gray-600" />
<div
className={cn(
'mt-6 pt-6',
!isIncomplete && 'border-t border-gray-100',
isIncomplete && '-mt-6',
)}
>
{!isIncomplete && (
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<Calendar className="size-5 text-gray-600" />
</div>
<div>
<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>
<span className="text-xs uppercase tracking-wider text-gray-400">
{billingDetails?.cancelAtPeriodEnd
? 'Expires On'
: 'Renews On'}
</span>
<h3 className="text-lg font-semibold text-black">
{formattedNextBillDate}
</h3>
</div>
</div>
)}
<div className="mt-8 flex gap-3 max-sm:flex-col">
{!isCanceled && (
{!isCanceled && !isIncomplete && (
<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={() => {
setShowUpgradeModal(true);
}}
@@ -201,7 +228,7 @@ export function BillingPage() {
)}
<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={() => {
createCustomerPortal({});
}}

View File

@@ -59,7 +59,7 @@ export function EmptyBillingScreen(props: EmptyBillingScreenProps) {
<button
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
</button>

View File

@@ -5,6 +5,7 @@ import {
MessageSquare,
Sparkles,
Heart,
MapIcon,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
@@ -33,17 +34,22 @@ type Perk = {
const PREMIUM_PERKS: Perk[] = [
{
icon: Zap,
title: 'Unlimited AI Course Generations',
description: 'Generate as many custom courses as you need',
title: 'AI Course Generations',
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,
title: 'No Daily Limits on course features',
description: 'Use all features without restrictions',
title: 'Extended Daily Limits',
description: 'Generate more content in a day',
},
{
icon: MessageSquare,
title: 'Unlimited Course Follow-ups',
title: 'Course Follow-ups',
description: 'Ask as many questions as you need',
},
{
@@ -228,7 +234,14 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
)}
</p>
)}
<p className="text-2xl font-bold text-black sm:text-3xl">
<p
className={cn(
'text-2xl font-bold text-black sm:text-3xl',
{
'mt-0 md:mt-6': !isYearly,
},
)}
>
${plan.amount}{' '}
<span className="text-xs font-normal text-gray-500 sm:text-sm">
/ {isYearly ? 'year' : 'month'}
@@ -236,12 +249,12 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
</p>
</div>
<div className="flex-grow"></div>
<div className="grow"></div>
<div>
<button
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',
)}
disabled={
@@ -284,7 +297,10 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
{PREMIUM_PERKS.map((perk, index) => {
const Icon = perk.icon;
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" />
<div>
<h4 className="text-sm font-medium text-black sm:text-base">

View File

@@ -17,11 +17,11 @@ const formattedDate = DateTime.fromISO(frontmatter.date).toFormat(
<div class='relative mb-6' id={changelog.id}>
<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>
<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}
</span>
<span class='truncate text-base font-medium text-balance'>

View File

@@ -6,13 +6,13 @@ const formattedDate = DateTime.fromISO('2024-09-13').toFormat('dd LLL, yyyy');
<div class='relative mb-6'>
<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>
<div
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}
</span>
<span class='truncate text-balance text-base font-medium'>

View File

@@ -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='container !max-w-[650px]'>
<div class='container max-w-[650px]!'>
<p class='text-2xl font-bold sm:text-5xl'>
<img
src='/images/gifs/rocket.gif'
@@ -40,10 +40,10 @@ const top10Changelogs = allChangelogs.slice(0, 10);
href={`/changelog#${changelog.id}`}
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}
</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'>
{changelog.frontmatter.title}
</span>

View File

@@ -1,4 +1,4 @@
import { ChevronLeft, ChevronRight, MoveRight } from 'lucide-react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import React, { useState, useEffect, useCallback } from 'react';
interface ChangelogImagesProps {
@@ -63,17 +63,17 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
alt={title}
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">
<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>
{enlargedImage && (
<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}
>
<img
@@ -82,7 +82,7 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
className="max-h-[90%] max-w-[90%] rounded-xl object-contain"
/>
<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) => {
e.stopPropagation();
handleNavigation('prev');
@@ -91,7 +91,7 @@ const ChangelogImages: React.FC<ChangelogImagesProps> = ({ images }) => {
<ChevronLeft size={24} />
</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) => {
e.stopPropagation();
handleNavigation('next');

View File

@@ -194,13 +194,13 @@ export function CommandMenu() {
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="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
ref={inputRef}
autoFocus={true}
type="text"
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 .."
autoComplete="off"
onInput={(e) => {
@@ -249,7 +249,7 @@ export function CommandMenu() {
)}
<a
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' : '',
)}
onMouseOver={() => setActiveCounter(counter)}

View File

@@ -286,7 +286,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
className="relative flex flex-col items-start overflow-hidden rounded-md border border-gray-300"
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">
{roadmapTitle}
</span>
@@ -341,7 +341,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
<button
type="button"
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={() => {
if (isCustomResource) {

View File

@@ -68,11 +68,11 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
);
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
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
type="button"
@@ -86,7 +86,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
ref={searchInputEl}
type="text"
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}
onInput={(e) => setSearchText((e.target as HTMLInputElement).value)}
/>

View File

@@ -76,7 +76,7 @@ export function Step0(props: Step0Props) {
{validTeamTypes.map((validTeamType) => (
<button
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
? 'border-gray-400 bg-gray-100'
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'

View File

@@ -135,7 +135,7 @@ export function Step1(props: Step1Props) {
ref={nameRef as any}
autoFocus={true}
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."
disabled={isLoading}
required
@@ -157,7 +157,7 @@ export function Step1(props: Step1Props) {
name="website"
required
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"
disabled={isLoading}
value={website}
@@ -178,7 +178,7 @@ export function Step1(props: Step1Props) {
type="url"
name="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"
disabled={isLoading}
value={linkedInUrl}
@@ -200,7 +200,7 @@ export function Step1(props: Step1Props) {
type="url"
name="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"
disabled={isLoading}
value={gitHubUrl}
@@ -219,7 +219,7 @@ export function Step1(props: Step1Props) {
<select
name="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'}
disabled={isLoading}
value={teamSize}

View File

@@ -50,7 +50,7 @@ export function Step2(props: Step2Props) {
onClick={onNext}
disabled={teamResourceConfig.length !== 0}
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

View File

@@ -109,7 +109,7 @@ export function Step3(props: Step3Props) {
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
selectedRole={user.role}
@@ -180,7 +180,7 @@ export function Step3(props: Step3Props) {
onClick={onNext}
disabled={users.filter((u) => u.email).length !== 0}
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

View File

@@ -148,12 +148,12 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
}, []);
return (
<div className="fixed left-0 right-0 top-0 z-[100] h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div 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
id={'customized-roadmap'}
ref={popupBodyEl}
className="popup-body relative rounded-lg bg-white shadow"
className="popup-body relative rounded-lg bg-white shadow-sm"
>
<div
className={

View File

@@ -179,7 +179,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="title"
id="title"
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"
value={title}
onChange={(e) => setTitle(e.target.value)}
@@ -199,7 +199,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="description"
required
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',
)}
placeholder="Enter Description"
@@ -219,7 +219,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
onClick={onClose}
type="button"
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',
)}
>
@@ -232,7 +232,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
disabled={isLoading}
type="button"
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 ? (
<Loader2 size={16} className="animate-spin" />
@@ -246,7 +246,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
disabled={isLoading}
type="submit"
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',
)}
>

View File

@@ -53,7 +53,7 @@ export function EmbedRoadmapModal(props: ShareRoadmapModalProps) {
<div className="flex items-center justify-between px-4 pb-4 pt-2">
<button
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-gray-900 hover:bg-gray-800 focus:bg-gray-800': !isCopied,

View File

@@ -1,4 +1,4 @@
import { ReadonlyEditor } from '../../../editor/readonly-editor';
import { ReadonlyEditor } from '@roadmapsh/editor';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import {
refreshProgressCounters,
@@ -9,7 +9,7 @@ import {
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
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 { EmptyRoadmap } from './EmptyRoadmap';
import { cn } from '../../lib/classname';

View File

@@ -34,7 +34,7 @@ export function PersonalRoadmapActionDropdown(
<button
disabled={false}
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} />
Options
@@ -53,7 +53,7 @@ export function PersonalRoadmapActionDropdown(
setIsOpen(false);
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" />
Sharing
@@ -67,7 +67,7 @@ export function PersonalRoadmapActionDropdown(
setIsOpen(false);
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" />
Customize
@@ -81,7 +81,7 @@ export function PersonalRoadmapActionDropdown(
setIsOpen(false);
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" />
Delete

View File

@@ -176,7 +176,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
<a
href={editorLink}
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'}
>
@@ -186,7 +186,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
<a
href={`/r/${roadmap?.slug}`}
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'}
>

View File

@@ -231,7 +231,7 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) {
</label>
<textarea
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"
value={userFeedback}
onChange={(e) => {

View File

@@ -55,7 +55,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
className="flex text-sm opacity-0 transition-opacity duration-300"
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>

View File

@@ -32,7 +32,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
{isOpen && (
<div
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>
{onCustomize && (
@@ -42,7 +42,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
setIsOpen(false);
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" />
Edit
@@ -56,7 +56,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
setIsOpen(false);
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" />
Sharing
@@ -70,7 +70,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
setIsOpen(false);
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" />
Delete

View File

@@ -128,7 +128,7 @@ export function ShareRoadmapModal(props: ShareRoadmapModalProps) {
<div className="flex items-center justify-between p-4">
<button
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}
>
{isLoading ? (
@@ -141,7 +141,7 @@ export function ShareRoadmapModal(props: ShareRoadmapModalProps) {
)}
</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}
>
{isCopied ? (

View File

@@ -92,7 +92,7 @@ export function SharedRoadmapList(props: SharedRoadmapListProps) {
>
<a
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'}
>
<span className="w-full truncate">

View File

@@ -79,7 +79,7 @@ export function SubmitShowcaseWarning(props: SubmitShowcaseWarningProps) {
<div className="mt-4 grid grid-cols-2 gap-2">
<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}
disabled={submit.isPending}
>

View File

@@ -53,7 +53,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
<a
key={roadmap.id}
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}
</a>

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ export function DashboardProjectCard(props: DashboardProjectCardProps) {
>
<span
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':
status === 'submitted',
@@ -41,8 +41,8 @@ export function DashboardProjectCard(props: DashboardProjectCardProps) {
/>
)}
</span>
<span className="flex-grow truncate group-hover:underline">{title.replace(/(System)|(Service)/, '')}</span>
<span className="flex-shrink-0 bg-transparent text-xs text-gray-400 no-underline">
<span className="grow truncate group-hover:underline">{title.replace(/(System)|(Service)/, '')}</span>
<span className="shrink-0 bg-transparent text-xs text-gray-400 no-underline">
{!!startedAt &&
status === 'started' &&
getRelativeTimeString(startedAt)}

View File

@@ -206,7 +206,7 @@ export function DashboardTeamRoadmaps(props: DashboardTeamRoadmapsProps) {
const roadmapHeading = (
<div className="mb-3 flex h-[20px] items-center justify-between gap-2 text-xs">
<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 && (
<a
href={`/team/roadmaps?t=${teamId}`}

View File

@@ -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={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,
)}
>

View File

@@ -392,7 +392,7 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
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="container">
<h2

View File

@@ -67,7 +67,7 @@ function ProgressLane(props: ProgressLaneProps) {
return (
<div
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,
)}
>
@@ -92,7 +92,7 @@ function ProgressLane(props: ProgressLaneProps) {
</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 && (
<div
className={cn('grid grid-cols-2 gap-2', loadingWrapperClassName)}
@@ -105,7 +105,7 @@ function ProgressLane(props: ProgressLaneProps) {
{!isLoading && children}
{!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
size={37}
strokeWidth={1.5}
@@ -201,7 +201,7 @@ export function ProgressStack(props: ProgressStackProps) {
emptyLinkHref={'/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.map((progress) => {
@@ -351,7 +351,7 @@ function StatsCard(props: StatsCardProps) {
const { title, value, isLoading = false } = props;
return (
<div className="flex flex-col gap-1 rounded-md border bg-white p-4 shadow-sm">
<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>
{isLoading ? (
<CardSkeleton className="h-8" />

View File

@@ -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">
Team Members
<span className="flex-grow h-[1px] bg-gray-200 mx-3" />
<span className="grow h-[1px] bg-gray-200 mx-3" />
{canManageCurrentTeam && (
<a
href={`/team/members?t=${teamId}`}

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