mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-12 17:51:53 +08:00
Compare commits
70 Commits
feat/xyflo
...
fix/pagina
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97d5934b03 | ||
|
|
8b69b266d5 | ||
|
|
3f0db1526d | ||
|
|
69d9dd23b2 | ||
|
|
3e1bc34d4a | ||
|
|
dea689b068 | ||
|
|
de237ec6fc | ||
|
|
5ec61cc32f | ||
|
|
7bffc1004d | ||
|
|
c06218910d | ||
|
|
130e381054 | ||
|
|
d5d38ee919 | ||
|
|
6b7138b8d8 | ||
|
|
242e40ddd8 | ||
|
|
9ea70fcc97 | ||
|
|
823c31eac4 | ||
|
|
d4a1180c4d | ||
|
|
483c942338 | ||
|
|
f28b018e99 | ||
|
|
c683db2757 | ||
|
|
6dd8f29bff | ||
|
|
671b59c0ac | ||
|
|
1197a0fd6d | ||
|
|
9ebb288f9b | ||
|
|
ca38c0cede | ||
|
|
ff7c981f2f | ||
|
|
3455e6ef1c | ||
|
|
f7f409ca90 | ||
|
|
2538db4786 | ||
|
|
d5a8814add | ||
|
|
0cadde1092 | ||
|
|
3f4bbef211 | ||
|
|
715352eeab | ||
|
|
e5e43de98a | ||
|
|
f085a226ba | ||
|
|
2e90823af4 | ||
|
|
50df3eda0f | ||
|
|
69b0d7abb3 | ||
|
|
c4af3c57f0 | ||
|
|
2cee3a8859 | ||
|
|
7f28a755dc | ||
|
|
a2e83e909e | ||
|
|
e4f53ed90e | ||
|
|
5e836ab7a5 | ||
|
|
9851978dbd | ||
|
|
82c52aca7e | ||
|
|
0d62847053 | ||
|
|
7a00234f9a | ||
|
|
64a65fa2e9 | ||
|
|
09d8c709d4 | ||
|
|
6a14170e64 | ||
|
|
ac3ebb2162 | ||
|
|
56ea91b11c | ||
|
|
5a1f52892e | ||
|
|
74781d6e7b | ||
|
|
06bdfc42d2 | ||
|
|
0a42ea6f41 | ||
|
|
2dc4041228 | ||
|
|
4b7eab66da | ||
|
|
999f6b09a8 | ||
|
|
a9cd557dd3 | ||
|
|
3d3423f8e5 | ||
|
|
a5eb5231cb | ||
|
|
8662416c96 | ||
|
|
7564895d7a | ||
|
|
7b15ed39a3 | ||
|
|
e72622f2b2 | ||
|
|
deb9aaafc2 | ||
|
|
63b6d471a2 | ||
|
|
2485b716dd |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1742812122664
|
||||
}
|
||||
}
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1743851801172
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/workflows/cloudfront-api-cache.yml
vendored
2
.github/workflows/cloudfront-api-cache.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/cloudfront-fe-cache.yml
vendored
2
.github/workflows/cloudfront-fe-cache.yml
vendored
@@ -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
|
||||
|
||||
23
.github/workflows/greetings.yml
vendored
23
.github/workflows/greetings.yml
vendored
@@ -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
7
.gitignore
vendored
@@ -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
|
||||
@@ -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.*$/],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export function ReadonlyEditor(props: any) {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
|
||||
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
|
||||
<p className="mb-4">
|
||||
Renderer is a private component. If you are a collaborator and have
|
||||
access to it. Run the following command:
|
||||
</p>
|
||||
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
|
||||
npm run generate-renderer
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export function Renderer(props: any) {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
|
||||
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
|
||||
<p className="mb-4">
|
||||
Renderer is a private component. If you are a collaborator and have
|
||||
access to it. Run the following command:
|
||||
</p>
|
||||
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
|
||||
npm run generate-renderer
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export function renderFlowJSON(data: any, options?: any) {
|
||||
console.warn("renderFlowJSON is not implemented");
|
||||
console.warn("run the following command to generate the renderer:");
|
||||
console.warn("> npm run generate-renderer");
|
||||
}
|
||||
29
package.json
29
package.json
@@ -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
0
packages/.gitkeep
Normal file
3863
pnpm-lock.yaml
generated
3863
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- packages/*
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 red–black trees with data elements in the same order. The insertion and deletion operations on 2-4 trees are also equivalent to color-flipping and rotations in red–black trees. This makes 2-4 trees an important tool for understanding the logic behind red–black trees, and this is why many introductory algorithm texts introduce 2-4 trees just before red–black trees, even though 2-4 trees are not often used in practice.\n\nVisit the following resources to learn more:",
|
||||
"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",
|
||||
|
||||
@@ -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": []
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export function Renderer(props: any) {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
|
||||
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
|
||||
<p className="mb-4">
|
||||
Renderer is a private component. If you are a collaborator and have
|
||||
access to it. Run the following command:
|
||||
</p>
|
||||
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
|
||||
npm run generate-renderer
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export function renderFlowJSON(data: any, options?: any) {
|
||||
console.warn("renderFlowJSON is not implemented");
|
||||
console.warn("run the following command to generate the renderer:");
|
||||
console.warn("> npm run generate-renderer");
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import 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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
76
scripts/migrate-editor-roadmap.ts
Normal file
76
scripts/migrate-editor-roadmap.ts
Normal 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
58
scripts/rename-content.ts
Normal 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));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
128
src/components/AITutor/AIExploreCourseListing.tsx
Normal file
128
src/components/AITutor/AIExploreCourseListing.tsx
Normal 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';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
131
src/components/AITutor/AIFeaturedCoursesListing.tsx
Normal file
131
src/components/AITutor/AIFeaturedCoursesListing.tsx
Normal 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';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
src/components/AITutor/AILoadingState.tsx
Normal file
27
src/components/AITutor/AILoadingState.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
src/components/AITutor/AITutorHeader.tsx
Normal file
40
src/components/AITutor/AITutorHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
src/components/AITutor/AITutorLayout.tsx
Normal file
42
src/components/AITutor/AITutorLayout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
43
src/components/AITutor/AITutorLimits.tsx
Normal file
43
src/components/AITutor/AITutorLimits.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
143
src/components/AITutor/AITutorSidebar.tsx
Normal file
143
src/components/AITutor/AITutorSidebar.tsx
Normal 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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/components/AITutor/AITutorSidebarProps.tsx
Normal file
13
src/components/AITutor/AITutorSidebarProps.tsx
Normal 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>
|
||||
31
src/components/AITutor/AITutorTallMessage.tsx
Normal file
31
src/components/AITutor/AITutorTallMessage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
69
src/components/AITutor/DifficultyDropdown.tsx
Normal file
69
src/components/AITutor/DifficultyDropdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
src/components/AITutor/LoginToView.tsx
Normal file
38
src/components/AITutor/LoginToView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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`}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('_', ' ')}
|
||||
|
||||
@@ -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]" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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] ||
|
||||
|
||||
6
src/components/Analytics/Hubspot.astro
Normal file
6
src/components/Analytics/Hubspot.astro
Normal 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>
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script is:inline>
|
||||
// @ts-nocheck
|
||||
!(function (w, d) {
|
||||
if (!w.rdt) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag: any;
|
||||
varify: any;
|
||||
fireEvent: (props: {
|
||||
action: string;
|
||||
category: string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({});
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}
|
||||
>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user